Spin lifecycle
Phase-by-phase walkthrough of a round, with the sequence diagram as the anchor.
Phase-by-phase
idle
The resting state. UIStore.spinning = false, the spin button is enabled,
the stop button is disabled. HUDPresenter's click handler on #spin
is live, waiting for user input.
Enter: sets UI flags.
Exit: nothing — idle holds no resources.
spin
Three things happen in enter, in this order:
BalanceStore.debitBet()— optimistic UI update, not authoritative.DataStore.clear()— forget the previous round.-
reels.startSpin()andnetwork.spin({ bet })launch in parallel viaPromise.all. The reels start rolling immediately; the network promise is awaited.
When network.spin resolves, the response is written to
DataStore and the FSM transitions to stopSpin.
Timing note: the reels usually haven't finished their wind-up
by the time the response arrives — which is fine. The reels keep rolling; the
stopSpin phase is what lands them.
stopSpin
Reads DataStore.grid and DataStore.teasingReels.
If the server directed teasers, they're passed to the engine first
(reels.setAnticipation(teasers)) so the specified reels slow
down before landing. Then reels.stopWithResult(grid) — this
is where pixi-reels lands each reel on its target symbols in
staggered sequence.
skip() calls reels.forceStop(), which jumps every
reel to its final position immediately. Used by Turbo / SuperTurbo speed
and by slam-stop. No state mutates — skipping is purely visual.
winShow
Reads DataStore.winlines and totalWin.
- If there are no winlines, transitions to
idleimmediately. -
Otherwise:
reels.showWin(winlines)(spotlight + per-symbol win animation),BalanceStore.credit(totalWin), thenctx.ticker.schedule(WIN_HOLD_MS, () => transition('idle'))to pause on the win for 1.5 seconds before returning to idle.
skip() cancels the hold and transitions immediately. exit()
disposes the scheduled ticker handle — important if the FSM is torn down
mid-phase (scene change, page unload).
Key invariants
- Exactly one phase is in at a time.
fsm.phasereturns its name. - Transitions are explicit:
await fsm.transition('next'). No flag-based branching. - Phases don't call each other. Only the FSM transitions.
- The FSM is the only writer to stores. Presenters and views read.
Adding a phase
See Add a phase for the end-to-end steps.
Nested FSMs (bonus games)
Bonus games — "pick three chests", mini-slot, wheel-spin — nest as an inner FSM
inside a parent phase. Same Phase interface, same rules, own lifecycle.
See Add a bonus game.