diff options
Diffstat (limited to 'internal/game/render_coin_flip.go')
| -rw-r--r-- | internal/game/render_coin_flip.go | 238 |
1 files changed, 238 insertions, 0 deletions
diff --git a/internal/game/render_coin_flip.go b/internal/game/render_coin_flip.go new file mode 100644 index 0000000..6408b26 --- /dev/null +++ b/internal/game/render_coin_flip.go @@ -0,0 +1,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 "" +} |
