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
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
|
package game
import (
"fmt"
"strings"
"thehouseoficarus/internal/casino"
"thehouseoficarus/internal/color"
)
// A half-table: numbered boxes and the come/field/line rails on the left;
// the shared centre-action bets on the right. Every region is a real box,
// and shared edges are joined rather than pasted together as broken borders.
const crapsFelt = 22 // #005f00: darker than ANSI green / the roulette felt.
const (
crapsInk = iota
crapsGold
crapsRed
crapsMuted
crapsMarked
crapsPuckOn
crapsPuckOff
crapsBonus
crapsGoldInk
crapsBonusInk
crapsBonusDim
)
var crapsPaint = []color.ColorSpec{
{Fg: 15, Bg: crapsFelt},
{Fg: 229, Bg: crapsFelt, Bold: true},
{Fg: 160, Bg: crapsFelt, Bold: true}, // deep red for come/pass/props
{Fg: 108, Bg: crapsFelt},
{Fg: 0, Bg: 220, Bold: true},
{Fg: 0, Bg: 15, Bold: true},
{Fg: 15, Bg: 0, Bold: true},
{Fg: 229, Bg: 24, Bold: true}, // Fire/ATS tracker: yellow on blue
{Fg: 229, Bg: crapsFelt}, // regular-weight gold, used for field notes
{Fg: 229, Bg: 24}, // regular-weight tracker text and brackets
{Fg: 108, Bg: 24}, // dim tracker odds, matching the place/lay payouts
}
type crapsCell struct {
glyph rune
edges uint8
ink int
}
type crapsCanvas struct{ cells [][]crapsCell }
// fill paints one rectangular region with an ink without touching glyphs or
// box edges, so a whole tracker section can share one background colour.
func (c *crapsCanvas) fill(x1, y1, x2, y2, ink int) {
for y := y1; y <= y2; y++ {
if y < 0 || y >= len(c.cells) {
continue
}
for x := x1; x <= x2; x++ {
if x >= 0 && x < len(c.cells[y]) {
c.cells[y][x].ink = ink
}
}
}
}
// crapsCanvasWidth is the fixed felt width every craps render uses.
const crapsCanvasWidth = 77
func newCrapsCanvas(h int) *crapsCanvas {
c := &crapsCanvas{cells: make([][]crapsCell, h)}
for y := range c.cells {
c.cells[y] = make([]crapsCell, crapsCanvasWidth)
}
return c
}
func (c *crapsCanvas) box(x1, y1, x2, y2 int) {
// N=1, E=2, S=4, W=8. Unioning edges makes all T/cross joints exact.
for x := x1; x < x2; x++ {
c.cells[y1][x].edges |= 2
c.cells[y1][x+1].edges |= 8
c.cells[y2][x].edges |= 2
c.cells[y2][x+1].edges |= 8
}
for y := y1; y < y2; y++ {
c.cells[y][x1].edges |= 4
c.cells[y+1][x1].edges |= 1
c.cells[y][x2].edges |= 4
c.cells[y+1][x2].edges |= 1
}
}
func (c *crapsCanvas) text(x, y int, s string, ink int) {
for _, r := range s {
if x >= 0 && x < len(c.cells[y]) {
c.cells[y][x] = crapsCell{glyph: r, ink: ink}
}
x++
}
}
func (c *crapsCanvas) center(x1, x2, y int, s string, ink int) {
c.text(x1+(x2-x1+1-color.VisibleLen(s))/2, y, s, ink)
}
// crapsSpan is one color run inside a centered line. centerSpans lays the runs
// end to end and centers their combined width, so a label can stay regular
// weight while its payout is dim, or a heading can pair two inks on one row.
type crapsSpan struct {
text string
ink int
}
func (c *crapsCanvas) centerSpans(x1, x2, y int, spans ...crapsSpan) {
total := 0
for _, s := range spans {
total += color.VisibleLen(s.text)
}
x := x1 + (x2-x1+1-total)/2
for _, s := range spans {
c.text(x, y, s.text, s.ink)
x += color.VisibleLen(s.text)
}
}
// crapsJoints maps a union of box edges (N=1, E=2, S=4, W=8) to its glyph.
var crapsJoints = map[uint8]rune{3: '└', 6: '┌', 9: '┘', 12: '┐', 7: '├', 11: '┴', 13: '┤', 14: '┬', 15: '┼'}
// crapsEdgeRune is the box glyph for a cell's edges, downgraded to ASCII when
// unicode is off.
func crapsEdgeRune(cell crapsCell, unicode bool) rune {
if cell.edges == 0 {
return ' '
}
var r rune
switch {
case cell.edges&5 == 0:
r = '─'
case cell.edges&10 == 0:
r = '│'
default:
r = crapsJoints[cell.edges]
}
if !unicode {
switch r {
case '─':
r = '-'
case '│':
r = '|'
default:
r = '+'
}
}
return r
}
func (c *crapsCanvas) render(mode string, unicode bool) string {
var out strings.Builder
for _, row := range c.cells {
var run strings.Builder
ink := row[0].ink
flush := func() {
if run.Len() > 0 {
out.WriteString(color.Render(mode, crapsPaint[ink], run.String()))
run.Reset()
}
}
for _, cell := range row {
if cell.ink != ink {
flush()
ink = cell.ink
}
r := cell.glyph
if r == 0 {
r = crapsEdgeRune(cell, unicode)
}
run.WriteRune(r)
}
flush()
out.WriteByte('\n')
}
return out.String()
}
func crapsHas(ns []int, n int) bool {
for _, v := range ns {
if v == n {
return true
}
}
return false
}
// Die-face glyph, e.g. ⚀ for one, used only where a felt paints dice pictures:
// the hardway pairs and horn combinations. Blobs come first so a die face
// never lexes as 1–6 elsewhere.
func crapsDie(n int) string {
return string([]rune{'⚀', '⚁', '⚂', '⚃', '⚄', '⚅'}[n-1])
}
// crapsDrawPuck paints the point puck: OFF on the layout, ON in the point's
// number box. It is shared by the bank and variant felts.
func crapsDrawPuck(c *crapsCanvas, x, y int, on, unicode bool) {
ink, word := crapsPuckOff, "OFF"
if on {
ink, word = crapsPuckOn, "ON "
}
a, b, d := "+---+", "|"+word+"|", "+---+"
if unicode {
a, b, d = "╭───╮", "│"+word+"│", "╰───╯"
}
c.text(x, y, a, ink)
c.text(x, y+1, b, ink)
c.text(x, y+2, d, ink)
}
func renderCrapsTrackers(s *casino.CrapsSnapshot, mode string, unicode bool) string {
c := newCrapsCanvas(7)
// The whole Fire/ATS tracker sits on one blue field.
c.fill(0, 0, 76, 6, crapsBonus)
c.box(0, 0, 76, 2)
c.box(0, 4, 38, 6)
c.box(38, 4, 76, 6)
c.text(3, 1, "FIRE", crapsBonus)
for i, n := range []int{4, 5, 6, 8, 9, 10} {
crapsTrackerPoint(c, 15+i*9, 1, n, crapsHas(s.Made, n))
}
bounds := []int{0, 25, 50, 76}
for i, payout := range []string{"24:1", "249:1", "999:1"} {
lo, hi := bounds[i], bounds[i+1]
c.box(lo, 2, hi, 4)
c.centerSpans(lo+1, hi-1, 3,
crapsSpan{fmt.Sprintf("%d POINTS", i+4), crapsBonusInk},
crapsSpan{" " + payout, crapsBonusDim})
}
c.text(2, 5, "SMALL", crapsBonus)
c.text(40, 5, "TALL", crapsBonus)
for i, n := range []int{2, 3, 4, 5, 6, 8, 9, 10, 11, 12} {
x := 12 + i*5
if i >= 5 {
x = 48 + (i-5)*5
}
crapsTrackerPoint(c, x, 5, n, crapsHas(s.Seen, n))
}
return c.render(mode, unicode)
}
// crapsTrackerPoint draws one bracketed total for the Fire/ATS tracker. The
// brackets stay regular weight while the number itself is bold; a made or
// seen point keeps its solid highlight across the whole token.
func crapsTrackerPoint(c *crapsCanvas, x, y, n int, marked bool) {
if marked {
c.text(x, y, fmt.Sprintf("[%2d]", n), crapsMarked)
return
}
c.text(x, y, "[", crapsBonusInk)
c.text(x+1, y, fmt.Sprintf("%2d", n), crapsBonus)
c.text(x+3, y, "]", crapsBonusInk)
}
func renderCrapsFelt(s *casino.CrapsSnapshot, mode string, unicode bool) string {
// Compact half-table: every rail keeps its label and value on adjacent
// rows, so the felt is five rows shorter than the spaced-out layout.
c := newCrapsCanvas(21)
c.box(0, 0, 14, 6)
c.center(1, 13, 4, "DON'T COME", crapsInk)
c.center(1, 13, 5, "BAR 12", crapsMuted)
nums := []int{4, 5, 6, 8, 9, 10}
numLabels := []string{"4", "5", "SIX", "8", "NINE", "10"}
pays := []string{"9:5", "7:5", "7:6", "7:6", "7:5", "9:5"}
if s.Point == 0 {
crapsDrawPuck(c, 5, 1, false, unicode)
}
for i, n := range nums {
x := 14 + i*6
c.box(x, 0, x+6, 6)
c.center(x+1, x+5, 4, numLabels[i], crapsGold)
c.center(x+1, x+5, 5, pays[i], crapsMuted)
if s.Point == n {
crapsDrawPuck(c, x+1, 1, true, unicode)
}
}
c.box(0, 6, 50, 10)
c.center(1, 49, 7, "PLACE / BUY / LAY ON THE NUMBERS ABOVE", crapsMuted)
c.center(1, 49, 9, "C O M E", crapsRed)
c.box(0, 10, 50, 14)
c.center(1, 49, 11, "F I E L D", crapsGold)
c.center(1, 49, 12, "( 2 ) 3 4 9 10 11 ( 12 )", crapsInk)
twelve := s.Field12
if twelve == 0 {
twelve = 2
}
twelvePay := "DOUBLE"
if twelve == 3 {
twelvePay = "TRIPLE"
}
c.center(1, 49, 13, fmt.Sprintf("2 PAYS DOUBLE 12 PAYS %s", twelvePay), crapsGoldInk)
c.box(0, 14, 50, 16)
c.centerSpans(1, 49, 15, crapsSpan{"DON'T PASS", crapsInk}, crapsSpan{" BAR 12", crapsMuted})
c.box(0, 16, 50, 18)
c.center(1, 49, 17, "PASS LINE", crapsRed)
c.box(0, 18, 50, 20)
odds := s.Odds
if odds == "" {
odds = "3x-4x-5x"
}
c.centerSpans(1, 49, 19, crapsSpan{"FREE ODDS", crapsInk}, crapsSpan{" (" + odds + ")", crapsMuted})
// Centre action: four bonus spots, four hardways, six propositions, horn/hop.
// The Fire and ATS spots share one blue field.
bonus := []struct{ label, pays string }{{"FIRE", "24x"}, {"SMALL", "34:1"}, {"ALL", "175:1"}, {"TALL", "34:1"}}
xs := []int{50, 56, 63, 69, 76}
for i := range bonus {
c.box(xs[i], 0, xs[i+1], 5)
}
c.fill(50, 0, 76, 5, crapsBonus)
for i, spot := range bonus {
c.center(xs[i]+1, xs[i+1]-1, 2, spot.label, crapsBonus)
c.center(xs[i]+1, xs[i+1]-1, 4, spot.pays, crapsBonusDim)
}
for i, n := range []int{4, 6, 8, 10} {
x, y := 50+(i%2)*13, 5+(i/2)*3
c.box(x, y, x+13, y+3)
c.center(x+1, x+12, y+1, fmt.Sprintf("HARD %d", n), crapsInk)
pay := 7
if n == 6 || n == 8 {
pay = 9
}
pair := fmt.Sprintf("%d+%d", n/2, n/2)
if unicode {
pair = crapsDie(n/2) + "+" + crapsDie(n/2)
}
c.centerSpans(x+1, x+12, y+2, crapsSpan{pair + " ", crapsGold}, crapsSpan{fmt.Sprintf("%d:1", pay), crapsMuted})
}
props := []string{"SEVEN", "CRAPS", "ELEVEN", "ACES", "ACE 3", "BOXCARS"}
oddsRows := []string{"4:1", "7:1", "15:1", "30:1", "15:1", "30:1"}
px := []int{50, 59, 67, 76}
for i, label := range props {
col, y := i%3, 11+(i/3)*3
x, x2 := px[col], px[col+1]
c.box(x, y, x2, y+3)
c.center(x+1, x2-1, y+1, label, crapsRed)
c.center(x+1, x2-1, y+2, oddsRows[i], crapsMuted)
}
c.box(50, 17, 63, 20)
c.center(51, 62, 18, "HORN", crapsGold)
horn := "2 3 11 12"
if unicode {
// Horn combinations as painted dice pairs: 2, 3, 11, 12.
horn = strings.Join([]string{crapsDie(1) + crapsDie(1), crapsDie(1) + crapsDie(2), crapsDie(5) + crapsDie(6), crapsDie(6) + crapsDie(6)}, " ")
}
c.center(51, 62, 19, horn, crapsMuted)
c.box(63, 17, 76, 20)
c.center(64, 75, 18, "HOP", crapsGold)
c.center(64, 75, 19, "15:1 / 30:1", crapsMuted)
return c.render(mode, unicode)
}
// renderHighPointFelt draws High Point's number row, point puck and pass
// line, with the come-out special case printed on the felt.
func renderHighPointFelt(s *casino.CrapsSnapshot, mode string, unicode bool) string {
c := newCrapsCanvas(16)
c.box(0, 0, 76, 15)
c.center(1, 75, 1, "HIGH POINT CRAPS", crapsGold)
c.center(1, 75, 2, "COME-OUT 2 OR 3: IGNORED | 11 OR 12: WINS", crapsInk)
nums := []int{4, 5, 6, 7, 8, 9, 10}
labels := []string{"4", "5", "SIX", "7", "8", "NINE", "10"}
for i, n := range nums {
x := 3 + i*10
c.box(x, 5, x+10, 10)
c.center(x+1, x+9, 9, labels[i], crapsGold)
if s.Point == n {
crapsDrawPuck(c, x+3, 6, true, unicode)
}
}
c.box(2, 12, 74, 14)
c.center(3, 73, 13, "PASS LINE", crapsGold)
return c.render(mode, unicode)
}
// renderCraplessFelt gives every total a point box and shows the pass rail;
// crapless has no don't bets or bonuses.
func renderCraplessFelt(s *casino.CrapsSnapshot, mode string, unicode bool) string {
c := newCrapsCanvas(16)
c.box(0, 0, 76, 15)
c.center(1, 75, 1, "CRAPLESS CRAPS", crapsGold)
c.center(1, 75, 2, "COME-OUT 7 WINS | ALL OTHER TOTALS BECOME POINTS", crapsInk)
nums := []int{2, 3, 4, 5, 6, 8, 9, 10, 11, 12}
for i, n := range nums {
x := 3 + i*7
c.box(x, 5, x+6, 10)
c.center(x+1, x+5, 9, fmt.Sprint(n), crapsGold)
if s.Point == n {
crapsDrawPuck(c, x+1, 6, true, unicode)
}
}
c.box(2, 12, 74, 14)
c.center(3, 73, 13, "PASS LINE", crapsGold)
return c.render(mode, unicode)
}
// renderNewYorkFelt is the pass/don't-pass, buy/lay layout.
func renderNewYorkFelt(s *casino.CrapsSnapshot, mode string, unicode bool) string {
c := newCrapsCanvas(16)
c.box(0, 0, 76, 15)
c.center(1, 75, 1, "NEW YORK CRAPS", crapsGold)
c.center(1, 75, 2, "PASS | DON'T PASS (BAR 12) | FREE ODDS | BUY / LAY 4 5 6 8 9 10", crapsInk)
nums := []int{4, 5, 6, 8, 9, 10}
for i, n := range nums {
x := 3 + i*12
c.box(x, 5, x+10, 10)
c.center(x+1, x+9, 9, fmt.Sprint(n), crapsGold)
if s.Point == n {
crapsDrawPuck(c, x+3, 6, true, unicode)
}
}
c.box(2, 12, 74, 14)
c.center(3, 73, 13, "PASS LINE / DON'T PASS", crapsGold)
return c.render(mode, unicode)
}
// renderDieRichFelt shows the single-die come-out targets and the four point
// faces, with the pass line beneath them.
func renderDieRichFelt(s *casino.CrapsSnapshot, mode string, unicode bool) string {
c := newCrapsCanvas(15)
c.box(0, 0, 76, 14)
c.center(1, 75, 1, "DIE RICH CRAPS", crapsGold)
c.center(1, 75, 2, "ONE DIE | COME-OUT 6 WINS, 1 LOSES | 2-5 BECOME THE POINT", crapsInk)
nums := []int{2, 3, 4, 5}
for i, n := range nums {
x := 8 + i*16
c.box(x, 5, x+12, 10)
c.center(x+1, x+11, 9, fmt.Sprint(n), crapsGold)
if s.Point == n {
crapsDrawPuck(c, x+4, 6, true, unicode)
}
}
c.box(2, 11, 74, 13)
c.center(3, 73, 12, "PASS LINE", crapsGold)
return c.render(mode, unicode)
}
// renderCrapsDice uses half-block caps and white backgrounds to make a
// square tile in a terminal's roughly 1:2 character cells. Pips are inset;
// monochrome clients get an outlined face rather than disconnected dots.
func renderCrapsDice(dice [2]int, mode string, unicode bool) string {
if dice[0] < 1 || dice[0] > 6 || dice[1] < 0 || dice[1] > 6 {
return ""
}
patterns := [7][3]string{
{},
{" ", " ● ", " "},
{" ● ", " ", " ● "},
{" ● ", " ● ", " ● "},
{" ● ● ", " ", " ● ● "},
{" ● ● ", " ● ", " ● ● "},
{" ● ● ", " ● ● ", " ● ● "},
}
colored := mode != "" && mode != "none"
face := color.ColorSpec{Fg: 0, Bg: 15}
cap := color.ColorSpec{Fg: 15, Bg: -1}
var out strings.Builder
for row := 0; row < 5; row++ {
out.WriteString(" ")
for i, n := range dice {
if n == 0 {
continue
}
if i > 0 {
out.WriteString(" ")
}
line := ""
spec := face
if row == 0 || row == 4 {
line = " "
if unicode && colored {
line, spec = "▄▄▄▄▄▄▄", cap
if row == 4 {
line = "▀▀▀▀▀▀▀"
}
} else if !colored {
line = "+-----+"
}
} else {
line = patterns[n][row-1]
if !colored {
line = "|" + strings.TrimPrefix(strings.TrimSuffix(line, " "), " ") + "|"
}
if !unicode {
line = strings.ReplaceAll(line, "●", "o")
}
}
out.WriteString(color.Render(mode, spec, line))
}
if row == 2 {
out.WriteString(color.Render(mode, color.ColorSpec{Fg: 229, Bg: -1, Bold: true}, fmt.Sprintf(" Total: %d", dice[0]+dice[1])))
}
out.WriteByte('\n')
}
return out.String()
}
func renderCrapsSnapshot(s *casino.CrapsSnapshot, mode string, unicode bool, styles ...string) string {
if s == nil {
return ""
}
style := "dark"
if len(styles) > 0 {
style = styles[0]
}
var out strings.Builder
title := func(text string) string {
return color.Render(mode, color.ColorSpec{Fg: 229, Bg: -1, Bold: true}, text)
}
subtle := func(text string) string { return color.Render(mode, color.ColorSpec{Fg: 250, Bg: -1}, text) }
point := "COME-OUT"
if s.Point != 0 {
point = fmt.Sprintf("POINT %d", s.Point)
}
if s.Variant == "simplified" {
// The title alone says it: simplified craps has no point phase.
point = ""
}
variant := s.Variant
if variant == "" {
variant = "bank"
}
out.WriteString("\n" + title(" "+strings.ToUpper(strings.ReplaceAll(variant, "_", " "))+" CRAPS"))
if point != "" {
out.WriteString(subtle(" / " + point))
}
out.WriteString("\n")
if variant == "bank" || variant == "diceless" {
out.WriteString(renderCrapsTrackers(s, mode, unicode))
out.WriteByte('\n')
out.WriteString(renderCrapsFelt(s, mode, unicode))
} else if variant == "simplified" {
out.WriteString(renderSimplifiedCrapsFelt(mode, unicode))
} else {
switch variant {
case "high_point":
out.WriteString(renderHighPointFelt(s, mode, unicode))
case "crapless":
out.WriteString(renderCraplessFelt(s, mode, unicode))
case "new_york":
out.WriteString(renderNewYorkFelt(s, mode, unicode))
case "die_rich":
out.WriteString(renderDieRichFelt(s, mode, unicode))
default:
c := newCrapsCanvas(7)
c.box(0, 0, 76, 6)
c.center(1, 75, 3, "HOUSE EDITION — rules teaches this edition", crapsGold)
out.WriteString(c.render(mode, unicode))
}
}
out.WriteByte('\n')
if variant == "diceless" {
out.WriteString(renderCrapsCards(s.Dice, mode, unicode, style))
} else {
out.WriteString(renderCrapsDice(s.Dice, mode, unicode))
}
if len(s.Queue) > 1 {
out.WriteString(subtle(" On deck "+strings.Join(s.Queue[1:], " > ")) + "\n")
}
hint := ""
if s.Phase != "waiting" && s.Phase != "" {
hint = ""
if s.Variant == "simplified" {
if s.ShootIn > 0 {
hint = fmt.Sprintf("The dice roll in %dt.", s.ShootIn)
}
} else if s.ShootIn > 0 {
hint = fmt.Sprintf("Bets open. Shooter ready in %dt.", s.ShootIn)
} else if s.DealerIn > 0 {
hint = fmt.Sprintf("Shooter ready. Dealer rolls in %dt; bets still open.", s.DealerIn)
}
}
if s.AutoRoll {
hint = fmt.Sprintf("AUTOROLL / Shooter's next roll in %dt", s.AutoIn)
}
if hint != "" {
out.WriteString(subtle(" "+hint) + "\n")
}
return out.String()
}
|