mirror of
https://github.com/barelyprofessional/KfChatDotNet.git
synced 2026-06-15 16:55:18 -04:00
Adds Cecil (#119)
* Add Cecil for mechanics Used to predetermine the outcome of games according to a probability function, which is stored as a skew. Each game made using cecil is intended to have its own skew (or maybe multiple skews for different difficulties) Will eventually be used for planes 2 * Add CecilCommand for gambling functionality Skip all the fancy casino visuals and let Cecil take the wheel Customizeable difficulty !cecil <bet> <optional difficulty, default 1> <optional max win> https://i.ddos.lgbt/raw/CecilHelper.html * Validate max win value in CecilCommand Added validation to ensure max win is greater than 1. * Adjust slot symbol probabilities and random range 9208000 | Payout: 0 | RTP: 97.84% | Feach Chance: 0.78% | Hit Rate: 34.03% | Win Rate: 15.14% | Biggest Win: 14100.0x | Avg Win: 6.01x | Median Hit: 0.6x got some complaint about slots so figured I'd change it up a bit
This commit is contained in:
@@ -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<StandardRng> _rand = RandomShim.Create<StandardRng>(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;
|
||||
}
|
||||
}
|
||||
@@ -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<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+)?)", RegexOptions.IgnoreCase),
|
||||
new Regex(@"^cecil (?<bet>\d+(?:\.\d+)?)", RegexOptions.IgnoreCase),
|
||||
new Regex("^keno")
|
||||
];
|
||||
|
||||
public string? HelpText => "!cecil <bet> <optional difficulty> <optional max win>";
|
||||
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 <bet> <optional difficulty> <[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);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user