FSM & phases
Named phases, explicit transitions, one current state.
Why an FSM
A slot round is a finite sequence of states. It can be in exactly one at a
time. Anything that tracks game state with booleans (isSpinning,
canStop, awaitingResponse) ends up with four booleans,
then five, then six — and you can't prove which combinations are reachable.
A named FSM collapses those booleans to a single enum. The set of reachable states
is the set of registered phases. The set of transitions is readable by grepping for
fsm.transition. Review, test, debug — all easier.
The Phase contract
interface Phase {
readonly name: string;
enter(ctx: PhaseContext): void | Promise<void>;
skip?(ctx: PhaseContext): void;
exit?(ctx: PhaseContext): void;
} enter— the phase runs. Await work here; callctx.fsm.transition(...)to move on.skip— fast-forward. Called when the user slam-stops, or when Turbo skips a reveal.exit— dispose anything the phase allocated (tickers, listeners).
The context
Every phase receives the same PhaseContext:
interface PhaseContext {
fsm: FSM;
stores: RootStore;
ticker: Ticker;
network: NetworkManager;
reels: ReelsPresenter;
hud: HUDPresenter;
}
Phases don't reach for globals. They use ctx. This makes them trivially
testable — instantiate with mocks and assert what happens. No vi.mock gymnastics.
Adding a phase
- Create
src/flow/phases/MyPhase.ts. ImplementPhase. - Register in
src/composition.tswithfsm.register(new MyPhase()). - Add a transition to it from wherever in the existing flow should enter it.
- Add a test in
tests/flow/MyPhase.test.ts.
The slash command /add-phase MyPhase does all of this for you.
Nested FSMs
A bonus game runs its own FSM inside a single parent phase. The parent phase's
enter instantiates a new FSM, registers bonus phases
(bonusIntro, bonusRound, bonusReveal,
bonusOutro), runs it to completion, then calls ctx.fsm.transition
on the parent to move past the bonus.
Main-game state stays frozen during the bonus. No main-game phase runs until the parent phase exits.
What belongs here vs elsewhere
- In the phase: orchestration, timing, transitions.
- In a presenter: translating state changes into view calls.
- In a store: holding observable state.
- In infrastructure: I/O (network, assets, analytics).
- In the server: everything about what outcomes happen.