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:
| field | meaning |
|---|---|
mv | from and to as square indices (row * 10 + col, a1 = 0) |
piece | the moving piece's type, full information |
combat | attacker, defender and result when the move was an attack |
ms | thinking time in milliseconds when it was measured |
value | the 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 |
nodes | nodes 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.