package game import ( "fmt" "strings" "thehouseoficarus/internal/casino" "thehouseoficarus/internal/color" ) // Crazy Time inks: one bright colour per betting symbol, so the wheel, // paytable and bonus boards all read consistently. func crazyTimeInk(label string) int { switch label { case "1": return 46 case "2": return 51 case "5": return 39 case "10": return 201 case "COIN FLIP": return 220 case "CASH HUNT": return 129 case "PACHINKO": return 208 case "CRAZY TIME": return 196 } return 15 } // crazyTimeShortLabel fits a symbol into a wheel cell. func crazyTimeShortLabel(label string) string { switch label { case "COIN FLIP": return "COIN" case "CASH HUNT": return "CASH" case "PACHINKO": return "PACH" case "CRAZY TIME": return "CRAZY" } return label } func crazyTimeRender(mode string, spec color.ColorSpec, text string) string { return color.Render(mode, spec, text) } // renderCrazyTimeWheel draws the main 54-pocket wheel as a bordered strip with // a single pointer. The centre pocket is the landed symbol on a result. func renderCrazyTimeWheel(s *casino.CrazyTimeSnapshot, mode string, unicode bool) string { width := 6 n := len(s.Pockets) if n == 0 { return "" } b := rouletteBorders(unicode) result := s.Phase == "result" if result { width = 7 } center := n / 2 pointer := 1 + center*(width+1) + width/2 dim := func(text string) string { return crazyTimeRender(mode, color.ColorSpec{Fg: 240}, text) } var out strings.Builder if result && s.TopSymbol != "" { top := crazyTimeRender(mode, color.ColorSpec{Fg: 244}, "TOP SLOT ") + crazyTimeRender(mode, color.ColorSpec{Fg: crazyTimeInk(s.TopSymbol), Bold: true}, s.TopSymbol) if s.TopMult > 1 { top += crazyTimeRender(mode, color.ColorSpec{Fg: 226, Bold: true}, fmt.Sprintf(" x%d", s.TopMult)) } else { top += crazyTimeRender(mode, color.ColorSpec{Fg: 244}, " x1") } if s.TopSymbol == s.Pockets[center] && result { top += crazyTimeRender(mode, color.ColorSpec{Fg: 46, Bold: true}, " HIT!") } out.WriteString(top) out.WriteByte('\n') } tip := "\\/" if unicode { tip = "▼" } out.WriteString(strings.Repeat(" ", pointer) + crazyTimeRender(mode, color.ColorSpec{Fg: 226, Bold: true}, tip) + "\n") top := b.tl + strings.Repeat(strings.Repeat(b.h, width)+b.teeTop, n-1) + strings.Repeat(b.h, width) + b.tr bottom := b.bl + strings.Repeat(strings.Repeat(b.h, width)+b.teeBottom, n-1) + strings.Repeat(b.h, width) + b.br out.WriteString(dim(top) + "\n") out.WriteString(dim(b.v)) for i, pocket := range s.Pockets { label := crazyTimeShortLabel(pocket) cell := rouletteCenter(label, width) spec := color.ColorSpec{Fg: crazyTimeInk(pocket), Bold: true} if result && i != center { spec = color.ColorSpec{Fg: crazyTimeInk(pocket), Dim: true} } out.WriteString(crazyTimeRender(mode, spec, cell)) out.WriteString(dim(b.v)) } out.WriteByte('\n') out.WriteString(dim(bottom)) if result { out.WriteString("\n" + strings.Repeat(" ", pointer-3) + crazyTimeRender(mode, color.ColorSpec{Fg: crazyTimeInk(s.Pockets[center]), Bold: true}, "WINNER")) } return out.String() } // crazyTimeFeltPanel draws one betting panel: a bordered box with a symbol // label and a short payout line. func crazyTimeFeltPanel(label, sub string, width, ink int, mode string, unicode bool) []string { b := rouletteBorders(unicode) lines := []string{ b.tl + strings.Repeat(b.h, width) + b.tr, b.v + rouletteCenter(label, width) + b.v, b.v + rouletteCenter(sub, width) + b.v, b.bl + strings.Repeat(b.h, width) + b.br, } for i := range lines { lines[i] = crazyTimeRender(mode, color.ColorSpec{Fg: ink, Bold: true}, lines[i]) } return lines } // crazyTimeFeltRow prints a row of equal-height panels side by side. func crazyTimeFeltRow(out *strings.Builder, panels [][]string) { for line := range panels[0] { out.WriteString(" ") for col, panel := range panels { if col > 0 { out.WriteByte(' ') } out.WriteString(panel[line]) } out.WriteByte('\n') } } // renderCrazyTimeFelt is the table command's felt: every available bet drawn // as a panel, with ALL sitting between the four minigame squares. func renderCrazyTimeFelt(s *casino.CrazyTimeSnapshot, mode string, unicode, showOdds bool, precision int) string { payout := map[string]string{} for _, sp := range s.Spots { payout[sp.Label] = sp.Payout } var out strings.Builder out.WriteString(crazyTimeRender(mode, color.ColorSpec{Fg: 253}, "A giant 54 pocket wheel! Bets on numbers have odds-based payouts. Bet on minigames to play those games when they are spun!")) out.WriteString("\n\n") // The four number bets, in ascending order. const numWidth, bonusWidth = 14, 29 var numbers [][]string for _, label := range []string{"1", "2", "5", "10"} { sub := "pays " + payout[label] numbers = append(numbers, crazyTimeFeltPanel(label, sub, numWidth, crazyTimeInk(label), mode, unicode)) } crazyTimeFeltRow(&out, numbers) // The four minigames in a 2x2 grid with ALL in the middle. coin := crazyTimeFeltPanel("COIN FLIP", "bonus", bonusWidth, crazyTimeInk("COIN FLIP"), mode, unicode) cash := crazyTimeFeltPanel("CASH HUNT", "bonus", bonusWidth, crazyTimeInk("CASH HUNT"), mode, unicode) pach := crazyTimeFeltPanel("PACHINKO", "bonus", bonusWidth, crazyTimeInk("PACHINKO"), mode, unicode) crazy := crazyTimeFeltPanel("CRAZY TIME", "bonus", bonusWidth, crazyTimeInk("CRAZY TIME"), mode, unicode) out.WriteByte('\n') crazyTimeFeltRow(&out, [][]string{coin, cash}) out.WriteByte('\n') all := crazyTimeFeltPanel("ALL", "all four", 11, 226, mode, unicode) // Center the small ALL box under the 2x2 minigame block: two bordered // panels, one gap between them, and the leading two-space row indent. rowWidth := 2*(bonusWidth+2) + 1 indent := 2 + (rowWidth-13)/2 for _, line := range all { out.WriteString(strings.Repeat(" ", indent) + line + "\n") } out.WriteByte('\n') crazyTimeFeltRow(&out, [][]string{pach, crazy}) rtp := "98%" if showOdds && s.RTP > 0 { rtp = fmt.Sprintf("%.*f%%", precision, s.RTP) } out.WriteString("\n") out.WriteString(crazyTimeRender(mode, color.ColorSpec{Fg: 244}, "Every target returns "+rtp+" including the Top Slot multiplier.")) return out.String() } // ----- Bonus renderers ----- func renderCrazyBonus(v *casino.CrazyTimeBonusView, mode string, unicode bool) string { if v == nil { return "" } switch v.Kind { case casino.CrazyTimeBonusCoinFlip: return renderCrazyCoinFlip(v, mode, unicode) case casino.CrazyTimeBonusCashHunt: return renderCrazyCashHunt(v, mode, unicode) case casino.CrazyTimeBonusPachinko: return renderCrazyPachinko(v, mode, unicode) case casino.CrazyTimeBonusWheel: return renderCrazyBonusWheel(v, mode, unicode) } return "" } // renderCrazyCoinFlip shows the two weighted faces and the flipping coin. It // reuses the standalone cabinet's coin art. func renderCrazyCoinFlip(v *casino.CrazyTimeBonusView, mode string, unicode bool) string { b := rouletteBorders(unicode) dim := func(text string) string { return crazyTimeRender(mode, color.ColorSpec{Fg: 245}, text) } cell := func(label string, value int, ink int) string { text := fmt.Sprintf("%s %dx", label, value) return crazyTimeRender(mode, color.ColorSpec{Fg: ink, Bold: true}, rouletteCenter(text, 18)) } switch v.Phase { case "faces": top := dim(b.tl + strings.Repeat(b.h, 18) + b.teeTop + strings.Repeat(b.h, 18) + b.tr) mid := dim(b.v) + cell("RED", v.CoinLow, 196) + dim(b.v) + cell("BLUE", v.CoinHigh, 45) + dim(b.v) bot := dim(b.bl + strings.Repeat(b.h, 18) + b.teeBottom + strings.Repeat(b.h, 18) + b.br) return crazyTimeRender(mode, color.ColorSpec{Fg: 226, Bold: true}, "COIN FLIP — two prizes, one fair coin") + "\n" + top + "\n" + mid + "\n" + bot case "flip": value, side := v.CoinLow, casino.CoinFlipSideRed if v.CoinFlipAlt { value, side = v.CoinHigh, casino.CoinFlipSideBlue } lines := coinFlipArt(value, side, false, v.CoinFlipAlt, mode, unicode) return strings.Join(lines, "\n") case "result": label := "RED" ink := 196 if v.CoinSide == "blue" { label, ink = "BLUE", 45 } art := coinFlipArt(v.CoinValue, v.CoinSide, true, false, mode, unicode) head := crazyTimeRender(mode, color.ColorSpec{Fg: 226, Bold: true}, fmt.Sprintf("COIN FLIP — %s %dx", label, v.CoinValue)) return head + "\n" + strings.Join(art, "\n") + "\n" + crazyTimeRender(mode, color.ColorSpec{Fg: ink, Bold: true}, "WIN") } return "" } // renderCrazyCashHunt draws the 108-tile board: a preview with values, a // sealed board, then the final board with the chosen tile highlighted. func renderCrazyCashHunt(v *casino.CrazyTimeBonusView, mode string, unicode bool) string { const cols = 10 const tile = 6 boardWidth := cols*(tile+1) + 1 title := crazyTimeRender(mode, color.ColorSpec{Fg: 220, Bold: true}, "CASH HUNT — 108 positive tiles") if v.Phase == "hidden" { return title + "\n" + crazyTimeRender(mode, color.ColorSpec{Fg: 245}, "The board is sealed and its positions are mixed...") } squares := v.HuntPreview if v.Phase != "preview" { squares = v.HuntFinal } if len(squares) == 0 { return title } tl, tr, _, ml, mm, mr, bl, bm, br, hz, vt := "┌", "┐", "┬", "├", "┼", "┤", "└", "┴", "┘", "─", "│" if !unicode { tl, tr, _, ml, mm, mr, bl, bm, br, hz, vt = "+", "+", "+", "+", "+", "+", "+", "+", "+", "-", "|" } border := func(left, mid, right string) string { var sb strings.Builder sb.WriteString(left) for c := 0; c < cols; c++ { sb.WriteString(strings.Repeat(hz, tile)) if c < cols-1 { sb.WriteString(mid) } } sb.WriteString(right) return sb.String() } box := func(s string) string { return crazyTimeRender(mode, color.ColorSpec{Fg: 240, Bg: 16}, s) } cellText := func(text string) string { if len(text) >= tile { return text } left := (tile - len(text)) / 2 return strings.Repeat(" ", left) + text + strings.Repeat(" ", tile-len(text)-left) } lines := []string{title, box(tl + strings.Repeat(hz, boardWidth-2) + tr)} rows := (len(squares) + cols - 1) / cols for row := 0; row < rows; row++ { var cells []string for c := 0; c < cols; c++ { k := row*cols + c if k >= len(squares) { cells = append(cells, strings.Repeat(" ", tile)) continue } text := fmt.Sprintf("%dx", squares[k]) fg := cashHuntPrizeColor(squares[k]) bg := 16 bold := squares[k] > 0 if v.Phase == "result" && v.HuntPick == k { text = fmt.Sprintf(">%d<", squares[k]) fg, bg, bold = 16, 226, true } cells = append(cells, crazyTimeRender(mode, color.ColorSpec{Fg: fg, Bg: bg, Bold: bold}, cellText(text))) } lines = append(lines, box(vt)+strings.Join(cells, box(vt))+box(vt)) if row < rows-1 { lines = append(lines, box(border(ml, mm, mr))) } } lines = append(lines, box(border(bl, bm, br))) if v.Phase == "result" { value := 0 if v.HuntPick >= 0 && v.HuntPick < len(v.HuntFinal) { value = v.HuntFinal[v.HuntPick] } lines = append(lines, crazyTimeRender(mode, color.ColorSpec{Fg: 46, Bold: true}, fmt.Sprintf("TILE %d UNLOCKED — %dx", v.HuntPick+1, value))) } return strings.Join(lines, "\n") } // ----- Pachinko bonus ----- // crazyPachinkoCell renders one 4-wide prize cell; landing >= 0 highlights it. func crazyPachinkoCell(value, landing, lane, doubling int, mode string) string { label := "DBL" fg := 198 if value >= 0 { label = fmt.Sprintf("%d", value) switch { case value >= 100: fg = 51 case value >= 50: fg = 220 case value >= 20: fg = 208 case value >= 10: fg = 46 default: fg = 49 } } bold := value < 0 || value >= 20 || (landing == lane && lane >= 0) if landing == lane && lane >= 0 { fg = 226 } if len(label) > 4 { label = label[:4] } text := fmt.Sprintf("%4s", label) return color.Render(mode, color.ColorSpec{Fg: fg, Bold: bold}, text) } // renderCrazyPachinko prints the multiplier row at the top, a short drop, then // the multiplier row again at the bottom with the landing marked. It never // prints the whole board twice. func renderCrazyPachinko(v *casino.CrazyTimeBonusView, mode string, unicode bool) string { if len(v.PachoPockets) == 0 { return "" } ball := "o" if unicode { ball = "●" } row := func(landing int) string { var sb strings.Builder for i, value := range v.PachoPockets { sb.WriteString(crazyPachinkoCell(value, landing, i, v.PachoDoubling, mode)) } return sb.String() } switch v.Phase { case "top": head := crazyTimeRender(mode, color.ColorSpec{Fg: 226, Bold: true}, "PACHINKO — prizes") if v.PachoDoubling > 0 { head += crazyTimeRender(mode, color.ColorSpec{Fg: 201, Bold: true}, fmt.Sprintf(" x%d", 1< 0 { head += crazyTimeRender(mode, color.ColorSpec{Fg: 201, Bold: true}, fmt.Sprintf(" after %d DOUBLE", v.PachoDoubling)) } return head + "\n" + row(v.PachoLane) } return "" } // ----- Crazy Time bonus wheel (three pointers) ----- func renderCrazyBonusWheel(v *casino.CrazyTimeBonusView, mode string, unicode bool) string { const width = 6 wheel := v.CTWheel n := len(wheel) if n == 0 { return "" } // The view window is a contiguous run starting at CTOffset. const cells = 11 pick := v.CTPick if pick < 0 || pick >= len(casino.CrazyBonusPointerCells) { pick = 0 } window := make([]int, cells) for i := 0; i < cells; i++ { window[i] = wheel[((v.CTOffset+i)%n+n)%n] } b := rouletteBorders(unicode) dim := func(text string) string { return crazyTimeRender(mode, color.ColorSpec{Fg: 240}, text) } tip := "\\/" if unicode { tip = "▼" } tipRunes := []rune(tip) var out strings.Builder // Pointer line: three arrows over their cells, the chosen one bright. for i := 0; i < 1+cells*(width+1); i++ { ch := ' ' ink, bold := 244, false for p, cell := range casino.CrazyBonusPointerCells { col := 1 + cell*(width+1) + width/2 if i >= col && i < col+len(tipRunes) { ch = tipRunes[i-col] if p == pick { ink, bold = 226, true } break } } if ch == ' ' { out.WriteByte(' ') } else { out.WriteString(crazyTimeRender(mode, color.ColorSpec{Fg: ink, Bold: bold}, string(ch))) } } out.WriteByte('\n') top := b.tl + strings.Repeat(strings.Repeat(b.h, width)+b.teeTop, cells-1) + strings.Repeat(b.h, width) + b.tr bottom := b.bl + strings.Repeat(strings.Repeat(b.h, width)+b.teeBottom, cells-1) + strings.Repeat(b.h, width) + b.br out.WriteString(dim(top) + "\n" + dim(b.v)) for i, value := range window { label := "x2" ink := 201 if value >= 0 { label = fmt.Sprintf("x%d", value) ink = 46 if value >= 100 { ink = 51 } else if value >= 50 { ink = 220 } else if value >= 25 { ink = 208 } } spec := color.ColorSpec{Fg: ink, Bold: true} // Highlight the chosen pointer's column. if i == casino.CrazyBonusPointerCells[pick] { spec = color.ColorSpec{Fg: 226, Bg: -1, Bold: true} if v.CTResult { spec.Bold = true } } out.WriteString(crazyTimeRender(mode, spec, rouletteCenter(label, width))) out.WriteString(dim(b.v)) } out.WriteByte('\n') out.WriteString(dim(bottom)) if v.CTResult { out.WriteString("\n" + crazyTimeRender(mode, color.ColorSpec{Fg: 226, Bold: true}, fmt.Sprintf("POINTER %d PAYS %dx", pick+1, v.CTValue))) } return out.String() } func renderCrazyTime(s *casino.CrazyTimeSnapshot, mode string, unicode bool, showOdds bool, precision int) string { if s == nil { return "" } if s.Bonus != nil { return renderCrazyBonus(s.Bonus, mode, unicode) } if len(s.Pockets) > 0 { return renderCrazyTimeWheel(s, mode, unicode) } return renderCrazyTimeFelt(s, mode, unicode, showOdds, precision) }