Many changes. Will update merge request description with details

This commit is contained in:
Kwoth
2022-07-15 05:04:01 +02:00
parent d9011106ac
commit 0b720a0439
23 changed files with 1429 additions and 998 deletions

View File

@@ -1,7 +1,7 @@
#nullable disable
using Nadeko.Common;
namespace NadekoBot.Modules.Gambling.Common;
namespace Nadeko.Econ;
public class Deck
{

View File

@@ -0,0 +1,7 @@
namespace Nadeko.Econ.Gambling.Betdraw;
public enum BetdrawColorGuess
{
Red,
Black
}

View File

@@ -0,0 +1,83 @@
namespace Nadeko.Econ.Gambling.Betdraw;
public sealed class BetdrawGame
{
private static readonly NadekoRandom _rng = new();
private const decimal SINGLE_GUESS_MULTI = 2.075M;
private const decimal DOUBLE_GUESS_MULTI = 4.15M;
public BetdrawGame()
{
}
public BetdrawResult Draw(BetdrawValueGuess? val, BetdrawColorGuess? col, decimal amount)
{
if (val is null && col is null)
throw new ArgumentNullException(nameof(val));
var card = new Deck().CardPool[_rng.Next(0, 52)];
var realVal = card.Number < 7
? BetdrawValueGuess.Low
: BetdrawValueGuess.High;
var realCol = card.Suit is Deck.CardSuit.Diamonds or Deck.CardSuit.Hearts
? BetdrawColorGuess.Red
: BetdrawColorGuess.Black;
// if card is 7, autoloss
if (card.Number == 7)
{
return new()
{
Won = 0M,
Multiplier = 0M,
ResultType = BetdrawResultType.Lose,
Card = card,
};
}
byte win = 0;
if (val is BetdrawValueGuess valGuess)
{
if (realVal != valGuess)
return new()
{
Won = 0M,
Multiplier = 0M,
ResultType = BetdrawResultType.Lose,
Card = card
};
++win;
}
if (col is BetdrawColorGuess colGuess)
{
if (realCol != colGuess)
return new()
{
Won = 0M,
Multiplier = 0M,
ResultType = BetdrawResultType.Lose,
Card = card
};
++win;
}
var multi = win == 1
? SINGLE_GUESS_MULTI
: DOUBLE_GUESS_MULTI;
return new()
{
Won = amount * multi,
Multiplier = multi,
ResultType = BetdrawResultType.Win,
Card = card
};
}
}

View File

@@ -0,0 +1,9 @@
namespace Nadeko.Econ.Gambling.Betdraw;
public readonly struct BetdrawResult
{
public decimal Won { get; init; }
public decimal Multiplier { get; init; }
public BetdrawResultType ResultType { get; init; }
public Deck.Card Card { get; init; }
}

View File

@@ -0,0 +1,7 @@
namespace Nadeko.Econ.Gambling.Betdraw;
public enum BetdrawResultType
{
Win,
Lose
}

View File

@@ -0,0 +1,7 @@
namespace Nadeko.Econ.Gambling.Betdraw;
public enum BetdrawValueGuess
{
High,
Low,
}

View File

@@ -3,26 +3,31 @@
public sealed class BetflipGame
{
private readonly decimal _winMulti;
private readonly NadekoRandom _rng;
private static readonly NadekoRandom _rng = new NadekoRandom();
public BetflipGame(decimal winMulti)
{
_winMulti = winMulti;
_rng = new NadekoRandom();
}
public BetflipResult Flip(byte guess, decimal amount)
{
var side = _rng.Next(0, 2);
decimal won = 0;
if (side == guess)
won = amount * _winMulti;
{
return new BetflipResult()
{
Side = side,
Won = amount * _winMulti,
Multiplier = _winMulti
};
}
return new BetflipResult()
{
Side = side,
Won = won,
Won = 0,
Multiplier = 0,
};
}
}

View File

@@ -4,4 +4,5 @@ public readonly struct BetflipResult
{
public decimal Won { get; init; }
public byte Side { get; init; }
public decimal Multiplier { get; init; }
}

View File

@@ -2,15 +2,14 @@
public sealed class LulaGame
{
public static IReadOnlyList<decimal> DEFAULT_MULTIPLIERS = new[] { 1.7M, 1.5M, 0.2M, 0.1M, 0.3M, 0.5M, 1.2M, 2.4M };
private static readonly 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;
private static readonly NadekoRandom _rng = new();
public LulaGame(IReadOnlyList<decimal> multipliers)
{
_multipliers = multipliers;
_rng = new();
}
public LulaGame() : this(DEFAULT_MULTIPLIERS)

View File

@@ -1,4 +1,6 @@
#nullable disable
using Nadeko.Econ;
namespace NadekoBot.Modules.Gambling.Common.Blackjack;
public class Blackjack

View File

@@ -1,4 +1,6 @@
#nullable disable
using Nadeko.Econ;
namespace NadekoBot.Modules.Gambling.Common.Blackjack;
public abstract class Player

View File

@@ -35,9 +35,18 @@ public partial class Gambling
using var img2 = await GetDiceAsync(num2);
using var img = new[] { img1, img2 }.Merge(out var format);
await using var ms = await img.ToStreamAsync(format);
var fileName = $"dice.{format.FileExtensions.First()}";
var eb = _eb.Create(ctx)
.WithOkColor()
.WithAuthor(ctx.User)
.AddField(GetText(strs.roll2), gen)
.WithImageUrl($"attachment://{fileName}");
await ctx.Channel.SendFileAsync(ms,
$"dice.{format.FileExtensions.First()}",
Format.Bold(ctx.User.ToString()) + " " + GetText(strs.dice_rolled(Format.Code(gen.ToString()))));
fileName,
embed: eb.Build());
}
[Cmd]
@@ -105,14 +114,18 @@ public partial class Gambling
foreach (var d in dice)
d.Dispose();
var imageName = $"dice.{format.FileExtensions.First()}";
var eb = _eb.Create(ctx)
.WithOkColor()
.WithAuthor(ctx.User)
.AddField(GetText(strs.rolls), values.Select(x => Format.Code(x.ToString())).Join(' '), true)
.AddField(GetText(strs.total), values.Sum(), true)
.WithDescription(GetText(strs.dice_rolled_num(Format.Bold(values.Count.ToString()))))
.WithImageUrl($"attachment://{imageName}");
await ctx.Channel.SendFileAsync(ms,
$"dice.{format.FileExtensions.First()}",
Format.Bold(ctx.User.ToString())
+ " "
+ GetText(strs.dice_rolled_num(Format.Bold(values.Count.ToString())))
+ " "
+ GetText(strs.total_average(Format.Bold(values.Sum().ToString()),
Format.Bold((values.Sum() / (1.0f * values.Count)).ToString("N2")))));
imageName,
embed: eb.Build());
}
private async Task InternallDndRoll(string arg, bool ordered)
@@ -130,9 +143,8 @@ public partial class Gambling
rolls.Add(_fateRolls[rng.Next(0, _fateRolls.Length)]);
var embed = _eb.Create()
.WithOkColor()
.WithDescription(ctx.User.Mention
+ " "
+ GetText(strs.dice_rolled_num(Format.Bold(n1.ToString()))))
.WithAuthor(ctx.User)
.WithDescription(GetText(strs.dice_rolled_num(Format.Bold(n1.ToString()))))
.AddField(Format.Bold("Result"),
string.Join(" ", rolls.Select(c => Format.Code($"[{c}]"))));
@@ -160,10 +172,9 @@ public partial class Gambling
var sum = arr.Sum();
var embed = _eb.Create()
.WithOkColor()
.WithDescription(ctx.User.Mention
+ " "
+ GetText(strs.dice_rolled_num(n1 + $"`1 - {n2}`")))
.AddField(Format.Bold("Rolls"),
.WithAuthor(ctx.User)
.WithDescription(GetText(strs.dice_rolled_num(n1 + $"`1 - {n2}`")))
.AddField(Format.Bold(GetText(strs.rolls)),
string.Join(" ",
(ordered ? arr.OrderBy(x => x).AsEnumerable() : arr).Select(x
=> Format.Code(x.ToString()))))

View File

@@ -1,5 +1,7 @@
#nullable disable
using Nadeko.Econ;
using NadekoBot.Modules.Gambling.Common;
using NadekoBot.Modules.Gambling.Services;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Image = SixLabors.ImageSharp.Image;
@@ -9,23 +11,23 @@ namespace NadekoBot.Modules.Gambling;
public partial class Gambling
{
[Group]
public partial class DrawCommands : NadekoModule
public partial class DrawCommands : GamblingSubmodule<IGamblingService>
{
private static readonly ConcurrentDictionary<IGuild, Deck> _allDecks = new();
private readonly IImageCache _images;
public DrawCommands(IImageCache images)
public DrawCommands(IImageCache images, GamblingConfigService gcs) : base(gcs)
=> _images = images;
private async Task<(Stream ImageStream, string ToSend)> InternalDraw(int num, ulong? guildId = null)
private async Task InternalDraw(int count, ulong? guildId = null)
{
if (num is < 1 or > 10)
throw new ArgumentOutOfRangeException(nameof(num));
if (count is < 1 or > 10)
throw new ArgumentOutOfRangeException(nameof(count));
var cards = guildId is null ? new() : _allDecks.GetOrAdd(ctx.Guild, _ => new());
var images = new List<Image<Rgba32>>();
var cardObjects = new List<Deck.Card>();
for (var i = 0; i < num; i++)
for (var i = 0; i < count; i++)
{
if (cards.CardPool.Count == 0 && i != 0)
{
@@ -43,22 +45,43 @@ public partial class Gambling
var currentCard = cards.Draw();
cardObjects.Add(currentCard);
var cardName = currentCard.ToString().ToLowerInvariant().Replace(' ', '_');
images.Add(Image.Load(await File.ReadAllBytesAsync($"data/images/cards/{cardName}.jpg")));
var image = await GetCardImageAsync(currentCard);
images.Add(image);
}
var imgName = "cards.jpg";
using var img = images.Merge();
foreach (var i in images)
i.Dispose();
var toSend = $"{Format.Bold(ctx.User.ToString())}";
var eb = _eb.Create(ctx)
.WithOkColor();
var toSend = string.Empty;
if (cardObjects.Count == 5)
toSend += $" drew `{Deck.GetHandValue(cardObjects)}`";
eb.AddField(GetText(strs.hand_value), Deck.GetHandValue(cardObjects), true);
if (guildId is not null)
toSend += "\n" + GetText(strs.cards_left(Format.Bold(cards.CardPool.Count.ToString())));
toSend += GetText(strs.cards_left(Format.Bold(cards.CardPool.Count.ToString())));
return (img.ToStream(), toSend);
eb.WithDescription(toSend)
.WithAuthor(ctx.User)
.WithImageUrl($"attachment://{imgName}");
if (count > 1)
eb.AddField(GetText(strs.cards), count.ToString(), true);
await using var imageStream = await img.ToStreamAsync();
await ctx.Channel.SendFileAsync(imageStream,
imgName,
embed: eb.Build());
}
private async Task<Image<Rgba32>> GetCardImageAsync(Deck.Card currentCard)
{
var cardName = currentCard.ToString().ToLowerInvariant().Replace(' ', '_');
var cardBytes = await File.ReadAllBytesAsync($"data/images/cards/{cardName}.jpg");
return Image.Load(cardBytes);
}
[Cmd]
@@ -66,30 +89,24 @@ public partial class Gambling
public async partial Task Draw(int num = 1)
{
if (num < 1)
num = 1;
return;
if (num > 10)
num = 10;
var (imageStream, toSend) = await InternalDraw(num, ctx.Guild.Id);
await using (imageStream)
{
await ctx.Channel.SendFileAsync(imageStream, num + " cards.jpg", toSend);
}
await InternalDraw(num, ctx.Guild.Id);
}
[Cmd]
public async partial Task DrawNew(int num = 1)
{
if (num < 1)
num = 1;
return;
if (num > 10)
num = 10;
var (imageStream, toSend) = await InternalDraw(num);
await using (imageStream)
{
await ctx.Channel.SendFileAsync(imageStream, num + " cards.jpg", toSend);
}
await InternalDraw(num);
}
[Cmd]
@@ -108,5 +125,98 @@ public partial class Gambling
await ReplyConfirmLocalizedAsync(strs.deck_reshuffled);
}
[Cmd]
[RequireContext(ContextType.Guild)]
public partial Task BetDraw(ShmartNumber amount, InputValueGuess val, InputColorGuess? col = null)
=> BetDrawInternal(amount, val, col);
[Cmd]
[RequireContext(ContextType.Guild)]
public partial Task BetDraw(ShmartNumber amount, InputColorGuess col, InputValueGuess? val = null)
=> BetDrawInternal(amount, val, col);
public async Task BetDrawInternal(long amount, InputValueGuess? val, InputColorGuess? col)
{
var res = await _service.BetDrawAsync(ctx.User.Id,
amount,
(byte?)val,
(byte?)col);
if (!res.TryPickT0(out var result, out _))
{
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
return;
}
var eb = _eb.Create(ctx)
.WithOkColor()
.WithAuthor(ctx.User)
.AddField(GetText(strs.guess), GetGuessInfo(val, col), true)
.AddField(GetText(strs.card), GetCardInfo(result.Card), true)
.AddField(GetText(strs.won), N((long)result.Won), false)
.WithImageUrl("attachment://card.png");
using var img = await GetCardImageAsync(result.Card);
await using var imgStream = await img.ToStreamAsync();
await ctx.Channel.SendFileAsync(imgStream, "card.png", embed: eb.Build());
}
private string GetGuessInfo(InputValueGuess? valG, InputColorGuess? colG)
{
var val = valG switch
{
InputValueGuess.H => "Hi ⬆️",
InputValueGuess.L => "Lo ⬇️",
_ => "❓"
};
var col = colG switch
{
InputColorGuess.Red => "R 🔴",
InputColorGuess.Black => "B ⚫",
_ => "❓"
};
return $"{val} / {col}";
}
private string GetCardInfo(Deck.Card card)
{
var val = card.Number switch
{
< 7 => "Lo ⬇️",
> 7 => "Hi ⬆️",
_ => "7 💀"
};
var col = card.Number == 7
? "7 💀"
: card.Suit switch
{
Deck.CardSuit.Diamonds or Deck.CardSuit.Hearts => "R 🔴",
_ => "B ⚫"
};
return $"{val} / {col}";
}
public enum InputValueGuess
{
High = 0,
H = 0,
Hi = 0,
Low = 1,
L = 1,
Lo = 1,
}
public enum InputColorGuess
{
R = 0,
Red = 0,
B = 1,
Bl = 1,
Black = 1,
}
}
}

View File

@@ -76,17 +76,23 @@ public partial class Gambling
foreach (var i in imgs)
i.Dispose();
var msg = count != 1
? Format.Bold(ctx.User.ToString())
+ " "
+ GetText(strs.flip_results(count, headCount, tailCount))
: Format.Bold(ctx.User.ToString())
+ " "
+ GetText(strs.flipped(headCount > 0
? Format.Bold(GetText(strs.heads))
: Format.Bold(GetText(strs.tails))));
var imgName = $"coins.{format.FileExtensions.First()}";
await ctx.Channel.SendFileAsync(stream, $"{count} coins.{format.FileExtensions.First()}", msg);
var msg = count != 1
? Format.Bold(GetText(strs.flip_results(count, headCount, tailCount)))
: GetText(strs.flipped(headCount > 0
? Format.Bold(GetText(strs.heads))
: Format.Bold(GetText(strs.tails))));
var eb = _eb.Create(ctx)
.WithOkColor()
.WithAuthor(ctx.User)
.WithDescription(msg)
.WithImageUrl($"attachment://{imgName}");
await ctx.Channel.SendFileAsync(stream,
imgName,
embed: eb.Build());
}
[Cmd]

