Files
nadekobot/src/NadekoBot/Modules/Games/Nunchi/Nunchi.cs

183 lines
5.9 KiB
C#

#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<NunchiGame, Task> OnGameStarted;
public event Func<NunchiGame, int, Task> OnRoundStarted;
public event Func<NunchiGame, Task> OnUserGuessed;
public event Func<NunchiGame, (ulong Id, string Name)?, Task> OnRoundEnded; // tuple of the user who failed
public event Func<NunchiGame, string, Task> 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<bool> 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<bool> 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;
}
}