Participants
Build against this.
Quickstart
Fork the starter repo: a working agent, baselines to beat, and a harness that plays games locally under the real clock. Or start from scratch, below.
A submission is a zip. At its root, meaning not inside a folder:
agent.zip ├── agent.py required ├── requirements.txt optional, installed from PyPI └── model files anything else you need, 200 MB total
The smallest legal agent:
import chess
import random
def get_move(fen: str, time_left_ms: int) -> str:
board = chess.Board(fen)
return random.choice(list(board.legal_moves)).uci()The base image ships Python 3.12 with torch (CPU), numpy, python-chess and onnxruntime preinstalled. Compiled wheels from PyPI are fine. Native binaries inside the zip are rejected at validation: ship Python source and list compiled dependencies in requirements.txt.
requirements.txt takes plain package names and version specifiers. No URLs, no index options, no local wheels, and wheels only, so a package with no Linux wheel fails the build. Your zip goes first on sys.path, so a file named after a module you import, chess.py or types.py, shadows the real one.
Agent API
agent.py must expose one function:
def get_move(fen: str, time_left_ms: int) -> str
| Name | Type | Meaning |
|---|---|---|
fen | str | the position to move in, standard FEN |
time_left_ms | int | your remaining clock in milliseconds |
| returns | str | a legal move in UCI, e.g. e2e4 or e7e8q |
One process serves one game, started fresh for every game. Load your model at import or on the first call: a 60 second init budget runs before the clock starts, and the process stays alive between moves, so state you keep in memory carries across your own moves. The referee claims threefold and fifty-move draws automatically, so an agent that wants to avoid a repetition tracks the positions it has been asked about.
Wire protocol
The runner talks to your process over stdin and stdout, one JSON object per line. You only implement get_move; the provided runner handles the wire. Each request:
{"fen": "rnbqkbnr/...", "time_left_ms": 87500}Each response:
{"move": "e2e4"}| your colour | the side to move in the fen |
time_left_ms | your clock before this move; the increment lands after you move |
| output cap | 4096 bytes per move; past it the game is lost |
| malformed output | counts as an illegal move, which loses the game |
Your own output cannot corrupt this. The runner moves the protocol onto a private handle and points file descriptor 1 at stderr before importing your agent, so print is safe. Everything you write to stdout or stderr is discarded during rated games and shown back to you in the validation log, up to 8 KB.
Match environment
| CPU | 1 dedicated core |
| Memory | 2 GB |
| Network | none, in either direction |
| GPU | none |
| Filesystem | read-only, plus 256 MB scratch at /tmp, where HOME and the cache paths already point |
| Processes | 128; on one core, threads past the first cost you time |
| Hardware | identical for every game; both agents on the same machine |
No network means no hosted inference and no engine APIs, by construction. Everything your agent needs ships inside the zip.
Clocks and scoring
| Time control | 120 s per side, plus 0.5 s per move |
| Init budget | 60 s before the clock starts |
| Game end | FIDE rules; 300 plies without a result is adjudicated on material, else drawn |
| Ladder | rated rounds every 2 hours, 08:00 to 22:00, ranked by Elo |
| Qualification | an 11-round Swiss over the locked submissions decides the top 48 |
Submissions
| Size | 200 MB expanded |
| Rate | 6 uploads per team per rolling 24 hours |
| Which plays | your latest submission that passed validation |
| Validation | build, then two smoke games, one as each colour; the verbatim log appears on your dashboard |
| Lock | 11 September 12:00; uploads close 11:00 |
Prohibited: Stockfish, Lc0, or wrappers around any existing engine. A learned model must materially drive move selection. Submissions are analysed and disqualification can be retroactive.
What you ship has to be source a judge can read, so a flagged game can be cleared by reading your agent instead of by statistics alone. Model weights are not binaries: .onnx, .safetensors and .pt are fine. Compiled dependencies come from PyPI, and a package you published yourself needs public source. Obfuscated or opaque agents are disqualified.
Failure reference
| Termination | Cause | Result |
|---|---|---|
illegal | illegal or malformed move, or output past the cap | loss |
crash | your process exited, threw, or ran out of memory | loss |
flag | clock ran out mid-game | loss |
init | no ready line within the 60 s init budget | loss |
adjudication | 300 plies without a result | material decides, else draw |
void | both sides failed | no result recorded |