#nullable disable using System.Collections.Immutable; namespace NadekoBot.Modules.Games.Common.Nunchi; public sealed class NunchiGame : IDisposable { public enum Phase { Joining, Playing, WaitingForNextRound, Ended } private const int KILL_TIMEOUT = 20 * 1000; private const int NEXT_ROUND_TIMEOUT = 5 * 1000; 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 public int CurrentNumber { get; private set; } = new NadekoRandom().Next(0, 100); public Phase CurrentPhase { get; private set; } = Phase.Joining; public ImmutableArray<(ulong Id, string Name)> Participants => participants.ToImmutableArray(); public int ParticipantCount => participants.Count; private readonly SemaphoreSlim _locker = new(1, 1); private HashSet<(ulong Id, string Name)> participants = new(); private readonly HashSet<(ulong Id, string Name)> _passed = new(); private Timer killTimer; public NunchiGame(ulong creatorId, string creatorName) => participants.Add((creatorId, creatorName)); public async Task Join(ulong userId, string userName) { await _locker.WaitAsync(); 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); await _locker.WaitAsync(); try { if (participants.Count < 3) { CurrentPhase = Phase.Ended; return false; } killTimer = new(async _ => { await _locker.WaitAsync(); 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, KILL_TIMEOUT, KILL_TIMEOUT); CurrentPhase = Phase.Playing; _= OnGameStarted?.Invoke(this); var __ = OnRoundStarted?.Invoke(this, CurrentNumber); return true; } finally { _locker.Release(); } } public async Task Input(ulong userId, string userName, int input) { await _locker.WaitAsync(); 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) { killTimer.Change(Timeout.Infinite, Timeout.Infinite); CurrentPhase = Phase.Ended; _= 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(); OnUserGuessed?.Invoke(this); EndRound(failure); return; } } OnUserGuessed?.Invoke(this); } else { //if the user failed EndRound(userTuple); } } finally { _locker.Release(); } } private void EndRound((ulong, string)? failure = null) { killTimer.Change(KILL_TIMEOUT, KILL_TIMEOUT); CurrentNumber = new NadekoRandom().Next(0, 100); // reset the counter _passed.Clear(); // reset all users who passed (new round starts) if (failure is not 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 { killTimer.Change(Timeout.Infinite, Timeout.Infinite); CurrentPhase = Phase.Ended; _= OnGameEnded?.Invoke(this, participants.Count > 0 ? participants.First().Name : null); return; } CurrentPhase = Phase.WaitingForNextRound; var throwawayDelay = Task.Run(async () => { await Task.Delay(NEXT_ROUND_TIMEOUT); CurrentPhase = Phase.Playing; var ___ = OnRoundStarted?.Invoke(this, CurrentNumber); }); } public void Dispose() { OnGameEnded = null; OnGameStarted = null; OnRoundEnded = null; OnRoundStarted = null; OnUserGuessed = null; } }