From 26e0b1f49f0847b907424b1ea12dc37aaf283cec Mon Sep 17 00:00:00 2001 From: alogindtractor <251821224+A-Log-In-D-Tractor@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:30:49 -0700 Subject: [PATCH] message deletion and krash (#102) * Update message deletion in BlackjackCommand Refactor message deletion logic for non-whisper messages. * Add message deletion for non-whisper coinflip * Implement message deletion for non-whispers Added a check to delete non-whisper messages if they have a MessageUuid. * Delete non-whisper messages in KenoCommand * Implement KrashBetCommand for betting functionality * Lambchop message deletion Lambchop message deletion * limbo message deletion limbo message deletion * Delete message if not a whisper mines Delete message if not a whisper mines * planes message dleete planes message dleete * plinko message delete plinko message delete * Add message deletion for non-whisper messages roulette Delete the message if it's not a whisper and has a UUID. * add message deletion for non-whisper slots add message deletion for non-whisper slots * Implement message deletion for WheelCommand Add message deletion for non-whisper messages. * Add KasinoKrash service initialization * Add KasinoKrash service for game management Implement KasinoKrash service for managing the Krash game, including state management, betting, and payout logic. * Update message formatting in KasinoMines.cs add buttons * Update MinesCommand.cs allow more mines spam since message will be deleted anyways, spam will be supported via button --- .../Commands/Kasino/BlackjackCommand.cs | 9 +- .../Commands/Kasino/CoinflipCommand.cs | 7 +- .../Commands/Kasino/DiceCommand.cs | 9 +- .../Commands/Kasino/KenoCommand.cs | 5 +- .../Commands/Kasino/KrashCommand.cs | 81 ++++++ .../Commands/Kasino/LambchopCommand.cs | 9 +- .../Commands/Kasino/LimboCommand.cs | 5 +- .../Commands/Kasino/MinesCommand.cs | 6 +- .../Commands/Kasino/PlanesCommand.cs | 5 +- .../Commands/Kasino/PlinkoCommand.cs | 5 +- .../Commands/Kasino/RouletteCommand.cs | 8 +- .../Commands/Kasino/SlotsCommand.cs | 5 +- .../Commands/Kasino/WheelCommand.cs | 9 +- KfChatDotNetBot/Services/BotServices.cs | 10 +- KfChatDotNetBot/Services/KasinoKrash.cs | 254 ++++++++++++++++++ KfChatDotNetBot/Services/KasinoMines.cs | 3 +- 16 files changed, 409 insertions(+), 21 deletions(-) create mode 100644 KfChatDotNetBot/Commands/Kasino/KrashCommand.cs create mode 100644 KfChatDotNetBot/Services/KasinoKrash.cs diff --git a/KfChatDotNetBot/Commands/Kasino/BlackjackCommand.cs b/KfChatDotNetBot/Commands/Kasino/BlackjackCommand.cs index 851c8ae..d74ae1b 100644 --- a/KfChatDotNetBot/Commands/Kasino/BlackjackCommand.cs +++ b/KfChatDotNetBot/Commands/Kasino/BlackjackCommand.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using System.Text.Json; using System.Text.RegularExpressions; using KfChatDotNetBot.Extensions; using KfChatDotNetBot.Models; @@ -61,6 +61,11 @@ public class BlackjackCommand : ICommand true, autoDeleteAfter: gameDisabledCleanupDelay); return; } + + if (message is { IsWhisper: false, MessageUuid: not null }) + { + await botInstance.KfClient.DeleteMessageAsync(message.MessageUuid); + } var cleanupDelay = TimeSpan.FromMilliseconds(settings[BuiltIn.Keys.KasinoBlackjackCleanupDelay].ToType()); @@ -801,4 +806,4 @@ internal record HandResultData( List Hand, int PlayerValue, HandOutcome Outcome, - decimal Effect); \ No newline at end of file + decimal Effect); diff --git a/KfChatDotNetBot/Commands/Kasino/CoinflipCommand.cs b/KfChatDotNetBot/Commands/Kasino/CoinflipCommand.cs index aaf5773..0fa7cca 100644 --- a/KfChatDotNetBot/Commands/Kasino/CoinflipCommand.cs +++ b/KfChatDotNetBot/Commands/Kasino/CoinflipCommand.cs @@ -51,6 +51,11 @@ public class CoinflipCommand : ICommand true, autoDeleteAfter: gameDisabledCleanupDelay); return; } + + if (message is { IsWhisper: false, MessageUuid: not null }) + { + await botInstance.KfClient.DeleteMessageAsync(message.MessageUuid); + } var cleanupDelay = TimeSpan.FromMilliseconds(settings[BuiltIn.Keys.KasinoCoinflipCleanupDelay].ToType()); @@ -145,4 +150,4 @@ public class CoinflipCommand : ICommand return $"{baseUrl}/bossmancoin-{choiceStr}.webp?{unixTime}"; } -} \ No newline at end of file +} diff --git a/KfChatDotNetBot/Commands/Kasino/DiceCommand.cs b/KfChatDotNetBot/Commands/Kasino/DiceCommand.cs index 713f89f..ad60153 100644 --- a/KfChatDotNetBot/Commands/Kasino/DiceCommand.cs +++ b/KfChatDotNetBot/Commands/Kasino/DiceCommand.cs @@ -1,4 +1,4 @@ -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using KfChatDotNetBot.Extensions; using KfChatDotNetBot.Models; using KfChatDotNetBot.Models.DbModels; @@ -36,6 +36,11 @@ public class DiceCommand : ICommand BuiltIn.Keys.KasinoGameDisabledMessageCleanupDelay, BuiltIn.Keys.KasinoDiceCleanupDelay, BuiltIn.Keys.KasinoDiceEnabled ]); + + if (message is { IsWhisper: false, MessageUuid: not null }) + { + await botInstance.KfClient.DeleteMessageAsync(message.MessageUuid); + } // Check if dice is enabled var diceEnabled = (settings[BuiltIn.Keys.KasinoDiceEnabled]).ToBoolean(); @@ -155,4 +160,4 @@ public class DiceCommand : ICommand } return $"{diceDisplayShifted}\n{diceMeter}"; } -} \ No newline at end of file +} diff --git a/KfChatDotNetBot/Commands/Kasino/KenoCommand.cs b/KfChatDotNetBot/Commands/Kasino/KenoCommand.cs index 5b25a1a..37ca714 100644 --- a/KfChatDotNetBot/Commands/Kasino/KenoCommand.cs +++ b/KfChatDotNetBot/Commands/Kasino/KenoCommand.cs @@ -47,7 +47,10 @@ public class KenoCommand : ICommand BuiltIn.Keys.KasinoGameDisabledMessageCleanupDelay, BuiltIn.Keys.KasinoKenoCleanupDelay, BuiltIn.Keys.KasinoKenoFrameDelay, BuiltIn.Keys.KasinoKenoEnabled ]); - + if (message is { IsWhisper: false, MessageUuid: not null }) + { + await botInstance.KfClient.DeleteMessageAsync(message.MessageUuid); + } // Check if keno is enabled var kenoEnabled = (settings[BuiltIn.Keys.KasinoKenoEnabled]).ToBoolean(); if (!kenoEnabled) diff --git a/KfChatDotNetBot/Commands/Kasino/KrashCommand.cs b/KfChatDotNetBot/Commands/Kasino/KrashCommand.cs new file mode 100644 index 0000000..86f7f70 --- /dev/null +++ b/KfChatDotNetBot/Commands/Kasino/KrashCommand.cs @@ -0,0 +1,81 @@ +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; + +[KasinoCommand] +[WagerCommand] +public class KrashBetCommand : ICommand +{ + public List Patterns => [ + new Regex(@"^krash (?\d+(?:\.\d+)?) (?\d+(?:\.\d+)?)$", RegexOptions.IgnoreCase), + new Regex(@"^krash (?\d+(?:\.\d+)?)$", RegexOptions.IgnoreCase), + new Regex(@"^krash", RegexOptions.IgnoreCase) + ]; + + public string? HelpText => "!rain to start a rain, !rain to join all active rains"; + public UserRight RequiredRight => UserRight.Loser; + public TimeSpan Timeout => TimeSpan.FromSeconds(90); + public RateLimitOptionsModel? RateLimitOptions => null; + public bool WhisperCanInvoke => false; + + public async Task RunCommand(ChatBot botInstance, BotCommandMessageModel message, UserDbModel user, + GroupCollection arguments, + CancellationToken ctx) + { + var cleanupDelay = TimeSpan.FromSeconds(10); + + if (message is { IsWhisper: false, MessageUuid: not null }) + { + await botInstance.KfClient.DeleteMessageAsync(message.MessageUuid); + } + 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 (botInstance.BotServices.KasinoKrash == null) + { + await botInstance.SendChatMessageAsync("Krash is not currently running.", true, autoDeleteAfter: cleanupDelay); + return; + } + decimal multi; + decimal wager; + if (!arguments.TryGetValue("amount", out var amountGroup)) + { + //attempt to cash out a currently running game + await botInstance.BotServices.KasinoKrash.AttemptKrash(gambler); + } + if (!arguments.TryGetValue("multi", out var multiGroup)) + { + multi = -1; + } + else + { + multi = Convert.ToDecimal(multiGroup.Value); + } + wager = Convert.ToDecimal(amountGroup.Value); + if (wager > gambler.Balance) + { + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, your balance of {gambler.Balance} is not enough to bet {wager} on krash.", + true, autoDeleteAfter: TimeSpan.FromSeconds(5)); + return; + } + if (botInstance.BotServices.KasinoKrash.theGame == null) + { + //start a new game + await botInstance.BotServices.KasinoKrash.StartGame(gambler, wager, multi); + } + else + { + //add to the existing game + await botInstance.BotServices.KasinoKrash.AddParticipant(gambler, wager, multi); + } + } +} diff --git a/KfChatDotNetBot/Commands/Kasino/LambchopCommand.cs b/KfChatDotNetBot/Commands/Kasino/LambchopCommand.cs index 1c7830b..a25a0b9 100644 --- a/KfChatDotNetBot/Commands/Kasino/LambchopCommand.cs +++ b/KfChatDotNetBot/Commands/Kasino/LambchopCommand.cs @@ -1,4 +1,4 @@ -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using KfChatDotNetBot.Extensions; using KfChatDotNetBot.Models; using KfChatDotNetBot.Models.DbModels; @@ -66,7 +66,10 @@ public class LambchopCommand : ICommand BuiltIn.Keys.KasinoGameDisabledMessageCleanupDelay, BuiltIn.Keys.KasinoLambchopCleanupDelay, BuiltIn.Keys.KasinoLambchopEnabled ]); - + if (message is { IsWhisper: false, MessageUuid: not null }) + { + await botInstance.KfClient.DeleteMessageAsync(message.MessageUuid); + } // Check if lambchop is enabled var lambchopEnabled = (settings[BuiltIn.Keys.KasinoLambchopEnabled]).ToBoolean(); if (!lambchopEnabled) @@ -453,4 +456,4 @@ public class LambchopCommand : ICommand return (decimal)lambChopMultis[targetTile]; } -} \ No newline at end of file +} diff --git a/KfChatDotNetBot/Commands/Kasino/LimboCommand.cs b/KfChatDotNetBot/Commands/Kasino/LimboCommand.cs index 1b98032..15bb4b0 100644 --- a/KfChatDotNetBot/Commands/Kasino/LimboCommand.cs +++ b/KfChatDotNetBot/Commands/Kasino/LimboCommand.cs @@ -42,7 +42,10 @@ public class LimboCommand : ICommand BuiltIn.Keys.KasinoLimboCleanupDelay, BuiltIn.Keys.KasinoLimboEnabled, BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor ]); - + if (message is { IsWhisper: false, MessageUuid: not null }) + { + await botInstance.KfClient.DeleteMessageAsync(message.MessageUuid); + } // Check if limbo is enabled var limboEnabled = (settings[BuiltIn.Keys.KasinoLimboEnabled]).ToBoolean(); if (!limboEnabled) diff --git a/KfChatDotNetBot/Commands/Kasino/MinesCommand.cs b/KfChatDotNetBot/Commands/Kasino/MinesCommand.cs index c20e7e5..4603094 100644 --- a/KfChatDotNetBot/Commands/Kasino/MinesCommand.cs +++ b/KfChatDotNetBot/Commands/Kasino/MinesCommand.cs @@ -39,7 +39,7 @@ public class MinesCommand : ICommand public RateLimitOptionsModel? RateLimitOptions => new RateLimitOptionsModel { - MaxInvocations = 3, + MaxInvocations = 10, Window = TimeSpan.FromSeconds(10) }; public bool WhisperCanInvoke => false; @@ -56,6 +56,10 @@ public class MinesCommand : ICommand BuiltIn.Keys.KasinoMinesEnabled, BuiltIn.Keys.KasinoGameDisabledMessageCleanupDelay ]); var cleanupDelay = TimeSpan.FromMilliseconds(settings[BuiltIn.Keys.KasinoMinesCleanupDelay].ToType()); + if (message is { IsWhisper: false, MessageUuid: not null }) + { + await botInstance.KfClient.DeleteMessageAsync(message.MessageUuid); + } if (!settings[BuiltIn.Keys.KasinoMinesEnabled].ToBoolean()) { var gameDisabledCleanupDelay= TimeSpan.FromMilliseconds(settings[BuiltIn.Keys.KasinoGameDisabledMessageCleanupDelay].ToType()); diff --git a/KfChatDotNetBot/Commands/Kasino/PlanesCommand.cs b/KfChatDotNetBot/Commands/Kasino/PlanesCommand.cs index 3687351..c46b668 100644 --- a/KfChatDotNetBot/Commands/Kasino/PlanesCommand.cs +++ b/KfChatDotNetBot/Commands/Kasino/PlanesCommand.cs @@ -51,7 +51,10 @@ public class Planes : ICommand BuiltIn.Keys.KasinoPlanesCleanupDelay, BuiltIn.Keys.KasinoPlanesRandomRiggeryEnabled, BuiltIn.Keys.KasinoPlanesTargetedRiggeryEnabled, BuiltIn.Keys.KasinoPlanesTargetedRiggeryVictims ]); - + if (message is { IsWhisper: false, MessageUuid: not null }) + { + await botInstance.KfClient.DeleteMessageAsync(message.MessageUuid); + } // Check if planes is enabled var planesEnabled = (settings[BuiltIn.Keys.KasinoPlanesEnabled]).ToBoolean(); if (!planesEnabled) diff --git a/KfChatDotNetBot/Commands/Kasino/PlinkoCommand.cs b/KfChatDotNetBot/Commands/Kasino/PlinkoCommand.cs index 2cef255..7c89dda 100644 --- a/KfChatDotNetBot/Commands/Kasino/PlinkoCommand.cs +++ b/KfChatDotNetBot/Commands/Kasino/PlinkoCommand.cs @@ -72,7 +72,10 @@ public class PlinkoCommand : ICommand VACUUM += 1 - (double)HOUSE_EDGE; validPositions = new List<(int row, int col)>() { (0, DIFFICULTY-1) }; validColumnsForRow = new Dictionary>(){{0, new List(){DIFFICULTY-1}}}; - + if (message is { IsWhisper: false, MessageUuid: not null }) + { + await botInstance.KfClient.DeleteMessageAsync(message.MessageUuid); + } //calculate all the valid positions for the difficulty for (int i = 1; i < DIFFICULTY; i++) { diff --git a/KfChatDotNetBot/Commands/Kasino/RouletteCommand.cs b/KfChatDotNetBot/Commands/Kasino/RouletteCommand.cs index 008e434..df657ca 100644 --- a/KfChatDotNetBot/Commands/Kasino/RouletteCommand.cs +++ b/KfChatDotNetBot/Commands/Kasino/RouletteCommand.cs @@ -1,4 +1,4 @@ -using System.Net.Http.Headers; +using System.Net.Http.Headers; using System.Text.Json; using System.Text.RegularExpressions; using KfChatDotNetBot.Extensions; @@ -126,6 +126,10 @@ public class RouletteCommand : ICommand } await PlaceBet(botInstance, user, amountGroup.Value, betGroup.Value.Trim(), countdownDuration, ctx); + if (message is { IsWhisper: false, MessageUuid: not null }) + { + await botInstance.KfClient.DeleteMessageAsync(message.MessageUuid); + } } private async Task PlaceBet(ChatBot botInstance, UserDbModel user, string amountStr, string betStr, @@ -1035,4 +1039,4 @@ public static class RouletteAnimationGenerator return img; } -} \ No newline at end of file +} diff --git a/KfChatDotNetBot/Commands/Kasino/SlotsCommand.cs b/KfChatDotNetBot/Commands/Kasino/SlotsCommand.cs index af37142..7aaefee 100644 --- a/KfChatDotNetBot/Commands/Kasino/SlotsCommand.cs +++ b/KfChatDotNetBot/Commands/Kasino/SlotsCommand.cs @@ -49,7 +49,10 @@ public class SlotsCommand : ICommand var settings = await SettingsProvider.GetMultipleValuesAsync([ BuiltIn.Keys.KasinoGameDisabledMessageCleanupDelay, BuiltIn.Keys.KasinoSlotsEnabled ]); - + if (message is { IsWhisper: false, MessageUuid: not null }) + { + await botInstance.KfClient.DeleteMessageAsync(message.MessageUuid); + } // Check if slots is enabled var slotsEnabled = (settings[BuiltIn.Keys.KasinoSlotsEnabled]).ToBoolean(); if (!slotsEnabled) diff --git a/KfChatDotNetBot/Commands/Kasino/WheelCommand.cs b/KfChatDotNetBot/Commands/Kasino/WheelCommand.cs index 6f732c4..785b5b1 100644 --- a/KfChatDotNetBot/Commands/Kasino/WheelCommand.cs +++ b/KfChatDotNetBot/Commands/Kasino/WheelCommand.cs @@ -1,4 +1,4 @@ -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using System.Globalization; using KfChatDotNetBot.Extensions; using KfChatDotNetBot.Models; @@ -69,7 +69,10 @@ public class WheelCommand : ICommand BuiltIn.Keys.KasinoGameDisabledMessageCleanupDelay, BuiltIn.Keys.KasinoWheelCleanupDelay, BuiltIn.Keys.KasinoWheelEnabled ]); - + if (message is { IsWhisper: false, MessageUuid: not null }) + { + await botInstance.KfClient.DeleteMessageAsync(message.MessageUuid); + } // Check if wheel is enabled var wheelEnabled = (settings[BuiltIn.Keys.KasinoWheelEnabled]).ToBoolean(); if (!wheelEnabled) @@ -281,4 +284,4 @@ public class Wheel string bottom = string.Concat(reversedBottom); return $"{top}\n{middle}\n{bottom}"; } -} \ No newline at end of file +} diff --git a/KfChatDotNetBot/Services/BotServices.cs b/KfChatDotNetBot/Services/BotServices.cs index 87af245..8ece493 100644 --- a/KfChatDotNetBot/Services/BotServices.cs +++ b/KfChatDotNetBot/Services/BotServices.cs @@ -42,6 +42,7 @@ public class BotServices private YouTubePubSub? _youTubePubSub; public KasinoRain? KasinoRain; public KasinoShop? KasinoShop; + public KasinoKrash? KasinoKrash; private Task? _websocketWatchdog; private Task? _howlggGetUserTimer; @@ -95,7 +96,8 @@ public class BotServices BuildShuffleDotUs(), BuildYouTubePubSub(), BuildKasinoRain(), - BuildKasinoShop() + BuildKasinoShop(), + BuildKasinoKrash() ]; try { @@ -112,6 +114,12 @@ public class BotServices _howlggGetUserTimer = HowlggGetUserTimer(); } + private async Task BuildKasinoKrash() + { + _logger.Debug("Building the Kasino Krash thingy"); + KasinoKrash = new KasinoKrash(_chatBot, _cancellationToken); + } + private async Task BuildKasinoRain() { _logger.Debug("Building the Kasino Rain thingy"); diff --git a/KfChatDotNetBot/Services/KasinoKrash.cs b/KfChatDotNetBot/Services/KasinoKrash.cs new file mode 100644 index 0000000..627832b --- /dev/null +++ b/KfChatDotNetBot/Services/KasinoKrash.cs @@ -0,0 +1,254 @@ +using System.Text.Json; +using KfChatDotNetBot.Extensions; +using KfChatDotNetBot.Models; +using KfChatDotNetBot.Models.DbModels; +using KfChatDotNetBot.Settings; +using NLog; +using StackExchange.Redis; +using System.Text.Json.Serialization; +using KfChatDotNetBot.Commands.Kasino; +using RandN; +using RandN.Compat; + +namespace KfChatDotNetBot.Services; + +public class KasinoKrash : IDisposable +{ + private readonly Logger _logger = LogManager.GetCurrentClassLogger(); + private Task? _krashTimerTask; + private IDatabase? _redisDb; + private ChatBot _kfChatBot; + private CancellationToken _ct; + public decimal HOUSE_EDGE = 0.98m; + public KasinoKrashModel? theGame; + + + public KasinoKrash(ChatBot kfChatBot, CancellationToken ct = default) //the service itself + { + _kfChatBot = kfChatBot; + _ct = ct; + var connectionString = SettingsProvider.GetValueAsync(BuiltIn.Keys.BotRedisConnectionString).Result; + if (string.IsNullOrEmpty(connectionString.Value)) + { + _logger.Error($"Can't initialize the Kasino Krash service as Redis isn't configured in {BuiltIn.Keys.BotRedisConnectionString}"); + return; + } + + var redis = ConnectionMultiplexer.Connect(connectionString.Value); + _redisDb = redis.GetDatabase(); + //attempt to pull a game from the db in case the bot crashed while a game was ongoing. if so it will restart the run + theGame = GetKrashState().Result; + if (theGame != null) _ = RunGame(); + } + public bool IsInitialized() + { + return _redisDb != null; + } + public async Task GetKrashState() + { + if (_redisDb == null) throw new InvalidOperationException("Kasino Krash service isn't initialized"); + var json = await _redisDb.StringGetAsync("Krash.State"); + if (string.IsNullOrEmpty(json)) return null; + var data = JsonSerializer.Deserialize(json.ToString()); + return data; + } + public async Task RemoveKrashState() + { + if (_redisDb == null) throw new InvalidOperationException("Kasino krash service isn't initialized"); + await _redisDb.KeyDeleteAsync("Krash.State"); + theGame = null; + } + public async Task SaveKrashState(KasinoKrashModel krash) + { + if (_redisDb == null) throw new InvalidOperationException("Kasino Krash service isn't initialized"); + var json = JsonSerializer.Serialize(krash); + await _redisDb.StringSetAsync("Krash.State", json, null, When.Always); + } + + public async Task AttemptKrash(GamblerDbModel gambler) + { + if (theGame == null) + { + throw new InvalidOperationException("Failed to retrieve state or no krash is in progress"); + } + if (!theGame.bets.Any(x => x.gambler.User.KfId == gambler.User.KfId)) return; + if (!theGame.krashAccepted) return; + + //find which bet is yours + int index = 0; + foreach (var bet in theGame.bets) + { + if (bet.gambler.User.KfId == gambler.User.KfId) break; + index++; + } + + var krashBet = theGame.bets[index]; + theGame.bets.RemoveAt(index); + decimal payout = theGame.currentMulti * krashBet.wager - krashBet.wager; + var newBalance = await Money.NewWagerAsync(krashBet.gambler.Id, krashBet.wager, payout, WagerGame.Krash); + await _kfChatBot.SendChatMessageAsync( + $"{krashBet.gambler.User.FormatUsername()}, you [color=limegreen][b]won[/b][/color] {await payout.FormatKasinoCurrencyAsync()}!", + true, autoDeleteAfter: TimeSpan.FromSeconds(10)); + if (_kfChatBot.BotServices.KasinoShop != null) + { + await _kfChatBot.BotServices.KasinoShop.ProcessWagerTracking(krashBet.gambler, WagerGame.Krash, krashBet.wager, + payout, newBalance); + } + await SaveKrashState(theGame); + } + + public async Task AddParticipant(GamblerDbModel gambler, decimal wager, decimal multi = -1) + { + if (theGame == null) + { + theGame = await GetKrashState(); + if (theGame == null) throw new InvalidOperationException("Failed to retrieve state or no krash is in progress"); + _ = RunGame(); + } + if (theGame.bets.Any(x => x.gambler.User.KfId == gambler.User.KfId)) return; + if (!theGame.betsAccepted) return; + KrashBet bet = new KrashBet{gambler = gambler, wager = wager, multi = multi}; + theGame.bets.Add(bet); + await SaveKrashState(theGame); + } + + public async Task StartGame(GamblerDbModel creator, decimal wager, decimal multi = -1) + { + theGame = new KasinoKrashModel(creator); + theGame.bets.Add(new KrashBet{gambler = creator, wager = wager, multi = multi}); + await SaveKrashState(theGame); + _ = RunGame(); + } + public async Task RunGame() //running the actual game + { + if (theGame == null) + { + await RemoveKrashState(); + await _kfChatBot.SendChatMessageAsync("Krash error 1", true); + return; + } + var msg = await _kfChatBot.SendChatMessageAsync( + $"{theGame.creator.User.FormatUsername()} started a Krash! You have 30 seconds to place your bets.", true); + TimeSpan preGameTimer = TimeSpan.FromSeconds(30); + TimeSpan interval = TimeSpan.FromSeconds(1); + var timer = new PeriodicTimer(interval); + string bets; + while (await timer.WaitForNextTickAsync(_ct)) //timer before starting the game + { + bets = ""; + foreach (var bet in theGame.bets) bets += $"{bet.gambler.User.FormatUsername()} is betting {bet.wager}[br]"; + await _kfChatBot.KfClient.EditMessageAsync(msg.ChatMessageUuid, + $"{theGame.creator.User.FormatUsername()} started a Krash! You have {preGameTimer} to place your bets.[br]{bets}"); + preGameTimer -= interval; + if (preGameTimer <= TimeSpan.Zero) + { + break; + } + } + + //any bets placed after this point will be cancelled, must wait until the last game finishes to start a new one. + theGame.betsAccepted = false; + await SaveKrashState(theGame); + + //start the display of the game + + //change these to change the speed of the game + decimal growthRate = 1.02m; + decimal growthAcceleration = 1.00185m; + await _kfChatBot.KfClient.DeleteMessageAsync(msg.ChatMessageUuid!); + msg = await _kfChatBot.SendChatMessageAsync($"[center][b][size=200][color=limegreen]{theGame.currentMulti}x"); + decimal defaultGrowth = 0.01m; + interval = TimeSpan.FromSeconds(0.1); + timer = new PeriodicTimer(interval); + while (await timer.WaitForNextTickAsync(_ct)) + { + await _kfChatBot.KfClient.EditMessageAsync(msg.ChatMessageUuid!, $"[center][b][size=200][color=limegreen]{theGame.currentMulti}x"); + theGame.currentMulti += defaultGrowth; + defaultGrowth *= growthRate; + growthRate *= growthAcceleration; + if (theGame.currentMulti >= theGame.finalMulti) break; + } + //at this point the game crashes and everybody who did not cash out or pre bet on a multi will have balance subtracted, winners will be paid out. + await _kfChatBot.KfClient.EditMessageAsync(msg.ChatMessageUuid!, $"[center][b][size=200][color=red]{theGame.finalMulti}x"); + foreach (var bet in theGame.bets) + { + if (bet.multi <= theGame.finalMulti) + { + //you win + decimal payout = theGame.currentMulti * bet.wager - bet.wager; + var newBalance = await Money.NewWagerAsync(bet.gambler.Id, bet.wager, payout, WagerGame.Krash); + await _kfChatBot.SendChatMessageAsync( + $"{bet.gambler.User.FormatUsername()}, you [color=limegreen][b]won[/b][/color] {await payout.FormatKasinoCurrencyAsync()}!", + true, autoDeleteAfter: TimeSpan.FromSeconds(10)); + if (_kfChatBot.BotServices.KasinoShop != null) + { + await _kfChatBot.BotServices.KasinoShop.ProcessWagerTracking(bet.gambler, WagerGame.Krash, bet.wager, + payout, newBalance); + } + } + else + { + //automatically lose, no pre entered multi or it was greater than the final multi and failed to cash out + var newBalance = await Money.NewWagerAsync(bet.gambler.Id, bet.wager, -bet.wager, WagerGame.Krash); + await _kfChatBot.SendChatMessageAsync( + $"{bet.gambler.User.FormatUsername()}, you [color=red][b]lost[/b][/color] {await bet.wager.FormatKasinoCurrencyAsync()}!", + true, autoDeleteAfter: TimeSpan.FromSeconds(10)); + if (_kfChatBot.BotServices.KasinoShop != null) + { + await _kfChatBot.BotServices.KasinoShop.ProcessWagerTracking(bet.gambler, WagerGame.Krash, bet.wager, + -bet.wager, newBalance); + } + } + } + + //now close the game + await _kfChatBot.KfClient.DeleteMessageAsync(msg.ChatMessageUuid!); + await RemoveKrashState(); + } + + + + public class KasinoKrashModel + { + public GamblerDbModel creator; + public decimal finalMulti = 0; + public decimal currentMulti = 1.01m; + public List bets = new(); + public decimal HOUSE_EDGE = 0.98m; + public bool betsAccepted = true; + public bool krashAccepted = false; + public KasinoKrashModel(GamblerDbModel creator) + { + this.creator = creator; + finalMulti = GetLinearWeightedRandom(1.01, 25000); + } + + private decimal GetLinearWeightedRandom(double minValue, double maxValue) + { + var random = RandomShim.Create(StandardRng.Create()); + double r = random.NextDouble(); // Returns 0.0 to 1.0 + + // The core 1/x logic + double result = 1.0 / (1.0 - r); + + // Clamp the result to your specific range + if (result < minValue) result = minValue; + if (result > maxValue) result = maxValue; + + return (decimal)result; + } + } + + public class KrashBet + { + public required GamblerDbModel gambler{ get; set;} + public required decimal wager { get; set; } + public required decimal multi { get; set; } + } + public void Dispose() + { + _krashTimerTask?.Dispose(); + GC.SuppressFinalize(this); + } +} + diff --git a/KfChatDotNetBot/Services/KasinoMines.cs b/KfChatDotNetBot/Services/KasinoMines.cs index 3e6f64b..fe6a54a 100644 --- a/KfChatDotNetBot/Services/KasinoMines.cs +++ b/KfChatDotNetBot/Services/KasinoMines.cs @@ -189,7 +189,7 @@ public class KasinoMines value += "[br]"; } - value += $"{Creator.User.FormatUsername()}"; + value += $"{Creator.User.FormatUsername()} [ditto]!mines 1[/ditto] [ditto]!mines cashout[/ditto]"; return value; } @@ -330,6 +330,7 @@ public class KasinoMines else str += "⬜"; } + str += "[br]"; } await _kfChatBot.KfClient.EditMessageAsync(game.LastMessageId!, str); var net = payout - game.Wager;