View File

@@ -345,7 +345,8 @@ public partial class Gambling : GamblingModule<GamblingService>
("award", var name, ulong userId) => GetText(strs.curtr_award(name, userId)),
("take", var name, ulong userId) => GetText(strs.curtr_take(name, userId)),
("blackjack", _, _) => $"Blackjack - {subType}",
("wheel", _, _) => $"Wheel Of Fortune - {subType}",
("wheel", _, _) => $"Lucky Ladder - {subType}",
("lula", _, _) => $"Lucky Ladder - {subType}",
("rps", _, _) => $"Rock Paper Scissors - {subType}",
(null, _, _) => null,
(_, null, _) => null,
@@ -671,7 +672,7 @@ public partial class Gambling : GamblingModule<GamblingService>
var eb = _eb.Create(ctx)
.WithAuthor(ctx.User)
.WithFooter(str)
.WithDescription(Format.Bold(str))
.AddField(GetText(strs.roll2), result.Roll.ToString(CultureInfo.InvariantCulture))
.WithOkColor();
@@ -772,7 +773,6 @@ public partial class Gambling : GamblingModule<GamblingService>
Scissors = 2
}
// todo check if trivia is being disposed
[Cmd]
public async partial Task Rps(InputRpsPick pick, ShmartNumber amount = default)
{
@@ -839,7 +839,7 @@ public partial class Gambling : GamblingModule<GamblingService>
if (!await CheckBetMandatory(amount))
return;
var res = await _gs.WofAsync(ctx.User.Id, amount);
var res = await _gs.LulaAsync(ctx.User.Id, amount);
if (!res.TryPickT0(out var result, out _))
{
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
@@ -871,4 +871,99 @@ public partial class Gambling : GamblingModule<GamblingService>
await ctx.Channel.EmbedAsync(eb);
}
public enum GambleTestTarget
{
Slot,
BetDraw,
BetDrawHL,
BetDrawRB,
Betflip,
BetflipT,
Lula,
Rps,
}
[Cmd]
[OwnerOnly]
public async partial Task BetTest()
{
await SendConfirmAsync(GetText(strs.available_tests),
Enum.GetValues<GambleTestTarget>()
.Select(x => $"`{x}`")
.Join(", "));
}
[Cmd]
[OwnerOnly]
public async partial Task BetTest(GambleTestTarget target, int tests = 1000)
{
if (tests <= 0)
return;
await ctx.Channel.TriggerTypingAsync();
var streak = 0;
var maxW = 0;
var maxL = 0;
var dict = new Dictionary<decimal, int>();
for (var i = 0; i < tests; i++)
{
var multi = target switch
{
GambleTestTarget.BetDraw => (await _gs.BetDrawAsync(ctx.User.Id, 0, 1, 0)).AsT0.Multiplier,
GambleTestTarget.BetDrawRB => (await _gs.BetDrawAsync(ctx.User.Id, 0, null, 1)).AsT0.Multiplier,
GambleTestTarget.BetDrawHL => (await _gs.BetDrawAsync(ctx.User.Id, 0, 0, null)).AsT0.Multiplier,
GambleTestTarget.Slot => (await _gs.SlotAsync(ctx.User.Id, 0)).AsT0.Multiplier,
GambleTestTarget.Betflip => (await _gs.BetFlipAsync(ctx.User.Id, 0, 0)).AsT0.Multiplier,
GambleTestTarget.BetflipT => (await _gs.BetFlipAsync(ctx.User.Id, 0, 1)).AsT0.Multiplier,
GambleTestTarget.Lula => (await _gs.LulaAsync(ctx.User.Id, 0)).AsT0.Multiplier,
GambleTestTarget.Rps => (await _gs.RpsAsync(ctx.User.Id, 0, (byte)(i % 3))).AsT0.Multiplier,
_ => throw new ArgumentOutOfRangeException(nameof(target))
};
if (dict.ContainsKey(multi))
dict[multi] += 1;
else
dict.Add(multi, 1);
if (multi < 1)
{
if (streak <= 0)
--streak;
else
streak = -1;
maxL = Math.Max(maxL, -streak);
}
else if (multi > 1)
{
if (streak >= 0)
++streak;
else
streak = 1;
maxW = Math.Max(maxW, streak);
}
}
var sb = new StringBuilder();
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}%");
payout += key * dict[key];
}
sb.AppendLine();
sb.AppendLine($"Longest win streak: `{maxW}`");
sb.AppendLine($"Longest lose streak: `{maxL}`");
await SendConfirmAsync(GetText(strs.test_results_for(target)),
sb.ToString(),
footer: $"Total Bet: {tests} | Payout: {payout:F0} | {payout * 1.0M / tests * 100}%");
}
}

