aboutsummaryrefslogtreecommitdiff
path: root/internal/game/render_blackjack_felt.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/render_blackjack_felt.go')
-rw-r--r--internal/game/render_blackjack_felt.go137
1 files changed, 137 insertions, 0 deletions
diff --git a/internal/game/render_blackjack_felt.go b/internal/game/render_blackjack_felt.go
new file mode 100644
index 0000000..41a5382
--- /dev/null
+++ b/internal/game/render_blackjack_felt.go
@@ -0,0 +1,137 @@
+package game
+
+import (
+ "fmt"
+ "strconv"
+
+ "thehouseoficarus/internal/casino"
+)
+
+// A blackjack felt: the classic centre-display phrases a real table prints
+// (blackjack payout, dealer draw rule, insurance), then the remaining
+// configurable rules arranged in labelled boxes. It reuses the craps felt
+// canvas and palette: fixed 77-column art, dark green baize, gold headings.
+// Fixed engine behaviour (peek timing, rounding) is left off the felt.
+func renderBlackjackRulesFelt(cfg casino.TableConfig, mode string, unicode bool) string {
+ b := cfg.Blackjack.Normalize()
+ c := newCrapsCanvas(21)
+ // Centre display: the phrases printed on every real blackjack table.
+ c.box(0, 0, 76, 5)
+ c.center(1, 75, 2, "BLACKJACK PAYS "+ratioText(b.BlackjackPayout), crapsGold)
+ dealer := "DEALER MUST STAND ON 17 AND DRAW TO 16"
+ if b.DealerHitsSoft17() {
+ dealer = "DEALER HITS SOFT 17"
+ }
+ c.center(1, 75, 3, dealer, crapsInk)
+ c.center(1, 75, 4, "INSURANCE PAYS "+ratioText(b.InsurancePayout), crapsGold)
+ // Four labelled rule boxes share edges with the centre display above.
+ boxY := [4][2]int{{5, 12}, {5, 12}, {12, 20}, {12, 20}}
+ xs := [4][2]int{{0, 38}, {38, 76}, {0, 38}, {38, 76}}
+ for i := range boxY {
+ c.box(xs[i][0], boxY[i][0], xs[i][1], boxY[i][1])
+ }
+ titles := [4]string{"THE SHOE", "PAYS", "DOUBLE & SPLIT", "HOUSE"}
+ for i, title := range titles {
+ c.center(xs[i][0]+1, xs[i][1]-1, boxY[i][0]+1, title, crapsGold)
+ }
+ rows := [4][][2]string{
+ {
+ {"DECKS", fmt.Sprintf("%d", b.Decks)},
+ {"SHUFFLE AT", fmt.Sprintf("%g%% REMAINING", b.ShuffleAt*100)},
+ {"SHOE LOW", "RESHUFFLE MID-ROUND"},
+ {"HOLE CARD", "YES (PEEK)"},
+ {"", ""},
+ },
+ {
+ {"ORDINARY WIN", "1:1"},
+ {"TIE", "PUSH"},
+ {"BJ VS BJ", "PUSH"},
+ {"SPLIT-HAND 21", "1:1"},
+ {"SPLIT+DOUBLE 21", splitDouble21Text(b)},
+ {"", ""},
+ },
+ {
+ {"DOUBLE", "ANY TWO, ONE CARD"},
+ {"AFTER SPLIT", yesNoText(b.AllowsDoubleAfterSplit())},
+ {"MAX HANDS", fmt.Sprintf("%d (RESPLIT %s)", b.MaxSplitHands, yesNoText(b.MaxSplitHands > 2))},
+ {"RESPLIT ACES", yesNoText(b.AllowsResplitAces())},
+ {"PAIRS", pairsText(b)},
+ {"SPLIT ACES", "ONE CARD, NO HITS"},
+ },
+ {
+ {"SURRENDER", surrenderText(b)},
+ {"INSURANCE COST", "HALF THE BET"},
+ {"BET LIMITS", betLimitsText(cfg.MinBet, cfg.MaxBet)},
+ {"SEATS", fmt.Sprintf("%d", cfg.MaxPlayers)},
+ {"BETTING WINDOW", fmt.Sprintf("%d TICKS", cfg.BettingTicks)},
+ {"ACTION WINDOW", fmt.Sprintf("%d TICKS", b.ActionTicks)},
+ },
+ }
+ for i, box := range rows {
+ for j, row := range box {
+ if row[0] == "" && row[1] == "" {
+ continue
+ }
+ c.bjRule(xs[i][0], xs[i][1], boxY[i][0]+2+j, row[0], row[1])
+ }
+ }
+ return c.render(mode, unicode)
+}
+
+// bjRule paints one "LABEL ..... VALUE" row inside a box: the label muted at
+// the left margin, the value right-aligned to the box's inner right edge.
+func (c *crapsCanvas) bjRule(x1, x2, y int, label, value string) {
+ c.text(x1+2, y, label, crapsMuted)
+ vx := x2 - 1 - len([]rune(value))
+ if vx < x1+2+len([]rune(label))+1 {
+ vx = x1 + 2 + len([]rune(label)) + 1
+ }
+ c.text(vx, y, value, crapsInk)
+}
+
+// betLimitsText renders the wager range compactly so it fits inside a box.
+func betLimitsText(minBet, maxBet int) string {
+ if maxBet == int(^uint(0)>>1) {
+ return fmt.Sprintf("%d+ CREDITS", minBet)
+ }
+ return fmt.Sprintf("%d-%d CREDITS", minBet, maxBet)
+}
+
+func ratioText(p float64) string {
+ switch p {
+ case 1.5:
+ return "3 TO 2"
+ case 1.2:
+ return "6 TO 5"
+ case 2:
+ return "2 TO 1"
+ case 1:
+ return "1 TO 1"
+ default:
+ return strconv.FormatFloat(p, 'g', -1, 64) + " TO 1"
+ }
+}
+func splitDouble21Text(b casino.BlackjackConfig) string {
+ if b.CountsDoubleSplitBlackjack() {
+ return "BLACKJACK " + ratioText(b.BlackjackPayout)
+ }
+ return "1:1"
+}
+func yesNoText(v bool) string {
+ if v {
+ return "YES"
+ }
+ return "NO"
+}
+func pairsText(b casino.BlackjackConfig) string {
+ if b.AllowsUnlikeTenSplit() {
+ return "RANKS OR TENS"
+ }
+ return "IDENTICAL RANKS"
+}
+func surrenderText(b casino.BlackjackConfig) string {
+ if b.AllowsSurrender() {
+ return "LATE, TWO CARDS"
+ }
+ return "NOT OFFERED"
+}