Game records
Game reports use the APGameRecord JSON format, modelled after chess PGN but expressed as JSON so multi-player and complex games (such as Homeworlds) can be represented.
Schema
The authoritative schema is src/schemas/gamerecord.json. TypeScript types are generated from it via npm run json2ts.
See the schema reference for a generated field listing.
Key fields
Header
| Field | Purpose |
|---|---|
header.game.name |
Game name (must be consistent across services) |
header.site.name |
Site identifier (e.g. Abstract Play) |
header.site.gameid |
Unique game ID on the site (required for rating) |
header.date-end |
When the game ended (used for chronological rating) |
header.players |
Player list with userid, name, and result |
header.unrated |
When true, raters skip this record |
header.outcome-type |
Solo: binary, graded, score, or timed |
header.score-direction |
Solo: higher or lower (timed is always lower) |
header.challenge-seed |
Persisted at archive when the run used seeded RNG (GameRng): the challenge id assigned before play (provided or platform-generated). Required for replay, rematch, and same-seed leaderboards. |
players[].grade |
Solo graded: tier id (e.g. good) |
players[].passed |
Solo binary: pass/fail |
Player results
Each player gets a result number. Higher numbers indicate better performance. For two-player games:
- Winner:
1, loser:0(convention) - Draw: same number for both players (e.g. both
2) - Both
1or both0is treated as contradictory data and skipped
Moves
moves is an array of rounds; each round is an array of moves in seating order. Raters use moves.length as the round count. By default, records with fewer than 3 rounds are skipped (minRounds option).
Validation
Use Rater.run() to validate records against the JSON Schema before rating:
import { ELOBasic } from "@abstractplay/recranks";
const rater = new ELOBasic();
const results = rater.run([jsonString1, jsonString2]);
runProcessed() accepts pre-parsed objects and skips schema validation — useful when records are already validated upstream.
CLI tools
| Script | Purpose |
|---|---|
bin/validate.ts |
Validate game record JSON files (<game-uid> [file.json]) |
bin/validate.ts --solo-leaderboard <file.json> |
Dev CSV: best-per-user solo ranks by game + variant + seed |
bin/convert.ts |
Convert legacy formats to APGameRecord |
bin/convert-homeworlds.ts |
Convert Homeworlds-specific records |
Edge cases
Raters skip (with warnings) records that have:
- Missing or empty
useridfor either player - Same
useridfor both players (self-play) - Contradictory results (both
result: 1or bothresult: 0) - Fewer rounds than
minRounds
Solo records (players.length === 1) are skipped silently — competitive raters only process two-player games.
Solo seeded play — archive contract
When a solo title uses GameRng (initRng / challengeSeed on game state), the completed record must include header.challenge-seed — whether the seed was chosen at create or generated by the platform. gameslib genRecord() writes this field; node-backend archive must not strip it.
| Field | When present |
|---|---|
header.challenge-seed |
Every archived solo run that consumed seeded RNG |
header.outcome-type |
Solo titles that declare an outcome model (binary, graded, score, timed) |
Solo records with challenge-seed can be replay-validated via validateSeededSoloReplay() (see bin/validate.ts). Same-seed leaderboards group on (game.name, variants, challenge-seed).
Set failHard: true to throw instead of warning for self-play and contradictory results.