diff --git a/src/NadekoBot/Modules/Gambling/Slot/SlotCommands.cs b/src/NadekoBot/Modules/Gambling/Slot/SlotCommands.cs index 2cc43ee6c..ae38d88b7 100644 --- a/src/NadekoBot/Modules/Gambling/Slot/SlotCommands.cs +++ b/src/NadekoBot/Modules/Gambling/Slot/SlotCommands.cs @@ -8,12 +8,356 @@ using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using System.Text; -using Nadeko.Common; +using Grpc.Core; +using NadekoBot.Modules.Gambling.WheelOfFortune; +using NadekoBot.Services.Currency; using Color = SixLabors.ImageSharp.Color; using Image = SixLabors.ImageSharp.Image; +using OneOf; namespace NadekoBot.Modules.Gambling; +public enum SlotError +{ + 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 partial class Gambling { [Group] @@ -220,50 +564,50 @@ public partial class Gambling }); } } + } +} - public sealed class SlotMachine +public sealed class SlotMachine +{ + public const int MAX_VALUE = 5; + + private static readonly List> _winningCombos = new() + { + //three flowers + arr => arr.All(a => a == MAX_VALUE) ? 30 : 0, + //three of the same + arr => !arr.Any(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() + { + 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; + foreach (var t in _winningCombos) { - public const int MAX_VALUE = 5; - - private static readonly List> _winningCombos = new() - { - //three flowers - arr => arr.All(a => a == MAX_VALUE) ? 30 : 0, - //three of the same - arr => !arr.Any(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() - { - 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; - foreach (var t in _winningCombos) - { - multi = t(numbers); - if (multi != 0) - 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; - } - } + multi = t(numbers); + if (multi != 0) + 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; } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Wheel/WheelOfFortune.cs b/src/NadekoBot/Modules/Gambling/Wheel/WheelOfFortune.cs new file mode 100644 index 000000000..dabffac5c --- /dev/null +++ b/src/NadekoBot/Modules/Gambling/Wheel/WheelOfFortune.cs @@ -0,0 +1,35 @@ +#nullable disable +namespace NadekoBot.Modules.Gambling.WheelOfFortune; + +public sealed class WheelOfFortuneGame +{ + 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) + { + _multipliers = multipliers; + _rng = new(); + } + + public WheelOfFortuneGame() : this(DEFAULT_MULTIPLIERS) + { + } + + public WofResult Spin(long bet) + { + var result = _rng.Next(0, _multipliers.Count); + + var multi = _multipliers[result]; + var amount = (long)(bet * multi); + + return new() + { + Index = result, + Multiplier = multi, + Amount = amount + }; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Wheel/WofResult.cs b/src/NadekoBot/Modules/Gambling/Wheel/WofResult.cs new file mode 100644 index 000000000..31eeedf3f --- /dev/null +++ b/src/NadekoBot/Modules/Gambling/Wheel/WofResult.cs @@ -0,0 +1,9 @@ +#nullable disable +namespace NadekoBot.Modules.Gambling.WheelOfFortune; + +public readonly struct WofResult +{ + public int Index { get; init; } + public decimal Multiplier { get; init; } + public long Amount { get; init; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/WheelOfFortune.cs b/src/NadekoBot/Modules/Gambling/WheelOfFortune.cs deleted file mode 100644 index 14a9ed4fb..000000000 --- a/src/NadekoBot/Modules/Gambling/WheelOfFortune.cs +++ /dev/null @@ -1,46 +0,0 @@ -#nullable disable -namespace NadekoBot.Modules.Gambling.Common.WheelOfFortune; - -public class WheelOfFortuneGame -{ - private readonly NadekoRandom _rng; - private readonly ICurrencyService _cs; - private readonly long _bet; - private readonly GamblingConfig _config; - private readonly ulong _userId; - - public WheelOfFortuneGame( - ulong userId, - long bet, - GamblingConfig config, - ICurrencyService cs) - { - _rng = new(); - _cs = cs; - _bet = bet; - _config = config; - _userId = userId; - } - - public async Task SpinAsync() - { - var result = _rng.Next(0, _config.WheelOfFortune.Multipliers.Length); - - var amount = (long)(_bet * _config.WheelOfFortune.Multipliers[result]); - - if (amount > 0) - await _cs.AddAsync(_userId, amount, new("wheel", "win")); - - return new() - { - Index = result, - Amount = amount - }; - } - - public class Result - { - public int Index { get; set; } - public long Amount { 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 index adbfea188..578698a42 100644 --- a/src/NadekoBot/Modules/Gambling/~Shared/BetRoll.cs +++ b/src/NadekoBot/Modules/Gambling/~Shared/BetRoll.cs @@ -6,7 +6,7 @@ public class Betroll private readonly IOrderedEnumerable _thresholdPairs; private readonly Random _rng; - public Betroll(BetRollConfig settings) + public Betroll(IReadOnlyList pairs) { _thresholdPairs = settings.Pairs.OrderByDescending(x => x.WhenAbove); _rng = new();