if/else guards across the codebase, all bet-flow transitions are encoded in one place and every component can query the current state through a small set of derived functions.
Why a State Machine?
Betting logic involves many interlocking conditions: is a bet already in flight? Is auto-bet running? Is there an unfinished round to resume? Without a state machine, these checks accumulate into fragileif/else chains. XState makes valid transitions explicit and prevents impossible states (e.g. starting a new bet while one is already running).
The Game States
The machine is created inpackages/utils-xstate/src/createGameActor.ts. It has five states:
rendering
rendering
The initial state. The game stays here while the loading screen is visible and assets are being processed. Once the
RENDERED event is sent, the machine transitions to idle.idle
idle
The machine is waiting for user input. The bet button is enabled. From here the machine can transition to
bet (single bet), autoBet (auto-spin sequence), or resumeBet (if an unfinished round was detected at authenticate time).bet
bet
A single bet is in flight. Invokes the
bet intermediate machine as a child actor. Returns to idle when done.autoBet
autoBet
An auto-spin sequence is running. Invokes the
autoBet intermediate machine which counts down stateBet.autoSpinsCounter. Returns to idle when the counter reaches zero or is cancelled.resumeBet
resumeBet
An unfinished round from a previous session is being replayed. Invokes the
resumeBet intermediate machine. Returns to idle when done. This state is only entered on game load when stateBet.betToResume is set.Creating the Actor
createGameActor is a factory that wires the five states together with the intermediate machine actors:
apps/lines, the actor is created via the createXstate() factory:
Querying State with stateXstateDerived
createXstate() returns two objects:
stateXstate— Svelte$stateholding the raw XStateStateValuestateXstateDerived— plain functions that callmatchesState()from xstate
| Function | Returns true when |
|---|---|
isRendering() | The loading screen is active |
isIdle() | Ready and waiting for input |
isBetting() | A single bet is in progress |
isAutoBetting() | An auto-spin sequence is running |
isResumingBet() | A previous unfinished round is being resumed |
isPlaying() | Any bet state — !isRendering() && !isIdle() |
Using State in Components
AccessstateXstateDerived via context. Because it is built on Svelte $state, any component reading it will react to state changes:
BET_TYPE_METHODS_MAP and end-round Timing
The createPrimaryMachines factory in packages/utils-xstate/src/createPrimaryMachines.ts defines BET_TYPE_METHODS_MAP, which controls when the end-round API is called relative to game animations:
noWin
noWin
end-round is never called. The round closed itself with no payout.singleRoundWin
singleRoundWin
end-round is called immediately after the RGS bet response (newGame), before any animations run. The balance value is cached and only applied to the UI after animations complete (endGame). This allows the round to be resumed if the player refreshes mid-animation.bonusWin
bonusWin
end-round is called after all animations complete (endGame). This is required for multi-request rounds (e.g. free spins) where the round remains active until the player finishes the bonus game.Whether a bet can be resumed from the authenticate request is determined by when
end-round is called. If end-round has not been called, stateBet.betToResume.active will be true, and the machine will enter the resumeBet state on next load.