{"id":51573042,"url":"https://github.com/niklasf/lichess-puzzle-tb-check","last_synced_at":"2026-07-10T21:04:20.480Z","repository":{"id":362359467,"uuid":"1257588607","full_name":"niklasf/lichess-puzzle-tb-check","owner":"niklasf","description":"Verify Lichess puzzles against tablebase data","archived":false,"fork":false,"pushed_at":"2026-06-03T21:01:49.000Z","size":2261,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-28T15:00:51.621Z","etag":null,"topics":["chess","lichess","syzygy","tablebases"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/niklasf.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-02T20:28:59.000Z","updated_at":"2026-06-03T21:01:54.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/niklasf/lichess-puzzle-tb-check","commit_stats":null,"previous_names":["niklasf/lichess-puzzle-tb-check"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/niklasf/lichess-puzzle-tb-check","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/niklasf%2Flichess-puzzle-tb-check","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/niklasf%2Flichess-puzzle-tb-check/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/niklasf%2Flichess-puzzle-tb-check/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/niklasf%2Flichess-puzzle-tb-check/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/niklasf","download_url":"https://codeload.github.com/niklasf/lichess-puzzle-tb-check/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/niklasf%2Flichess-puzzle-tb-check/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35343136,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-10T02:00:06.465Z","response_time":60,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["chess","lichess","syzygy","tablebases"],"created_at":"2026-07-10T21:04:19.544Z","updated_at":"2026-07-10T21:04:20.474Z","avatar_url":"https://github.com/niklasf.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Lichess puzzle-tb-check\n\nVerify Lichess puzzles against tablebase data.\n\nReads a [Lichess puzzle database](https://database.lichess.org/#puzzles) CSV,\nlooks at the positions that have tablebase coverage, probes the\n[lila-tablebase](https://github.com/lichess-org/lila-tablebase) HTTP API, and\nrecords — per puzzle — whether every puzzler move is the **unique winning move**\n(or the **unique drawing move** for `equality` puzzles), plus a DTM check for\n`mateInX` puzzles.\n\n## Requirements\n\nPython 3.14+ (uses the stdlib `compression.zstd` module), `uv`.\n\n## Usage\n\nAgainst public API (slow):\n\n```sh\nuv run puzzle-tb-check lichess_db_puzzle.csv.zst --out report.csv\n```\n\nAgainst a local instance:\n\n```sh\nuv run puzzle-tb-check lichess_db_puzzle.csv.zst --out report.csv --endpoint http://127.0.0.1:9000 --max-rps 0 --concurrency 120\n```\n\nThe input may be a plain `.csv` or a `.csv.zst`.\n\nVerification runs are **resumable**: any `PuzzleId` already present in `--out`\nis skipped. So interrupt with Ctrl-C or check new puzzles from an updated\ndatabase at any time.\n\n## Output\n\nThe `--out` CSV has columns `PuzzleId,PGN,CliCommand`. The latter are both empty\nfor valid puzzles.\n\nExample excerpt:\n\n```\n0ddFb,,\n0d6GM,,\n0bGDG,[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\n0cVwQ,,\n```\n\nExtract `CliCommand` column:\n\n```sh\ntail -n +2 report.csv | cut -d, -f3 | grep .\n```\n\nSample some random rejected puzzles as PGN for studying on Lichess:\n\n```sh\ntail -n +2 report.csv | awk -F, '$2 != \"\"' | shuf -n 64 | awk -F, '{print \"[Site \\\"https://lichess.org/training/\" $1 \"\\\"]\" $2 \" *\\n\"}'\n```\n\n## Rejection reasons\n\n| Code | Meaning |\n|---|---|\n| `NOT_WINNING:\u003ccat\u003e` | the played move is known not to win |\n| `WIN_FRUSTRATED:\u003ccat\u003e` | the played move is only a frustrated win (`blessed-loss`) |\n| `NOT_UNIQUE:\u003ccat\u003e` | a different move also wins/holds |\n| `WRONG_MOVE:\u003ccat\u003e` | a different move wins/holds while the played is not known to |\n| `EQUALITY_HAS_WIN:\u003ccat\u003e` | an `equality` puzzle where a winning move exists |\n| `EQUALITY_NOT_DRAW:\u003ccat\u003e` | an `equality` puzzle whose played move does not draw |\n| `DTM_MISMATCH:\u003cdtm\u003e` | a `mateInX` puzzle whose DTM does not match the expected mate distance (exact for mateIn1–4; a lower bound for mateIn5 = \"5 or more\") |\n\n## Tablebase coverage\n\nA position is *verifiable* if it is directly covered (≤7 pieces via Syzygy, or an\n8-piece **op1** position) or is a ≤9-piece boundary position with a capture into\ncovered territory (9→8-op1, or 8→7). The verifier checks every verifiable\npuzzler-to-move position and ignores the rest; a puzzle with no verifiable\nposition is skipped.\n\n## Unconditional vs. frustrated wins, the 50-move rule, and unknowns\n\n`maybe-win`/`syzygy-win` (and their losing variants) are treated as\n**unconditional** wins and losses, respectively.\n\n`cursed-win`/`blessed-loss` are **frustrated** wins/losses under the 50-move rule.\nWe would reject a supposedly winning puzzle if the played move is a frustrated win.\n\nFor ambiguity, generally a frustrated win would prevent an unconditional win\nfrom being considered unique. A puzzle with a winning move and a frustrated win\nwould not be rejected.\n\nHowever, before the first capture, the puzzler can't\nknow the 50-move counter, so we conservatively reject puzzles with\nfrustrated alternative moves to avoid confusion.\n\nRejection is otherwise **evidence-based**: only *positive known* tablebase facts\nreject a puzzle. An `unknown` move itself is not enough to reject a puzzle —\nbut incomplete information can still suffice (two known winning moves prove non-uniqueness\nregardless of unknown moves). The precise category `\u003ccat\u003e` is always recorded in each\nrejection reason.\n\n## Generative AI usage\n\nConsiderable portions of this program were generated by AI.\n\n## License\n\nGPL-3.0-or-later. See [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fniklasf%2Flichess-puzzle-tb-check","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fniklasf%2Flichess-puzzle-tb-check","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fniklasf%2Flichess-puzzle-tb-check/lists"}