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; using RandN; using RandN.Compat; namespace KfChatDotNetBot.Commands.Kasino; public class PlinkoCommand : ICommand { public List Patterns => [ new Regex(@"^plinko (?\d+\.\d+) (?\d+)$", RegexOptions.IgnoreCase), new Regex(@"^plinko (?\d+) (?\d+)$", RegexOptions.IgnoreCase), new Regex(@"^plinko (?\d+)$", RegexOptions.IgnoreCase), new Regex(@"^plinko (?\d+\.\d+)$", RegexOptions.IgnoreCase), new Regex("^plinko") ]; public string? HelpText => "!plinko "; public UserRight RequiredRight => UserRight.Loser; public TimeSpan Timeout => TimeSpan.FromSeconds(10); public RateLimitOptionsModel? RateLimitOptions => null; private readonly string NULLSPACE = "⚫"; private readonly string EMPTYSPACE = "⚪"; private readonly string BALL = "🟠"; private readonly int DIFFICULTY = 7;//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.02; private static readonly Dictionary PlinkoPayoutBoard = new() { {0, 25}, {1, 2_5}, {2, 0_25}, {3, 0_1}, {4, 0_25}, {5, 2_5}, {6, 25}, }; public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx) { decimal payout = 0; decimal currentPayout = 0; var settings = await SettingsProvider.GetMultipleValuesAsync([ BuiltIn.Keys.KasinoPlinkoCleanupDelay, BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor, BuiltIn.Keys.KasinoPlinkoEnabled, BuiltIn.Keys.KasinoGameDisabledMessageCleanupDelay BuiltIn.Keys.KasinoPlinkoCleanupDelay, BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor ]); if (!settings[BuiltIn.Keys.KasinoPlinkoEnabled].ToBoolean()) { var gameDisabledCleanupDelay= TimeSpan.FromMilliseconds(settings[BuiltIn.Keys.KasinoGameDisabledMessageCleanupDelay].ToType()); await botInstance.SendChatMessageAsync( $"{user.FormatUsername()}, plinko is currently disabled.", true, autoDeleteAfter: gameDisabledCleanupDelay); return; } var cleanupDelay = TimeSpan.FromMilliseconds(settings[BuiltIn.Keys.KasinoPlinkoCleanupDelay].ToType()); if (!arguments.TryGetValue("amount", out var amount)) { await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, not enough arguments. !plinko ", true, autoDeleteAfter: cleanupDelay); return; } var wager = Convert.ToDecimal(amount.Value); var gambler = await Money.GetGamblerEntityAsync(user.Id, ct: ctx); if (gambler == null) throw new InvalidOperationException($"Caught a null when retrieving gambler for {user.KfUsername}"); int numberOfBalls = 0; if (!arguments.TryGetValue("number", out var number)) { numberOfBalls = 1; } else numberOfBalls = Convert.ToInt32(number.Value); if (numberOfBalls < 1 || numberOfBalls > 10) { await botInstance.SendChatMessageAsync( $"{user.FormatUsername()}, you can only play with 1 - 10 balls at a time", true, autoDeleteAfter: cleanupDelay); return; } if (gambler.Balance < wager * numberOfBalls) { await botInstance.SendChatMessageAsync( $"{user.FormatUsername()}, your balance of {await gambler.Balance.FormatKasinoCurrencyAsync()} isn't enough for this wager.", true, autoDeleteAfter: cleanupDelay); return; } List ballsNotInPlay = new List(); List ballsInPlay = new List(); for (int i = 0; i < numberOfBalls; i++) { ballsNotInPlay.Add(new PlinkoBall()); } //game starts here int breakCounter = 0; var plinkoMessageID = await botInstance.SendChatMessageAsync(PlinkoBoardDisplay(ballsInPlay), true, autoDeleteAfter: cleanupDelay); while (plinkoMessageID.ChatMessageId == null && breakCounter < 1000) { await Task.Delay(100); breakCounter++; } if (breakCounter >= 999){ throw new Exception("game broke while waiting for chat message id"); } breakCounter = 0; while (ballsNotInPlay.Count > 0 || ballsInPlay.Count > 0) { breakCounter++; if (breakCounter >= 1000) throw new Exception("stuck in while loop in plinko"); currentPayout = 0; if (ballsNotInPlay.Count > 0) { ballsInPlay.Add(ballsNotInPlay[0]); ballsNotInPlay.RemoveAt(0); } await botInstance.KfClient.EditMessageAsync(plinkoMessageID.ChatMessageId!.Value,PlinkoBoardDisplay(ballsInPlay)); if (ballsInPlay[0].POSITION.row == DIFFICULTY - 1) //once your ball has reached the bottom calculate the payout { currentPayout = wager * PlinkoPayoutBoard[ballsInPlay[0].POSITION.col]; payout += currentPayout; ballsInPlay.RemoveAt(0); if (currentPayout > wager) { await botInstance.SendChatMessageAsync( $"{user.FormatUsername()}, you [color={settings[BuiltIn.Keys.KiwiFarmsGreenColor].Value!}]won[/color] ${currentPayout} KKK from a plinko ball worth {wager}!", true, autoDeleteAfter: TimeSpan.FromSeconds(5)); } else { await botInstance.SendChatMessageAsync( $"{user.FormatUsername()}, you [color={settings[BuiltIn.Keys.KiwiFarmsRedColor].Value!}lost[/color] ${wager-currentPayout} KKK from a plinko ball worth {wager}.", true, autoDeleteAfter: TimeSpan.FromSeconds(5)); } } foreach (var ball in ballsInPlay) { ball.Iterate(); } await Task.Delay(250); await botInstance.KfClient.EditMessageAsync(plinkoMessageID.ChatMessageId!.Value,PlinkoBoardDisplay(ballsInPlay)); await Task.Delay(250); } var newBalance = await Money.NewWagerAsync(gambler.Id, wager*numberOfBalls, payout, WagerGame.Plinko, ct: ctx); await botInstance.SendChatMessageAsync($"[u]{user.FormatUsername()}, you won ${payout} KKK from {numberOfBalls} plinko balls worth ${wager} KKK. Balance: ${newBalance} KKK", true, autoDeleteAfter: cleanupDelay); } public string PlinkoBoardDisplay(List balls) { string board = ""; bool spaceIsBall = false; bool spaceIsValid = false; List<(int row, int col)> validPositions = new() //would need to come up with a formula to make this to have user defined difficulty, good luck { (0, 3), (1, 2), (1, 4), (2, 2), (2, 3), (2, 4), (3, 1), (3,2), (3, 4), (3, 5), (4, 1), (4, 2), (4, 3), (4, 4), (4, 5), (5, 0), (5, 1), (5, 2), (5, 4), (5, 5), (5, 6), (6, 0), (6, 1), (6, 2), (6, 3), (6, 4), (6, 5), (6,6) }; for (int row = 0; row < DIFFICULTY; row++) { for (int col = 0; col < DIFFICULTY; col++) { spaceIsBall = false; spaceIsValid = false; foreach (var position in validPositions) { if (position.row == row && position.col == col) { spaceIsValid = true; foreach (var ball in balls) { if (ball.POSITION.row == row && ball.POSITION.col == col) { board += BALL; spaceIsBall = true; break; } } if (!spaceIsBall) board += EMPTYSPACE; } } if (!spaceIsValid) board += NULLSPACE; } board += "[br]"; } return board; } public class PlinkoBall { private RandomShim RAND = RandomShim.Create(StandardRng.Create()); public (int row, int col) POSITION; public PlinkoBall() { POSITION = (0, 3); } public void Iterate() { double rng = RAND.NextDouble(); bool evenrow = POSITION.row % 2 == 0; if (POSITION.col < 2) { rng -= VACUUM; } else if (POSITION.col > 4) { rng += VACUUM; } switch (rng) { case >= 0.5: if (!evenrow && Math.Abs(POSITION.col) > POSITION.row / 2) POSITION.col--; break; case < 0.5: if (!evenrow && POSITION.col > POSITION.row / 2) POSITION.col++; break; default: throw new Exception("generated an incorrect number"); } POSITION.row++; } } }