diff --git a/src/NadekoBot/Modules/Gambling/Commands/Slots.cs b/src/NadekoBot/Modules/Gambling/Commands/Slots.cs new file mode 100644 index 00000000..05c46af5 --- /dev/null +++ b/src/NadekoBot/Modules/Gambling/Commands/Slots.cs @@ -0,0 +1,301 @@ +using Discord; +using Discord.Commands; +using NadekoBot.Attributes; +using NadekoBot.Extensions; +using NadekoBot.Services; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Gambling +{ + public partial class Gambling + { + [Group] + public class Slots : ModuleBase + { + private static int totalBet = 0; + private static int totalPaidOut = 0; + + private const string backgroundPath = "data/slots/background.png"; + + private static readonly byte[] backgroundBuffer; + private static readonly byte[][] numbersBuffer = new byte[10][]; + private static readonly byte[][] emojiBuffer; + + const int alphaCutOut = byte.MaxValue / 3; + + static Slots() + { + backgroundBuffer = File.ReadAllBytes(backgroundPath); + + for (int i = 0; i < 10; i++) + { + numbersBuffer[i] = File.ReadAllBytes("data/slots/" + i + ".png"); + } + int throwaway; + var emojiFiles = Directory.GetFiles("data/slots/emojis/", "*.png") + .Where(f => int.TryParse(Path.GetFileNameWithoutExtension(f), out throwaway)) + .OrderBy(f => int.Parse(Path.GetFileNameWithoutExtension(f))) + .ToArray(); + + emojiBuffer = new byte[emojiFiles.Length][]; + for (int i = 0; i < emojiFiles.Length; i++) + { + emojiBuffer[i] = File.ReadAllBytes(emojiFiles[i]); + } + } + + + private static MemoryStream InternalGetStream(string path) + { + var ms = new MemoryStream(); + using (var fs = File.Open(path, FileMode.Open)) + { + fs.CopyTo(ms); + fs.Flush(); + } + ms.Position = 0; + return ms; + } + + //here is a payout chart + //https://lh6.googleusercontent.com/-i1hjAJy_kN4/UswKxmhrbPI/AAAAAAAAB1U/82wq_4ZZc-Y/DE6B0895-6FC1-48BE-AC4F-14D1B91AB75B.jpg + //thanks to judge for helping me with this + + public class SlotMachine + { + public const int MaxValue = 5; + + static readonly List> winningCombos = new List>() + { + //three flowers + (arr) => arr.All(a=>a==MaxValue) ? 30 : 0, + //three of the same + (arr) => !arr.Any(a => a != arr[0]) ? 10 : 0, + //two flowers + (arr) => arr.Count(a => a == MaxValue) == 2 ? 4 : 0, + //one flower + (arr) => arr.Any(a => a == MaxValue) ? 1 : 0, + }; + + public static SlotResult Pull() + { + var numbers = new int[3]; + for (int i = 0; i < numbers.Length; i++) + { + numbers[i] = new NadekoRandom().Next(0, MaxValue + 1); + } + int multi = 0; + for (int i = 0; i < winningCombos.Count; i++) + { + multi = winningCombos[i](numbers); + if (multi != 0) + break; + } + + return new SlotResult(numbers, multi); + } + + public struct SlotResult + { + public int[] Numbers { get; } + public int Multiplier { get; } + public SlotResult(int[] nums, int multi) + { + this.Numbers = nums; + this.Multiplier = multi; + } + } + } + + [NadekoCommand, Usage, Description, Aliases] + [OwnerOnly] + public async Task SlotStats() + { + //i remembered to not be a moron + var paid = totalPaidOut; + var bet = totalBet; + + if (bet <= 0) + bet = 1; + + var embed = new EmbedBuilder() + .WithOkColor() + .WithTitle("Slot Stats") + .AddField(efb => efb.WithName("Total Bet").WithValue(bet.ToString()).WithIsInline(true)) + .AddField(efb => efb.WithName("Paid Out").WithValue(paid.ToString()).WithIsInline(true)) + .WithFooter(efb => efb.WithText($"Payout Rate: {paid * 1.0 / bet * 100:f4}%")); + + await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); + } + + [NadekoCommand, Usage, Description, Aliases] + [OwnerOnly] + public async Task SlotTest(int tests = 1000) + { + if (tests <= 0) + return; + //multi vs how many times it occured + var dict = new Dictionary(); + for (int i = 0; i < tests; i++) + { + var res = SlotMachine.Pull(); + if (dict.ContainsKey(res.Multiplier)) + dict[res.Multiplier] += 1; + else + dict.Add(res.Multiplier, 1); + } + + var sb = new StringBuilder(); + const int bet = 1; + int 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]; + } + await Context.Channel.SendConfirmAsync("Slot Test Results", sb.ToString(), + footer: $"Total Bet: {tests * bet} | Payout: {payout * bet} | {payout * 1.0f / tests * 100}%"); + } + + static HashSet runningUsers = new HashSet(); + [NadekoCommand, Usage, Description, Aliases] + public async Task Slot(int amount = 0) + { + if (!runningUsers.Add(Context.User.Id)) + return; + try + { + if (amount < 1) + { + await Context.Channel.SendErrorAsync($"You can't bet less than 1{NadekoBot.BotConfig.CurrencySign}").ConfigureAwait(false); + return; + } + + if (amount > 999) + { + await Context.Channel.SendErrorAsync($"You can't bet more than 999{NadekoBot.BotConfig.CurrencySign}").ConfigureAwait(false); + return; + } + + if (!await CurrencyHandler.RemoveCurrencyAsync(Context.User, "Slot Machine", amount, false)) + return; + Interlocked.Add(ref totalBet, amount); + using (var bgFileStream = new MemoryStream(backgroundBuffer)) + { + var bgImage = new ImageSharp.Image(bgFileStream); + + var result = SlotMachine.Pull(); + int[] numbers = result.Numbers; + using (var bgPixels = bgImage.Lock()) + { + for (int i = 0; i < 3; i++) + { + using (var file = new MemoryStream(emojiBuffer[numbers[i]])) + { + var randomImage = new ImageSharp.Image(file); + using (var toAdd = randomImage.Lock()) + { + for (int j = 0; j < toAdd.Width; j++) + { + for (int k = 0; k < toAdd.Height; k++) + { + var x = 95 + 142 * i + j; + int y = 330 + k; + var toSet = toAdd[j, k]; + if (toSet.A < alphaCutOut) + continue; + bgPixels[x, y] = toAdd[j, k]; + } + } + } + } + } + + var won = amount * result.Multiplier; + var printWon = won; + var n = 0; + do + { + var digit = printWon % 10; + using (var fs = new MemoryStream(numbersBuffer[digit])) + { + var img = new ImageSharp.Image(fs); + using (var pixels = img.Lock()) + { + for (int i = 0; i < pixels.Width; i++) + { + for (int j = 0; j < pixels.Height; j++) + { + if (pixels[i, j].A < alphaCutOut) + continue; + var x = 230 - n * 16 + i; + bgPixels[x, 462 + j] = pixels[i, j]; + } + } + } + } + n++; + } while ((printWon /= 10) != 0); + + var printAmount = amount; + n = 0; + do + { + var digit = printAmount % 10; + using (var fs = new MemoryStream(numbersBuffer[digit])) + { + var img = new ImageSharp.Image(fs); + using (var pixels = img.Lock()) + { + for (int i = 0; i < pixels.Width; i++) + { + for (int j = 0; j < pixels.Height; j++) + { + if (pixels[i, j].A < alphaCutOut) + continue; + var x = 395 - n * 16 + i; + bgPixels[x, 462 + j] = pixels[i, j]; + } + } + } + } + n++; + } while ((printAmount /= 10) != 0); + } + + var msg = "Better luck next time ^_^"; + if (result.Multiplier != 0) + { + await CurrencyHandler.AddCurrencyAsync(Context.User, $"Slot Machine x{result.Multiplier}", amount * result.Multiplier, false); + Interlocked.Add(ref totalPaidOut, amount * result.Multiplier); + if (result.Multiplier == 1) + msg = $"A single {NadekoBot.BotConfig.CurrencySign}, x1 - Try again!"; + else if (result.Multiplier == 4) + msg = $"Good job! Two {NadekoBot.BotConfig.CurrencySign} - bet x4"; + else if (result.Multiplier == 10) + msg = "Wow! Lucky! Three of a kind! x10"; + else if (result.Multiplier == 30) + msg = "WOAAHHHHHH!!! Congratulations!!! x30"; + } + + await Context.Channel.SendFileAsync(bgImage.ToStream(), "result.png", Context.User.Mention + " " + msg + $"\n`Bet:`{amount} `Won:` {amount * result.Multiplier}{NadekoBot.BotConfig.CurrencySign}").ConfigureAwait(false); + } + } + finally + { + var t = Task.Run(async () => + { + await Task.Delay(3000); + runningUsers.Remove(Context.User.Id); + }); + } + } + } + } +} diff --git a/src/NadekoBot/Resources/CommandStrings.Designer.cs b/src/NadekoBot/Resources/CommandStrings.Designer.cs index 717d767e..5fe6ae20 100644 --- a/src/NadekoBot/Resources/CommandStrings.Designer.cs +++ b/src/NadekoBot/Resources/CommandStrings.Designer.cs @@ -7079,6 +7079,87 @@ namespace NadekoBot.Resources { } } + /// + /// Looks up a localized string similar to slot. + /// + public static string slot_cmd { + get { + return ResourceManager.GetString("slot_cmd", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Play Nadeko slots. Max bet is 999. 3 seconds cooldown per user.. + /// + public static string slot_desc { + get { + return ResourceManager.GetString("slot_desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to `{0}slot 5`. + /// + public static string slot_usage { + get { + return ResourceManager.GetString("slot_usage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to slotstats. + /// + public static string slotstats_cmd { + get { + return ResourceManager.GetString("slotstats_cmd", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Shows the total stats of the slot command for this bot's session.. + /// + public static string slotstats_desc { + get { + return ResourceManager.GetString("slotstats_desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to `{0}slotstats`. + /// + public static string slotstats_usage { + get { + return ResourceManager.GetString("slotstats_usage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to slottest. + /// + public static string slottest_cmd { + get { + return ResourceManager.GetString("slottest_cmd", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tests to see how much slots payout for X number of plays.. + /// + public static string slottest_desc { + get { + return ResourceManager.GetString("slottest_desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to `{0}slottest 1000`. + /// + public static string slottest_usage { + get { + return ResourceManager.GetString("slottest_usage", resourceCulture); + } + } + /// /// Looks up a localized string similar to slowmode. /// diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index 1e9ba5d4..460d6b7c 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -2952,4 +2952,31 @@ `{0}startevent flowerreaction` + + slotstats + + + Shows the total stats of the slot command for this bot's session. + + + `{0}slotstats` + + + slottest + + + Tests to see how much slots payout for X number of plays. + + + `{0}slottest 1000` + + + slot + + + Play Nadeko slots. Max bet is 999. 3 seconds cooldown per user. + + + `{0}slot 5` + \ No newline at end of file diff --git a/src/NadekoBot/data/slots/0.png b/src/NadekoBot/data/slots/0.png new file mode 100644 index 00000000..e260e400 Binary files /dev/null and b/src/NadekoBot/data/slots/0.png differ diff --git a/src/NadekoBot/data/slots/1.png b/src/NadekoBot/data/slots/1.png new file mode 100644 index 00000000..4766b854 Binary files /dev/null and b/src/NadekoBot/data/slots/1.png differ diff --git a/src/NadekoBot/data/slots/2.png b/src/NadekoBot/data/slots/2.png new file mode 100644 index 00000000..57c5525a Binary files /dev/null and b/src/NadekoBot/data/slots/2.png differ diff --git a/src/NadekoBot/data/slots/3.png b/src/NadekoBot/data/slots/3.png new file mode 100644 index 00000000..04d6e982 Binary files /dev/null and b/src/NadekoBot/data/slots/3.png differ diff --git a/src/NadekoBot/data/slots/4.png b/src/NadekoBot/data/slots/4.png new file mode 100644 index 00000000..d4941650 Binary files /dev/null and b/src/NadekoBot/data/slots/4.png differ diff --git a/src/NadekoBot/data/slots/5.png b/src/NadekoBot/data/slots/5.png new file mode 100644 index 00000000..bc78e972 Binary files /dev/null and b/src/NadekoBot/data/slots/5.png differ diff --git a/src/NadekoBot/data/slots/6.png b/src/NadekoBot/data/slots/6.png new file mode 100644 index 00000000..b596051d Binary files /dev/null and b/src/NadekoBot/data/slots/6.png differ diff --git a/src/NadekoBot/data/slots/7.png b/src/NadekoBot/data/slots/7.png new file mode 100644 index 00000000..0ffc89b9 Binary files /dev/null and b/src/NadekoBot/data/slots/7.png differ diff --git a/src/NadekoBot/data/slots/8.png b/src/NadekoBot/data/slots/8.png new file mode 100644 index 00000000..48ceed02 Binary files /dev/null and b/src/NadekoBot/data/slots/8.png differ diff --git a/src/NadekoBot/data/slots/9.png b/src/NadekoBot/data/slots/9.png new file mode 100644 index 00000000..b71dceec Binary files /dev/null and b/src/NadekoBot/data/slots/9.png differ diff --git a/src/NadekoBot/data/slots/background.png b/src/NadekoBot/data/slots/background.png new file mode 100644 index 00000000..6ed6026a Binary files /dev/null and b/src/NadekoBot/data/slots/background.png differ diff --git a/src/NadekoBot/data/slots/emojis/0.png b/src/NadekoBot/data/slots/emojis/0.png new file mode 100644 index 00000000..1f99099b Binary files /dev/null and b/src/NadekoBot/data/slots/emojis/0.png differ diff --git a/src/NadekoBot/data/slots/emojis/1.png b/src/NadekoBot/data/slots/emojis/1.png new file mode 100644 index 00000000..7cb68815 Binary files /dev/null and b/src/NadekoBot/data/slots/emojis/1.png differ diff --git a/src/NadekoBot/data/slots/emojis/2.png b/src/NadekoBot/data/slots/emojis/2.png new file mode 100644 index 00000000..c567b887 Binary files /dev/null and b/src/NadekoBot/data/slots/emojis/2.png differ diff --git a/src/NadekoBot/data/slots/emojis/3.png b/src/NadekoBot/data/slots/emojis/3.png new file mode 100644 index 00000000..d6803b20 Binary files /dev/null and b/src/NadekoBot/data/slots/emojis/3.png differ diff --git a/src/NadekoBot/data/slots/emojis/4.png b/src/NadekoBot/data/slots/emojis/4.png new file mode 100644 index 00000000..36dd054c Binary files /dev/null and b/src/NadekoBot/data/slots/emojis/4.png differ diff --git a/src/NadekoBot/data/slots/emojis/5.png b/src/NadekoBot/data/slots/emojis/5.png new file mode 100644 index 00000000..84b4db13 Binary files /dev/null and b/src/NadekoBot/data/slots/emojis/5.png differ