.hangman completely rewritten. Should work almost the same, with some minor improvements (such as showing the category, and you can now guess the whole word at once)

This commit is contained in:
Master Kwoth 2017-07-28 12:28:08 +02:00
parent 53661b3337
commit d5978a0d66
9 changed files with 339 additions and 236 deletions

View File

@ -2,18 +2,12 @@
using Discord.Commands; using Discord.Commands;
using Discord.WebSocket; using Discord.WebSocket;
using NadekoBot.Extensions; using NadekoBot.Extensions;
using NLog;
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using NadekoBot.Common;
using NadekoBot.Common.Attributes; using NadekoBot.Common.Attributes;
using NadekoBot.Common.Collections;
using NadekoBot.Services.Impl;
using NadekoBot.Modules.Games.Common.Acrophobia; using NadekoBot.Modules.Games.Common.Acrophobia;
namespace NadekoBot.Modules.Games namespace NadekoBot.Modules.Games

View File

@ -0,0 +1,11 @@
using System;
namespace NadekoBot.Modules.Games.Common.Hangman.Exceptions
{
public class TermNotFoundException : Exception
{
public TermNotFoundException() : base("Term of that type couldn't be found")
{
}
}
}

View File

@ -0,0 +1,169 @@
using NadekoBot.Extensions;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Games.Common.Hangman
{
public class Hangman : IDisposable
{
public string TermType { get; }
public HangmanObject Term { get; }
public string ScrambledWord => "`" + String.Concat(Term.Word.Select(c =>
{
if (c == ' ')
return " \u2000";
if (!(char.IsLetter(c) || char.IsDigit(c)))
return $" {c}";
c = char.ToLowerInvariant(c);
return _previousGuesses.Contains(c) ? $" {c}" : " ◯";
})) + "`";
private Phase _currentPhase = Phase.Active;
public Phase CurrentPhase
{
get => _currentPhase;
set
{
if (value == Phase.Ended)
_endingCompletionSource.TrySetResult(true);
_currentPhase = value;
}
}
private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1);
private readonly HashSet<ulong> _recentUsers = new HashSet<ulong>();
public uint Errors { get; private set; } = 0;
public uint MaxErrors { get; } = 6;
public event Func<Hangman, string, Task> OnGameEnded = delegate { return Task.CompletedTask; };
public event Func<Hangman, string, char, Task> OnLetterAlreadyUsed = delegate { return Task.CompletedTask; };
public event Func<Hangman, string, char, Task> OnGuessFailed = delegate { return Task.CompletedTask; };
public event Func<Hangman, string, char, Task> OnGuessSucceeded = delegate { return Task.CompletedTask; };
private readonly HashSet<char> _previousGuesses = new HashSet<char>();
public ImmutableArray<char> PreviousGuesses => _previousGuesses.ToImmutableArray();
private readonly TaskCompletionSource<bool> _endingCompletionSource = new TaskCompletionSource<bool>();
public Task EndedTask => _endingCompletionSource.Task;
public Hangman(TermType type)
{
this.TermType = type.ToString().Replace('_', ' ').ToTitleCase();
this.Term = TermPool.GetTerm(type);
}
private void AddError()
{
Errors++;
if (Errors > MaxErrors)
{
CurrentPhase = Phase.Ended;
var _ = OnGameEnded(this, null);
}
}
public string GetHangman() => $@". ┌─────┐
................
................
.{(Errors > 0 ? ".............😲" : "")}
.{(Errors > 1 ? "............./" : "")} {(Errors > 2 ? "|" : "")} {(Errors > 3 ? "\\" : "")}
.{(Errors > 4 ? "............../" : "")} {(Errors > 5 ? "\\" : "")}
/-\";
public async Task Input(ulong userId, string userName, string input)
{
if (CurrentPhase == Phase.Ended)
return;
if (string.IsNullOrWhiteSpace(input))
return;
input = input.Trim().ToLowerInvariant();
await _locker.WaitAsync().ConfigureAwait(false);
try
{
if (CurrentPhase == Phase.Ended)
return;
if (input.Length > 1) // tried to guess the whole word
{
if (input != Term.Word) // failed
return;
CurrentPhase = Phase.Ended;
var _ = OnGameEnded?.Invoke(this, userName);
return;
}
var ch = input[0];
if (!(char.IsLetterOrDigit(ch)))
return;
if (!_recentUsers.Add(userId)) // don't let a single user spam guesses
return;
if (!_previousGuesses.Add(ch)) // that latter was already guessed
{
var _ = OnLetterAlreadyUsed?.Invoke(this, userName, ch);
AddError();
}
else if (!Term.Word.Contains(ch)) // guessed letter doesn't exist
{
var _ = OnGuessFailed?.Invoke(this, userName, ch);
AddError();
}
else if (Term.Word.All(x => _previousGuesses.IsSupersetOf(Term.Word.ToLowerInvariant()
.Where(c => char.IsLetterOrDigit(c)))))
{
var _ = OnGameEnded.Invoke(this, userName); //if all letters are guessed
}
else //guessed but not last letter
{
var _ = OnGuessSucceeded?.Invoke(this, userName, ch);
_recentUsers.Remove(userId); // he can guess again right away
return;
}
var clearSpam = Task.Run(async () =>
{
await Task.Delay(3000).ConfigureAwait(false); // remove the user from the spamlist after 5 seconds
_recentUsers.Remove(userId);
});
}
finally { _locker.Release(); }
}
public async Task Stop()
{
await _locker.WaitAsync().ConfigureAwait(false);
try
{
CurrentPhase = Phase.Ended;
}
finally { _locker.Release(); }
}
public void Dispose()
{
OnGameEnded = null;
OnGuessFailed = null;
OnGuessSucceeded = null;
OnLetterAlreadyUsed = null;
_previousGuesses.Clear();
_recentUsers.Clear();
_locker.Dispose();
}
}
}

