From 5f7b030a667165900fc72bc39472cab2061dc6ae Mon Sep 17 00:00:00 2001 From: Kwoth Date: Wed, 13 Jul 2022 02:21:32 +0200 Subject: [PATCH] More work on gambling --- NadekoBot.sln | 9 + .../Common => Nadeko.Common}/NadekoRandom.cs | 2 +- .../Gambling/Betflip/BetflipGame.cs | 28 ++ .../Gambling/Betflip/BetflipResult.cs | 7 + .../Gambling/Betroll/BetrollGame.cs | 42 ++ .../Gambling/Betroll/BetrollResult.cs | 9 + .../Gambling/Slot/SlotGame.cs | 22 +- src/Nadeko.Econ/Gambling/Slot/SlotResult.cs | 8 + .../Gambling/Wof/WofGame.cs} | 13 +- .../Gambling/Wof}/WofResult.cs | 5 +- src/Nadeko.Econ/GlobalUsings.cs | 1 + src/Nadeko.Econ/Nadeko.Econ.csproj | 13 + .../Administration/GreetBye/GreetService.cs | 10 + .../Modules/Gambling/EconomyResult.cs | 12 + .../Modules/Gambling/FlipCoin/FlipResult.cs | 7 + src/NadekoBot/Modules/Gambling/Gambling.cs | 19 +- .../Modules/Gambling/GamblingService.cs | 55 +-- .../Modules/Gambling/Slot/SlotCommands.cs | 413 ++---------------- .../Modules/Gambling/Slot/SlotResponse.cs | 10 - .../Modules/Gambling/~Shared/BetRoll.cs | 43 -- .../Modules/Gambling/~Shared/GamblingError.cs | 2 +- .../Gambling/~Shared/IGamblingService.cs | 14 + .../Gambling/~Shared/NewGamblingService.cs | 251 +++++++++++ src/NadekoBot/Modules/Xp/XpService.cs | 6 - src/NadekoBot/NadekoBot.csproj | 2 + 25 files changed, 481 insertions(+), 522 deletions(-) rename src/{NadekoBot/Common => Nadeko.Common}/NadekoRandom.cs (98%) create mode 100644 src/Nadeko.Econ/Gambling/Betflip/BetflipGame.cs create mode 100644 src/Nadeko.Econ/Gambling/Betflip/BetflipResult.cs create mode 100644 src/Nadeko.Econ/Gambling/Betroll/BetrollGame.cs create mode 100644 src/Nadeko.Econ/Gambling/Betroll/BetrollResult.cs rename src/{NadekoBot/Modules => Nadeko.Econ}/Gambling/Slot/SlotGame.cs (56%) create mode 100644 src/Nadeko.Econ/Gambling/Slot/SlotResult.cs rename src/{NadekoBot/Modules/Gambling/Wheel/WheelOfFortune.cs => Nadeko.Econ/Gambling/Wof/WofGame.cs} (64%) rename src/{NadekoBot/Modules/Gambling/Wheel => Nadeko.Econ/Gambling/Wof}/WofResult.cs (51%) create mode 100644 src/Nadeko.Econ/GlobalUsings.cs create mode 100644 src/Nadeko.Econ/Nadeko.Econ.csproj create mode 100644 src/NadekoBot/Modules/Gambling/EconomyResult.cs create mode 100644 src/NadekoBot/Modules/Gambling/FlipCoin/FlipResult.cs delete mode 100644 src/NadekoBot/Modules/Gambling/Slot/SlotResponse.cs delete mode 100644 src/NadekoBot/Modules/Gambling/~Shared/BetRoll.cs create mode 100644 src/NadekoBot/Modules/Gambling/~Shared/IGamblingService.cs create mode 100644 src/NadekoBot/Modules/Gambling/~Shared/NewGamblingService.cs diff --git a/NadekoBot.sln b/NadekoBot.sln index ba47a3446..024214cc4 100644 --- a/NadekoBot.sln +++ b/NadekoBot.sln @@ -32,6 +32,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nadeko.Medusa", "src\Nadeko EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nadeko.Common", "src\Nadeko.Common\Nadeko.Common.csproj", "{A6022F5F-A764-4D3F-847B-36F0391FF659}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nadeko.Econ", "src\Nadeko.Econ\Nadeko.Econ.csproj", "{4F4FBF7C-74F0-4AE4-B451-9E60BDCA9C37}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -87,6 +89,12 @@ Global {A6022F5F-A764-4D3F-847B-36F0391FF659}.GlobalNadeko|Any CPU.Build.0 = Debug|Any CPU {A6022F5F-A764-4D3F-847B-36F0391FF659}.Release|Any CPU.ActiveCfg = Release|Any CPU {A6022F5F-A764-4D3F-847B-36F0391FF659}.Release|Any CPU.Build.0 = Release|Any CPU + {4F4FBF7C-74F0-4AE4-B451-9E60BDCA9C37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F4FBF7C-74F0-4AE4-B451-9E60BDCA9C37}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F4FBF7C-74F0-4AE4-B451-9E60BDCA9C37}.GlobalNadeko|Any CPU.ActiveCfg = Debug|Any CPU + {4F4FBF7C-74F0-4AE4-B451-9E60BDCA9C37}.GlobalNadeko|Any CPU.Build.0 = Debug|Any CPU + {4F4FBF7C-74F0-4AE4-B451-9E60BDCA9C37}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F4FBF7C-74F0-4AE4-B451-9E60BDCA9C37}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -101,6 +109,7 @@ Global {3BC82CFE-BEE7-451F-986B-17EDD1570C4F} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2} {E685977E-31A4-46F4-A5D7-4E3E39E82E43} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2} {A6022F5F-A764-4D3F-847B-36F0391FF659} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2} + {4F4FBF7C-74F0-4AE4-B451-9E60BDCA9C37} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5F3F555C-855F-4BE8-B526-D062D3E8ACA4} diff --git a/src/NadekoBot/Common/NadekoRandom.cs b/src/Nadeko.Common/NadekoRandom.cs similarity index 98% rename from src/NadekoBot/Common/NadekoRandom.cs rename to src/Nadeko.Common/NadekoRandom.cs index 7094171b2..da7688824 100644 --- a/src/NadekoBot/Common/NadekoRandom.cs +++ b/src/Nadeko.Common/NadekoRandom.cs @@ -1,7 +1,7 @@ #nullable disable using System.Security.Cryptography; -namespace NadekoBot.Common; +namespace Nadeko.Common; public class NadekoRandom : Random { diff --git a/src/Nadeko.Econ/Gambling/Betflip/BetflipGame.cs b/src/Nadeko.Econ/Gambling/Betflip/BetflipGame.cs new file mode 100644 index 000000000..6b9446af9 --- /dev/null +++ b/src/Nadeko.Econ/Gambling/Betflip/BetflipGame.cs @@ -0,0 +1,28 @@ +namespace Nadeko.Econ.Gambling; + +public sealed class BetflipGame +{ + private readonly decimal _winMulti; + private readonly NadekoRandom _rng; + + public BetflipGame(decimal winMulti) + { + _winMulti = winMulti; + _rng = new NadekoRandom(); + } + + public BetflipResult Flip(int guess, decimal amount) + { + var side = _rng.Next(0, 1); + decimal won = 0; + + if (side == guess) + won = amount * _winMulti; + + return new BetflipResult() + { + Side = side, + Won = won, + }; + } +} \ No newline at end of file diff --git a/src/Nadeko.Econ/Gambling/Betflip/BetflipResult.cs b/src/Nadeko.Econ/Gambling/Betflip/BetflipResult.cs new file mode 100644 index 000000000..5faf94b8c --- /dev/null +++ b/src/Nadeko.Econ/Gambling/Betflip/BetflipResult.cs @@ -0,0 +1,7 @@ +namespace Nadeko.Econ.Gambling; + +public readonly struct BetflipResult +{ + public decimal Won { get; init; } + public int Side { get; init; } +} \ No newline at end of file diff --git a/src/Nadeko.Econ/Gambling/Betroll/BetrollGame.cs b/src/Nadeko.Econ/Gambling/Betroll/BetrollGame.cs new file mode 100644 index 000000000..6cfdb5b91 --- /dev/null +++ b/src/Nadeko.Econ/Gambling/Betroll/BetrollGame.cs @@ -0,0 +1,42 @@ +namespace Nadeko.Econ.Gambling; + +public sealed class BetrollGame +{ + private readonly (decimal WhenAbove, decimal MultiplyBy)[] _thresholdPairs; + private readonly Random _rng; + + public BetrollGame(IReadOnlyList<(decimal WhenAbove, decimal MultiplyBy)> pairs) + { + _thresholdPairs = pairs.OrderByDescending(x => x.WhenAbove).ToArray(); + _rng = new(); + } + + public BetrollResult Roll(decimal amount = 0) + { + var roll = _rng.Next(0, 101); + + for (var i = 0; i < _thresholdPairs.Length; i++) + { + ref var pair = ref _thresholdPairs[i]; + + if (pair.WhenAbove < roll) + { + return new() + { + Multiplier = pair.MultiplyBy, + Roll = roll, + Threshold = pair.WhenAbove, + Won = amount * pair.MultiplyBy + }; + } + } + + return new() + { + Multiplier = 0, + Roll = roll, + Threshold = -1, + Won = 0, + }; + } +} \ No newline at end of file diff --git a/src/Nadeko.Econ/Gambling/Betroll/BetrollResult.cs b/src/Nadeko.Econ/Gambling/Betroll/BetrollResult.cs new file mode 100644 index 000000000..88303d80a --- /dev/null +++ b/src/Nadeko.Econ/Gambling/Betroll/BetrollResult.cs @@ -0,0 +1,9 @@ +namespace Nadeko.Econ.Gambling; + +public readonly struct BetrollResult +{ + public int Roll { get; init; } + public decimal Multiplier { get; init; } + public decimal Threshold { get; init; } + public decimal Won { get; init; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Slot/SlotGame.cs b/src/Nadeko.Econ/Gambling/Slot/SlotGame.cs similarity index 56% rename from src/NadekoBot/Modules/Gambling/Slot/SlotGame.cs rename to src/Nadeko.Econ/Gambling/Slot/SlotGame.cs index f2000568a..5e18a75c1 100644 --- a/src/NadekoBot/Modules/Gambling/Slot/SlotGame.cs +++ b/src/Nadeko.Econ/Gambling/Slot/SlotGame.cs @@ -1,11 +1,10 @@ -#nullable disable -namespace NadekoBot.Modules.Gambling.Common.Slot; +namespace Nadeko.Econ.Gambling; public class SlotGame { private static readonly Random _rng = new NadekoRandom(); - public Result Spin() + public SlotResult Spin(decimal bet) { var rolls = new[] { _rng.Next(0, 6), _rng.Next(0, 6), _rng.Next(0, 6) }; var multi = 0; @@ -19,18 +18,11 @@ public class SlotGame else if (rolls.Any(x => x == 5)) multi = 1; - return new(multi, rolls); - } - - public class Result - { - public float Multiplier { get; } - public int[] Rolls { get; } - - public Result(float multiplier, int[] rolls) + return new() { - Multiplier = multiplier; - Rolls = rolls; - } + Won = bet * multi, + Multiplier = multi, + Rolls = rolls, + }; } } \ No newline at end of file diff --git a/src/Nadeko.Econ/Gambling/Slot/SlotResult.cs b/src/Nadeko.Econ/Gambling/Slot/SlotResult.cs new file mode 100644 index 000000000..14117f696 --- /dev/null +++ b/src/Nadeko.Econ/Gambling/Slot/SlotResult.cs @@ -0,0 +1,8 @@ +namespace Nadeko.Econ.Gambling; + +public readonly struct SlotResult +{ + public decimal Multiplier { get; init; } + public int[] Rolls { get; init; } + public decimal Won { get; init; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Wheel/WheelOfFortune.cs b/src/Nadeko.Econ/Gambling/Wof/WofGame.cs similarity index 64% rename from src/NadekoBot/Modules/Gambling/Wheel/WheelOfFortune.cs rename to src/Nadeko.Econ/Gambling/Wof/WofGame.cs index dabffac5c..4dc78406a 100644 --- a/src/NadekoBot/Modules/Gambling/Wheel/WheelOfFortune.cs +++ b/src/Nadeko.Econ/Gambling/Wof/WofGame.cs @@ -1,20 +1,19 @@ -#nullable disable -namespace NadekoBot.Modules.Gambling.WheelOfFortune; +namespace Nadeko.Econ.Gambling; -public sealed class WheelOfFortuneGame +public sealed class WofGame { public static IReadOnlyList DEFAULT_MULTIPLIERS = new[] { 1.7M, 1.5M, 0.2M, 0.1M, 0.3M, 0.5M, 1.2M, 2.4M }; private readonly IReadOnlyList _multipliers; private readonly NadekoRandom _rng; - public WheelOfFortuneGame(IReadOnlyList multipliers) + public WofGame(IReadOnlyList multipliers) { _multipliers = multipliers; _rng = new(); } - public WheelOfFortuneGame() : this(DEFAULT_MULTIPLIERS) + public WofGame() : this(DEFAULT_MULTIPLIERS) { } @@ -23,13 +22,13 @@ public sealed class WheelOfFortuneGame var result = _rng.Next(0, _multipliers.Count); var multi = _multipliers[result]; - var amount = (long)(bet * multi); + var amount = bet * multi; return new() { Index = result, Multiplier = multi, - Amount = amount + Won = amount }; } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Wheel/WofResult.cs b/src/Nadeko.Econ/Gambling/Wof/WofResult.cs similarity index 51% rename from src/NadekoBot/Modules/Gambling/Wheel/WofResult.cs rename to src/Nadeko.Econ/Gambling/Wof/WofResult.cs index 31eeedf3f..fe5717a5f 100644 --- a/src/NadekoBot/Modules/Gambling/Wheel/WofResult.cs +++ b/src/Nadeko.Econ/Gambling/Wof/WofResult.cs @@ -1,9 +1,8 @@ -#nullable disable -namespace NadekoBot.Modules.Gambling.WheelOfFortune; +namespace Nadeko.Econ.Gambling; public readonly struct WofResult { public int Index { get; init; } public decimal Multiplier { get; init; } - public long Amount { get; init; } + public decimal Won { get; init; } } \ No newline at end of file diff --git a/src/Nadeko.Econ/GlobalUsings.cs b/src/Nadeko.Econ/GlobalUsings.cs new file mode 100644 index 000000000..6d9d33d0f --- /dev/null +++ b/src/Nadeko.Econ/GlobalUsings.cs @@ -0,0 +1 @@ +global using Nadeko.Common; \ No newline at end of file diff --git a/src/Nadeko.Econ/Nadeko.Econ.csproj b/src/Nadeko.Econ/Nadeko.Econ.csproj new file mode 100644 index 000000000..30effaa55 --- /dev/null +++ b/src/Nadeko.Econ/Nadeko.Econ.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/src/NadekoBot/Modules/Administration/GreetBye/GreetService.cs b/src/NadekoBot/Modules/Administration/GreetBye/GreetService.cs index f3c089496..2e06950fa 100644 --- a/src/NadekoBot/Modules/Administration/GreetBye/GreetService.cs +++ b/src/NadekoBot/Modules/Administration/GreetBye/GreetService.cs @@ -191,6 +191,11 @@ public class GreetService : INService, IReadyExecutor if (conf.AutoDeleteByeMessagesTimer > 0) toDelete.DeleteAfter(conf.AutoDeleteByeMessagesTimer); } + catch (HttpException ex) when (ex.DiscordCode == DiscordErrorCode.InsufficientPermissions) + { + Log.Warning(ex, "Missing permissions to send a bye message, the bye message will be disabled on server: {GuildId}", channel.GuildId); + await SetBye(channel.GuildId, channel.Id, false); + } catch (Exception ex) { Log.Warning(ex, "Error embeding bye message"); @@ -219,6 +224,11 @@ public class GreetService : INService, IReadyExecutor if (conf.AutoDeleteGreetMessagesTimer > 0) toDelete.DeleteAfter(conf.AutoDeleteGreetMessagesTimer); } + catch (HttpException ex) when (ex.DiscordCode == DiscordErrorCode.InsufficientPermissions) + { + Log.Warning(ex, "Missing permissions to send a bye message, the greet message will be disabled on server: {GuildId}", channel.GuildId); + await SetGreet(channel.GuildId, channel.Id, false); + } catch (Exception ex) { Log.Warning(ex, "Error embeding greet message"); diff --git a/src/NadekoBot/Modules/Gambling/EconomyResult.cs b/src/NadekoBot/Modules/Gambling/EconomyResult.cs new file mode 100644 index 000000000..066915be4 --- /dev/null +++ b/src/NadekoBot/Modules/Gambling/EconomyResult.cs @@ -0,0 +1,12 @@ +#nullable disable +namespace NadekoBot.Modules.Gambling.Services; + +public sealed class EconomyResult +{ + public decimal Cash { get; init; } + public decimal Planted { get; init; } + public decimal Waifus { get; init; } + public decimal OnePercent { get; init; } + public decimal Bank { get; init; } + public long Bot { get; init; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/FlipCoin/FlipResult.cs b/src/NadekoBot/Modules/Gambling/FlipCoin/FlipResult.cs new file mode 100644 index 000000000..3a2af8f43 --- /dev/null +++ b/src/NadekoBot/Modules/Gambling/FlipCoin/FlipResult.cs @@ -0,0 +1,7 @@ +namespace Nadeko.Econ.Gambling; + +public readonly struct FlipResult +{ + public long Won { get; init; } + public int Side { get; init; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Gambling.cs b/src/NadekoBot/Modules/Gambling/Gambling.cs index 6f5dfe48e..2da1f9762 100644 --- a/src/NadekoBot/Modules/Gambling/Gambling.cs +++ b/src/NadekoBot/Modules/Gambling/Gambling.cs @@ -38,6 +38,7 @@ public partial class Gambling : GamblingModule Draw } + private readonly IGamblingService _gs; private readonly DbService _db; private readonly ICurrencyService _cs; private readonly DiscordSocketClient _client; @@ -51,6 +52,7 @@ public partial class Gambling : GamblingModule private IUserMessage rdMsg; public Gambling( + IGamblingService gs, DbService db, ICurrencyService currency, DiscordSocketClient client, @@ -61,6 +63,7 @@ public partial class Gambling : GamblingModule RemindService remind) : base(configService) { + _gs = gs; _db = db; _cs = currency; _client = client; @@ -657,27 +660,29 @@ public partial class Gambling : GamblingModule } } - private async Task InternallBetroll(long amount) + [Cmd] + public async partial Task BetRoll(ShmartNumber amount) { if (!await CheckBetMandatory(amount)) { return; } - if (!await _cs.RemoveAsync(ctx.User, amount, new("betroll", "bet"))) + var result = await _gs.BetRollAsync() + if (!) { await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); return; } - var br = new Betroll(Config.BetRoll); + var br = new BetrollGame(Config.BetRoll); var result = br.Roll(); + var win = (long)result.Won; var str = Format.Bold(ctx.User.ToString()) + Format.Code(GetText(strs.roll(result.Roll))); - if (result.Multiplier > 0) + if (win > 0) { - var win = (long)(amount * result.Multiplier); str += GetText(strs.br_win(N(win), result.Threshold + (result.Roll == 100 ? " 👑" : ""))); await _cs.AddAsync(ctx.User, win, new("betroll", "win")); } @@ -689,10 +694,6 @@ public partial class Gambling : GamblingModule await SendConfirmAsync(str); } - [Cmd] - public partial Task BetRoll(ShmartNumber amount) - => InternallBetroll(amount); - [Cmd] [NadekoOptions(typeof(LbOpts))] [Priority(0)] diff --git a/src/NadekoBot/Modules/Gambling/GamblingService.cs b/src/NadekoBot/Modules/Gambling/GamblingService.cs index 37f06f157..0f7d5db81 100644 --- a/src/NadekoBot/Modules/Gambling/GamblingService.cs +++ b/src/NadekoBot/Modules/Gambling/GamblingService.cs @@ -6,8 +6,6 @@ using NadekoBot.Db; using NadekoBot.Db.Models; using NadekoBot.Modules.Gambling.Common; using NadekoBot.Modules.Gambling.Common.Connect4; -using NadekoBot.Modules.Gambling.Common.Slot; -using NadekoBot.Modules.Gambling.Common.WheelOfFortune; namespace NadekoBot.Modules.Gambling.Services; @@ -22,6 +20,8 @@ public class GamblingService : INService, IReadyExecutor private readonly IBotCache _cache; private readonly GamblingConfigService _gss; + private static readonly TypedKey _curDecayKey = new("currency:last_decay"); + public GamblingService( DbService db, Bot bot, @@ -69,8 +69,7 @@ public class GamblingService : INService, IReadyExecutor } } } - - private static readonly TypedKey _curDecayKey = new("currency:last_decay"); + private async Task CurrencyDecayLoopAsync() { if (_bot.Client.ShardId != 0) @@ -131,40 +130,6 @@ public class GamblingService : INService, IReadyExecutor } } - public async Task SlotAsync(ulong userId, long amount) - { - var takeRes = await _cs.RemoveAsync(userId, amount, new("slot", "bet")); - - if (!takeRes) - { - return new() - { - Error = GamblingError.NotEnough - }; - } - - var game = new SlotGame(); - var result = game.Spin(); - long won = 0; - - if (result.Multiplier > 0) - { - won = (long)(result.Multiplier * amount); - - await _cs.AddAsync(userId, won, new("slot", "win", $"Slot Machine x{result.Multiplier}")); - } - - var toReturn = new SlotResponse - { - Multiplier = result.Multiplier, - Won = won - }; - - toReturn.Rolls.AddRange(result.Rolls); - - return toReturn; - } - private static readonly TypedKey _ecoKey = new("nadeko:economy"); public async Task GetEconomyAsync() @@ -198,9 +163,6 @@ public class GamblingService : INService, IReadyExecutor return data; } - public Task WheelOfFortuneSpinAsync(ulong userId, long bet) - => new WheelOfFortuneGame(userId, bet, _gss.Data, _cs).SpinAsync(); - private static readonly SemaphoreSlim _timelyLock = new (1, 1); @@ -250,15 +212,4 @@ public class GamblingService : INService, IReadyExecutor public async Task RemoveAllTimelyClaimsAsync() => await _cache.RemoveAsync(_timelyKey); - - - public readonly struct EconomyResult - { - public decimal Cash { get; init; } - public decimal Planted { get; init; } - public decimal Waifus { get; init; } - public decimal OnePercent { get; init; } - public decimal Bank { get; init; } - public long Bot { get; init; } - } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Slot/SlotCommands.cs b/src/NadekoBot/Modules/Gambling/Slot/SlotCommands.cs index ae38d88b7..dbab85738 100644 --- a/src/NadekoBot/Modules/Gambling/Slot/SlotCommands.cs +++ b/src/NadekoBot/Modules/Gambling/Slot/SlotCommands.cs @@ -8,363 +8,29 @@ using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using System.Text; -using Grpc.Core; -using NadekoBot.Modules.Gambling.WheelOfFortune; -using NadekoBot.Services.Currency; +using Nadeko.Econ.Gambling; using Color = SixLabors.ImageSharp.Color; using Image = SixLabors.ImageSharp.Image; -using OneOf; namespace NadekoBot.Modules.Gambling; -public enum SlotError +public enum GamblingError { InsufficientFunds, } -public enum WofError -{ - InsufficientFunds, -} - -public interface ISlotService -{ - ValueTask> PullAsync(ulong userId, long amount); -} - -public record struct WofRequest(ulong UserId, long Amount); - -public record struct BetrollRequest(ulong UserId, long Amount); - -public sealed class DefaultSlotService : INService -{ - private readonly GamblingConfigService _bcs; - private readonly ICurrencyService _cs; - // public ValueTask> PullAsync(ulong userId, long amount) - // { - // - // } - - public DefaultSlotService(GamblingConfigService bcs, ICurrencyService cs) - { - _bcs = bcs; - _cs = cs; - } - - public async Task> Wof(WofRequest request, ServerCallContext context) - { - var isTakeSuccess = await _cs.RemoveAsync(request.UserId, request.Amount, new TxData("wof", "bet")); - - if (!isTakeSuccess) - { - return WofError.InsufficientFunds; - } - - var game = new WheelOfFortuneGame(_bcs.Data.WheelOfFortune.Multipliers); - var result = game.Spin(request.Amount); - - if (result.Amount > 0) - { - await _cs.AddAsync(request.UserId, result.Amount, new("wof", "win")); - } - - return result; - } - - public override async Task> BetRoll(BetRollRequest request, ServerCallContext context) - { - var takeRes = await _currency.TransferCurrencyAsync(new TransferCurrencyRequest - { - Amount = request.Amount, - Type = "bet-roll", - Subtype = "bet", - FromId = request.UserId, - ToId = 0, - }); - - if (!takeRes.Success) - { - return new BetRollReply - { - Error = GamblingError.NotEnough - }; - } - - var game = new Betroll(_config.Data.BetRoll); - var result = game.Roll(); - - if (result.Multiplier > 0) - { - var won = (long)(request.Amount * result.Multiplier); - - await _currency.GrantToUserAsync(new GrantToUserRequest - { - Amount = won, - Type = "bet-roll", - Subtype = "won", - UserId = request.UserId, - GranterId = 0, - }); - - return new BetRollReply - { - WonAmount = won, - Multiplier = result.Multiplier, - Roll = result.Roll, - Threshold = result.Threshold, - }; - } - - return new BetRollReply - { - WonAmount = 0, - Multiplier = result.Multiplier, - Roll = result.Roll, - }; - } - - // public override async Task BetFlip(BetFlipRequest request, ServerCallContext context) - // { - // var takeRes = await _currency.TransferCurrencyAsync(new TransferCurrencyRequest - // { - // Amount = request.Amount, - // Type = "bet-flip", - // Subtype = "bet", - // FromId = request.UserId, - // ToId = 0, - // }); - // - // if (!takeRes.Success) - // { - // return new BetFlipReply - // { - // Error = GamblingError.NotEnough - // }; - // } - // - // var roll = _rng.Next(0, 1000) <= 499; - // long won = 0; - // - // if (roll == request.Guess) - // { - // won = (long) (_config.Data.Multipliers.BetFlip * request.Amount); - // - // await _currency.GrantToUserAsync(new GrantToUserRequest - // { - // Amount = won, - // Type = "bet-flip", - // Subtype = "won", - // UserId = request.UserId, - // GranterId = 0, - // }); - // } - // - // - // return new BetFlipReply - // { - // Result = roll - // ? BetFlipReply.Types.Side.Heads - // : BetFlipReply.Types.Side.Tails, - // WonAmount = won - // }; - // } - // - // public override Task Flip(FlipRequest request, ServerCallContext context) - // { - // if (request.Count <= 0) - // throw new RpcException(new Status(StatusCode.InvalidArgument, "Count has to be greater than 0.")); - // - // var results = Enumerable.Range(0, request.Count) - // .Select(x => (FlipReply.Types.Roll) _rng.Next(0, 2)); - // - // var toReturn = new FlipReply(); - // toReturn.Rolls.AddRange(results); - // return Task.FromResult(toReturn); - // } - // - // public override async Task Slot(SlotRequest request, ServerCallContext context) - // { - // var takeRes = await _currency.TransferCurrencyAsync(new TransferCurrencyRequest - // { - // Amount = request.Amount, - // Type = "slot", - // Subtype = "bet", - // FromId = request.UserId, - // ToId = 0, - // }); - // - // if (!takeRes.Success) - // { - // return new SlotResponse - // { - // Error = GamblingError.NotEnough - // }; - // } - // - // var game = new SlotGame(); - // var result = game.Spin(); - // long won = 0; - // - // if (result.Multiplier > 0) - // { - // won = (long) (result.Multiplier * request.Amount); - // - // await _currency.GrantToUserAsync(new GrantToUserRequest - // { - // Amount = won, - // Type = "slot", - // Subtype = "won", - // UserId = request.UserId, - // GranterId = 0, - // }); - // } - // - // var toReturn = new SlotResponse - // { - // Multiplier = result.Multiplier, - // Won = won, - // }; - // - // toReturn.Rolls.AddRange(result.Rolls); - // - // return toReturn; - // } - // - // private readonly ConcurrentDictionary _decks = new ConcurrentDictionary(); - // - // public override Task DeckShuffle(DeckShuffleRequest request, ServerCallContext context) - // { - // _decks.AddOrUpdate(request.Id, new Deck(), (key, old) => new Deck()); - // return Task.FromResult(new DeckShuffleReply { }); - // } - // - // public override Task DeckDraw(DeckDrawRequest request, ServerCallContext context) - // { - // if (request.Count < 1 || request.Count > 10) - // throw new ArgumentOutOfRangeException(nameof(request.Id)); - // - // var deck = request.UseNew - // ? new Deck() - // : _decks.GetOrAdd(request.Id, new Deck()); - // - // var list = new List(request.Count); - // for (int i = 0; i < request.Count; i++) - // { - // var card = deck.DrawNoRestart(); - // if (card is null) - // { - // if (i == 0) - // { - // deck.Restart(); - // list.Add(deck.DrawNoRestart()); - // continue; - // } - // - // break; - // } - // - // list.Add(card); - // } - // - // // todo 3.2 should replace all "placeholder" words in command strings with a link to the placeholder list explanation - // var cards = list - // .Select(x => new Card - // { - // Name = x.ToString().ToLowerInvariant().Replace(' ', '_'), - // Number = x.Number, - // Suit = (CardSuit) x.Suit - // }); - // - // var toReturn = new DeckDrawReply(); - // toReturn.Cards.AddRange(cards); - // - // return Task.FromResult(toReturn); - // } - // - // public override async Task Rps(RpsRequest request, ServerCallContext context) - // { - // if (request.Amount > 0) - // { - // var res = await _currency.TransferCurrencyAsync(new TransferCurrencyRequest - // { - // Amount = request.Amount, - // FromId = request.UserId, - // Type = "rps", - // Subtype = "bet", - // }); - // - // if (!res.Success) - // { - // return new RpsReply - // { - // Result = RpsReply.Types.ResultType.NotEnough - // }; - // } - // } - // - // var botPick = _rng.Next(0, 3); - // var userPick = (int) request.Pick; - // - // if (botPick == userPick) - // { - // if (request.Amount > 0) - // { - // await _currency.GrantToUserAsync(new GrantToUserRequest - // { - // Amount = request.Amount, - // GranterId = 0, - // Type = "rps", - // Subtype = "draw", - // UserId = request.UserId, - // }); - // } - // - // return new RpsReply - // { - // BotPick = (RpsPick) botPick, - // WonAmount = request.Amount, - // Result = RpsReply.Types.ResultType.Draw - // }; - // } - // - // if ((botPick == 1 && userPick == 2) || (botPick == 2 && userPick == 0) || (botPick == 0 && userPick == 1)) - // { - // if (request.Amount > 0) - // { - // await _currency.GrantToUserAsync(new GrantToUserRequest - // { - // Amount = (long) (request.Amount * 1.95f), - // GranterId = 0, - // Type = "rps", - // Subtype = "draw", - // UserId = request.UserId, - // }); - // } - // - // return new RpsReply - // { - // BotPick = (RpsPick) botPick, - // WonAmount = (long) (request.Amount * 1.95f), - // Result = RpsReply.Types.ResultType.Won - // }; - // } - // - // return new RpsReply - // { - // BotPick = (RpsPick) botPick, - // WonAmount = 0, - // Result = RpsReply.Types.ResultType.Lost - // }; - // } -} +// public interface ISlotService +// { +// ValueTask> PullAsync(ulong userId, long amount); +// } public partial class Gambling { [Group] - public partial class SlotCommands : GamblingSubmodule + public partial class SlotCommands : GamblingSubmodule { - private static long totalBet; - private static long totalPaidOut; + private static decimal totalBet; + private static decimal totalPaidOut; private static readonly HashSet _runningUsers = new(); @@ -375,6 +41,7 @@ public partial class Gambling private readonly IImageCache _images; private readonly FontProvider _fonts; private readonly DbService _db; + private object _slotStatsLock = new(); public SlotCommands( ImageCache images, @@ -405,9 +72,9 @@ public partial class Gambling var embed = _eb.Create() .WithOkColor() .WithTitle("Slot Stats") - .AddField("Total Bet", bet.ToString(), true) - .AddField("Paid Out", paid.ToString(), true) - .WithFooter($"Payout Rate: {paid * 1.0 / bet * 100:f4}%"); + .AddField("Total Bet", N(bet), true) + .AddField("Paid Out", N(paid), true) + .WithFooter($"Payout Rate: {paid * 1.0M / bet * 100:f4}%"); await ctx.Channel.EmbedAsync(embed); } @@ -419,10 +86,10 @@ public partial class Gambling if (tests <= 0) return; //multi vs how many times it occured - var dict = new Dictionary(); + var dict = new Dictionary(); for (var i = 0; i < tests; i++) { - var res = SlotMachine.Pull(); + var res = SlotMachine.Pull(0); if (dict.ContainsKey(res.Multiplier)) dict[res.Multiplier] += 1; else @@ -430,7 +97,7 @@ public partial class Gambling } var sb = new StringBuilder(); - var payout = 0; + decimal payout = 0; foreach (var key in dict.Keys.OrderByDescending(x => x)) { sb.AppendLine($"x{key} occured {dict[key]} times. {dict[key] * 1.0f / tests * 100}%"); @@ -439,7 +106,7 @@ public partial class Gambling await SendConfirmAsync("Slot Test Results", sb.ToString(), - footer: $"Total Bet: {tests} | Payout: {payout} | {payout * 1.0f / tests * 100}%"); + footer: $"Total Bet: {tests} | Payout: {payout:F0} | {payout * 1.0M / tests * 100}%"); } [Cmd] @@ -455,18 +122,21 @@ public partial class Gambling await ctx.Channel.TriggerTypingAsync(); - var result = await _service.SlotAsync(ctx.User.Id, amount); + var maybeResult = await _service.SlotAsync(ctx.User.Id, amount); - if (result.Error != GamblingError.None) + if (!maybeResult.TryPickT0(out var result, out var error)) { - if (result.Error == GamblingError.NotEnough) + if (error == GamblingError.InsufficientFunds) await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); return; } - Interlocked.Add(ref totalBet, amount); - Interlocked.Add(ref totalPaidOut, result.Won); + lock (_slotStatsLock) + { + totalBet += amount; + totalPaidOut += result.Won; + } long ownedAmount; await using (var uow = _db.GetDbContext()) @@ -537,13 +207,13 @@ public partial class Gambling var msg = GetText(strs.better_luck); if (result.Multiplier > 0) { - if (Math.Abs(result.Multiplier - 1f) <= float.Epsilon) + if (Math.Abs(result.Multiplier - 1M) <= decimal.) msg = GetText(strs.slot_single(CurrencySign, 1)); - else if (Math.Abs(result.Multiplier - 4f) < float.Epsilon) + else if (Math.Abs(result.Multiplier - 4M) < float.Epsilon) msg = GetText(strs.slot_two(CurrencySign, 4)); - else if (Math.Abs(result.Multiplier - 10f) <= float.Epsilon) + else if (Math.Abs(result.Multiplier - 10M) <= float.Epsilon) msg = GetText(strs.slot_three(10)); - else if (Math.Abs(result.Multiplier - 30f) <= float.Epsilon) + else if (Math.Abs(result.Multiplier - 30M) <= float.Epsilon) msg = GetText(strs.slot_jackpot(30)); } @@ -576,19 +246,19 @@ public sealed class SlotMachine //three flowers arr => arr.All(a => a == MAX_VALUE) ? 30 : 0, //three of the same - arr => !arr.Any(a => a != arr[0]) ? 10 : 0, + arr => arr.All(a => a == arr[0]) ? 10 : 0, //two flowers arr => arr.Count(a => a == MAX_VALUE) == 2 ? 4 : 0, //one flower arr => arr.Any(a => a == MAX_VALUE) ? 1 : 0 }; - public static SlotResult Pull() + public static SlotResult Pull(decimal amount) { var numbers = new int[3]; for (var i = 0; i < numbers.Length; i++) numbers[i] = new NadekoRandom().Next(0, MAX_VALUE + 1); - var multi = 0; + var multi = 0M; foreach (var t in _winningCombos) { multi = t(numbers); @@ -596,18 +266,11 @@ public sealed class SlotMachine break; } - return new(numbers, multi); - } -} - -public struct SlotResult -{ - public int[] Numbers { get; } - public int Multiplier { get; } - - public SlotResult(int[] nums, int multi) - { - Numbers = nums; - Multiplier = multi; + return new() + { + Rolls = numbers, + Multiplier = multi, + Won = multi * amount, + }; } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Slot/SlotResponse.cs b/src/NadekoBot/Modules/Gambling/Slot/SlotResponse.cs deleted file mode 100644 index 6d1770652..000000000 --- a/src/NadekoBot/Modules/Gambling/Slot/SlotResponse.cs +++ /dev/null @@ -1,10 +0,0 @@ -#nullable disable -namespace NadekoBot.Modules.Gambling; - -public class SlotResponse -{ - public float Multiplier { get; set; } - public long Won { get; set; } - public List Rolls { get; set; } = new(); - public GamblingError Error { get; set; } -} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/~Shared/BetRoll.cs b/src/NadekoBot/Modules/Gambling/~Shared/BetRoll.cs deleted file mode 100644 index 578698a42..000000000 --- a/src/NadekoBot/Modules/Gambling/~Shared/BetRoll.cs +++ /dev/null @@ -1,43 +0,0 @@ -#nullable disable -namespace NadekoBot.Modules.Gambling.Common; - -public class Betroll -{ - private readonly IOrderedEnumerable _thresholdPairs; - private readonly Random _rng; - - public Betroll(IReadOnlyList pairs) - { - _thresholdPairs = settings.Pairs.OrderByDescending(x => x.WhenAbove); - _rng = new(); - } - - public Result Roll() - { - var roll = _rng.Next(0, 101); - - var pair = _thresholdPairs.FirstOrDefault(x => x.WhenAbove < roll); - if (pair is null) - { - return new() - { - Multiplier = 0, - Roll = roll - }; - } - - return new() - { - Multiplier = pair.MultiplyBy, - Roll = roll, - Threshold = pair.WhenAbove - }; - } - - public class Result - { - public int Roll { get; set; } - public float Multiplier { get; set; } - public int Threshold { get; set; } - } -} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/~Shared/GamblingError.cs b/src/NadekoBot/Modules/Gambling/~Shared/GamblingError.cs index cf74c09b7..e466f1960 100644 --- a/src/NadekoBot/Modules/Gambling/~Shared/GamblingError.cs +++ b/src/NadekoBot/Modules/Gambling/~Shared/GamblingError.cs @@ -1,7 +1,7 @@ #nullable disable namespace NadekoBot.Modules.Gambling; -public enum GamblingError +public enum OldGamblingError { None, NotEnough diff --git a/src/NadekoBot/Modules/Gambling/~Shared/IGamblingService.cs b/src/NadekoBot/Modules/Gambling/~Shared/IGamblingService.cs new file mode 100644 index 000000000..d1b27bb8b --- /dev/null +++ b/src/NadekoBot/Modules/Gambling/~Shared/IGamblingService.cs @@ -0,0 +1,14 @@ +#nullable disable +using Nadeko.Econ.Gambling; +using OneOf; + +namespace NadekoBot.Modules.Gambling; + +public interface IGamblingService +{ + Task> WofAsync(ulong userId, long amount); + Task> BetRollAsync(ulong userId, long amount); + Task> BetFlipAsync(ulong userId, long amount, int guess); + Task> SlotAsync(ulong userId, long amount); + Task FlipAsync(int count); +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/~Shared/NewGamblingService.cs b/src/NadekoBot/Modules/Gambling/~Shared/NewGamblingService.cs new file mode 100644 index 000000000..f4358309e --- /dev/null +++ b/src/NadekoBot/Modules/Gambling/~Shared/NewGamblingService.cs @@ -0,0 +1,251 @@ +#nullable disable +using Nadeko.Econ.Gambling; +using NadekoBot.Modules.Gambling.Services; +using OneOf; + +namespace NadekoBot.Modules.Gambling; + +public sealed class NewGamblingService : IGamblingService, INService +{ + private readonly GamblingConfigService _bcs; + private readonly ICurrencyService _cs; + + public NewGamblingService(GamblingConfigService bcs, ICurrencyService cs) + { + _bcs = bcs; + _cs = cs; + } + + // todo input checks + public async Task> WofAsync(ulong userId, long amount) + { + var isTakeSuccess = await _cs.RemoveAsync(userId, amount, new("wof", "bet")); + + if (!isTakeSuccess) + { + return GamblingError.InsufficientFunds; + } + + var game = new WofGame(_bcs.Data.WheelOfFortune.Multipliers); + var result = game.Spin(amount); + + var won = (long)result.Won; + if (won > 0) + { + await _cs.AddAsync(userId, won, new("wof", "win")); + } + + return result; + } + + public async Task> BetRollAsync(ulong userId, long amount) + { + var isTakeSuccess = await _cs.RemoveAsync(userId, amount, new("betroll", "bet")); + + if (!isTakeSuccess) + { + return GamblingError.InsufficientFunds; + } + + var game = new BetrollGame(_bcs.Data.BetRoll.Pairs + .Select(x => ((decimal)x.WhenAbove, (decimal)x.MultiplyBy)) + .ToList()); + var result = game.Roll(amount); + + var won = (long)result.Won; + if (won > 0) + { + await _cs.AddAsync(userId, won, new("betroll", "win")); + } + + return result; + } + + public async Task> BetFlipAsync(ulong userId, long amount, int guess) + { + var isTakeSuccess = await _cs.RemoveAsync(userId, amount, new("betflip", "bet")); + + if (!isTakeSuccess) + { + return GamblingError.InsufficientFunds; + } + + var game = new BetflipGame(_bcs.Data.BetFlip.Multiplier); + var result = game.Flip(guess, amount); + + var won = (long)result.Won; + if (won > 0) + { + await _cs.AddAsync(userId, won, new("betflip", "win")); + } + + return result; + } + + public async Task> SlotAsync(ulong userId, long amount) + { + var isTakeSuccess = await _cs.RemoveAsync(userId, amount, new("slot", "bet")); + + if (!isTakeSuccess) + { + return GamblingError.InsufficientFunds; + } + + var game = new SlotGame(); + var result = game.Spin(amount); + + var won = (long)result.Won; + if (won > 0) + { + await _cs.AddAsync(userId, won, new("slot", "won")); + } + + return result; + } + + public Task FlipAsync(int count) + { + var game = new BetflipGame(0); + + var results = new FlipResult[count]; + for (var i = 0; i < count; i++) + { + results[i] = new() + { + Side = game.Flip(0, 0).Side + }; + } + + return Task.FromResult(results); + } + + // todo deck draw black/white? + + // + // private readonly ConcurrentDictionary _decks = new ConcurrentDictionary(); + // + // public override Task DeckShuffle(DeckShuffleRequest request, ServerCallContext context) + // { + // _decks.AddOrUpdate(request.Id, new Deck(), (key, old) => new Deck()); + // return Task.FromResult(new DeckShuffleReply { }); + // } + // + // public override Task DeckDraw(DeckDrawRequest request, ServerCallContext context) + // { + // if (request.Count < 1 || request.Count > 10) + // throw new ArgumentOutOfRangeException(nameof(request.Id)); + // + // var deck = request.UseNew + // ? new Deck() + // : _decks.GetOrAdd(request.Id, new Deck()); + // + // var list = new List(request.Count); + // for (int i = 0; i < request.Count; i++) + // { + // var card = deck.DrawNoRestart(); + // if (card is null) + // { + // if (i == 0) + // { + // deck.Restart(); + // list.Add(deck.DrawNoRestart()); + // continue; + // } + // + // break; + // } + // + // list.Add(card); + // } + // + // // todo 3.2 should replace all "placeholder" words in command strings with a link to the placeholder list explanation + // var cards = list + // .Select(x => new Card + // { + // Name = x.ToString().ToLowerInvariant().Replace(' ', '_'), + // Number = x.Number, + // Suit = (CardSuit) x.Suit + // }); + // + // var toReturn = new DeckDrawReply(); + // toReturn.Cards.AddRange(cards); + // + // return Task.FromResult(toReturn); + // } + // + // public override async Task Rps(RpsRequest request, ServerCallContext context) + // { + // if (request.Amount > 0) + // { + // var res = await _currency.TransferCurrencyAsync(new TransferCurrencyRequest + // { + // Amount = request.Amount, + // FromId = request.UserId, + // Type = "rps", + // Subtype = "bet", + // }); + // + // if (!res.Success) + // { + // return new RpsReply + // { + // Result = RpsReply.Types.ResultType.NotEnough + // }; + // } + // } + // + // var botPick = _rng.Next(0, 3); + // var userPick = (int) request.Pick; + // + // if (botPick == userPick) + // { + // if (request.Amount > 0) + // { + // await _currency.GrantToUserAsync(new GrantToUserRequest + // { + // Amount = request.Amount, + // GranterId = 0, + // Type = "rps", + // Subtype = "draw", + // UserId = request.UserId, + // }); + // } + // + // return new RpsReply + // { + // BotPick = (RpsPick) botPick, + // WonAmount = request.Amount, + // Result = RpsReply.Types.ResultType.Draw + // }; + // } + // + // if ((botPick == 1 && userPick == 2) || (botPick == 2 && userPick == 0) || (botPick == 0 && userPick == 1)) + // { + // if (request.Amount > 0) + // { + // await _currency.GrantToUserAsync(new GrantToUserRequest + // { + // Amount = (long) (request.Amount * 1.95f), + // GranterId = 0, + // Type = "rps", + // Subtype = "draw", + // UserId = request.UserId, + // }); + // } + // + // return new RpsReply + // { + // BotPick = (RpsPick) botPick, + // WonAmount = (long) (request.Amount * 1.95f), + // Result = RpsReply.Types.ResultType.Won + // }; + // } + // + // return new RpsReply + // { + // BotPick = (RpsPick) botPick, + // WonAmount = 0, + // Result = RpsReply.Types.ResultType.Lost + // }; + // } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Xp/XpService.cs b/src/NadekoBot/Modules/Xp/XpService.cs index d832e56cd..2373bf430 100644 --- a/src/NadekoBot/Modules/Xp/XpService.cs +++ b/src/NadekoBot/Modules/Xp/XpService.cs @@ -1281,10 +1281,4 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand uow.RemoveRange(guildConfig.XpSettings.CurrencyRewards); await uow.SaveChangesAsync(); } - - private enum NotifOf - { - Server, - Global - } // is it a server level-up or global level-up notification } \ No newline at end of file diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index 27e263644..0ca214a23 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -102,6 +102,7 @@ + @@ -125,6 +126,7 @@ +