Added MathNet.Numerics for Cecil. Refactored and fixed some issues

This commit is contained in:
barelyprofessional
2026-05-16 11:34:37 -05:00
parent e14a08e3d5
commit 915a4cc8bf
5 changed files with 75 additions and 37 deletions
@@ -4,24 +4,24 @@ using KfChatDotNetBot.Models;
using KfChatDotNetBot.Models.DbModels; using KfChatDotNetBot.Models.DbModels;
using KfChatDotNetBot.Services; using KfChatDotNetBot.Services;
using KfChatDotNetBot.Settings; using KfChatDotNetBot.Settings;
using KfChatDotNetWsClient.Models.Events;
using NLog;
namespace KfChatDotNetBot.Commands.Kasino; namespace KfChatDotNetBot.Commands.Kasino;
[KasinoCommand]
[WagerCommand]
public class CecilCommand : ICommand public class CecilCommand : ICommand
{ {
public List<Regex> Patterns => [ public List<Regex> Patterns => [
new Regex(@"^cecil (?<bet>\d+(?:\.\d+)?) (?<difficulty>\d+(?:\.\d+)?) (?<maxwin>\d+(?:\.\d+)?)", RegexOptions.IgnoreCase), new Regex(@"^cecil (?<bet>\d+(?:\.\d+)?) (?<difficulty>\d+(?:\.\d+)?) (?<maxwin>\d+(?:\.\d+)?)", RegexOptions.IgnoreCase),
new Regex(@"^cecil (?<bet>\d+(?:\.\d+)?) (?<difficulty>\d+(?:\.\d+)?)", RegexOptions.IgnoreCase), new Regex(@"^cecil (?<bet>\d+(?:\.\d+)?) (?<difficulty>\d+(?:\.\d+)?)", RegexOptions.IgnoreCase),
new Regex(@"^cecil (?<bet>\d+(?:\.\d+)?)", RegexOptions.IgnoreCase), new Regex(@"^cecil (?<bet>\d+(?:\.\d+)?)", RegexOptions.IgnoreCase),
new Regex("^keno") new Regex("^cecil")
]; ];
public string? HelpText => "!cecil <bet> <optional difficulty> <optional max win>"; public string? HelpText => "!cecil <bet> <optional difficulty> <optional max win>";
public UserRight RequiredRight => UserRight.Loser; public UserRight RequiredRight => UserRight.Loser;
public TimeSpan Timeout => TimeSpan.FromSeconds(60); public TimeSpan Timeout => TimeSpan.FromSeconds(60);
public RateLimitOptionsModel? RateLimitOptions => new RateLimitOptionsModel public RateLimitOptionsModel? RateLimitOptions => new()
{ {
MaxInvocations = 5, MaxInvocations = 5,
Window = TimeSpan.FromSeconds(10) Window = TimeSpan.FromSeconds(10)
@@ -41,7 +41,7 @@ public class CecilCommand : ICommand
if (!arguments.TryGetValue("bet", out var amount)) //if user just enters !keno if (!arguments.TryGetValue("bet", out var amount)) //if user just enters !keno
{ {
await botInstance.SendChatMessageAsync( await botInstance.ReplyToUser(message,
$"{user.FormatUsername()}, not enough arguments. !cecil <bet> <optional difficulty> <[i]optional max win > 1[/i] - Cecil Tool: https://i.ddos.lgbt/raw/CecilHelper.html>", $"{user.FormatUsername()}, not enough arguments. !cecil <bet> <optional difficulty> <[i]optional max win > 1[/i] - Cecil Tool: https://i.ddos.lgbt/raw/CecilHelper.html>",
true, autoDeleteAfter: cleanupDelay); true, autoDeleteAfter: cleanupDelay);
RateLimitService.RemoveMostRecentEntry(user, this); RateLimitService.RemoveMostRecentEntry(user, this);
@@ -54,14 +54,13 @@ public class CecilCommand : ICommand
throw new InvalidOperationException($"Caught a null when retrieving gambler for {user.KfUsername}"); throw new InvalidOperationException($"Caught a null when retrieving gambler for {user.KfUsername}");
if (gambler.Balance < wager) if (gambler.Balance < wager)
{ {
await botInstance.SendChatMessageAsync( await botInstance.ReplyToUser(message,
$"{user.FormatUsername()}, your balance of {await gambler.Balance.FormatKasinoCurrencyAsync()} isn't enough for this wager.", $"{user.FormatUsername()}, your balance of {await gambler.Balance.FormatKasinoCurrencyAsync()} isn't enough for this wager.",
true, autoDeleteAfter: cleanupDelay); true, autoDeleteAfter: cleanupDelay);
RateLimitService.RemoveMostRecentEntry(user, this); RateLimitService.RemoveMostRecentEntry(user, this);
return; return;
} }
bool beta;
double difficulty; double difficulty;
double result; double result;
@@ -76,32 +75,32 @@ public class CecilCommand : ICommand
if (!arguments.TryGetValue("maxwin", out var maxWin)) if (!arguments.TryGetValue("maxwin", out var maxWin))
{ {
GammaSkew skew = new GammaSkew(difficulty, 0); var skew = new GammaSkew(difficulty, 0);
result = Cecil.Consult(skew, 0); result = Cecil.Consult(skew, 0);
} }
else else
{ {
double mWin = Convert.ToDouble(maxWin.Value); var mWin = Convert.ToDouble(maxWin.Value);
if (mWin < 1) if (mWin < 1)
{ {
await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, max win must be greater than 1.", true, autoDeleteAfter: cleanupDelay); await botInstance.ReplyToUser(message, $"{user.FormatUsername()}, max win must be greater than 1.", true, autoDeleteAfter: cleanupDelay);
return; return;
} }
BetaSkew skew = new BetaSkew(difficulty, mWin, 0); var skew = new BetaSkew(difficulty, mWin, 0);
result = Cecil.Consult(skew, 0); result = Cecil.Consult(skew);
} }
var payout = wager * Convert.ToDecimal(result); var payout = wager * Convert.ToDecimal(result);
var net = payout - wager; var net = payout - wager;
var newBalance = await Money.NewWagerAsync(gambler.Id, wager, net, WagerGame.Cecil); var newBalance = await Money.NewWagerAsync(gambler.Id, wager, net, WagerGame.Cecil, ct: ctx);
var colors = var colors =
await SettingsProvider.GetMultipleValuesAsync([ await SettingsProvider.GetMultipleValuesAsync([
BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor
]); ]);
var red = $"{colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}"; var red = colors[BuiltIn.Keys.KiwiFarmsRedColor].Value;
var green = $"{colors[BuiltIn.Keys.KiwiFarmsGreenColor].Value}"; var green = colors[BuiltIn.Keys.KiwiFarmsGreenColor].Value;
var color = (payout > wager) ? green : red; var color = (payout > wager) ? green : red;
await botInstance.SendChatMessageAsync( await botInstance.ReplyToUser(message,
$"{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()}", $"{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); true, autoDeleteAfter: cleanupDelay);
+1
View File
@@ -14,6 +14,7 @@
<PackageReference Include="Homoglyphic" Version="2.0.1" /> <PackageReference Include="Homoglyphic" Version="2.0.1" />
<PackageReference Include="HtmlAgilityPack" Version="1.12.4" /> <PackageReference Include="HtmlAgilityPack" Version="1.12.4" />
<PackageReference Include="Humanizer.Core" Version="3.0.1" /> <PackageReference Include="Humanizer.Core" Version="3.0.1" />
<PackageReference Include="MathNet.Numerics" Version="5.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.3" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.3"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.3">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
@@ -310,7 +310,8 @@ public enum WagerGame
Plinko, Plinko,
[Description("Roulette but live")] [Description("Roulette but live")]
Roulette, Roulette,
Krash Krash,
Cecil
} }
public enum GamblerState public enum GamblerState
+36
View File
@@ -65,6 +65,42 @@ namespace KfChatDotNetBot
} }
await db.SaveChangesAsync(); await db.SaveChangesAsync();
} }
if (Path.Exists("tags.csv"))
{
logger.Info("Importing from tags.csv");
var tags = await File.ReadAllTextAsync("tags.csv");
var i = 0;
foreach (var row in tags.Split(Environment.NewLine))
{
i++;
var values = row.Split(",", StringSplitOptions.RemoveEmptyEntries);
if (values.Length < 2)
{
logger.Error($"Row {i} does not have enough columns");
continue;
}
var image = await db.Images.FirstOrDefaultAsync(image => image.Id == Convert.ToInt32(values[0]));
if (image == null)
{
logger.Error($"Row {i} has an unknown image ID");
continue;
}
var importTags = values[1].ToLower().Split(" ",
StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList();
if (importTags.Count == 0)
{
logger.Error($"Row {i} has no tags after splitting the string");
continue;
}
var newList = image.TagList.Concat(importTags).Distinct().ToList();
if (newList == image.TagList) continue;
image.TagList = newList;
}
await db.SaveChangesAsync();
}
logger.Info("Handing over to bot now"); logger.Info("Handing over to bot now");
Console.OutputEncoding = Encoding.UTF8; Console.OutputEncoding = Encoding.UTF8;
_ = new ChatBot(); _ = new ChatBot();
@@ -1,50 +1,51 @@
using KfChatDotNetBot.Migrations;
using MathNet.Numerics; using MathNet.Numerics;
using KfChatDotNetBot.Services;
using MathNet.Numerics.Distributions; using MathNet.Numerics.Distributions;
using RandN; using RandN;
using RandN.Compat; using RandN.Compat;
namespace KfChatDotNetBot.Commands.Kasino;
namespace KfChatDotNetBot.Services;
public static class Cecil public static class Cecil
{ {
private static RandomShim<StandardRng> _rand = RandomShim.Create<StandardRng>(StandardRng.Create()); private static readonly RandomShim<StandardRng> Rand = RandomShim.Create(StandardRng.Create());
public static double Consult(Skew skew, double minThreshold = 0) public static double Consult(Skew skew, double minThreshold = 0)
{ {
double r = _rand.NextDouble(); var r = Rand.NextDouble();
if (r < skew.LossRate) if (r < skew.LossRate)
{ {
return 0; return 0;
} }
double winRate = 1 - skew.LossRate; var winRate = 1 - skew.LossRate;
double scaledR = (r - skew.LossRate) / winRate; var scaledR = (r - skew.LossRate) / winRate;
double baseResult; double baseResult;
if (skew is BetaSkew betaSkew) switch (skew)
{ {
baseResult = Beta.InvCDF(betaSkew.Weight, betaSkew.Beta, scaledR) * betaSkew.CalibratedMaxWin; case BetaSkew betaSkew:
baseResult = Beta.InvCDF(betaSkew.Weight, betaSkew.Beta, scaledR) * betaSkew.CalibratedMaxWin;
break;
case GammaSkew gammaSkew:
baseResult = Gamma.InvCDF(gammaSkew.Weight, gammaSkew.Weight, scaledR);
break;
default:
return 0;
} }
else if (skew is GammaSkew gammaSkew)
{
baseResult = Gamma.InvCDF(gammaSkew.Weight, gammaSkew.Weight, scaledR);
}
else return 0;
baseResult /= winRate; baseResult /= winRate;
if (minThreshold == 0) return baseResult; if (minThreshold == 0) return baseResult;
double limit = (skew is BetaSkew b) ? b.MaxWin : double.MaxValue; var limit = (skew is BetaSkew b) ? b.MaxWin : double.MaxValue;
return Math.Min(Round(baseResult, minThreshold), limit); return Math.Min(Round(baseResult, minThreshold), limit);
double Round(double baseR, double minT) double Round(double baseR, double minT)
{ {
double lower = Math.Floor(baseR / minT) * minT; var lower = Math.Floor(baseR / minT) * minT;
double upper = lower + minT; var upper = lower + minT;
double roundChance = (baseR - lower) / minT; var roundChance = (baseR - lower) / minT;
return (_rand.NextDouble() < roundChance) ? lower : upper; return (Rand.NextDouble() < roundChance) ? lower : upper;
} }
} }
@@ -82,7 +83,7 @@ public class BetaSkew : Skew
public override void Calibrate(double winRate) public override void Calibrate(double winRate)
{ {
CalibratedMaxWin = MaxWin * winRate; CalibratedMaxWin = MaxWin * winRate;
double normalizer = TargetEv / CalibratedMaxWin; var normalizer = TargetEv / CalibratedMaxWin;
Alpha = normalizer * Weight; Alpha = normalizer * Weight;
Beta = (1 - normalizer) * Weight; Beta = (1 - normalizer) * Weight;
} }