Splitable blackjack (#21)

* Blackjack

* changed blackjack randomness to use player bound randomness.

* blackjack splitting. auto standing on 21. fixed duplicate bust message.

* vibecoded transactions fix

* update to match proper balance modification
This commit is contained in:
CrackmaticSoftware
2026-01-05 16:22:34 +01:00
committed by GitHub
parent a288f3f4eb
commit 3992ff3119
2 changed files with 303 additions and 133 deletions

View File

@@ -8,8 +8,6 @@ using KfChatDotNetBot.Settings;
using KfChatDotNetWsClient.Models.Events;
using Microsoft.EntityFrameworkCore;
using NLog;
using RandN.Compat;
using RandN;
namespace KfChatDotNetBot.Commands.Kasino;
@@ -24,11 +22,11 @@ public class BlackjackCommand : ICommand
new Regex(@"^blackjack (?<amount>\d+\.\d+)$", RegexOptions.IgnoreCase),
new Regex(@"^bj (?<amount>\d+)$", RegexOptions.IgnoreCase),
new Regex(@"^bj (?<amount>\d+\.\d+)$", RegexOptions.IgnoreCase),
new Regex(@"^blackjack (?<action>hit|stand|double)$", RegexOptions.IgnoreCase),
new Regex(@"^bj (?<action>hit|stand|double)$", RegexOptions.IgnoreCase)
new Regex(@"^blackjack (?<action>hit|stand|double|split)$", RegexOptions.IgnoreCase),
new Regex(@"^bj (?<action>hit|stand|double|split)$", RegexOptions.IgnoreCase)
];
public string? HelpText => "!blackjack <amount> or !bj <amount> to start, then !bj hit/stand/double";
public string? HelpText => "!blackjack <amount> or !bj <amount> to start, then !bj hit/stand/double/split";
public UserRight RequiredRight => UserRight.Loser;
public TimeSpan Timeout => TimeSpan.FromSeconds(15);
public RateLimitOptionsModel? RateLimitOptions => new()
@@ -127,9 +125,7 @@ public class BlackjackCommand : ICommand
// Create deck and deal initial hands
var rng = StandardRng.Create();
var random = RandomShim.Create(rng);
var deck = BlackjackHelper.CreateDeck(random);
var deck = BlackjackHelper.CreateDeck(gambler);
var playerHand = new List<Card> { deck[0], deck[2] };
var dealerHand = new List<Card> { deck[1], deck[3] };
@@ -138,10 +134,12 @@ public class BlackjackCommand : ICommand
// Create game state
var newGameState = new BlackjackGameMetaModel
{
PlayerHand = playerHand,
PlayerHands = new List<List<Card>> { playerHand },
DealerHand = dealerHand,
Deck = deck,
HasDoubledDown = false
HasDoubledDown = new List<bool> { false },
CurrentHandIndex = 0,
OriginalWagerAmount = wager
};
// Create incomplete wager
@@ -181,12 +179,15 @@ public class BlackjackCommand : ICommand
var colors = await SettingsProvider.GetMultipleValuesAsync([
BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor
]);
var canSplit = BlackjackHelper.CanSplit(playerHand);
var splitText = canSplit ? " or [B]!bj split[/B]" : "";
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",
$"Use [B]!bj hit[/B] or [B]!bj stand[/B]{splitText} to continue",
true, autoDeleteAfter: cleanupDelay);
}
@@ -234,9 +235,6 @@ public class BlackjackCommand : ICommand
return;
}
var rng = StandardRng.Create();
var random = RandomShim.Create(rng);
switch (action)
{
case "hit":
@@ -246,7 +244,10 @@ public class BlackjackCommand : ICommand
await HandleStand(botInstance, user, gambler, activeWager, currentGameState, cleanupDelay, ctx);
break;
case "double":
await HandleDouble(botInstance, user, gambler, activeWager, currentGameState, random, cleanupDelay, ctx);
await HandleDouble(botInstance, user, gambler, activeWager, currentGameState, cleanupDelay, ctx);
break;
case "split":
await HandleSplit(botInstance, user, gambler, activeWager, currentGameState, cleanupDelay, ctx);
break;
}
}
@@ -254,7 +255,6 @@ public class BlackjackCommand : ICommand
private async Task HandleHit(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
WagerDbModel wager, BlackjackGameMetaModel gameState, TimeSpan cleanupDelay, CancellationToken ctx)
{
if (gameState.Deck.Count == 0)
{
await botInstance.SendChatMessageAsync(
@@ -264,36 +264,56 @@ public class BlackjackCommand : ICommand
return;
}
var currentHand = gameState.PlayerHands[gameState.CurrentHandIndex];
var handLabel = gameState.PlayerHands.Count > 1 ? $" (Hand {gameState.CurrentHandIndex + 1})" : "";
// Draw card
var card = gameState.Deck[0];
gameState.Deck.RemoveAt(0);
gameState.PlayerHand.Add(card);
currentHand.Add(card);
var playerValue = BlackjackHelper.CalculateHandValue(gameState.PlayerHand);
var playerValue = BlackjackHelper.CalculateHandValue(currentHand);
if (playerValue > 21)
{
var redColor = (await SettingsProvider.GetValueAsync(BuiltIn.Keys.KiwiFarmsRedColor)).Value;
// Bust - player loses
var colors = await SettingsProvider.GetMultipleValuesAsync([
BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor
]);
await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()} hit and drew {card}[br]" +
$"[B]Your hand:[/B] {BlackjackHelper.FormatHand(gameState.PlayerHand)} = {playerValue}[br]" +
$"[B][COLOR={redColor}]BUST![/COLOR][/B]",
$"{user.FormatUsername()}{handLabel} hit and drew {card}[br]" +
$"[B]Your hand:[/B] {BlackjackHelper.FormatHand(currentHand)} = {playerValue}[br]" +
$"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}]BUST![/COLOR][/B]",
true, autoDeleteAfter: cleanupDelay);
await ResolveGame(botInstance, user, gambler, wager, gameState, false, cleanupDelay, ctx);
// Move to next hand or resolve game
await MoveToNextHandOrResolve(botInstance, user, gambler, wager, gameState, cleanupDelay, ctx);
return;
}
if (gameState.HasDoubledDown)
// Auto-stand on 21
if (playerValue == 21)
{
await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()}{handLabel} hit and drew {card}[br]" +
$"[B]Your hand:[/B] {BlackjackHelper.FormatHand(currentHand)} = {playerValue}[br]" +
$"[B]Standing on 21[/B]",
true, autoDeleteAfter: cleanupDelay);
await MoveToNextHandOrResolve(botInstance, user, gambler, wager, gameState, cleanupDelay, ctx);
return;
}
if (gameState.HasDoubledDown[gameState.CurrentHandIndex])
{
// 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}",
$"{user.FormatUsername()}{handLabel} hit and drew {card}[br]" +
$"[B]Your hand:[/B] {BlackjackHelper.FormatHand(currentHand)} = {playerValue}",
true, autoDeleteAfter: cleanupDelay);
await HandleStand(botInstance, user, gambler, wager, gameState, cleanupDelay, ctx);
await MoveToNextHandOrResolve(botInstance, user, gambler, wager, gameState, cleanupDelay, ctx);
return;
}
@@ -302,8 +322,8 @@ public class BlackjackCommand : ICommand
await _dbContext.SaveChangesAsync(ctx);
await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()} hit and drew {card}[br]" +
$"[B]Your hand:[/B] {BlackjackHelper.FormatHand(gameState.PlayerHand)} = {playerValue}[br]" +
$"{user.FormatUsername()}{handLabel} hit and drew {card}[br]" +
$"[B]Your hand:[/B] {BlackjackHelper.FormatHand(currentHand)} = {playerValue}[br]" +
$"Use [B]!bj hit[/B] or [B]!bj stand[/B] to continue",
true, autoDeleteAfter: cleanupDelay);
}
@@ -311,35 +331,24 @@ public class BlackjackCommand : ICommand
private async Task HandleStand(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
WagerDbModel wager, BlackjackGameMetaModel gameState, TimeSpan cleanupDelay, CancellationToken ctx)
{
var handLabel = gameState.PlayerHands.Count > 1 ? $" (Hand {gameState.CurrentHandIndex + 1})" : "";
var currentHand = gameState.PlayerHands[gameState.CurrentHandIndex];
var playerValue = BlackjackHelper.CalculateHandValue(currentHand);
// Dealer plays
var dealerValue = BlackjackHelper.CalculateHandValue(gameState.DealerHand);
await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()}{handLabel} stands with {BlackjackHelper.FormatHand(currentHand)} = {playerValue}",
true, autoDeleteAfter: cleanupDelay);
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);
await MoveToNextHandOrResolve(botInstance, user, gambler, wager, gameState, cleanupDelay, ctx);
}
private async Task HandleDouble(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
WagerDbModel wager, BlackjackGameMetaModel gameState, Random random, TimeSpan cleanupDelay, CancellationToken ctx)
WagerDbModel wager, BlackjackGameMetaModel gameState, TimeSpan cleanupDelay, CancellationToken ctx)
{
var currentHand = gameState.PlayerHands[gameState.CurrentHandIndex];
// Check if player can double (only on first action with 2 cards)
if (gameState.PlayerHand.Count != 2)
if (currentHand.Count != 2)
{
await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()}, you can only double down on your first action.",
@@ -348,7 +357,7 @@ public class BlackjackCommand : ICommand
}
// Check if player has enough balance for double
if (gambler.Balance < wager.WagerAmount)
if (gambler.Balance < gameState.OriginalWagerAmount)
{
await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()}, you don't have enough balance to double down.",
@@ -362,110 +371,246 @@ public class BlackjackCommand : ICommand
$"Double down for {wager.Id}", ct: ctx);
wager.WagerAmount *= 2;
wager.WagerEffect -= additionalWager; // Subtract the additional wager
gameState.HasDoubledDown = true;
gameState.HasDoubledDown[gameState.CurrentHandIndex] = true;
await _dbContext.SaveChangesAsync(ctx);
var handLabel = gameState.PlayerHands.Count > 1 ? $" (Hand {gameState.CurrentHandIndex + 1})" : "";
await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()} doubled down! Wager is now {await wager.WagerAmount.FormatKasinoCurrencyAsync()}",
$"{user.FormatUsername()}{handLabel} 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, cleanupDelay, ctx);
}
private async Task HandleSplit(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
WagerDbModel wager, BlackjackGameMetaModel gameState, TimeSpan cleanupDelay, CancellationToken ctx)
{
var currentHand = gameState.PlayerHands[gameState.CurrentHandIndex];
// Check if player can split
if (!BlackjackHelper.CanSplit(currentHand))
{
await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()}, you can only split with two cards of the same rank.",
true, autoDeleteAfter: cleanupDelay);
return;
}
// Check if already split
if (gameState.PlayerHands.Count > 1)
{
await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()}, you can only split once per game.",
true, autoDeleteAfter: cleanupDelay);
return;
}
// Check if player has enough balance
if (gambler.Balance < gameState.OriginalWagerAmount)
{
await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()}, you don't have enough balance to split.",
true, autoDeleteAfter: cleanupDelay);
return;
}
// Check if deck has enough cards
if (gameState.Deck.Count < 2)
{
await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()}, not enough cards in deck to split.",
true, autoDeleteAfter: cleanupDelay);
return;
}
// Need to reload gambler with tracking to modify balance
var trackedGambler = await _dbContext.Gamblers.FirstOrDefaultAsync(g => g.Id == gambler.Id, ctx);
if (trackedGambler == null)
{
throw new InvalidOperationException($"Could not find gambler {gambler.Id}");
}
// Perform the split
var card1 = currentHand[0];
var card2 = currentHand[1];
var hand1 = new List<Card> { card1, gameState.Deck[0] };
var hand2 = new List<Card> { card2, gameState.Deck[1] };
gameState.Deck.RemoveRange(0, 2);
gameState.PlayerHands = new List<List<Card>> { hand1, hand2 };
gameState.HasDoubledDown = new List<bool> { false, false };
gameState.CurrentHandIndex = 0;
// Charge for the split
var additionalWager = gameState.OriginalWagerAmount;
trackedGambler.Balance -= additionalWager;
wager.WagerAmount += additionalWager;
wager.WagerEffect -= additionalWager;
wager.GameMeta = JsonSerializer.Serialize(gameState);
await _dbContext.SaveChangesAsync(ctx);
var value1 = BlackjackHelper.CalculateHandValue(hand1);
var value2 = BlackjackHelper.CalculateHandValue(hand2);
await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()} split their hand! Total wager: {await wager.WagerAmount.FormatKasinoCurrencyAsync()}[br]" +
$"[B]Hand 1:[/B] {BlackjackHelper.FormatHand(hand1)} = {value1}[br]" +
$"[B]Hand 2:[/B] {BlackjackHelper.FormatHand(hand2)} = {value2}[br]" +
$"Playing Hand 1 - Use [B]!bj hit[/B] or [B]!bj stand[/B]",
true, autoDeleteAfter: cleanupDelay);
}
private async Task MoveToNextHandOrResolve(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
WagerDbModel wager, BlackjackGameMetaModel gameState, TimeSpan cleanupDelay, CancellationToken ctx)
{
gameState.CurrentHandIndex++;
if (gameState.CurrentHandIndex < gameState.PlayerHands.Count)
{
// Move to next hand
wager.GameMeta = JsonSerializer.Serialize(gameState);
await _dbContext.SaveChangesAsync(ctx);
var nextHand = gameState.PlayerHands[gameState.CurrentHandIndex];
var nextValue = BlackjackHelper.CalculateHandValue(nextHand);
await botInstance.SendChatMessageAsync(
$"Playing Hand {gameState.CurrentHandIndex + 1}[br]" +
$"[B]Your hand:[/B] {BlackjackHelper.FormatHand(nextHand)} = {nextValue}[br]" +
$"Use [B]!bj hit[/B] or [B]!bj stand[/B] to continue",
true, autoDeleteAfter: cleanupDelay);
}
else
{
// All hands played, dealer plays and resolve
await PlayDealerAndResolve(botInstance, user, gambler, wager, gameState, cleanupDelay, ctx);
}
}
private async Task PlayDealerAndResolve(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
WagerDbModel wager, BlackjackGameMetaModel gameState, TimeSpan cleanupDelay, CancellationToken ctx)
{
// Check if all hands busted
bool allHandsBusted = gameState.PlayerHands.All(hand => BlackjackHelper.CalculateHandValue(hand) > 21);
if (!allHandsBusted)
{
// 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 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;
decimal totalEffect = 0;
var colors = await SettingsProvider.GetMultipleValuesAsync([
BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor
]);
// Determine outcome
if (playerBlackjack && dealerBlackjack)
var message = $"🃏 {user.FormatUsername()}'s blackjack game:[br]";
// Process each hand
for (int i = 0; i < gameState.PlayerHands.Count; i++)
{
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]";
var hand = gameState.PlayerHands[i];
var playerValue = BlackjackHelper.CalculateHandValue(hand);
var playerBlackjack = BlackjackHelper.IsBlackjack(hand);
var handWager = gameState.OriginalWagerAmount;
var handLabel = gameState.PlayerHands.Count > 1 ? $" {i + 1}" : "";
message += $"[B]Your hand{handLabel}:[/B] {BlackjackHelper.FormatHand(hand)} = {playerValue}[br]";
decimal handEffect;
string result;
// Determine outcome for this hand
if (playerBlackjack && dealerBlackjack)
{
handEffect = 0;
result = $"[B][COLOR=orange]PUSH![/COLOR][/B]";
}
else if (playerBlackjack)
{
handEffect = handWager * 1.5m;
result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsGreenColor].Value}]BLACKJACK! +{await handEffect.FormatKasinoCurrencyAsync()}[/COLOR][/B]";
}
else if (dealerBlackjack)
{
handEffect = -handWager;
result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}]DEALER BLACKJACK! -{await handWager.FormatKasinoCurrencyAsync()}[/COLOR][/B]";
}
else if (playerValue > 21)
{
handEffect = -handWager;
result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}]BUST! -{await handWager.FormatKasinoCurrencyAsync()}[/COLOR][/B]";
}
else if (dealerValue > 21)
{
handEffect = handWager;
result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsGreenColor].Value}]DEALER BUST! +{await handEffect.FormatKasinoCurrencyAsync()}[/COLOR][/B]";
}
else if (playerValue > dealerValue)
{
handEffect = handWager;
result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsGreenColor].Value}]WIN! +{await handEffect.FormatKasinoCurrencyAsync()}[/COLOR][/B]";
}
else if (playerValue < dealerValue)
{
handEffect = -handWager;
result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}]LOSE! -{await handWager.FormatKasinoCurrencyAsync()}[/COLOR][/B]";
}
else
{
handEffect = 0;
result = $"[B][COLOR=orange]PUSH![/COLOR][/B]";
}
message += $"{result}[br]";
totalEffect += handEffect;
}
message += $"[B]Dealer:[/B] {BlackjackHelper.FormatHand(gameState.DealerHand)} = {dealerValue}[br]";
// Update wager to complete
wager.IsComplete = true;
wager.WagerEffect = finalEffect;
wager.Multiplier = multiplier;
wager.WagerEffect = totalEffect;
wager.Multiplier = (totalEffect + wager.WagerAmount) / wager.WagerAmount;
// Update balance and create transaction in same context
await _dbContext.SaveChangesAsync(ctx);
var balanceAdjustment = finalEffect + wager.WagerAmount;
var balanceAdjustment = totalEffect + wager.WagerAmount;
var newBalance = await Money.ModifyBalanceAsync(gambler.Id, balanceAdjustment, TransactionSourceEventType.Gambling,
$"Blackjack outcome from wager {wager.Id}", null, 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 newBalance.FormatKasinoCurrencyAsync()}";
message += $"[B]Net:[/B] {(totalEffect >= 0 ? "+" : "")}{await totalEffect.FormatKasinoCurrencyAsync()} | Balance: {await newBalance.FormatKasinoCurrencyAsync()}";
await botInstance.SendChatMessageAsync(message, true, autoDeleteAfter: cleanupDelay);
}

