aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/roulette.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/casino/roulette.go')
-rw-r--r--internal/casino/roulette.go77
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