using NadekoBot.Common; 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.Acrophobia { /// /// Platform-agnostic acrophobia game /// public class Acrophobia : IDisposable { private const int VotingPhaseLength = 30; public enum Phase { Submission, Voting, Ended } public enum UserInputResult { Submitted, SubmissionFailed, Voted, VotingFailed, Failed } public int SubmissionPhaseLength { get; } public Phase CurrentPhase { get; private set; } = Phase.Submission; public ImmutableArray StartingLetters { get; private set; } private readonly Dictionary submissions = new Dictionary(); private readonly SemaphoreSlim locker = new SemaphoreSlim(1, 1); private readonly NadekoRandom _rng; public event Func OnStarted = delegate { return Task.CompletedTask; }; public event Func>, Task> OnVotingStarted = delegate { return Task.CompletedTask; }; public event Func OnUserVoted = delegate { return Task.CompletedTask; }; public event Func>, Task> OnEnded = delegate { return Task.CompletedTask; }; private readonly HashSet _usersWhoVoted = new HashSet(); public Acrophobia(int submissionPhaseLength = 30) { _rng = new NadekoRandom(); SubmissionPhaseLength = submissionPhaseLength; InitializeStartingLetters(); } public async Task Run() { await OnStarted(this).ConfigureAwait(false); await Task.Delay(SubmissionPhaseLength * 1000); await locker.WaitAsync().ConfigureAwait(false); try { if (submissions.Count == 0) { CurrentPhase = Phase.Ended; await OnVotingStarted(this, ImmutableArray.Create>()).ConfigureAwait(false); return; } if (submissions.Count == 1) { CurrentPhase = Phase.Ended; await OnVotingStarted(this, submissions.ToArray().ToImmutableArray()).ConfigureAwait(false); return; } CurrentPhase = Phase.Voting; await OnVotingStarted(this, submissions.ToArray().ToImmutableArray()).ConfigureAwait(false); } finally { locker.Release(); } await Task.Delay(VotingPhaseLength * 1000); await locker.WaitAsync().ConfigureAwait(false); try { CurrentPhase = Phase.Ended; await OnEnded(this, submissions.ToArray().ToImmutableArray()).ConfigureAwait(false) ; } finally { locker.Release(); } } private void InitializeStartingLetters() { var wordCount = _rng.Next(3, 6); var lettersArr = new char[wordCount]; for (int i = 0; i < wordCount; i++) { var randChar = (char)_rng.Next(65, 91); lettersArr[i] = randChar == 'X' ? (char)_rng.Next(65, 88) : randChar; } StartingLetters = lettersArr.ToImmutableArray(); } public async Task UserInput(ulong userId, string userName, string input) { var user = new AcrophobiaUser(userId, userName, input.ToLowerInvariant().ToTitleCase()); await locker.WaitAsync(); try { switch (CurrentPhase) { case Phase.Submission: if (submissions.ContainsKey(user) || !IsValidAnswer(input)) break; submissions.Add(user, 0); return true; case Phase.Voting: AcrophobiaUser toVoteFor; if (!int.TryParse(input, out var index) || --index < 0 || index >= submissions.Count || (toVoteFor = submissions.ToArray()[index].Key).UserId == user.UserId || !_usersWhoVoted.Add(userId)) break; ++submissions[toVoteFor]; var _ = Task.Run(() => OnUserVoted(userName)); return true; default: break; } return false; } finally { locker.Release(); } } private bool IsValidAnswer(string input) { input = input.ToUpperInvariant(); var inputWords = input.Split(' '); if (inputWords.Length != StartingLetters.Length) // number of words must be the same as the number of the starting letters return false; for (int i = 0; i < StartingLetters.Length; i++) { var letter = StartingLetters[i]; if (!inputWords[i].StartsWith(letter.ToString())) // all first letters must match return false; } return true; } public void Dispose() { this.CurrentPhase = Phase.Ended; OnStarted = null; OnEnded = null; OnUserVoted = null; OnVotingStarted = null; _usersWhoVoted.Clear(); submissions.Clear(); locker.Dispose(); } } }