View File

@@ -1,4 +1,4 @@
#nullable disable
#nullable disable warnings
using NadekoBot.Db.Models;
using NadekoBot.Modules.Gambling.Common;
using NadekoBot.Modules.Gambling.Services;
@@ -73,178 +73,58 @@ public partial class Gambling
await ctx.Channel.EmbedAsync(embed);
}
[Cmd]
[OwnerOnly]
public async partial Task SlotTest(int tests = 1000)
public sealed class SlotInteraction : NInteraction
{
if (tests <= 0)
return;
//multi vs how many times it occured
int streak = 0;
int maxW = 0;
int maxL = 0;
var dict = new Dictionary<decimal, int>();
for (var i = 0; i < tests; i++)
public SlotInteraction(DiscordSocketClient client, ulong userId, Func<SocketMessageComponent, Task> action) : base(client, userId, action)
{
var res = await _service.SlotAsync(ctx.User.Id, 0);
var multi = res.AsT0.Multiplier;
if (dict.ContainsKey(multi))
dict[multi] += 1;
else
dict.Add(multi, 1);
if (multi == 0)
{
if (streak <= 0)
--streak;
else
streak = -1;
maxL = Math.Max(maxL, -streak);
}
else
{
if (streak >= 0)
++streak;
else
streak = 1;
maxW = Math.Max(maxW, streak);
}
}
var sb = new StringBuilder();
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}%");
payout += key * dict[key];
}
sb.AppendLine();
sb.AppendLine($"Longest win streak: `{maxW}`");
sb.AppendLine($"Longest lose streak: `{maxL}`");
await SendConfirmAsync("Slot Test Results",
sb.ToString(),
footer: $"Total Bet: {tests} | Payout: {payout:F0} | {payout * 1.0M / tests * 100}%");
protected override NadekoInteractionData Data { get; } = new(Emoji.Parse("🔁"),
"slot:again",
"Pull Again");
}
[Cmd]
public async partial Task Slot(ShmartNumber amount)
{
if (!await CheckBetMandatory(amount))
return;
// var slotInteraction = CreateSlotInteractionIntenal(amount);
await ctx.Channel.TriggerTypingAsync();
if (!_runningUsers.Add(ctx.User.Id))
return;
try
{
if (!await CheckBetMandatory(amount))
return;
await ctx.Channel.TriggerTypingAsync();
var maybeResult = await _service.SlotAsync(ctx.User.Id, amount);
if (!maybeResult.TryPickT0(out var result, out var error))
if (await InternalSlotAsync(amount) is not SlotResult result)
{
if (error == GamblingError.InsufficientFunds)
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
return;
}
lock (_slotStatsLock)
{
totalBet += amount;
totalPaidOut += result.Won;
}
var msg = GetSlotMessageInternal(result);
long ownedAmount;
await using (var uow = _db.GetDbContext())
{
ownedAmount = uow.Set<DiscordUser>().FirstOrDefault(x => x.UserId == ctx.User.Id)?.CurrencyAmount
?? 0;
}
using var image = await GenerateSlotImageAsync(amount, result);
await using var imgStream = await image.ToStreamAsync();
var slotBg = await _images.GetSlotBgAsync();
using (var bgImage = Image.Load<Rgba32>(slotBg, out _))
{
var numbers = new int[3];
result.Rolls.CopyTo(numbers, 0);
Color fontColor = Config.Slots.CurrencyFontColor;
var eb = _eb.Create(ctx)
.WithAuthor(ctx.User)
.WithDescription(Format.Bold(msg))
.WithImageUrl($"attachment://result.png")
.WithOkColor();
bgImage.Mutate(x => x.DrawText(new()
{
TextOptions = new()
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
WrapTextWidth = 140
}
},
((long)result.Won).ToString(),
_fonts.DottyFont.CreateFont(65),
fontColor,
new(227, 92)));
// var inter = slotInteraction.GetInteraction();
await ctx.Channel.SendFileAsync(imgStream,
"result.png",
embed: eb.Build()
// components: inter.CreateComponent()
);
var bottomFont = _fonts.DottyFont.CreateFont(50);
bgImage.Mutate(x => x.DrawText(new()
{
TextOptions = new()
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
WrapTextWidth = 135
}
},
amount.ToString(),
bottomFont,
fontColor,
new(129, 472)));
bgImage.Mutate(x => x.DrawText(new()
{
TextOptions = new()
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
WrapTextWidth = 135
}
},
ownedAmount.ToString(),
bottomFont,
fontColor,
new(325, 472)));
//sw.PrintLap("drew red text");
for (var i = 0; i < 3; i++)
{
using var img = Image.Load(await _images.GetSlotEmojiAsync(numbers[i]));
bgImage.Mutate(x => x.DrawImage(img, new Point(148 + (105 * i), 217), 1f));
}
var multi = result.Multiplier.ToString("0.##");
var msg = result.WinType switch
{
SlotWinType.SingleJoker => GetText(strs.slot_single(CurrencySign, multi)),
SlotWinType.DoubleJoker => GetText(strs.slot_two(CurrencySign, multi)),
SlotWinType.TrippleNormal => GetText(strs.slot_three(multi)),
SlotWinType.TrippleJoker => GetText(strs.slot_jackpot(multi)),
_ => GetText(strs.better_luck),
};
await using (var imgStream = await bgImage.ToStreamAsync())
{
await ctx.Channel.SendFileAsync(imgStream,
"result.png",
Format.Bold(ctx.User.ToString()) + " " + msg);
}
}
// await inter.RunAsync(resMsg);
}
finally
{
@@ -252,5 +132,156 @@ public partial class Gambling
_runningUsers.TryRemove(ctx.User.Id);
}
}
// private SlotInteraction CreateSlotInteractionIntenal(long amount)
// {
// return new SlotInteraction((DiscordSocketClient)ctx.Client,
// ctx.User.Id,
// async (smc) =>
// {
// try
// {
// if (await InternalSlotAsync(amount) is not SlotResult result)
// {
// await smc.RespondErrorAsync(_eb, GetText(strs.not_enough(CurrencySign)), true);
// return;
// }
//
// var msg = GetSlotMessageInternal(result);
//
// using var image = await GenerateSlotImageAsync(amount, result);
// await using var imgStream = await image.ToStreamAsync();
//
// var guid = Guid.NewGuid();
// var imgName = $"result_{guid}.png";
//
// var slotInteraction = CreateSlotInteractionIntenal(amount).GetInteraction();
//
// await smc.Message.ModifyAsync(m =>
// {
// m.Content = msg;
// m.Attachments = new[]
// {
// new FileAttachment(imgStream, imgName)
// };
// m.Components = slotInteraction.CreateComponent();
// });
//
// _ = slotInteraction.RunAsync(smc.Message);
// }
// catch (Exception ex)
// {
// Log.Error(ex, "Error pulling slot again");
// }
// // finally
// // {
// // await Task.Delay(1000);
// // _runningUsers.TryRemove(ctx.User.Id);
// // }
// });
// }
private string GetSlotMessageInternal(SlotResult result)
{
var multi = result.Multiplier.ToString("0.##");
var msg = result.WinType switch
{
SlotWinType.SingleJoker => GetText(strs.slot_single(CurrencySign, multi)),
SlotWinType.DoubleJoker => GetText(strs.slot_two(CurrencySign, multi)),
SlotWinType.TrippleNormal => GetText(strs.slot_three(multi)),
SlotWinType.TrippleJoker => GetText(strs.slot_jackpot(multi)),
_ => GetText(strs.better_luck),
};
return msg;
}
private async Task<SlotResult?> InternalSlotAsync(long amount)
{
var maybeResult = await _service.SlotAsync(ctx.User.Id, amount);
if (!maybeResult.TryPickT0(out var result, out var error))
{
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
return null;
}
lock (_slotStatsLock)
{
totalBet += amount;
totalPaidOut += result.Won;
}
return result;
}
private async Task<Image<Rgba32>> GenerateSlotImageAsync(long amount, SlotResult result)
{
long ownedAmount;
await using (var uow = _db.GetDbContext())
{
ownedAmount = uow.Set<DiscordUser>().FirstOrDefault(x => x.UserId == ctx.User.Id)?.CurrencyAmount
?? 0;
}
var slotBg = await _images.GetSlotBgAsync();
var bgImage = Image.Load<Rgba32>(slotBg, out _);
var numbers = new int[3];
result.Rolls.CopyTo(numbers, 0);
Color fontColor = Config.Slots.CurrencyFontColor;
bgImage.Mutate(x => x.DrawText(new()
{
TextOptions = new()
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
WrapTextWidth = 140
}
},
((long)result.Won).ToString(),
_fonts.DottyFont.CreateFont(65),
fontColor,
new(227, 92)));
var bottomFont = _fonts.DottyFont.CreateFont(50);
bgImage.Mutate(x => x.DrawText(new()
{
TextOptions = new()
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
WrapTextWidth = 135
}
},
amount.ToString(),
bottomFont,
fontColor,
new(129, 472)));
bgImage.Mutate(x => x.DrawText(new()
{
TextOptions = new()
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
WrapTextWidth = 135
}
},
ownedAmount.ToString(),
bottomFont,
fontColor,
new(325, 472)));
//sw.PrintLap("drew red text");
for (var i = 0; i < 3; i++)
{
using var img = Image.Load(await _images.GetSlotEmojiAsync(numbers[i]));
bgImage.Mutate(x => x.DrawImage(img, new Point(148 + (105 * i), 217), 1f));
}
return bgImage;
}
}
}

