https://github.com/niklasf/lichess-puzzle-tb-check
Verify Lichess puzzles against tablebase data
https://github.com/niklasf/lichess-puzzle-tb-check
chess lichess syzygy tablebases
Last synced: 10 days ago
JSON representation
Verify Lichess puzzles against tablebase data
- Host: GitHub
- URL: https://github.com/niklasf/lichess-puzzle-tb-check
- Owner: niklasf
- License: gpl-3.0
- Created: 2026-06-02T20:28:59.000Z (about 2 months ago)
- Default Branch: main
- Last Pushed: 2026-06-03T21:01:49.000Z (about 2 months ago)
- Last Synced: 2026-06-28T15:00:51.621Z (22 days ago)
- Topics: chess, lichess, syzygy, tablebases
- Language: Python
- Homepage:
- Size: 2.16 MB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Lichess puzzle-tb-check
Verify Lichess puzzles against tablebase data.
Reads a [Lichess puzzle database](https://database.lichess.org/#puzzles) CSV,
looks at the positions that have tablebase coverage, probes the
[lila-tablebase](https://github.com/lichess-org/lila-tablebase) HTTP API, and
records — per puzzle — whether every puzzler move is the **unique winning move**
(or the **unique drawing move** for `equality` puzzles), plus a DTM check for
`mateInX` puzzles.
## Requirements
Python 3.14+ (uses the stdlib `compression.zstd` module), `uv`.
## Usage
Against public API (slow):
```sh
uv run puzzle-tb-check lichess_db_puzzle.csv.zst --out report.csv
```
Against a local instance:
```sh
uv run puzzle-tb-check lichess_db_puzzle.csv.zst --out report.csv --endpoint http://127.0.0.1:9000 --max-rps 0 --concurrency 120
```
The input may be a plain `.csv` or a `.csv.zst`.
Verification runs are **resumable**: any `PuzzleId` already present in `--out`
is skipped. So interrupt with Ctrl-C or check new puzzles from an updated
database at any time.
## Output
The `--out` CSV has columns `PuzzleId,PGN,CliCommand`. The latter are both empty
for valid puzzles.
Example excerpt:
```
0ddFb,,
0d6GM,,
0bGDG,[FEN "6rk/7p/2p1r3/5P1R/5Q1K/3B4/2P3qP/q7 b - - 2 37"] 37... Qe5 38. Rxh7+ Kxh7 39. fxe6+ Rg6 40. Bxg6+ Qxg6 41. Qxe5 Qxc2 42. e7 { NOT_UNIQUE:loss@9 },puzzle issue 0bGDG puzzle-tb-check:208fca81:NOT_UNIQUE:loss@9
0cVwQ,,
```
Extract `CliCommand` column:
```sh
tail -n +2 report.csv | cut -d, -f3 | grep .
```
Sample some random rejected puzzles as PGN for studying on Lichess:
```sh
tail -n +2 report.csv | awk -F, '$2 != ""' | shuf -n 64 | awk -F, '{print "[Site \"https://lichess.org/training/" $1 "\"]" $2 " *\n"}'
```
## Rejection reasons
| Code | Meaning |
|---|---|
| `NOT_WINNING:` | the played move is known not to win |
| `WIN_FRUSTRATED:` | the played move is only a frustrated win (`blessed-loss`) |
| `NOT_UNIQUE:` | a different move also wins/holds |
| `WRONG_MOVE:` | a different move wins/holds while the played is not known to |
| `EQUALITY_HAS_WIN:` | an `equality` puzzle where a winning move exists |
| `EQUALITY_NOT_DRAW:` | an `equality` puzzle whose played move does not draw |
| `DTM_MISMATCH:` | a `mateInX` puzzle whose DTM does not match the expected mate distance (exact for mateIn1–4; a lower bound for mateIn5 = "5 or more") |
## Tablebase coverage
A position is *verifiable* if it is directly covered (≤7 pieces via Syzygy, or an
8-piece **op1** position) or is a ≤9-piece boundary position with a capture into
covered territory (9→8-op1, or 8→7). The verifier checks every verifiable
puzzler-to-move position and ignores the rest; a puzzle with no verifiable
position is skipped.
## Unconditional vs. frustrated wins, the 50-move rule, and unknowns
`maybe-win`/`syzygy-win` (and their losing variants) are treated as
**unconditional** wins and losses, respectively.
`cursed-win`/`blessed-loss` are **frustrated** wins/losses under the 50-move rule.
We would reject a supposedly winning puzzle if the played move is a frustrated win.
For ambiguity, generally a frustrated win would prevent an unconditional win
from being considered unique. A puzzle with a winning move and a frustrated win
would not be rejected.
However, before the first capture, the puzzler can't
know the 50-move counter, so we conservatively reject puzzles with
frustrated alternative moves to avoid confusion.
Rejection is otherwise **evidence-based**: only *positive known* tablebase facts
reject a puzzle. An `unknown` move itself is not enough to reject a puzzle —
but incomplete information can still suffice (two known winning moves prove non-uniqueness
regardless of unknown moves). The precise category `` is always recorded in each
rejection reason.
## Generative AI usage
Considerable portions of this program were generated by AI.
## License
GPL-3.0-or-later. See [LICENSE](LICENSE).