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.
Files changed (32) hide show
  1. pgn_postmortem-0.1.0/LICENSE +21 -0
  2. pgn_postmortem-0.1.0/PKG-INFO +327 -0
  3. pgn_postmortem-0.1.0/README.md +316 -0
  4. pgn_postmortem-0.1.0/pgn_postmortem/__init__.py +39 -0
  5. pgn_postmortem-0.1.0/pgn_postmortem/__main__.py +5 -0
  6. pgn_postmortem-0.1.0/pgn_postmortem/analysis.py +288 -0
  7. pgn_postmortem-0.1.0/pgn_postmortem/cli.py +159 -0
  8. pgn_postmortem-0.1.0/pgn_postmortem/collection.py +400 -0
  9. pgn_postmortem-0.1.0/pgn_postmortem/site.py +1544 -0
  10. pgn_postmortem-0.1.0/pgn_postmortem/static/history.js +363 -0
  11. pgn_postmortem-0.1.0/pgn_postmortem.egg-info/PKG-INFO +327 -0
  12. pgn_postmortem-0.1.0/pgn_postmortem.egg-info/SOURCES.txt +30 -0
  13. pgn_postmortem-0.1.0/pgn_postmortem.egg-info/dependency_links.txt +1 -0
  14. pgn_postmortem-0.1.0/pgn_postmortem.egg-info/entry_points.txt +2 -0
  15. pgn_postmortem-0.1.0/pgn_postmortem.egg-info/requires.txt +1 -0
  16. pgn_postmortem-0.1.0/pgn_postmortem.egg-info/top_level.txt +1 -0
  17. pgn_postmortem-0.1.0/pyproject.toml +38 -0
  18. pgn_postmortem-0.1.0/setup.cfg +4 -0
  19. pgn_postmortem-0.1.0/tests/test_analysis.py +91 -0
  20. pgn_postmortem-0.1.0/tests/test_boards.py +177 -0
  21. pgn_postmortem-0.1.0/tests/test_cli.py +97 -0
  22. pgn_postmortem-0.1.0/tests/test_collection.py +193 -0
  23. pgn_postmortem-0.1.0/tests/test_history.py +259 -0
  24. pgn_postmortem-0.1.0/tests/test_library_analysis.py +181 -0
  25. pgn_postmortem-0.1.0/tests/test_lichess_links.py +246 -0
  26. pgn_postmortem-0.1.0/tests/test_openings.py +36 -0
  27. pgn_postmortem-0.1.0/tests/test_outcome_swings.py +392 -0
  28. pgn_postmortem-0.1.0/tests/test_pgn_io.py +41 -0
  29. pgn_postmortem-0.1.0/tests/test_presumed_results.py +269 -0
  30. pgn_postmortem-0.1.0/tests/test_publish.py +68 -0
  31. pgn_postmortem-0.1.0/tests/test_quiz.py +355 -0
  32. 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
+ [![CI](https://github.com/diegoami/pgn-postmortem/actions/workflows/ci.yml/badge.svg)](https://github.com/diegoami/pgn-postmortem/actions/workflows/ci.yml)
15
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
16
+ ![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)
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)