diff options
Diffstat (limited to 'internal/game/render_crazy_time.go')
| -rw-r--r-- | internal/game/render_crazy_time.go | 487 |
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) +} |
