# Agent contract

The participant-facing interface, rendered at `/docs` on the site together with `RULES.md`. It does not change once the qualifier starts.

## Submission

A zip whose root holds:

- `agent.py` exposing `get_move(fen: str, time_left_ms: int) -> str` returning a UCI move (`e2e4`, `e7e8q`).
- optionally `requirements.txt`, installed from PyPI at validation. Plain package names and version specifiers only: no URLs, no index options, no local wheels. Wheels only, so a package with no wheel for Linux fails the build.
- model weights and any other files, total <= 200 MB.

At the root means `agent.py` sits at the top of the archive, not inside a folder. Most zip tools wrap the folder you selected; check before uploading.

Native binaries inside the zip are rejected: 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, so `.onnx`, `.safetensors` and `.pt` are fine. Compiled dependencies come from PyPI, and a package you published yourself needs public source. Your zip is first on `sys.path`, so a file named after a module you import, `chess.py` or `types.py`, shadows the real one.

The base image ships Python 3.12 with pinned torch (CPU), numpy, python-chess, and onnxruntime; most agents need no extra installs.

## Execution

One persistent process per agent per game, started fresh for every game. Load your model at import or first call: a 60 s init budget runs before the clock starts. Between moves the process stays alive, 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.

The runner handles the wire. It writes one JSON line to your process per move request:

```json
{"fen": "...", "time_left_ms": 87500}
```

and reads one JSON line back:

```json
{"move": "e2e4"}
```

`time_left_ms` is your remaining clock before this move; the increment lands after you move. Your colour is the side to move in the fen.

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 conditions

- 1 dedicated CPU core, 2 GB RAM, no network, no GPU. Identical hardware for all games; both agents of a game on the same machine.
- The filesystem is read-only apart from 256 MB at `/tmp`. `HOME`, `TORCH_HOME`, `HF_HOME` and the other cache paths already point there.
- 128 processes, one core: threads past the first cost you time rather than winning it.
- Clock: 120 s base + 0.5 s per move, per side, enforced on wall time by the runner.
- 300 plies without a result: material adjudication, else draw. Draws otherwise per FIDE rules (python-chess).

## Failure semantics

Illegal move, malformed output, a move payload over 4 KB, crash, running out of memory, or flag fall: you lose that game. Both sides fail: the game is void. There are no retries within a game.

Validation plays two smoke games against a house agent, one as each colour, and publishes the verbatim log.

## Versions

The latest submission that passed validation plays each round. At the Sep 11 12:00 lock, that version freezes and alone plays the final qualification Swiss. Uploads close 11:00; validation queue time before the cutoff is the participant's risk. Six uploads per team per day.
