aboutsummaryrefslogtreecommitdiff
path: root/internal/game/render_roulette.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/render_roulette.go')
-rw-r--r--internal/game/render_roulette.go211
1 files changed, 136 insertions, 75 deletions
diff --git a/internal/game/render_roulette.go b/internal/game/render_roulette.go
index 4861939..cfded31 100644
--- a/internal/game/render_roulette.go
+++ b/internal/game/render_roulette.go
@@ -2,6 +2,8 @@ package game
import (
"fmt"
+ "math"
+ "sort"
"strconv"
"strings"
@@ -192,54 +194,16 @@ func renderRouletteLayout(double bool, mode string, unicode bool) string {
return strings.Join(lines, "\n")
}
-// rouletteWheelSlots are the pocket anchor points (clockwise from the top) for
-// the single-zero ring. The double-zero ring inserts one extra slot at the
-// bottom centre. This hand-tuned geometry is what keeps a full 37/38-pocket
-// circle readable in a monospace terminal.
-var rouletteWheelSlots = [][2]int{
- {25, 0}, {33, 0}, {41, 0}, {46, 1}, {50, 2}, {53, 3}, {57, 4}, {59, 6}, {59, 7},
- {60, 9}, {61, 11}, {60, 13}, {58, 15}, {58, 16}, {56, 18}, {54, 19}, {50, 20},
- {46, 21}, {41, 22}, {33, 22}, {25, 22}, {20, 21}, {17, 20}, {13, 19}, {9, 18},
- {7, 17}, {5, 16}, {4, 14}, {3, 12}, {3, 10}, {4, 8}, {7, 6}, {9, 5}, {12, 4},
- {14, 3}, {17, 2}, {21, 1},
-}
-
-func rouletteWheelSlotsFor(double bool) [][2]int {
- slots := append([][2]int(nil), rouletteWheelSlots...)
- if !double {
- return slots
- }
- insert := 20 // between {33,22} and {25,22}
- slots = append(slots, [2]int{})
- copy(slots[insert+1:], slots[insert:])
- slots[insert] = [2]int{29, 22}
- return slots
-}
-
-// roulettePocketToken renders one three-column pocket. With color it is just
-// the number; without color a trailing letter keeps red/black/green distinct.
-func roulettePocketToken(value, label, mode string) string {
- if mode == "none" || mode == "" {
- switch len(value) {
- case 1:
- return value + label[:1] + " "
- case 2:
- return value + label[:1]
- default:
- return value
- }
- }
- return rouletteCenter(value, 3)
-}
-
type rouletteCell struct {
r rune
bg int
}
-// renderRouletteWheel draws a large circular wheel. The pocket count carries
-// the real zero count (one green for single zero, two for American), and the
-// hub shows the ball during the spin.
+// renderRouletteWheel draws a representative spinning wheel. There are no
+// numbers: only the rim carries solid red and black pockets, alternating like
+// a real wheel, with one green zero (plus its diametrically opposite pocket on
+// American wheels). A simple plus marks the croupier's spindle. Without color
+// the pockets fall back to R/B/G so the wheel still reads.
func renderRouletteWheel(wheel []string, mode string, unicode bool) string {
double := false
for _, p := range wheel {
@@ -247,14 +211,13 @@ func renderRouletteWheel(wheel []string, mode string, unicode bool) string {
double = true
}
}
- slots := rouletteWheelSlotsFor(double)
- if len(wheel) != len(slots) {
- slots = rouletteWheelSlots
- if len(wheel) > len(slots) {
- wheel = wheel[:len(slots)]
- }
- }
- const width, height = 68, 23
+ const width, height = 17, 9
+ // Half-widths are hand-rounded so the 17x9 grid reads as a circle once the
+ // terminal's roughly 2:1 character cells are taken into account. The top
+ // and bottom rows stay wide instead of collapsing to a single cell.
+ halfWidths := [height]int{2, 5, 7, 8, 8, 8, 7, 5, 2}
+ cx, cy := float64(width-1)/2, float64(height-1)/2
+ rx, ry := cx, cy
grid := make([][]rouletteCell, height)
for y := range grid {
grid[y] = make([]rouletteCell, width)
@@ -262,27 +225,114 @@ func renderRouletteWheel(wheel []string, mode string, unicode bool) string {
grid[y][x] = rouletteCell{' ', -1}
}
}
- place := func(x, y int, s string, bg int) {
- for k, r := range []rune(s) {
- px := x + k
- if px >= 0 && px < width && y >= 0 && y < height {
- grid[y][px] = rouletteCell{r, bg}
+ plain := mode == "none" || mode == ""
+ pocketRune := func(bg int) rune {
+ if !plain {
+ return ' '
+ }
+ switch bg {
+ case 1:
+ return 'R'
+ case 2:
+ return 'G'
+ default:
+ return 'B'
+ }
+ }
+ // Collect the rim cells in angular order so the pockets can alternate
+ // around the circumference instead of filling the disc with wedges.
+ type rimCell struct {
+ x, y int
+ ang float64
+ }
+ var rim []rimCell
+ for y := 0; y < height; y++ {
+ hw := halfWidths[y]
+ for x := int(cx) - hw; x <= int(cx)+hw; x++ {
+ nx := (float64(x) - cx) / rx
+ ny := (float64(y) - cy) / ry
+ if nx*nx+ny*ny >= 0.75 {
+ rim = append(rim, rimCell{x, y, math.Atan2(ny, nx)})
}
}
}
- for i, value := range wheel {
- if i >= len(slots) {
+ sort.Slice(rim, func(i, j int) bool {
+ if rim[i].ang != rim[j].ang {
+ return rim[i].ang < rim[j].ang
+ }
+ return rim[i].x < rim[j].x
+ })
+ // The top and bottom rows hold an odd number of cells, so anchor the walk
+ // there and leave those centre pockets one wide while every other pocket
+ // takes two adjacent rim cells.
+ topIdx, botIdx := -1, -1
+ for i, c := range rim {
+ switch {
+ case c.x == int(cx) && c.y == 0:
+ topIdx = i
+ case c.x == int(cx) && c.y == height-1:
+ botIdx = i
+ }
+ }
+ pocketIndex := make([]int, len(rim))
+ cur := 0
+ pocketIndex[topIdx] = cur
+ for i := (topIdx + 1) % len(rim); i != topIdx; {
+ cur++
+ pocketIndex[i] = cur
+ if i == botIdx {
+ i = (i + 1) % len(rim)
+ continue
+ }
+ i = (i + 1) % len(rim)
+ pocketIndex[i] = cur
+ i = (i + 1) % len(rim)
+ }
+ pockets := cur + 1
+ greenPocket := 0
+ for i, pocket := range wheel {
+ if pocket == "0" && len(wheel) > 0 {
+ greenPocket = i * pockets / len(wheel)
break
}
- bg, label := roulettePocketColor(value)
- place(slots[i][0]-1, slots[i][1], roulettePocketToken(value, label, mode), bg)
}
- hub := []string{"\\ | /", " \\ | / ", "--( o )--", " / | \\ ", "/ | \\"}
+ opposite := (greenPocket + pockets/2) % pockets
+ for i, c := range rim {
+ p := pocketIndex[i]
+ bg := 1
+ if p%2 == 1 {
+ bg = 0
+ }
+ if p == greenPocket || (double && p == opposite) {
+ bg = 2
+ }
+ grid[c.y][c.x] = rouletteCell{pocketRune(bg), bg}
+ }
+ // The croupier spins the wheel by its central spindle: a simple plus.
+ plus := []string{
+ " | ",
+ " | ",
+ "---+---",
+ " | ",
+ " | ",
+ }
if unicode {
- hub = []string{"╲ │ ╱", " ╲ │ ╱ ", "──( ● )──", " ╱ │ ╲ ", "╱ │ ╲"}
+ plus = []string{
+ " │ ",
+ " │ ",
+ "───┼───",
+ " │ ",
+ " │ ",
+ }
}
- for k, text := range hub {
- place(29, 9+k, text, -1)
+ baseX, baseY := int(cx)-len([]rune(plus[0]))/2, int(cy)-len(plus)/2
+ for dy, row := range plus {
+ for dx, r := range []rune(row) {
+ if r == ' ' {
+ continue
+ }
+ grid[baseY+dy][baseX+dx] = rouletteCell{r, -1}
+ }
}
lines := make([]string, 0, height)
for _, row := range grid {
@@ -293,7 +343,7 @@ func renderRouletteWheel(wheel []string, mode string, unicode bool) string {
func renderRouletteRow(row []rouletteCell, mode string) string {
last := len(row)
- for last > 0 && row[last-1].r == ' ' {
+ for last > 0 && row[last-1].r == ' ' && row[last-1].bg < 0 {
last--
}
if last == 0 {
@@ -320,8 +370,10 @@ func renderRouletteRow(row []rouletteCell, mode string) string {
}
// renderRouletteResult draws the winning pocket and its two physical
-// neighbours at playing-card size. The ball marks the middle pocket; there is
-// no surrounding prose.
+// neighbours at playing-card size. The pocket number sits on the top row, a
+// rule separates it from the ball (one row above centre), and the color is
+// spelled out on the bottom row only when color is off. There is no
+// surrounding prose.
func renderRouletteResult(pockets [3]string, mode string, unicode bool, size int) string {
size = cardRenderSize(unicode, size)
if size < 5 {
@@ -333,28 +385,37 @@ func renderRouletteResult(pockets [3]string, mode string, unicode bool, size int
if unicode {
ball = "●"
}
+ // The rule needs a spare row, so the smallest cards put the ball directly
+ // under the number instead.
+ const numberRow, ruleRow = 1, 2
+ ballRow := size / 2
+ hasRule := ballRow > ruleRow
+ colorRow := size - 2
+ plain := mode == "none" || mode == ""
var lines []string
for row := 0; row < size; row++ {
var cells []string
for i, pocket := range pockets {
bg, name := roulettePocketColor(pocket)
var text string
- switch row {
- case 0:
+ switch {
+ case row == 0:
text = b.tl + strings.Repeat(b.h, width-2) + b.tr
- case size - 1:
+ case row == size-1:
text = b.bl + strings.Repeat(b.h, width-2) + b.br
+ case hasRule && row == ruleRow:
+ text = b.teeLeft + strings.Repeat(b.h, width-2) + b.teeRight
default:
content := ""
- if row == 1 {
- content = name
- }
- if row == size/2 {
+ if row == numberRow {
content = pocket
}
- if i == 1 && row == size/2+1 {
+ if i == 1 && row == ballRow {
content = ball
}
+ if plain && row == colorRow {
+ content = name
+ }
text = b.v + rouletteCenter(content, width-2) + b.v
}
cells = append(cells, roulettePaint(mode, text, bg))