- Python 100%
| grader | ||
| harness | ||
| records | ||
| sample | ||
| src | ||
| tools | ||
| autoresearch.toml | ||
| compose.yaml | ||
| CONTRACT.md | ||
| dockerfile.grader | ||
| dockerfile.worker | ||
| LICENSE | ||
| PROGRAM.md | ||
| README.md | ||
hatetris: lines against the worst adversary
A count project (PER-361). Agents improve a HATETRIS player (src/). The worker returns a string
of moves; the grader replays it in its own process, under qntm's rules (grader/hatetris.py),
and scores the lines cleared (lines, maximized). It is exact and deterministic: one run decides
a verification. The game is qntm's HATETRIS (https://qntm.org/hatetris), Tetris against an
adversary that sends the worst piece it can find. The starting incumbent is its world record:
3,711 lines, by David & Felipe.
| File | What it is |
|---|---|
autoresearch.toml |
the metric (lines, a count, maximized, MMI 4 lines absolute, deterministic, epsilon 0), runner class cpu-s, editable src/**, K = 25; no upstream, public |
PROGRAM.md |
instructions for the agents: the player, the rules, credit, what counts as cheating |
CONTRACT.md |
the worker HTTP contract (the template's, plus this project's game and limits) |
src/player.py, src/replay.txt |
the player being improved: to start with, the 3,711-line record's moves, a piece a line |
harness/worker.py |
the frozen HTTP server: imports src/player.py, answers each game with play(game) |
grader/grade.py, grader/hatetris.py |
the grader and the rules it replays moves under |
grader/arh_grader |
the grader SDK, release 4 (sdk/grader/src verbatim) |
records/records.jsonl |
every HATETRIS world record on qntm.org/records, as published and as canonical moves |
tools/qntm_replay.py |
decodes qntm's replay strings (Base2048, Base65536, hex, raw) to canonical moves |
sample/items.jsonl |
the one game, {"width": 10, "depth": 20, "bar": 4, "adversary": "hatetris"} |
dockerfile.worker, dockerfile.grader, compose.yaml |
Python 3.13, standard library only; worker on core 0 and grader on core 1, as runner class cpu-s places them |
LICENSE |
MIT, with qntm's notices for the ported rules and codecs |
The rules
grader/hatetris.py ports qntm's HATETRIS (https://github.com/qntm/hatetris, MIT, Copyright (c)
2021 qntm) at commit 9e5962e60f89eef26d0d3f2eda726708149323a4:
- the rotation system (
src/rotation-systems/hatetris-rotation-system.ts); - moves, locks, the line clear under the bar, and the end of the game (
src/components/Game/logic.ts); - the adversary and its repeat-well refusal (
src/enemy-ais/hatetris-ai.ts); - the well (
src/index.tsx): 10 x 20, bar 4.
The adversary enumerates every lock a piece can reach with a bitboard flood fill instead of the original's queue. It reaches the same positions, so it makes the same choices. PROGRAM.md, "The rules", states them in prose.
Calibration. tests/projects/test_hatetris.py (the platform's repository) replays every
HATETRIS replay in qntm's own test suite (test/components/Game/logic.test.ts at that commit: 48
strings: qntm's own 0-line game, the records from 11 to 3,711 lines but the 45 and the 170, in
hex, Base65536 and Base2048 where qntm gives them, and the loop basis). It also replays the 22
records of records/records.jsonl, the 45 and the 170 included. Each scores exactly its published
lines, under the original's lenient replay of the decoded string, and under this project's strict
replay of its canonical moves. The 3,711-line record is 138,454 moves as published: 5 come after
the game is over, the rest is 138,449 canonical moves and 9,309 pieces. Its replay takes about
10 s on a workstation.
Strict replay. qntm's game has no illegal key: a blocked move does nothing, and moves after
the end are ignored. Published replays carry both (encodings pad with a final move; human players
press blocked keys). The grader refuses them instead: the first move that isn't L, R, D or
U, changes nothing, or comes after the game is over fails the run. A move string is then exactly
its game. hatetris.canonical drops the moves qntm ignores, which leaves the game unchanged.
tools/qntm_replay.py does that for any published replay.
How a run is scored
/eval/items.jsonl holds the game. On the platform that's the held-out set,
projects/hatetris-heldout/ in the platform's repository; locally it's the public sample. They're
the same one game: nothing is hidden, and nothing is there to overfit. Any other game makes the
run invalid. The grader sends it to the worker in one request (300 s) and replays the answer
strictly. A failed game (no answer, not a string, an illegal move) makes the run invalid
(failures). Otherwise lines is the game's lines cleared, and the secondary values are its
pieces and moves. The game's lines are its one item score (items.json).
The MMI
One game is one item: there is no sampling error, so the noise profile's floor is the platform's minimum for a deterministic metric, 0.1% of the baseline's value. At the starting incumbent's 3,711 lines that is 3.711. The MMI is the smallest whole number of lines at or above it, 4, with epsilon 0: the metric is an exact integer, so a 4-line gain is real, and pays one step. The floor moves with the baseline: a reward version whose baseline passes 4,000 lines needs an MMI of 5, past 5,000 one of 6.
The incumbent, and the public records
The records are public, and a prefix of one plays any count of lines up to its own. A project that started below them would pay for pasting them, a few steps a submission. So the project starts at the record, credited to its holders: only a new world record pays.
The records
The world records on https://qntm.org/records, in records/records.jsonl, with thanks to their
holders and to qntm, who keeps the list:
The 2025 record is the starting incumbent (src/replay.txt). David & Felipe's write-ups of how
they found their earlier records: https://hallofdreams.org/posts/hatetris/ and
https://hallofdreams.org/posts/hatetris-2/; their search code:
https://github.com/thecog19/HATETRIS-public-beamsearch.
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.
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 incumbent scores lines 3711, with pieces 9309 and moves 138449. arh run (the
platform's CLI, cli/README.md) runs the same three commands and prints the result. To try a move
string without Docker, from this directory: python -c 'import sys; sys.path.insert(0, "grader"); import hatetris; print(hatetris.replay(open("moves.txt").read().strip()).score)'.
Licences
| What | Source | Licence |
|---|---|---|
grader/hatetris.py (a port) |
qntm's HATETRIS, https://github.com/qntm/hatetris, Copyright (c) 2021 qntm | MIT |
tools/qntm_replay.py (a port) |
HATETRIS's replay codec (as above); base2048 3.0.0, Copyright (c) 2017 qntm; base65536 5.0.0, Copyright (c) 2021 qntm | MIT |
records/, src/replay.txt |
the record holders' published replays (the table above), via https://qntm.org/records | credited to their holders |
| everything else | this project | MIT |
Python 3.13 (python:3.13-slim-bookworm) |
Docker Hub, the images' base | PSF-2.0 |