View File

@ -1,214 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;
using NadekoBot.Common;
using NadekoBot.Extensions;
using Newtonsoft.Json;
using NLog;
namespace NadekoBot.Modules.Games.Common.Hangman
{
public class HangmanTermPool
{
const string termsPath = "data/hangman3.json";
public static IReadOnlyDictionary<string, HangmanObject[]> data { get; }
static HangmanTermPool()
{
try
{
data = JsonConvert.DeserializeObject<Dictionary<string, HangmanObject[]>>(File.ReadAllText(termsPath));
}
catch (Exception)
{
//ignored
}
}
public static HangmanObject GetTerm(string type)
{
if (string.IsNullOrWhiteSpace(type))
throw new ArgumentNullException(nameof(type));
type = type.Trim();
var rng = new NadekoRandom();
if (type == "All") {
var keys = data.Keys.ToArray();
type = keys[rng.Next(0, keys.Length)];
}
HangmanObject[] termTypes;
data.TryGetValue(type, out termTypes);
if (termTypes == null || termTypes.Length == 0)
return null;
return termTypes[rng.Next(0, termTypes.Length)];
}
}
public class HangmanGame: IDisposable
{
private readonly Logger _log;
private readonly DiscordSocketClient _client;
public IMessageChannel GameChannel { get; }
public HashSet<char> Guesses { get; } = new HashSet<char>();
public HangmanObject Term { get; private set; }
public uint Errors { get; private set; } = 0;
public uint MaxErrors { get; } = 6;
public uint MessagesSinceLastPost { get; private set; } = 0;
public string ScrambledWord => "`" + String.Concat(Term.Word.Select(c =>
{
if (c == ' ')
return " \u2000";
if (!(char.IsLetter(c) || char.IsDigit(c)))
return $" {c}";
c = char.ToUpperInvariant(c);
return Guesses.Contains(c) ? $" {c}" : " ◯";
})) + "`";
public bool GuessedAll => Guesses.IsSupersetOf(Term.Word.ToUpperInvariant()
.Where(c => char.IsLetter(c) || char.IsDigit(c)));
public string TermType { get; }
public event Action<HangmanGame> OnEnded;
public HangmanGame(DiscordSocketClient client, IMessageChannel channel, string type)
{
_log = LogManager.GetCurrentClassLogger();
_client = client;
this.GameChannel = channel;
this.TermType = type.ToTitleCase();
}
public void Start()
{
this.Term = HangmanTermPool.GetTerm(TermType);
if (this.Term == null)
throw new KeyNotFoundException("Can't find a term with that type. Use hangmanlist command.");
// start listening for answers when game starts
_client.MessageReceived += PotentialGuess;
}
public async Task End()
{
_client.MessageReceived -= PotentialGuess;
OnEnded(this);
var toSend = "Game ended. You **" + (Errors >= MaxErrors ? "LOSE" : "WIN") + "**!\n" + GetHangman();
var embed = new EmbedBuilder().WithTitle("Hangman Game")
.WithDescription(toSend)
.AddField(efb => efb.WithName("It was").WithValue(Term.Word))
.WithFooter(efb => efb.WithText(string.Join(" ", Guesses)));
if(Uri.IsWellFormedUriString(Term.ImageUrl, UriKind.Absolute))
embed.WithImageUrl(Term.ImageUrl);
if (Errors >= MaxErrors)
await GameChannel.EmbedAsync(embed.WithErrorColor()).ConfigureAwait(false);
else
await GameChannel.EmbedAsync(embed.WithOkColor()).ConfigureAwait(false);
}
private Task PotentialGuess(SocketMessage msg)
{
var _ = Task.Run(async () =>
{
try
{
if (!(msg is SocketUserMessage))
return;
if (msg.Channel != GameChannel)
return; // message's channel has to be the same as game's
if (msg.Content.Length == 1) // message must be 1 char long
{
if (++MessagesSinceLastPost > 10)
{
MessagesSinceLastPost = 0;
try
{
await GameChannel.SendConfirmAsync("Hangman Game",
ScrambledWord + "\n" + GetHangman(),
footer: string.Join(" ", Guesses)).ConfigureAwait(false);
}
catch { }
}
if (!(char.IsLetter(msg.Content[0]) || char.IsDigit(msg.Content[0])))// and a letter or a digit
return;
var guess = char.ToUpperInvariant(msg.Content[0]);
if (Guesses.Contains(guess))
{
MessagesSinceLastPost = 0;
++Errors;
if (Errors < MaxErrors)
await GameChannel.SendErrorAsync("Hangman Game", $"{msg.Author} Letter `{guess}` has already been used.\n" + ScrambledWord + "\n" + GetHangman(),
footer: string.Join(" ", Guesses)).ConfigureAwait(false);
else
await End().ConfigureAwait(false);
return;
}
Guesses.Add(guess);
if (Term.Word.ToUpperInvariant().Contains(guess))
{
if (GuessedAll)
{
try { await GameChannel.SendConfirmAsync("Hangman Game", $"{msg.Author} guessed a letter `{guess}`!").ConfigureAwait(false); } catch { }
await End().ConfigureAwait(false);
return;
}
MessagesSinceLastPost = 0;
try
{
await GameChannel.SendConfirmAsync("Hangman Game", $"{msg.Author} guessed a letter `{guess}`!\n" + ScrambledWord + "\n" + GetHangman(),
footer: string.Join(" ", Guesses)).ConfigureAwait(false);
}
catch { }
}
else
{
MessagesSinceLastPost = 0;
++Errors;
if (Errors < MaxErrors)
await GameChannel.SendErrorAsync("Hangman Game", $"{msg.Author} Letter `{guess}` does not exist.\n" + ScrambledWord + "\n" + GetHangman(),
footer: string.Join(" ", Guesses)).ConfigureAwait(false);
else
await End().ConfigureAwait(false);
}
}
}
catch (Exception ex) { _log.Warn(ex); }
});
return Task.CompletedTask;
}
public string GetHangman() => $@". ┌─────┐
................
................
.{(Errors > 0 ? ".............😲" : "")}
.{(Errors > 1 ? "............./" : "")} {(Errors > 2 ? "|" : "")} {(Errors > 3 ? "\\" : "")}
.{(Errors > 4 ? "............../" : "")} {(Errors > 5 ? "\\" : "")}
/-\";
public void Dispose()
{
_client.MessageReceived -= PotentialGuess;
OnEnded = null;
}
}
}

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Games.Common.Hangman
{
public enum Phase
{
Active,
Ended,
}
}

