Grand Century Grand CenturyDocumentation
Play
Engineering/Contributing

Architecture

Last updated 2026-08-28

Grand Century is a browser game with the entire simulation running off the main thread. This document is the contract contributors build against.

The one hard boundary#

src/sim/** and src/worker/** are pure logic. No DOM, no React, no window, no imports from src/ui or src/map. They may import only from src/shared, src/sim, and src/data.

src/ui/** and src/map/** only read. They render the latest WorldSnapshot and dispatch Commands. They never mutate world state.

The two sides communicate only through the message protocol in src/shared/types.ts (ToWorker / FromWorker).

This boundary is what allows the same simulation code to run in a browser worker and in the Node multiplayer server without modification. Breaking it breaks multiplayer, not just tidiness.

Source of truth#

Layout#

src/shared/types.ts       domain contract (data + protocol + commands)
src/sim/rng.ts            seeded PRNG
src/sim/world.ts          tick loop + cadence dispatch
src/sim/bootstrap.ts      createWorld(data, seed) -> initial World
src/sim/commands.ts       applyCommand(world, data, cmd, post)
src/sim/snapshot.ts       buildSnapshot(world, data) -> WorldSnapshot
src/sim/detail.ts         detailProvince / detailNation (on-demand views)
src/sim/balance.ts        central tuning constants
src/sim/systems/*.ts      one file per simulation system
src/worker/sim.worker.ts  worker entry (message loop + fixed timestep)
src/worker/saveSlots.ts   IndexedDB save slots
src/net/*                 multiplayer transport, codec, session protocol
src/data/gameData.ts      baked static GameData
src/map/*                 MapLibre province map, paint and mapmodes
src/ui/*                  React panels and HUD

Systems#

Every simulation system is a function with the same shape:

(world: World, data: GameData, rng: Rng) => void

They mutate World in place and are dispatched by cadence from advanceDay. Adding a system means adding a file under src/sim/systems/ and calling it from the right cadence block in src/sim/world.ts — see The simulation loop for why the order within a cadence matters.

Determinism#

All randomness goes through src/sim/rng.ts, a seeded mulberry32 generator threaded through world.rngState. Never call Math.random() in sim code.

This is not a style preference. Saves, replays and multiplayer all depend on the same inputs producing the same world. A single unseeded random call desynchronises a multiplayer session and makes a save unreproducible.

Some systems accept an Rng parameter they never use — culture is one — purely to keep the system signature uniform. That is deliberate.

Performance#

Verification gate#

Before a milestone is committed:

  1. npm run build — typechecks and bundles clean.
  2. npm run test — vitest green.
  3. npm run dev boots and the milestone's acceptance criterion is met.

Aesthetic#

An archival nineteenth-century atlas: aged parchment map, muted province fills, period display type, ornate but legible panel frames. Dark, readable panel text on parchment and leather surfaces.

See also#

Ask