- Python 100%
| grader | ||
| harness | ||
| sample | ||
| src | ||
| autoresearch.toml | ||
| compose.yaml | ||
| CONTRACT-javascript.md | ||
| CONTRACT-jvm.md | ||
| CONTRACT-perl.md | ||
| CONTRACT-ruby.md | ||
| CONTRACT-rust.md | ||
| CONTRACT-shell.md | ||
| CONTRACT.md | ||
| dockerfile.grader | ||
| dockerfile.worker | ||
| LICENSE | ||
| PROGRAM.md | ||
| README.md | ||
hatetris-policy: lines in wells you haven't seen
A count project (PER-361), the hatetris project's companion. Agents improve a HATETRIS player
(src/), and the grader plays it live against 48 held-out variants of the game: wells of other
sizes, each starting from hidden debris, qntm's adversary with a hidden tie-break order, and Heidi
Burgiel's adversary. The score is
the mean lines a game (lines, maximized). The worker sees one piece at a time and never the
adversary, so a memorised game can't win. The hatetris project scores the opposite way: one known
game, replayed from a committed move string.
| File | What it is |
|---|---|
autoresearch.toml |
the metric (lines: a count, maximized, MMI 0.4 lines a game absolute, deterministic, epsilon 0), runner class cpu-s, editable src/**, K = 50; no upstream; public |
PROGRAM.md |
instructions for the agents: the player, its budget, the rules, what counts as cheating |
CONTRACT.md |
the worker's HTTP contract: the template's, plus this project's rounds and states |
CONTRACT-*.md |
the template's language companions of the contract (auto-loaded files and loader variables per ecosystem), verbatim |
src/player.py |
the player being improved: a greedy one-piece placer to start with |
harness/worker.py |
the frozen HTTP server: it imports src/player.py and answers each state with play(state) |
grader/grade.py, grader/hatetris_variants.py |
the grader, and the rules it plays the games under |
grader/arh_grader |
the grader SDK, release 4 (sdk/grader/src verbatim) |
sample/items.jsonl |
the public sample: 8 variants, none in the held-out set |
dockerfile.worker, dockerfile.grader, compose.yaml |
Python 3.13, standard library only; the worker on core 0 and the grader on core 1, as runner class cpu-s places them |
LICENSE |
MIT, with qntm's notice for the ported rules |
The rules and the variants
grader/hatetris_variants.py is the hatetris project's port (its grader/hatetris.py) of qntm's
HATETRIS (https://github.com/qntm/hatetris, MIT, Copyright (c) 2021 qntm, commit
9e5962e60f89eef26d0d3f2eda726708149323a4). It adds:
- the variants: any size, a starting well (
garbage: rows of debris at the bottom, none full, none empty), the HATETRIS adversary's tie-breakorder, and Burgiel's adversary (src/enemy-ais/burgiel.ts), which starts with S or Z; - per-piece play:
Game.play_piece.
Calibration. tests/projects/test_hatetris_policy.py (in the platform's repository) replays
records with this copy of the engine, each at exactly its published lines:
- the hatetris project's standard-game records up to 366 lines, with the default variant;
- both records of Burgiel's adversary on qntm.org/records, under qntm's lenient replay of the published string:
| Date | Holder | Lines | Source |
|---|---|---|---|
| 2024-12-25 | Piotr Grochowski | 643 | https://qntm.org/records |
| 2025-10-29 | David & Felipe | 676 | https://qntm.org/records |
The sets. sdk/hatetris-policy/variants.py (in the platform's repository) draws them with
fixed seeds. Each variant is 16 to 24 deep, with the bar at 4. It starts from 2 to 5 rows of debris
at the bottom, each with 1 to 3 holes, drawn for each game. Its adversary is one of:
- qntm's, with its order a random permutation of the seven pieces, on a well 8 to 12 wide;
- Burgiel's, starting with S or Z, on a well 9 to 12 wide. At width 8 a one-piece greedy placer survives Burgiel's alternation to the piece cap from about one starting well in six, and one such game would be the whole baseline.
Each variant draws Burgiel's adversary with probability one in four. The held-out set holds 48 variants: 36 against qntm's adversary and 12 against Burgiel's. The sample holds 8: 3 against qntm's and 5 against Burgiel's. No variant is in both sets.
The debris is what keeps public games out. Without it, every game starts from an empty well, where the published replays have been. A book of their states, with greedy where it has none, transfers. In review (PER-361), Burgiel's 676-line replay alone scored 17 MMIs over the baseline, and every public replay together scored 85, qntm's games included. From hidden debris, no game reaches a published state. A test plays that book against the held-out set and requires it to stay within one MMI of the baseline. Apart from its debris, no variant is a published record's game either: 10 x 20 against the original order, or against Burgiel starting with S.
How a run is scored
- The variants are checked: each one's fields, its size, and an order of the seven pieces. Otherwise the run is invalid.
- The games are played in lockstep. The grader starts every game. Each round, one request
carries the state of every game still going, and each answer is that game's piece's moves,
played strictly (
Game.play_piece). A game fails at the first move that isn'tL,R,DorU, that changes nothing, or that comes after the lock, or when its moves end before the lock. A game ends when it is over, or at 1,000 pieces. - A failed game makes the run invalid, and the grader stops there: the games still going
count as failed (
abandoned). The result counts the failures by kind (the SDK's release 4failure_kinds): the SDK's own for an answer that never came or didn't fit the schema, andnot_a_move,blocked,after_the_lockorno_lockfor an illegal one. Every game is accounted for: completed + failed + abandoned =n_items. - Otherwise
linesis the mean of the games' lines. Each game's lines are its item score, in the file's order. The secondary values are the pieces locked, and the games that reached 1,000 pieces (capped).
The MMI
The metric is deterministic: the grader's play depends only on the variants and the player's answers, and players must be deterministic too (PROGRAM.md). Its floor is the held-out set's sampling error: twice the baseline's bootstrap standard error over its 48 games' lines. At the greedy baseline's 1.1667 (56 lines in 48 games) that is 2 x 0.1952 = 0.390, so the MMI is 0.4 lines a game (19.2 lines over the 48 games), with epsilon 0.
As players improve, their games' lines spread out, and so does the error: a later reward version measures its own floor. A game ends at 1,000 pieces, which caps what a game can score; a player that reaches the cap on most games needs a new version with a higher one.
A known limit: Burgiel's games will saturate. Burgiel's alternation can be survived for a long time: its published records run to 643 and 676 lines, and at width 8 even greedy loops. A strong player will learn to reach the cap against it at widths 9 to 12 too, about 400 lines a game. Once it does, those 12 games carry nearly all of the mean and its error. A later reward version will rebalance them, with a smaller share of Burgiel's games, a separate cap, or a weight.
Running it
docker compose up --build --abort-on-container-exit --exit-code-from grader
docker compose run --rm --no-deps --entrypoint cat grader /result/result.json
docker compose down --volumes
The baseline scores lines 1.5 on the public sample (12 lines in 8 games, 147 pieces). arh run
(the platform's CLI, cli/README.md) runs the same three commands and prints the result.
Licences
| What | Source | Licence |
|---|---|---|
grader/hatetris_variants.py (a port), the piece drawings in src/player.py |
qntm's HATETRIS, https://github.com/qntm/hatetris, Copyright (c) 2021 qntm | MIT |
| everything else | this project | MIT |
Python 3.13 (python:3.13-slim-bookworm) |
Docker Hub, the images' base | PSF-2.0 |
The project has no upstream ([project].upstream left out, ADR-0004). Its repository on the
platform is its own, based on this directory in one commit, with no pull mirror.