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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
|
package game
import (
"fmt"
"strings"
"thehouseoficarus/internal/casino"
"thehouseoficarus/internal/color"
)
// Coin Flip inks. The two faces are deliberately far apart on the xterm
// wheel: bright red for the loss face and a deep sky blue for the win face.
const (
coinFlipRedInk = 196
coinFlipBlueInk = 45
coinFlipDimInk = 245
)
// coinFlipLabel renders a signed net multiplier, e.g. "+50x" or "-100x".
func coinFlipLabel(multiplier int) string {
return fmt.Sprintf("%+dx", multiplier)
}
// coinFlipWheelInk tiers a wheel value: under 50x is plain, 50-74x takes the
// face colour, and 75x and up is bold as well.
func coinFlipWheelInk(value int, side string) (int, bool) {
magnitude := value
if magnitude < 0 {
magnitude = -magnitude
}
if magnitude < 50 {
return -1, false
}
ink := coinFlipRedInk
if side == casino.CoinFlipSideBlue {
ink = coinFlipBlueInk
}
return ink, magnitude >= 75
}
// coinFlipRow is one scanline of the coin: padding, an outline run, the
// interior (which alone is filled when the coin settles), the other outline
// run, and trailing padding.
type coinFlipRow struct {
pre, left, interior, right, post string
}
// coinFlipArt draws one medium coin: a nine-column, five-row circle with the
// signed multiplier centered inside. A settled coin fills only the interior
// cells, so the partial outline blocks keep the circle's round edge instead of
// growing into a square. A spinning coin gets whoosh marks on either side.
func coinFlipArt(multiplier int, side string, settled, alt bool, mode string, unicode bool) []string {
const inner = 7
label := coinFlipLabel(multiplier)
if len(label) > inner {
label = label[:inner]
}
content := rouletteCenter(label, inner)
ink := coinFlipRedInk
if side == casino.CoinFlipSideBlue {
ink = coinFlipBlueInk
}
plain := func(text string) string { return color.Render(mode, color.ColorSpec{Fg: -1, Bg: -1}, text) }
outline := func(text string) string {
if text == "" {
return ""
}
return color.Render(mode, color.ColorSpec{Fg: ink, Bg: -1, Bold: true}, text)
}
interior := func(text string) string {
if text == "" {
return ""
}
bg, fg := -1, ink
if settled {
// Fill only the interior: the outline's partial cells stay open.
bg, fg = ink, 16
}
var b strings.Builder
for _, r := range text {
if r == ' ' {
b.WriteString(color.Render(mode, color.ColorSpec{Fg: -1, Bg: bg}, " "))
} else {
b.WriteString(color.Render(mode, color.ColorSpec{Fg: fg, Bg: bg, Bold: true}, string(r)))
}
}
return b.String()
}
var rows []coinFlipRow
if unicode {
rows = []coinFlipRow{
{" ", strings.Repeat("▄", inner-2), "", "", " "},
{" ", "▟", strings.Repeat(" ", inner-2), "▙", " "},
{"", "▐", content, "▌", ""},
{" ", "▜", strings.Repeat(" ", inner-2), "▛", " "},
{" ", strings.Repeat("▀", inner-2), "", "", " "},
}
} else {
rows = []coinFlipRow{
{" ", strings.Repeat("_", inner-2), "", "", " "},
{" ", "/", strings.Repeat(" ", inner-2), "\\", " "},
{"", "|", content, "|", ""},
{" ", "\\", strings.Repeat(" ", inner-2), "/", " "},
{" ", strings.Repeat("-", inner-2), "", "", " "},
}
}
lines := make([]string, 0, len(rows))
for _, r := range rows {
lines = append(lines, plain(r.pre)+outline(r.left)+interior(r.interior)+outline(r.right)+plain(r.post))
}
if !settled {
// Only the two flank rows carry motion marks, and they swap the whoosh
// and arrow each frame so the coin reads as tumbling.
arrow := color.Render(mode, color.ColorSpec{Fg: ink, Bold: true}, "↺")
whoosh := color.Render(mode, color.ColorSpec{Fg: ink, Bold: true}, "~")
for i := range lines {
var left, right string
switch i {
case 1:
if alt {
left, right = arrow, whoosh
} else {
left, right = whoosh, arrow
}
case 3:
if alt {
left, right = whoosh, arrow
} else {
left, right = arrow, whoosh
}
default:
continue
}
lines[i] = left + " " + lines[i] + " " + right
}
}
return lines
}
// coinFlipWheelChar is one character of the scrolling multiplier strip.
type coinFlipWheelChar struct {
ch rune
value int
sep bool
label bool
}
// coinFlipWheel builds the full strip: a separator, then each value centred in
// a fixed cell. The engine and renderer share the geometry constants, so the
// landing offset and the drawn pointer always agree.
func coinFlipWheel(values []int, unicode bool) []coinFlipWheelChar {
sep := '│'
if !unicode {
sep = '|'
}
chars := make([]coinFlipWheelChar, 0, len(values)*casino.CoinFlipPitch+1)
for _, v := range values {
chars = append(chars, coinFlipWheelChar{ch: sep, sep: true})
for _, r := range rouletteCenter(coinFlipLabel(v), casino.CoinFlipCellWidth) {
chars = append(chars, coinFlipWheelChar{ch: r, value: v, label: r != ' '})
}
}
chars = append(chars, coinFlipWheelChar{ch: sep, sep: true})
return chars
}
// renderCoinFlipSpin draws the scrolling multiplier strip, a fixed pointer, and
// a bold SET under the landed value.
func renderCoinFlipSpin(s *casino.CoinFlipSnapshot, mode string, unicode bool) string {
pointer := "▼"
if !unicode {
pointer = "V"
}
wheel := coinFlipWheel(s.SpinValues, unicode)
var out strings.Builder
out.WriteString(strings.Repeat(" ", casino.CoinFlipPointerCol))
out.WriteString(color.Render(mode, color.ColorSpec{Fg: -1, Bold: true}, pointer))
out.WriteByte('\n')
for i := 0; i < casino.CoinFlipViewWidth; i++ {
idx := s.SpinOffset + i
cell := coinFlipWheelChar{ch: ' '}
if idx >= 0 && idx < len(wheel) {
cell = wheel[idx]
}
spec := color.ColorSpec{Fg: coinFlipDimInk}
switch {
case cell.label:
ink, bold := coinFlipWheelInk(cell.value, s.SpinSide)
spec = color.ColorSpec{Fg: ink, Bold: bold}
case cell.sep:
spec = color.ColorSpec{Fg: coinFlipDimInk}
}
out.WriteString(color.Render(mode, spec, string(cell.ch)))
}
out.WriteByte('\n')
if s.Set {
out.WriteString(strings.Repeat(" ", casino.CoinFlipPointerCol-1))
out.WriteString(color.Render(mode, color.ColorSpec{Fg: -1, Bold: true}, "SET"))
}
return strings.TrimRight(out.String(), " \n")
}
// coinFlipResultCell centers a coloured label/value pair inside one panel cell.
func coinFlipResultCell(label, value string, ink, width int, mode string) string {
text := label + " " + value
pad := max(0, width-len(text))
left := pad / 2
right := pad - left
return strings.Repeat(" ", left) +
color.Render(mode, color.ColorSpec{Fg: ink, Bold: true}, label) + " " +
color.Render(mode, color.ColorSpec{Fg: 250, Bold: true}, value) +
strings.Repeat(" ", right)
}
// renderCoinFlipResults draws the two landed faces as a compact coloured panel.
func renderCoinFlipResults(s *casino.CoinFlipSnapshot, mode string, unicode bool) string {
const cell = 15
b := rouletteBorders(unicode)
dim := func(text string) string { return color.Render(mode, color.ColorSpec{Fg: coinFlipDimInk}, text) }
top := dim(b.tl + strings.Repeat(b.h, cell) + b.teeTop + strings.Repeat(b.h, cell) + b.tr)
mid := dim(b.v) +
coinFlipResultCell("RED", coinFlipLabel(s.Red), coinFlipRedInk, cell, mode) + dim(b.v) +
coinFlipResultCell("BLUE", coinFlipLabel(s.Blue), coinFlipBlueInk, cell, mode) + dim(b.v)
bottom := dim(b.bl + strings.Repeat(b.h, cell) + b.teeBottom + strings.Repeat(b.h, cell) + b.br)
return top + "\n" + mid + "\n" + bottom
}
func renderCoinFlip(s *casino.CoinFlipSnapshot, mode string, unicode bool) string {
switch s.Phase {
case casino.CoinFlipPhaseRedSpin, casino.CoinFlipPhaseBlueSpin:
return renderCoinFlipSpin(s, mode, unicode)
case casino.CoinFlipPhaseResults:
return renderCoinFlipResults(s, mode, unicode)
case casino.CoinFlipPhaseFlipping, casino.CoinFlipPhaseResult:
return strings.Join(coinFlipArt(s.Multiplier, s.Side, s.Result, s.FlipAlt, mode, unicode), "\n")
}
return ""
}
|