From f3984c824ec09fb6271dc0d07462d877a3cf0119 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Thu, 3 Aug 2017 00:29:38 +0200 Subject: [PATCH] Nunchi game added. A bit confusing now. Will be polished further tomorrow. --- .../Modules/Games/Common/Nunchi/Nunchi.cs | 170 ++++++++++++++++++ src/NadekoBot/Modules/Games/Games.cs | 1 - src/NadekoBot/Modules/Games/NunchiCommands.cs | 121 +++++++++++++ src/NadekoBot/Resources/CommandStrings.resx | 9 + .../_strings/ResponseStrings.en-US.json | 10 ++ 5 files changed, 310 insertions(+), 1 deletion(-) create mode 100644 src/NadekoBot/Modules/Games/Common/Nunchi/Nunchi.cs create mode 100644 src/NadekoBot/Modules/Games/NunchiCommands.cs diff --git a/src/NadekoBot/Modules/Games/Common/Nunchi/Nunchi.cs b/src/NadekoBot/Modules/Games/Common/Nunchi/Nunchi.cs new file mode 100644 index 00000000..0cf517db --- /dev/null +++ b/src/NadekoBot/Modules/Games/Common/Nunchi/Nunchi.cs @@ -0,0 +1,170 @@ +using NadekoBot.Common; +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.Nunchi +{ + public class Nunchi : IDisposable + { + public enum Phase + { + Joining, + Playing, + Ended, + } + + public int CurrentNumber { get; private set; } = new NadekoRandom().Next(0, 100); + public Phase CurrentPhase { get; private set; } = Phase.Joining; + + public event Func OnGameStarted; + public event Func OnRoundStarted; + public event Func OnUserGuessed; + public event Func OnRoundEnded; // tuple of the user who failed + public event Func OnGameEnded; // name of the user who won + + private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1); + + private HashSet<(ulong Id, string Name)> _participants = new HashSet<(ulong Id, string Name)>(); + private HashSet<(ulong Id, string Name)> _passed = new HashSet<(ulong Id, string Name)>(); + + public ImmutableArray<(ulong Id, string Name)> Participants => _participants.ToImmutableArray(); + public int ParticipantCount => _participants.Count; + + private const int _killTimeout = 20 * 1000; + private Timer _killTimer; + + public Nunchi(ulong creatorId, string creatorName) + { + _participants.Add((creatorId, creatorName)); + } + + public async Task Join(ulong userId, string userName) + { + await _locker.WaitAsync().ConfigureAwait(false); + try + { + if (CurrentPhase != Phase.Joining) + return false; + + return _participants.Add((userId, userName)); + } + finally { _locker.Release(); } + } + + public async Task Initialize() + { + CurrentPhase = Phase.Joining; + await Task.Delay(30000).ConfigureAwait(false); + await _locker.WaitAsync().ConfigureAwait(false); + try + { + if (_participants.Count < 2) + { + CurrentPhase = Phase.Ended; + return false; + } + + _killTimer = new Timer(async state => + { + await _locker.WaitAsync().ConfigureAwait(false); + try + { + if (CurrentPhase != Phase.Playing) + return; + + //if some players took too long to type a number, boot them all out and start a new round + _participants = new HashSet<(ulong, string)>(_passed); + EndRound(); + } + finally { _locker.Release(); } + }, null, _killTimeout, _killTimeout); + + CurrentPhase = Phase.Playing; + var _ = OnGameStarted?.Invoke(this); + var __ = OnRoundStarted?.Invoke(this); + return true; + } + finally { _locker.Release(); } + } + + public async Task Input(ulong userId, string userName, int input) + { + await _locker.WaitAsync().ConfigureAwait(false); + try + { + if (CurrentPhase != Phase.Playing) + return; + + var userTuple = (Id: userId, Name: userName); + + // if the user is not a member of the race, + // or he already successfully typed the number + // ignore the input + if (!_participants.Contains(userTuple) || !_passed.Add(userTuple)) + return; + + //if the number is correct + if (CurrentNumber == input - 1) + { + //increment current number + ++CurrentNumber; + if (_passed.Count == _participants.Count - 1) + { + // if only n players are left, and n - 1 type the correct number, round is over + + // if only 2 players are left, game is over + if (_participants.Count == 2) + { + CurrentPhase = Phase.Ended; + var _ = OnGameEnded?.Invoke(this, userTuple.Name); + } + else // else just start the new round without the user who was the last + { + var failure = _participants.Except(_passed).First(); + EndRound(failure); + } + } + var __ = OnUserGuessed?.Invoke(this); + } + else + { + //if the user failed + + EndRound(userTuple); + } + } + finally { _locker.Release(); } + } + + private void EndRound((ulong, string)? failure = null) + { + _killTimer.Change(_killTimeout, _killTimeout); + CurrentNumber = new NadekoRandom().Next(0, 100); // reset the counter + _passed.Clear(); // reset all users who passed (new round starts) + if(failure != null) + _participants.Remove(failure.Value); // remove the dude who failed from the list of players + + var __ = OnRoundEnded?.Invoke(this, failure); + if (_participants.Count <= 1) // means we have a winner or everyone was booted out + { + CurrentPhase = Phase.Ended; + var _ = OnGameEnded?.Invoke(this, _participants.Count > 0 ? _participants.First().Name : null); + return; + } + var ___ = OnRoundStarted?.Invoke(this); + } + + public void Dispose() + { + OnGameEnded = null; + OnGameStarted = null; + OnRoundEnded = null; + OnRoundStarted = null; + OnUserGuessed = null; + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Games.cs b/src/NadekoBot/Modules/Games/Games.cs index c5cc0b13..1b8898c9 100644 --- a/src/NadekoBot/Modules/Games/Games.cs +++ b/src/NadekoBot/Modules/Games/Games.cs @@ -16,7 +16,6 @@ namespace NadekoBot.Modules.Games - Shiritori - Simple RPG adventure - The nunchi game - - Wheel of fortune - Connect 4 */ public partial class Games : NadekoTopLevelModule diff --git a/src/NadekoBot/Modules/Games/NunchiCommands.cs b/src/NadekoBot/Modules/Games/NunchiCommands.cs new file mode 100644 index 00000000..9f2aa58a --- /dev/null +++ b/src/NadekoBot/Modules/Games/NunchiCommands.cs @@ -0,0 +1,121 @@ +using Discord; +using Discord.Commands; +using Discord.WebSocket; +using NadekoBot.Common.Attributes; +using NadekoBot.Modules.Games.Common.Nunchi; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Games +{ + public partial class Games + { + public class NunchiCommands : NadekoSubmodule + { + public static readonly ConcurrentDictionary Games = new ConcurrentDictionary(); + private readonly DiscordSocketClient _client; + + public NunchiCommands(DiscordSocketClient client) + { + _client = client; + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Nunchi() + { + var newNunchi = new Nunchi(Context.User.Id, Context.User.ToString()); + Nunchi nunchi; + + //if a game was already active + if ((nunchi = Games.GetOrAdd(Context.Guild.Id, newNunchi)) != newNunchi) + { + // join it + if (!await nunchi.Join(Context.User.Id, Context.User.ToString())) + { + // if you failed joining, that means game is running or just ended + // await ReplyErrorLocalized("nunchi_already_started").ConfigureAwait(false); + return; + } + + await ReplyConfirmLocalized("nunchi_joined", nunchi.ParticipantCount).ConfigureAwait(false); + return; + } + + + try { await ReplyConfirmLocalized("nunchi_created").ConfigureAwait(false); } catch { } + + nunchi.OnGameEnded += Nunchi_OnGameEnded; + //nunchi.OnGameStarted += Nunchi_OnGameStarted; + nunchi.OnRoundEnded += Nunchi_OnRoundEnded; + nunchi.OnUserGuessed += Nunchi_OnUserGuessed; + nunchi.OnRoundStarted += Nunchi_OnRoundStarted; + _client.MessageReceived += _client_MessageReceived; + + var success = await nunchi.Initialize().ConfigureAwait(false); + if (!success) + { + if (Games.TryRemove(Context.Guild.Id, out var game)) + game.Dispose(); + await ReplyErrorLocalized("nunchi_failed_to_start").ConfigureAwait(false); + } + + async Task _client_MessageReceived(SocketMessage arg) + { + if (arg.Channel.Id != Context.Channel.Id) + return; + + if (!int.TryParse(arg.Content, out var number)) + return; + try + { + await nunchi.Input(arg.Author.Id, arg.Author.ToString(), number).ConfigureAwait(false); + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + } + } + + private Task Nunchi_OnRoundStarted(Nunchi arg) + { + return ReplyConfirmLocalized("nunchi_round_started", Format.Bold(arg.CurrentNumber.ToString())); + } + + private Task Nunchi_OnUserGuessed(Nunchi arg) + { + return ReplyConfirmLocalized("nunchi_next_number", Format.Bold(arg.CurrentNumber.ToString())); + } + + private Task Nunchi_OnRoundEnded(Nunchi arg1, (ulong Id, string Name)? arg2) + { + if(arg2.HasValue) + return ReplyConfirmLocalized("nunchi_round_ended", Format.Bold(arg2.Value.Name)); + else + return ReplyConfirmLocalized("nunchi_round_ended_boot", + Format.Bold("\n" + string.Join("\n, ", arg1.Participants.Select(x => x.Name)))); // this won't work if there are too many users + } + + private Task Nunchi_OnGameStarted(Nunchi arg) + { + return ReplyConfirmLocalized("nunchi_started", Format.Bold(arg.ParticipantCount.ToString())); + } + + private Task Nunchi_OnGameEnded(Nunchi arg1, string arg2) + { + if (Games.TryRemove(Context.Guild.Id, out var game)) + game.Dispose(); + + if(arg2 == null) + return ReplyConfirmLocalized("nunchi_ended_no_winner", Format.Bold(arg2)); + else + return ReplyConfirmLocalized("nunchi_ended", Format.Bold(arg2)); + } + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index 6900c091..51fe44fb 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -1269,6 +1269,15 @@ `{0}jr` or `{0}jr 5` + + nunchi + + + Creates or joins a nunchi game. Minimum 3 users required. + + + `{0}nunchi` + raffle diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index 5e1b3ead..c96f9a68 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -363,6 +363,16 @@ "games_leaderboard": "Leaderboard", "games_not_enough": "You don't have enough {0}", "games_no_results": "No results", + "games_nunchi_joined": "Joined nunchi game. {0} users joined so far.", + "games_nunchi_ended": "Nunchi game ended. {0} won", + "games_nunchi_ended_no_winner": "Nunchi game ended with no winner.", + "games_nunchi_started": "Nunchi game started with {0} participants.", + "games_nunchi_round_ended": "Nunchi round ended. {0} is out of the game.", + "games_nunchi_round_ended_boot": "Nunchi round ended due to timeout of some users. These users are still in the game: {0}", + "games_nunchi_round_started": "Nunchi round started. Start counting from the number {0}.", + "games_nunchi_next_number": "Number registered. Last number was {0}.", + "games_nunchi_failed_to_start": "Nunchi failed to start because there were not enough participants.", + "games_nunchi_created": "Nunchi game created. Waiting for users to join.", "games_picked": "picked {0}", "games_planted": "{0} planted {1}", "games_trivia_already_running": "Trivia game is already running on this server.",