Yesterdays bullshit served tomorrow (#100)

* Minimize amount of lines blackjack needs

* selfdestruct sloppa images

* massivly reduce amount of time slot graphic stays in chat
This commit is contained in:
CrackmaticSoftware
2026-03-19 01:52:40 +01:00
committed by GitHub
parent a6810591de
commit 606e7867d0
4 changed files with 420 additions and 281 deletions

View File

@@ -204,7 +204,7 @@ public class GetRandomImage : ICommand
: new Random().Next(settings[BuiltIn.Keys.BotImagePigCubeSelfDestructMin].ToType<int>(), : new Random().Next(settings[BuiltIn.Keys.BotImagePigCubeSelfDestructMin].ToType<int>(),
settings[BuiltIn.Keys.BotImagePigCubeSelfDestructMax].ToType<int>())); settings[BuiltIn.Keys.BotImagePigCubeSelfDestructMax].ToType<int>()));
} }
else if (key == "chink" && settings[BuiltIn.Keys.BotImageChinkSelfDestruct].ToBoolean()) else if (key is "chink" or "sloppa" && settings[BuiltIn.Keys.BotImageChinkSelfDestruct].ToBoolean())
{ {
RateLimitService.AddEntry(user, this, message.MessageRawHtmlDecoded); RateLimitService.AddEntry(user, this, message.MessageRawHtmlDecoded);
timeToDeletion = TimeSpan.FromMilliseconds(settings[BuiltIn.Keys.BotImageChinkSelfDestructDelay].ToType<int>()); timeToDeletion = TimeSpan.FromMilliseconds(settings[BuiltIn.Keys.BotImageChinkSelfDestructDelay].ToType<int>());

View File

@@ -17,6 +17,10 @@ public class BlackjackCommand : ICommand
{ {
private static readonly TimeSpan GameTimeout = TimeSpan.FromMinutes(5); private static readonly TimeSpan GameTimeout = TimeSpan.FromMinutes(5);
// Colors fetched once per user action and threaded through to all helpers,
// so we never fetch them redundantly mid-game-flow.
private record GameColors(string Green, string Red);
public List<Regex> Patterns => [ public List<Regex> Patterns => [
new Regex(@"^blackjack (?<amount>\d+)$", RegexOptions.IgnoreCase), new Regex(@"^blackjack (?<amount>\d+)$", RegexOptions.IgnoreCase),
new Regex(@"^blackjack (?<amount>\d+\.\d+)$", RegexOptions.IgnoreCase), new Regex(@"^blackjack (?<amount>\d+\.\d+)$", RegexOptions.IgnoreCase),
@@ -46,8 +50,7 @@ public class BlackjackCommand : ICommand
BuiltIn.Keys.KasinoBlackjackEnabled BuiltIn.Keys.KasinoBlackjackEnabled
]); ]);
// Check if blackjack is enabled var blackjackEnabled = settings[BuiltIn.Keys.KasinoBlackjackEnabled].ToBoolean();
var blackjackEnabled = (settings[BuiltIn.Keys.KasinoBlackjackEnabled]).ToBoolean();
if (!blackjackEnabled) if (!blackjackEnabled)
{ {
var gameDisabledCleanupDelay = TimeSpan.FromMilliseconds(settings[BuiltIn.Keys.KasinoGameDisabledMessageCleanupDelay].ToType<int>()); var gameDisabledCleanupDelay = TimeSpan.FromMilliseconds(settings[BuiltIn.Keys.KasinoGameDisabledMessageCleanupDelay].ToType<int>());
@@ -59,7 +62,6 @@ public class BlackjackCommand : ICommand
var cleanupDelay = TimeSpan.FromMilliseconds(settings[BuiltIn.Keys.KasinoBlackjackCleanupDelay].ToType<int>()); var cleanupDelay = TimeSpan.FromMilliseconds(settings[BuiltIn.Keys.KasinoBlackjackCleanupDelay].ToType<int>());
// Check if this is a new game or continuing existing game
if (arguments.TryGetValue("amount", out var amountGroup)) if (arguments.TryGetValue("amount", out var amountGroup))
{ {
await StartNewGame(botInstance, user, amountGroup.Value, cleanupDelay, ctx); await StartNewGame(botInstance, user, amountGroup.Value, cleanupDelay, ctx);
@@ -80,15 +82,31 @@ public class BlackjackCommand : ICommand
TimeSpan cleanupDelay, CancellationToken ctx) TimeSpan cleanupDelay, CancellationToken ctx)
{ {
var logger = LogManager.GetCurrentClassLogger(); var logger = LogManager.GetCurrentClassLogger();
// Fetch colors upfront — needed for both the immediate-blackjack ResolveGame path
// and the normal GameStart display path.
var colorSettings = await SettingsProvider.GetMultipleValuesAsync([
BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor
]);
var colors = new GameColors(
colorSettings[BuiltIn.Keys.KiwiFarmsGreenColor].Value,
colorSettings[BuiltIn.Keys.KiwiFarmsRedColor].Value);
var wager = Convert.ToDecimal(amountStr); var wager = Convert.ToDecimal(amountStr);
var gambler = await Money.GetGamblerEntityAsync(user.Id, ct: ctx); var gambler = await Money.GetGamblerEntityAsync(user.Id, ct: ctx);
if (gambler == null) if (gambler == null)
{
throw new InvalidOperationException($"Caught a null when retrieving gambler for {user.KfUsername}"); throw new InvalidOperationException($"Caught a null when retrieving gambler for {user.KfUsername}");
if (wager == 0)
{
await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()}, you have to wager more than {await wager.FormatKasinoCurrencyAsync()}",
true, autoDeleteAfter: cleanupDelay);
RateLimitService.RemoveMostRecentEntry(user, this);
return;
} }
// Check if user has enough balance
if (gambler.Balance < wager) if (gambler.Balance < wager)
{ {
await botInstance.SendChatMessageAsync( await botInstance.SendChatMessageAsync(
@@ -98,16 +116,7 @@ public class BlackjackCommand : ICommand
return; return;
} }
if (wager == 0) // Check for an existing incomplete game
{
await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()}, you have to wager more than {await wager.FormatKasinoCurrencyAsync()}", true,
autoDeleteAfter: cleanupDelay);
RateLimitService.RemoveMostRecentEntry(user, this);
return;
}
// Check for existing incomplete blackjack game
var existingGame = await _dbContext.Wagers var existingGame = await _dbContext.Wagers
.OrderBy(x => x.Id) .OrderBy(x => x.Id)
.LastOrDefaultAsync(w => w.Gambler.Id == gambler.Id && .LastOrDefaultAsync(w => w.Gambler.Id == gambler.Id &&
@@ -133,9 +142,8 @@ public class BlackjackCommand : ICommand
await _dbContext.SaveChangesAsync(ctx); await _dbContext.SaveChangesAsync(ctx);
throw; throw;
} }
// Check if game has timed out
var timeSinceStart = DateTimeOffset.UtcNow - existingGame.Time;
var timeSinceStart = DateTimeOffset.UtcNow - existingGame.Time;
if (timeSinceStart > GameTimeout) if (timeSinceStart > GameTimeout)
{ {
await ForfeitGame(botInstance, user, gambler, existingGame, cleanupDelay, ctx); await ForfeitGame(botInstance, user, gambler, existingGame, cleanupDelay, ctx);
@@ -148,16 +156,12 @@ public class BlackjackCommand : ICommand
return; return;
} }
// Deal initial hands
// Create deck and deal initial hands
var deck = BlackjackHelper.CreateDeck(gambler); var deck = BlackjackHelper.CreateDeck(gambler);
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] };
deck.RemoveRange(0, 4); deck.RemoveRange(0, 4);
// Create game state
// HasDoubledDown is a single bool — doubles are not permitted after a split
var newGameState = new BlackjackGameMetaModel var newGameState = new BlackjackGameMetaModel
{ {
PlayerHands = new List<List<Card>> { playerHand }, PlayerHands = new List<List<Card>> { playerHand },
@@ -168,19 +172,14 @@ public class BlackjackCommand : ICommand
OriginalWagerAmount = wager OriginalWagerAmount = wager
}; };
// Create incomplete wager
await Money.NewWagerAsync( await Money.NewWagerAsync(
gambler.Id, gambler.Id, wager, -wager,
wager,
-wager, // This will be the effect for incomplete wagers
WagerGame.Blackjack, WagerGame.Blackjack,
autoModifyBalance: true, autoModifyBalance: true,
gameMeta: newGameState, gameMeta: newGameState,
isComplete: false, isComplete: false,
ct: ctx ct: ctx);
);
// Update wager ID in game state
var createdWager = await _dbContext.Wagers var createdWager = await _dbContext.Wagers
.OrderBy(x => x.Id) .OrderBy(x => x.Id)
.LastOrDefaultAsync( .LastOrDefaultAsync(
@@ -189,50 +188,44 @@ public class BlackjackCommand : ICommand
createdWager.GameMeta = JsonSerializer.Serialize(newGameState); createdWager.GameMeta = JsonSerializer.Serialize(newGameState);
await _dbContext.SaveChangesAsync(ctx); await _dbContext.SaveChangesAsync(ctx);
// Check for immediate blackjacks // Immediate blackjack check — goes straight to resolution
var playerValue = BlackjackHelper.CalculateHandValue(playerHand); if (BlackjackHelper.IsBlackjack(playerHand) || BlackjackHelper.IsBlackjack(dealerHand))
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); await ResolveGame(botInstance, user, gambler, createdWager, newGameState, colors, cleanupDelay, ctx);
return; return;
} }
// Display initial game state var playerValue = BlackjackHelper.CalculateHandValue(playerHand);
var colors = await SettingsProvider.GetMultipleValuesAsync([
BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor
]);
var canSplit = BlackjackHelper.CanSplit(playerHand); 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]" + await BlackjackDisplay.GameStart(user, wager, playerHand, playerValue, dealerHand, canSplit, colors.Red),
$"[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]{splitText} to continue",
true, autoDeleteAfter: cleanupDelay); true, autoDeleteAfter: cleanupDelay);
} }
private async Task ContinueGame(ChatBot botInstance, UserDbModel user, string action, private async Task ContinueGame(ChatBot botInstance, UserDbModel user, string action,
TimeSpan cleanupDelay, CancellationToken ctx) TimeSpan cleanupDelay, CancellationToken ctx)
{ {
// Fetch colors once here; pass them to every downstream method
var colorSettings = await SettingsProvider.GetMultipleValuesAsync([
BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor
]);
var colors = new GameColors(
colorSettings[BuiltIn.Keys.KiwiFarmsGreenColor].Value,
colorSettings[BuiltIn.Keys.KiwiFarmsRedColor].Value);
var gambler = await Money.GetGamblerEntityAsync(user.Id, ct: ctx); var gambler = await Money.GetGamblerEntityAsync(user.Id, ct: ctx);
if (gambler == null) if (gambler == null)
{
throw new InvalidOperationException($"Caught a null when retrieving gambler for {user.KfUsername}"); throw new InvalidOperationException($"Caught a null when retrieving gambler for {user.KfUsername}");
}
// Find active game
var activeWager = await _dbContext.Wagers var activeWager = await _dbContext.Wagers
.OrderBy(x => x.Id) .OrderBy(x => x.Id)
.LastOrDefaultAsync(w => w.Gambler.Id == gambler.Id && .LastOrDefaultAsync(w => w.Gambler.Id == gambler.Id &&
w.Game == WagerGame.Blackjack && w.Game == WagerGame.Blackjack &&
!w.IsComplete && w.GameMeta != null, cancellationToken: ctx); !w.IsComplete && w.GameMeta != null,
cancellationToken: ctx);
if (activeWager == null) if (activeWager == null)
{ {
@@ -255,7 +248,6 @@ public class BlackjackCommand : ICommand
return; return;
} }
// Check timeout
var timeSinceStart = DateTimeOffset.UtcNow - activeWager.Time; var timeSinceStart = DateTimeOffset.UtcNow - activeWager.Time;
if (timeSinceStart > GameTimeout) if (timeSinceStart > GameTimeout)
{ {
@@ -266,22 +258,24 @@ public class BlackjackCommand : ICommand
switch (action) switch (action)
{ {
case "hit": case "hit":
await HandleHit(botInstance, user, gambler, activeWager, currentGameState, cleanupDelay, ctx); await HandleHit(botInstance, user, gambler, activeWager, currentGameState, colors, cleanupDelay, ctx);
break; break;
case "stand": case "stand":
await HandleStand(botInstance, user, gambler, activeWager, currentGameState, cleanupDelay, ctx); await HandleStand(botInstance, user, gambler, activeWager, currentGameState, colors, cleanupDelay, ctx);
break; break;
case "double": case "double":
await HandleDouble(botInstance, user, gambler, activeWager, currentGameState, cleanupDelay, ctx); await HandleDouble(botInstance, user, gambler, activeWager, currentGameState, colors, cleanupDelay, ctx);
break; break;
case "split": case "split":
await HandleSplit(botInstance, user, gambler, activeWager, currentGameState, cleanupDelay, ctx); await HandleSplit(botInstance, user, gambler, activeWager, currentGameState, colors, cleanupDelay, ctx);
break; break;
} }
} }
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, GameColors colors,
TimeSpan cleanupDelay, CancellationToken ctx)
{ {
if (gameState.Deck.Count == 0) if (gameState.Deck.Count == 0)
{ {
@@ -294,86 +288,54 @@ public class BlackjackCommand : ICommand
} }
var currentHand = gameState.PlayerHands[gameState.CurrentHandIndex]; var currentHand = gameState.PlayerHands[gameState.CurrentHandIndex];
var handLabel = gameState.PlayerHands.Count > 1 ? $" (Hand {gameState.CurrentHandIndex + 1})" : ""; var handLabel = gameState.PlayerHands.Count > 1 ? $" (H{gameState.CurrentHandIndex + 1})" : "";
// Draw card
var card = gameState.Deck[0]; var card = gameState.Deck[0];
gameState.Deck.RemoveAt(0); gameState.Deck.RemoveAt(0);
currentHand.Add(card); currentHand.Add(card);
var playerValue = BlackjackHelper.CalculateHandValue(currentHand); var playerValue = BlackjackHelper.CalculateHandValue(currentHand);
// Whether this hit ends the current hand (bust, 21, or post-double auto-stand)
bool handEnded = playerValue > 21 || playerValue == 21 || gameState.HasDoubledDown; bool handEnded = playerValue > 21 || playerValue == 21 || gameState.HasDoubledDown;
// Whether this is the last hand — if so, skip the intermediate message and let ResolveGame if (!handEnded)
// produce the single consolidated output, avoiding a duplicate bust/result message.
bool isLastHand = gameState.CurrentHandIndex >= gameState.PlayerHands.Count - 1;
if (handEnded && !isLastHand)
{ {
// Transitioning between split hands: show what happened on this hand before moving on // Hand is still live — show updated state and prompt for next action
string transitionalResult;
if (playerValue > 21)
{
var colors = await SettingsProvider.GetMultipleValuesAsync([BuiltIn.Keys.KiwiFarmsRedColor]);
transitionalResult = $"[B][COLOR={colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}]BUST![/COLOR][/B]";
}
else
{
transitionalResult = $"[B]Standing on {playerValue}[/B]";
}
await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()}{handLabel} hit and drew {card}[br]" +
$"[B]Your hand:[/B] {BlackjackHelper.FormatHand(currentHand)} = {playerValue}[br]" +
transitionalResult,
true, autoDeleteAfter: cleanupDelay);
}
else if (!handEnded)
{
// Hand is still in progress — show current state and prompt for next action
wager.GameMeta = JsonSerializer.Serialize(gameState); wager.GameMeta = JsonSerializer.Serialize(gameState);
await _dbContext.SaveChangesAsync(ctx); await _dbContext.SaveChangesAsync(ctx);
await botInstance.SendChatMessageAsync( await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()}{handLabel} hit and drew {card}[br]" + BlackjackDisplay.HitInProgress(user, card, currentHand, playerValue, gameState.DealerHand, handLabel, colors.Red),
$"[B]Your hand:[/B] {BlackjackHelper.FormatHand(currentHand)} = {playerValue}[br]" +
$"Use [B]!bj hit[/B] or [B]!bj stand[/B] to continue",
true, autoDeleteAfter: cleanupDelay); true, autoDeleteAfter: cleanupDelay);
return; return;
} }
// If handEnded && isLastHand: fall through silently — ResolveGame will show everything
await MoveToNextHandOrResolve(botInstance, user, gambler, wager, gameState, cleanupDelay, ctx); // Hand ended (bust / 21 / post-double auto-stand).
// MoveToNextHandOrResolve sends the combined transition message when moving
// to the next split hand, or falls through silently to ResolveGame.
await MoveToNextHandOrResolve(botInstance, user, gambler, wager, gameState,
currentHand, busted: playerValue > 21, colors, cleanupDelay, ctx);
} }
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, GameColors colors,
TimeSpan cleanupDelay, CancellationToken ctx)
{ {
var handLabel = gameState.PlayerHands.Count > 1 ? $" (Hand {gameState.CurrentHandIndex + 1})" : "";
var currentHand = gameState.PlayerHands[gameState.CurrentHandIndex]; var currentHand = gameState.PlayerHands[gameState.CurrentHandIndex];
var playerValue = BlackjackHelper.CalculateHandValue(currentHand);
// Only send an intermediate stand message when transitioning between split hands. // No stand message needed here — MoveToNextHandOrResolve handles all output:
// For the final/only hand, ResolveGame produces the sole consolidated message. // a combined split-transition message when moving to the next hand, and
bool isLastHand = gameState.CurrentHandIndex >= gameState.PlayerHands.Count - 1; // silence when falling through to the final resolution.
if (!isLastHand) await MoveToNextHandOrResolve(botInstance, user, gambler, wager, gameState,
{ currentHand, busted: false, colors, cleanupDelay, ctx);
await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()}{handLabel} stands with {BlackjackHelper.FormatHand(currentHand)} = {playerValue}",
true, autoDeleteAfter: cleanupDelay);
} }
await MoveToNextHandOrResolve(botInstance, user, gambler, wager, gameState, 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, TimeSpan cleanupDelay, CancellationToken ctx) WagerDbModel wager, BlackjackGameMetaModel gameState, GameColors colors,
TimeSpan cleanupDelay, CancellationToken ctx)
{ {
var currentHand = gameState.PlayerHands[gameState.CurrentHandIndex]; var currentHand = gameState.PlayerHands[gameState.CurrentHandIndex];
// Check if player can double (only on first action with 2 cards)
if (currentHand.Count != 2) if (currentHand.Count != 2)
{ {
await botInstance.SendChatMessageAsync( await botInstance.SendChatMessageAsync(
@@ -382,7 +344,6 @@ public class BlackjackCommand : ICommand
return; return;
} }
// Doubling after a split is not permitted
if (gameState.PlayerHands.Count > 1) if (gameState.PlayerHands.Count > 1)
{ {
await botInstance.SendChatMessageAsync( await botInstance.SendChatMessageAsync(
@@ -391,7 +352,6 @@ public class BlackjackCommand : ICommand
return; return;
} }
// Check if player has enough balance for double
if (gambler.Balance < gameState.OriginalWagerAmount) if (gambler.Balance < gameState.OriginalWagerAmount)
{ {
await botInstance.SendChatMessageAsync( await botInstance.SendChatMessageAsync(
@@ -400,30 +360,31 @@ public class BlackjackCommand : ICommand
return; return;
} }
// Double the wager: charge the additional amount and record it
var additionalWager = gameState.OriginalWagerAmount; var additionalWager = gameState.OriginalWagerAmount;
await Money.ModifyBalanceAsync(gambler.Id, -additionalWager, TransactionSourceEventType.Gambling, await Money.ModifyBalanceAsync(gambler.Id, -additionalWager, TransactionSourceEventType.Gambling,
$"Double down for {wager.Id}", ct: ctx); $"Double down for {wager.Id}", ct: ctx);
wager.WagerAmount += additionalWager; // Total wager is now OriginalWagerAmount * 2 wager.WagerAmount += additionalWager;
wager.WagerEffect -= additionalWager; // Outstanding loss reflects the extra stake wager.WagerEffect -= additionalWager;
gameState.HasDoubledDown = true; gameState.HasDoubledDown = true;
await _dbContext.SaveChangesAsync(ctx); await _dbContext.SaveChangesAsync(ctx);
// Confirm the double, then let HandleHit draw the one card and auto-stand.
// HasDoubledDown is now true, so HandleHit treats the hand as ended and falls
// through silently to ResolveGame — just two total messages: this + the result.
await botInstance.SendChatMessageAsync( await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()} doubled down! Wager is now {await wager.WagerAmount.FormatKasinoCurrencyAsync()}", await BlackjackDisplay.DoubledDown(user, wager.WagerAmount),
true, autoDeleteAfter: cleanupDelay); true, autoDeleteAfter: cleanupDelay);
// Draw exactly one card then auto-stand (handled inside HandleHit via HasDoubledDown flag) await HandleHit(botInstance, user, gambler, wager, gameState, colors, cleanupDelay, ctx);
await HandleHit(botInstance, user, gambler, wager, gameState, cleanupDelay, ctx);
} }
private async Task HandleSplit(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler, private async Task HandleSplit(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
WagerDbModel wager, BlackjackGameMetaModel gameState, TimeSpan cleanupDelay, CancellationToken ctx) WagerDbModel wager, BlackjackGameMetaModel gameState, GameColors colors,
TimeSpan cleanupDelay, CancellationToken ctx)
{ {
var currentHand = gameState.PlayerHands[gameState.CurrentHandIndex]; var currentHand = gameState.PlayerHands[gameState.CurrentHandIndex];
// Check if player can split
if (!BlackjackHelper.CanSplit(currentHand)) if (!BlackjackHelper.CanSplit(currentHand))
{ {
await botInstance.SendChatMessageAsync( await botInstance.SendChatMessageAsync(
@@ -433,7 +394,6 @@ public class BlackjackCommand : ICommand
return; return;
} }
// Check if already split
if (gameState.PlayerHands.Count > 1) if (gameState.PlayerHands.Count > 1)
{ {
await botInstance.SendChatMessageAsync( await botInstance.SendChatMessageAsync(
@@ -443,7 +403,6 @@ public class BlackjackCommand : ICommand
return; return;
} }
// Check if player has enough balance
if (gambler.Balance < gameState.OriginalWagerAmount) if (gambler.Balance < gameState.OriginalWagerAmount)
{ {
await botInstance.SendChatMessageAsync( await botInstance.SendChatMessageAsync(
@@ -453,7 +412,6 @@ public class BlackjackCommand : ICommand
return; return;
} }
// Check if deck has enough cards
if (gameState.Deck.Count < 2) if (gameState.Deck.Count < 2)
{ {
await botInstance.SendChatMessageAsync( await botInstance.SendChatMessageAsync(
@@ -463,22 +421,19 @@ public class BlackjackCommand : ICommand
return; return;
} }
// Perform the split
var card1 = currentHand[0]; var card1 = currentHand[0];
var card2 = currentHand[1]; var card2 = currentHand[1];
var hand1 = new List<Card> { card1, gameState.Deck[0] }; var hand1 = new List<Card> { card1, gameState.Deck[0] };
var hand2 = new List<Card> { card2, gameState.Deck[1] }; var hand2 = new List<Card> { card2, gameState.Deck[1] };
gameState.Deck.RemoveRange(0, 2); gameState.Deck.RemoveRange(0, 2);
gameState.PlayerHands = new List<List<Card>> { hand1, hand2 }; gameState.PlayerHands = new List<List<Card>> { hand1, hand2 };
gameState.HasDoubledDown = false; // Single bool — doubles are blocked after splitting gameState.HasDoubledDown = false;
gameState.CurrentHandIndex = 0; gameState.CurrentHandIndex = 0;
// Charge for the split
var additionalWager = gameState.OriginalWagerAmount; var additionalWager = gameState.OriginalWagerAmount;
await Money.ModifyBalanceAsync(gambler.Id, -additionalWager, TransactionSourceEventType.Gambling, await Money.ModifyBalanceAsync(gambler.Id, -additionalWager, TransactionSourceEventType.Gambling,
$"Split down for {wager.Id}", ct: ctx); $"Split for {wager.Id}", ct: ctx);
wager.WagerAmount += additionalWager; wager.WagerAmount += additionalWager;
wager.WagerEffect -= additionalWager; wager.WagerEffect -= additionalWager;
@@ -489,51 +444,59 @@ public class BlackjackCommand : ICommand
var value2 = BlackjackHelper.CalculateHandValue(hand2); var value2 = BlackjackHelper.CalculateHandValue(hand2);
await botInstance.SendChatMessageAsync( await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()} split their hand! Total wager: {await wager.WagerAmount.FormatKasinoCurrencyAsync()}[br]" + await BlackjackDisplay.SplitDeal(user, wager.WagerAmount, hand1, value1, hand2, value2, colors.Red),
$"[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); true, autoDeleteAfter: cleanupDelay);
} }
/// <summary>
/// Advances to the next split hand, or kicks off dealer play and resolution.
/// </summary>
/// <param name="finishedHand">The hand that just ended (bust, stand, or doubled auto-stand).</param>
/// <param name="busted">True if the finished hand went over 21.</param>
private async Task MoveToNextHandOrResolve(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler, private async Task MoveToNextHandOrResolve(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
WagerDbModel wager, BlackjackGameMetaModel gameState, TimeSpan cleanupDelay, CancellationToken ctx) WagerDbModel wager, BlackjackGameMetaModel gameState,
List<Card> finishedHand, bool busted,
GameColors colors, TimeSpan cleanupDelay, CancellationToken ctx)
{ {
var finishedIndex = gameState.CurrentHandIndex;
gameState.CurrentHandIndex++; gameState.CurrentHandIndex++;
if (gameState.CurrentHandIndex < gameState.PlayerHands.Count) if (gameState.CurrentHandIndex < gameState.PlayerHands.Count)
{ {
// Move to next split hand // More split hands to play — one combined message covers both
// "what happened to the hand that just ended" and "here's your next hand".
wager.GameMeta = JsonSerializer.Serialize(gameState); wager.GameMeta = JsonSerializer.Serialize(gameState);
await _dbContext.SaveChangesAsync(ctx); await _dbContext.SaveChangesAsync(ctx);
var finishedValue = BlackjackHelper.CalculateHandValue(finishedHand);
var nextHand = gameState.PlayerHands[gameState.CurrentHandIndex]; var nextHand = gameState.PlayerHands[gameState.CurrentHandIndex];
var nextValue = BlackjackHelper.CalculateHandValue(nextHand); var nextValue = BlackjackHelper.CalculateHandValue(nextHand);
await botInstance.SendChatMessageAsync( await botInstance.SendChatMessageAsync(
$"Playing Hand {gameState.CurrentHandIndex + 1}[br]" + BlackjackDisplay.SplitTransition(
$"[B]Your hand:[/B] {BlackjackHelper.FormatHand(nextHand)} = {nextValue}[br]" + finishedIndex, finishedHand, finishedValue, busted,
$"Use [B]!bj hit[/B] or [B]!bj stand[/B] to continue", gameState.CurrentHandIndex, nextHand, nextValue,
colors.Red),
true, autoDeleteAfter: cleanupDelay); true, autoDeleteAfter: cleanupDelay);
} }
else else
{ {
// All hands played, dealer plays and resolve await PlayDealerAndResolve(botInstance, user, gambler, wager, gameState, colors, cleanupDelay, ctx);
await PlayDealerAndResolve(botInstance, user, gambler, wager, gameState, cleanupDelay, ctx);
} }
} }
private async Task PlayDealerAndResolve(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler, private async Task PlayDealerAndResolve(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
WagerDbModel wager, BlackjackGameMetaModel gameState, TimeSpan cleanupDelay, CancellationToken ctx) WagerDbModel wager, BlackjackGameMetaModel gameState, GameColors colors,
TimeSpan cleanupDelay, CancellationToken ctx)
{ {
// Check if all hands busted // Dealer only plays when at least one player hand hasn't busted
bool allHandsBusted = gameState.PlayerHands.All(hand => BlackjackHelper.CalculateHandValue(hand) > 21); bool allHandsBusted = gameState.PlayerHands.All(hand => BlackjackHelper.CalculateHandValue(hand) > 21);
if (!allHandsBusted) if (!allHandsBusted)
{ {
// Dealer plays
var dealerValue = BlackjackHelper.CalculateHandValue(gameState.DealerHand); var dealerValue = BlackjackHelper.CalculateHandValue(gameState.DealerHand);
while (dealerValue < 17) while (dealerValue < 17)
{ {
if (gameState.Deck.Count == 0) if (gameState.Deck.Count == 0)
@@ -552,109 +515,54 @@ public class BlackjackCommand : ICommand
} }
} }
await ResolveGame(botInstance, user, gambler, wager, gameState, false, cleanupDelay, ctx); await ResolveGame(botInstance, user, gambler, wager, gameState, colors, 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, GameColors colors,
TimeSpan cleanupDelay, CancellationToken ctx) TimeSpan cleanupDelay, CancellationToken ctx)
{ {
var dealerValue = BlackjackHelper.CalculateHandValue(gameState.DealerHand); var dealerValue = BlackjackHelper.CalculateHandValue(gameState.DealerHand);
var dealerBlackjack = BlackjackHelper.IsBlackjack(gameState.DealerHand); var dealerBlackjack = BlackjackHelper.IsBlackjack(gameState.DealerHand);
decimal totalEffect = 0;
var colors = await SettingsProvider.GetMultipleValuesAsync([
BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor
]);
var message = $"🃏 {user.FormatUsername()}'s blackjack game:[br]";
// For a split game each hand pays out at the original wager per hand.
// For a single hand (with optional double down) the full wager.WagerAmount is at stake,
// which already reflects the doubled stake when the player doubled down.
bool isSplitGame = gameState.PlayerHands.Count > 1; bool isSplitGame = gameState.PlayerHands.Count > 1;
// Process each hand decimal totalEffect = 0;
var results = new List<HandResultData>();
for (int i = 0; i < gameState.PlayerHands.Count; i++) for (int i = 0; i < gameState.PlayerHands.Count; i++)
{ {
var hand = gameState.PlayerHands[i]; var hand = gameState.PlayerHands[i];
var playerValue = BlackjackHelper.CalculateHandValue(hand); var playerValue = BlackjackHelper.CalculateHandValue(hand);
var playerBlackjack = BlackjackHelper.IsBlackjack(hand); var playerBlackjack = BlackjackHelper.IsBlackjack(hand);
// Split hands each pay the original per-hand wager; a single hand pays the full
// Split hands each pay at the original wager amount. // (possibly doubled) wager amount already tracked in wager.WagerAmount.
// A single hand pays at the full (possibly doubled) wager amount.
var handWager = isSplitGame ? gameState.OriginalWagerAmount : wager.WagerAmount; var handWager = isSplitGame ? gameState.OriginalWagerAmount : wager.WagerAmount;
var handLabel = isSplitGame ? $" {i + 1}" : ""; var (outcome, effect) = BlackjackDisplay.ClassifyHand(
message += $"[B]Your hand{handLabel}:[/B] {BlackjackHelper.FormatHand(hand)} = {playerValue}[br]"; playerValue, playerBlackjack, dealerValue, dealerBlackjack, handWager);
decimal handEffect; results.Add(new HandResultData(i, hand, playerValue, outcome, effect));
string result; totalEffect += effect;
// 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.IsComplete = true;
wager.WagerEffect = totalEffect; wager.WagerEffect = totalEffect;
wager.Multiplier = (totalEffect + wager.WagerAmount) / wager.WagerAmount; 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 = totalEffect + 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,
$"Blackjack outcome from wager {wager.Id}", null, ctx); TransactionSourceEventType.Gambling, $"Blackjack outcome from wager {wager.Id}", null, ctx);
message += $"[u][B]Net:[/B] {(totalEffect >= 0 ? "+" : "")}{await totalEffect.FormatKasinoCurrencyAsync()} | Balance: {await newBalance.FormatKasinoCurrencyAsync()}"; await botInstance.SendChatMessageAsync(
await BlackjackDisplay.FinalResult(
await botInstance.SendChatMessageAsync(message, true, autoDeleteAfter: cleanupDelay); user, results, gameState.DealerHand, dealerValue,
totalEffect, newBalance, isSplitGame, colors.Green, colors.Red),
true, autoDeleteAfter: cleanupDelay);
} }
private async Task ForfeitGame(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler, private async Task ForfeitGame(ChatBot botInstance, UserDbModel user, GamblerDbModel gambler,
WagerDbModel wager, TimeSpan cleanupDelay, CancellationToken ctx) WagerDbModel wager, TimeSpan cleanupDelay, CancellationToken ctx)
{ {

View File

@@ -0,0 +1,231 @@
using KfChatDotNetBot.Extensions;
using KfChatDotNetBot.Models;
using KfChatDotNetBot.Models.DbModels;
namespace KfChatDotNetBot.Commands.Kasino;
/// <summary>
/// Builds every chat message string for the blackjack game.
/// No game logic lives here — only presentation.
/// <para>
/// Keeping all string construction in one place means tweaking the UI never
/// requires touching the game-flow code in <see cref="BlackjackCommand"/>.
/// </para>
/// </summary>
internal static class BlackjackDisplay
{
// ─────────────────────────────────────────────────────────────────────────
// Primitive helpers
// ─────────────────────────────────────────────────────────────────────────
/// Wraps ♥ and ♦ in the game's losing-text red so suit glyphs render in red.
/// Applied to every hand string so the color is consistent with loss messages.
internal static string ColorizeSuits(string text, string redHex) =>
text.Replace("♥", $"[COLOR={redHex}]♥[/COLOR]")
.Replace("♦", $"[COLOR={redHex}]♦[/COLOR]");
private static string FmtHand(List<Card> hand, string redHex, bool hideFirst = false) =>
ColorizeSuits(BlackjackHelper.FormatHand(hand, hideFirstCard: hideFirst), redHex);
private static string FmtCard(Card card, string redHex) =>
ColorizeSuits(card.ToString()!, redHex);
/// Compact action-hint line. Only advertises actions the player can actually take right now.
/// ✦ marks double-down; ✂ marks split — both are hidden once unavailable.
private static string ActionHints(bool canDouble = false, bool canSplit = false)
{
var parts = new List<string> { "[B]hit[/B]", "[B]stand[/B]" };
if (canDouble) parts.Add("[B]double[/B] ✦");
if (canSplit) parts.Add("[B]split[/B] ✂");
return "!bj: " + string.Join(" · ", parts);
}
// ─────────────────────────────────────────────────────────────────────────
// Game-start (fresh deal)
// Two lines: hand state + action hints.
// Double is always shown — balance check happens inside HandleDouble if attempted.
// ─────────────────────────────────────────────────────────────────────────
public static async Task<string> GameStart(
UserDbModel user, decimal wager,
List<Card> playerHand, int playerValue,
List<Card> dealerHand,
bool canSplit, string redHex)
{
return
$"🃏 [B]{user.FormatUsername()}[/B] · {await wager.FormatKasinoCurrencyAsync()} — " +
$"[B]You:[/B] {FmtHand(playerHand, redHex)} ({playerValue}) " +
$"[I]vs[/I] [B]Dealer:[/B] {FmtHand(dealerHand, redHex, hideFirst: true)}[br]" +
ActionHints(canDouble: true, canSplit: canSplit);
}
// ─────────────────────────────────────────────────────────────────────────
// Hit still in progress (hand not yet resolved)
// Two lines: drew-card + updated state + action hints.
// ─────────────────────────────────────────────────────────────────────────
public static string HitInProgress(
UserDbModel user, Card drawnCard,
List<Card> currentHand, int handValue,
List<Card> dealerHand,
string handLabel, string redHex)
{
return
$"{user.FormatUsername()}{handLabel} drew {FmtCard(drawnCard, redHex)} — " +
$"[B]You:[/B] {FmtHand(currentHand, redHex)} ({handValue}) " +
$"[I]vs[/I] [B]Dealer:[/B] {FmtHand(dealerHand, redHex, hideFirst: true)}[br]" +
ActionHints();
}
// ─────────────────────────────────────────────────────────────────────────
// Double-down confirmation
// One line, shown once before the auto-hit silently proceeds to resolution.
// ─────────────────────────────────────────────────────────────────────────
public static async Task<string> DoubledDown(UserDbModel user, decimal newTotalWager) =>
$"{user.FormatUsername()} doubled down · Wager: [B]{await newTotalWager.FormatKasinoCurrencyAsync()}[/B]";
// ─────────────────────────────────────────────────────────────────────────
// Split: initial deal display
// Three lines: wager header, both hands side-by-side, action hints for Hand 1.
// ─────────────────────────────────────────────────────────────────────────
public static async Task<string> SplitDeal(
UserDbModel user, decimal totalWager,
List<Card> hand1, int value1,
List<Card> hand2, int value2,
string redHex)
{
return
$"{user.FormatUsername()} split · Wager: [B]{await totalWager.FormatKasinoCurrencyAsync()}[/B][br]" +
$"[B]H1:[/B] {FmtHand(hand1, redHex)} ({value1}) · [B]H2:[/B] {FmtHand(hand2, redHex)} ({value2})[br]" +
$"Playing [B]H1[/B] — {ActionHints()}";
}
// ─────────────────────────────────────────────────────────────────────────
// Split: hand transition
// Two lines combining "what happened to the finished hand" and "what you
// have on the next hand" into one message, saving a separate chat post.
// ─────────────────────────────────────────────────────────────────────────
public static string SplitTransition(
int finishedIndex, List<Card> finishedHand, int finishedValue, bool busted,
int nextIndex, List<Card> nextHand, int nextValue,
string redHex)
{
var outcome = busted
? $"[B][COLOR={redHex}]BUST[/COLOR][/B]"
: $"stood [B]{finishedValue}[/B]";
return
$"[B]H{finishedIndex + 1}:[/B] {FmtHand(finishedHand, redHex)} ({finishedValue}) — {outcome} " +
$"→ [B]H{nextIndex + 1}:[/B] {FmtHand(nextHand, redHex)} ({nextValue})[br]" +
ActionHints();
}
// ─────────────────────────────────────────────────────────────────────────
// Final result
// Single hand → 2 lines: You vs Dealer — RESULT / Net · Balance
// Split game → 3 lines: header / H1 — R · H2 — R / Dealer · Net · Balance
// ─────────────────────────────────────────────────────────────────────────
public static async Task<string> FinalResult(
UserDbModel user,
IReadOnlyList<HandResultData> results,
List<Card> dealerHand, int dealerValue,
decimal totalEffect, decimal newBalance,
bool isSplitGame, string greenHex, string redHex)
{
var sb = new System.Text.StringBuilder();
var sign = totalEffect >= 0 ? "+" : "";
var netLine =
$"[U]Net {sign}{await totalEffect.FormatKasinoCurrencyAsync()} · " +
$"Balance {await newBalance.FormatKasinoCurrencyAsync()}[/U]";
if (!isSplitGame)
{
// ── Single hand: hand + dealer + result all on one line ──────────
var r = results[0];
sb.Append(
$"🃏 [B]{user.FormatUsername()}[/B] · " +
$"[B]You:[/B] {FmtHand(r.Hand, redHex)} ({r.PlayerValue}) " +
$"[I]vs[/I] [B]Dealer:[/B] {FmtHand(dealerHand, redHex)} ({dealerValue}) — " +
$"{await FormatOutcomeTag(r, greenHex, redHex)}[br]" +
netLine);
}
else
{
// ── Split game: header, then both hands on one line, dealer + net ─
sb.Append($"🃏 [B]{user.FormatUsername()}[/B][br]");
var handParts = new List<string>();
foreach (var r in results)
{
handParts.Add(
$"[B]H{r.HandIndex + 1}:[/B] {FmtHand(r.Hand, redHex)} ({r.PlayerValue}) — " +
$"{await FormatOutcomeTag(r, greenHex, redHex)}");
}
sb.Append(string.Join(" · ", handParts) + "[br]");
sb.Append($"[B]Dealer:[/B] {FmtHand(dealerHand, redHex)} ({dealerValue}) · {netLine}");
}
return sb.ToString();
}
// ─────────────────────────────────────────────────────────────────────────
// Outcome classification
// Called by BlackjackCommand.ResolveGame to populate HandResultData before
// passing it here for display. Keeping it in this file co-locates it with
// the outcome tags it feeds into.
// ─────────────────────────────────────────────────────────────────────────
internal static (HandOutcome Outcome, decimal Effect) ClassifyHand(
int playerValue, bool playerBlackjack,
int dealerValue, bool dealerBlackjack,
decimal handWager)
{
if (playerBlackjack && dealerBlackjack) return (HandOutcome.Push, 0);
if (playerBlackjack) return (HandOutcome.Blackjack, handWager * 1.5m);
if (dealerBlackjack) return (HandOutcome.DealerBlackjack, -handWager);
if (playerValue > 21) return (HandOutcome.Bust, -handWager);
if (dealerValue > 21) return (HandOutcome.DealerBust, handWager);
if (playerValue > dealerValue) return (HandOutcome.Win, handWager);
if (playerValue < dealerValue) return (HandOutcome.Lose, -handWager);
return (HandOutcome.Push, 0);
}
private static async Task<string> FormatOutcomeTag(HandResultData r, string greenHex, string redHex)
{
var amt = await Math.Abs(r.Effect).FormatKasinoCurrencyAsync();
return r.Outcome switch
{
HandOutcome.Blackjack => $"[B][COLOR={greenHex}]BLACKJACK! +{amt}[/COLOR][/B]",
HandOutcome.Win => $"[B][COLOR={greenHex}]WIN! +{amt}[/COLOR][/B]",
HandOutcome.DealerBust => $"[B][COLOR={greenHex}]DEALER BUST! +{amt}[/COLOR][/B]",
HandOutcome.Lose => $"[B][COLOR={redHex}]LOSE! -{amt}[/COLOR][/B]",
HandOutcome.Bust => $"[B][COLOR={redHex}]BUST! -{amt}[/COLOR][/B]",
HandOutcome.DealerBlackjack => $"[B][COLOR={redHex}]DEALER BLACKJACK! -{amt}[/COLOR][/B]",
HandOutcome.Push => "[B][COLOR=orange]PUSH[/COLOR][/B]",
_ => "?"
};
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Supporting types used across BlackjackDisplay and BlackjackCommand
// ─────────────────────────────────────────────────────────────────────────────
internal enum HandOutcome
{
Blackjack, DealerBlackjack, Win, Lose, Bust, DealerBust, Push
}
/// Pre-computed per-hand result data passed from BlackjackCommand.ResolveGame
/// to BlackjackDisplay.FinalResult for rendering.
internal record HandResultData(
int HandIndex,
List<Card> Hand,
int PlayerValue,
HandOutcome Outcome,
decimal Effect);

View File

@@ -131,7 +131,7 @@ public class SlotsCommand : ICommand
} }
var imageUrl = await Zipline.Upload(finalImageStream, new MediaTypeHeaderValue("image/webp"), "1h", ctx); var imageUrl = await Zipline.Upload(finalImageStream, new MediaTypeHeaderValue("image/webp"), "1h", ctx);
await botInstance.SendChatMessageAsync($"[img]{imageUrl}[/img]", true, await botInstance.SendChatMessageAsync($"[img]{imageUrl}[/img]", true,
autoDeleteAfter: TimeSpan.FromSeconds(150)); autoDeleteAfter: TimeSpan.FromSeconds(60)); // delay till slots graphic deletion.
} }
winnings = (decimal)board.RunningTotalDisplay; winnings = (decimal)board.RunningTotalDisplay;