diff --git a/KfChatDotNetBot/Commands/Cecil.cs b/KfChatDotNetBot/Commands/Cecil.cs new file mode 100644 index 0000000..cd3a79c --- /dev/null +++ b/KfChatDotNetBot/Commands/Cecil.cs @@ -0,0 +1,108 @@ +using KfChatDotNetBot.Migrations; +using MathNet.Numerics; +using KfChatDotNetBot.Services; +using MathNet.Numerics.Distributions; +using RandN; +using RandN.Compat; +namespace KfChatDotNetBot.Commands.Kasino; + +public static class Cecil +{ + private static RandomShim _rand = RandomShim.Create(StandardRng.Create()); + public static double Consult(Skew skew, double minThreshold = 0) + { + double r = _rand.NextDouble(); + if (r < skew.LossRate) + { + return 0; + } + + double winRate = 1 - skew.LossRate; + double scaledR = (r - skew.LossRate) / winRate; + + double baseResult; + if (skew is BetaSkew betaSkew) + { + baseResult = Beta.InvCDF(betaSkew.Weight, betaSkew.Beta, scaledR) * betaSkew.CalibratedMaxWin; + } + else if (skew is GammaSkew gammaSkew) + { + baseResult = Gamma.InvCDF(gammaSkew.Weight, gammaSkew.Weight, scaledR); + } + else return 0; + + baseResult /= winRate; + + if (minThreshold == 0) return baseResult; + + double limit = (skew is BetaSkew b) ? b.MaxWin : double.MaxValue; + + return Math.Min(Round(baseResult, minThreshold), limit); + + double Round(double baseR, double minT) + { + double lower = Math.Floor(baseR / minT) * minT; + double upper = lower + minT; + double roundChance = (baseR - lower) / minT; + return (_rand.NextDouble() < roundChance) ? lower : upper; + } + + } +} + +public abstract class Skew +{ + public double Weight; + public double LossRate; + protected double TargetEv = 1; + public abstract void Calibrate(double winRate); + + public void Rig(double desiredEv, double lr) + { + TargetEv = desiredEv; + Calibrate(1-lr); + } +} + +public class BetaSkew : Skew +{ + public readonly double MaxWin; + public double CalibratedMaxWin; + public double Beta; + public double Alpha; + + public BetaSkew(double vol, double mw, double lr) + { + MaxWin = mw; + Weight = vol; + LossRate = lr; + Rig(1, lr); + } + + public override void Calibrate(double winRate) + { + CalibratedMaxWin = MaxWin * winRate; + double normalizer = TargetEv / CalibratedMaxWin; + Alpha = normalizer * Weight; + Beta = (1 - normalizer) * Weight; + } +} + +public class GammaSkew : Skew +{ + public double Alpha; + public double B; + + public GammaSkew(double wght, double lr) + { + Weight = wght; + LossRate = lr; + Alpha = Math.Pow(Weight, Weight) / SpecialFunctions.Gamma(wght); + Rig(1, lr); + } + + public override void Calibrate(double winRate) + { + B = (Weight / TargetEv) * winRate; + } +} diff --git a/KfChatDotNetBot/Commands/CecilCommand.cs b/KfChatDotNetBot/Commands/CecilCommand.cs new file mode 100644 index 0000000..74e703f --- /dev/null +++ b/KfChatDotNetBot/Commands/CecilCommand.cs @@ -0,0 +1,109 @@ +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; + +public class CecilCommand : ICommand +{ + public List Patterns => [ + new Regex(@"^cecil (?\d+(?:\.\d+)?) (?\d+(?:\.\d+)?) (?\d+(?:\.\d+)?)", RegexOptions.IgnoreCase), + new Regex(@"^cecil (?\d+(?:\.\d+)?) (?\d+(?:\.\d+)?)", RegexOptions.IgnoreCase), + new Regex(@"^cecil (?\d+(?:\.\d+)?)", RegexOptions.IgnoreCase), + new Regex("^keno") + ]; + + public string? HelpText => "!cecil "; + public UserRight RequiredRight => UserRight.Loser; + public TimeSpan Timeout => TimeSpan.FromSeconds(60); + public RateLimitOptionsModel? RateLimitOptions => new RateLimitOptionsModel + { + MaxInvocations = 5, + Window = TimeSpan.FromSeconds(10) + }; + public bool WhisperCanInvoke => true; + + public async Task RunCommand(ChatBot botInstance, BotCommandMessageModel message, UserDbModel user, + GroupCollection arguments, + CancellationToken ctx) + { + if (message is { IsWhisper: false, MessageUuid: not null }) + { + await botInstance.KfClient.DeleteMessageAsync(message.MessageUuid); + } + + var cleanupDelay = TimeSpan.FromSeconds(15); + + if (!arguments.TryGetValue("bet", out var amount)) //if user just enters !keno + { + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, not enough arguments. !cecil <[i]optional max win > 1[/i] - Cecil Tool: https://i.ddos.lgbt/raw/CecilHelper.html>", + true, autoDeleteAfter: cleanupDelay); + RateLimitService.RemoveMostRecentEntry(user, this); + 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}"); + 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); + RateLimitService.RemoveMostRecentEntry(user, this); + return; + } + + bool beta; + double difficulty; + + double result; + if (!arguments.TryGetValue("difficulty", out var diff)) + { + difficulty = 1; + } + else + { + difficulty = Convert.ToDouble(diff.Value); + } + + if (!arguments.TryGetValue("maxwin", out var maxWin)) + { + GammaSkew skew = new GammaSkew(difficulty, 0); + result = Cecil.Consult(skew, 0); + } + else + { + double mWin = Convert.ToDouble(maxWin.Value); + if (mWin < 1) + { + await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, max win must be greater than 1.", true, autoDeleteAfter: cleanupDelay); + return; + } + BetaSkew skew = new BetaSkew(difficulty, mWin, 0); + result = Cecil.Consult(skew, 0); + } + + var payout = wager * Convert.ToDecimal(result); + var net = payout - wager; + var newBalance = await Money.NewWagerAsync(gambler.Id, wager, net, WagerGame.Cecil); + var colors = + await SettingsProvider.GetMultipleValuesAsync([ + BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor + ]); + var red = $"{colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}"; + var green = $"{colors[BuiltIn.Keys.KiwiFarmsGreenColor].Value}"; + var color = (payout > wager) ? green : red; + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, Cecil has determined you are due [color={color}]{await payout.FormatKasinoCurrencyAsync()}[/color] from your wager of {await wager.FormatKasinoCurrencyAsync()}. Balance: {await newBalance.FormatKasinoCurrencyAsync()}", + true, autoDeleteAfter: cleanupDelay); + + } +} diff --git a/KfChatDotNetBot/Commands/Kasino/SlotsCommand.cs b/KfChatDotNetBot/Commands/Kasino/SlotsCommand.cs index fe54e88..e5510d0 100644 --- a/KfChatDotNetBot/Commands/Kasino/SlotsCommand.cs +++ b/KfChatDotNetBot/Commands/Kasino/SlotsCommand.cs @@ -477,7 +477,7 @@ public class SlotsCommand : ICommand for (var i = 0; i < 5; i++) { for (var j = 0; j < 5; j++) { - var r = _rand.NextDouble() * 100.1; + var r = _rand.NextDouble() * 100; if (f != 0 && j > 1) r *= 1.1; if (rigged == 'W') // guarantee max win @@ -608,18 +608,18 @@ public class SlotsCommand : ICommand if (rigged == 'L') RigSlotBoard(); char PickSlotSymbol(double r, int i, int j) { - if (r < 22.5) return 'A'; - else if (r < 44.5) return 'B'; - else if (r < 52.5) return 'C'; - else if (r < 66.5) return 'D'; - else if (r < 78.5) return 'E'; - else if (r < 84.5) return 'F'; - else if (r < 89.5) return 'G'; - else if (r < 92.5) return 'H'; - else if (r < 95.5) return 'I'; - else if (r < 97.5) return 'J'; - else if (r < 98.5) return WILD; - else if (r < (j <= 2 ? 99 : 99.5)) { if (!ex.Contains(j)) { return EXPANDER; } else return WILD; } + if (r < 15) return 'A'; + else if (r < 30) return 'B'; + else if (r < 40) return 'C'; + else if (r < 50) return 'D'; + else if (r < 65) return 'E'; + else if (r < 72.5) return 'F'; + else if (r < 80) return 'G'; + else if (r < 86) return 'H'; + else if (r < 92) return 'I'; + else if (r < 97) return 'J'; + else if (r < 97.5) return WILD; + else if (r < (j <= 2 ? 98.25 : 98.5)) { if (!ex.Contains(j)) { return EXPANDER; } else return WILD; } else { if (fc < 5) { fc++; return FEATURE; } else return WILD; }