Troubleshooting

A move silently does nothing

The action was rejected by validate. applyAction returns { ok: false, error }; the UI surfaces it via the ErrorBanner. Check requireCurrentPlayer / requirePhase and your payload shape. In code, inspect result.error.

“It is not your turn” online when it is my turn

Online control is gated on controlSeat === turn.current. Confirm your client id matches a player id in the match (createMatch uses the room’s player ids). Re-join the room; the seat is keyed off the stable per-browser client id.

Refreshing a local game loses undo history

Expected. Undo/redo lives in the in-memory MatchClient. The state is autosaved to localStorage and reloaded on refresh, but the history stack isn’t persisted.

Online room not found / can’t join across two computers

The default backend is local (same browser only). Cross-machine play needs Firebase — see Firebase Multiplayer. Across two tabs of the same browser, the local adapter works out of the box.

Cross-tab sync isn’t updating

The local adapter uses BroadcastChannel + the storage event. Both require the same origin. Private/incognito windows have isolated storage and won’t sync with a normal window.

Bots don’t move

Bots auto-play only on the Play (local) screen, driven by botAction, which requires the game to define an ai hook. Online rooms are human-only by design.

Build fails on tsc -b

The build type-checks src/ including tests. Run npm run build to see the exact file/line. Common causes: a non-JSON value in game state, or an unguarded undefined from getGame/getView.

Determinism differs between runs

A reducer called Math.random()/Date.now() instead of ctx.random. All randomness must flow through ctx.random so replays and sync stay in lockstep.

You registered the def but forgot the import line. Add it to src/games/index.ts (def) and src/games/views/index.ts (view) — registration happens on import.