diff --git a/KfChatDotNetBot/Commands/Kasino/BlackjackCommand.cs b/KfChatDotNetBot/Commands/Kasino/BlackjackCommand.cs
index f816989..e5ea5b0 100644
--- a/KfChatDotNetBot/Commands/Kasino/BlackjackCommand.cs
+++ b/KfChatDotNetBot/Commands/Kasino/BlackjackCommand.cs
@@ -573,4 +573,230 @@ public class BlackjackCommand : ICommand
$"{user.FormatUsername()}, your blackjack game timed out and you forfeited {await wager.WagerAmount.FormatKasinoCurrencyAsync()}",
true, autoDeleteAfter: cleanupDelay);
}
-}
\ No newline at end of file
+}
+
+///
+/// Builds every chat message string for the blackjack game.
+/// No game logic lives here — only presentation.
+///
+/// Keeping all string construction in one place means tweaking the UI never
+/// requires touching the game-flow code in .
+///
+///
+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 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 { "[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 GameStart(
+ UserDbModel user, decimal wager,
+ List playerHand, int playerValue,
+ List 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 currentHand, int handValue,
+ List 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 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 SplitDeal(
+ UserDbModel user, decimal totalWager,
+ List hand1, int value1,
+ List 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 finishedHand, int finishedValue, bool busted,
+ int nextIndex, List 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 FinalResult(
+ UserDbModel user,
+ IReadOnlyList results,
+ List 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();
+ 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 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 Hand,
+ int PlayerValue,
+ HandOutcome Outcome,
+ decimal Effect);
\ No newline at end of file
diff --git a/KfChatDotNetBot/Commands/Kasino/BlackjackDisplay.cs b/KfChatDotNetBot/Commands/Kasino/BlackjackDisplay.cs
deleted file mode 100644
index bd74006..0000000
--- a/KfChatDotNetBot/Commands/Kasino/BlackjackDisplay.cs
+++ /dev/null
@@ -1,231 +0,0 @@
-using KfChatDotNetBot.Extensions;
-using KfChatDotNetBot.Models;
-using KfChatDotNetBot.Models.DbModels;
-
-namespace KfChatDotNetBot.Commands.Kasino;
-
-///
-/// Builds every chat message string for the blackjack game.
-/// No game logic lives here — only presentation.
-///
-/// Keeping all string construction in one place means tweaking the UI never
-/// requires touching the game-flow code in .
-///
-///
-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 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 { "[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 GameStart(
- UserDbModel user, decimal wager,
- List playerHand, int playerValue,
- List 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 currentHand, int handValue,
- List 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 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 SplitDeal(
- UserDbModel user, decimal totalWager,
- List hand1, int value1,
- List 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 finishedHand, int finishedValue, bool busted,
- int nextIndex, List 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 FinalResult(
- UserDbModel user,
- IReadOnlyList results,
- List 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();
- 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 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 Hand,
- int PlayerValue,
- HandOutcome Outcome,
- decimal Effect);
\ No newline at end of file