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 "" }