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 KfChatDotNetWsClient.Models.Events;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NLog; using NLog;
using RandN.Compat;
using RandN;
namespace KfChatDotNetBot.Commands.Kasino; namespace KfChatDotNetBot.Commands.Kasino;
@@ -24,11 +22,11 @@ public class BlackjackCommand : ICommand
new Regex(@"^blackjack (?<amount>\d+\.\d+)$", RegexOptions.IgnoreCase), new Regex(@"^blackjack (?<amount>\d+\.\d+)$", RegexOptions.IgnoreCase),
new Regex(@"^bj (?<amount>\d+)$", RegexOptions.IgnoreCase), new Regex(@"^bj (?<amount>\d+)$", RegexOptions.IgnoreCase),
new Regex(@"^bj (?<amount>\d+\.\d+)$", RegexOptions.IgnoreCase), new Regex(@"^bj (?<amount>\d+\.\d+)$", RegexOptions.IgnoreCase),
new Regex(@"^blackjack (?<action>hit|stand|double)$", RegexOptions.IgnoreCase), new Regex(@"^blackjack (?<action>hit|stand|double|split)$", RegexOptions.IgnoreCase),
new Regex(@"^bj (?<action>hit|stand|double)$", 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 UserRight RequiredRight => UserRight.Loser;
public TimeSpan Timeout => TimeSpan.FromSeconds(15); public TimeSpan Timeout => TimeSpan.FromSeconds(15);
public RateLimitOptionsModel? RateLimitOptions => new() public RateLimitOptionsModel? RateLimitOptions => new()
@@ -127,9 +125,7 @@ public class BlackjackCommand : ICommand
// Create deck and deal initial hands // Create deck and deal initial hands
var rng = StandardRng.Create(); var deck = BlackjackHelper.CreateDeck(gambler);
var random = RandomShim.Create(rng);
var deck = BlackjackHelper.CreateDeck(random);
var playerHand = new List<Card> { deck[0], deck[2] }; var playerHand = new List<Card> { deck[0], deck[2] };
var dealerHand = new List<Card> { deck[1], deck[3] }; var dealerHand = new List<Card> { deck[1], deck[3] };
@@ -138,10 +134,12 @@ public class BlackjackCommand : ICommand
// Create game state // Create game state
var newGameState = new BlackjackGameMetaModel var newGameState = new BlackjackGameMetaModel
{ {
PlayerHand = playerHand, PlayerHands = new List<List<Card>> { playerHand },
DealerHand = dealerHand, DealerHand = dealerHand,
Deck = deck, Deck = deck,
HasDoubledDown = false HasDoubledDown = new List<bool> { false },
CurrentHandIndex = 0,
OriginalWagerAmount = wager
}; };
// Create incomplete wager // Create incomplete wager
@@ -181,12 +179,15 @@ public class BlackjackCommand : ICommand
var colors = await SettingsProvider.GetMultipleValuesAsync([ var colors = await SettingsProvider.GetMultipleValuesAsync([
BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor
]); ]);
var canSplit = BlackjackHelper.CanSplit(playerHand);
var splitText = canSplit ? " or [B]!bj split[/B]" : "";
await botInstance.SendChatMessageAsync( await botInstance.SendChatMessageAsync(
$"🃏 {user.FormatUsername()} started blackjack with {await wager.FormatKasinoCurrencyAsync()}[br]" + $"🃏 {user.FormatUsername()} started blackjack with {await wager.FormatKasinoCurrencyAsync()}[br]" +
$"[B]Your hand:[/B] {BlackjackHelper.FormatHand(playerHand)} = {playerValue}[br]" + $"[B]Your hand:[/B] {BlackjackHelper.FormatHand(playerHand)} = {playerValue}[br]" +
$"[B]Dealer:[/B] {BlackjackHelper.FormatHand(dealerHand, hideFirstCard: true)}[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); true, autoDeleteAfter: cleanupDelay);
} }
@@ -234,9 +235,6 @@ public class BlackjackCommand : ICommand
return; return;
} }
var rng = StandardRng.Create();
var random = RandomShim.Create(rng);
switch (action) switch (action)
{ {
case "hit": case "hit":
@@ -246,7 +244,10 @@ public class BlackjackCommand : ICommand
await HandleStand(botInstance, user, gambler, activeWager, currentGameState, cleanupDelay, ctx); await HandleStand(botInstance, user, gambler, activeWager, currentGameState, cleanupDelay, ctx);
break; break;
case "double": 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; break;
} }
} }
@@ -254,7 +255,6 @@ public class BlackjackCommand : ICommand
private async Task HandleHit(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler, private async Task HandleHit(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
WagerDbModel wager, BlackjackGameMetaModel gameState, TimeSpan cleanupDelay, CancellationToken ctx) WagerDbModel wager, BlackjackGameMetaModel gameState, TimeSpan cleanupDelay, CancellationToken ctx)
{ {
if (gameState.Deck.Count == 0) if (gameState.Deck.Count == 0)
{ {
await botInstance.SendChatMessageAsync( await botInstance.SendChatMessageAsync(
@@ -264,36 +264,56 @@ public class BlackjackCommand : ICommand
return; return;
} }
var currentHand = gameState.PlayerHands[gameState.CurrentHandIndex];
var handLabel = gameState.PlayerHands.Count > 1 ? $" (Hand {gameState.CurrentHandIndex + 1})" : "";
// Draw card // Draw card
var card = gameState.Deck[0]; var card = gameState.Deck[0];
gameState.Deck.RemoveAt(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) if (playerValue > 21)
{ {
var redColor = (await SettingsProvider.GetValueAsync(BuiltIn.Keys.KiwiFarmsRedColor)).Value;
// Bust - player loses // Bust - player loses
var colors = await SettingsProvider.GetMultipleValuesAsync([
BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor
]);
await botInstance.SendChatMessageAsync( await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()} hit and drew {card}[br]" + $"{user.FormatUsername()}{handLabel} hit and drew {card}[br]" +
$"[B]Your hand:[/B] {BlackjackHelper.FormatHand(gameState.PlayerHand)} = {playerValue}[br]" + $"[B]Your hand:[/B] {BlackjackHelper.FormatHand(currentHand)} = {playerValue}[br]" +
$"[B][COLOR={redColor}]BUST![/COLOR][/B]", $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}]BUST![/COLOR][/B]",
true, autoDeleteAfter: cleanupDelay); 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; 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 // Auto-stand after double down hit
await botInstance.SendChatMessageAsync( await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()} hit and drew {card}[br]" + $"{user.FormatUsername()}{handLabel} hit and drew {card}[br]" +
$"[B]Your hand:[/B] {BlackjackHelper.FormatHand(gameState.PlayerHand)} = {playerValue}", $"[B]Your hand:[/B] {BlackjackHelper.FormatHand(currentHand)} = {playerValue}",
true, autoDeleteAfter: cleanupDelay); true, autoDeleteAfter: cleanupDelay);
await HandleStand(botInstance, user, gambler, wager, gameState, cleanupDelay, ctx); await MoveToNextHandOrResolve(botInstance, user, gambler, wager, gameState, cleanupDelay, ctx);
return; return;
} }
@@ -302,8 +322,8 @@ public class BlackjackCommand : ICommand
await _dbContext.SaveChangesAsync(ctx); await _dbContext.SaveChangesAsync(ctx);
await botInstance.SendChatMessageAsync( await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()} hit and drew {card}[br]" + $"{user.FormatUsername()}{handLabel} hit and drew {card}[br]" +
$"[B]Your hand:[/B] {BlackjackHelper.FormatHand(gameState.PlayerHand)} = {playerValue}[br]" + $"[B]Your hand:[/B] {BlackjackHelper.FormatHand(currentHand)} = {playerValue}[br]" +
$"Use [B]!bj hit[/B] or [B]!bj stand[/B] to continue", $"Use [B]!bj hit[/B] or [B]!bj stand[/B] to continue",
true, autoDeleteAfter: cleanupDelay); true, autoDeleteAfter: cleanupDelay);
} }
@@ -311,35 +331,24 @@ public class BlackjackCommand : ICommand
private async Task HandleStand(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler, private async Task HandleStand(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
WagerDbModel wager, BlackjackGameMetaModel gameState, TimeSpan cleanupDelay, CancellationToken ctx) 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 await botInstance.SendChatMessageAsync(
var dealerValue = BlackjackHelper.CalculateHandValue(gameState.DealerHand); $"{user.FormatUsername()}{handLabel} stands with {BlackjackHelper.FormatHand(currentHand)} = {playerValue}",
true, autoDeleteAfter: cleanupDelay);
while (dealerValue < 17) await MoveToNextHandOrResolve(botInstance, user, gambler, wager, gameState, cleanupDelay, ctx);
{
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, 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) // Check if player can double (only on first action with 2 cards)
if (gameState.PlayerHand.Count != 2) if (currentHand.Count != 2)
{ {
await botInstance.SendChatMessageAsync( await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()}, you can only double down on your first action.", $"{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 // Check if player has enough balance for double
if (gambler.Balance < wager.WagerAmount) if (gambler.Balance < gameState.OriginalWagerAmount)
{ {
await botInstance.SendChatMessageAsync( await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()}, you don't have enough balance to double down.", $"{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); $"Double down for {wager.Id}", ct: ctx);
wager.WagerAmount *= 2; wager.WagerAmount *= 2;
wager.WagerEffect -= additionalWager; // Subtract the additional wager wager.WagerEffect -= additionalWager; // Subtract the additional wager
gameState.HasDoubledDown = true; gameState.HasDoubledDown[gameState.CurrentHandIndex] = true;
await _dbContext.SaveChangesAsync(ctx); await _dbContext.SaveChangesAsync(ctx);
var handLabel = gameState.PlayerHands.Count > 1 ? $" (Hand {gameState.CurrentHandIndex + 1})" : "";
await botInstance.SendChatMessageAsync( 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); true, autoDeleteAfter: cleanupDelay);
// Draw one card and auto-stand // Draw one card and auto-stand
await HandleHit(botInstance, user, gambler, wager, gameState, cleanupDelay, ctx); 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, private async Task ResolveGame(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
WagerDbModel wager, BlackjackGameMetaModel gameState, bool immediateResolution, WagerDbModel wager, BlackjackGameMetaModel gameState, bool immediateResolution,
TimeSpan cleanupDelay, CancellationToken ctx) TimeSpan cleanupDelay, CancellationToken ctx)
{ {
var playerValue = BlackjackHelper.CalculateHandValue(gameState.PlayerHand);
var dealerValue = BlackjackHelper.CalculateHandValue(gameState.DealerHand); var dealerValue = BlackjackHelper.CalculateHandValue(gameState.DealerHand);
var playerBlackjack = BlackjackHelper.IsBlackjack(gameState.PlayerHand);
var dealerBlackjack = BlackjackHelper.IsBlackjack(gameState.DealerHand); var dealerBlackjack = BlackjackHelper.IsBlackjack(gameState.DealerHand);
decimal finalEffect; decimal totalEffect = 0;
decimal multiplier;
string result;
var colors = await SettingsProvider.GetMultipleValuesAsync([ var colors = await SettingsProvider.GetMultipleValuesAsync([
BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor
]); ]);
// Determine outcome var message = $"🃏 {user.FormatUsername()}'s blackjack game:[br]";
if (playerBlackjack && dealerBlackjack)
// Process each hand
for (int i = 0; i < gameState.PlayerHands.Count; i++)
{ {
finalEffect = 0; var hand = gameState.PlayerHands[i];
multiplier = 1m; var playerValue = BlackjackHelper.CalculateHandValue(hand);
result = $"[B][COLOR=orange]PUSH![/COLOR][/B] Both have blackjack"; var playerBlackjack = BlackjackHelper.IsBlackjack(hand);
} var handWager = gameState.OriginalWagerAmount;
else if (playerBlackjack)
{ var handLabel = gameState.PlayerHands.Count > 1 ? $" {i + 1}" : "";
finalEffect = wager.WagerAmount * 1.5m; message += $"[B]Your hand{handLabel}:[/B] {BlackjackHelper.FormatHand(hand)} = {playerValue}[br]";
multiplier = 2.5m;
result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsGreenColor].Value}]BLACKJACK![/COLOR][/B]"; decimal handEffect;
} string result;
else if (dealerBlackjack)
{ // Determine outcome for this hand
finalEffect = -wager.WagerAmount; if (playerBlackjack && dealerBlackjack)
multiplier = 0m; {
result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}]DEALER BLACKJACK![/COLOR][/B]"; handEffect = 0;
} result = $"[B][COLOR=orange]PUSH![/COLOR][/B]";
else if (playerValue > 21) }
{ else if (playerBlackjack)
finalEffect = -wager.WagerAmount; {
multiplier = 0m; handEffect = handWager * 1.5m;
result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}]BUST![/COLOR][/B]"; result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsGreenColor].Value}]BLACKJACK! +{await handEffect.FormatKasinoCurrencyAsync()}[/COLOR][/B]";
} }
else if (dealerValue > 21) else if (dealerBlackjack)
{ {
finalEffect = wager.WagerAmount; handEffect = -handWager;
multiplier = 2m; result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}]DEALER BLACKJACK! -{await handWager.FormatKasinoCurrencyAsync()}[/COLOR][/B]";
result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsGreenColor].Value}]DEALER BUST - YOU WIN![/COLOR][/B]"; }
} else if (playerValue > 21)
else if (playerValue > dealerValue) {
{ handEffect = -handWager;
finalEffect = wager.WagerAmount; result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}]BUST! -{await handWager.FormatKasinoCurrencyAsync()}[/COLOR][/B]";
multiplier = 2m; }
result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsGreenColor].Value}]YOU WIN![/COLOR][/B]"; else if (dealerValue > 21)
} {
else if (playerValue < dealerValue) handEffect = handWager;
{ result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsGreenColor].Value}]DEALER BUST! +{await handEffect.FormatKasinoCurrencyAsync()}[/COLOR][/B]";
finalEffect = -wager.WagerAmount; }
multiplier = 0m; else if (playerValue > dealerValue)
result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}]DEALER WINS![/COLOR][/B]"; {
} handEffect = handWager;
else result = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsGreenColor].Value}]WIN! +{await handEffect.FormatKasinoCurrencyAsync()}[/COLOR][/B]";
{ }
finalEffect = 0; else if (playerValue < dealerValue)
multiplier = 1m; {
result = $"[B][COLOR=orange]PUSH![/COLOR][/B]"; 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 // Update wager to complete
wager.IsComplete = true; wager.IsComplete = true;
wager.WagerEffect = finalEffect; wager.WagerEffect = totalEffect;
wager.Multiplier = multiplier; wager.Multiplier = (totalEffect + wager.WagerAmount) / wager.WagerAmount;
// Update balance and create transaction in same context
await _dbContext.SaveChangesAsync(ctx); await _dbContext.SaveChangesAsync(ctx);
var balanceAdjustment = finalEffect + wager.WagerAmount; var balanceAdjustment = totalEffect + wager.WagerAmount;
var newBalance = await Money.ModifyBalanceAsync(gambler.Id, balanceAdjustment, TransactionSourceEventType.Gambling, var newBalance = await Money.ModifyBalanceAsync(gambler.Id, balanceAdjustment, TransactionSourceEventType.Gambling,
$"Blackjack outcome from wager {wager.Id}", null, ctx); $"Blackjack outcome from wager {wager.Id}", null, ctx);
// Display result message += $"[B]Net:[/B] {(totalEffect >= 0 ? "+" : "")}{await totalEffect.FormatKasinoCurrencyAsync()} | Balance: {await newBalance.FormatKasinoCurrencyAsync()}";
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()}";
await botInstance.SendChatMessageAsync(message, true, autoDeleteAfter: cleanupDelay); 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 public class BlackjackGameMetaModel
{ {
/// <summary> /// <summary>
/// Player's hand /// Player's hands (multiple if split)
/// </summary> /// </summary>
public required List<Card> PlayerHand { get; set; } public required List<List<Card>> PlayerHands { get; set; }
/// <summary> /// <summary>
/// Dealer's hand /// Dealer's hand
@@ -18,9 +21,19 @@ public class BlackjackGameMetaModel
public required List<Card> Deck { get; set; } public required List<Card> Deck { get; set; }
/// <summary> /// <summary>
/// Whether player has doubled down (can only hit once more) /// Whether each hand has doubled down (can only hit once more)
/// </summary> /// </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 public class Card
@@ -65,7 +78,7 @@ public static class BlackjackHelper
/// <summary> /// <summary>
/// Create a new shuffled deck /// Create a new shuffled deck
/// </summary> /// </summary>
public static List<Card> CreateDeck(Random random) public static List<Card> CreateDeck(GamblerDbModel gambler)
{ {
var deck = new List<Card>(); var deck = new List<Card>();
foreach (var suit in Suits) foreach (var suit in Suits)
@@ -79,7 +92,7 @@ public static class BlackjackHelper
// Shuffle using Fisher-Yates // Shuffle using Fisher-Yates
for (int i = deck.Count - 1; i > 0; i--) 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]); (deck[i], deck[j]) = (deck[j], deck[i]);
} }
@@ -125,6 +138,18 @@ public static class BlackjackHelper
return hand.Count == 2 && CalculateHandValue(hand) == 21; 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> /// <summary>
/// Format hand for display /// Format hand for display
/// </summary> /// </summary>