View File

@@ -1,4 +1,6 @@
namespace NadekoBot.Modules.Gambling.Common;
using Nadeko.Econ;
namespace NadekoBot.Modules.Gambling.Common;
public class QuadDeck : Deck
{

View File

@@ -1,5 +1,6 @@
#nullable disable
using Nadeko.Econ.Gambling;
using Nadeko.Econ.Gambling.Betdraw;
using Nadeko.Econ.Gambling.Rps;
using OneOf;
@@ -7,10 +8,11 @@ namespace NadekoBot.Modules.Gambling;
public interface IGamblingService
{
Task<OneOf<LuLaResult, GamblingError>> WofAsync(ulong userId, long amount);
Task<OneOf<LuLaResult, GamblingError>> LulaAsync(ulong userId, long amount);
Task<OneOf<BetrollResult, GamblingError>> BetRollAsync(ulong userId, long amount);
Task<OneOf<BetflipResult, GamblingError>> BetFlipAsync(ulong userId, long amount, byte guess);
Task<OneOf<SlotResult, GamblingError>> SlotAsync(ulong userId, long amount);
Task<FlipResult[]> FlipAsync(int count);
Task<OneOf<RpsResult, GamblingError>> RpsAsync(ulong userId, long amount, byte pick);
Task<OneOf<BetdrawResult, GamblingError>> BetDrawAsync(ulong userId, long amount, byte? guessValue, byte? guessColor);
}

View File

@@ -1,5 +1,6 @@
#nullable disable
using Nadeko.Econ.Gambling;
using Nadeko.Econ.Gambling.Betdraw;
using Nadeko.Econ.Gambling.Rps;
using NadekoBot.Modules.Gambling.Services;
using OneOf;
@@ -17,9 +18,7 @@ public sealed class NewGamblingService : IGamblingService, INService
_cs = cs;
}
// todo input checks
// todo ladder of fortune
public async Task<OneOf<LuLaResult, GamblingError>> WofAsync(ulong userId, long amount)
public async Task<OneOf<LuLaResult, GamblingError>> LulaAsync(ulong userId, long amount)
{
if (amount < 0)
throw new ArgumentOutOfRangeException(nameof(amount));
@@ -81,6 +80,9 @@ public sealed class NewGamblingService : IGamblingService, INService
if (amount < 0)
throw new ArgumentOutOfRangeException(nameof(amount));
if (guess > 1)
throw new ArgumentOutOfRangeException(nameof(guess));
if (amount > 0)
{
var isTakeSuccess = await _cs.RemoveAsync(userId, amount, new("betflip", "bet"));
@@ -103,6 +105,42 @@ public sealed class NewGamblingService : IGamblingService, INService
return result;
}
public async Task<OneOf<BetdrawResult, GamblingError>> BetDrawAsync(ulong userId, long amount, byte? guessValue, byte? guessColor)
{
if (amount < 0)
throw new ArgumentOutOfRangeException(nameof(amount));
if (guessColor is null && guessValue is null)
throw new ArgumentNullException();
if (guessColor > 1)
throw new ArgumentOutOfRangeException(nameof(guessColor));
if (guessValue > 1)
throw new ArgumentOutOfRangeException(nameof(guessValue));
if (amount > 0)
{
var isTakeSuccess = await _cs.RemoveAsync(userId, amount, new("betflip", "bet"));
if (!isTakeSuccess)
{
return GamblingError.InsufficientFunds;
}
}
var game = new BetdrawGame();
var result = game.Draw((BetdrawValueGuess?)guessValue, (BetdrawColorGuess?)guessColor, 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)
{
if (amount < 0)
@@ -132,6 +170,9 @@ public sealed class NewGamblingService : IGamblingService, INService
public Task<FlipResult[]> FlipAsync(int count)
{
if (count < 1)
throw new ArgumentOutOfRangeException(nameof(count));
var game = new BetflipGame(0);
var results = new FlipResult[count];

View File

@@ -108,7 +108,7 @@ public partial class NSFW : NadekoModule<ISearchImagesService>
return t;
});
await ReplyConfirmLocalizedAsync(strs.autohentai_started(interval, string.Join(", ", tags)));
await SendConfirmAsync($"Autohentai started. Interval: {interval}, Tags: {string.Join(", ", tags)}");
}
[Cmd]

File diff suppressed because it is too large Load Diff

View File

@@ -653,7 +653,7 @@ rolluo:
- "7"
- "3d5"
nroll:
desc: "Rolls in a given range. If you specify just one number instead of the range, it will role from 0 to that number."
desc: "Rolls in a given range. If you specify just one number instead of the range, it will roll from 0 to that number."
args:
- "5"
- "5-15"
@@ -2232,3 +2232,22 @@ eval:
- "123 / 4.5f"
- "await ctx.OkAsync();"
- 'await ctx.SendConfirmAsync("uwu");'
betdraw:
desc: |-
Bet on the card value and/or color. Specify the amount followed by your guess.
You can specify `r` or `b` for red or black, and `h` or `l` for high or low.
You can specify only h/l or only r/b or both.
Returns are high but **7 always loses**.
args:
- "50 r"
- "200 b l"
- "1000 h"
- "38 hi black"
bettest:
desc: |-
Tests a betting command by specifying the name followed by the number of tests. Some have multiple variations.
See the list of all tests by specifying no parameters.
args:
- ""
- "betflip 1000"
- "slot 2000"

View File

@@ -81,7 +81,6 @@
"boostdel_on": "Boost messages will be deleted after {0} seconds.",
"hierarchy": "You can't use this command on users with a role higher or equal than yours (or mine) in the role hierarchy.",
"role_too_high": "You can't use this command with roles which are above your highest role, unless you're server administrator.",
"images_loading": "Images will be reloaded within a few seconds.",
"insuf_perms_i": "I have insufficient permissions.",
"insuf_perms_u": "You have insufficient permissions.",
"invalid_format": "Invalid input format.",
@@ -216,11 +215,13 @@
"better_luck": "Better luck next time ^_^",
"br_win": "Congratulations! You won {0} for rolling above {1}",
"deck_reshuffled": "Deck reshuffled.",
"flipped": "flipped {0}.",
"flipped": "Flipped {0}",
"flip_guess": "You guessed it! You won {0}",
"flip_invalid": "Invalid number specified. You can flip 1 to {0} coins.",
"flip_results": "Flipped {0} coins. {1} heads, {2} tails.",
"cards_left": "{0} cards left in the deck.",
"cards": "Cards",
"hand_value": "Hand value",
"gifted": "has gifted {0} to {1}",
"has": "{0} has {1}",
"heads": "Head",
@@ -232,11 +233,13 @@
"no_more_cards": "No more cards in the deck.",
"raffled_user": "Raffled user",
"roll2": "Roll",
"slot_bet": "Bet",
"rolls": "Rolls",
"slot_jackpot": "WOAAHHHHHH!!! Congratulations!!! x{0}",
"slot_single": "A single {0}, x{1}",
"slot_three": "Wow! Lucky! Three of a kind! x{0}",
"slot_two": "Good job! Two {0} - bet x{1}",
"available_tests": "Available Tests",
"test_results_for": "Test results for {0}",
"won": "Won",
"multiplier": "Multiplier",
"tails": "Tail",
@@ -256,7 +259,6 @@
"usage": "Usage",
"options": "Options",
"requires": "Requires",
"autohentai_started": "Autohentai started. Reposting every {0}s with one of the following tags:\n{1}",
"tag": "Tag",
"animal_race": "Animal race",
"animal_race_failed": "Failed to start since there was not enough participants.",
@@ -319,7 +321,6 @@
"rps_win": "{0} won! {1} beats {2}",
"submissions_closed": "Submissions closed",
"animal_race_already_started": "Animal Race is already running.",
"total_average": "Total: {0} Average: {1}",
"category": "Category",
"cleverbot_disabled": "Disabled cleverbot on this server.",
"cleverbot_enabled": "Enabled cleverbot on this server.",
@@ -327,7 +328,6 @@
"curgen_enabled": "Currency generation has been enabled on this channel.",
"curgen_pl": "{0} random {1} appeared!",
"curgen_sn": "A random {0} appeared!",
"failed_loading_question": "Failed loading a question.",
"game_started": "Game started",
"hangman_game_started": "Hangman game started",
"hangman_running": "Hangman game already running on this channel.",
@@ -357,10 +357,7 @@
"vs": "{0} vs {1}",
"attempting_to_queue": "Attempting to queue {0} tracks...",
"dir_queue_complete": "Directory queue complete.",
"fairplay": "Fairplay",
"finished_track": "Track Finished",
"fp_disabled": "Fair play disabled.",
"fp_enabled": "Fair play enabled.",
"from_position": "From position",
"id": "Id",
"now_playing": "Now playing",
@@ -447,10 +444,8 @@
"word_filter_channel_on": "Word filtering enabled on this channel.",
"word_filter_server_off": "Word filtering disabled on this server.",
"word_filter_server_on": "Word filtering enabled on this server.",
"avatar_none": "User {0} has no avatar set.",
"abilities": "Abilities",
"anime_no_fav": "No favorite anime yet",
"atl_ad_started": "Started automatic translation of messages on this channel. User messages will be auto-deleted.",
"atl_removed": "your auto-translate language has been removed.",
"atl_set": "Your auto-translate language has been set to {0}>{1}",
"atl_started": "Started automatic translation of messages on this channel.",
@@ -458,6 +453,8 @@
"atl_not_enabled": "Automatic translation is not enabled on this channel or you've provided an invalid language.",
"bad_input_format": "Bad input format, or something went wrong.",
"card_not_found": "Couldn't find that card.",
"card": "Card",
"guess": "Guess",
"catfact": "fact",
"chapters": "Chapters",
"comic_number": "Comic #",
@@ -483,7 +480,6 @@
"height_weight": "Height/Weight",
"height_weight_val": "{0}m/{1}kg",
"humidity": "Humidity",
"image_search_for": "Image search for:",
"imdb_fail": "Failed to find that movie.",
"invalid_lang": "Invalid source or target language.",
"jokes_not_loaded": "Jokes not loaded.",
@@ -506,7 +502,6 @@
"pokemon_none": "No pokemon found.",
"rating": "Rating",
"score": "Score:",
"search_for": "Search for:",
"short_url": "Short url",
"something_went_wrong": "Something went wrong.",
"specify_search_params": "Please specify search parameters.",
@@ -793,7 +788,6 @@
"config_prop_not_found": "Property {0} not found on {1} configuration",
"config_list": "Config list",
"bot_strings_reloaded": "Bot strings have been reloaded.",
"level_req": "Level Req.",
"xpn_setting_global": "Global Level-Up notifications",
"xpn_setting_server": "Server Level-Up notifications",
"xpn_notif_channel": "In the channel where you sent the last message.",
@@ -902,7 +896,6 @@
"mass_ban_in_progress": "Banning {0} users...",
"mass_ban_completed": "Banned {0} users.",
"mass_kill_completed": "Mass Banning and Blacklisting of {0} users is complete.",
"failed_finding_novel": "Can't find that novel. Make sure you've typed the exact full name, and that it exists on novelupdates.com",
"club_transfered": "Ownership of the club {0} has been transferred to {1}",
"club_transfer_failed": "Transfer failed. You must be the club owner. Target must be a member of your club.",
"roll_duel_challenge": "challenged {1} for a roll duel for {2}",
@@ -913,11 +906,6 @@
"account_not_found": "That account does not exist or is set to private.",
"ninja_not_found": "Currency with that name was not found or an invalid league name was provided.",
"leagues_not_found": "Unable to retrieve data from Path of Exile API.",
"reaction_roles_message": "**Roles:** {0}\n**Content:** {1}",
"no_reaction_roles": "There are no ReactionRole features enabled on this server.",
"reaction_cant_access": "I can't access {0} reaction. You can only use emotes from servers I'm in.",
"reaction_role_removed": "Removed ReactionRole message #{0}",
"reaction_roles_full": "You've reached the limit on ReactionRole messages. You have to delete some.",
"reminder_list": "List of reminders",
"reminder_server_list": "List of server reminders",
"reminder_deleted": "Reminder #{0} was deleted.",
@@ -966,7 +954,6 @@
"module_description_xp": "Gain xp based on chat activity, check users' xp cards",
"module_description_medusa": "**Bot Owner only.** Load, unload and handle dynamic modules. Read more [here](https://nadekobot.readthedocs.io/en/latest/medusa/creating-a-medusa/)",
"module_description_missing": "Description is missing for this module.",
"obsolete_use": "⚠ Obsolete, use {0} instead.",
"purge_user_confirm": "Are you sure that you want to purge {0} from the database?",
"expr_import_no_input": "Invalid input. No valid file upload or input text found.",
"expr_import_invalid_data": "Unable to parse the file. Make sure it's a valid .yml file",