diff options
Diffstat (limited to 'internal/game/render_roulette.go')
| -rw-r--r-- | internal/game/render_roulette.go | 211 |
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)) |
