diff --git a/src/NadekoBot/Modules/Games/Commands/Acropobia.cs b/src/NadekoBot/Modules/Games/Commands/Acropobia.cs new file mode 100644 index 00000000..728845b4 --- /dev/null +++ b/src/NadekoBot/Modules/Games/Commands/Acropobia.cs @@ -0,0 +1,266 @@ +using Discord; +using Discord.Commands; +using NadekoBot.Attributes; +using NadekoBot.Extensions; +using NadekoBot.Services; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Games +{ + public partial class Games + { + [Group] + public class Acropobia + { + //channelId, game + public static ConcurrentDictionary AcrophobiaGames { get; } = new ConcurrentDictionary(); + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Acro(IUserMessage imsg, int time = 45) + { + var channel = (ITextChannel)imsg.Channel; + + var game = new AcrophobiaGame(channel, time); + if (AcrophobiaGames.TryAdd(channel.Id, game)) + { + try + { + await game.Run(); + } + finally + { + game.EnsureStopped(); + AcrophobiaGames.TryRemove(channel.Id, out game); + } + } + else + { + await channel.SendErrorAsync("Acrophobia game is already running in this channel.").ConfigureAwait(false); + } + } + } + + public enum AcroPhase + { + Submitting, + Idle, // used to wait for some other actions while transitioning through phases + Voting + } + + public class AcrophobiaGame + { + private readonly ITextChannel channel; + private readonly int time; + private readonly NadekoRandom rng; + private readonly ImmutableArray startingLetters; + private readonly CancellationTokenSource source; + private AcroPhase phase { get; set; } = AcroPhase.Submitting; + + private readonly ConcurrentDictionary submissions = new ConcurrentDictionary(); + public IReadOnlyDictionary Submissions => submissions; + + private int spamCount = 0; + + //text, votes + private readonly ConcurrentDictionary votes = new ConcurrentDictionary(); + + public AcrophobiaGame(ITextChannel channel, int time) + { + this.channel = channel; + this.time = time; + this.source = new CancellationTokenSource(); + + this.rng = new NadekoRandom(); + 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(); + } + + private EmbedBuilder GetEmbed() + { + var i = 0; + return phase == AcroPhase.Submitting + + ? new EmbedBuilder().WithOkColor() + .WithTitle("Acrophobia") + .WithDescription($"Game started. Create a sentence with the following acronym: **{string.Join(".", startingLetters)}.**\n") + .WithFooter(efb => efb.WithText("You have " + this.time + " seconds to make a submission.")) + + : new EmbedBuilder() + .WithOkColor() + .WithTitle("Acrophobia - Submissions Closed") + .WithDescription($@"Acronym was **{string.Join(".", startingLetters)}.** +-- +{this.submissions.Aggregate("", (agg, cur) => agg + $"`{++i}.` **{cur.Key.ToLowerInvariant().ToTitleCase()}**\n")} +--") + .WithFooter(efb => efb.WithText("Vote by retyping one of the submissions")); + } + + public async Task Run() + { + NadekoBot.Client.MessageReceived += PotentialAcro; + var embed = GetEmbed(); + + //SUBMISSIONS PHASE + await channel.EmbedAsync(embed.Build()).ConfigureAwait(false); + try + { + await Task.Delay(time * 1000, source.Token).ConfigureAwait(false); + phase = AcroPhase.Idle; + } + catch (OperationCanceledException) + { + return; + } + + var i = 0; + if (submissions.Count == 0) + { + await channel.SendErrorAsync("Acrophobia", "Game ended with no submissions."); + return; + } + else if (submissions.Count == 1) + { + await channel.SendConfirmAsync("Acrophobia", $"{submissions.First().Value.Mention} is the winner for being the only user who made a submission!").ConfigureAwait(false); + return; + } + var submissionClosedEmbed = GetEmbed(); + + await channel.EmbedAsync(submissionClosedEmbed.Build()).ConfigureAwait(false); + + //VOTING PHASE + this.phase = AcroPhase.Voting; + try + { + //30 secondds for voting + await Task.Delay(30000, source.Token).ConfigureAwait(false); + this.phase = AcroPhase.Idle; + } + catch (OperationCanceledException) + { + return; + } + await End().ConfigureAwait(false); + } + + private Task PotentialAcro(IMessage arg) + { + var t = Task.Run(async () => + { + try + { + var msg = arg as IUserMessage; + if (msg == null || msg.Author.IsBot || msg.Channel.Id != channel.Id) + return; + + ++spamCount; + + var guildUser = (IGuildUser)msg.Author; + + var input = msg.Content.ToUpperInvariant().Trim(); + + if (phase == AcroPhase.Submitting) + { + if (spamCount > 10) + { + spamCount = 0; + try { await channel.EmbedAsync(GetEmbed().Build()).ConfigureAwait(false); } + catch { } + } + //user didn't input something already + IGuildUser throwaway; + if (submissions.TryGetValue(input, out throwaway)) + return; + var inputWords = input.Split(' '); //get all words + + if (inputWords.Length != startingLetters.Length) // number of words must be the same as the number of the starting letters + return; + + for (int i = 0; i < startingLetters.Length; i++) + { + var letter = startingLetters[i]; + + if (!inputWords[i].StartsWith(letter.ToString())) // all first letters must match + return; + } + + //try adding it to the list of answers + if (!submissions.TryAdd(input, guildUser)) + return; + + // all good. valid input. answer recorded + await channel.SendConfirmAsync("Acrophobia", $"{guildUser.Mention} submitted their sentence. ({submissions.Count} total)"); + try + { + await msg.DeleteAsync(); + } + catch + { + await msg.DeleteAsync(); //try twice + } + } + else if (phase == AcroPhase.Voting) + { + if (spamCount > 10) + { + spamCount = 0; + try { await channel.EmbedAsync(GetEmbed().Build()).ConfigureAwait(false); } + catch { } + } + + IGuildUser usr; + if (submissions.TryGetValue(input, out usr) && usr.Id != guildUser.Id) + { + votes.AddOrUpdate(input, 1, (key, old) => ++old); + await channel.SendConfirmAsync("Acrophobia", $"{guildUser.Mention} cast their vote!").ConfigureAwait(false); + await msg.DeleteAsync().ConfigureAwait(false); + } + + } + } + catch { } + }); + return Task.CompletedTask; + } + + public async Task End() + { + if (!votes.Any()) + { + await channel.SendErrorAsync("Acrophobia", "No votes cast. Game ended with no winner.").ConfigureAwait(false); + return; + } + var table = votes.OrderByDescending(v => v.Value); + var winner = table.First(); + var embed = new EmbedBuilder().WithOkColor() + .WithTitle("Acrophobia") + .WithDescription($"Winner is {submissions[winner.Key].Mention} with {winner.Value} points.\n") + .WithFooter(efb => efb.WithText(winner.Key.ToLowerInvariant().ToTitleCase())); + + await channel.EmbedAsync(embed.Build()).ConfigureAwait(false); + } + + public void EnsureStopped() + { + NadekoBot.Client.MessageReceived -= PotentialAcro; + if (!source.IsCancellationRequested) + source.Cancel(); + } + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Resources/CommandStrings.Designer.cs b/src/NadekoBot/Resources/CommandStrings.Designer.cs index 6ad23be8..dc386816 100644 --- a/src/NadekoBot/Resources/CommandStrings.Designer.cs +++ b/src/NadekoBot/Resources/CommandStrings.Designer.cs @@ -86,6 +86,33 @@ namespace NadekoBot.Resources { } } + /// + /// Looks up a localized string similar to acro. + /// + public static string acro_cmd { + get { + return ResourceManager.GetString("acro_cmd", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Starts an Acrophobia game. Second argment is optional round length in seconds. (default is 45). + /// + public static string acro_desc { + get { + return ResourceManager.GetString("acro_desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to `{0}acro` or `{0}acro 30`. + /// + public static string acro_usage { + get { + return ResourceManager.GetString("acro_usage", resourceCulture); + } + } + /// /// Looks up a localized string similar to addcustreact acr. /// diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index 51bf4cf4..410ccbf5 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -2790,4 +2790,13 @@ `{0}ow us Battletag#1337` or `{0}overwatch eu Battletag#2016` + + acro + + + Starts an Acrophobia game. Second argment is optional round length in seconds. (default is 45) + + + `{0}acro` or `{0}acro 30` + \ No newline at end of file