package game import ( "fmt" "strings" "thehouseoficarus/internal/casino" "thehouseoficarus/internal/color" ) // cashHuntGradient is the board's accent gradient: red, orange, yellow, green, // cyan, purple. It deliberately omits dark blue and ends on purple. var cashHuntGradient = []int{196, 208, 220, 46, 51, 129} func cashHuntPrizeColor(multiplier int) int { switch { case multiplier >= 10000: return 225 case multiplier >= 500: return 201 case multiplier >= 100: return 220 case multiplier >= 20: return 207 case multiplier >= 5: return 141 case multiplier >= 2: return 81 case multiplier == 1: return 120 default: // 0x is not a prize: match the dim square numbers so it recedes. return 237 } } const ( cashHuntColumns = 10 cashHuntTileWidth = 6 // One outer border on each side plus one separator between each of the // columns. cashHuntBoardWidth = cashHuntColumns*(cashHuntTileWidth+1) + 1 ) // The whole board sits on black; the box-drawing grid supplies the separation. // cashHuntBoxFg keeps the grid lines a touch darker than the square numbers. const ( cashHuntTileBlack = 16 cashHuntBoxFg = 240 ) // cashHuntCenter pads text to a fixed tile width, keeping every column aligned // whether it holds a one- or three-digit square number. func cashHuntCenter(text string) string { width := cashHuntTileWidth if len(text) >= width { return text } left := (width - len(text)) / 2 return strings.Repeat(" ", left) + text + strings.Repeat(" ", width-len(text)-left) } // cashHuntHeader builds the board's single title row: difficulty (and the // optional RTP readout) on the left, the gradient title centered, and the // board's top multiplier on the right. Every segment carries the board's black // background so the header reads as part of the same panel. func cashHuntHeader(s *casino.CashHuntSnapshot, mode, rtp string, top int) string { left := strings.ToUpper(s.Risk) if rtp != "" { left += " " + rtp } rightLabel := "TOP PRIZE: " rightValue := fmt.Sprintf("%dx", top) right := rightLabel + rightValue mid := "CASH HUNT" inner := cashHuntBoardWidth - 2 gap := inner - len(left) - len(mid) - len(right) if gap < 1 { gap = 1 } leftPad, rightPad := gap/2, gap-gap/2 left = color.Render(mode, color.ColorSpec{Fg: 250, Bg: cashHuntTileBlack}, left) mid = color.Render(mode, color.ColorSpec{Gradient: cashHuntGradient, Bold: true, Bg: cashHuntTileBlack}, mid) right = color.Render(mode, color.ColorSpec{Fg: 250, Bg: cashHuntTileBlack}, rightLabel) + color.Render(mode, color.ColorSpec{Fg: cashHuntPrizeColor(top), Bold: true, Bg: cashHuntTileBlack}, rightValue) space := func(n int) string { return color.Render(mode, color.ColorSpec{Bg: cashHuntTileBlack}, strings.Repeat(" ", n)) } return left + space(leftPad) + mid + space(rightPad) + right } func renderCashHunt(s *casino.CashHuntSnapshot, mode string, unicode bool, rtp string) string { paint := func(text string, fg int) string { return color.Render(mode, color.ColorSpec{Fg: fg, Bg: -1}, text) } title := paint("CASH HUNT / "+strings.ToUpper(s.Risk)+" / 98% RTP", 220) switch s.Phase { case "hidden": side, topLeft, topRight, bottomLeft, bottomRight, bar := "|", "+", "+", "+", "+", "-" if unicode { side, topLeft, topRight, bottomLeft, bottomRight, bar = "│", "╭", "╮", "╰", "╯", "─" } inner := cashHuntBoardWidth - 2 lines := []string{topLeft + strings.Repeat(bar, inner) + topRight} for _, text := range []string{"* THE VAULT WHIRLS ... THE PRIZES VANISH! *", "All 100 positions have been hidden and mixed.", "The prizes are sealed. Your choice unlocks one.", "Choose your square: type 1-100, or pick N."} { padding := inner - len(text) lines = append(lines, side+strings.Repeat(" ", padding/2)+text+strings.Repeat(" ", padding-padding/2)+side) } lines = append(lines, bottomLeft+strings.Repeat(bar, inner)+bottomRight) for i, line := range lines { lines[i] = color.Render(mode, color.ColorSpec{Fg: -1, Bg: -1, Gradient: cashHuntGradient}, line) } return strings.Join(lines, "\n") case "paytable": lines := []string{title, "Prize Chance per square"} for _, prize := range s.Odds { lines = append(lines, paint(fmt.Sprintf("%6dx %10.6f%%", prize.Multiplier, float64(prize.Weight)/1_000_000), cashHuntPrizeColor(prize.Multiplier))) } return strings.Join(append(lines, "Returns include stake. Each board's mix varies; jackpots are not guaranteed."), "\n") case "preview", "result": // The subtitle carries the run state; the boxed header and grid below it // form the board itself. lines := []string{} top := 0 for _, value := range s.Squares { top = max(top, value) } if s.Phase == "preview" { lines = append(lines, paint("PRIZE PREVIEW / positions will be mixed", cashHuntPrizeColor(top))) } else { lines = append(lines, paint(fmt.Sprintf("YOUR PICK: %d / %dx", s.Selected, s.Squares[s.Selected-1]), 220)) } tl, tr := "┌", "┐" tm := "┬" ml, mm, mr := "├", "┼", "┤" bl, bm, br := "└", "┴", "┘" hz, vt := "─", "│" if !unicode { tl, tr = "+", "+" tm = "+" ml, mm, mr = "+", "+", "+" bl, bm, br = "+", "+", "+" hz, vt = "-", "|" } border := func(left, mid, right string) string { var b strings.Builder b.WriteString(left) for col := 0; col < cashHuntColumns; col++ { b.WriteString(strings.Repeat(hz, cashHuntTileWidth)) if col < cashHuntColumns-1 { b.WriteString(mid) } } b.WriteString(right) return b.String() } // Every grid character carries the board's black background, and a // foreground a shade darker than the numbers, so the panel reads as one // piece. box := func(s string) string { return color.Render(mode, color.ColorSpec{Fg: cashHuntBoxFg, Bg: cashHuntTileBlack}, s) } rows := casino.CashHuntSquares / cashHuntColumns lines = append(lines, box(tl+strings.Repeat(hz, cashHuntBoardWidth-2)+tr), box(vt)+cashHuntHeader(s, mode, rtp, top)+box(vt), // The header above is one merged cell, so the grid's top border uses // down-T junctions (no vertical line rises into the header). box(border(ml, tm, mr)), ) for row := 0; row < rows; row++ { numbers := make([]string, cashHuntColumns) prizes := make([]string, cashHuntColumns) for col := 0; col < cashHuntColumns; col++ { k := row*cashHuntColumns + col bg := cashHuntTileBlack number := cashHuntCenter(fmt.Sprintf("%d", k+1)) value := cashHuntCenter(fmt.Sprintf("%dx", s.Squares[k])) nf, vf := 244, cashHuntPrizeColor(s.Squares[k]) selected := s.Selected == k+1 if selected { number = cashHuntCenter(fmt.Sprintf(">%d<", k+1)) nf, vf, bg = 16, 16, 226 } numbers[col] = color.Render(mode, color.ColorSpec{Fg: nf, Bg: bg, Bold: selected}, number) // Zero is not a prize: leave 0x unbolded so only real multipliers pop. prizes[col] = color.Render(mode, color.ColorSpec{Fg: vf, Bg: bg, Bold: s.Squares[k] > 0}, value) } lines = append(lines, box(vt)+strings.Join(numbers, box(vt))+box(vt)) lines = append(lines, box(vt)+strings.Join(prizes, box(vt))+box(vt)) if row < rows-1 { lines = append(lines, box(border(ml, mm, mr))) } } lines = append(lines, box(border(bl, bm, br))) return strings.Join(lines, "\n") default: return title + "\n" + paint("100 squares. One choice. Prizes from 0x to 50000x.", 81) } }