This platform uses a client-authoritative architecture with host-as-server:
- Host: Processes game logic, validates actions, manages bots, and syncs state
- Guest: Receives state patches/updates from host and renders UI
- BaseGame: Core framework for synchronization and state management
graph TB
UI[Game UI] -->|Actions| Game[Game Instance]
Game -->|State Updates| UI
Game <-->|Socket.IO| Server[Socket Server]
Room[Room Store] -->|Info| Game
BaseGame uses Immer & Proxy for state management:
- Proxy Tracking: Direct mutations (
state.x++) are automatically recorded as patches - Immutable Snapshot:
useGameStateprovides an immutable snapshot (lastSnapshot) - React Optimization: Each update forces a new reference for reliable React re-renders
Three-layer sync mechanism for bandwidth optimization:
- Patch Compaction: Changes are collected in a
Map. If a path changes multiple times (e.g., during movement), only the final value is kept - Incremental Patching: Only changed parts (patches) are sent via
game:state:patch - Full Sync: Complete state is sent when new players join or sync errors occur
| File | Purpose |
|---|---|
MyGame.ts |
Extends BaseGame<T>, implements getInitState and onSocketGameAction |
MyGameUI.tsx |
Uses const [state] = useGameState(game) to get state |
types.ts |
Defines State and Action types |
// MyGame.ts
export default class MyGame extends BaseGame<MyGameState> {
getInitState() {
return { score: 0 };
}
onSocketGameAction({ action }) {
if (!this.isHost) return;
if (action.type === 'ADD') this.state.score++;
}
}Scoring, win/lose conditions should only be in the Game class (runs on Host). UI only sends actions.
State should only contain data (objects, arrays, primitives). Do not store classes or functions.
Never reassign this.state = .... Mutate directly or use Object.assign for Proxy to work correctly.
Call this.setGameName("name") to enable automatic save/load on page refresh.
Avoid scattered setTimeout calls. If you need delayed bot actions, manage timers carefully and always clean up in destroy() to prevent memory leaks.
Keep state as small as possible to optimize bandwidth (games sync via JSON patches):
| Technique | Example |
|---|---|
| Numeric constants | Use 0 instead of "betting" for game phases |
| Tuples over objects | Use [id, amount] instead of {id, amount} |
| History limits | Limit array lengths with shift() or delete old keys |
| Precision | Round decimals: Math.round(val * 10) / 10 |
| Short IDs | Use uuidShort() or round IDs (R1, R2) instead of Date.now() |