diff options
Diffstat (limited to 'internal/casino/roulette.go')
| -rw-r--r-- | internal/casino/roulette.go | 77 |
1 files changed, 67 insertions, 10 deletions
diff --git a/internal/casino/roulette.go b/internal/casino/roulette.go index fa87800..783d22f 100644 --- a/internal/casino/roulette.go +++ b/internal/casino/roulette.go @@ -474,6 +474,7 @@ type rouletteTable struct { tableCore[*roulettePlayer] rng *rand.Rand spinRemaining int + winningIndex int // selected once at spin start, shared by final frame and settlement lastResult *RouletteSnapshot history []string } @@ -601,11 +602,7 @@ func (t *rouletteTable) bet(name, spot string, amount int, w Wallet) []Event { e := t.event(fmt.Sprintf("Roulette betting is open for %d ticks.", t.Config.BettingTicks), true) events = append(events, e) } else { - t.phase = phasePlaying - t.spinRemaining = t.Config.Roulette.SpinTicks - e := t.event("No more bets! The roulette wheel spins...", true) - e.Snapshot = t.view() - events = append(events, e) + events = append(events, t.startSpin()) } } return events @@ -618,6 +615,14 @@ func (t *rouletteTable) tick() []Event { if t.spinRemaining <= 0 { return t.settle() } + frame := t.Config.Roulette.SpinTicks - t.spinRemaining + if frame <= 5 { + // Cosmetic randomness must not consume the outcome RNG. + s := t.spinFrame(frame, rand.Intn(3) == 0, rand.Intn(10), 1+rand.Intn(36)) + e := t.event("", true) + e.Snapshot = s + return []Event{e} + } return nil } if t.phase != phaseBetting { @@ -636,15 +641,64 @@ func (t *rouletteTable) tick() []Event { t.resetRound() return nil } + return []Event{t.startSpin()} +} + +func (t *rouletteTable) startSpin() Event { t.phase = phasePlaying t.spinRemaining = t.Config.Roulette.SpinTicks - e := t.event("No more bets! The roulette wheel spins...", true) - e.Snapshot = t.view() - return []Event{e} + t.winningIndex = t.rng.Intn(len(rouletteWheel(t.Config.Roulette.DoubleZero))) + return t.event("No more bets! The roulette wheel spins...", true) } + +// spinFrame keeps cosmetic motion separate from the committed outcome. The +// final strip uses physical wheel neighbors, including American same-color +// neighbors around zero pockets, rather than inventing alternating colors. +func (t *rouletteTable) spinFrame(frame int, showBall bool, ball, green int) *RouletteSnapshot { + s := t.view() + s.SpinFrame, s.BallPosition = frame, -1 + if frame == 5 { + s.BallPosition = 4 + wheel := rouletteWheel(t.Config.Roulette.DoubleZero) + // Frame four starts black. Slide the final window to start red, + // keeping the ball and both adjacent pockets inside the strip. + for offset := 0; offset < 8; offset++ { + position := 1 + (3+offset)%8 + start := wheel[(t.winningIndex-position+len(wheel))%len(wheel)] + n, _ := strconv.Atoi(start) + if rouletteRed(n) { + s.BallPosition = position + break + } + } + for i := range s.SpinPockets { + s.SpinPockets[i] = wheel[(t.winningIndex+i-s.BallPosition+len(wheel))%len(wheel)] + } + return s + } + if showBall { + s.BallPosition = ball + } + parity := frame + for i := range s.SpinPockets { + // Insert green between alternating neighbors, never replace one of + // them (which would leave equal colors on either side). + if i == green && i > 0 && i < len(s.SpinPockets)-1 { + s.SpinPockets[i] = "0" + continue + } + s.SpinPockets[i] = "1" // red + if parity%2 == 0 { + s.SpinPockets[i] = "2" // black + } + parity++ + } + return s +} + func (t *rouletteTable) settle() []Event { wheel := rouletteWheel(t.Config.Roulette.DoubleZero) - index := t.rng.Intn(len(wheel)) + index := t.winningIndex pocket := wheel[index] t.history = append(t.history, pocket) if len(t.history) > rouletteHistoryLimit { @@ -786,7 +840,10 @@ func (t *rouletteTable) refundAll() []Event { // RouletteSnapshot describes the view without coupling the engine to terminal // colors, Unicode preferences, or card-art settings. type RouletteSnapshot struct { - DoubleZero bool + DoubleZero bool + SpinFrame int // 1–5 for animation events; zero for ordinary snapshots + BallPosition int // -1 when the ball is not visible + SpinPockets [10]string // cosmetic pockets, or actual wheel order on the final frame // Racetrack reports that the table also offers the French call bets and // neighbours bets, so the renderer draws the racetrack panel. Racetrack bool |
