1. High-level architecture
┌────────────────────────┐ WebSocket (JSON) ┌──────────────────────────┐
│ Browser client (JS) │ ── ClientMessage ───────────▶ │ Rust server │
│ PixiJS renderer │ │ axum + tokio │
│ - lobby UI │ ◀─ ServerMessage ────────── │ - static file serving │
│ - input / selection │ │ - /ws upgrade │
│ - camera / minimap │ │ - Lobby (rooms) │
│ - fog overlay (local) │ │ - Game (authoritative) │
└────────────────────────┘ └──────────────────────────┘
- The server owns the authoritative game state and runs a fixed-rate simulation
loop (
TICK_HZ). Clients only send commands (intent); they never mutate game state directly. - Every tick the server produces a per-player snapshot, applying fog of war: a player only receives neutral/enemy entities standing on tiles that player can currently see, plus visual-only entities inside the one-second lingering sight left behind when that player’s unit/building dies. This makes the fog cheat-proof (hidden enemies are never sent outside live or explicit lingering death vision).
- Lobby-time spectators are connected humans who are not seated in the simulation. They receive snapshots filtered to the union of all active players’ current fog, all player resource rows, and no controllable units/buildings. Spectators must join or switch roles before the match starts; mid-match joins are rejected.
- The client renders snapshots, interpolating entity positions between them for smoothness, and draws the fog overlay from the server-provided current visibility grid while keeping explored history locally. Local sight stamping exists only as a fallback for older/dev object snapshots; the server remains the fog authority.
- Local development exposes game-backed dev scenario pages under
/dev/scenariosand a neutral saved-artifact replay launcher at/dev/replay-artifact?replay=<artifact_name>. Scenario rooms stream full-world snapshots for authored local debugging; saved self-play artifacts use the same replay viewer runtime as post-match and match-history replays. - The same server exposes a lightweight documentation wiki at
/wiki. It renders only allowlisted Markdown underdocs/contextanddocs/design, rewrites relative Markdown doc links into/wiki/...links, rejects traversal or unsupported paths, and serves/wiki/statsfrom Rust-authoritative rules and faction catalog data instead of client mirrors. - The same Rust process serves the static client files, so development is a single
cargo runand then open the printed local URL.
Compatibility policy
The game is pre-alpha and latest-version-only. Do not preserve obsolete protocol, replay, client/server, map, or asset behavior just for backwards compatibility unless a specific migration or debugging workflow requires it. Breaking changes are acceptable when the design docs and all current Rust/JS mirrors are updated together.
Workspace crate boundaries
The Rust server workspace is split by dependency direction. Lower crates must not depend on higher crates:
rts-server -> rts-ai, rts-sim, rts-rules, rts-protocol, rts-contract
rts-ai -> rts-sim, rts-rules, rts-protocol, rts-contract
rts-sim -> rts-rules, rts-protocol, rts-contract
rts-protocol -> rts-contract
rts-rules
rts-contract
rts-server is the only crate that may own Axum/Tokio WebSocket/static-file serving and lobby room
tasks. rts-sim owns Game, tick systems, deterministic replay, map/fog/entity state, and
simulation perf accounting without importing server transport. rts-ai owns live controllers and
self-play harnesses and drives the sim only by observing snapshots and enqueueing ordinary
SimCommands. rts-rules owns pure vocabulary, balance data, terrain, economy, and combat
formulas. rts-protocol owns serde wire DTOs and compact snapshot transport. rts-contract owns
shared semantic DTOs that are below the wire and sim layers.
The server wiki belongs to rts-server because it is an Axum route and because generated reference
HTML is a presentation of lower-crate data. Wiki prose comes from repository Markdown files; wiki
stats rows come from rts-rules definitions and faction catalogs. After changing docs links,
allowlisted docs, rules definitions, faction catalogs, upgrades, or ability metadata, run
node scripts/check-wiki.mjs to cover route safety, link integrity, generated table completeness,
and client catalog parity.
scripts/check-crate-boundaries.mjs enforces the implemented Cargo package graph and rejects
server-only imports in lower crates. Any intentional graph change must update this section, the
script, and the affected context capsule in the same change.
Tick & networking model
TICK_HZ = 30(~33 ms per simulated tick).- The server broadcasts a snapshot every
SNAPSHOT_EVERY_N_TICKSticks (default 1 → 30 snapshots/s). - Commands are queued on arrival and drained at the start of each tick (deterministic ordering per connection; ordering across connections is arrival order).
- The client renders at
requestAnimationFrame(~60fps), interpolating between the two most recent snapshots using wall-clock time.