Deterministic lockstep architecture.
The server runs one authoritative game world. Clients receive snapshots and command frames, then run the same fixed-tick simulation locally for prediction and checksum validation.
- Low network overhead through semantic commands.
- Deterministic simulation with replay support.
- Predictable latency through fixed command delay.
core: deterministic command model, binary protocol helpers, checksums.game: single esper world, components, movement processor, simulation step.- esper: ECS library.
server: websocket server and match coordinator over the shared world.config: match and initial spawn configuration.web/index.htmlandweb/client/: browser canvas demo client.
- Simulation uses integer coordinates and velocities.
- Every tick applies commands in canonical
(tick, player_id, sequence, command)order. - Entity iteration is sorted by stable unit id.
- Networking never mutates simulation state directly.
- Async is allowed around I/O, not inside the simulation update.
Install for development:
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
pytestRun a server (tokens are pre-shared secrets issued to each client by the launcher):
server --host 127.0.0.1 --port 8766 --player-tokens alice-token bob-tokenOmit --player-tokens to connections become spectators and cannot issue commands.
New clients receive a state_sync message:
snapshot: deterministic world snapshot atsnapshot_tick.command_frames: authoritative command history after the snapshot.current_tick: tick to replay up to before live frames continue.
The server keeps this snapshot as a bootstrap cache. Late-joining clients reconstruct state by loading the latest snapshot and replaying only the command tail after it.
Runtime transport uses websocket frames carrying MessagePack payloads.
Every connection must send an auth message as its first frame before any other message is accepted. The server closes the connection silently on an unknown token or a 10-second timeout.
# client → server (first message)
{"kind": "auth", "token": "<pre-shared-token>"}
# server → client (on success)
{"kind": "state_sync", "player_id": 6, "snapshot": ..., "command_frames": [...], ...}player_id is the server-assigned entity ID that owns the client's units.
Command frames contain intentions, not replicated unit state. The server
assigns the issuer from the authenticated connection — the wire field is
ignored.
# client → server
{"kind": "command", "command": {"type": "MOVE", "sequence": 1, "targets": [7, 8], "x": 1}}
# server → client
{"kind": "command_accepted", "sequence": 1, "assigned_tick": 105}
# server → all clients (each tick)
{"kind": "command_frame", "tick": 105, "commands": [...]}Clients periodically send deterministic state checksums. The server validates
only ticks within the window [snapshot_tick, current_tick + checksum_interval].
# client → server
{"kind": "checksum", "tick": 200, "checksum": "7d87f1ab"}Server compares client checksums for the same tick against its own
authoritative value and other clients. If values differ, it broadcasts a
desync_report with checksum groups by participant.
- No backpressure for broadcast
- No Rate-Limit on commands