Reference

Game records and replays

Every finished game that the site can replay is a GameRecordDto, the wire type defined in server/crates/protocol and generated into design/src/protocol.ts. One format serves the arena, the site's own games and, later, training runs, so a replay never depends on who produced it.

The format

A record carries both setups (forty glyphs each, back row first from the owner's view), the rules, the seed when there was one, the two contestants (display name, registry policy name for a program, its parameters), the result (status, ending, plies) and one entry per ply:

fieldmeaning
mvfrom and to as square indices (row * 10 + col, a1 = 0)
piecethe moving piece's type, full information
combatattacker, defender and result when the move was an attack
msthinking time in milliseconds when it was measured
valuethe mover's own estimate of the position after the move, in [-1, 1] with 1 a certain win; absent when the policy does not evaluate
nodesnodes or playouts searched for the decision when the policy counts them

value is the mean over the policy's sampled worlds of tanh(v / 1200), where v is that world's search value in material units, so a Marshal's worth of lead reads about 0.76, a terminal score reads plus or minus one, and one world in which the enemy Flag happens to be capturable moves the mean by at most one over the number of worlds. The policy still chooses by the raw mean of v, which is what the earlier arena results measured. Values from different policies share a scale but not a meaning: a greedy evaluation and an expectimax search are different estimators. Compare values within one contestant, not across contestants.

A record exists only for a finished game. Nothing about a live game is written down with both setups, so the format cannot leak hidden information.

The library

records/games/index.json is a RecordIndex: newest first, one RecordSummary per game with a file path relative to the index. Records sit in one folder per matchup. The web build stages the folder into web/public/records/, so /records/index.json and every record are static files and /replay/<id> resolves the id against the index. Games played on the site stay in the visitor's browser and appear beside the library as "Your games".

The library is curated content, not raw output: keep it under 3 MB and prefer games that a report refers to. The web build enforces it (web/scripts/check-records.mjs refuses more than 500 records or 3 MB), so a run that writes into records/games by mistake fails the build instead of shipping. Raw arena output stays in the ignored runs/ directory as before.

The archive

Bulk games, a self-play experiment's thousands or millions, go to the archive: one DynamoDB table in the project's AWS account, never git and never the static site. A record is about 28 KB of JSON and about 2 KB gzipped, which is how it is stored. Nothing is archived automatically. Write the games to a folder outside records/games and push the folder:

cargo run --release -p armies-arena --bin tournament -- \
  --policies greedy,rollout,expectimax --games 2000 --rules training \
  --threads 8 --record-dir ../research/runs/selfplay-0/records
just archive runs/selfplay-0/records --source training --tag selfplay-0

just archive runs web/scripts/archive.ts push: it checks every record against the wire schema, skips a game whose id is already archived (so a push can be repeated after an interruption) and keeps per-player and per-pair counters exact. The table is indexed by player, by player and result in ply order, and by pair, so the replay page answers "the shortest wins of this checkpoint against that one" by reading one partition, however many games there are. The site reads it through /api/archive/*; web/docs/archive.md has the key layout, the costs and the local table for development.

Sample before archiving a long run. A million games is about 9 GB stored and costs on the order of ten dollars to write; a game every few hundred of a training run's self-play, plus every evaluation game, is what a replay viewer can use.

Writing records

The arena binaries write records when asked:

cargo run --release -p armies-arena --bin matchup -- \
  --red expectimax:'{"samples":16,"depth":2}' --blue greedy \
  --games 20 --rules training --seed 0 \
  --record-dir ../research/records/games/expectimax-vs-greedy \
  --tag bbb0214b-63cf-46a3-b5a9-a1a7d322770e

Each game becomes <red>-v-<blue>-<seed>.json in that folder and the folder's index.json is merged. Run just records from the project root to rebuild the top-level records/games/index.json from the folders; it replays every record through the engine and refuses one that disagrees with the rules.

The site's server offers the same record for a finished game at GET /v1/games/{id}/record, and the browser build through the worker, so a game played against an agent can be studied in the replay viewer right after it ends.

Analysis in the viewer

The replay viewer can ask the engine for an analysis of any position from one side's point of view: the belief over every hidden enemy piece and the values of the candidate moves. The analysis is computed from that side's observation alone, never from the arbiter state, so what the viewer shows as "what Red believes" is exactly what a policy in Red's seat could know.