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
This commit is contained in:
alogindtractor
2026-06-09 21:39:10 -07:00
committed by GitHub
parent 80916c9b2d
commit 2b61433dc6
4 changed files with 241 additions and 3 deletions
@@ -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;
+169
View File
@@ -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<Regex> Patterns =>
[
new Regex(@"^shop difficulty (?<betstr>.*)$", 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<Regex> Patterns =>
+48 -1
View File
@@ -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);
}
/// <summary>
/// Parses a serialized configuration token string from the HTML configuration utility
/// and instantiates a fully calibrated Skew state profile.
/// </summary>
/// <param name="token">Example formats: "B:0.5:50:0:0.95" or "G:1.2:0.05:1.0"</param>
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
+1
View File
@@ -1050,6 +1050,7 @@ public class KasinoShop
public string name;
private decimal CryptoBalance;
public decimal OutstandingLoanBalance;
public string Difficulty = "";
public Dictionary<int, Asset> Assets;
public Dictionary<int, Loan> Loans = new();
public decimal[] SponsorWagerLock = new decimal[2]; //[0] is how much you've wagered against your wager requirement, [1] is the wager requirement