SP slotplate
Architecture

Layered architecture

The stack with enforced import direction.

SLOTPLATE · CLIENT UI plain DOM · optional React/Vue scenes Pixi application lifecycle · mounts pixi-reels flow · FSM + phase handlers idle → spin → stopSpin → winShow → idle owns GAME TIME · only writer to stores presenters ReelsPresenter · HUDPresenter state → view infrastructure Ticker · Network · AssetLoader · Analytics I/O boundary state · MobX RootStore = Balance + Data + UI domain · WIRE TYPES ONLY SpinRequest · SpinResponse · Grid · Winline config columns · rows · symbolIds (client-side only) ↓ import direction (enforced by Biome) SERVER (RGS) not slotplate paytable reelstrips RNG + spin eval RTP / math balance (truth) SpinResponse The client never computes these. It renders what the server said happened.
Each named block is a folder under src/. Arrows show the only direction imports may point.

Reading the stack

Top to bottom is outer to inner: the UI (HTML or React) mounts scenes, scenes hand off to the FSM, the FSM drives presenters, presenters observe stores, stores hold domain wire-types. Infrastructure (network, timing, assets) is a sibling service layer that flow and presenters can call into.

Import rules

  • A layer may import from layers below it.
  • A layer may NOT import from layers above or alongside it (except infrastructure, which is a shared service layer callable by flow/ and presenters/).
  • domain/ imports nothing but types and other domain/ files.
  • Only view/ and presenters/ may import pixi-reels / pixi.js.

These are enforced by biome.json using no-restricted-imports rules. Violating them fails lint locally and in CI.

One layer per folder

Here's what lives where:

FolderContent
src/composition.tsComposition root. Wires every service.
container/Tiny DI container (type-keyed, lazy).
state/MobX stores. Observable state + @action mutators.
domain/Wire types only. SpinRequest, SpinResponse, Grid, Winline.
infrastructure/I/O. Network, ticker, assets, analytics.
flow/FSM + phase handlers. Owns game time.
presenters/State → view. ReelsPresenter, HUDPresenter.
view/Pixi scenes, symbol classes, overlays.
config/Client-side config (grid dims, symbol ids).

Data flow through the layers

input HUD click FSM owns time state MobX stores presenters observe → drive view pixi-reels resolution allLanded, onComplete transition action autorun render done next transition ONE DIRECTION no back-channels
The loop never breaks. Input at the left, resolution feeds back to the FSM, FSM transitions again.

Why layers, not packages

A package per layer is overkill for a single-game repo. It introduces build orchestration, version management, and workspace linking — all of which are costs. Layers as folders plus lint boundaries give you the isolation without the ceremony.

Violating the rules

If you really need to cross a boundary (and sometimes you do — Pixi math utilities, for example, are a judgement call), do it with an ADR. An architecture decision record is a committed document explaining why this case is different. Adding a rule exception via code comment is not acceptable; the justification lives in version control where future-you can find it.