From 2bb56c2388cb6788c8f108b7ed51d203bedd5acd Mon Sep 17 00:00:00 2001 From: alogindtractor <251821224+A-Log-In-D-Tractor@users.noreply.github.com> Date: Sun, 1 Feb 2026 19:48:17 -0800 Subject: [PATCH] Mines (#60) * Add MinesCommand Add MinesCommand parses user input and submits it to mines service * Add KasinoMines service to bot services Add KasinoMines service to bot services * kasinomines service code kasinomines service code holds all the game information so that games can be ongoing, you can leave your game and come back to it later, * Update MinesCommand.cs * Update KasinoMines.cs * Update MinesCommand.cs * add house edge to limbo add house edge to limbo * add house edge to keno add house edge to keno * Update BotServices.cs forgot to add kasino mines item * Update BuiltIn.cs add kasinomines cleanup delay setting * Update KenoCommand.cs add difficulty options to keno, classic low medium high default high * Update PlanesCommand.cs adds house edge to planes if your buffs cause house edge to be greater than 1, you have a HOUSE_EDGE - 1.0 % chance to get a guaranteed win, if house edge is less than 1, 1-HOUSE EDGE chance for a guaranteed loss * Update PlanesCommand.cs missed a counter update * Update PlinkoCommand.cs plinko house edge update changes vacuum strength based on house edge --- .../Commands/Kasino/KenoCommand.cs | 122 ++++-- .../Commands/Kasino/LimboCommand.cs | 7 +- .../Commands/Kasino/MinesCommand.cs | 225 +++++++++++ .../Commands/Kasino/PlanesCommand.cs | 250 ++++++------ .../Commands/Kasino/PlinkoCommand.cs | 5 +- KfChatDotNetBot/Services/BotServices.cs | 14 +- KfChatDotNetBot/Services/KasinoMines.cs | 379 ++++++++++++++++++ KfChatDotNetBot/Settings/BuiltIn.cs | 4 +- 8 files changed, 833 insertions(+), 173 deletions(-) create mode 100644 KfChatDotNetBot/Commands/Kasino/MinesCommand.cs create mode 100644 KfChatDotNetBot/Services/KasinoMines.cs diff --git a/KfChatDotNetBot/Commands/Kasino/KenoCommand.cs b/KfChatDotNetBot/Commands/Kasino/KenoCommand.cs index fe4e101..a8233b5 100644 --- a/KfChatDotNetBot/Commands/Kasino/KenoCommand.cs +++ b/KfChatDotNetBot/Commands/Kasino/KenoCommand.cs @@ -14,6 +14,8 @@ namespace KfChatDotNetBot.Commands.Kasino; public class KenoCommand : ICommand { public List Patterns => [ + new Regex(@"^keno (?classic|low|medium|high) (?\d+) (?\d+)$", RegexOptions.IgnoreCase), + new Regex(@"^keno (?classic|low|medium|high) (?\d+\.\d+) (?\d+)$", RegexOptions.IgnoreCase), new Regex(@"^keno (?\d+) (?\d+)$", RegexOptions.IgnoreCase), new Regex(@"^keno (?\d+\.\d+) (?\d+)$", RegexOptions.IgnoreCase), new Regex(@"^keno (?\d+)$", RegexOptions.IgnoreCase), @@ -29,11 +31,14 @@ public class KenoCommand : ICommand Window = TimeSpan.FromSeconds(10) }; + private List playerNumbers; + private List casinoNumbers; + private decimal HOUSE_EDGE = (decimal)0.98; private const string PlayerNumberDisplay = "⬜"; private const string CasinoNumberDisplay = "πŸ”Ά"; private const string MatchRevealDisplay = "πŸ’ "; private const string BlankSpaceDisplay = "⬛"; - + private SentMessageTrackerModel? _kenoTable; public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, @@ -43,20 +48,21 @@ public class KenoCommand : ICommand BuiltIn.Keys.KasinoGameDisabledMessageCleanupDelay, BuiltIn.Keys.KasinoKenoCleanupDelay, BuiltIn.Keys.KasinoKenoFrameDelay, BuiltIn.Keys.KasinoKenoEnabled ]); - + // Check if keno is enabled var kenoEnabled = (settings[BuiltIn.Keys.KasinoKenoEnabled]).ToBoolean(); if (!kenoEnabled) { - var gameDisabledCleanupDelay= TimeSpan.FromMilliseconds(settings[BuiltIn.Keys.KasinoGameDisabledMessageCleanupDelay].ToType()); + var gameDisabledCleanupDelay = + TimeSpan.FromMilliseconds(settings[BuiltIn.Keys.KasinoGameDisabledMessageCleanupDelay].ToType()); await botInstance.SendChatMessageAsync( - $"{user.FormatUsername()}, keno is currently disabled.", + $"{user.FormatUsername()}, keno is currently disabled.", true, autoDeleteAfter: gameDisabledCleanupDelay); return; } - + var cleanupDelay = TimeSpan.FromMilliseconds(settings[BuiltIn.Keys.KasinoKenoCleanupDelay].ToType()); - + if (!arguments.TryGetValue("amount", out var amount)) //if user just enters !keno { await botInstance.SendChatMessageAsync( @@ -64,8 +70,20 @@ public class KenoCommand : ICommand true, autoDeleteAfter: cleanupDelay); return; } + + string difficultyString; + if (!arguments.TryGetValue("difficulty", out var difficultyArg)) + { + difficultyString = "high"; + } + else + { + difficultyString = difficultyArg.Value; + } var wager = Convert.ToDecimal(amount.Value); - var numbers = !arguments.TryGetValue("numbers", out var userNumbers) ? 10 : Convert.ToInt32(userNumbers.Value); //if user just enters !keno + var numbers = !arguments.TryGetValue("numbers", out var userNumbers) + ? 10 + : Convert.ToInt32(userNumbers.Value); //if user just enters !keno var gambler = await Money.GetGamblerEntityAsync(user.Id, ct: ctx); if (gambler == null) throw new InvalidOperationException($"Caught a null when retrieving gambler for {user.KfUsername}"); @@ -76,31 +94,81 @@ public class KenoCommand : ICommand true, autoDeleteAfter: cleanupDelay); return; } - + if (numbers is < 1 or > 10) //if user picks invalid numbers { await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, you can only pick numbers from 1 - 10", true, autoDeleteAfter: cleanupDelay); return; } - - var payoutMultipliers = new[,]//stole the payout multis from stake keno and re added the RTP, except for the 1000x - { - {0.0, 4.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, // 1 selection - {0.0, 0.0, 17.27, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, // 2 selections - {0.0, 0.0, 0.0, 82.32, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, // 3 selections - {0.0, 0.0, 0.0, 10.1, 261.61, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, // 4 selections - {0.0, 0.0, 0.0, 4.5, 48.48, 454.54, 0.0, 0.0, 0.0, 0.0, 0.0}, // 5 selections - {0.0, 0.0, 0.0, 0.0, 11.11, 353.53, 717.17, 0.0, 0.0, 0.0, 0.0}, // 6 selections - {0.0, 0.0, 0.0, 0.0, 7.07, 90.90, 404.04, 808.08, 0.0, 0.0, 0.0}, // 7 selections - {0.0, 0.0, 0.0, 0.0, 5.05, 20.20, 272.72, 606.06, 909.09, 0.0, 0.0}, // 8 selections - {0.0, 0.0, 0.0, 0.0, 4.04, 11.11, 56.56, 505.05, 808.08, 1000.0, 0.0}, // 9 selections - {0.0, 0.0, 0.0, 0.0, 3.53, 8.08, 13.13, 63.63, 505.05, 808.08, 1000.0} // 10 selections + + var payoutMultipliersHigh = + new[,] //stole the payout multis from stake keno and re added the RTP, except for the 1000x + { + { 0.0, 4.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, // 1 selection + { 0.0, 0.0, 17.27, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, // 2 selections + { 0.0, 0.0, 0.0, 82.32, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, // 3 selections + { 0.0, 0.0, 0.0, 10.1, 261.61, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, // 4 selections + { 0.0, 0.0, 0.0, 4.5, 48.48, 454.54, 0.0, 0.0, 0.0, 0.0, 0.0 }, // 5 selections + { 0.0, 0.0, 0.0, 0.0, 11.11, 353.53, 717.17, 0.0, 0.0, 0.0, 0.0 }, // 6 selections + { 0.0, 0.0, 0.0, 0.0, 7.07, 90.90, 404.04, 808.08, 0.0, 0.0, 0.0 }, // 7 selections + { 0.0, 0.0, 0.0, 0.0, 5.05, 20.20, 272.72, 606.06, 909.09, 0.0, 0.0 }, // 8 selections + { 0.0, 0.0, 0.0, 0.0, 4.04, 11.11, 56.56, 505.05, 808.08, 1000.0, 0.0 }, // 9 selections + { 0.0, 0.0, 0.0, 0.0, 3.53, 8.08, 13.13, 63.63, 505.05, 808.08, 1000.0 } // 10 selections + }; + var payoutMultipliersClassic = + new[,] //stole the payout multis from stake keno and re added the RTP, except for the 1000x + { + { 0.0, 4.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, // 1 selection + { 0.0, 1.93, 4.59, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, // 2 selections + { 0.0, 1.02, 3.16, 10.6, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, // 3 selections + { 0.0, 0.81, 1.83, 10.1, 5.1, 22.96, 0.0, 0.0, 0.0, 0.0, 0.0 }, // 4 selections + { 0.0, 0.26, 1.42, 4.18, 16.83, 36.73, 0.0, 0.0, 0.0, 0.0, 0.0 }, // 5 selections + { 0.0, 0.0, 1.02, 3.75, 7.14, 16.83, 40.81, 0.0, 0.0, 0.0, 0.0 }, // 6 selections + { 0.0, 0.0, 0.46, 3.06, 4.59, 14.28, 31.63, 61.22, 0.0, 0.0, 0.0 }, // 7 selections + { 0.0, 0.0, 0.0, 2.24, 4.08, 13.26, 22.44, 56.12, 71.42, 0.0, 0.0 }, // 8 selections + { 0.0, 0.0, 0.0, 1.58, 3.06, 8.16, 15.30, 44.89, 61.22, 86.73, 0.0 }, // 9 selections + { 0.0, 0.0, 0.0, 1.42, 2.29, 4.59, 8.16, 17.34, 51.02, 81.63, 102.04 } // 10 selections + }; + var payoutMultipliersLow = + new[,] //stole the payout multis from stake keno and re added the RTP, except for the 1000x + { + { 0.7, 1.85, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, // 1 selection + { 0.0, 2.04, 3.87, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, // 2 selections + { 0.0, 1.12, 1.4, 26.53, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, // 3 selections + { 0.0, 0.0, 2.24, 8.06, 91.83, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, // 4 selections + { 0.0, 0.0, 1.53, 4.28, 13.26, 306.12, 0.0, 0.0, 0.0, 0.0, 0.0 }, // 5 selections + { 0.0, 0.0, 1.12, 2.04, 6.32, 102.04, 714.28, 0.0, 0.0, 0.0, 0.0 }, // 6 selections + { 0.0, 0.0, 1.12, 1.63, 3.57, 15.3, 229.59, 714.28, 0.0, 0.0, 0.0 }, // 7 selections + { 0.0, 0.0, 1.12, 1.53, 2.04, 5.61, 39.79, 102.04, 816.32, 0.0, 0.0 }, // 8 selections + { 0.0, 0.0, 1.12, 1.32, 1.73, 2.55, 7.65, 51.02, 255.1, 1000.0, 0.0 }, // 9 selections + { 0.0, 0.0, 1.12, 1.22, 1.32, 1.83, 3.57, 13.26, 51.02, 255.1, 1000.0 } // 10 selections + }; + var payoutMultipliersMedium = + new[,] //stole the payout multis from stake keno and re added the RTP, except for the 1000x + { + { 0.4, 2.8, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, // 1 selection + { 0.0, 1.83, 5.2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, // 2 selections + { 0.0, 0.0, 2.85, 51.02, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, // 3 selections + { 0.0, 0.0, 1.73, 10.2, 102.04, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, // 4 selections + { 0.0, 0.0, 1.42, 4.08, 14.28, 397.95, 0.0, 0.0, 0.0, 0.0, 0.0 }, // 5 selections + { 0.0, 0.0, 0.0, 3.06, 9.18, 183.67, 724.48, 0.0, 0.0, 0.0, 0.0 }, // 6 selections + { 0.0, 0.0, 0.0, 2.04, 7.14, 30.61, 408.16, 816.32, 0.0, 0.0, 0.0 }, // 7 selections + { 0.0, 0.0, 0.0, 2.04, 4.08, 11.22, 68.36, 408.16, 918.36, 0.0, 0.0 }, // 8 selections + { 0.0, 0.0, 0.0, 2.04, 2.55, 11.11, 56.56, 505.05, 808.08, 1000.0, 0.0 }, // 9 selections + { 0.0, 0.0, 0.0, 0.0, 3.53, 5.1, 15.3, 63.63, 102.04, 510.2, 1000.0 } // 10 selections + }; + Dictionary payoutMultipliers = new Dictionary{ + { "high", payoutMultipliersHigh }, + { "low", payoutMultipliersLow}, + { "medium", payoutMultipliersMedium}, + { "classic", payoutMultipliersClassic} }; - var playerNumbers = GenerateKenoNumbers(numbers, gambler); - var casinoNumbers = GenerateKenoNumbers(10, gambler); + + playerNumbers = GenerateKenoNumbers(numbers, gambler); + casinoNumbers = GenerateKenoNumbers(10, gambler, true); var matches = playerNumbers.Intersect(casinoNumbers).ToList(); - var payoutMulti = payoutMultipliers[numbers - 1, matches.Count]; + var payoutMulti = payoutMultipliers[difficultyString][numbers - 1, matches.Count]; await AnimatedDisplayTable(playerNumbers, casinoNumbers, matches, botInstance); var colors = @@ -202,7 +270,7 @@ public class KenoCommand : ICommand } } - private List GenerateKenoNumbers(int size, GamblerDbModel gambler) + private List GenerateKenoNumbers(int size, GamblerDbModel gambler, bool kasino = false) { var numbers = new List(); for (var i = 0; i < size; i++) @@ -212,6 +280,8 @@ public class KenoCommand : ICommand { var randomNum = Money.GetRandomNumber(gambler, 1, 40); if (numbers.Contains(randomNum)) continue; + if (kasino && Money.GetRandomDouble(gambler) > (double)HOUSE_EDGE && + playerNumbers.Contains(randomNum)) continue; //rigging function numbers.Add(randomNum); repeatNum = false; } @@ -219,4 +289,4 @@ public class KenoCommand : ICommand return numbers; } -} \ No newline at end of file +} diff --git a/KfChatDotNetBot/Commands/Kasino/LimboCommand.cs b/KfChatDotNetBot/Commands/Kasino/LimboCommand.cs index 756a540..f9f88b0 100644 --- a/KfChatDotNetBot/Commands/Kasino/LimboCommand.cs +++ b/KfChatDotNetBot/Commands/Kasino/LimboCommand.cs @@ -34,6 +34,7 @@ public class LimboCommand : ICommand private const double Min = 1; private const double Max = 10000; + private decimal HOUSE_EDGE = (decimal)0.98; public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx) @@ -119,10 +120,10 @@ public class LimboCommand : ICommand //returns a distribution with a 1/multi chance of getting a number below or above sqr(min * max) (so max should basically be multi^2). basically gives you a 1/x fair chance to win //then scales the number using the number scaling function - private static decimal[] Get1XWeightedRandomNumber(double minValue, double maxValue, decimal multi) + private decimal[] Get1XWeightedRandomNumber(double minValue, double maxValue, decimal multi) { var random = RandomShim.Create(StandardRng.Create()); - var skew = 1.0 / (double)(multi * (decimal)1.01); + var skew = 1.0 / (double)(multi); var gamma = Math.Log(0.5) / Math.Log(skew); var r = random.NextDouble(); var rP = 1 - Math.Pow(1 - r, gamma); @@ -130,7 +131,7 @@ public class LimboCommand : ICommand var lnMax = Math.Log(maxValue); var exponent = lnMin + rP * (lnMax - lnMin); var result = new decimal[2]; - result[0] = (decimal)Math.Exp(exponent); + result[0] = (decimal)Math.Exp(exponent) * HOUSE_EDGE; result[1] = GetScaledNumber(lnMin, lnMax, exponent, result[0], multi); return result; } diff --git a/KfChatDotNetBot/Commands/Kasino/MinesCommand.cs b/KfChatDotNetBot/Commands/Kasino/MinesCommand.cs new file mode 100644 index 0000000..32f74ec --- /dev/null +++ b/KfChatDotNetBot/Commands/Kasino/MinesCommand.cs @@ -0,0 +1,225 @@ +using System.Text.RegularExpressions; +using KfChatDotNetBot.Extensions; +using KfChatDotNetBot.Models; +using KfChatDotNetBot.Models.DbModels; +using KfChatDotNetBot.Services; +using KfChatDotNetBot.Settings; +using KfChatDotNetWsClient.Models.Events; + +namespace KfChatDotNetBot.Commands.Kasino; + + +public class MinesCommand : ICommand +{ + public List Patterns => [ + //attempting to continue a game below here + new Regex(@"^mines (?.+) (?cashout|)$", RegexOptions.IgnoreCase), + new Regex(@"^mines (?\d+) (?cashout|)$", RegexOptions.IgnoreCase), + //attempting to start a game below here + new Regex(@"^mines (?\d+\.\d+) (?\d+) (?\d+) (?.+) (?cashout|)$", RegexOptions.IgnoreCase), + new Regex(@"^mines (?\d+) (?\d+) (?\d+) (?.+) (?cashout|)$", RegexOptions.IgnoreCase), + new Regex(@"^mines (?\d+\.\d+) (?\d+) (?\d+) (?\d+) (?cashout|)$", RegexOptions.IgnoreCase), + new Regex(@"^mines (?\d+) (?\d+) (?\d+) (?\d+) (?cashout|)$", RegexOptions.IgnoreCase), + //cashout + new Regex(@"^mines (?cashout)$", RegexOptions.IgnoreCase), + //refresh + new Regex(@"^mines (?refresh)$", RegexOptions.IgnoreCase), + //get info + new Regex("^mines") + ]; + public string? HelpText => "!mines to play simple mines. !mines for advanced mines. Tool: https://i.ddos.lgbt/raw/UJ9Dty.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/Kasino%20Mines%20Interface.html"; + + public RateLimitOptionsModel? RateLimitOptions => new RateLimitOptionsModel + { + MaxInvocations = 1, + Window = TimeSpan.FromSeconds(10) + }; + + 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}"); + bool cashout = false; + if (message.Message.Contains("cashout")) cashout = true; + //check if user has an existing game already + if (!botInstance.BotServices.KasinoMines.activeGames.ContainsKey(gambler.Id)) + { + if (arguments.TryGetValue("refresh", out var refresh)) + { + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, you 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. !mines to play simple mines. !mines for advanced mines. Tool: {toolUrl}", + true, autoDeleteAfter: cleanupDelay); + 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); + return; + } + if (!arguments.TryGetValue("size", out var size) || !arguments.TryGetValue("mines", out var mines)) + { + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, not enough arguments. !mines to play simple mines. !mines for advanced mines. Tool: {toolUrl}", + true, autoDeleteAfter: cleanupDelay); + 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 || 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); + 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. !mines to play simple mines. !mines for advanced mines. Tool: {toolUrl}", + true, autoDeleteAfter: cleanupDelay); + return; + } + int boardSize = Convert.ToInt32(size.Value); + if (boardSize < 2 || boardSize > 10) + { + await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, board size must be between 2 and 10.",true, autoDeleteAfter: cleanupDelay); + 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}(boardSize * boardSize - 1).",true, autoDeleteAfter: cleanupDelay); + return; + } + //at this point all valid values so good to continue making the game + await botInstance.BotServices.KasinoMines.CreateGame(gambler, wager, boardSize, minesCount); + var msg = await botInstance.SendChatMessageAsync( + $"{botInstance.BotServices.KasinoMines.activeGames[gambler.Id].ToString()}", true); + + if (pick == 0) //if using coordinates + { + var game = botInstance.BotServices.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 botInstance.BotServices.KasinoMines.Bet(gambler.Id, precisePicks, msg, cashout); + } + else //if using picks + { + await botInstance.BotServices.KasinoMines.Bet(gambler.Id, pick, msg, cashout); + } + } + else + { + //if there is a game already running + if (arguments.TryGetValue("refresh", out var refresh)) + { + await botInstance.BotServices.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 + { + 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); + 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 (cashout) + { + await botInstance.BotServices.KasinoMines.Cashout(botInstance.BotServices.KasinoMines.activeGames[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); + return; + } + var msg = await botInstance.SendChatMessageAsync( + $"{botInstance.BotServices.KasinoMines.activeGames[gambler.Id].ToString()}", true); + + if (pick == 0) //if using coordinates + { + var game = botInstance.BotServices.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 botInstance.BotServices.KasinoMines.Bet(gambler.Id, precisePicks, msg, cashout); + + } + else //if using picks + { + await botInstance.BotServices.KasinoMines.Bet(gambler.Id, pick, msg, cashout); + } + + } + } +} diff --git a/KfChatDotNetBot/Commands/Kasino/PlanesCommand.cs b/KfChatDotNetBot/Commands/Kasino/PlanesCommand.cs index 6258454..4ee70a9 100644 --- a/KfChatDotNetBot/Commands/Kasino/PlanesCommand.cs +++ b/KfChatDotNetBot/Commands/Kasino/PlanesCommand.cs @@ -39,8 +39,10 @@ public class Planes : ICommand private const string Water = "🌊"; private const string Air = "\u2B1C"; // White square private const string BlankSpace = "β €"; //need 35? - private bool _rigged; - private bool _superRigged; + private bool _rigged = false; + private bool _riggedWin = false; + private const int CarrierCount = 6; + private decimal HOUSE_EDGE = (decimal)0.98; public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx) { @@ -81,21 +83,30 @@ public class Planes : ICommand return; } - const int carrierCount = 6; + if (HOUSE_EDGE < 1) + { + if (Money.GetRandomDouble(gambler, 1) > (double)HOUSE_EDGE) + { + _rigged = true; + } + } + else + { + if ((double)HOUSE_EDGE - Money.GetRandomDouble(gambler, 1) > 1) + { + _riggedWin = true; + } + } + var planesBoard = CreatePlanesBoard(gambler,0); var planesBoard2 = CreatePlanesBoard(gambler); var planesBoard3 = CreatePlanesBoard(gambler); - if (_rigged) - { - planesBoard2 = RigPlanesBoard(planesBoard2, carrierCount, 0); - planesBoard3 = RigPlanesBoard(planesBoard3, carrierCount, 0); - } List planesBoards = [planesBoard, planesBoard2, planesBoard3]; var plane = new Plane(gambler); const double frameLength = 1000.0; var fullCounter = 0; var noseUp = true; - var planesDisplay = GetPreGameBoard(-3, planesBoard2, plane, carrierCount, noseUp); + var planesDisplay = GetPreGameBoard(-3, planesBoard2, plane, CarrierCount, noseUp); var msgId = await botInstance.SendChatMessageAsync(planesDisplay, true); var num = 0; while (msgId.ChatMessageId == null) @@ -113,13 +124,13 @@ public class Planes : ICommand */ do { - var counter = (fullCounter - 3) % 20; + var counter = (fullCounter - 3) % 24; await Task.Delay(TimeSpan.FromMilliseconds(frameLength / 3), ctx); if (fullCounter >= 3) { - planesDisplay = GetGameBoard(fullCounter, planesBoards, plane, carrierCount, noseUp); + planesDisplay = GetGameBoard(fullCounter, planesBoards, plane, CarrierCount, noseUp); planesDisplay += $"[br]Multi: {plane.MultiTracker}x"; for (var i = 0; i < 10; i++) { @@ -136,8 +147,8 @@ public class Planes : ICommand { while (fullCounter < 3) { - counter = fullCounter % 23 - 3; - planesDisplay = GetPreGameBoard(fullCounter, planesBoard2, plane, carrierCount, noseUp); + counter = (fullCounter - 3) % 24; + planesDisplay = GetPreGameBoard(fullCounter, planesBoard2, plane, CarrierCount, noseUp); await botInstance.KfClient.EditMessageAsync(msgId.ChatMessageId!.Value, planesDisplay); await Task.Delay(TimeSpan.FromMilliseconds(frameLength), ctx); fullCounter++; @@ -200,7 +211,7 @@ public class Planes : ICommand try { - planesDisplay = GetGameBoard(fullCounter, planesBoards, plane, carrierCount, noseUp); + planesDisplay = GetGameBoard(fullCounter, planesBoards, plane, CarrierCount, noseUp); } catch (Exception e) { @@ -216,36 +227,21 @@ public class Planes : ICommand var winnings = plane.MultiTracker * wager; planesDisplay += $"Winnings: {await winnings.FormatKasinoCurrencyAsync()}"; await botInstance.KfClient.EditMessageAsync(msgId.ChatMessageId!.Value, planesDisplay); - if (plane.Height >= 6) + if (plane.Height > 5) { break; } //maybe fuckery around here } fullCounter++; + if ((fullCounter - 3) % 24 == 0 && fullCounter != 3) + { + planesBoards.RemoveAt(0); + planesBoards.Add(CreatePlanesBoard(gambler)); + } } plane.Gravity(); - if ((fullCounter - 3) % 20 == 0 && fullCounter != 3)//removes old planesboard, adds new planeboard when necessary **********************************************************************NEEDS MORE UPDATES - { - if (Money.GetRandomNumber(gambler, 0, 100) == 0 && settings[BuiltIn.Keys.KasinoPlanesRandomRiggeryEnabled].ToBoolean()) _rigged = true; - if (settings[BuiltIn.Keys.KasinoPlanesTargetedRiggeryEnabled].ToBoolean() && - settings[BuiltIn.Keys.KasinoPlanesTargetedRiggeryVictims].JsonDeserialize>()!.Contains(user.KfId)) - { - _rigged = true; - } - logger.Info($"Switching planes boards. FullCounter: {fullCounter} | Counter: {counter}"); - planesBoards.RemoveAt(0); - planesBoards.Add(CreatePlanesBoard(gambler)); - if (_rigged && Money.GetRandomNumber(gambler, 0, 100) == 0) { - planesBoards[1] = CreatePlanesBoard(gambler, 1); //1% chance to update to a board full of rockets if rigged - _superRigged = true; - } - else if (_rigged) - { - planesBoards[1] = RigPlanesBoard(planesBoards[1], carrierCount, fullCounter); - planesBoards[2] = RigPlanesBoard(planesBoards[2], carrierCount, fullCounter); - } - } + //maybe need to add one more frame here?*************** } while (plane.Height < 6); //now plane is too low so you have either won or lost depending on your position var colors = @@ -253,11 +249,11 @@ public class Planes : ICommand BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor ]); decimal newBalance; - if ((fullCounter - 3) % carrierCount == 0) //if you landed on the carrier + if ((fullCounter - 3) % CarrierCount == 0) //if you landed on the carrier { var win = plane.MultiTracker * wager; newBalance = await Money.NewWagerAsync(gambler.Id, wager, win, WagerGame.Planes, ct: ctx); - planesDisplay = GetGameBoard(fullCounter, planesBoards, plane, carrierCount, noseUp); + planesDisplay = GetGameBoard(fullCounter, planesBoards, plane, CarrierCount, noseUp); await botInstance.KfClient.EditMessageAsync(msgId.ChatMessageId!.Value, planesDisplay); await botInstance.SendChatMessageAsync( $"{user.FormatUsername()}, you [color={colors[BuiltIn.Keys.KiwiFarmsGreenColor].Value}]successfully landed with {await win.FormatKasinoCurrencyAsync()} from a total {plane.MultiTracker:N2}x multi![/color]. Your balance is now: {await newBalance.FormatKasinoCurrencyAsync()}", @@ -267,7 +263,7 @@ public class Planes : ICommand } plane.Crash(); newBalance = await Money.NewWagerAsync(gambler.Id, wager, -wager, WagerGame.Planes, ct: ctx); - planesDisplay = GetGameBoard(fullCounter, planesBoards, plane, carrierCount, noseUp); + planesDisplay = GetGameBoard(fullCounter, planesBoards, plane, CarrierCount, noseUp); await Task.Delay(TimeSpan.FromMilliseconds(frameLength), ctx); await botInstance.KfClient.EditMessageAsync(msgId.ChatMessageId!.Value, planesDisplay); await botInstance.SendChatMessageAsync( @@ -279,7 +275,7 @@ public class Planes : ICommand private string GetPreGameBoard(int fullCounter, int[,] planesBoard, Plane plane, int carrierCount, bool noseUp) { //counter < 5 - var counter = fullCounter % 23 - 3; + var counter = (fullCounter - 3) % 24; var output = ""; for (var row = 0; row < 8; row++) { @@ -335,7 +331,6 @@ public class Planes : ICommand } output += "[br]"; - } return output; } @@ -343,76 +338,75 @@ public class Planes : ICommand private string GetGameBoard(int fullCounter, List planesBoards, Plane plane, int carrierCount, bool noseUp) { var output = ""; - + // worldXPlane is the absolute distance the plane has traveled from the start. + int worldXPlane = fullCounter - 3; + for (var row = 0; row < 8; row++) { - for (var column = -3; - column < 10; - column++) //plane starts out 3 space behind to give some space to the view, + for (var column = -3; column < 10; column++) { - var useBoard = 1; - int counter; - if (fullCounter < 23) counter = fullCounter % 23 - 3; - else counter = (fullCounter - 3) % 20; - //--- - if (counter + column < 0) + // worldXTile is the absolute coordinate of the specific tile we are currently drawing. + int worldXTile = worldXPlane + column; + + // 1. WATER & CARRIER ROW (Row 7) + if (row == 7) { - counter = 20 + counter; - useBoard = 0; - } - else if (counter + column > 19) - { - useBoard = 2; + // We use worldXTile so the carrier stays pinned to a global position. + if (worldXTile >= 0 && worldXTile % carrierCount == 0) output += Carrier; + else output += Water; + continue; } - //---actual game board displays below here + // 2. THE PLANE (At Column 0 relative to the camera) + if (row == plane.Height && column == 0) + { + if (plane.Crashed) output += PlaneExplosion; + else output += noseUp ? PlaneUp : PlaneDown; + continue; + } + + // 3. BOOST EFFECT if (row == plane.Height && column == -1 && plane.JustHitMulti > 1) { output += Boost; - } - else if (row == 7) //water/carrier row - { - if (((fullCounter - 3)+ column) % carrierCount == 0) output += Carrier; - else output += Water; - } - else if (row == plane.Height && column == 0) - { - if (plane.Crashed) output += PlaneExplosion; - else - { - switch (noseUp) - { - case true: - output += PlaneUp; - break; - case false: - output += PlaneDown; - break; - } - } - } - else if (row == 6) output += Air; - else - { - //logger.Info($"GetGameBoard: attempting to access planeboard index [{row},{(column + counter) % 20}]. RawCounter: {fullCounter} | Counter: {counter} | UseBoard: {useBoard}"); - switch (planesBoards[useBoard][row, (counter + column) % 20]) - { - case 0: - output += Air; - break; - case 1: - output += Bomb; - break; - case 2: - output += Multi; - break; - } - + continue; } + // 4. THE SKY & GAME OBJECTS (Rows 0-6) + // Row 6 is always Air. Any tile with a negative world coordinate is also Air. + if (row == 6 || worldXTile < 0) + { + output += Air; + } + else + { + // Calculate which BOARD the tile belongs to (0, 1, 2, 3...) + int boardNumber = worldXTile / 24; + int localX = worldXTile % 24; + + // Map the boardNumber to our sliding window (List of 3 boards). + // Our list always contains: [Board N-1, Board N, Board N+1] + // relative to where the plane is currently flying. + int planeBoardNumber = worldXPlane / 24; + int listIndex = boardNumber - (planeBoardNumber - 1); + + if (listIndex >= 0 && listIndex < planesBoards.Count) + { + int tileValue = planesBoards[listIndex][row, localX]; + output += tileValue switch + { + 1 => Bomb, + 2 => Multi, + _ => Air + }; + } + else + { + // Fallback if the tile is beyond our current 3-board window + output += Air; + } + } } - // Was https://i.postimg.cc/rmX59qtV/avelloonaircall2.webp previously - if (_superRigged && row == 0) output += "[img]https://i.ddos.lgbt/u/6v8WJ5.webp[/img]"; output += "[br]"; } return output; @@ -420,13 +414,26 @@ public class Planes : ICommand private int[,] CreatePlanesBoard(GamblerDbModel gambler, int forceTiles = -1) { - var board = new int [6, 20]; + var board = new int [6, 24]; + for (var row = 0; row < 6; row++) { - for (var column = 0; column < 20; column++) + for (var column = 0; column < 24; column++) { - var randomNum = Money.GetRandomNumber(gambler, 1, 100); + var randomNum = Money.GetRandomNumber(gambler, 0, 100); if (forceTiles != -1) board[row, column] = forceTiles; + else if (_rigged && (column == 5 || column == 11 || column == 17 || column == 23) && row == 5) + { + board[row, column] = 2; + } + else if (_riggedWin && (column == 5 || column == 11 || column == 17 || column == 23) && row == 5) + { + board[row, column] = 0; + } + else if (_riggedWin && row == 5 && (column != 5 && column != 11 && column != 17 && column != 23)) + { + board[row, column] = 2; + } else board[row, column] = randomNum switch { @@ -436,41 +443,8 @@ public class Planes : ICommand }; } } - return board; - } - private int[,] RigPlanesBoard(int[,] planesBoard, int carrierCount, int fullCounter) - { - var returnBoard = new int[6,20]; - bool startUpdating; - var spaceToUpdate = (fullCounter-3) % 20; //how far along is the game into the current board - if (spaceToUpdate > 0) startUpdating = false; - - for (var row = 0; row < 6; row++) - { - for (var column = 0; column < 20; column++) - { - if (column >= spaceToUpdate) startUpdating = true; - else startUpdating = false; - if (startUpdating) - { - if (row == 5 && column+1 == (fullCounter-3) % carrierCount) - { - returnBoard[row, column] = 2; //force set as multi - } - else - { - returnBoard[row, column] = planesBoard[row, column]; - } - } - else - { - returnBoard[row, column] = planesBoard[row, column]; - } - - } - } - return returnBoard; + return board; } } @@ -522,10 +496,10 @@ public class Plane(GamblerDbModel gambler) private int WeightedRandomNumber(int min, int max) { var range = max - min + 1; - var weight = 6.25 + Height; + var weight = 6.55 + Height; var r = _random.NextDouble(); var exp = -Math.Log(1 - r) / weight; var returnVal = min + (int)Math.Round(exp * range); return Math.Clamp(returnVal, min, max); } -} \ No newline at end of file +} diff --git a/KfChatDotNetBot/Commands/Kasino/PlinkoCommand.cs b/KfChatDotNetBot/Commands/Kasino/PlinkoCommand.cs index 408cac0..a9b883f 100644 --- a/KfChatDotNetBot/Commands/Kasino/PlinkoCommand.cs +++ b/KfChatDotNetBot/Commands/Kasino/PlinkoCommand.cs @@ -39,7 +39,8 @@ public class PlinkoCommand : ICommand private const string BIGWINSPACE = "πŸ’²"; private const int DIFFICULTY = 8;//maybe plan to allow user to change difficulty of plinko in future updates, would need to change the payout logic though - private static readonly double VACUUM = 0.27; + private static double VACUUM = 0.25; + private decimal HOUSE_EDGE = (decimal)0.98; private static Dictionary PAYOUTSTOSTRING = new Dictionary() { @@ -69,7 +70,7 @@ public class PlinkoCommand : ICommand public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx) { - + VACUUM += 1 - (double)HOUSE_EDGE; validPositions = new List<(int row, int col)>() { (0, DIFFICULTY-1) }; validColumnsForRow = new Dictionary>(){{0, new List(){DIFFICULTY-1}}}; diff --git a/KfChatDotNetBot/Services/BotServices.cs b/KfChatDotNetBot/Services/BotServices.cs index e31eb54..813162e 100644 --- a/KfChatDotNetBot/Services/BotServices.cs +++ b/KfChatDotNetBot/Services/BotServices.cs @@ -1,4 +1,4 @@ -ο»Ώusing System.Text.Json; +using System.Text.Json; using Humanizer; using KfChatDotNetBot.Extensions; using KfChatDotNetBot.Models; @@ -41,6 +41,7 @@ public class BotServices private ShuffleDotUs? _shuffleDotUs; private YouTubePubSub? _youTubePubSub; public KasinoRain? KasinoRain; + public KasinoMines KasinoMines; private Task? _websocketWatchdog; private Task? _howlggGetUserTimer; @@ -93,7 +94,8 @@ public class BotServices BuildOwncastLiveStatusCheck(), BuildShuffleDotUs(), BuildYouTubePubSub(), - BuildKasinoRain() + BuildKasinoRain(), + BuildKasinoMines() ]; try { @@ -115,6 +117,12 @@ public class BotServices _logger.Debug("Building the Kasino Rain thingy"); KasinoRain = new KasinoRain(_chatBot, _cancellationToken); } + + private async Task BuildKasinoMines() + { + _logger.Debug("Building the Kasino mines service"); + KasinoMines = new KasinoMines(_chatBot, _cancellationToken); + } private async Task BuildShuffle() { @@ -1305,4 +1313,4 @@ public class BotServices _logger.Error($"YouTube live broadcast content '{video.Snippet.LiveBroadcastContent}' was unhandled for {data.Id}"); } } -} \ No newline at end of file +} diff --git a/KfChatDotNetBot/Services/KasinoMines.cs b/KfChatDotNetBot/Services/KasinoMines.cs new file mode 100644 index 0000000..32aa3be --- /dev/null +++ b/KfChatDotNetBot/Services/KasinoMines.cs @@ -0,0 +1,379 @@ +using System.Text.Json; +using KfChatDotNetBot.Extensions; +using KfChatDotNetBot.Models; +using KfChatDotNetBot.Models.DbModels; +using KfChatDotNetBot.Settings; +using Microsoft.EntityFrameworkCore; +using NLog; +using SixLabors.Fonts; +using StackExchange.Redis; + +namespace KfChatDotNetBot.Services; + +public class KasinoMines : IDisposable +{ + private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); + private Task? _minesTimerTask; + private IDatabase? _redisDb; + private static ChatBot _kfChatBot; + private CancellationToken _ct; + private CancellationTokenSource _minesCts = new(); + public Dictionary? activeGames; + private decimal HOUSE_EDGE = (decimal)0.98; //used to rig win rate, payout is 100% fair. with shop i plan to implement a sort of kasino shop profile holding the investments and buffs and tracking the gamblers current house edge + public class KasinoMinesGame + { + public GamblerDbModel creator { get; set; } + public DateTime lastInteracted = DateTime.UtcNow; + public char[,] minesBoard; + public decimal wager { get; set; } + public int size { get; set; } + public int mines { get; set; } + public List<(int r, int c)> betsPlaced = new(); + public SentMessageTrackerModel? lastMessage; + + + public KasinoMinesGame(GamblerDbModel creator, decimal wager, int size, int mines) + { + this.creator = creator; + this.size = size; + this.mines = mines; + this.wager = wager; + minesBoard = CreateBoard(); + } + + public async Task ResetMessage(SentMessageTrackerModel msg) + { + await _kfChatBot.KfClient.DeleteMessageAsync(lastMessage.ChatMessageId.Value); + lastMessage = msg; + } + + public async Task RigBoard((int r, int c) coord) //moves one of the mines to a specified coordinate for house edge rigging + { + //find the first mine + (int r, int c) originalMine = (11, 11); + for (int r = 0; r < size; r++) + { + for (int c = 0; c < size; c++) + { + if (minesBoard[r, c] == 'M') originalMine = (r, c); + } + } + + minesBoard[coord.r, coord.c] = 'M'; + if (originalMine.r == 11) + { + _logger.Error("Rigboard failed to find a mine somehow?"); + return; + } + minesBoard[originalMine.r, originalMine.c] = 'G'; + + } + public async Task Explode((int r, int c) mineLocation, SentMessageTrackerModel msg) + { + if (lastMessage != msg) + { + await ResetMessage(msg); + } + int frames = mineLocation.c; + if (size - mineLocation.c > frames) frames = size - mineLocation.c; + string str; + bool revealedSpace; + int yellowWave = 1; + int orangeWave = 2; + int redWave = 3; + int whiteWave = 0; + for (int f = 0; f < frames; f++) + { + str = ""; + for (int r = 0; r < size; r++) + { + await Task.Delay(100); + revealedSpace = false; + for (int c = 0; c < size; c++) + { + foreach (var bet in betsPlaced) + { + if (bet.r == r && bet.c == c) revealedSpace = true; + } + + if (mineLocation.r == r && mineLocation.c == c) + { + str += "πŸ’£"; + } + else if (revealedSpace) + { + str += "πŸ’Ž"; + } + else if (DistanceFromMine((r, c)).vertical == yellowWave || DistanceFromMine((r, c)).horizontal == yellowWave) + { + str += "🟨"; + } + else if (DistanceFromMine((r, c)).vertical == orangeWave || + DistanceFromMine((r, c)).horizontal == orangeWave) + { + str += "🟧"; + } + else if (DistanceFromMine((r, c)).vertical == redWave || + DistanceFromMine((r, c)).horizontal == redWave) + { + str += "πŸŸ₯"; + } + else if (DistanceFromMine((r, c)).vertical == whiteWave || + DistanceFromMine((r, c)).horizontal == whiteWave) + { + str += "⬜"; + } + else + { + str += "⬜"; + } + } + } + + await _kfChatBot.KfClient.EditMessageAsync(msg.ChatMessageId.Value, $"{str}[br]{creator.User.FormatUsername()}"); + } + + await Task.Delay(TimeSpan.FromSeconds(10)); + await _kfChatBot.KfClient.DeleteMessageAsync(msg.ChatMessageId.Value); + + (int vertical, int horizontal) DistanceFromMine((int r, int c) coord) + { + return (Math.Abs(coord.r - mineLocation.r), Math.Abs(coord.c - mineLocation.c)); + } + } + + public string ToString() + { + string value = ""; + bool revealedSpace; + for (int r = 0; r < size; r++) + { + revealedSpace = false; + for (int c = 0; c < size; c++) + { + foreach (var bet in betsPlaced) + { + if (bet.r == r && bet.c == c) revealedSpace = true; + } + + if (!revealedSpace) + { + value += "⬜"; + } + else if (minesBoard[r, c] == 'M') value += "πŸ’£"; + else value += "πŸ’Ž"; + } + + value += "[br]"; + } + + value += $"{creator.User.FormatUsername()}"; + return value; + } + + public char[,] CreateBoard() + { + char[,] board = new char[size, size]; + List<(int r, int c)> minesCoords = new List<(int r, int c)>(); + (int r, int c) coord; + int counter = 0; + bool gems = !(mines < (size * size)/2); //if there are more mines than gems, generate list of gem locations instead since thats less generations + int coordsCounter; + if (gems) coordsCounter = size * size - mines; + else coordsCounter = mines; + while (minesCoords.Count != coordsCounter) + { + coord = (Money.GetRandomNumber(creator, 0, size), Money.GetRandomNumber(creator, 0, size)); + if (!minesCoords.Contains(coord)) minesCoords.Add(coord); + else counter++; + if (counter >= 100000) throw new Exception($"mines failed to generate mines coordinates. Mines: {mines} | Board size: {size} | Current count of mines list {minesCoords.Count}"); + } + + foreach (var coords in minesCoords) + { + if (gems) board[coords.r, coords.c] = 'G'; + else board[coords.r, coords.c] = 'M'; + } + for (int r = 0; r < size; r++) + { + for (int c = 0; c < size; c++) + { + if (gems) + { + if (!(board[r,c] == 'G')) board[r, c] = 'M'; + } + else + { + if (!(board[r,c] == 'M')) board[r, c] = 'G'; + } + } + } + + return board; + } + public async Task DeleteMessage(SentMessageTrackerModel msg) + { + await _kfChatBot.KfClient.DeleteMessageAsync(msg.ChatMessageId.Value); + } + } + + public KasinoMines(ChatBot kfChatBot, CancellationToken ct = default) + { + _kfChatBot = kfChatBot; + _ct = ct; + var connectionString = SettingsProvider.GetValueAsync(BuiltIn.Keys.BotRedisConnectionString).Result; + if (string.IsNullOrEmpty(connectionString.Value)) + { + _logger.Error($"Can't initialize the Kasino Mines service as Redis isn't configured in {BuiltIn.Keys.BotRedisConnectionString}"); + return; + } + + var redis = ConnectionMultiplexer.Connect(connectionString.Value); + _redisDb = redis.GetDatabase(); + } + + public async Task RefreshGameMessage(int gamblerId) + { + await GetSavedGames(); + var game = activeGames[gamblerId]; + game.lastInteracted = DateTime.UtcNow; + var msg = await _kfChatBot.SendChatMessageAsync($"{game.ToString()}", true); + await game.ResetMessage(msg); + activeGames[gamblerId] = game; + await SaveActiveGames(); + } + + public async Task GetSavedGames() + { + if (_redisDb == null) throw new InvalidOperationException("Kasino mines service isn't initialized"); + var json = await _redisDb.StringGetAsync("Mines.State"); + if (string.IsNullOrEmpty(json)) return; + activeGames = JsonSerializer.Deserialize>(json.ToString()); + if (activeGames == null) + { + _logger.Error("Potentially failed to deserialize active mines games in GetSavedGames() in KasinoMines in Services"); + activeGames = new Dictionary(); + } + } + public async Task SaveActiveGames() + { + if (_redisDb == null) throw new InvalidOperationException("Kasino mines service isn't initialized"); + var json = JsonSerializer.Serialize(activeGames); + await _redisDb.StringSetAsync("Mines.State", json, null, When.Always); + } + public void Dispose() + { + GC.SuppressFinalize(this); + } + + public async Task RemoveGame(int gamblerId) + { + await GetSavedGames(); + activeGames?.Remove(gamblerId); + await SaveActiveGames(); + } + + public async Task Cashout(KasinoMinesGame game) + { + decimal payout = 0; + decimal possiblePicks = game.size * game.size - game.mines; + for (int i = 0; i < game.betsPlaced.Count; i++) + { + payout += game.wager * (possiblePicks / game.betsPlaced.Count); + possiblePicks--; + } + + var newBalance = await Money.NewWagerAsync(game.creator.Id, game.wager, payout, WagerGame.Mines); + await _kfChatBot.SendChatMessageAsync( + $"{game.creator.User.FormatUsername()}, you won {payout.FormatKasinoCurrencyAsync()} from your {game.wager.FormatKasinoCurrencyAsync()} bet on mines, collecting {game.betsPlaced.Count} gems while avoiding {game.mines} mines. Net: {(payout - game.wager).FormatKasinoCurrencyAsync()}. Balance: {newBalance.FormatKasinoCurrencyAsync()}"); + await RemoveGame(game.creator.Id); + } + + public async Task Bet(int gamblerId, int count, SentMessageTrackerModel msg, bool cashOut = false) //returns false if you hit a bomb, true if you didn't + { + await GetSavedGames(); + var game = activeGames[gamblerId]; + game.lastInteracted = DateTime.UtcNow; + if (game.lastMessage != msg) + { + await game.ResetMessage(msg); + } + List<(int r, int c)> betCoords = new(); + (int r, int c) coord; + while (betCoords.Count != count)//creates a list of coordinates to bet on using the coordinate bet function + { + coord = (Money.GetRandomNumber(game.creator, 0, game.size), Money.GetRandomNumber(game.creator, 0, game.size)); + if (!betCoords.Contains(coord) && !game.betsPlaced.Contains(coord)) betCoords.Add(coord); + } + + return await Bet(gamblerId, betCoords, msg, cashOut); + } + + public async Task Bet(int gamblerId, List<(int r, int c)> coords, SentMessageTrackerModel msg, bool cashOut = false) + { + await GetSavedGames(); + var game = activeGames[gamblerId]; + game.lastInteracted = DateTime.UtcNow; + if (game.lastMessage != msg) + { + await game.ResetMessage(msg); + } + foreach (var coord in coords) //the main portion of the game + { + await Task.Delay(100); + if (game.minesBoard[coord.r, coord.c] == 'M') + { + game.betsPlaced.Add(coord); + await _kfChatBot.KfClient.EditMessageAsync(msg.ChatMessageId!.Value, game.ToString()); + game.Explode((coord.r, coord.c), msg); + var newBalance = await Money.NewWagerAsync(game.creator.Id, game.wager, -game.wager, WagerGame.Mines); + await _kfChatBot.SendChatMessageAsync( + $"{game.creator.User.FormatUsername()}, you lost your {game.wager.FormatKasinoCurrencyAsync()} bet on mines, collecting {game.betsPlaced.Count} gems until you hit one of {game.mines} mines. Net: {(-game.wager).FormatKasinoCurrencyAsync()}. Balance: {newBalance.FormatKasinoCurrencyAsync()}", + true, autoDeleteAfter: TimeSpan.FromSeconds(15)); + await RemoveGame(gamblerId); + return false; + } + + if (Money.GetRandomNumber(game.creator, 0, 100) < 100 * HOUSE_EDGE)//if you didn't lose, check to see if the switch was flipped + { + game.betsPlaced.Add(coord); + await _kfChatBot.KfClient.EditMessageAsync(msg.ChatMessageId!.Value, game.ToString()); + await game.RigBoard(coord); + await Task.Delay(50); + await _kfChatBot.KfClient.EditMessageAsync(msg.ChatMessageId!.Value, game.ToString()); + game.Explode(coord, msg); + var newBalance = await Money.NewWagerAsync(game.creator.Id, game.wager, -game.wager, WagerGame.Mines); + await _kfChatBot.SendChatMessageAsync( + $"{game.creator.User.FormatUsername()}, you lost your {game.wager.FormatKasinoCurrencyAsync()} bet on mines, collecting {game.betsPlaced.Count} gems until you hit one of {game.mines} mines. Net: {(-game.wager).FormatKasinoCurrencyAsync()}. Balance: {newBalance.FormatKasinoCurrencyAsync()}", + true, autoDeleteAfter: TimeSpan.FromSeconds(15)); + } + else + { + game.betsPlaced.Add(coord); + } + await _kfChatBot.KfClient.EditMessageAsync(msg.ChatMessageId!.Value, game.ToString()); + } + + + activeGames[gamblerId] = game; + if (cashOut) await Cashout(game); + else await SaveActiveGames(); + return true; + } + + public bool IsInitialized() + { + return _redisDb != null; + } + + public async Task CreateGame(GamblerDbModel gambler, decimal bet, int size, int mines) + { + await GetSavedGames(); + activeGames?.Add(gambler.Id, new KasinoMinesGame(gambler, bet, size, mines)); + await SaveActiveGames(); + } + +} + + + diff --git a/KfChatDotNetBot/Settings/BuiltIn.cs b/KfChatDotNetBot/Settings/BuiltIn.cs index f5790bd..98560ef 100644 --- a/KfChatDotNetBot/Settings/BuiltIn.cs +++ b/KfChatDotNetBot/Settings/BuiltIn.cs @@ -414,7 +414,9 @@ public static class BuiltIn public static string KasinoGuessWhatNumberCleanupDelay = "Kasino.GuessWhatNumber.CleanupDelay"; [BuiltInSetting("Delay in milliseconds before cleaning up the Keno board", SettingValueType.Text, "30000", WholeNumberRegex)] public static string KasinoKenoCleanupDelay = "Kasino.Keno.CleanupDelay"; - [BuiltInSetting("Delay in milliseconds before cleaning up the Planes board and result", SettingValueType.Text, "60000", WholeNumberRegex)] + [BuiltInSetting("Delay in milliseconds before cleaning up the keno board and result", SettingValueType.Text, "60000", WholeNumberRegex)] + public static string KasinoMinesCleanupDelay = "Kasino.Mines.CleanupDelay"; + [BuiltInSetting("Delay in milliseconds before cleaning up the mines command messages", SettingValueType.Text, "60000", WholeNumberRegex)] public static string KasinoPlanesCleanupDelay = "Kasino.Planes.CleanupDelay"; [BuiltInSetting("Delay in milliseconds between each check to see whether tehre's messages to be deleted", SettingValueType.Text, "1000", WholeNumberRegex)] public static string BotScheduledDeletionInterval = "Bot.ScheduledDeletionInterval";