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; namespace KfChatDotNetBot.Commands.Kasino; [KasinoCommand] [WagerCommand] public class KenoCommand : ICommand { public List Patterns => [ new Regex(@"^keno (?\d+) (?\d+)$", RegexOptions.IgnoreCase), new Regex(@"^keno (?\d+\.\d+) (?\d+)$", RegexOptions.IgnoreCase), new Regex(@"^keno (?\d+)$", RegexOptions.IgnoreCase), new Regex(@"^keno (?\d+\.\d+)$", RegexOptions.IgnoreCase), new Regex("^keno$") ]; public string? HelpText => "!keno [bet amount] [numbers to pick(optional, default 10)]"; public UserRight RequiredRight => UserRight.Loser; public TimeSpan Timeout => TimeSpan.FromSeconds(60); public RateLimitOptionsModel? RateLimitOptions => new RateLimitOptionsModel { MaxInvocations = 3, Window = TimeSpan.FromSeconds(10) }; 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, CancellationToken ctx) { var cleanupDelay = TimeSpan.FromMilliseconds((await SettingsProvider.GetValueAsync(BuiltIn.Keys.KasinoKenoCleanupDelay)).ToType()); if (!arguments.TryGetValue("amount", out var amount)) //if user just enters !keno { await botInstance.SendChatMessageAsync( $"{user.FormatUsername()}, not enough arguments. !keno , or !keno and 10 will be selected automatically", true, autoDeleteAfter: cleanupDelay); return; } 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 gambler = await Money.GetGamblerEntityAsync(user.Id, ct: ctx); if (gambler == null) throw new InvalidOperationException($"Caught a null when retrieving gambler for {user.KfUsername}"); if (gambler.Balance < wager) { await botInstance.SendChatMessageAsync( $"{user.FormatUsername()}, your balance of {await gambler.Balance.FormatKasinoCurrencyAsync()} isn't enough for this wager.", 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 playerNumbers = GenerateKenoNumbers(numbers, gambler); var casinoNumbers = GenerateKenoNumbers(10, gambler); var matches = playerNumbers.Intersect(casinoNumbers).ToList(); var payoutMulti = payoutMultipliers[numbers - 1, matches.Count]; await AnimatedDisplayTable(playerNumbers, casinoNumbers, matches, botInstance); var colors = await SettingsProvider.GetMultipleValuesAsync([ BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor ]); var newBalance = gambler.Balance - wager; if (payoutMulti == 0) //you lose { await Money.NewWagerAsync(gambler.Id, wager, -wager, WagerGame.Keno, ct: ctx); await botInstance.SendChatMessageAsync( $"{user.FormatUsername()}, you [color={colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}]lost {await wager.FormatKasinoCurrencyAsync()}[/color]. Your balance is now: {await newBalance.FormatKasinoCurrencyAsync()}.", true, autoDeleteAfter: cleanupDelay); botInstance.ScheduleMessageAutoDelete(_kenoTable, cleanupDelay); return; } //you win var win = wager * (decimal)payoutMulti; // Required to avoid compiler errors when trying to format it in the win message newBalance = gambler.Balance + win; await Money.NewWagerAsync(gambler.Id, wager, wager * (decimal)payoutMulti, WagerGame.Keno, ct: ctx); await botInstance.SendChatMessageAsync( $"{user.FormatUsername()}, you [color={colors[BuiltIn.Keys.KiwiFarmsGreenColor].Value}]won {await win.FormatKasinoCurrencyAsync()} with a {payoutMulti}x multi![/color]. Your balance is now: {await newBalance.FormatKasinoCurrencyAsync()}.", true, autoDeleteAfter: cleanupDelay); botInstance.ScheduleMessageAutoDelete(_kenoTable, cleanupDelay); } private async Task AnimatedDisplayTable(List playerNumbers, List casinoNumbers, List matches, ChatBot botInstance) { var cleanupDelay = TimeSpan.FromMilliseconds((await SettingsProvider.GetValueAsync(BuiltIn.Keys.KasinoKenoCleanupDelay)).ToType()); var logger = LogManager.GetCurrentClassLogger(); var displayMessage = ""; //keno board is 8 x 5, numbers left to right, top to bottom //FIRST FRAME 11111111111111111111111111111 var totalCounter = 1; for (var column = 0; column < 5; column++) { for (var row = 0; row < 8; row++) { if (playerNumbers.Contains(totalCounter)) displayMessage += PlayerNumberDisplay; else displayMessage += BlankSpaceDisplay; totalCounter++; } displayMessage += "[br]"; } _kenoTable = await botInstance.SendChatMessageAsync(displayMessage, true); var i = 0; while (_kenoTable.ChatMessageId == null) { i++; if (_kenoTable.Status is SentMessageTrackerStatus.NotSending or SentMessageTrackerStatus.Lost) return; if (i > 60) return; await Task.Delay(100); } var frameDelay = (await SettingsProvider.GetValueAsync(BuiltIn.Keys.KasinoKenoFrameDelay)).ToType(); //FIRST FRAME 11111111111111111111111111111 for (var frame = 0; frame < 10; frame++) //1 frame per casino number { displayMessage = ""; totalCounter = 1; for (var column = 0; column < 5; column++) { for (var row = 0; row < 8; row++) { if (casinoNumbers.Take(frame+1).Contains(totalCounter)) { if (matches.Contains(totalCounter)) { displayMessage += MatchRevealDisplay; } else { displayMessage += CasinoNumberDisplay; } } else if (playerNumbers.Contains(totalCounter)) displayMessage += PlayerNumberDisplay; else displayMessage += BlankSpaceDisplay; totalCounter++; } displayMessage += "[br]"; } await botInstance.KfClient.EditMessageAsync(_kenoTable.ChatMessageId!.Value, displayMessage); await Task.Delay(frameDelay); if (displayMessage.Length <= 79 && displayMessage.Contains(BlankSpaceDisplay) && (displayMessage.Contains(CasinoNumberDisplay) || displayMessage.Contains(MatchRevealDisplay) || frame == 9)) continue; //every board should have blank spaces and casino numbers or matches. player numbers might be hidden by matches logger.Error($"Casino numbers: {string.Join(",", casinoNumbers)} | Player Numbers: {string.Join(",", playerNumbers)} | Matches: {string.Join(",", matches)} | Frame: {frame - 1} | Display Board:"); logger.Error(displayMessage); await botInstance.SendChatMessageAsync($"Keno is bugged dewd, died on frame {frame} :bossman:", true, autoDeleteAfter: cleanupDelay); } } private List GenerateKenoNumbers(int size, GamblerDbModel gambler) { var numbers = new List(); for (var i = 0; i < size; i++) { var repeatNum = true; while (repeatNum) { var randomNum = Money.GetRandomNumber(gambler, 1, 40); if (numbers.Contains(randomNum)) continue; numbers.Add(randomNum); repeatNum = false; } } return numbers; } }