aboutsummaryrefslogtreecommitdiff
path: root/internal/game/render_crazy_time.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-10-10 04:08:09 -0400
committerhistoria <[not public]>2026-10-10 04:08:09 -0400
commit9fb1f9010526100e12dd1ab1b0ca98401bb6ed75 (patch)
tree4a128211bfb0dd429bbb559a591d6c4be25c7757 /internal/game/render_crazy_time.go
parentced77d7c89129da6ba4001ab8e3dad265aa41396 (diff)
downloadthehouseoficarus-main.tar.gz
feat: add casino game: crazy timeHEADmain
Diffstat (limited to 'internal/game/render_crazy_time.go')
-rw-r--r--internal/game/render_crazy_time.go487
1 files changed, 487 insertions, 0 deletions
diff --git a/internal/game/render_crazy_time.go b/internal/game/render_crazy_time.go
new file mode 100644
index 0000000..5a8b49d
--- /dev/null
+++ b/internal/game/render_crazy_time.go
@@ -0,0 +1,487 @@
+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<<v.PachoDoubling))
+ }
+ return head + "\n" + row(-1)
+ case "drop":
+ // A single compact drop line: the ball slides toward its slot.
+ return strings.Repeat(" ", v.PachoLane*4+1) + color.Render(mode, color.ColorSpec{Fg: 231, Bold: true}, ball)
+ case "result":
+ head := crazyTimeRender(mode, color.ColorSpec{Fg: 226, Bold: true}, "PACHINKO — landed")
+ if v.PachoDoubling > 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)
+}