using System.Text.RegularExpressions; using KfChatDotNetBot.Extensions; using KfChatDotNetBot.Models; using KfChatDotNetBot.Models.DbModels; using KfChatDotNetBot.Services; using KfChatDotNetBot.Settings; using KfChatDotNetWsClient.Models.Events; using NLog.LayoutRenderers; namespace KfChatDotNetBot.Commands.Kasino; public class MinesCommand : ICommand { public List Patterns => [ //cashout new Regex(@"^mines\s+(?cashout)$", RegexOptions.IgnoreCase), //refresh new Regex(@"^mines\s+refresh$", RegexOptions.IgnoreCase), //clear - admin only new Regex(@"^mines\s+clear$", RegexOptions.IgnoreCase), //start game with number of picks new Regex(@"^mines\s+(?\d+(?:\.\d+)?)\s+(?\d+)\s+(?\d+)\s+(?\d+)(?:\s+(?cashout))?$", RegexOptions.IgnoreCase), //start game with coordinate string (must contain comma) new Regex(@"^mines\s+(?\d+(?:\.\d+)?)\s+(?\d+)\s+(?\d+)\s+(?\d+,\d+(?:\s+\d+,\d+)*)(?:\s+(?cashout))?$", RegexOptions.IgnoreCase), //continue game with number of picks new Regex(@"^mines\s+(?\d+)(?:\s+(?cashout))?$", RegexOptions.IgnoreCase), //continue game with coordinate string (must contain comma) new Regex(@"^mines\s+(?\d+,\d+(?:\s+\d+,\d+)*)(?:\s+(?cashout))?$", RegexOptions.IgnoreCase), //get info new Regex(@"^mines$", RegexOptions.IgnoreCase) ]; public string? HelpText => "!mines to play simple mines. !mines for advanced mines. Tool: https://i.ddos.lgbt/raw/baV63V.html"; public UserRight RequiredRight => UserRight.Loser; public TimeSpan Timeout => TimeSpan.FromSeconds(30); private const string BetPattern = @"(?\d+),(?\d+)"; private const string ToolUrl = "https://i.ddos.lgbt/raw/baV63V.html"; public RateLimitOptionsModel? RateLimitOptions => new RateLimitOptionsModel { MaxInvocations = 3, Window = TimeSpan.FromSeconds(10) }; private KasinoMines? KasinoMines; public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx) { var settings = await SettingsProvider.GetMultipleValuesAsync([ BuiltIn.Keys.KasinoMinesCleanupDelay, BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor, BuiltIn.Keys.KasinoMinesEnabled, BuiltIn.Keys.KasinoGameDisabledMessageCleanupDelay ]); var cleanupDelay = TimeSpan.FromMilliseconds(settings[BuiltIn.Keys.KasinoMinesCleanupDelay].ToType()); if (!settings[BuiltIn.Keys.KasinoMinesEnabled].ToBoolean()) { var gameDisabledCleanupDelay= TimeSpan.FromMilliseconds(settings[BuiltIn.Keys.KasinoGameDisabledMessageCleanupDelay].ToType()); await botInstance.SendChatMessageAsync( $"{user.FormatUsername()}, mines is currently disabled.", true, autoDeleteAfter: gameDisabledCleanupDelay); return; } var gambler = await Money.GetGamblerEntityAsync(user.Id, ct: ctx); if (gambler == null) throw new InvalidOperationException($"Caught a null when retrieving gambler for {user.KfUsername}"); KasinoMines = new KasinoMines(botInstance, gambler.Id); if (message.Message.Contains("clear")) { if (user.UserRight >= UserRight.TrueAndHonest) { await KasinoMines.GetSavedGames(gambler.Id); foreach (var game in KasinoMines.ActiveGames.Values) { await botInstance.KfClient.DeleteMessageAsync(game.LastMessageId!); } KasinoMines.ActiveGames.Clear(); await KasinoMines.SaveActiveGames(gambler.Id); await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, cleared all mines games.", true, autoDeleteAfter: cleanupDelay); return; } await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, you don't have permission to clear saved games.", true, autoDeleteAfter: cleanupDelay); return; } bool cashout = false; if (arguments.TryGetValue("cashout", out var cashOut) && cashOut.Success && !string.IsNullOrWhiteSpace(cashOut.Value)) cashout = true; if (!Regex.IsMatch(message.Message, @"\d") && cashout) //if the message has no ints its a cashout attempt { if (KasinoMines.ActiveGames.ContainsKey(gambler.Id)) { await KasinoMines.Cashout(KasinoMines.ActiveGames[gambler.Id]); return; } await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, you don't have a game running to cash out.", true, autoDeleteAfter: cleanupDelay); RateLimitService.RemoveMostRecentEntry(user, this); return; } //check if user has an existing game already if (!KasinoMines.ActiveGames.ContainsKey(gambler.Id)) { if (arguments.TryGetValue("refresh", out var refresh)) { await botInstance.SendChatMessageAsync( $"{user.FormatUsername()}, you tried to refresh but don't have a game running. !mines to play simple mines. !mines for advanced mines. Tool: {ToolUrl}", true, autoDeleteAfter: cleanupDelay); return; } //if there is no game currently running if (!arguments.TryGetValue("bet", out var bet)) { await botInstance.SendChatMessageAsync( $"{user.FormatUsername()}, not enough arguments(bet+). !mines to play simple mines. !mines for advanced mines. Tool: {ToolUrl}", true, autoDeleteAfter: cleanupDelay); RateLimitService.RemoveMostRecentEntry(user, this); return; } decimal wager = Convert.ToDecimal(bet.Value); if (gambler.Balance < wager) { await botInstance.SendChatMessageAsync( $"{user.FormatUsername()}, your balance is too low. Balance: {gambler.Balance.FormatKasinoCurrencyAsync()}", true, autoDeleteAfter: cleanupDelay); RateLimitService.RemoveMostRecentEntry(user, this); return; } if (wager <= 0) { await botInstance.SendChatMessageAsync( $"{user.FormatUsername()}, you have to bet something to play mines.", true, autoDeleteAfter: cleanupDelay); RateLimitService.RemoveMostRecentEntry(user, this); return; } if (!arguments.TryGetValue("size", out var size) || !arguments.TryGetValue("mines", out var mines)) { await botInstance.SendChatMessageAsync( $"{user.FormatUsername()}, not enough arguments(mines and or size+). !mines to play simple mines. !mines for advanced mines. Tool: {ToolUrl}", true, autoDeleteAfter: cleanupDelay); RateLimitService.RemoveMostRecentEntry(user, this); return; } int pick = 0; List<(int r, int c)> precisePicks = new(); if (arguments.TryGetValue("picks", out var picks)) //if they are using picks to randomly select squares to reveal { pick = Convert.ToInt32(picks.Value); } else if (arguments.TryGetValue("betString", out var betString)) //if they are using precise picks manually or from the tool to select specific squares to reveal { var matches = Regex.Matches(message.Message, BetPattern); if (matches.Count == 0) //if invalid bet string { await botInstance.SendChatMessageAsync( $"{user.FormatUsername()}, invalid bet string. Example: !mines 100 10 10 1,3 1,5 2,6 - or use the tool: {ToolUrl}", true, autoDeleteAfter: cleanupDelay); return; } foreach (Match match in matches) { precisePicks.Add((Convert.ToInt32(match.Groups["row"].Value), Convert.ToInt32(match.Groups["col"].Value))); } } else //if they didn't put anything { await botInstance.SendChatMessageAsync( $"{user.FormatUsername()}, not enough arguments(picks or betstring). !mines to play simple mines. !mines for advanced mines. Tool: {ToolUrl}", true, autoDeleteAfter: cleanupDelay); RateLimitService.RemoveMostRecentEntry(user, this); return; } int boardSize = Convert.ToInt32(size.Value); if (boardSize < 2 || boardSize > 8) { await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, board size must be between 2 and 9.",true, autoDeleteAfter: cleanupDelay); RateLimitService.RemoveMostRecentEntry(user, this); return; } int minesCount = Convert.ToInt32(mines.Value); if (minesCount < 1 || minesCount > (boardSize * boardSize) - 1) { await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, number of mines must be between 1 and {boardSize * boardSize - 1}(size^2 - 1).",true, autoDeleteAfter: cleanupDelay); RateLimitService.RemoveMostRecentEntry(user, this); return; } //at this point all valid values so good to continue making the game await KasinoMines.CreateGame(gambler, wager, boardSize, minesCount); var msg = await botInstance.SendChatMessageAsync( $"{KasinoMines.ActiveGames[gambler.Id].ToString()}", true); var msgSuccess = await botInstance.WaitForChatMessageAsync(msg, ct: ctx); if (!msgSuccess) throw new InvalidOperationException("Timed out waiting for the message"); if (pick == 0) //if using coordinates { var game = KasinoMines.ActiveGames[gambler.Id]; foreach (var coord in precisePicks) { if (game.BetsPlaced.Contains(coord) || coord.r < 0 || coord.r > game.Size || coord.c < 0 || coord.c > game.Size) { await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, you can't place duplicate or invalid bets. Use the tool: {ToolUrl}", true, autoDeleteAfter: cleanupDelay); RateLimitService.RemoveMostRecentEntry(user, this); return; } } await KasinoMines.Bet(gambler, precisePicks, msg, cashout); } else //if using picks { await KasinoMines.Bet(gambler, pick, msg, cashout); } } else { //if there is a game already running if (arguments.TryGetValue("refresh", out var refresh)) { await KasinoMines.RefreshGameMessage(gambler.Id); return; } int pick = 0; List<(int r, int c)> precisePicks = new(); if (arguments.TryGetValue("picks", out var picks)) //if they are using picks to randomly select squares to reveal { pick = Convert.ToInt32(picks.Value); } else if (arguments.TryGetValue("betString", out var betString)) //if they are using precise picks manually or from the tool to select specific squares to reveal { if (betString.Value == "cashout" || betString.Value == " cashout") { await KasinoMines.Cashout(KasinoMines.ActiveGames[gambler.Id]); return; } var matches = Regex.Matches(message.Message, BetPattern); if (matches.Count == 0 || matches == null) //if invalid bet string { await botInstance.SendChatMessageAsync( $"{user.FormatUsername()}, invalid bet string. Example: !mines 100 10 10 1,3 1,5 2,6 - or use the tool: {ToolUrl}", true, autoDeleteAfter: cleanupDelay); RateLimitService.RemoveMostRecentEntry(user, this); return; } foreach (Match match in matches) { precisePicks.Add((Convert.ToInt32(match.Groups["row"].Value), Convert.ToInt32(match.Groups["col"].Value))); } } else //if they didn't put anything { if (message.Message.Contains("cashout")) cashout = true; if (cashout) { await KasinoMines.Cashout(KasinoMines.ActiveGames[gambler.Id]); return; } else if (message.Message.Contains("refresh")) { await KasinoMines.RefreshGameMessage(gambler.Id); return; } await botInstance.SendChatMessageAsync( $"{user.FormatUsername()}, you already have a game running. !mines to reveal more spaces, !mines cashout to cash out, !mines to place precise picks. Tool: {ToolUrl}", true, autoDeleteAfter: cleanupDelay); RateLimitService.RemoveMostRecentEntry(user, this); return; } var lastmsg = KasinoMines.ActiveGames[gambler.Id].LastMessageId; SentMessageTrackerModel msg; if (lastmsg == null) { msg = (await botInstance.SendChatMessageAsync($"{KasinoMines.ActiveGames[gambler.Id].ToString()}", true)); await botInstance.WaitForChatMessageAsync(msg, ct: ctx); if (msg.ChatMessageUuid == null) throw new InvalidOperationException("Timed out waiting for the message"); } else { msg = botInstance.GetSentMessageStatus(KasinoMines.ActiveGames[gambler.Id].LastMessageReference); } if (pick == 0) //if using coordinates { var game = KasinoMines.ActiveGames[gambler.Id]; foreach (var coord in precisePicks) { if (game.BetsPlaced.Contains(coord) || coord.r <= 0 || coord.r > game.Size || coord.c <= 0 || coord.c > game.Size) { await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, you can't place duplicate or invalid bets. Use the tool: {ToolUrl}", true, autoDeleteAfter: cleanupDelay); return; } } await KasinoMines.Bet(gambler, precisePicks, msg, cashout); } else //if using picks { await KasinoMines.Bet(gambler, pick, msg, cashout); } } } }