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,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

@@ -30,14 +30,23 @@ public partial class Gambling
var num1 = gen / 10;
var num2 = gen % 10;
using var img1 = await GetDiceAsync(num1);
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 imgName = $"coins.{format.FileExtensions.First()}";
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))));
? 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, $"{count} coins.{format.FileExtensions.First()}", msg);
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();
// var inter = slotInteraction.GetInteraction();
await ctx.Channel.SendFileAsync(imgStream,
"result.png",
embed: eb.Build()
// components: inter.CreateComponent()
);
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));
}
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,311 +0,0 @@
#nullable disable
using Nadeko.Common;
namespace NadekoBot.Modules.Gambling.Common;
public class Deck
{
public enum CardSuit
{
Spades = 1,
Hearts = 2,
Diamonds = 3,
Clubs = 4
}
private static readonly Dictionary<int, string> _cardNames = new()
{
{ 1, "Ace" },
{ 2, "Two" },
{ 3, "Three" },
{ 4, "Four" },
{ 5, "Five" },
{ 6, "Six" },
{ 7, "Seven" },
{ 8, "Eight" },
{ 9, "Nine" },
{ 10, "Ten" },
{ 11, "Jack" },
{ 12, "Queen" },
{ 13, "King" }
};
private static Dictionary<string, Func<List<Card>, bool>> handValues;
public List<Card> CardPool { get; set; }
private readonly Random _r = new NadekoRandom();
static Deck()
=> InitHandValues();
/// <summary>
/// Creates a new instance of the BlackJackGame, this allows you to create multiple games running at one time.
/// </summary>
public Deck()
=> RefillPool();
/// <summary>
/// Restart the game of blackjack. It will only refill the pool for now. Probably wont be used, unless you want to have
/// only 1 bjg running at one time,
/// then you will restart the same game every time.
/// </summary>
public void Restart()
=> RefillPool();
/// <summary>
/// Removes all cards from the pool and refills the pool with all of the possible cards. NOTE: I think this is too
/// expensive.
/// We should probably make it so it copies another premade list with all the cards, or something.
/// </summary>
protected virtual void RefillPool()
{
CardPool = new(52);
//foreach suit
for (var j = 1; j < 14; j++)
// and number
for (var i = 1; i < 5; i++)
//generate a card of that suit and number and add it to the pool
// the pool will go from ace of spades,hears,diamonds,clubs all the way to the king of spades. hearts, ...
CardPool.Add(new((CardSuit)i, j));
}
/// <summary>
/// Take a card from the pool, you either take it from the top if the deck is shuffled, or from a random place if the
/// deck is in the default order.
/// </summary>
/// <returns>A card from the pool</returns>
public Card Draw()
{
if (CardPool.Count == 0)
Restart();
//you can either do this if your deck is not shuffled
var num = _r.Next(0, CardPool.Count);
var c = CardPool[num];
CardPool.RemoveAt(num);
return c;
// if you want to shuffle when you fill, then take the first one
/*
Card c = cardPool[0];
cardPool.RemoveAt(0);
return c;
*/
}
/// <summary>
/// Shuffles the deck. Use this if you want to take cards from the top of the deck, instead of randomly. See DrawACard
/// method.
/// </summary>
private void Shuffle()
{
if (CardPool.Count <= 1)
return;
var orderedPool = CardPool.Shuffle();
CardPool ??= orderedPool.ToList();
}
public override string ToString()
=> string.Concat(CardPool.Select(c => c.ToString())) + Environment.NewLine;
private static void InitHandValues()
{
bool HasPair(List<Card> cards)
{
return cards.GroupBy(card => card.Number).Count(group => group.Count() == 2) == 1;
}
bool IsPair(List<Card> cards)
{
return cards.GroupBy(card => card.Number).Count(group => group.Count() == 3) == 0 && HasPair(cards);
}
bool IsTwoPair(List<Card> cards)
{
return cards.GroupBy(card => card.Number).Count(group => group.Count() == 2) == 2;
}
bool IsStraight(List<Card> cards)
{
if (cards.GroupBy(card => card.Number).Count() != cards.Count())
return false;
var toReturn = cards.Max(card => card.Number) - cards.Min(card => card.Number) == 4;
if (toReturn || cards.All(c => c.Number != 1))
return toReturn;
var newCards = cards.Select(c => c.Number == 1 ? new(c.Suit, 14) : c).ToArray();
return newCards.Max(card => card.Number) - newCards.Min(card => card.Number) == 4;
}
bool HasThreeOfKind(List<Card> cards)
{
return cards.GroupBy(card => card.Number).Any(group => group.Count() == 3);
}
bool IsThreeOfKind(List<Card> cards)
{
return HasThreeOfKind(cards) && !HasPair(cards);
}
bool IsFlush(List<Card> cards)
{
return cards.GroupBy(card => card.Suit).Count() == 1;
}
bool IsFourOfKind(List<Card> cards)
{
return cards.GroupBy(card => card.Number).Any(group => group.Count() == 4);
}
bool IsFullHouse(List<Card> cards)
{
return HasPair(cards) && HasThreeOfKind(cards);
}
bool HasStraightFlush(List<Card> cards)
{
return IsFlush(cards) && IsStraight(cards);
}
bool IsRoyalFlush(List<Card> cards)
{
return cards.Min(card => card.Number) == 1
&& cards.Max(card => card.Number) == 13
&& HasStraightFlush(cards);
}
bool IsStraightFlush(List<Card> cards)
{
return HasStraightFlush(cards) && !IsRoyalFlush(cards);
}
handValues = new()
{
{ "Royal Flush", IsRoyalFlush },
{ "Straight Flush", IsStraightFlush },
{ "Four Of A Kind", IsFourOfKind },
{ "Full House", IsFullHouse },
{ "Flush", IsFlush },
{ "Straight", IsStraight },
{ "Three Of A Kind", IsThreeOfKind },
{ "Two Pairs", IsTwoPair },
{ "A Pair", IsPair }
};
}
public static string GetHandValue(List<Card> cards)
{
if (handValues is null)
InitHandValues();
foreach (var kvp in handValues.Where(x => x.Value(cards)))
return kvp.Key;
return "High card " + (cards.FirstOrDefault(c => c.Number == 1)?.GetValueText() ?? cards.Max().GetValueText());
}
public class Card : IComparable
{
private static readonly IReadOnlyDictionary<CardSuit, string> _suitToSuitChar = new Dictionary<CardSuit, string>
{
{ CardSuit.Diamonds, "♦" },
{ CardSuit.Clubs, "♣" },
{ CardSuit.Spades, "♠" },
{ CardSuit.Hearts, "♥" }
};
private static readonly IReadOnlyDictionary<string, CardSuit> _suitCharToSuit = new Dictionary<string, CardSuit>
{
{ "♦", CardSuit.Diamonds },
{ "d", CardSuit.Diamonds },
{ "♣", CardSuit.Clubs },
{ "c", CardSuit.Clubs },
{ "♠", CardSuit.Spades },
{ "s", CardSuit.Spades },
{ "♥", CardSuit.Hearts },
{ "h", CardSuit.Hearts }
};
private static readonly IReadOnlyDictionary<char, int> _numberCharToNumber = new Dictionary<char, int>
{
{ 'a', 1 },
{ '2', 2 },
{ '3', 3 },
{ '4', 4 },
{ '5', 5 },
{ '6', 6 },
{ '7', 7 },
{ '8', 8 },
{ '9', 9 },
{ 't', 10 },
{ 'j', 11 },
{ 'q', 12 },
{ 'k', 13 }
};
public CardSuit Suit { get; }
public int Number { get; }
public string FullName
{
get
{
var str = string.Empty;
if (Number is <= 10 and > 1)
str += "_" + Number;
else
str += GetValueText().ToLowerInvariant();
return str + "_of_" + Suit.ToString().ToLowerInvariant();
}
}
private readonly string[] _regIndicators =
{
"🇦", ":two:", ":three:", ":four:", ":five:", ":six:", ":seven:", ":eight:", ":nine:", ":keycap_ten:",
"🇯", "🇶", "🇰"
};
public Card(CardSuit s, int cardNum)
{
Suit = s;
Number = cardNum;
}
public string GetValueText()
=> _cardNames[Number];
public override string ToString()
=> _cardNames[Number] + " Of " + Suit;
public int CompareTo(object obj)
{
if (obj is not Card card)
return 0;
return Number - card.Number;
}
public static Card Parse(string input)
{
if (string.IsNullOrWhiteSpace(input))
throw new ArgumentNullException(nameof(input));
if (input.Length != 2
|| !_numberCharToNumber.TryGetValue(input[0], out var n)
|| !_suitCharToSuit.TryGetValue(input[1].ToString(), out var s))
throw new ArgumentException("Invalid input", nameof(input));
return new(s, n);
}
public string GetEmojiString()
{
var str = string.Empty;
str += _regIndicators[Number - 1];
str += _suitToSuitChar[Suit];
return str;
}
}
}

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));
@@ -64,9 +63,9 @@ public sealed class NewGamblingService : IGamblingService, INService
var game = new BetrollGame(_bcs.Data.BetRoll.Pairs
.Select(x => (x.WhenAbove, (decimal)x.MultiplyBy))
.ToList());
var result = game.Roll(amount);
var won = (long)result.Won;
if (won > 0)
{
@@ -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"));
@@ -102,6 +104,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)
{
@@ -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]