View File

@ -0,0 +1,52 @@
using NadekoBot.Common;
using NadekoBot.Extensions;
using NadekoBot.Modules.Games.Common.Hangman.Exceptions;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
namespace NadekoBot.Modules.Games.Common.Hangman
{
public class TermPool
{
const string termsPath = "data/hangman3.json";
public static IReadOnlyDictionary<string, HangmanObject[]> data { get; } = new Dictionary<string, HangmanObject[]>();
static TermPool()
{
try
{
data = JsonConvert.DeserializeObject<Dictionary<string, HangmanObject[]>>(File.ReadAllText(termsPath));
}
catch (Exception)
{
//ignored
}
}
private static readonly ImmutableArray<TermType> _termTypes = Enum.GetValues(typeof(TermType))
.Cast<TermType>()
.ToImmutableArray();
public static HangmanObject GetTerm(TermType type)
{
var rng = new NadekoRandom();
if (type == TermType.Random)
{
var keys = data.Keys.ToArray();
type = _termTypes[rng.Next(0, _termTypes.Length - 1)]; // - 1 because last one is 'all'
}
if (!data.TryGetValue(type.ToString(), out var termTypes) || termTypes.Length == 0)
throw new TermNotFoundException();
var obj = termTypes[rng.Next(0, termTypes.Length)];
obj.Word = obj.Word.Trim().ToLowerInvariant();
return obj;
}
}
}

