From df869c6e82b80080a5cecc9ec0d7bb4329676176 Mon Sep 17 00:00:00 2001 From: CrackmaticSoftware <248342529+CrackmaticSoftware@users.noreply.github.com> Date: Sat, 3 Jan 2026 00:12:46 +0100 Subject: [PATCH] Blackjack (#18) * Blackjack * idk --- .../Commands/Kasino/BlackjackCommand.cs | 534 ++++++++++++++++++ KfChatDotNetBot/Extensions/Extensions.cs | 27 + .../Models/BlackjackGameMetaModel.cs | 149 +++++ .../Models/DbModels/MoneyDbModels.cs | 3 +- 4 files changed, 712 insertions(+), 1 deletion(-) create mode 100644 KfChatDotNetBot/Commands/Kasino/BlackjackCommand.cs create mode 100644 KfChatDotNetBot/Models/BlackjackGameMetaModel.cs diff --git a/KfChatDotNetBot/Commands/Kasino/BlackjackCommand.cs b/KfChatDotNetBot/Commands/Kasino/BlackjackCommand.cs new file mode 100644 index 0000000..670694a --- /dev/null +++ b/KfChatDotNetBot/Commands/Kasino/BlackjackCommand.cs @@ -0,0 +1,534 @@ +using System.Text.RegularExpressions; +using KfChatDotNetBot.Extensions; +using KfChatDotNetBot.Models; +using KfChatDotNetBot.Models.DbModels; +using KfChatDotNetBot.Services; +using KfChatDotNetBot.Settings; +using KfChatDotNetWsClient.Models.Events; +using Microsoft.EntityFrameworkCore; +using NLog; +using RandN.Compat; +using RandN; + +namespace KfChatDotNetBot.Commands.Kasino; + +[KasinoCommand] +[WagerCommand] +public class BlackjackCommand : ICommand +{ + private static readonly TimeSpan GameTimeout = TimeSpan.FromMinutes(5); + + public List Patterns => [ + new Regex(@"^blackjack (?\d+)$", RegexOptions.IgnoreCase), + new Regex(@"^blackjack (?\d+\.\d+)$", RegexOptions.IgnoreCase), + new Regex(@"^bj (?\d+)$", RegexOptions.IgnoreCase), + new Regex(@"^bj (?\d+\.\d+)$", RegexOptions.IgnoreCase), + new Regex(@"^blackjack (?hit|stand|double)$", RegexOptions.IgnoreCase), + new Regex(@"^bj (?hit|stand|double)$", RegexOptions.IgnoreCase) + ]; + + public string? HelpText => "!blackjack or !bj to start, then !bj hit/stand/double"; + public UserRight RequiredRight => UserRight.Loser; + public TimeSpan Timeout => TimeSpan.FromSeconds(15); + public RateLimitOptionsModel? RateLimitOptions => new() + { + MaxInvocations = 5, + Window = TimeSpan.FromSeconds(20), + Flags = RateLimitFlags.NoAutoDeleteCooldownResponse + }; + + public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, + CancellationToken ctx) + { + var cleanupDelay = TimeSpan.FromMilliseconds( + (await SettingsProvider.GetValueAsync(BuiltIn.Keys.KasinoDiceCleanupDelay)).ToType()); + + try + { + // Check if this is a new game or continuing existing game + if (arguments.TryGetValue("amount", out var amountGroup)) + { + await StartNewGame(botInstance, user, amountGroup.Value, cleanupDelay, ctx); + } + else if (arguments.TryGetValue("action", out var actionGroup)) + { + await ContinueGame(botInstance, user, actionGroup.Value.ToLower(), cleanupDelay, ctx); + } + } + catch (Exception ex) + { + await botInstance.SendChatMessageAsync( + $"[DEBUG] Blackjack error for {user.FormatUsername()}: {ex.Message}", true); + throw; + } + } + + + private async Task StartNewGame(ChatBot botInstance, UserDbModel user, string amountStr, + TimeSpan cleanupDelay, CancellationToken ctx) + { + var wager = Convert.ToDecimal(amountStr); + var gambler = await Money.GetGamblerEntityAsync(user.Id, ct: ctx); + + if (gambler == null) + { + throw new InvalidOperationException($"Caught a null when retrieving gambler for {user.KfUsername}"); + } + + // Check if user has enough balance + if (gambler.Balance < wager) + { + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, your balance of {await gambler.Balance.FormatKasinoCurrencyAsync()} isn't enough for this wager.", + true, autoDeleteAfter: cleanupDelay); + return; + } + + // Check for existing incomplete blackjack game + await using var db = new ApplicationDbContext(); + var existingGame = await db.Wagers + .Where(w => w.Gambler.Id == gambler.Id && + w.Game == WagerGame.Blackjack && + !w.IsComplete) + .OrderByDescending(w => w.Id) + .FirstOrDefaultAsync(ctx); + + if (existingGame != null) + { + if (existingGame.GameMeta == null) + { + // Mark as complete with loss and continue + db.Attach(existingGame); + existingGame.IsComplete = true; + existingGame.WagerEffect = -existingGame.WagerAmount; + existingGame.Multiplier = 0m; + await db.SaveChangesAsync(ctx); + } + else + { + var existingGameState = existingGame.GameMeta.JsonDeserialize(); + + if (existingGameState == null) + { + db.Attach(existingGame); + existingGame.IsComplete = true; + existingGame.WagerEffect = -existingGame.WagerAmount; + existingGame.Multiplier = 0m; + await db.SaveChangesAsync(ctx); + } + else + { + // Check if game has timed out + var timeSinceStart = DateTimeOffset.UtcNow - existingGameState.GameStarted; + + if (timeSinceStart > GameTimeout) + { + await ForfeitGame(botInstance, user, gambler, existingGame, cleanupDelay, ctx); + } + else + { + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, you already have an active blackjack game. Use !bj hit or !bj stand to continue.", + true, autoDeleteAfter: cleanupDelay); + return; + } + } + } + } + + + // Create deck and deal initial hands + var rng = StandardRng.Create(); + var random = RandomShim.Create(rng); + var deck = BlackjackHelper.CreateDeck(random); + + var playerHand = new List { deck[0], deck[2] }; + var dealerHand = new List { deck[1], deck[3] }; + deck.RemoveRange(0, 4); + + // Create game state + var newGameState = new BlackjackGameMetaModel + { + WagerId = 0, // Will be set after wager is created + PlayerHand = playerHand, + DealerHand = dealerHand, + Deck = deck, + GameStarted = DateTimeOffset.UtcNow, + HasDoubledDown = false + }; + + // Create incomplete wager + var newBalance = await Money.NewWagerAsync( + gambler.Id, + wager, + -wager, // This will be the effect for incomplete wagers + WagerGame.Blackjack, + autoModifyBalance: true, + gameMeta: newGameState, + isComplete: false, + ct: ctx + ); + + // Update wager ID in game state + var createdWager = await db.Wagers + .Where(w => w.Gambler.Id == gambler.Id && w.Game == WagerGame.Blackjack && !w.IsComplete) + .OrderByDescending(w => w.Id) + .FirstAsync(ctx); + + newGameState.WagerId = createdWager.Id; + db.Attach(createdWager); + createdWager.GameMeta = newGameState.JsonSerialize(); + await db.SaveChangesAsync(ctx); + + // Check for immediate blackjacks + var playerValue = BlackjackHelper.CalculateHandValue(playerHand); + var dealerValue = BlackjackHelper.CalculateHandValue(dealerHand); + var playerBlackjack = BlackjackHelper.IsBlackjack(playerHand); + var dealerBlackjack = BlackjackHelper.IsBlackjack(dealerHand); + + if (playerBlackjack || dealerBlackjack) + { + await ResolveGame(botInstance, user, gambler, createdWager, newGameState, true, cleanupDelay, ctx); + return; + } + + // Display initial game state + var colors = await SettingsProvider.GetMultipleValuesAsync([ + BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor + ]); + + await botInstance.SendChatMessageAsync( + $"🃏 {user.FormatUsername()} started blackjack with {await wager.FormatKasinoCurrencyAsync()}[br]" + + $"[B]Your hand:[/B] {BlackjackHelper.FormatHand(playerHand)} = {playerValue}[br]" + + $"[B]Dealer:[/B] {BlackjackHelper.FormatHand(dealerHand, hideFirstCard: true)}[br]" + + $"Use [B]!bj hit[/B] or [B]!bj stand[/B] to continue", + true, autoDeleteAfter: cleanupDelay); + } + + private async Task ContinueGame(ChatBot botInstance, UserDbModel user, string action, + TimeSpan cleanupDelay, CancellationToken ctx) + { + var gambler = await Money.GetGamblerEntityAsync(user.Id, ct: ctx); + + if (gambler == null) + { + throw new InvalidOperationException($"Caught a null when retrieving gambler for {user.KfUsername}"); + } + + // Find active game + await using var db = new ApplicationDbContext(); + var activeWager = await db.Wagers + .Where(w => w.Gambler.Id == gambler.Id && + w.Game == WagerGame.Blackjack && + !w.IsComplete) + .OrderByDescending(w => w.Id) + .FirstOrDefaultAsync(ctx); + + if (activeWager == null) + { + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, you don't have an active blackjack game. Start one with !bj ", + true, autoDeleteAfter: cleanupDelay); + return; + } + + if (activeWager.GameMeta == null) + { + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, your game data is corrupted. Please start a new game.", + true, autoDeleteAfter: cleanupDelay); + return; + } + + var currentGameState = activeWager.GameMeta.JsonDeserialize(); + if (currentGameState == null) + { + + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, your game data is corrupted. Please start a new game.", + true, autoDeleteAfter: cleanupDelay); + return; + } + + // Check timeout + var timeSinceStart = DateTimeOffset.UtcNow - currentGameState.GameStarted; + if (timeSinceStart > GameTimeout) + { + await ForfeitGame(botInstance, user, gambler, activeWager, cleanupDelay, ctx); + return; + } + + var rng = StandardRng.Create(); + var random = RandomShim.Create(rng); + + switch (action) + { + case "hit": + await HandleHit(botInstance, user, gambler, activeWager, currentGameState, random, cleanupDelay, ctx); + break; + case "stand": + await HandleStand(botInstance, user, gambler, activeWager, currentGameState, cleanupDelay, ctx); + break; + case "double": + await HandleDouble(botInstance, user, gambler, activeWager, currentGameState, random, cleanupDelay, ctx); + break; + } + } + + private async Task HandleHit(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler, + WagerDbModel wager, BlackjackGameMetaModel gameState, Random random, TimeSpan cleanupDelay, CancellationToken ctx) + { + + if (gameState.Deck.Count == 0) + { + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, game error: no cards left in deck. Game forfeited.", + true, autoDeleteAfter: cleanupDelay); + await ForfeitGame(botInstance, user, gambler, wager, cleanupDelay, ctx); + return; + } + + // Draw card + var card = gameState.Deck[0]; + gameState.Deck.RemoveAt(0); + gameState.PlayerHand.Add(card); + + var playerValue = BlackjackHelper.CalculateHandValue(gameState.PlayerHand); + + if (playerValue > 21) + { + // Bust - player loses + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()} hit and drew {card}[br]" + + $"[B]Your hand:[/B] {BlackjackHelper.FormatHand(gameState.PlayerHand)} = {playerValue}[br]" + + $"[B][COLOR=red]BUST![/COLOR][/B]", + true, autoDeleteAfter: cleanupDelay); + + await ResolveGame(botInstance, user, gambler, wager, gameState, false, cleanupDelay, ctx); + } + else if (gameState.HasDoubledDown) + { + // Auto-stand after double down hit + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()} hit and drew {card}[br]" + + $"[B]Your hand:[/B] {BlackjackHelper.FormatHand(gameState.PlayerHand)} = {playerValue}", + true, autoDeleteAfter: cleanupDelay); + + await HandleStand(botInstance, user, gambler, wager, gameState, cleanupDelay, ctx); + } + else + { + // Continue game + await using var db = new ApplicationDbContext(); + db.Attach(wager); + wager.GameMeta = gameState.JsonSerialize(); + await db.SaveChangesAsync(ctx); + + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()} hit and drew {card}[br]" + + $"[B]Your hand:[/B] {BlackjackHelper.FormatHand(gameState.PlayerHand)} = {playerValue}[br]" + + $"Use [B]!bj hit[/B] or [B]!bj stand[/B] to continue", + true, autoDeleteAfter: cleanupDelay); + } + } + + private async Task HandleStand(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler, + WagerDbModel wager, BlackjackGameMetaModel gameState, TimeSpan cleanupDelay, CancellationToken ctx) + { + + // Dealer plays + var dealerValue = BlackjackHelper.CalculateHandValue(gameState.DealerHand); + + while (dealerValue < 17) + { + if (gameState.Deck.Count == 0) + { + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, game error: dealer ran out of cards. Game forfeited.", + true, autoDeleteAfter: cleanupDelay); + await ForfeitGame(botInstance, user, gambler, wager, cleanupDelay, ctx); + return; + } + + var card = gameState.Deck[0]; + gameState.Deck.RemoveAt(0); + gameState.DealerHand.Add(card); + dealerValue = BlackjackHelper.CalculateHandValue(gameState.DealerHand); + } + + await ResolveGame(botInstance, user, gambler, wager, gameState, false, cleanupDelay, ctx); + } + + private async Task HandleDouble(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler, + WagerDbModel wager, BlackjackGameMetaModel gameState, Random random, TimeSpan cleanupDelay, CancellationToken ctx) + { + // Check if player can double (only on first action with 2 cards) + if (gameState.PlayerHand.Count != 2) + { + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, you can only double down on your first action.", + true, autoDeleteAfter: cleanupDelay); + return; + } + + // Check if player has enough balance for double + if (gambler.Balance < wager.WagerAmount) + { + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, you don't have enough balance to double down.", + true, autoDeleteAfter: cleanupDelay); + return; + } + + // Double the wager + await using var db = new ApplicationDbContext(); + db.Attach(wager); + db.Attach(gambler); + + var additionalWager = wager.WagerAmount; + gambler.Balance -= additionalWager; + wager.WagerAmount *= 2; + wager.WagerEffect -= additionalWager; // Subtract the additional wager + + gameState.HasDoubledDown = true; + + await db.SaveChangesAsync(ctx); + + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()} doubled down! Wager is now {await wager.WagerAmount.FormatKasinoCurrencyAsync()}", + true, autoDeleteAfter: cleanupDelay); + + // Draw one card and auto-stand + await HandleHit(botInstance, user, gambler, wager, gameState, random, cleanupDelay, ctx); + } + + private async Task ResolveGame(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler, + WagerDbModel wager, BlackjackGameMetaModel gameState, bool immediateResolution, + TimeSpan cleanupDelay, CancellationToken ctx) + { + var playerValue = BlackjackHelper.CalculateHandValue(gameState.PlayerHand); + var dealerValue = BlackjackHelper.CalculateHandValue(gameState.DealerHand); + var playerBlackjack = BlackjackHelper.IsBlackjack(gameState.PlayerHand); + var dealerBlackjack = BlackjackHelper.IsBlackjack(gameState.DealerHand); + + decimal finalEffect; + decimal multiplier; + string result; + + var colors = await SettingsProvider.GetMultipleValuesAsync([ + BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor + ]); + + // Determine outcome + if (playerBlackjack && dealerBlackjack) + { + finalEffect = 0; + multiplier = 1m; + result = $"[B][COLOR=orange]PUSH![/COLOR][/B] Both have blackjack"; + } + else if (playerBlackjack) + { + finalEffect = wager.WagerAmount * 1.5m; + multiplier = 2.5m; + result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsGreenColor].Value}]BLACKJACK![/COLOR][/B]"; + } + else if (dealerBlackjack) + { + finalEffect = -wager.WagerAmount; + multiplier = 0m; + result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}]DEALER BLACKJACK![/COLOR][/B]"; + } + else if (playerValue > 21) + { + finalEffect = -wager.WagerAmount; + multiplier = 0m; + result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}]BUST![/COLOR][/B]"; + } + else if (dealerValue > 21) + { + finalEffect = wager.WagerAmount; + multiplier = 2m; + result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsGreenColor].Value}]DEALER BUST - YOU WIN![/COLOR][/B]"; + } + else if (playerValue > dealerValue) + { + finalEffect = wager.WagerAmount; + multiplier = 2m; + result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsGreenColor].Value}]YOU WIN![/COLOR][/B]"; + } + else if (playerValue < dealerValue) + { + finalEffect = -wager.WagerAmount; + multiplier = 0m; + result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}]DEALER WINS![/COLOR][/B]"; + } + else + { + finalEffect = 0; + multiplier = 1m; + result = $"[B][COLOR=orange]PUSH![/COLOR][/B]"; + } + + // Update wager to complete + await using var db = new ApplicationDbContext(); + db.Attach(wager); + db.Attach(gambler); + + wager.IsComplete = true; + wager.WagerEffect = finalEffect; + wager.Multiplier = multiplier; + + var balanceAdjustment = finalEffect + wager.WagerAmount; + gambler.Balance += balanceAdjustment; + + // Create transaction for the outcome + await db.Transactions.AddAsync(new TransactionDbModel + { + Gambler = gambler, + EventSource = TransactionSourceEventType.Gambling, + Time = DateTimeOffset.UtcNow, + Effect = balanceAdjustment, + Comment = $"Blackjack outcome from wager {wager.Id}", + NewBalance = gambler.Balance, + TimeUnixEpochSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + }, ctx); + + await db.SaveChangesAsync(ctx); + + // Display result + var message = $"🃏 {user.FormatUsername()}'s blackjack game:[br]" + + $"[B]Your hand:[/B] {BlackjackHelper.FormatHand(gameState.PlayerHand)} = {playerValue}[br]" + + $"[B]Dealer:[/B] {BlackjackHelper.FormatHand(gameState.DealerHand)} = {dealerValue}[br]" + + $"{result}[br]"; + + if (finalEffect > 0) + { + message += $"You won {await finalEffect.FormatKasinoCurrencyAsync()}! "; + } + else if (finalEffect < 0) + { + message += $"You lost {await Math.Abs(finalEffect).FormatKasinoCurrencyAsync()}! "; + } + + message += $"Balance: {await gambler.Balance.FormatKasinoCurrencyAsync()}"; + + await botInstance.SendChatMessageAsync(message, true, autoDeleteAfter: cleanupDelay); + } + + private async Task ForfeitGame(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler, + WagerDbModel wager, TimeSpan cleanupDelay, CancellationToken ctx) + { + await using var db = new ApplicationDbContext(); + db.Attach(wager); + + wager.IsComplete = true; + wager.WagerEffect = -wager.WagerAmount; + wager.Multiplier = 0m; + + await db.SaveChangesAsync(ctx); + + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, your blackjack game timed out and you forfeited {await wager.WagerAmount.FormatKasinoCurrencyAsync()}", + true, autoDeleteAfter: cleanupDelay); + } +} \ No newline at end of file diff --git a/KfChatDotNetBot/Extensions/Extensions.cs b/KfChatDotNetBot/Extensions/Extensions.cs index f9eca18..f6c6759 100644 --- a/KfChatDotNetBot/Extensions/Extensions.cs +++ b/KfChatDotNetBot/Extensions/Extensions.cs @@ -1,6 +1,7 @@ using System.Text; using System.Text.RegularExpressions; using KfChatDotNetBot.Models.DbModels; +using Newtonsoft.Json; namespace KfChatDotNetBot.Extensions; @@ -137,4 +138,30 @@ public static class Extensions { return $"@{user.KfUsername}"; } + + /// + /// Deserialize a JSON string to the specified type using Newtonsoft.Json + /// + /// Type to deserialize to + /// JSON string to deserialize + /// Deserialized object or null if string is null/empty + public static T? JsonDeserialize(this string? jsonString) where T : class + { + if (string.IsNullOrEmpty(jsonString)) + { + return null; + } + + return JsonConvert.DeserializeObject(jsonString); + } + + /// + /// Serialize an object to JSON string using Newtonsoft.Json + /// + /// Object to serialize + /// JSON string representation + public static string JsonSerialize(this object obj) + { + return JsonConvert.SerializeObject(obj, Formatting.Indented); + } } \ No newline at end of file diff --git a/KfChatDotNetBot/Models/BlackjackGameMetaModel.cs b/KfChatDotNetBot/Models/BlackjackGameMetaModel.cs new file mode 100644 index 0000000..818d88c --- /dev/null +++ b/KfChatDotNetBot/Models/BlackjackGameMetaModel.cs @@ -0,0 +1,149 @@ +namespace KfChatDotNetBot.Models; + +public class BlackjackGameMetaModel +{ + /// + /// The wager ID associated with this game + /// + public required int WagerId { get; set; } + + /// + /// Player's hand + /// + public required List PlayerHand { get; set; } + + /// + /// Dealer's hand + /// + public required List DealerHand { get; set; } + + /// + /// Remaining cards in the deck + /// + public required List Deck { get; set; } + + /// + /// When the game was started + /// + public required DateTimeOffset GameStarted { get; set; } + + /// + /// Whether player has doubled down (can only hit once more) + /// + public bool HasDoubledDown { get; set; } = false; +} + +public class Card +{ + /// + /// Card rank (2-10, J, Q, K, A) + /// + public required string Rank { get; set; } + + /// + /// Card suit (♠, ♥, ♦, ♣) + /// + public required string Suit { get; set; } + + /// + /// Get the blackjack value of this card + /// + public int GetValue() + { + return Rank switch + { + "A" => 11, // Aces are handled specially in hand calculation + "K" or "Q" or "J" => 10, + _ => int.Parse(Rank) + }; + } + + /// + /// Display card as string + /// + public override string ToString() + { + return $"{Rank}{Suit}"; + } +} + +public static class BlackjackHelper +{ + private static readonly string[] Ranks = { "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A" }; + private static readonly string[] Suits = { "♠", "♥", "♦", "♣" }; + + /// + /// Create a new shuffled deck + /// + public static List CreateDeck(Random random) + { + var deck = new List(); + foreach (var suit in Suits) + { + foreach (var rank in Ranks) + { + deck.Add(new Card { Rank = rank, Suit = suit }); + } + } + + // Shuffle using Fisher-Yates + for (int i = deck.Count - 1; i > 0; i--) + { + int j = random.Next(0, i + 1); + (deck[i], deck[j]) = (deck[j], deck[i]); + } + + return deck; + } + + /// + /// Calculate hand value with proper Ace handling + /// + public static int CalculateHandValue(List hand) + { + int value = 0; + int aces = 0; + + foreach (var card in hand) + { + if (card.Rank == "A") + { + aces++; + value += 11; + } + else + { + value += card.GetValue(); + } + } + + // Convert Aces from 11 to 1 if needed to avoid bust + while (value > 21 && aces > 0) + { + value -= 10; + aces--; + } + + return value; + } + + /// + /// Check if hand is blackjack (21 with 2 cards) + /// + public static bool IsBlackjack(List hand) + { + return hand.Count == 2 && CalculateHandValue(hand) == 21; + } + + /// + /// Format hand for display + /// + public static string FormatHand(List hand, bool hideFirstCard = false) + { + if (hideFirstCard && hand.Count > 0) + { + return $"[HIDDEN] {string.Join(" ", hand.Skip(1).Select(c => c.ToString()))}"; + } + return string.Join(" ", hand.Select(c => c.ToString())); + } +} \ No newline at end of file diff --git a/KfChatDotNetBot/Models/DbModels/MoneyDbModels.cs b/KfChatDotNetBot/Models/DbModels/MoneyDbModels.cs index eae0b09..7f870e4 100644 --- a/KfChatDotNetBot/Models/DbModels/MoneyDbModels.cs +++ b/KfChatDotNetBot/Models/DbModels/MoneyDbModels.cs @@ -291,7 +291,8 @@ public enum WagerGame [Description("Guess what number I'm thinking of")] GuessWhatNumber, Wheel, - Slots + Slots, + Blackjack } public enum GamblerState