mirror of
				https://gitlab.com/Kwoth/nadekobot.git
				synced 2025-11-04 00:34:26 -05:00 
			
		
		
		
	More work on gambling
This commit is contained in:
		@@ -1,69 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Security.Cryptography;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Common;
 | 
			
		||||
 | 
			
		||||
public class NadekoRandom : Random
 | 
			
		||||
{
 | 
			
		||||
    private readonly RandomNumberGenerator _rng;
 | 
			
		||||
 | 
			
		||||
    public NadekoRandom()
 | 
			
		||||
        => _rng = RandomNumberGenerator.Create();
 | 
			
		||||
 | 
			
		||||
    public override int Next()
 | 
			
		||||
    {
 | 
			
		||||
        var bytes = new byte[sizeof(int)];
 | 
			
		||||
        _rng.GetBytes(bytes);
 | 
			
		||||
        return Math.Abs(BitConverter.ToInt32(bytes, 0));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override int Next(int maxValue)
 | 
			
		||||
    {
 | 
			
		||||
        if (maxValue <= 0)
 | 
			
		||||
            throw new ArgumentOutOfRangeException(nameof(maxValue));
 | 
			
		||||
        var bytes = new byte[sizeof(int)];
 | 
			
		||||
        _rng.GetBytes(bytes);
 | 
			
		||||
        return Math.Abs(BitConverter.ToInt32(bytes, 0)) % maxValue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override int Next(int minValue, int maxValue)
 | 
			
		||||
    {
 | 
			
		||||
        if (minValue > maxValue)
 | 
			
		||||
            throw new ArgumentOutOfRangeException(nameof(maxValue));
 | 
			
		||||
        if (minValue == maxValue)
 | 
			
		||||
            return minValue;
 | 
			
		||||
        var bytes = new byte[sizeof(int)];
 | 
			
		||||
        _rng.GetBytes(bytes);
 | 
			
		||||
        var sign = Math.Sign(BitConverter.ToInt32(bytes, 0));
 | 
			
		||||
        return (sign * BitConverter.ToInt32(bytes, 0) % (maxValue - minValue)) + minValue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public long NextLong(long minValue, long maxValue)
 | 
			
		||||
    {
 | 
			
		||||
        if (minValue > maxValue)
 | 
			
		||||
            throw new ArgumentOutOfRangeException(nameof(maxValue));
 | 
			
		||||
        if (minValue == maxValue)
 | 
			
		||||
            return minValue;
 | 
			
		||||
        var bytes = new byte[sizeof(long)];
 | 
			
		||||
        _rng.GetBytes(bytes);
 | 
			
		||||
        var sign = Math.Sign(BitConverter.ToInt64(bytes, 0));
 | 
			
		||||
        return (sign * BitConverter.ToInt64(bytes, 0) % (maxValue - minValue)) + minValue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override void NextBytes(byte[] buffer)
 | 
			
		||||
        => _rng.GetBytes(buffer);
 | 
			
		||||
 | 
			
		||||
    protected override double Sample()
 | 
			
		||||
    {
 | 
			
		||||
        var bytes = new byte[sizeof(double)];
 | 
			
		||||
        _rng.GetBytes(bytes);
 | 
			
		||||
        return Math.Abs((BitConverter.ToDouble(bytes, 0) / double.MaxValue) + 1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override double NextDouble()
 | 
			
		||||
    {
 | 
			
		||||
        var bytes = new byte[sizeof(double)];
 | 
			
		||||
        _rng.GetBytes(bytes);
 | 
			
		||||
        return BitConverter.ToDouble(bytes, 0);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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");
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								src/NadekoBot/Modules/Gambling/EconomyResult.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/NadekoBot/Modules/Gambling/EconomyResult.cs
									
									
									
									
									
										Normal file
									
								
							@@ -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; }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								src/NadekoBot/Modules/Gambling/FlipCoin/FlipResult.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/NadekoBot/Modules/Gambling/FlipCoin/FlipResult.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
namespace Nadeko.Econ.Gambling;
 | 
			
		||||
 | 
			
		||||
public readonly struct FlipResult
 | 
			
		||||
{
 | 
			
		||||
    public long Won { get; init; }
 | 
			
		||||
    public int Side { get; init; }
 | 
			
		||||
}
 | 
			
		||||
@@ -38,6 +38,7 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
        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<GamblingService>
 | 
			
		||||
    private IUserMessage rdMsg;
 | 
			
		||||
 | 
			
		||||
    public Gambling(
 | 
			
		||||
        IGamblingService gs,
 | 
			
		||||
        DbService db,
 | 
			
		||||
        ICurrencyService currency,
 | 
			
		||||
        DiscordSocketClient client,
 | 
			
		||||
@@ -61,6 +63,7 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
        RemindService remind)
 | 
			
		||||
        : base(configService)
 | 
			
		||||
    {
 | 
			
		||||
        _gs = gs;
 | 
			
		||||
        _db = db;
 | 
			
		||||
        _cs = currency;
 | 
			
		||||
        _client = client;
 | 
			
		||||
@@ -657,27 +660,29 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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<GamblingService>
 | 
			
		||||
        await SendConfirmAsync(str);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    public partial Task BetRoll(ShmartNumber amount)
 | 
			
		||||
        => InternallBetroll(amount);
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [NadekoOptions(typeof(LbOpts))]
 | 
			
		||||
    [Priority(0)]
 | 
			
		||||
 
 | 
			
		||||
@@ -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<long> _curDecayKey = new("currency:last_decay");
 | 
			
		||||
 | 
			
		||||
    public GamblingService(
 | 
			
		||||
        DbService db,
 | 
			
		||||
        Bot bot,
 | 
			
		||||
@@ -69,8 +69,7 @@ public class GamblingService : INService, IReadyExecutor
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static readonly TypedKey<long> _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<SlotResponse> 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<EconomyResult> _ecoKey = new("nadeko:economy");
 | 
			
		||||
 | 
			
		||||
    public async Task<EconomyResult> GetEconomyAsync()
 | 
			
		||||
@@ -198,9 +163,6 @@ public class GamblingService : INService, IReadyExecutor
 | 
			
		||||
        return data;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task<WheelOfFortuneGame.Result> 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; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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<OneOf<SlotResult, SlotError>> 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<OneOf<SlotResult, SlotError>> PullAsync(ulong userId, long amount)
 | 
			
		||||
    // {
 | 
			
		||||
    //     
 | 
			
		||||
    // }
 | 
			
		||||
 | 
			
		||||
    public DefaultSlotService(GamblingConfigService bcs, ICurrencyService cs)
 | 
			
		||||
    {
 | 
			
		||||
        _bcs = bcs;
 | 
			
		||||
        _cs = cs;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public async Task<OneOf<WofResult, WofError>> 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<OneOf<>> 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<BetFlipReply> 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<FlipReply> 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<SlotResponse> 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<ulong, Deck> _decks = new ConcurrentDictionary<ulong, Deck>();
 | 
			
		||||
    //
 | 
			
		||||
    // public override Task<DeckShuffleReply> DeckShuffle(DeckShuffleRequest request, ServerCallContext context)
 | 
			
		||||
    // {
 | 
			
		||||
    //  _decks.AddOrUpdate(request.Id, new Deck(), (key, old) => new Deck());
 | 
			
		||||
    //  return Task.FromResult(new DeckShuffleReply { });
 | 
			
		||||
    // }
 | 
			
		||||
    //
 | 
			
		||||
    // public override Task<DeckDrawReply> 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<Deck.Card>(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<RpsReply> 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<OneOf<SlotResult, SlotError>> PullAsync(ulong userId, long amount);
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
public partial class Gambling
 | 
			
		||||
{
 | 
			
		||||
    [Group]
 | 
			
		||||
    public partial class SlotCommands : GamblingSubmodule<GamblingService>
 | 
			
		||||
    public partial class SlotCommands : GamblingSubmodule<IGamblingService>
 | 
			
		||||
    {
 | 
			
		||||
        private static long totalBet;
 | 
			
		||||
        private static long totalPaidOut;
 | 
			
		||||
        private static decimal totalBet;
 | 
			
		||||
        private static decimal totalPaidOut;
 | 
			
		||||
 | 
			
		||||
        private static readonly HashSet<ulong> _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<int, int>();
 | 
			
		||||
            var dict = new Dictionary<decimal, int>();
 | 
			
		||||
            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,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,36 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Modules.Gambling.Common.Slot;
 | 
			
		||||
 | 
			
		||||
public class SlotGame
 | 
			
		||||
{
 | 
			
		||||
    private static readonly Random _rng = new NadekoRandom();
 | 
			
		||||
 | 
			
		||||
    public Result Spin()
 | 
			
		||||
    {
 | 
			
		||||
        var rolls = new[] { _rng.Next(0, 6), _rng.Next(0, 6), _rng.Next(0, 6) };
 | 
			
		||||
        var multi = 0;
 | 
			
		||||
 | 
			
		||||
        if (rolls.All(x => x == 5))
 | 
			
		||||
            multi = 30;
 | 
			
		||||
        else if (rolls.All(x => x == rolls[0]))
 | 
			
		||||
            multi = 10;
 | 
			
		||||
        else if (rolls.Count(x => x == 5) == 2)
 | 
			
		||||
            multi = 4;
 | 
			
		||||
        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)
 | 
			
		||||
        {
 | 
			
		||||
            Multiplier = multiplier;
 | 
			
		||||
            Rolls = rolls;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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<int> Rolls { get; set; } = new();
 | 
			
		||||
    public GamblingError Error { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,35 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Modules.Gambling.WheelOfFortune;
 | 
			
		||||
 | 
			
		||||
public sealed class WheelOfFortuneGame
 | 
			
		||||
{
 | 
			
		||||
    public static IReadOnlyList<decimal> DEFAULT_MULTIPLIERS = new[] { 1.7M, 1.5M, 0.2M, 0.1M, 0.3M, 0.5M, 1.2M, 2.4M };
 | 
			
		||||
    
 | 
			
		||||
    private readonly IReadOnlyList<decimal> _multipliers;
 | 
			
		||||
    private readonly NadekoRandom _rng;
 | 
			
		||||
 | 
			
		||||
    public WheelOfFortuneGame(IReadOnlyList<decimal> 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
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,9 +0,0 @@
 | 
			
		||||
#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; }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,43 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Modules.Gambling.Common;
 | 
			
		||||
 | 
			
		||||
public class Betroll
 | 
			
		||||
{
 | 
			
		||||
    private readonly IOrderedEnumerable<BetRollPair> _thresholdPairs;
 | 
			
		||||
    private readonly Random _rng;
 | 
			
		||||
 | 
			
		||||
    public Betroll(IReadOnlyList<long> 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; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Modules.Gambling;
 | 
			
		||||
 | 
			
		||||
public enum GamblingError
 | 
			
		||||
public enum OldGamblingError
 | 
			
		||||
{
 | 
			
		||||
    None,
 | 
			
		||||
    NotEnough
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										14
									
								
								src/NadekoBot/Modules/Gambling/~Shared/IGamblingService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/NadekoBot/Modules/Gambling/~Shared/IGamblingService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Nadeko.Econ.Gambling;
 | 
			
		||||
using OneOf;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling;
 | 
			
		||||
 | 
			
		||||
public interface IGamblingService
 | 
			
		||||
{
 | 
			
		||||
    Task<OneOf<WofResult, GamblingError>> WofAsync(ulong userId, long amount);
 | 
			
		||||
    Task<OneOf<BetrollResult, GamblingError>> BetRollAsync(ulong userId, long amount);
 | 
			
		||||
    Task<OneOf<BetflipResult, GamblingError>> BetFlipAsync(ulong userId, long amount, int guess);
 | 
			
		||||
    Task<OneOf<SlotResult, GamblingError>> SlotAsync(ulong userId, long amount);
 | 
			
		||||
    Task<FlipResult[]> FlipAsync(int count);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										251
									
								
								src/NadekoBot/Modules/Gambling/~Shared/NewGamblingService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										251
									
								
								src/NadekoBot/Modules/Gambling/~Shared/NewGamblingService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -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<OneOf<WofResult, GamblingError>> 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<OneOf<BetrollResult, GamblingError>> 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<OneOf<BetflipResult, GamblingError>> 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<OneOf<SlotResult, GamblingError>> 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<FlipResult[]> 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<ulong, Deck> _decks = new ConcurrentDictionary<ulong, Deck>();
 | 
			
		||||
    //
 | 
			
		||||
    // public override Task<DeckShuffleReply> DeckShuffle(DeckShuffleRequest request, ServerCallContext context)
 | 
			
		||||
    // {
 | 
			
		||||
    //  _decks.AddOrUpdate(request.Id, new Deck(), (key, old) => new Deck());
 | 
			
		||||
    //  return Task.FromResult(new DeckShuffleReply { });
 | 
			
		||||
    // }
 | 
			
		||||
    //
 | 
			
		||||
    // public override Task<DeckDrawReply> 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<Deck.Card>(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<RpsReply> 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
 | 
			
		||||
    //  };
 | 
			
		||||
    // }
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
}
 | 
			
		||||
@@ -102,6 +102,7 @@
 | 
			
		||||
    <ItemGroup>
 | 
			
		||||
        <ProjectReference Include="..\ayu\Ayu.Discord.Voice\Ayu.Discord.Voice.csproj" />
 | 
			
		||||
        <ProjectReference Include="..\Nadeko.Common\Nadeko.Common.csproj" />
 | 
			
		||||
        <ProjectReference Include="..\Nadeko.Econ\Nadeko.Econ.csproj" />
 | 
			
		||||
        <ProjectReference Include="..\Nadeko.Medusa\Nadeko.Medusa.csproj" />
 | 
			
		||||
        <ProjectReference Include="..\NadekoBot.Generators\NadekoBot.Generators.csproj" OutputItemType="Analyzer" />
 | 
			
		||||
    </ItemGroup>
 | 
			
		||||
@@ -125,6 +126,7 @@
 | 
			
		||||
    </ItemGroup>
 | 
			
		||||
    <ItemGroup>
 | 
			
		||||
      <Folder Include="Common\Collections" />
 | 
			
		||||
      <Folder Include="Modules\Gambling\Wheel" />
 | 
			
		||||
    </ItemGroup>
 | 
			
		||||
 | 
			
		||||
    <PropertyGroup Condition=" '$(Version)' == '' ">
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user