View File

@@ -1,11 +1,14 @@
namespace KfChatDotNetBot.Models;
using KfChatDotNetBot.Models.DbModels;
using Money = KfChatDotNetBot.Services.Money;
namespace KfChatDotNetBot.Models;
public class BlackjackGameMetaModel
{
/// <summary>
/// Player's hand
/// Player's hands (multiple if split)
/// </summary>
public required List<Card> PlayerHand { get; set; }
public required List<List<Card>> PlayerHands { get; set; }
/// <summary>
/// Dealer's hand
@@ -18,9 +21,19 @@ public class BlackjackGameMetaModel
public required List<Card> Deck { get; set; }
/// <summary>
/// Whether player has doubled down (can only hit once more)
/// Whether each hand has doubled down (can only hit once more)
/// </summary>
public bool HasDoubledDown { get; set; } = false;
public required List<bool> HasDoubledDown { get; set; }
/// <summary>
/// Current hand being played (for split hands)
/// </summary>
public int CurrentHandIndex { get; set; } = 0;
/// <summary>
/// Original wager amount (per hand)
/// </summary>
public decimal OriginalWagerAmount { get; set; }
}
public class Card
@@ -65,7 +78,7 @@ public static class BlackjackHelper
/// <summary>
/// Create a new shuffled deck
/// </summary>
public static List<Card> CreateDeck(Random random)
public static List<Card> CreateDeck(GamblerDbModel gambler)
{
var deck = new List<Card>();
foreach (var suit in Suits)
@@ -79,7 +92,7 @@ public static class BlackjackHelper
// Shuffle using Fisher-Yates
for (int i = deck.Count - 1; i > 0; i--)
{
int j = random.Next(0, i + 1);
int j = Money.GetRandomNumber(gambler, 0, i + 1);
(deck[i], deck[j]) = (deck[j], deck[i]);
}
@@ -125,6 +138,18 @@ public static class BlackjackHelper
return hand.Count == 2 && CalculateHandValue(hand) == 21;
}
/// <summary>
/// Check if a hand can be split (two cards of same rank)
/// </summary>
public static bool CanSplit(List<Card> hand)
{
if (hand.Count != 2)
return false;
// Check if both cards have the same value (not rank, to allow 10/J/Q/K splits)
return hand[0].GetValue() == hand[1].GetValue();
}
/// <summary>
/// Format hand for display
/// </summary>