pgn-postmortem 0.1.0__tar.gz → 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/PKG-INFO +88 -3
- {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/README.md +87 -2
- {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/pgn_postmortem/__init__.py +45 -1
- {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/pgn_postmortem/analysis.py +47 -4
- pgn_postmortem-2.0.0/pgn_postmortem/cli.py +304 -0
- {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/pgn_postmortem/collection.py +46 -5
- pgn_postmortem-2.0.0/pgn_postmortem/results.py +446 -0
- pgn_postmortem-2.0.0/pgn_postmortem/selection.py +220 -0
- {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/pgn_postmortem/site.py +216 -56
- pgn_postmortem-2.0.0/pgn_postmortem/workspace.py +373 -0
- {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/pgn_postmortem.egg-info/PKG-INFO +88 -3
- {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/pgn_postmortem.egg-info/SOURCES.txt +11 -1
- pgn_postmortem-2.0.0/tests/test_book_demo.py +69 -0
- pgn_postmortem-2.0.0/tests/test_demo.py +151 -0
- {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/tests/test_history.py +4 -1
- {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/tests/test_publish.py +45 -0
- {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/tests/test_quiz.py +10 -1
- pgn_postmortem-2.0.0/tests/test_result_correction.py +493 -0
- pgn_postmortem-2.0.0/tests/test_result_docs.py +105 -0
- pgn_postmortem-2.0.0/tests/test_result_policies.py +394 -0
- pgn_postmortem-2.0.0/tests/test_selection.py +104 -0
- pgn_postmortem-2.0.0/tests/test_workspace.py +210 -0
- pgn_postmortem-0.1.0/pgn_postmortem/cli.py +0 -159
- {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/LICENSE +0 -0
- {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/pgn_postmortem/__main__.py +0 -0
- {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/pgn_postmortem/static/history.js +0 -0
- {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/pgn_postmortem.egg-info/dependency_links.txt +0 -0
- {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/pgn_postmortem.egg-info/entry_points.txt +0 -0
- {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/pgn_postmortem.egg-info/requires.txt +0 -0
- {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/pgn_postmortem.egg-info/top_level.txt +0 -0
- {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/pyproject.toml +0 -0
- {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/setup.cfg +0 -0
- {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/tests/test_analysis.py +0 -0
- {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/tests/test_boards.py +0 -0
- {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/tests/test_cli.py +0 -0
- {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/tests/test_collection.py +0 -0
- {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/tests/test_library_analysis.py +0 -0
- {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/tests/test_lichess_links.py +0 -0
- {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/tests/test_openings.py +0 -0
- {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/tests/test_outcome_swings.py +0 -0
- {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/tests/test_pgn_io.py +0 -0
- {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/tests/test_presumed_results.py +0 -0
- {pgn_postmortem-0.1.0 → pgn_postmortem-2.0.0}/tests/test_site.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pgn-postmortem
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: Turn a folder of PGN chess games into browsable Markdown post-mortems of every blunder, analyzed locally with Stockfish.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -22,8 +22,18 @@ should have been punished, and an opening-theory breakdown showing where the gam
|
|
|
22
22
|
Everything runs locally with [Stockfish](https://stockfishchess.org/), and the output is plain Markdown
|
|
23
23
|
and SVG. There's no account and no server, and the site lives in git.
|
|
24
24
|
|
|
25
|
-
**[→ Live demo](https://diegoami.github.io/pgn-postmortem/)**:
|
|
26
|
-
(1892) to Carlsen–Anand (2014)
|
|
25
|
+
**[→ Live demo](https://diegoami.github.io/pgn-postmortem/)**: the library's Wikipedia-style site of five
|
|
26
|
+
classic games, from Chigorin–Steinitz (1892) to Carlsen–Anand (2014), with a "what would you play?"
|
|
27
|
+
question at each critical moment. The Markdown pages this README describes are at the demo's
|
|
28
|
+
[`/markdown/`](https://diegoami.github.io/pgn-postmortem/markdown/), and are also
|
|
29
|
+
[browsable on GitHub](examples/docs/index.md).
|
|
30
|
+
|
|
31
|
+
**[→ A real collection](https://diegoami.github.io/chessgamescollection/)**: the author's own games, built with
|
|
32
|
+
this library as three separate collections (source in
|
|
33
|
+
[diegoami/chessgamescollection](https://github.com/diegoami/chessgamescollection)):
|
|
34
|
+
[over-the-board games](https://diegoami.github.io/chessgamescollection/otb/) (148),
|
|
35
|
+
[correspondence games](https://diegoami.github.io/chessgamescollection/correspondence/) (1,068) and
|
|
36
|
+
[standard games](https://diegoami.github.io/chessgamescollection/standard/) (264, played online).
|
|
27
37
|
|
|
28
38
|
<table>
|
|
29
39
|
<tr>
|
|
@@ -176,6 +186,25 @@ analyzed.build_site("site/", title="Games of Ada Example") # the quiz is for th
|
|
|
176
186
|
|
|
177
187
|
Quote a `**` pattern so the library, not the shell, expands it. The scripts above are unchanged by it.
|
|
178
188
|
|
|
189
|
+
### F-1.3 book demo
|
|
190
|
+
|
|
191
|
+
`examples/book_demo/source/capablanca.pgn` is a five-game Capablanca subset from
|
|
192
|
+
[PGN Mentor's Capablanca collection](https://www.pgnmentor.com/players/Capablanca.zip),
|
|
193
|
+
listed on its [free PGN downloads page](https://www.pgnmentor.com/files.html). The files are
|
|
194
|
+
available for download completely free; the source page does not publish a separate open-source
|
|
195
|
+
license, so the README does not claim one. The checked-in subset is the 1906 Raubitschek game and
|
|
196
|
+
Marshall rounds 1, 2, 5 and 6 from 1909. The Stockfish 19, depth-22 files under
|
|
197
|
+
`examples/book_demo/analyzed/` are derived/analyzed data from that source. This repository's
|
|
198
|
+
distribution policy limits this demo data to non-commercial use. The originally selected Caissabase source remains documented in
|
|
199
|
+
`ROADMAP.md`; its archived page is readable but its 630 MB download was not captured by Wayback.
|
|
200
|
+
|
|
201
|
+
Build the separate local book demo from the committed analysis without running Stockfish:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
pgn-postmortem site examples/book_demo/analyzed --player "Capablanca, Jose Raul" \
|
|
205
|
+
--title "José Raúl Capablanca" --out examples/book_demo/site
|
|
206
|
+
```
|
|
207
|
+
|
|
179
208
|
Every game the library writes is named `<date>-<id>.pgn` and carries two headers of its own:
|
|
180
209
|
|
|
181
210
|
- `PostmortemId`, the game's content id;
|
|
@@ -282,6 +311,62 @@ The site of the test fixture is committed in [`tests/golden/site/`](tests/golden
|
|
|
282
311
|
the same site without the reading history in
|
|
283
312
|
[`tests/golden/site-no-history/`](tests/golden/site-no-history/index.html).
|
|
284
313
|
|
|
314
|
+
### Correcting recorded results
|
|
315
|
+
|
|
316
|
+
**Optional: nothing is corrected unless you ask.** Reading, `analyze`, `site` and `workspace` never touch
|
|
317
|
+
a result by default.
|
|
318
|
+
|
|
319
|
+
A source's `Result` is sometimes wrong, a residue of computer analysis: the side with the won position
|
|
320
|
+
recorded as the loser, a draw as a defeat. For games the library analyzed, `correct-results` reads the
|
|
321
|
+
final position (the board first: checkmate, stalemate, insufficient material; else the final `[%eval]`,
|
|
322
|
+
a win for a side with at least 70% winning chances, a draw otherwise) and writes the result it gives into
|
|
323
|
+
the game's `Result` header, keeping the source's value in an `OriginalResult` header. No engine runs.
|
|
324
|
+
|
|
325
|
+
**The caveat, honestly:** a decisive result in a level position can be genuine (a time forfeit, a
|
|
326
|
+
resignation, an adjudication), and the library cannot tell. So you choose how far it may go and which
|
|
327
|
+
games it must leave alone, every change is listed with its reason, and the original is always kept.
|
|
328
|
+
|
|
329
|
+
| policy | what it writes |
|
|
330
|
+
|---|---|
|
|
331
|
+
| `all` (default) | the final position's result whenever there is one: a recorded win in a level position becomes a draw |
|
|
332
|
+
| `contradictions` | only a recorded win or loss reversed, or a recorded draw made decisive; never a decisive result made a draw because the position is level, and `*` is left alone |
|
|
333
|
+
| `unrecorded` | only a missing or `*` result (the rule above, written to the file) |
|
|
334
|
+
| `board` | only what the board itself decides (checkmate, stalemate, insufficient material) |
|
|
335
|
+
|
|
336
|
+
```console tested
|
|
337
|
+
$ pgn-postmortem correct-results analyzed --policy contradictions --dry-run --explain
|
|
338
|
+
Policy: contradictions, threshold 70.
|
|
339
|
+
2013-01-01-30390dba02.pgn: 0-1 -> 1-0 (Ada Example vs. Bert Sample, corrected)
|
|
340
|
+
2013-01-02-545b079385.pgn: kept: policy (1-0, the final position says 1/2-1/2)
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
`--threshold` (55 to 95, one value) sets the winning chances that make a win, and `--skip-header
|
|
344
|
+
NAME=REGEX` (repeatable) leaves alone the games whose header matches, for example
|
|
345
|
+
`--skip-header 'Termination=(?i)forfeit|time'`. `--dry-run` lists and writes nothing; `--explain` also
|
|
346
|
+
says why each game was kept (`kept: agrees`, `kept: policy`, `kept: skipped`, `kept: no verdict`).
|
|
347
|
+
Running again changes nothing; a narrower policy restores what it would not correct. The game's id and
|
|
348
|
+
file name do not change (the id is computed from the source's result), so analysis is not redone and the
|
|
349
|
+
analyzed copy still matches its source. For new games, `pgn-postmortem analyze ... --correct-results`
|
|
350
|
+
(with `--result-policy`, `--result-threshold`, `--result-skip-header`) does it as each game is analyzed.
|
|
351
|
+
In Python, `correct_results("analyzed/", policy="contradictions", skip=...)`,
|
|
352
|
+
`Collection.correct_results(policy=...)` (in memory) and `analyze(..., correct_results=True)`. In a
|
|
353
|
+
workspace manifest, per collection, in memory only (no file is rewritten):
|
|
354
|
+
|
|
355
|
+
```toml tested
|
|
356
|
+
[[collection]]
|
|
357
|
+
slug = "otb"
|
|
358
|
+
title = "Over-the-board games"
|
|
359
|
+
inputs = ["analyzed"]
|
|
360
|
+
correct_results = "contradictions"
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
**For anyone scripting against the output:** `correct-results` and `analyze --correct-results` now
|
|
364
|
+
print a `Policy: NAME, threshold T.` line first (the change lines and the summary are as before).
|
|
365
|
+
|
|
366
|
+
An article of a corrected game shows a "Source result" row and says the source recorded something else.
|
|
367
|
+
The full reference, with a worked example of each policy, the skip rules, undoing a correction and the
|
|
368
|
+
library API, is in [`docs/result-correction.md`](docs/result-correction.md).
|
|
369
|
+
|
|
285
370
|
## Claude Code skill
|
|
286
371
|
|
|
287
372
|
[`.claude/skills/publish-games`](.claude/skills/publish-games/SKILL.md) is a
|
|
@@ -11,8 +11,18 @@ should have been punished, and an opening-theory breakdown showing where the gam
|
|
|
11
11
|
Everything runs locally with [Stockfish](https://stockfishchess.org/), and the output is plain Markdown
|
|
12
12
|
and SVG. There's no account and no server, and the site lives in git.
|
|
13
13
|
|
|
14
|
-
**[→ Live demo](https://diegoami.github.io/pgn-postmortem/)**:
|
|
15
|
-
(1892) to Carlsen–Anand (2014)
|
|
14
|
+
**[→ Live demo](https://diegoami.github.io/pgn-postmortem/)**: the library's Wikipedia-style site of five
|
|
15
|
+
classic games, from Chigorin–Steinitz (1892) to Carlsen–Anand (2014), with a "what would you play?"
|
|
16
|
+
question at each critical moment. The Markdown pages this README describes are at the demo's
|
|
17
|
+
[`/markdown/`](https://diegoami.github.io/pgn-postmortem/markdown/), and are also
|
|
18
|
+
[browsable on GitHub](examples/docs/index.md).
|
|
19
|
+
|
|
20
|
+
**[→ A real collection](https://diegoami.github.io/chessgamescollection/)**: the author's own games, built with
|
|
21
|
+
this library as three separate collections (source in
|
|
22
|
+
[diegoami/chessgamescollection](https://github.com/diegoami/chessgamescollection)):
|
|
23
|
+
[over-the-board games](https://diegoami.github.io/chessgamescollection/otb/) (148),
|
|
24
|
+
[correspondence games](https://diegoami.github.io/chessgamescollection/correspondence/) (1,068) and
|
|
25
|
+
[standard games](https://diegoami.github.io/chessgamescollection/standard/) (264, played online).
|
|
16
26
|
|
|
17
27
|
<table>
|
|
18
28
|
<tr>
|
|
@@ -165,6 +175,25 @@ analyzed.build_site("site/", title="Games of Ada Example") # the quiz is for th
|
|
|
165
175
|
|
|
166
176
|
Quote a `**` pattern so the library, not the shell, expands it. The scripts above are unchanged by it.
|
|
167
177
|
|
|
178
|
+
### F-1.3 book demo
|
|
179
|
+
|
|
180
|
+
`examples/book_demo/source/capablanca.pgn` is a five-game Capablanca subset from
|
|
181
|
+
[PGN Mentor's Capablanca collection](https://www.pgnmentor.com/players/Capablanca.zip),
|
|
182
|
+
listed on its [free PGN downloads page](https://www.pgnmentor.com/files.html). The files are
|
|
183
|
+
available for download completely free; the source page does not publish a separate open-source
|
|
184
|
+
license, so the README does not claim one. The checked-in subset is the 1906 Raubitschek game and
|
|
185
|
+
Marshall rounds 1, 2, 5 and 6 from 1909. The Stockfish 19, depth-22 files under
|
|
186
|
+
`examples/book_demo/analyzed/` are derived/analyzed data from that source. This repository's
|
|
187
|
+
distribution policy limits this demo data to non-commercial use. The originally selected Caissabase source remains documented in
|
|
188
|
+
`ROADMAP.md`; its archived page is readable but its 630 MB download was not captured by Wayback.
|
|
189
|
+
|
|
190
|
+
Build the separate local book demo from the committed analysis without running Stockfish:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
pgn-postmortem site examples/book_demo/analyzed --player "Capablanca, Jose Raul" \
|
|
194
|
+
--title "José Raúl Capablanca" --out examples/book_demo/site
|
|
195
|
+
```
|
|
196
|
+
|
|
168
197
|
Every game the library writes is named `<date>-<id>.pgn` and carries two headers of its own:
|
|
169
198
|
|
|
170
199
|
- `PostmortemId`, the game's content id;
|
|
@@ -271,6 +300,62 @@ The site of the test fixture is committed in [`tests/golden/site/`](tests/golden
|
|
|
271
300
|
the same site without the reading history in
|
|
272
301
|
[`tests/golden/site-no-history/`](tests/golden/site-no-history/index.html).
|
|
273
302
|
|
|
303
|
+
### Correcting recorded results
|
|
304
|
+
|
|
305
|
+
**Optional: nothing is corrected unless you ask.** Reading, `analyze`, `site` and `workspace` never touch
|
|
306
|
+
a result by default.
|
|
307
|
+
|
|
308
|
+
A source's `Result` is sometimes wrong, a residue of computer analysis: the side with the won position
|
|
309
|
+
recorded as the loser, a draw as a defeat. For games the library analyzed, `correct-results` reads the
|
|
310
|
+
final position (the board first: checkmate, stalemate, insufficient material; else the final `[%eval]`,
|
|
311
|
+
a win for a side with at least 70% winning chances, a draw otherwise) and writes the result it gives into
|
|
312
|
+
the game's `Result` header, keeping the source's value in an `OriginalResult` header. No engine runs.
|
|
313
|
+
|
|
314
|
+
**The caveat, honestly:** a decisive result in a level position can be genuine (a time forfeit, a
|
|
315
|
+
resignation, an adjudication), and the library cannot tell. So you choose how far it may go and which
|
|
316
|
+
games it must leave alone, every change is listed with its reason, and the original is always kept.
|
|
317
|
+
|
|
318
|
+
| policy | what it writes |
|
|
319
|
+
|---|---|
|
|
320
|
+
| `all` (default) | the final position's result whenever there is one: a recorded win in a level position becomes a draw |
|
|
321
|
+
| `contradictions` | only a recorded win or loss reversed, or a recorded draw made decisive; never a decisive result made a draw because the position is level, and `*` is left alone |
|
|
322
|
+
| `unrecorded` | only a missing or `*` result (the rule above, written to the file) |
|
|
323
|
+
| `board` | only what the board itself decides (checkmate, stalemate, insufficient material) |
|
|
324
|
+
|
|
325
|
+
```console tested
|
|
326
|
+
$ pgn-postmortem correct-results analyzed --policy contradictions --dry-run --explain
|
|
327
|
+
Policy: contradictions, threshold 70.
|
|
328
|
+
2013-01-01-30390dba02.pgn: 0-1 -> 1-0 (Ada Example vs. Bert Sample, corrected)
|
|
329
|
+
2013-01-02-545b079385.pgn: kept: policy (1-0, the final position says 1/2-1/2)
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
`--threshold` (55 to 95, one value) sets the winning chances that make a win, and `--skip-header
|
|
333
|
+
NAME=REGEX` (repeatable) leaves alone the games whose header matches, for example
|
|
334
|
+
`--skip-header 'Termination=(?i)forfeit|time'`. `--dry-run` lists and writes nothing; `--explain` also
|
|
335
|
+
says why each game was kept (`kept: agrees`, `kept: policy`, `kept: skipped`, `kept: no verdict`).
|
|
336
|
+
Running again changes nothing; a narrower policy restores what it would not correct. The game's id and
|
|
337
|
+
file name do not change (the id is computed from the source's result), so analysis is not redone and the
|
|
338
|
+
analyzed copy still matches its source. For new games, `pgn-postmortem analyze ... --correct-results`
|
|
339
|
+
(with `--result-policy`, `--result-threshold`, `--result-skip-header`) does it as each game is analyzed.
|
|
340
|
+
In Python, `correct_results("analyzed/", policy="contradictions", skip=...)`,
|
|
341
|
+
`Collection.correct_results(policy=...)` (in memory) and `analyze(..., correct_results=True)`. In a
|
|
342
|
+
workspace manifest, per collection, in memory only (no file is rewritten):
|
|
343
|
+
|
|
344
|
+
```toml tested
|
|
345
|
+
[[collection]]
|
|
346
|
+
slug = "otb"
|
|
347
|
+
title = "Over-the-board games"
|
|
348
|
+
inputs = ["analyzed"]
|
|
349
|
+
correct_results = "contradictions"
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
**For anyone scripting against the output:** `correct-results` and `analyze --correct-results` now
|
|
353
|
+
print a `Policy: NAME, threshold T.` line first (the change lines and the summary are as before).
|
|
354
|
+
|
|
355
|
+
An article of a corrected game shows a "Source result" row and says the source recorded something else.
|
|
356
|
+
The full reference, with a worked example of each policy, the skip rules, undoing a correction and the
|
|
357
|
+
library API, is in [`docs/result-correction.md`](docs/result-correction.md).
|
|
358
|
+
|
|
274
359
|
## Claude Code skill
|
|
275
360
|
|
|
276
361
|
[`.claude/skills/publish-games`](.claude/skills/publish-games/SKILL.md) is a
|
|
@@ -18,16 +18,54 @@ The same steps from the command line are ``pgn-postmortem read``,
|
|
|
18
18
|
|
|
19
19
|
from pgn_postmortem.analysis import AnalysisReport, EngineFailure, Thresholds, analyze_games
|
|
20
20
|
from pgn_postmortem.collection import CollectedGame, Collection, ReadReport, find_pgn_files
|
|
21
|
+
from pgn_postmortem.results import (
|
|
22
|
+
POLICIES,
|
|
23
|
+
CorrectionReport,
|
|
24
|
+
GameDecision,
|
|
25
|
+
ResultChange,
|
|
26
|
+
check_policy,
|
|
27
|
+
correct_game,
|
|
28
|
+
correct_results,
|
|
29
|
+
decided_result,
|
|
30
|
+
header_skip,
|
|
31
|
+
)
|
|
32
|
+
from pgn_postmortem.selection import (
|
|
33
|
+
ChapterWeights,
|
|
34
|
+
SelectionFeatures,
|
|
35
|
+
SelectionOptions,
|
|
36
|
+
SelectionWeights,
|
|
37
|
+
select_chapters,
|
|
38
|
+
)
|
|
21
39
|
from pgn_postmortem.site import SiteReport, build_site, critical_moments, shown_result
|
|
40
|
+
from pgn_postmortem.workspace import (
|
|
41
|
+
CollectionProfile,
|
|
42
|
+
Workspace,
|
|
43
|
+
WorkspaceBuildError,
|
|
44
|
+
WorkspaceConfigError,
|
|
45
|
+
WorkspaceReport,
|
|
46
|
+
)
|
|
22
47
|
|
|
23
|
-
__version__ = "0.
|
|
48
|
+
__version__ = "2.0.0"
|
|
24
49
|
|
|
25
50
|
__all__ = [
|
|
26
51
|
"AnalysisReport",
|
|
27
52
|
"CollectedGame",
|
|
28
53
|
"Collection",
|
|
54
|
+
"CorrectionReport",
|
|
55
|
+
"ResultChange",
|
|
56
|
+
"GameDecision",
|
|
57
|
+
"POLICIES",
|
|
58
|
+
"check_policy",
|
|
59
|
+
"header_skip",
|
|
60
|
+
"correct_game",
|
|
61
|
+
"correct_results",
|
|
62
|
+
"decided_result",
|
|
29
63
|
"EngineFailure",
|
|
30
64
|
"ReadReport",
|
|
65
|
+
"ChapterWeights",
|
|
66
|
+
"SelectionFeatures",
|
|
67
|
+
"SelectionOptions",
|
|
68
|
+
"SelectionWeights",
|
|
31
69
|
"Thresholds",
|
|
32
70
|
"__version__",
|
|
33
71
|
"SiteReport",
|
|
@@ -36,4 +74,10 @@ __all__ = [
|
|
|
36
74
|
"critical_moments",
|
|
37
75
|
"find_pgn_files",
|
|
38
76
|
"shown_result",
|
|
77
|
+
"select_chapters",
|
|
78
|
+
"CollectionProfile",
|
|
79
|
+
"Workspace",
|
|
80
|
+
"WorkspaceBuildError",
|
|
81
|
+
"WorkspaceConfigError",
|
|
82
|
+
"WorkspaceReport",
|
|
39
83
|
]
|
|
@@ -45,7 +45,7 @@ from collections.abc import Callable, Iterable
|
|
|
45
45
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
46
46
|
from dataclasses import dataclass, field
|
|
47
47
|
from pathlib import Path
|
|
48
|
-
from typing import NamedTuple
|
|
48
|
+
from typing import TYPE_CHECKING, NamedTuple
|
|
49
49
|
|
|
50
50
|
import chess
|
|
51
51
|
import chess.engine
|
|
@@ -53,6 +53,9 @@ import chess.pgn
|
|
|
53
53
|
|
|
54
54
|
from pgn_postmortem.collection import ANALYSIS_HEADER, CollectedGame, analyzed_ids, format_game
|
|
55
55
|
|
|
56
|
+
if TYPE_CHECKING:
|
|
57
|
+
from pgn_postmortem.results import ResultChange
|
|
58
|
+
|
|
56
59
|
FALLBACK_ENGINE_PATH = "/usr/games/stockfish" # where Debian and Ubuntu install it, off the default PATH
|
|
57
60
|
|
|
58
61
|
MATE_SCORE = 100000
|
|
@@ -86,6 +89,7 @@ class AnalysisReport:
|
|
|
86
89
|
analyzed: int = 0
|
|
87
90
|
skipped: int = 0
|
|
88
91
|
written: list[Path] = field(default_factory=list)
|
|
92
|
+
corrections: list[ResultChange] = field(default_factory=list) # with ``correct_results``: each result rewritten
|
|
89
93
|
|
|
90
94
|
|
|
91
95
|
def default_engine_path() -> str:
|
|
@@ -216,6 +220,10 @@ def analyze_games(
|
|
|
216
220
|
engine_path: str | None = None,
|
|
217
221
|
pv_plies: int = DEFAULT_PV_PLIES,
|
|
218
222
|
thresholds: Thresholds = LICHESS_THRESHOLDS,
|
|
223
|
+
correct_results: bool = False,
|
|
224
|
+
presume_threshold: float | None = None,
|
|
225
|
+
result_policy: str | None = None,
|
|
226
|
+
result_skip: Callable[[chess.pgn.Game], bool] | None = None,
|
|
219
227
|
progress: Callable[[int, int, CollectedGame], None] | None = None,
|
|
220
228
|
) -> AnalysisReport:
|
|
221
229
|
"""Analyze every game not yet in ``out_dir`` into ``out_dir/<date>-<id>.pgn``.
|
|
@@ -225,7 +233,35 @@ def analyze_games(
|
|
|
225
233
|
processes run side by side (0: one per CPU). No engine is started when
|
|
226
234
|
there is nothing to analyze. ``progress(done, total, game)`` is called
|
|
227
235
|
after each game.
|
|
236
|
+
|
|
237
|
+
With ``correct_results``, each game analyzed now has its ``Result``
|
|
238
|
+
corrected from its final position before it is written
|
|
239
|
+
(``pgn_postmortem.results``, ROADMAP.md F-14; ``presume_threshold`` is the
|
|
240
|
+
winning chances in percent, 70 by default, 55 to 95, checked before
|
|
241
|
+
anything is written or any engine started). ``result_policy`` (one of
|
|
242
|
+
``pgn_postmortem.results.POLICIES``, ``all`` by default) says which
|
|
243
|
+
corrections are made and ``result_skip(game) -> bool`` leaves a game's
|
|
244
|
+
result as it is (see ``pgn_postmortem.results``); giving either without
|
|
245
|
+
``correct_results`` raises ``ValueError``. A game already analyzed
|
|
246
|
+
into ``out_dir`` is not touched: ``pgn_postmortem.results.correct_results``
|
|
247
|
+
corrects those without an engine.
|
|
228
248
|
"""
|
|
249
|
+
from pgn_postmortem.results import (
|
|
250
|
+
DEFAULT_POLICY,
|
|
251
|
+
PRESUME_THRESHOLD,
|
|
252
|
+
check_policy,
|
|
253
|
+
check_presume_threshold,
|
|
254
|
+
correct_game,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
result_threshold = PRESUME_THRESHOLD if presume_threshold is None else presume_threshold
|
|
258
|
+
if correct_results or presume_threshold is not None:
|
|
259
|
+
check_presume_threshold(result_threshold)
|
|
260
|
+
if result_policy is not None:
|
|
261
|
+
check_policy(result_policy)
|
|
262
|
+
if not correct_results and (result_policy is not None or result_skip is not None):
|
|
263
|
+
raise ValueError("result_policy and result_skip only apply with correct_results=True")
|
|
264
|
+
policy = result_policy or DEFAULT_POLICY
|
|
229
265
|
out_dir = Path(out_dir)
|
|
230
266
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
231
267
|
done_ids = analyzed_ids(out_dir)
|
|
@@ -239,7 +275,7 @@ def analyze_games(
|
|
|
239
275
|
workers = max(1, min(workers or os.cpu_count() or 1, len(todo)))
|
|
240
276
|
engines: queue.Queue[chess.engine.SimpleEngine] = queue.Queue()
|
|
241
277
|
|
|
242
|
-
def run(item: CollectedGame) -> Path:
|
|
278
|
+
def run(item: CollectedGame) -> tuple[Path, ResultChange | None]:
|
|
243
279
|
engine = engines.get()
|
|
244
280
|
try:
|
|
245
281
|
analyzed = analyze_game(engine, limit, item.game, item.id, pv_plies, thresholds)
|
|
@@ -247,11 +283,14 @@ def analyze_games(
|
|
|
247
283
|
raise EngineFailure(f"the engine failed on {item.origin}: {err}") from err
|
|
248
284
|
finally:
|
|
249
285
|
engines.put(engine)
|
|
286
|
+
change = None
|
|
287
|
+
if correct_results:
|
|
288
|
+
change = correct_game(analyzed, result_threshold, item.filename, policy=policy, skip=result_skip)
|
|
250
289
|
path = out_dir / item.filename
|
|
251
290
|
partial = path.with_name(path.name + ".partial")
|
|
252
291
|
partial.write_text(format_game(analyzed), encoding="utf-8")
|
|
253
292
|
os.replace(partial, path) # a run that is interrupted never leaves a half-written game behind
|
|
254
|
-
return path
|
|
293
|
+
return path, change
|
|
255
294
|
|
|
256
295
|
engine_path = engine_path or default_engine_path()
|
|
257
296
|
try:
|
|
@@ -269,7 +308,10 @@ def analyze_games(
|
|
|
269
308
|
futures = {pool.submit(run, item): item for item in todo}
|
|
270
309
|
try:
|
|
271
310
|
for done, future in enumerate(as_completed(futures), start=1):
|
|
272
|
-
|
|
311
|
+
path, change = future.result()
|
|
312
|
+
report.written.append(path)
|
|
313
|
+
if change:
|
|
314
|
+
report.corrections.append(change)
|
|
273
315
|
report.analyzed += 1
|
|
274
316
|
if progress:
|
|
275
317
|
progress(done, len(todo), futures[future])
|
|
@@ -285,4 +327,5 @@ def analyze_games(
|
|
|
285
327
|
except ENGINE_ERRORS:
|
|
286
328
|
pass # it already died
|
|
287
329
|
report.written.sort()
|
|
330
|
+
report.corrections.sort(key=lambda change: change.path)
|
|
288
331
|
return report
|