The Elo tools are split so rating management never needs to start an engine:
elo.pyowns the engine registry, pairing suggestions, persistent match records, result reporting, and Elo calculations.elo_matchmaker.pyis an optional automatic runner. It schedules matches throughelo.pyand plays them concurrently throughautoplay.py.
Both scripts use logs/elo-matchmaker.sqlite3 by default. Put a custom
--database option before the subcommand.
An engine ID denotes one fixed playing policy. Register a new ID after any change that can affect move selection.
python elo.py register mcts --command "./build/5dchess mcts"
python elo.py register monkey --command "./build/5dchess monkey"
python elo.py register experimental-v1 \
--command "./build/5dchess linear --weights weights/v1.bin" \
--artifact weights/v1.binTracked artifacts are hashed at registration. A rated schedule is rejected if
one changes. Create a distinct version with clone:
python elo.py clone experimental-v1 experimental-v2 \
--command "./build/5dchess linear --weights weights/v2.bin" \
--artifact weights/v2.binAdd --inherit-rating to use the parent's current rating as the new version's
starting estimate. Match histories remain separate.
suggest is read-only. schedule reserves matches and gives them persistent
IDs:
python elo.py suggest 4 --engine experimental-v2
python elo.py schedule 4 --engine experimental-v2Focused matchmaking defaults to the adaptive strategy:
python elo.py suggest 4 --engine experimental-v2 --strategy adaptiveAdaptive matchmaking favors opponents near the focus engine's Elo, gives more weight to opponents with established ratings, mildly discourages repeats, balances opponents above and below the focus rating within a wave, and uses one bounded exploration game per ten focused games. For a broad diagnostic pass, the coverage strategy retains the original fewest-head-to-head-first behavior:
python elo.py suggest 4 --engine experimental-v2 --strategy coverage--strategy only applies together with --engine. Matchmaking without a focus
engine continues to use the existing global arena algorithm unchanged.
Run each pairing with autoplay.py or any other controller, then report the
observed result:
python autoplay.py --white "WHITE COMMAND" --black "BLACK COMMAND"
python elo.py report 42 white --pgn completed-game.5dpgnValid reports are white, black, draw, and void. A void result is stored
but does not affect ratings. The final result automatically finalizes a
complete batch. Use --no-auto-finalize and elo.py finalize BATCH_ID when
explicit control is preferable.
Abort means that an operator deliberately excludes work from ratings and
matchmaking. It differs from void, which means a game ran but did not produce
a valid result. Aborted matches retain their reason and diagnostic artifacts,
but their official result is cleared.
Abort the only open batch, or name a batch when several are open:
python elo.py abort batch --reason "engine configuration was wrong"
python elo.py abort batch 7 --reason "engine configuration was wrong"This aborts scheduled, running, and reported matches in the batch, applies no rating changes, and releases the open-rated-batch lock. Running automatic workers notice the state change, cancel autoplay, and close their engines.
Individual matches can be excluded while keeping the rest of the batch:
python elo.py abort match 42 44 --reason "bad opening position"If valid reported matches remain after all other work is aborted, the batch is finalized using only those valid results. If nothing valid remains, the batch is cancelled. Matches in an already finalized batch cannot be aborted because later rating snapshots may depend on them.
To remove every open match involving a broken engine:
python elo.py abort engine experimental-v2 --reason "loads the wrong weights"When that engine occurs in multiple open batches, select one with --batch or
explicitly use --all-open.
Disabling is deliberately separate from aborting:
python elo.py disable experimental-v2A disabled engine is excluded from future scheduling, but existing scheduled matches remain reproducible snapshots and can still be resumed. The command warns when such matches exist. To disable and abort all its open work in one operation, use:
python elo.py disable experimental-v2 --abort-open \
--reason "engine is broken"Re-enable the same unchanged engine when appropriate. If its playing behavior was fixed or its weights changed, register or clone a new engine ID instead.
Machine-readable scheduling is available with --format json or
--format tsv. Other useful commands are:
python elo.py pending
python elo.py history
python elo.py leaderboard
python elo.py update ENGINE --no-enabledFor engine A with rating
After the result, A's rating change is
where --k-factor. B is updated with the complementary
score, so the two changes sum to zero apart from floating-point rounding. Void
games are recorded but have no rating change; the automatic runner reports an
action-limit result as a draw.
The constants follow the conventional Elo scale: a 400-point difference means
the stronger engine has ten times the weaker engine's expected-score odds.
Starting at 1500 is only a neutral pool convention.
Concurrent games form one rating batch. Every game
Applying the sum once makes the final rating independent of which concurrent game finishes first. The next batch is scheduled from the updated ratings.
With --engine F, adaptive is the default strategy. For each eligible
opponent O, ordinary adaptive games maximize
using these factors:
| Factor | Formula | Reason for the formula and parameter |
|---|---|---|
| Rating proximity |
$\exp(- | R_F-R_O |
| Repeat factor |
|
|
| Confidence proxy |
|
|
| Batch load |
|
|
| Bracketing |
1.15 for the underrepresented rating side; otherwise 1 | If opponents exist both above and below |
Exact ties are resolved by the seeded pseudo-random generator, so the same
database and --seed produce the same suggestions.
Every tenth non-void game involving the focus engine is an exploration game.
Exploration first restricts candidates to
Here exploration's
--strategy coverage retains the original diagnostic behavior. It chooses the
minimum tuple
lexicographically. Because
Without --engine, the existing global arena scheduler is unchanged. For every
available unordered pair A/B, it minimizes
lexicographically. This prioritizes equal test participation, then pair diversity, then rating proximity. It is intended to maintain a broadly tested engine pool rather than imitate a player-facing live-game queue.
For rated scheduling, only rated non-void games contribute to max_parallel
limits are also enforced; zero means unlimited and a stateful engine defaults
to one.
After selecting two engines, colors are assigned independently of the opponent score:
- Give White to the engine that has had fewer White games in this exact head-to-head pairing.
- If tied, give White to the engine with fewer White games overall.
- If still tied, use the seeded tie-breaker.
These rules reduce color imbalance without distorting which opponents are selected.
The automatic runner schedules games in waves. --jobs controls actual
concurrency; the default wave size is the same as the job count.
python elo_matchmaker.py run \
--engine experimental-v2 \
--strategy adaptive \
--event "Experiment A" \
--games 100 \
--jobs 8 \
--movetime 1000The --strategy option may be omitted here because focused automatic runs
default to adaptive. Use --strategy coverage when initially checking a new
engine broadly against every available opponent.
The event name is copied into each game's PGN. The runner records the site as
Local/Batch N, the round as the match's one-based ordinal within that batch,
and the global database match number in the Matchid header.
Each completed game is printed as one block containing its pairing, result, full PGN, log path, and metrics path. Because concurrent games can finish in any order, their Elo changes are printed when the wave finishes.
All games in a wave use the ratings captured when the wave was scheduled. Their deltas are summed and applied atomically, making the result independent of process completion order. The next wave uses the updated ratings.
Each result and its completion block are reported as soon as its worker finishes. Pressing Ctrl+C before the first result cancels the entire wave: it does not report games, update ratings, or affect future matchmaking, and a new rated run can start immediately. The batch remains only as cancelled audit history.
If at least one game has already finished, those finished results are retained and unfinished games return to the scheduled state. The open batch can then be resumed without looking up its ID when it is the only open batch:
python elo_matchmaker.py resume --jobs 8An explicit ID remains available when more than one batch is open:
python elo_matchmaker.py resume 7 --jobs 8An abrupt termination that prevents this cleanup (for example, a power loss or
kill -9) can leave a batch to inspect and resume:
python elo.py pending
python elo_matchmaker.py resume --recover-running --jobs 8Use --recover-running only when the former workers are no longer alive. It
returns their matches to the scheduled state before launching replacements.
Elo assumes a reasonably fixed player. Games in which an engine learns should be scheduled as unrated:
python elo.py register learner --command "..." --training --stateful
python elo_matchmaker.py run --engine learner --games 1000 --unratedA stateful engine is limited to one game in each concurrent wave so two
processes cannot overwrite the same checkpoint. The engine must save weights
to disk because autoplay.py starts fresh engine processes for every game.
For rated evaluation, freeze a checkpoint, register it as a non-training engine
version, and run its matches in parallel. Frozen checkpoint versions appear on
the official leaderboard; training engines do not unless
elo.py leaderboard --include-training is requested.