From ff9002b96e35ad0808945e7c35791460c9ca8776 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Tue, 29 Sep 2026 17:50:33 -0400 Subject: feat: roulette racetrack betting, rename casino to domita fortuna --- internal/casino/roulette.go | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) (limited to 'internal/casino/roulette.go') diff --git a/internal/casino/roulette.go b/internal/casino/roulette.go index cae9d0d..252ac4c 100644 --- a/internal/casino/roulette.go +++ b/internal/casino/roulette.go @@ -8,8 +8,11 @@ import ( ) // RouletteConfig selects European (single zero) or American (double zero) rules. +// Racetrack adds the French call bets and neighbours bets; it is a single-zero +// layout only. type RouletteConfig struct { DoubleZero bool `yaml:"double_zero,omitempty"` + Racetrack bool `yaml:"racetrack,omitempty"` SpinTicks int `yaml:"spin_ticks,omitempty"` } @@ -501,7 +504,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), Wheel: rouletteWheel(t.Config.Roulette.DoubleZero)} + s := &RouletteSnapshot{DoubleZero: t.Config.Roulette.DoubleZero, Racetrack: t.Config.Roulette.Racetrack, Phase: string(t.phase), Wheel: rouletteWheel(t.Config.Roulette.DoubleZero)} for _, p := range t.players.ordered() { s.Players = append(s.Players, p.Name) } @@ -559,15 +562,16 @@ func (t *rouletteTable) bet(name, spot string, amount int, w Wallet) []Event { spot = p.lastSpot } spot = normalizeRouletteSpot(spot) - _, mult := rouletteBet(spot, t.Config.Roulette.DoubleZero) - if mult == 0 { + resolved, ok := rouletteWagerFor(spot, t.Config.Roulette.DoubleZero, t.Config.Roulette.Racetrack) + if !ok { return []Event{t.private(name, "Unknown roulette target. Use table to see all available bets.")} } if amount < t.Config.MinBet || amount > t.Config.MaxBet { return []Event{t.private(name, "Roulette bets must be "+betRangeText(t.Config.MinBet, t.Config.MaxBet)+".")} } - // Check total returns before committing even an unlimited wager. - if _, err := MulPayout(amount, float64(mult)); err != nil { + // Check every possible winning return before committing even an unlimited + // wager: a call bet's components pay different multiples. + if err := resolved.validate(amount); err != nil { return []Event{t.private(name, "That wager's potential payout is too large.")} } wager, err := w.Wager(amount) @@ -633,15 +637,14 @@ func (t *rouletteTable) settle() []Event { if !p.HasBet { continue } - covered, mult := rouletteBet(p.lastSpot, t.Config.Roulette.DoubleZero) + wager, resolved := rouletteWagerFor(p.lastSpot, t.Config.Roulette.DoubleZero, t.Config.Roulette.Racetrack) payout := 0 result := "loss" var err error - for _, n := range covered { - if n == pocket { + if resolved { + if share, ok := wager.covers(pocket); ok { result = "win" - payout, err = MulPayout(p.Wager.Total, float64(mult)) - break + payout, err = share.payout(p.Wager.Total) } } // PayCredits supports a zero return and atomically consumes a losing @@ -746,9 +749,12 @@ func (t *rouletteTable) refundAll() []Event { // 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 + // Racetrack reports that the table also offers the French call bets and + // neighbours bets, so the renderer draws the racetrack panel. + Racetrack 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. -- cgit v1.2.3