Every Last City
An original turn-based strategy game in the tradition of Empire — you begin with one city on a fogged map, cities produce units, units explore and fight and take more cities, and nobody can talk their way out of it.
It is not a reproduction of any existing game. Mechanics and ideas are not anybody's property; specific expression is, and none is taken. Every value in this specification was designed for a stated reason rather than inherited, and where the genre has a known weakness the design is obliged to attack it rather than reproduce it.
Play it at everylastcity.usvtg.net — the browser client, hot seat or against the AI. A first visit asks how much game you want (Field, Campaign or Staff) and offers a small solo game to learn on; Menu on the bar reaches every screen after that. Read it at docs.everylastcity.usvtg.net — the specification, design record and art documents, rendered.
Specification
Fifteen documents in docs/spec/, 2,802 numbered requirements (counted 2026-09-24;
pnpm build-site prints the live total). Start with the overview, then the turn model — it is the
spine everything else hangs from.
| Doc | Prefix | Reqs | Scope |
|---|---|---|---|
| 00-overview.md | OV | 12 | Pillars, document map, cross-document discipline, 47-term glossary |
| 01-game-rules.md | GR | 200 | Map, terrain, movement, fog, the Intelligence Picture, Regions and Landmarks, setup |
| 02-units-and-industry.md | US | 239 | Unit schema, the Industry ladder, the roster, production |
| 03-architecture.md | AR | 124 | Deterministic core, fixed-point arithmetic, event sourcing, saves, testing |
| 04-ui-ux.md | UX | 210 | Orders authoring, Cascade playback, the Dispatch, accessibility, mobile |
| 05-multiplayer.md | MP | 159 | Correspondence, real-time, hot seat, lobbies, fog enforcement, self-hosting |
| 06-ai.md | AI | 220 | The Staff — four layers, six Ranks, honest fairness, the self-play farm |
| 07-modding-content.md | MOD | 139 | Content formats, WASM plugin ABI, editors, workshop |
| 08-services-platform.md | SVC | 126 | Guest-first accounts, sync, notifications, privacy, operations |
| 09-roadmap.md | — | — | Phased delivery plan and risk register (non-normative) |
| 10-turn-model.md | TM | 279 | Orders, the Cascade, the Reckoning — Initiative, Stance, reactions, orders-as-data |
| 11-combat.md | CB | 288 | The Clash, strength and disorder, retreat and rout, emergent encirclement |
| 12-economy.md | EC | 244 | Works and Manpower, Integration, the six visible brakes on growth |
| 13-command.md | CM | 221 | Postures, Sanctions, the order stack, Reflexes, the Dispatch |
| 14-victory.md | VC | 341 | Tracks, the Vigil, Standing and Tenure, the endgame and the comeback path |
Design record
docs/design/ holds the reasoning behind the specification — fifteen papers covering the binding direction, three binding decisions, the index of the specification rulings, six subsystem proposals, and four angles on the AI including a red team whose only job was predicting how the AI will fail.
These carry what usually evaporates: what was rejected, and why. The binding ones are
00-direction.md and the three decisions — 01-decision-turn-model.md,
02-decision-reach-units.md and 03-decision-cost-of-conquest.md. 02-spec-rulings.md indexes the
rulings that changed individual requirements; each ruling lives at the requirement it changed. The
rest are the arguments that produced the specifications.
Art production
docs/art/ specifies the sprite sheets in enough detail to hand to an image generator: art direction and the tint-mask format, the 23 unit sprites with per-unit generation prompts, terrain and the city, the overlay glyph set, and the decisions behind them.
It is written for the production method it will actually face — one sprite per image rather than a sheet, a style plate fed back as an image reference for consistency, and honest yield figures (4-8 takes per sprite, 10-20 minutes of cleanup each). Text is never generated; small glyphs are vector-authored.
Code
Phase 0 of the roadmap — the determinism foundation — is complete, and work is now spread across Phases 1 and 2: the rules engine, a browser client that is live at the apex domain, a headless CLI, a network host with a live lobby, and the AI's Constraint Floor.
corepack pnpm install
corepack pnpm run verify
verify is the bar before any commit. It runs, in order: the case-collision and unit-id guards, the
typecheck, the determinism lint, the whole test suite, and then a dozen repository gates — the
specification cross-references, the requirement-coverage ratchet, the traceability index, the art
cross-references, the accessibility audit, and the brake-word, counter-web, protocol-reference,
AI-fairness and balance-band checks. It takes several minutes — that is the cost of the gate, not a
sign that something is stuck.
| Script | What it does |
|---|---|
pnpm client |
The browser client's dev server on 127.0.0.1:49170 |
pnpm --filter @everylastcity/client build |
Builds the client into play/, which is committed and is what the apex serves |
pnpm elc |
The headless CLI: create, play hot seat, save, load, replay and verify a game; run-ai plays AI against AI unattended |
pnpm test:browser |
Runs the determinism suite inside a real browser engine (Chromium by default; set ELC_BROWSER for firefox or webkit) |
pnpm test:sweep |
The CB-2670 monotonicity sweep — 86,016,000 evaluations, minutes of CPU. Kept out of pnpm test and verify; run it by hand after touching the combat resolution |
pnpm golden-hash |
Prints the reference log hash |
pnpm check-spec |
Proves every requirement citation in the specification resolves |
pnpm check-req-coverage |
Requirement-to-test coverage: fails when a covered requirement loses its test (--gaps TM lists the uncovered; --update moves the baseline, and refuses to move it down unless --allow-decrease is passed — run it without a flag first) |
pnpm coverage-gaps |
Sections where testing stopped part-way — the risk surface a per-document percentage hides (--ids TM names them) |
pnpm built-untested |
Requirements the source claims to implement and no test names (--sections groups them into slices) |
pnpm check-art-refs |
Cross-document citations in the art documents: fails when a new one names an identifier two documents define (--list shows them) |
pnpm bench |
The AR-760 performance re-baseline, isolating the AR-740 fog cost (--max-seconds N bounds the climb) |
pnpm self-play |
The AI self-play farm |
pnpm build-site |
Renders the documents to static HTML in site/ |
| Package | What it is |
|---|---|
packages/core |
The deterministic rules engine. Its whole mutation surface is createGame, applyOrder and loadFrom (AR-030); everything else is read-only. |
packages/core/src/numeric |
Int and Fixed (signed Q16.16) exact arithmetic. Overflow throws; never wraps, saturates, or yields NaN. |
packages/core/src/rng |
Seeded PCG32 with SplitMix64-derived named streams. Hi/lo 32-bit math, no BigInt on the draw path. |
packages/core/src/hash |
Strict canonical encoder and 64-bit hashing — how two states are proven identical. |
packages/core/src/harness |
Replay-and-compare over a toy fixture. Not the game; the proof that the machinery works across platforms. |
packages/client |
The browser client (UX-010): Phaser 3 draws the tile map, React 18 and the DOM draw everything else. It also hosts the AI seat. |
packages/cli |
The headless runner (AR-800), the elc command. |
packages/server |
The network host: one game per process, as a dedicated server or, with --lobby, a pre-launch lobby joined by code. |
packages/host-shared |
What every host shares: the save container, deadlines, absence and substitution. |
packages/eslint-plugin-determinism |
Lint rules banning float arithmetic, ambient clocks and entropy, locale-sensitive comparison, and unordered iteration. |
packages/core/test/reference-hash.ts |
The committed reference hash every supported platform must reproduce — the cross-platform gate. |
Cross-platform determinism
The reference hash is d661f1831854e15a, asserted inside the suite against a committed constant
rather than by diffing two runs: comparing a freshly computed hash to itself proves only that an engine
is self-consistent, which every non-deterministic engine also manages. It is not an engine-behaviour
hash — the fixture is the toy harness above, so a rules change must never move it (AGENTS.md explains
what does track engine behaviour).
It is reproduced today on the two machines the project runs on — Windows and the Linux server — and
pnpm test:browser checks it inside a browser engine locally. There is no automated cross-platform
CI at present. .github/workflows/determinism.yml defines one — Node on Linux, macOS and Windows,
and Chromium, Firefox and WebKit — but the only remote is the bare repository on the server, so no
runner ever executes it. Until one does, the macOS and multi-engine columns of that matrix are
defined, not observed.
Contributing
The server is the only shared state — there is no other remote, and nothing local is visible to anyone else. Clone and push there:
git clone everylastcity@195.26.247.75:git/everylastcity.git
A push to main is the only way to publish anything. The hook checks the tree out to the served
directory, removes files deleted in the commit, restarts the docs service, and warns if the rendered
site is older than the documents. There is no second step, and there is no other mechanism — do not
copy files to the server by scp, rsync, or any script that packs a working tree. A publish that
is not a commit is a state nobody can reproduce, review, or roll back, and the deploy hook will
delete it on the next push without asking.
The deploy runs no build. site/ and play/ are both committed, so a deploy cannot fail for a
toolchain reason and leave either half-updated. That makes two builds your job before you commit:
pnpm build-site after any change under docs/, and the client build after any change to
packages/client or packages/core — the browser bundle includes the engine, so a core-only
change that skips it publishes the previous game. Review the markdown and the source; treat site/
and play/ diffs as generated.
The loop
git pull --rebase # someone else may have pushed
# claim the work in SESSION.md's "Work in flight" table — one line, its own commit, pushed now
# ... work ...
corepack pnpm run verify # the full gate; several minutes
corepack pnpm run build-site # if you changed anything under docs/
corepack pnpm --filter @everylastcity/client build # if you changed client or core
git add -A && git commit # the commit that lands the work also deletes its claim line
git push # this deploys
Claim before you start. More than one agent works on this repository at once, from different
machines, and git status shows only your own tree. The "Work in flight" table at the top of
SESSION.md is the only mutual exclusion the project has: every other record is written after the
fact. AGENTS.md, at the repository root, holds the durable constraints for anyone working here —
the editing discipline, the code constraints, and how to move a hash.
Commit and push early, and often. Uncommitted work is invisible to the team — which is the whole
reason this project has a server — and it is the one state the tooling cannot protect. Work that
lives only in a working tree for a day is a day of work one git clean away from gone.
If pnpm verify fails, fix it before pushing. main is what deploys, so a red main is a broken
published site and game, and there is no staging.
Where to work
Your own machine is the normal case: clone, work, push. Everything the project needs runs on Windows and Linux, which are the two platforms it is developed on today.
If you work on the server, make your own clone rather than sharing one:
git clone ~/git/everylastcity.git ~/dev/<yourname>
Everyone SSHes in as the same everylastcity user, so a shared working tree has no locking, and two
people editing it at once will overwrite each other silently.
Never work in ~/apps/everylastcity. That directory is the deploy artifact, not a workspace:
every push runs git checkout -f and git clean -fd there, so anything uncommitted in it is
destroyed without warning and without an error message.
This is not a hypothetical. The Phase 1 turn machine — 5,200 lines — sat untracked in that
directory and was one push from deletion. A scripts/publish.mjs used to pack the working tree,
uncommitted files included, and extract it over ~/apps; the README told people to run it. Two
publishing mechanisms owned one directory and destroyed each other's output. The script is gone and
git push is the only path.
The server's toolchain
Node 22.23.2 and pnpm 11.25.0 are installed under ~/.local/opt/node, exported from both
~/.profile and ~/.bashrc, so any shell you log into has them. The distribution's own Node 18 is
below this repo's engines floor (>=22) and cannot run the build.
If you see v18.19.1, you are in a shell that reads no startup file — which is what
ssh host 'command' gives you, since that shell is neither a login shell nor interactive. Use
ssh host 'bash -lc "command"' for scripted access. Logging in normally needs nothing.
Domains and services
| Domain | Serves | Port | Unit |
|---|---|---|---|
everylastcity.usvtg.net |
the game — the committed play/ bundle |
49001 | everylastcity-game.service |
docs.everylastcity.usvtg.net |
this documentation — the committed site/ |
49000 | everylastcity.service |
| (not public) | a live pre-launch lobby (05-multiplayer.md §8), its join code printed to its journal | 49010 | everylastcity-lobby.service |
All three are systemctl --user units under ~/.config/systemd/user/, bound to 127.0.0.1. The two
static services open each file per request, so a deploy is served at once without restarting the
game. The lobby is loopback-only; exposing it is a DNS decision (vhostctl publish <sub> 49010), not
the unit's.
Repointing a domain is one command, and it rewrites the mapping in place:
vhostctl publish <domain> <port>
Never use vhostctl unpublish on either domain. It does not just detach the name, it releases it
for any other account on this shared host to claim. There is no case in normal operation that needs
it: publish repoints.
Playing a live game locally. Start a lobby on the committed example and join it from two browser
tabs of the client (pnpm client), with Menu → Multiplayer, the printed endpoint, GameId and join
code:
pnpm --filter @everylastcity/server exec tsx src/main.ts --setup examples/human-vs-ai.json --port 49175 --lobby
The lobby launches once both human seats are taken, and each tab then claims its seat from the roster.
That example is a worldless table: it has no map. For a game on a map, give --setup a copy whose
setup and seed come from a client game, with an integer top-level rulesVersion and
deadlineSeconds equal to setup.ordersDeadline (the server's config.ts checks both).
Ports must stay inside the assigned range 49000–49199, and every app must bind 127.0.0.1 only —
0.0.0.0 would expose it to every other account on the box. The shared Caddy reverse proxy is what
makes a bound port public, and it is driven entirely by vhostctl; the Caddyfile is not ours to edit.
Status
Phase 0 is complete. The fixed-point representation is chosen and implemented, the determinism harness and lint gate exist and are verified to actually fire, and the specification carries no claim to reproduce another game.
Phases 1 and 2 are in progress together. The rules engine, the browser client, the CLI, the
network host and the AI's Constraint Floor all exist and are played against each other in automated
self-play; the game is live at the apex domain. How much of the specification is built and tested is a
moving figure — pnpm check-req-coverage gives it, and pnpm coverage-gaps and
pnpm built-untested say where the untested parts are.
Phase 3 — playtesting and the tuning pass — has not begun. Every number — combat odds, Integration rates, Industry costs, Initiative bands — is a first estimate by a designer who has not yet seen the system played by people. That is the project's real risk, and the roadmap gives playtesting a phase of its own rather than assuming it.
Historical context only: Empire Deluxe Combined Edition (Walter Bright's Empire → Empire Deluxe 1993 → EDIE/EDEE 2002 → EDCE) is the ancestor this game learns from. No asset, value, name, or table is taken from it.