SP slotplate
Architecture

Spin lifecycle

Phase-by-phase walkthrough of a round, with the sequence diagram as the anchor.

End-to-end sequence
HUD presenter FSM flow Balance store Reels presenter Network infrastructure Data store user clicks Spin transition('spin') debitBet() par [in parallel] startSpin() spin({ bet }) SpinResponse transition('stopSpin') stopWithResult(grid) allLanded transition('winShow') showWin(winlines) credit(totalWin) transition('idle') time →
Time flows top to bottom. Solid arrows = calls; dashed = returns.

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:

  1. BalanceStore.debitBet() — optimistic UI update, not authoritative.
  2. DataStore.clear() — forget the previous round.
  3. reels.startSpin() and network.spin({ bet }) launch in parallel via Promise.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 idle immediately.
  • Otherwise: reels.showWin(winlines) (spotlight + per-symbol win animation), BalanceStore.credit(totalWin), then ctx.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.phase returns 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.