The 10 Principles
Hard rules. Break them and the diff should not pass review.
These aren't suggestions. They're the load-bearing rules the architecture rests on. Every layer, every file, every lint rule in slotplate points back to one of these. Where possible we encode the rule in Biome so the codebase itself rejects violations.
1. The client does not evaluate
The paytable lives on the server. The win evaluator lives on the server. The RNG and
the reelstrips live on the server. The client sends a SpinRequest and
receives a SpinResponse with grid, winlines, and
totalWin already resolved. The client renders. That is the entire loop.
Why: two evaluators mean two sources of truth. One of them is wrong. Finding out which in production, at 3am, with real money moving, is the bug you do not want.
Enforced by: the domain/ folder has no evaluateWin.
The template has no paytable JSON. If an agent tries to generate one, it has no place to wire it.
2. No setTimeout in game code
All scheduled work goes through Ticker.schedule(ms, fn) from
src/infrastructure/timing.ts. Under the hood it's gsap.delayedCall
on a GSAP ticker that's synced to Pixi's app.ticker.
Enforced by: Biome's no-restricted-globals rejects
setTimeout and setInterval in game code, with a message
pointing at the Ticker service.
3. Presenters don't decide when
A presenter observes state, translates it into view calls, and exposes a narrow API for the FSM to drive. It does not hold timers. It does not sequence animations across multiple frames of game state. That's the phase's job.
Why: presenters are dumb — easy to test, easy to replace. The moment a presenter owns timing, it becomes a miniature state machine, and you have two of them. Two state machines are four times harder to reason about than one.
4. State mutates via actions only
MobX @action-wrapped methods are the only mutators. Views read; presenters read;
phases call actions. A view or presenter that writes directly to a store is a bug.
5. Disposable everything
Any class that allocates a ticker handle, event listener, or Pixi resource implements
Disposable and is torn down by its parent scene. Scene teardown cascades.
Why: slots run for hours. Tiny leaks compound. Resource-leak bugs are usually invisible for the first week and lethal by the second.
6. Fail loud
No silent catch . No as any without a one-line comment
explaining why this is the least-bad option. No "fallback to a hidden default"
that masks a missing config. Throw with a message that names what's missing and who
should have provided it.
7. One composition root
src/composition.ts is the only file that instantiates services. Everything
else receives dependencies. If you find yourself writing new SomeService(...)
anywhere else, stop — pass it as a parameter instead.
8. pixi-reels lives behind one presenter
Only files in src/view/ and src/presenters/ may import
pixi-reels or pixi.js. The rest of the app talks to the view
through ReelsPresenter.
Enforced by: Biome's no-restricted-imports with a
pattern that catches any import of pixi-anything outside those two folders.
9. Every phase is a file
New FSM states go in src/flow/phases/ and register in
src/composition.ts. Phases implement Phase with
enter, optional skip, optional exit. They're
testable without a canvas — see tests/flow/SpinPhase.test.ts for the shape.
10. Docs are for agents too
When you add a concept, add a docs page. /llms.txt and /llms-full.txt
regenerate on build. Agents read docs the same way humans do — make sure yours exist.