pgn-postmortem 0.1.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/LICENSE +21 -0
- pgn_postmortem-0.1.0/PKG-INFO +327 -0
- pgn_postmortem-0.1.0/README.md +316 -0
- pgn_postmortem-0.1.0/pgn_postmortem/__init__.py +39 -0
- pgn_postmortem-0.1.0/pgn_postmortem/__main__.py +5 -0
- pgn_postmortem-0.1.0/pgn_postmortem/analysis.py +288 -0
- pgn_postmortem-0.1.0/pgn_postmortem/cli.py +159 -0
- pgn_postmortem-0.1.0/pgn_postmortem/collection.py +400 -0
- pgn_postmortem-0.1.0/pgn_postmortem/site.py +1544 -0
- pgn_postmortem-0.1.0/pgn_postmortem/static/history.js +363 -0
- pgn_postmortem-0.1.0/pgn_postmortem.egg-info/PKG-INFO +327 -0
- pgn_postmortem-0.1.0/pgn_postmortem.egg-info/SOURCES.txt +30 -0
- pgn_postmortem-0.1.0/pgn_postmortem.egg-info/dependency_links.txt +1 -0
- pgn_postmortem-0.1.0/pgn_postmortem.egg-info/entry_points.txt +2 -0
- pgn_postmortem-0.1.0/pgn_postmortem.egg-info/requires.txt +1 -0
- pgn_postmortem-0.1.0/pgn_postmortem.egg-info/top_level.txt +1 -0
- pgn_postmortem-0.1.0/pyproject.toml +38 -0
- pgn_postmortem-0.1.0/setup.cfg +4 -0
- pgn_postmortem-0.1.0/tests/test_analysis.py +91 -0
- pgn_postmortem-0.1.0/tests/test_boards.py +177 -0
- pgn_postmortem-0.1.0/tests/test_cli.py +97 -0
- pgn_postmortem-0.1.0/tests/test_collection.py +193 -0
- pgn_postmortem-0.1.0/tests/test_history.py +259 -0
- pgn_postmortem-0.1.0/tests/test_library_analysis.py +181 -0
- pgn_postmortem-0.1.0/tests/test_lichess_links.py +246 -0
- pgn_postmortem-0.1.0/tests/test_openings.py +36 -0
- pgn_postmortem-0.1.0/tests/test_outcome_swings.py +392 -0
- pgn_postmortem-0.1.0/tests/test_pgn_io.py +41 -0
- pgn_postmortem-0.1.0/tests/test_presumed_results.py +269 -0
- pgn_postmortem-0.1.0/tests/test_publish.py +68 -0
- pgn_postmortem-0.1.0/tests/test_quiz.py +355 -0
- pgn_postmortem-0.1.0/tests/test_site.py +511 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Diego Amicabile
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pgn-postmortem
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Turn a folder of PGN chess games into browsable Markdown post-mortems of every blunder, analyzed locally with Stockfish.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: chess==1.11.2
|
|
10
|
+
Dynamic: license-file
|
|
11
|
+
|
|
12
|
+
# pgn-postmortem
|
|
13
|
+
|
|
14
|
+
[](https://github.com/diegoami/pgn-postmortem/actions/workflows/ci.yml)
|
|
15
|
+
[](LICENSE)
|
|
16
|
+

|
|
17
|
+
|
|
18
|
+
Turn a folder of PGN chess games into a set of Markdown post-mortems you can browse on GitHub. Every
|
|
19
|
+
blunder a chosen player made gets a board diagram, the move the engine preferred, how the mistake
|
|
20
|
+
should have been punished, and an opening-theory breakdown showing where the game left known lines.
|
|
21
|
+
|
|
22
|
+
Everything runs locally with [Stockfish](https://stockfishchess.org/), and the output is plain Markdown
|
|
23
|
+
and SVG. There's no account and no server, and the site lives in git.
|
|
24
|
+
|
|
25
|
+
**[→ Live demo](https://diegoami.github.io/pgn-postmortem/)**: five classic games, from Chigorin–Steinitz
|
|
26
|
+
(1892) to Carlsen–Anand (2014). The same pages are also [browsable on GitHub](examples/docs/index.md).
|
|
27
|
+
|
|
28
|
+
<table>
|
|
29
|
+
<tr>
|
|
30
|
+
<td width="50%"><img src="examples/docs/games/1/blunder_4_move32w.svg" alt="Position before 32. Bb4"></td>
|
|
31
|
+
<td>
|
|
32
|
+
|
|
33
|
+
### Move 32. Bb4 by Mikhail Chigorin (Blunder)
|
|
34
|
+
|
|
35
|
+
**Moves since the previous diagram**: 29. Ne6+ Kf6 30. Re7 Rge2 31. d5 Rcd2
|
|
36
|
+
|
|
37
|
+
**Better was:** 32. Rxb7 Bh5 33. Rb3 Rxd5 34. Nf4 Rxd6 35. Nxh5+ Ke7 +-
|
|
38
|
+
|
|
39
|
+
**Best continuation:** 32... Rxh2+ 33. Kg1 Rdg2# -+
|
|
40
|
+
|
|
41
|
+
<sub>Excerpt from [game 1](examples/docs/games/1.md): World Championship 1892, round 23. Chigorin was
|
|
42
|
+
winning, then walked into mate in two.</sub>
|
|
43
|
+
|
|
44
|
+
</td>
|
|
45
|
+
</tr>
|
|
46
|
+
</table>
|
|
47
|
+
|
|
48
|
+
## Features
|
|
49
|
+
|
|
50
|
+
- **Independent engine analysis.** Stockfish re-evaluates every position itself. Any annotations the
|
|
51
|
+
PGN already carries are ignored, since site exports (chess.com's, for example) are inconsistent about
|
|
52
|
+
what they attach where.
|
|
53
|
+
- **Judged by win probability, not raw centipawns.** Evals are converted to win % using
|
|
54
|
+
[lichess's logistic fit](https://lichess.org/page/accuracy), and a move is flagged by how many win %
|
|
55
|
+
points it threw away (default thresholds are lichess's own: 10 / 20 / 30 for Inaccuracy / Mistake /
|
|
56
|
+
Blunder). Going from mate-in-4 to mate-in-9 is a huge centipawn swing but costs nothing, so it isn't
|
|
57
|
+
flagged. The same swing in an equal position is.
|
|
58
|
+
- **Two engine lines per mistake.** *Better was* is what should have been played. *Best continuation*
|
|
59
|
+
is how the mistake should have been punished, which is useful when the real opponent missed it.
|
|
60
|
+
- **Opening theory.** Each game is matched against the ~3,800 named lines in
|
|
61
|
+
[lichess-org/chess-openings](https://github.com/lichess-org/chess-openings). The page shows the
|
|
62
|
+
position where the game left known theory, who left it, and which named lines were still available
|
|
63
|
+
at that point.
|
|
64
|
+
- **Any player, any collection.** Filter to one player (`--player yourname`) to review your own games,
|
|
65
|
+
or use `--player '*'` to annotate both sides of master games.
|
|
66
|
+
- **Standard, reusable output.** The analyzed PGNs are ordinary PGN files: an eval comment on every
|
|
67
|
+
move, standard NAGs (`$2`/`$4`/`$6`), and engine lines as variations ending in position symbols (`±`,
|
|
68
|
+
`-+`, ...). They load into any chess GUI.
|
|
69
|
+
- **Incremental and deterministic.** Only new games are sent to Stockfish. Pages are fully regenerated
|
|
70
|
+
on each run and the output is byte-identical, which the test suite checks against `examples/`.
|
|
71
|
+
|
|
72
|
+
## Quickstart
|
|
73
|
+
|
|
74
|
+
Requires Python 3.11+ and a `stockfish` binary on your `PATH` (`apt install stockfish`,
|
|
75
|
+
`brew install stockfish`, ...).
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
git clone https://github.com/diegoami/pgn-postmortem.git && cd pgn-postmortem
|
|
79
|
+
python3 -m venv .venv
|
|
80
|
+
.venv/bin/pip install -r requirements.txt
|
|
81
|
+
cp .env.example .env # preconfigured for the bundled examples/
|
|
82
|
+
scripts/update_games.sh # analyze new games, then regenerate docs/
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### With your own games
|
|
86
|
+
|
|
87
|
+
1. Edit `.env` and set `CHESS_PLAYER` to your username as it appears in the PGN `White`/`Black`
|
|
88
|
+
headers, and `CHESS_DATA_DIR` to a directory of your own.
|
|
89
|
+
2. Put your games in `$CHESS_DATA_DIR/daily_games/` as `1.pgn`, `2.pgn`, ..., **one game per file**.
|
|
90
|
+
3. Run `scripts/update_games.sh`, then open `$CHESS_DATA_DIR/docs/index.md`, or push the directory to
|
|
91
|
+
GitHub to browse it there.
|
|
92
|
+
|
|
93
|
+
A handy setup is to keep your games in a **separate repository** (`daily_games/`, `analyzed_games/`,
|
|
94
|
+
`docs/`), so this one stays pure tooling and your games repo can be published independently, for
|
|
95
|
+
example with GitHub Pages.
|
|
96
|
+
|
|
97
|
+
## How it works
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
daily_games/*.pgn ──analyze_games.py──▶ analyzed_games/*.pgn ──publish_games.py──▶ docs/
|
|
101
|
+
(your PGNs) (Stockfish) (evals + NAGs + (Markdown) index.md
|
|
102
|
+
engine lines) games/<id>.md + SVGs
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**[`scripts/analyze_games.py`](scripts/analyze_games.py)** keeps only the mainline of each game,
|
|
106
|
+
evaluates every position, and writes a clean annotated copy. For each flagged move it attaches two
|
|
107
|
+
variations, each capped at `--pv-length` half-moves:
|
|
108
|
+
|
|
109
|
+
```pgn
|
|
110
|
+
32. Bb4 $4 { -999.98 } ( 32. Rxb7 Bh5 33. Rb3 Rxd5 34. Nf4 Rxd6 35. Nxh5+ Ke7 $18 )
|
|
111
|
+
32... Rxh2+ { -999.99 } ( 32... Rxh2+ 33. Kg1 Rdg2# $19 ) 0-1
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The first variation hangs off the position *before* the move (**Better was**). The second hangs off
|
|
115
|
+
the position *after* it (**Best continuation**). Games already present in `analyzed_games/` are
|
|
116
|
+
skipped; `--force` redoes them all.
|
|
117
|
+
|
|
118
|
+
**[`scripts/publish_games.py`](scripts/publish_games.py)** reads the analyzed PGNs and writes:
|
|
119
|
+
|
|
120
|
+
| Output | Contents |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `docs/index.md` | One row per game: date, players, result, opening, blunder and inaccuracy counts |
|
|
123
|
+
| `docs/games/<id>.md` | Game info, opening-theory section, every flagged move in order (inaccuracies folded under `<details>`), full PGN |
|
|
124
|
+
| `docs/games/<id>/*.svg` | Board before each flagged move, with the move played as a red arrow, plus the opening-deviation position |
|
|
125
|
+
|
|
126
|
+
A "blunder" on these pages means a move by `--player` rated Mistake (`$2`), Blunder (`$4`) or Miss
|
|
127
|
+
(`$9`). Note that the opening matching only covers *named* lines in the dataset, so a "deviation" means
|
|
128
|
+
"no longer in a named line", not "a bad move".
|
|
129
|
+
|
|
130
|
+
Each source file must contain **exactly one game**. Every output path is derived from the filename, and
|
|
131
|
+
python-chess silently reads only the first game of a multi-game file, so a file with more than one game
|
|
132
|
+
is skipped with a warning rather than losing games without a trace.
|
|
133
|
+
|
|
134
|
+
## Configuration
|
|
135
|
+
|
|
136
|
+
Every setting can be passed as a flag or set in `.env` (see [`.env.example`](.env.example)). A flag
|
|
137
|
+
always wins.
|
|
138
|
+
|
|
139
|
+
| Flag | `.env` variable | Default | Meaning |
|
|
140
|
+
|---|---|---|---|
|
|
141
|
+
| `--player` | `CHESS_PLAYER` | *(required)* | Whose moves to review (case-insensitive), or `*` for both sides |
|
|
142
|
+
| `--data-dir` | `CHESS_DATA_DIR` | repo root | Directory containing `daily_games/`; outputs are written next to it |
|
|
143
|
+
| `--time` | `ANALYSIS_TIME` | `0.3` | Seconds of search per position |
|
|
144
|
+
| `--depth` | `ANALYSIS_DEPTH` | — | Fixed search depth instead of a time limit |
|
|
145
|
+
| `--inaccuracy-threshold` | `ANALYSIS_INACCURACY_PCT` | `10` | Win % points lost to flag an Inaccuracy |
|
|
146
|
+
| `--mistake-threshold` | `ANALYSIS_MISTAKE_PCT` | `20` | … a Mistake |
|
|
147
|
+
| `--blunder-threshold` | `ANALYSIS_BLUNDER_PCT` | `30` | … a Blunder |
|
|
148
|
+
| `--pv-length` | — | `8` | Max half-moves per engine line |
|
|
149
|
+
| `--force` | — | off | Re-analyze games already in `analyzed_games/` |
|
|
150
|
+
| `--source` | — | `daily_games` | Which directory `publish_games.py` reads (`update_games.sh` uses `analyzed_games`) |
|
|
151
|
+
|
|
152
|
+
## The library (in progress)
|
|
153
|
+
|
|
154
|
+
`pgn-postmortem` is growing into a Python library that turns a player's PGN collections into a
|
|
155
|
+
Wikipedia-style site and an EPUB book ([`ROADMAP.md`](ROADMAP.md), F-1). So far it reads,
|
|
156
|
+
analyzes and writes the site: multi-game files, directories and glob patterns in, the player's games
|
|
157
|
+
kept once each with every source comment, variation and NAG stripped, then a parallel Stockfish pass
|
|
158
|
+
that writes standard `[%eval]` comments and skips games it has already analyzed, then a static site
|
|
159
|
+
with an article for every game and a quiz of the player's own mistakes.
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
.venv/bin/pip install -e .
|
|
163
|
+
pgn-postmortem read 'collections/**/*.pgn' --player "Ada Example" --alias adaex --out games/
|
|
164
|
+
pgn-postmortem analyze games/ --out analyzed/ --workers 4
|
|
165
|
+
pgn-postmortem site analyzed/ --player "Ada Example" --alias adaex --out site/
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from pgn_postmortem import Collection
|
|
170
|
+
|
|
171
|
+
games = Collection.read(["collections/**/*.pgn"], player="Ada Example", aliases=["adaex"])
|
|
172
|
+
games.analyze("analyzed/", depth=18, workers=4)
|
|
173
|
+
analyzed = Collection.read("analyzed/", player="Ada Example", aliases=["adaex"], keep_analysis=True)
|
|
174
|
+
analyzed.build_site("site/", title="Games of Ada Example") # the quiz is for the names read with
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Quote a `**` pattern so the library, not the shell, expands it. The scripts above are unchanged by it.
|
|
178
|
+
|
|
179
|
+
Every game the library writes is named `<date>-<id>.pgn` and carries two headers of its own:
|
|
180
|
+
|
|
181
|
+
- `PostmortemId`, the game's content id;
|
|
182
|
+
- `PostmortemAnalysis`, only on analyzed games: the engine and search limit, e.g. `Stockfish 16, depth 18`.
|
|
183
|
+
`analyze` skips a game when a file in its output directory carries that game's id and this header,
|
|
184
|
+
so games that `read --out` only stripped are still analyzed, even in the same directory. In turn,
|
|
185
|
+
`read --out` leaves such a file alone, so reading new games into an analysis directory keeps the
|
|
186
|
+
analysis already there. The skip ignores which engine and search limit the header records: to redo
|
|
187
|
+
a game with other settings (a deeper search, a newer Stockfish), delete its file and run `analyze`
|
|
188
|
+
again.
|
|
189
|
+
|
|
190
|
+
Two copies of a game count as one when they have the same start position, moves, result and date.
|
|
191
|
+
The players' names are not compared, so a game exported under two of your names or aliases is kept
|
|
192
|
+
once. A `FEN` header that spells out the standard starting position counts the same as none. The
|
|
193
|
+
`Result` and `Date` headers are compared exactly as written, which has two consequences:
|
|
194
|
+
|
|
195
|
+
- Copies with a missing, partial or differently written date (`2019.??.??` and `2019.03.14`, or
|
|
196
|
+
`2019.3.14`) are kept twice. The same goes for copies with different results (`1-0` and `*`).
|
|
197
|
+
- Two different games with the same moves and result on the same day are kept as one. That can
|
|
198
|
+
happen with a short trap, or an agreed draw in a well-known line, played against two opponents.
|
|
199
|
+
|
|
200
|
+
The exact rule is in [`pgn_postmortem/collection.py`](pgn_postmortem/collection.py).
|
|
201
|
+
|
|
202
|
+
### The site
|
|
203
|
+
|
|
204
|
+
`pgn-postmortem site` writes `index.html` (the games by year), one `games/<date>-<id>.html` article
|
|
205
|
+
per game, one stylesheet and, for a player, `quiz.html` (below). Open `index.html` in a browser, or copy the folder to a phone: every link
|
|
206
|
+
within the site is relative, nothing loads from the network, and the colours follow the system's light or dark mode.
|
|
207
|
+
The only links out are the two lichess links below, which open in a new tab only when you tap them.
|
|
208
|
+
The only JavaScript is the optional reading history below; without it the pages read the same. Each
|
|
209
|
+
article has an infobox with the final position, a lead paragraph, the moves, a conclusion and the
|
|
210
|
+
PGN, all in template prose.
|
|
211
|
+
|
|
212
|
+
For an analyzed game the moves carry notes (`?!` inaccuracy, `?` mistake, `??` blunder), and each
|
|
213
|
+
**critical moment** gets a diagram and a question, "What would you play?", with the answer hidden
|
|
214
|
+
until you tap it. A critical moment is a move that cost its side at least 20 points of winning chances
|
|
215
|
+
(a mistake or a blunder), computed from the `[%eval]` comments that `analyze` wrote, with the same win
|
|
216
|
+
percentage and thresholds that grade the moves.
|
|
217
|
+
|
|
218
|
+
A move that changed the expected result is a critical moment too, even when it cost less. After each
|
|
219
|
+
move the position is *White winning* (60% or more for White), *Black winning* (40% or less) or *level*.
|
|
220
|
+
A move counts when it made that worse for its side (winning to level, level to losing, or winning to
|
|
221
|
+
losing), cost its side at least 10 points (the inaccuracy threshold in use), and the analysis shows a
|
|
222
|
+
better move there: an engine line that `analyze` stored at that position starts with another move. Only
|
|
223
|
+
the stored analysis is read, so nothing is analyzed again. Its note says how the expected result changed, for example "an inaccuracy that turned a
|
|
224
|
+
level game into a losing one". The bands are `build_site(..., outcome_bands=(40, 60))`: the lower one
|
|
225
|
+
above 0 and below 50, the upper one above 50 and below 100.
|
|
226
|
+
|
|
227
|
+
**Lichess links.** Under the final position in the infobox, "Open this game on lichess" opens the
|
|
228
|
+
game on lichess's free analysis board: `https://lichess.org/analysis/pgn/<moves>`, the moves in SAN
|
|
229
|
+
without move numbers and without `+` and `#`, URL-encoded (`e4%20e5%20Nf3`). A game that starts from a
|
|
230
|
+
set-up position (a `FEN` header other than the standard start) or has no moves has no such link.
|
|
231
|
+
Inside each hidden answer, "Analyze this position on lichess" opens the question's position there
|
|
232
|
+
(`https://lichess.org/analysis/<FEN>`, the FEN's spaces written as `_`), so you can put an engine on
|
|
233
|
+
it; it is in the answer because an engine on the question gives the answer away. Both need the
|
|
234
|
+
network and lichess.org; the rest of the site does not.
|
|
235
|
+
|
|
236
|
+
Games that have not been analyzed, or whose only
|
|
237
|
+
evaluations came from their source, still get an article, without notes or questions. Pass the games
|
|
238
|
+
and their analysis together (`site games/ analyzed/`) and the analyzed copy of each game is used.
|
|
239
|
+
Building again into the same folder removes the pages it wrote before for games that are no longer in
|
|
240
|
+
the collection; a file it did not write is never touched.
|
|
241
|
+
|
|
242
|
+
A game whose result was not recorded (`Result "*"`, or no `Result` header) still gets one. If the game
|
|
243
|
+
ended in checkmate, stalemate or insufficient material, the board decides it. Otherwise, if the game
|
|
244
|
+
was analyzed, the final position decides it: a win for the side with at least 70% winning chances (a
|
|
245
|
+
forced mate counts as 100%), a draw otherwise. The 70 is `build_site(..., presume_threshold=70)`, from
|
|
246
|
+
55 to 95. Failing both, the article says the result was not recorded. The result is shown like a
|
|
247
|
+
recorded one, in the infobox, the lead, after the moves, in the conclusion and in the index. The
|
|
248
|
+
source's `*` is left as it is: the article's PGN section shows it, and the game's id, and therefore its
|
|
249
|
+
file name, is computed from that `*` result, not from the result shown.
|
|
250
|
+
|
|
251
|
+
**The quiz.** With `--player` or `--alias` (in the library, the names the collection was read with,
|
|
252
|
+
or `build_site(..., player=..., aliases=[...])`), the index links at its top to `quiz.html`: every
|
|
253
|
+
critical moment where the player was the one to move, across the player's games, from the move that
|
|
254
|
+
lost the most winning chances down. Each line shows its rank, the move played, the points it lost
|
|
255
|
+
and the game (date and opponent), and links straight to the question in its article; the page has no
|
|
256
|
+
diagrams and no answers, so it stays small on a phone. The opponents' critical moments are not in
|
|
257
|
+
it; in a game where both sides are the player's names, both sides' are. Ties go by the index's
|
|
258
|
+
order, then the article's file name, then move order. A player without a critical moment of their
|
|
259
|
+
own gets a quiz page that says so. Without a player there is no quiz page and no link to it, and a
|
|
260
|
+
rebuild without one removes the `quiz.html` an earlier build wrote (never one it did not write).
|
|
261
|
+
|
|
262
|
+
**The reading history.** Every page carries a small script, inline, that remembers in the reader's
|
|
263
|
+
own browser which games were opened and which answers were revealed. The index then shows a
|
|
264
|
+
**Recently viewed** list (the latest 10 games, newest first), a mark on each game already opened with
|
|
265
|
+
how many of its answers were revealed ("viewed · 2/4 answers"), and a **Clear history** button, which
|
|
266
|
+
asks before it removes anything. On the quiz page, a question whose answer was revealed is marked
|
|
267
|
+
"answered", and the order stays the same; the index's button clears these marks too. The history is kept in the browser's `localStorage`: per device and
|
|
268
|
+
per browser, never shared and never sent anywhere; the script loads nothing. It can be lost: when the
|
|
269
|
+
reader clears the browser's data, in Safari after 7 days without a visit, and for one game when its id
|
|
270
|
+
changes (a corrected `Date` or `Result`). With scripts off, or where the browser gives no storage (some
|
|
271
|
+
private windows, some browsers for `file://` pages), the history stays hidden and the pages read the
|
|
272
|
+
same. The EPUB will carry no script.
|
|
273
|
+
|
|
274
|
+
Browser storage is shared by every page of one origin: all the GitHub Pages sites of one user, and in
|
|
275
|
+
some browsers every `file://` page. So each site stores its history under a **site key**, derived from
|
|
276
|
+
its title by default; two sites with the same title on one origin share a history. Set a key of your
|
|
277
|
+
own with `--site-key KEY` (or `build_site(..., site_key="ada-otb")`): 1 to 64 ASCII letters, digits,
|
|
278
|
+
`.`, `_` or `-`. `--no-history` (`build_site(..., history=False)`) writes the pages without the script,
|
|
279
|
+
the history section, the quiz's marks and the `data-` attributes.
|
|
280
|
+
|
|
281
|
+
The site of the test fixture is committed in [`tests/golden/site/`](tests/golden/site/index.html), and
|
|
282
|
+
the same site without the reading history in
|
|
283
|
+
[`tests/golden/site-no-history/`](tests/golden/site-no-history/index.html).
|
|
284
|
+
|
|
285
|
+
## Claude Code skill
|
|
286
|
+
|
|
287
|
+
[`.claude/skills/publish-games`](.claude/skills/publish-games/SKILL.md) is a
|
|
288
|
+
[Claude Code](https://claude.com/claude-code) skill for this workflow. Say "I added new games, publish
|
|
289
|
+
them" and it runs the pipeline using your `.env`.
|
|
290
|
+
|
|
291
|
+
## Development
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
.venv/bin/pip install -r requirements-dev.txt -e .
|
|
295
|
+
.venv/bin/pytest # unit tests, a golden-file test against examples/, and the Stockfish tests
|
|
296
|
+
.venv/bin/ruff check .
|
|
297
|
+
node --test 'tests/js/*.test.mjs' # the reading-history script (Node 22 or newer, no npm packages)
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Quote the glob: Node expands it, and a bare `tests/js/` fails on Node 21 and newer.
|
|
301
|
+
|
|
302
|
+
If you intentionally change the page output, regenerate the golden files:
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
.venv/bin/python scripts/publish_games.py --player '*' --data-dir examples --source analyzed_games
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
and for the library's site (the fixture's analysis is committed, so this needs no Stockfish):
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
.venv/bin/python -m pgn_postmortem site tests/fixtures/site/analyzed --player "Ada Example" --alias adaex --alias "Example, Ada" --out tests/golden/site
|
|
312
|
+
.venv/bin/python -m pgn_postmortem site tests/fixtures/site/analyzed --player "Ada Example" --alias adaex --alias "Example, Ada" --no-history --out tests/golden/site-no-history
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Credits
|
|
316
|
+
|
|
317
|
+
- [python-chess](https://github.com/niklasf/python-chess) for PGN parsing, the engine protocol and the
|
|
318
|
+
SVG boards, and the piece set by Colin M.L. Burnett that the site's diagrams use
|
|
319
|
+
- [Stockfish](https://stockfishchess.org/) for the analysis
|
|
320
|
+
- [lichess-org/chess-openings](https://github.com/lichess-org/chess-openings) (CC0) for the opening
|
|
321
|
+
names, bundled in `data/openings/`
|
|
322
|
+
- [lichess's accuracy page](https://lichess.org/page/accuracy) for the win % model and default
|
|
323
|
+
thresholds
|
|
324
|
+
|
|
325
|
+
## License
|
|
326
|
+
|
|
327
|
+
[MIT](LICENSE)
|