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("%.6g%% 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" }