aboutsummaryrefslogtreecommitdiff
path: root/internal/game/render_blackjack_felt.go
blob: 41a53827ae9f4473f491d2cc9087f18793f026b4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
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"
}