View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Games.Common.Hangman
{
[Flags]
public enum TermType
{
Countries = 0,
Movies = 1,
Animals = 2,
Things = 4,
Random = 8,
}
}

View File

@ -23,43 +23,102 @@ namespace NadekoBot.Modules.Games
} }
//channelId, game //channelId, game
public static ConcurrentDictionary<ulong, HangmanGame> HangmanGames { get; } = new ConcurrentDictionary<ulong, HangmanGame>(); public static ConcurrentDictionary<ulong, Hangman> HangmanGames { get; } = new ConcurrentDictionary<ulong, Hangman>();
[NadekoCommand, Usage, Description, Aliases] [NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
public async Task Hangmanlist() public async Task Hangmanlist()
{ {
await Context.Channel.SendConfirmAsync(Format.Code(GetText("hangman_types", Prefix)) + "\n" + string.Join(", ", HangmanTermPool.data.Keys)); await Context.Channel.SendConfirmAsync(Format.Code(GetText("hangman_types", Prefix)) + "\n" + string.Join(", ", TermPool.data.Keys));
} }
[NadekoCommand, Usage, Description, Aliases] [NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
public async Task Hangman([Remainder]string type = "All") public async Task Hangman([Remainder]TermType type = TermType.Random)
{ {
var hm = new HangmanGame(_client, Context.Channel, type); var hm = new Hangman(type);
if (!HangmanGames.TryAdd(Context.Channel.Id, hm)) if (!HangmanGames.TryAdd(Context.Channel.Id, hm))
{ {
hm.Dispose();
await ReplyErrorLocalized("hangman_running").ConfigureAwait(false); await ReplyErrorLocalized("hangman_running").ConfigureAwait(false);
return; return;
} }
hm.OnGameEnded += Hm_OnGameEnded;
hm.OnGuessFailed += Hm_OnGuessFailed;
hm.OnGuessSucceeded += Hm_OnGuessSucceeded;
hm.OnLetterAlreadyUsed += Hm_OnLetterAlreadyUsed;
_client.MessageReceived += _client_MessageReceived;
hm.OnEnded += g =>
{
HangmanGames.TryRemove(g.GameChannel.Id, out _);
};
try try
{ {
hm.Start(); await Context.Channel.SendConfirmAsync(GetText("hangman_game_started") + $" ({hm.TermType})",
hm.ScrambledWord + "\n" + hm.GetHangman())
.ConfigureAwait(false);
} }
catch (Exception ex) catch { }
await hm.EndedTask.ConfigureAwait(false);
Task _client_MessageReceived(SocketMessage msg)
{ {
try { await Context.Channel.SendErrorAsync(GetText("hangman_start_errored") + " " + ex.Message).ConfigureAwait(false); } catch { } var _ = Task.Run(() =>
if(HangmanGames.TryRemove(Context.Channel.Id, out var removed)) {
removed.Dispose(); if (Context.Channel.Id == msg.Channel.Id)
return; return hm.Input(msg.Author.Id, msg.Author.ToString(), msg.Content);
else
return Task.CompletedTask;
});
return Task.CompletedTask;
}
} }
await Context.Channel.SendConfirmAsync(GetText("hangman_game_started"), hm.ScrambledWord + "\n" + hm.GetHangman()); Task Hm_OnGameEnded(Hangman game, string winner)
{
HangmanGames.TryRemove(Context.Channel.Id, out _);
if (winner == null)
{
var loseEmbed = new EmbedBuilder().WithTitle($"Hangman Game ({game.TermType}) - Ended")
.WithDescription(Format.Bold("You lose."))
.AddField(efb => efb.WithName("It was").WithValue(game.Term.Word.ToTitleCase()))
.WithFooter(efb => efb.WithText(string.Join(" ", game.PreviousGuesses)))
.WithErrorColor();
if (Uri.IsWellFormedUriString(game.Term.ImageUrl, UriKind.Absolute))
loseEmbed.WithImageUrl(game.Term.ImageUrl);
return Context.Channel.EmbedAsync(loseEmbed);
}
var winEmbed = new EmbedBuilder().WithTitle($"Hangman Game ({game.TermType}) - Ended")
.WithDescription(Format.Bold($"{winner} Won."))
.AddField(efb => efb.WithName("It was").WithValue(game.Term.Word.ToTitleCase()))
.WithFooter(efb => efb.WithText(string.Join(" ", game.PreviousGuesses)))
.WithOkColor();
if (Uri.IsWellFormedUriString(game.Term.ImageUrl, UriKind.Absolute))
winEmbed.WithImageUrl(game.Term.ImageUrl);
return Context.Channel.EmbedAsync(winEmbed);
}
private Task Hm_OnLetterAlreadyUsed(Hangman game, string user, char guess)
{
return Context.Channel.SendErrorAsync($"Hangman Game ({game.TermType})", $"{user} Letter `{guess}` has already been used. You can guess again in 3 seconds.\n" + game.ScrambledWord + "\n" + game.GetHangman(),
footer: string.Join(" ", game.PreviousGuesses));
}
private Task Hm_OnGuessSucceeded(Hangman game, string user, char guess)
{
return Context.Channel.SendConfirmAsync($"Hangman Game ({game.TermType})", $"{user} guessed a letter `{guess}`!\n" + game.ScrambledWord + "\n" + game.GetHangman());
}
private Task Hm_OnGuessFailed(Hangman game, string user, char guess)
{
return Context.Channel.SendErrorAsync($"Hangman Game ({game.TermType})", $"{user} Letter `{guess}` does not exist. You can guess again in 3 seconds.\n" + game.ScrambledWord + "\n" + game.GetHangman(),
footer: string.Join(" ", game.PreviousGuesses));
} }
[NadekoCommand, Usage, Description, Aliases] [NadekoCommand, Usage, Description, Aliases]