diff options
| author | historia <[not public]> | 2026-09-29 14:15:06 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-29 14:15:06 -0400 |
| commit | 3db3071c444abb2133e1fcaf7e685095415c1adf (patch) | |
| tree | a1d3c276aa7db2e89ea92a5751a81c197a1e1103 /internal/casino | |
| parent | 9803facd4368420a5dce3af59c835e13cb015cf5 (diff) | |
| download | thehouseoficarus-3db3071c444abb2133e1fcaf7e685095415c1adf.tar.gz | |
feat: better roulette wheel design
Diffstat (limited to 'internal/casino')
| -rw-r--r-- | internal/casino/roulette.go | 61 | ||||
| -rw-r--r-- | internal/casino/roulette_test.go | 19 | ||||
| -rw-r--r-- | internal/casino/table_ux.go | 22 |
3 files changed, 36 insertions, 66 deletions
diff --git a/internal/casino/roulette.go b/internal/casino/roulette.go index be8966c..ed649e9 100644 --- a/internal/casino/roulette.go +++ b/internal/casino/roulette.go @@ -22,6 +22,10 @@ func rouletteWheel(double bool) []string { } return europeanWheel } + +// WheelOrder returns a copy of the pocket order (clockwise from the top) for +// presentation layers that draw the wheel themselves. +func WheelOrder(double bool) []string { return append([]string(nil), rouletteWheel(double)...) } func rouletteRed(n int) bool { switch n { case 1, 3, 5, 7, 9, 12, 14, 16, 18, 19, 21, 23, 25, 27, 30, 32, 34, 36: @@ -101,7 +105,7 @@ func newRouletteTable(room int, cfg TableConfig, rng *rand.Rand) *rouletteTable return &rouletteTable{tableCore: newTableCore[*roulettePlayer](&tableBase{RoomID: room, Config: cfg}), rng: rng} } func (t *rouletteTable) view() *RouletteSnapshot { - s := &RouletteSnapshot{DoubleZero: t.Config.Roulette.DoubleZero, Phase: string(t.phase)} + s := &RouletteSnapshot{DoubleZero: t.Config.Roulette.DoubleZero, Phase: string(t.phase), Wheel: rouletteWheel(t.Config.Roulette.DoubleZero)} for _, p := range t.players.ordered() { s.Players = append(s.Players, p.Name) } @@ -109,7 +113,7 @@ func (t *rouletteTable) view() *RouletteSnapshot { } func (t *rouletteTable) menuEvent(name string) Event { e := t.betMenuEvent(name, []MenuCommand{ - {Command: "bet <amount> on <target>", Desc: "One wager per spin; numbers pay 35:1, outside bets 1:1, dozens/columns 2:1"}, + {Command: "bet <amount> on <target>", Desc: "Place one wager per spin (help roulette)"}, {Command: "bet / rebet", Desc: "Repeat your previous wager"}, {Command: "table / sitout / stop", Desc: "View layout / sit out / leave after this spin"}, }) @@ -256,7 +260,9 @@ func (t *rouletteTable) settle() []Event { public = p.Name + "'s settlement is pending recovery." } event := t.playerAction(p.Name, public, text) - event.ShowBalance = !failed + // Returning players get their balance in the next-spin menu. Only + // departures/sit-outs need a balance attached to the settlement. + event.ShowBalance = !failed && (p.LeaveAfter || p.SitOutAfter || !p.Connected) if result == "win" && !failed { event.Color = "casino_big_win" } @@ -296,7 +302,11 @@ func (t *rouletteTable) rebet(name string) (string, int) { func (t *rouletteTable) status(name string) []Event { t.mu.Lock() defer t.mu.Unlock() - e := t.private(name, t.overviewLocked()+"\n"+t.seatStatusLocked(name)) + message := t.overviewLocked() + if text := t.seatStatusLocked(name); text != "" { + message += "\n" + text + } + e := t.private(name, message) e.Snapshot = t.view() events := []Event{e} if t.phase == phaseWaiting && t.lastResult != nil { @@ -335,48 +345,17 @@ func (t *rouletteTable) refundAll() []Event { return events } -// RouletteSnapshot keeps ASCII views available to spectators and players -// without coupling the engine to terminal color or card-art settings. +// RouletteSnapshot describes the view without coupling the engine to terminal +// colors, Unicode preferences, or card-art settings. type RouletteSnapshot struct { DoubleZero bool Phase string Pockets [3]string // physical wheel neighbors, not numerical neighbors Players []string + // Wheel is the pocket order clockwise from the top; the renderer colors + // and places each pocket. It carries the zero count (one or two) so the + // drawn wheel always matches the configured table. + Wheel []string } func (*RouletteSnapshot) snapshot() {} -func (s *RouletteSnapshot) ASCII() string { - if s.Phase == "result" { - return fmt.Sprintf("+-----+-----+-----+\n| %-3s | %-3s | %-3s |\n| | o | |\n+-----+-----+-----+\n WINNER", s.Pockets[0], s.Pockets[1], s.Pockets[2]) - } - if s.Phase == string(phasePlaying) { - bottom := " * " - if s.DoubleZero { - bottom = "00 " - } - return " .--[ 0 ]--.\n / * | * \\\n | * --+-- * |\n \\ * | * /\n '--[" + bottom + "]--'\n Spinning" - } - var b strings.Builder - b.WriteString("ROULETTE | R=red B=black G=green\n[0 G]") - if s.DoubleZero { - b.WriteString(" [00 G]") - } - b.WriteString("\n") - for row := 3; row >= 1; row-- { - b.WriteString("|") - for n := row; n <= 36; n += 3 { - color := "B" - if rouletteRed(n) { - color = "R" - } - fmt.Fprintf(&b, "%2d%s|", n, color) - } - fmt.Fprintf(&b, " column%d\n", row) - } - b.WriteString("| dozen1 | dozen2 | dozen3 |\n| low (1-18) | even | red | black | odd | high (19-36) |\nTargets: 0-36") - if s.DoubleZero { - b.WriteString(", 00") - } - b.WriteString(", red, black, odd, even, low, high,\n dozen1/2/3, column1/2/3\nExample: bet 10 on 17 (one wager per spin)") - return b.String() -} diff --git a/internal/casino/roulette_test.go b/internal/casino/roulette_test.go index 8defe1e..3ef3814 100644 --- a/internal/casino/roulette_test.go +++ b/internal/casino/roulette_test.go @@ -137,24 +137,7 @@ func TestRouletteRefundAndPersistenceFailure(t *testing.T) { t.Fatal("failed settlement not refunded once") } } -func TestRouletteViewsAndConfig(t *testing.T) { - for _, double := range []bool{false, true} { - s := &RouletteSnapshot{DoubleZero: double, Phase: "playing"} - wheel := s.ASCII() - if strings.Contains(wheel, "00") != double || strings.Count(wheel, "[ 0 ]") != 1 { - t.Fatal("wrong wheel zero pockets", wheel) - } - s.Phase = "betting" - layout := s.ASCII() - if !strings.Contains(layout, "column3") || !strings.Contains(layout, "36R") || strings.Contains(layout, "[00 G]") != double { - t.Fatal("bad layout", layout) - } - s.Phase = "result" - s.Pockets = [3]string{"26", "0", "32"} - if !strings.Contains(s.ASCII(), "| 26 | 0 | 32 |") { - t.Fatal(s.ASCII()) - } - } +func TestRouletteConfig(t *testing.T) { cfg := TableConfig{ID: "r", Game: GameRoulette} if err := cfg.Validate(); err != nil { t.Fatal(err) diff --git a/internal/casino/table_ux.go b/internal/casino/table_ux.go index 48ea46e..cec766d 100644 --- a/internal/casino/table_ux.go +++ b/internal/casino/table_ux.go @@ -13,8 +13,7 @@ func (c TableConfig) RulesText() string { case GameBaccarat: return c.BaccaratRulesText() case GameRoulette: - s := &RouletteSnapshot{DoubleZero: c.Roulette.DoubleZero, Phase: "betting"} - return c.Label() + "\n" + s.ASCII() + "\nStraight numbers pay 35:1; dozens/columns 2:1; other outside bets 1:1.\nZero pockets lose all outside bets. One wager per spin. Use help roulette." + return c.Label() + "\nStraight numbers pay 35:1; dozens/columns 2:1; other outside bets 1:1.\nZero pockets lose all outside bets. One wager per spin. Use help roulette." default: return c.Label() } @@ -47,16 +46,25 @@ func (c *tableCore[P]) overview() string { } func (c *tableCore[P]) overviewLocked() string { + // Concise enough to sit beside a look minimap without wrapping into it. + // The betting countdown lives in the menu heading and warning lines. state := "Betting open" switch c.phase { - case phaseBetting: - state = fmt.Sprintf("Betting closes in %d ticks", c.window.timer.remaining()) case phasePlaying: - state = "Round in progress" + state = "In progress" case phaseInsurance: - state = "Insurance decisions" + state = "Insurance" } - return fmt.Sprintf("%s | Limits %s | %d/%d seated | %s", c.Config.Label(), betRangeText(c.Config.MinBet, c.Config.MaxBet), c.players.count(), c.Config.MaxPlayers, state) + return fmt.Sprintf("%s | Limits %s | %d/%d seated | %s", c.Config.Label(), compactBetRange(c.Config.MinBet, c.Config.MaxBet), c.players.count(), c.Config.MaxPlayers, state) +} + +// compactBetRange renders the wager range in the short form used by the +// one-line table overview (the verbose form stays in the betting menu). +func compactBetRange(minBet, maxBet int) string { + if maxBet == maxBetUnlimited { + return fmt.Sprintf("%d+", minBet) + } + return fmt.Sprintf("%d-%d", minBet, maxBet) } func (c *tableCore[P]) warning(name, text string) Event { |
