From 2b61433dc6ca98dc6490dc498939819ff88b2fb8 Mon Sep 17 00:00:00 2001 From: alogindtractor <251821224+A-Log-In-D-Tractor@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:39:10 -0700 Subject: [PATCH] Cecil and shop update (#121) * Implement FromToken method for Skew configuration Added a method to parse a serialized configuration token for Skew profiles. Tokens can be generated and viewed using the updated cecil helper tool. * Add Difficulty property to KasinoShop profile Add Difficulty property to KasinoShop profile * Add HOUSE_EDGE variable and custom difficulties Add HOUSE_EDGE variable and custom difficulties * Implement ShopSetDifficultyCommand for difficulty settings Added ShopSetDifficultyCommand to manage player difficulty settings in casino games, including validation for input parameters. * adds return to default difficulty setting adds return to default difficulty setting --- .../Commands/Kasino/CecilCommand.cs | 25 ++- KfChatDotNetBot/Commands/ShopCommands.cs | 169 ++++++++++++++++++ KfChatDotNetBot/Services/Cecil.cs | 49 ++++- KfChatDotNetBot/Services/KasinoShop.cs | 1 + 4 files changed, 241 insertions(+), 3 deletions(-) diff --git a/KfChatDotNetBot/Commands/Kasino/CecilCommand.cs b/KfChatDotNetBot/Commands/Kasino/CecilCommand.cs index d85a175..bece657 100644 --- a/KfChatDotNetBot/Commands/Kasino/CecilCommand.cs +++ b/KfChatDotNetBot/Commands/Kasino/CecilCommand.cs @@ -28,6 +28,8 @@ public class CecilCommand : ICommand }; public bool WhisperCanInvoke => true; + public decimal HOUSE_EDGE = 0.98m; + public async Task RunCommand(ChatBot botInstance, BotCommandMessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx) @@ -71,17 +73,34 @@ public class CecilCommand : ICommand return; } - var difficulty = 1.0; + bool shopActive = botInstance.BotServices.KasinoShop != null; + if (shopActive) + { + await GlobalShopFunctions.CheckProfile(botInstance, user, gambler); + HOUSE_EDGE += botInstance.BotServices.KasinoShop!.Gambler_Profiles[user.KfId].HouseEdgeModifier; + } + + var difficulty = 1.0; + bool customDiff = false; double result; if (arguments.TryGetValue("difficulty", out var diff)) { difficulty = Convert.ToDouble(diff.Value); + customDiff = true; } - if (!arguments.TryGetValue("maxwin", out var maxWin)) + if (!customDiff && shopActive && + botInstance.BotServices.KasinoShop!.Gambler_Profiles[user.KfId].Difficulty != "") + { + var skew = Skew.FromToken(botInstance.BotServices.KasinoShop!.Gambler_Profiles[user.KfId].Difficulty); + skew.Rig((double)HOUSE_EDGE, 0); + result = Cecil.Consult(skew); + } + else if (!arguments.TryGetValue("maxwin", out var maxWin)) { var skew = new GammaSkew(difficulty, 0); + skew.Rig((double)HOUSE_EDGE, 0); result = Cecil.Consult(skew, 0); } else @@ -93,8 +112,10 @@ public class CecilCommand : ICommand return; } var skew = new BetaSkew(difficulty, mWin, 0); + skew.Rig((double)HOUSE_EDGE, 0); result = Cecil.Consult(skew); } + var payout = wager * Convert.ToDecimal(result); var net = payout - wager; diff --git a/KfChatDotNetBot/Commands/ShopCommands.cs b/KfChatDotNetBot/Commands/ShopCommands.cs index a373ab9..0c38e9c 100644 --- a/KfChatDotNetBot/Commands/ShopCommands.cs +++ b/KfChatDotNetBot/Commands/ShopCommands.cs @@ -1,3 +1,4 @@ +using System.Globalization; using System.Text.RegularExpressions; using KfChatDotNetBot.Extensions; using KfChatDotNetBot.Models; @@ -107,6 +108,174 @@ public class ShopHelpCommand : ICommand } } +public class ShopSetDifficultyCommand : ICommand +{ + public List Patterns => + [ + new Regex(@"^shop difficulty (?.*)$", RegexOptions.IgnoreCase), + new Regex(@"^shop difficulty", RegexOptions.IgnoreCase) + ]; + + public string? HelpText => "Set your difficulty for cecil based games using the cecil tool"; + + public UserRight RequiredRight => UserRight.Loser; + + public TimeSpan Timeout => TimeSpan.FromSeconds(30); + + public RateLimitOptionsModel? RateLimitOptions => new RateLimitOptionsModel + { + MaxInvocations = 1, + Window = TimeSpan.FromSeconds(120) + }; + public bool WhisperCanInvoke => true; + + public async Task RunCommand(ChatBot botInstance, BotCommandMessageModel message, UserDbModel user, + GroupCollection arguments, CancellationToken ctx) + { + var cleanupDelay = TimeSpan.FromSeconds(10); + 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 shopActive = botInstance.BotServices.KasinoShop != null; + if (!shopActive) + { + await botInstance.SendChatMessageAsync("KasinoShop is not currently running.", true, autoDeleteAfter: cleanupDelay); + return; + } + await GlobalShopFunctions.CheckProfile(botInstance, user, gambler); + + + + + if (!arguments.TryGetValue("betstr", out var betstr)) + { + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, use the cecil tool to set your difficulty. https://i.ddos.lgbt/raw/CecilHelper.html", + true, autoDeleteAfter: cleanupDelay); + return; + } + //validate difficulty string + + string diff = betstr.Value; + if (diff.ToUpper() == "DEFAULT") + { + botInstance.BotServices.KasinoShop!.Gambler_Profiles[user.KfId].Difficulty = ""; + return; + } + var parts = diff.Trim().Split(':'); + string typeId = parts[0].ToUpper(CultureInfo.InvariantCulture); + + if (typeId != "B" && typeId != "G") + { + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, distribution type {typeId} is not valid, type must be B for beta or G for gamma.", true, autoDeleteAfter: cleanupDelay); + return; + } + if (typeId == "B") + { + if (parts.Length != 5) + { + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, Beta profile format error. Expected 5 parameters, but found {parts.Length}.", + true, autoDeleteAfter: cleanupDelay); + return; + } + + if (!double.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out double weight) || weight <= 0) + { + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, volatility weight value '{parts[1]}' is not valid, value must be a number greater than 0.", + true, autoDeleteAfter: cleanupDelay); + return; + } + + if (!double.TryParse(parts[2], NumberStyles.Any, CultureInfo.InvariantCulture, out double maxWin) || maxWin <= 1.0) + { + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, Max Win value '{parts[2]}' is not valid, value must be a number greater than 1.0x.", + true, autoDeleteAfter: cleanupDelay); + return; + } + + if (!double.TryParse(parts[3], NumberStyles.Any, CultureInfo.InvariantCulture, out double lossRate) || lossRate < 0 || lossRate >= 1.0) + { + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, loss rate percentage '{parts[3]}' is not valid, value must be between 0.0 and 1.0.", + true, autoDeleteAfter: cleanupDelay); + return; + } + + if (!double.TryParse(parts[4], NumberStyles.Any, CultureInfo.InvariantCulture, out double targetEv) || targetEv <= 0) + { + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, target EV calculation setup value '{parts[4]}' is not valid, value must be a number greater than 0.", + true, autoDeleteAfter: cleanupDelay); + return; + } + + if (targetEv > 1.0) + { + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, house protection violation, target EV value {targetEv} cannot exceed 1.0 (100% RTP).", + true, autoDeleteAfter: cleanupDelay); + return; + } + } + // --- Gamma Validation: G:[weight]:[lossRate]:[targetEv] --- + else if (typeId == "G") + { + if (parts.Length != 4) + { + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, Gamma profile format error. Expected 4 parameters, but found {parts.Length}.", + true, autoDeleteAfter: cleanupDelay); + return; + } + + if (!double.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out double weight) || weight <= 0) + { + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, inverse risk weight value '{parts[1]}' is not valid, value must be a number greater than 0.", + true, autoDeleteAfter: cleanupDelay); + return; + } + + if (!double.TryParse(parts[2], NumberStyles.Any, CultureInfo.InvariantCulture, out double lossRate) || lossRate < 0 || lossRate >= 1.0) + { + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, loss rate percentage '{parts[2]}' is not valid, value must be between 0.0 and 1.0.", + true, autoDeleteAfter: cleanupDelay); + return; + } + + if (!double.TryParse(parts[3], NumberStyles.Any, CultureInfo.InvariantCulture, out double targetEv) || targetEv <= 0) + { + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, target EV calculation setup value '{parts[3]}' is not valid, value must be a number greater than 0.", + true, autoDeleteAfter: cleanupDelay); + return; + } + + if (targetEv > 1.0) + { + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, house protection violation, target EV value {targetEv} cannot exceed 1.0 (100% RTP).", + true, autoDeleteAfter: cleanupDelay); + return; + } + } + + botInstance.BotServices.KasinoShop.Gambler_Profiles[user.KfId].Difficulty = diff; + await botInstance.BotServices.KasinoShop.SaveProfiles(); + + + + } +} + public class ShopListCommand : ICommand { public List Patterns => diff --git a/KfChatDotNetBot/Services/Cecil.cs b/KfChatDotNetBot/Services/Cecil.cs index 9c024cf..3028cf5 100644 --- a/KfChatDotNetBot/Services/Cecil.cs +++ b/KfChatDotNetBot/Services/Cecil.cs @@ -1,3 +1,4 @@ +using System.Globalization; using MathNet.Numerics; using MathNet.Numerics.Distributions; using RandN; @@ -23,7 +24,7 @@ public static class Cecil switch (skew) { case BetaSkew betaSkew: - baseResult = Beta.InvCDF(betaSkew.Weight, betaSkew.Beta, scaledR) * betaSkew.CalibratedMaxWin; + baseResult = Beta.InvCDF(betaSkew.Alpha, betaSkew.Beta, scaledR) * betaSkew.CalibratedMaxWin; break; case GammaSkew gammaSkew: baseResult = Gamma.InvCDF(gammaSkew.Weight, gammaSkew.Weight, scaledR); @@ -63,6 +64,52 @@ public abstract class Skew TargetEv = desiredEv; Calibrate(1-lr); } + + /// + /// Parses a serialized configuration token string from the HTML configuration utility + /// and instantiates a fully calibrated Skew state profile. + /// + /// Example formats: "B:0.5:50:0:0.95" or "G:1.2:0.05:1.0" + public static Skew FromToken(string token) + { + if (string.IsNullOrWhiteSpace(token)) + throw new ArgumentException("Engine initialization token cannot be empty.", nameof(token)); + + string[] parts = token.Split(':'); + string typeId = parts[0].ToUpper(CultureInfo.InvariantCulture); + + switch (typeId) + { + case "B": + if (parts.Length != 5) + throw new FormatException("BetaSkew token profile requires exactly 5 dynamic segments."); + + double bVol = double.Parse(parts[1], CultureInfo.InvariantCulture); + double bMw = double.Parse(parts[2], CultureInfo.InvariantCulture); + double bLr = double.Parse(parts[3], CultureInfo.InvariantCulture); + double bEv = double.Parse(parts[4], CultureInfo.InvariantCulture); + + // Instantiates structural object components, automatically applying calibrated settings. + BetaSkew beta = new BetaSkew(bVol, bMw, bLr); + beta.Rig(bEv, bLr); + return beta; + + case "G": + if (parts.Length != 4) + throw new FormatException("GammaSkew token profile requires exactly 4 dynamic segments."); + + double gWght = double.Parse(parts[1], CultureInfo.InvariantCulture); + double gLr = double.Parse(parts[2], CultureInfo.InvariantCulture); + double gEv = double.Parse(parts[3], CultureInfo.InvariantCulture); + + GammaSkew gamma = new GammaSkew(gWght, gLr); + gamma.Rig(gEv, gLr); + return gamma; + + default: + throw new NotSupportedException($"Unrecognized skew mathematical distribution identifier classification: '{typeId}'"); + } + } } public class BetaSkew : Skew diff --git a/KfChatDotNetBot/Services/KasinoShop.cs b/KfChatDotNetBot/Services/KasinoShop.cs index c83c90b..f2cf01c 100644 --- a/KfChatDotNetBot/Services/KasinoShop.cs +++ b/KfChatDotNetBot/Services/KasinoShop.cs @@ -1050,6 +1050,7 @@ public class KasinoShop public string name; private decimal CryptoBalance; public decimal OutstandingLoanBalance; + public string Difficulty = ""; public Dictionary Assets; public Dictionary Loans = new(); public decimal[] SponsorWagerLock = new decimal[2]; //[0] is how much you've wagered against your wager requirement, [1] is the wager requirement