chess-game-analyzer 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 (34) hide show
  1. chess_game_analyzer-0.1.0/LICENSE +21 -0
  2. chess_game_analyzer-0.1.0/PKG-INFO +489 -0
  3. chess_game_analyzer-0.1.0/README.md +452 -0
  4. chess_game_analyzer-0.1.0/chess_analyzer/__init__.py +3 -0
  5. chess_game_analyzer-0.1.0/chess_analyzer/accuracy.py +245 -0
  6. chess_game_analyzer-0.1.0/chess_analyzer/classify.py +355 -0
  7. chess_game_analyzer-0.1.0/chess_analyzer/cli.py +248 -0
  8. chess_game_analyzer-0.1.0/chess_analyzer/engines.py +835 -0
  9. chess_game_analyzer-0.1.0/chess_analyzer/jobs.py +156 -0
  10. chess_game_analyzer-0.1.0/chess_analyzer/library.py +250 -0
  11. chess_game_analyzer-0.1.0/chess_analyzer/live.py +546 -0
  12. chess_game_analyzer-0.1.0/chess_analyzer/openings.py +192 -0
  13. chess_game_analyzer-0.1.0/chess_analyzer/paths.py +82 -0
  14. chess_game_analyzer-0.1.0/chess_analyzer/position.py +271 -0
  15. chess_game_analyzer-0.1.0/chess_analyzer/review.py +425 -0
  16. chess_game_analyzer-0.1.0/chess_analyzer/server.py +781 -0
  17. chess_game_analyzer-0.1.0/chess_analyzer/sources/__init__.py +140 -0
  18. chess_game_analyzer-0.1.0/chess_analyzer/sources/chesscom.py +310 -0
  19. chess_game_analyzer-0.1.0/chess_analyzer/sources/common.py +206 -0
  20. chess_game_analyzer-0.1.0/chess_analyzer/sources/lichess.py +270 -0
  21. chess_game_analyzer-0.1.0/chess_analyzer/tcn.py +120 -0
  22. chess_game_analyzer-0.1.0/chess_analyzer/web/app.js +2469 -0
  23. chess_game_analyzer-0.1.0/chess_analyzer/web/index.html +337 -0
  24. chess_game_analyzer-0.1.0/chess_analyzer/web/style.css +448 -0
  25. chess_game_analyzer-0.1.0/chess_game_analyzer.egg-info/PKG-INFO +489 -0
  26. chess_game_analyzer-0.1.0/chess_game_analyzer.egg-info/SOURCES.txt +32 -0
  27. chess_game_analyzer-0.1.0/chess_game_analyzer.egg-info/dependency_links.txt +1 -0
  28. chess_game_analyzer-0.1.0/chess_game_analyzer.egg-info/entry_points.txt +2 -0
  29. chess_game_analyzer-0.1.0/chess_game_analyzer.egg-info/requires.txt +11 -0
  30. chess_game_analyzer-0.1.0/chess_game_analyzer.egg-info/top_level.txt +1 -0
  31. chess_game_analyzer-0.1.0/pyproject.toml +57 -0
  32. chess_game_analyzer-0.1.0/setup.cfg +4 -0
  33. chess_game_analyzer-0.1.0/tests/test_analyzer.py +675 -0
  34. chess_game_analyzer-0.1.0/tests/test_layout.py +210 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shubhro Dev
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,489 @@
1
+ Metadata-Version: 2.4
2
+ Name: chess-game-analyzer
3
+ Version: 0.1.0
4
+ Summary: Review any chess game -- Lichess, Chess.com or your own PGN -- with a local engine.
5
+ Author-email: Shubhro Dev <shubhro2004@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/spearb0lt/Lichess-Essentials
8
+ Project-URL: Repository, https://github.com/spearb0lt/Lichess-Essentials
9
+ Project-URL: Issues, https://github.com/spearb0lt/Lichess-Essentials/issues
10
+ Project-URL: Documentation, https://github.com/spearb0lt/Lichess-Essentials/blob/main/ChessAnalyzer/README.md
11
+ Keywords: chess,lichess,chess.com,pgn,stockfish,analysis,game-review
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Environment :: Web Environment
15
+ Classifier: Framework :: FastAPI
16
+ Classifier: Intended Audience :: End Users/Desktop
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Games/Entertainment :: Board Games
24
+ Requires-Python: >=3.10
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: chess>=1.11
28
+ Requires-Dist: requests>=2.31
29
+ Requires-Dist: fastapi>=0.110
30
+ Requires-Dist: uvicorn[standard]>=0.27
31
+ Requires-Dist: pydantic>=2.0
32
+ Provides-Extra: cloud
33
+ Requires-Dist: lichess-study-to-pdf>=0.1.0; extra == "cloud"
34
+ Provides-Extra: dev
35
+ Requires-Dist: pytest>=8.0; extra == "dev"
36
+ Dynamic: license-file
37
+
38
+ # Chess Analyzer
39
+
40
+ [![PyPI](https://img.shields.io/pypi/v/chess-game-analyzer?logo=pypi&logoColor=white)](https://pypi.org/project/chess-game-analyzer/)
41
+ [![Python](https://img.shields.io/pypi/pyversions/chess-game-analyzer)](https://pypi.org/project/chess-game-analyzer/)
42
+ [![Downloads](https://static.pepy.tech/badge/chess-game-analyzer)](https://pepy.tech/project/chess-game-analyzer)
43
+ [![Downloads](https://static.pepy.tech/badge/chess-game-analyzer/month)](https://pepy.tech/project/chess-game-analyzer)
44
+ [![License](https://img.shields.io/pypi/l/chess-game-analyzer)](LICENSE)
45
+
46
+
47
+ Review any chess game with an engine on your own machine — a Lichess game, a
48
+ Chess.com game, a PGN you pasted, or a position you typed. Accuracy, move
49
+ labels, an eval graph, the engine's best line at every point, and a live mode
50
+ that follows a game while it is still being played.
51
+
52
+ The two things it does that the sites do not: it works on **both** sites from
53
+ one place, and it tells you **why** it gave a move the label it gave it.
54
+
55
+ ```powershell
56
+ cd ChessAnalyzer
57
+ & "..\.lichess\Scripts\python.exe" -m chess_analyzer.cli serve # port 8779
58
+ ```
59
+
60
+ Then open <http://127.0.0.1:8779>.
61
+
62
+ ![A finished review of a Lichess game: three ranked engine lines above the board, a miss badge on the move played with a green arrow showing the engine's choice, the eval graph with opening, middlegame and endgame marked underneath, and the accuracy report on the right](https://raw.githubusercontent.com/spearb0lt/Lichess-Essentials/main/ChessAnalyzer/docs/review.png)
63
+
64
+ *Above: `54... h2`, the move this review calls the game's biggest turning point.
65
+ The strip over the board says `from the review`, so those three lines cost
66
+ nothing — the review already analysed this position to depth 44.*
67
+
68
+ ---
69
+
70
+ ## Install
71
+
72
+ ```bash
73
+ pip install chess-game-analyzer
74
+ chess-analyzer serve
75
+ ```
76
+
77
+ **Optional.** Lichess cloud-eval fallback: `pip install "chess-game-analyzer[cloud]"`.
78
+
79
+ Or take all five at once with `pip install lichess-essentials`. Installed this
80
+ way your files live in the usual per-user folder for your platform, and the
81
+ app prints the path in its startup banner. To run it from a checkout instead,
82
+ see [the repository README](https://github.com/spearb0lt/Lichess-Essentials/blob/main/README.md#setup-from-a-checkout).
83
+
84
+ ## Getting a game in
85
+
86
+ One box takes all of these — it works out which is which by looking at them,
87
+ not by asking you:
88
+
89
+ | Paste this | What happens |
90
+ |---|---|
91
+ | `https://lichess.org/Wi8IPxc3` | Fetched from Lichess. Public API, no token. |
92
+ | `https://www.chess.com/game/live/146864147370` | Fetched from Chess.com, **including a game still in progress**. |
93
+ | A PGN, from anywhere | Read as-is. A `Site` tag saying lichess.org does not make it a link. |
94
+ | A FEN | Becomes a position you can analyse and play from. |
95
+ | A bare game id | 8 characters is Lichess, a long number is Chess.com. |
96
+
97
+ Or use the sidebar: type a username on the **Lichess** or **Chess.com** tab
98
+ and get their recent games with the PGN already attached, so opening one costs
99
+ no second request.
100
+
101
+ ## The review
102
+
103
+ Pick a depth and press the button. Four presets, from ~10 seconds to a fixed
104
+ depth 22; a 90-ply game takes about 10 seconds on Quick and half a minute on
105
+ Standard.
106
+
107
+ <table>
108
+ <tr>
109
+ <td width="50%"><img src="docs/report.png" width="100%" alt="The review panel: accuracy for both players, centipawn loss, an ACPL-implied rating, how often each found the engine's move, per-phase accuracy, the opening name, the Chess.com-style label counts, the Lichess-scale counts, and the turning points ranked by what they cost"></td>
110
+ <td width="50%"><img src="docs/moves.png" width="100%" alt="The Moves panel: every move of the game with its label badge and its evaluation, White down the left and Black down the right"></td>
111
+ </tr>
112
+ <tr>
113
+ <td>The report. Two scales side by side, per-phase accuracy, and the turning points ranked by what they cost.</td>
114
+ <td>The move list. Every move wears its label, and the evaluation beside it is the one the review computed.</td>
115
+ </tr>
116
+ </table>
117
+
118
+ You get two scales side by side, on purpose:
119
+
120
+ **The Lichess scale** — inaccuracy, mistake, blunder at 10, 20 and 30
121
+ winning-chance points lost. Lichess publishes both these thresholds and the
122
+ accuracy formula, so the arithmetic here is theirs rather than an
123
+ approximation of it.
124
+
125
+ Checked against a game Lichess had already analysed
126
+ ([Wi8IPxc3](https://lichess.org/Wi8IPxc3)), at the Deep preset:
127
+
128
+ | | this app | lichess.org |
129
+ |---|---|---|
130
+ | White accuracy | 76.3% | 76% |
131
+ | Black accuracy | 53.6% | 60% |
132
+ | White blunders | 2 | 6 |
133
+
134
+ Accuracy lands where it should. **The blunder counts do not, and will not** —
135
+ those depend on what the engine actually saw, and a different engine at a
136
+ different depth disagrees about which moves were losing. Treat the counts as
137
+ this engine's opinion, and the accuracy as comparable. Both apps agree about
138
+ the move that mattered: the app independently flagged `68. Nf2` as throwing
139
+ away a forced mate, which is what Lichess says about it too.
140
+
141
+ **The Chess.com-style ladder** — brilliant, great, best, book, excellent,
142
+ good, inaccuracy, mistake, miss, blunder. Chess.com has never published its
143
+ criteria, so this cannot be a reimplementation and does not pretend to be.
144
+ What it is: the same idea, built from rules that are **written down and shown
145
+ in the app** — click *what do these mean?* in the review panel. When a label
146
+ surprises you, you can read why it fired instead of guessing.
147
+
148
+ <img src="docs/rules.png" width="640" alt="The what the labels mean dialog: every label from brilliant down to blunder with the rule that fires it written out, including the material threshold for brilliant and the winning-chance bands for the rest">
149
+
150
+ The rules that took the most tuning:
151
+
152
+ - **Brilliant** — the engine's top move, which gives up at least 1.8 pawns of
153
+ material that the engine's own line never wins back, and still leaves you at
154
+ 45% or better. Material is read off the engine's principal variation four
155
+ plies deep rather than from a hand-rolled exchange evaluator: if the engine's
156
+ best play leaves you a piece down and still says you are fine, you sacrificed
157
+ and it worked.
158
+ - **Great** — the top move, where the second-best is at least 15 points worse.
159
+ - Neither is ever given for a **recapture**, or once the game is decided past
160
+ 92%. Taking back the piece that was just taken beats the alternatives for
161
+ reasons that are not to your credit; without that rule, a third of every game
162
+ comes out "great".
163
+ - **Book** moves are excluded from accuracy and centipawn loss entirely, so
164
+ preparation is not scored as skill.
165
+
166
+ Also in the report: per-phase accuracy (opening / middlegame / endgame, with
167
+ the boundaries found from the position rather than a fixed move number),
168
+ average centipawn loss, how often you found the engine's move, the turning
169
+ points ranked by what they cost, and a rough ACPL-implied rating shown with its
170
+ formula attached — it is a fit, not a measurement, and it moves 200 points on
171
+ one blunder.
172
+
173
+ ## Reading it
174
+
175
+ - **The engine's ranked lines sit above the board**, in small text, and stay
176
+ there while the **Lines** pill in the top bar is lit — they follow whatever
177
+ position you are looking at rather than waiting to be asked. How many lines
178
+ and how long the engine gets are on the *Engine* tab. Click one to play it.
179
+
180
+ On a game you have reviewed these cost nothing: the review already analysed
181
+ every position several variations deep, so scrolling through it shows those
182
+ lines instantly, at the review's depth, and the strip says *from the review*
183
+ so you know which they are. Only the final position, which the review has no
184
+ row for, falls back to a live engine call.
185
+ - **The engine's preferred continuation sits beside the board controls**, as a
186
+ score chip and the line. On a reviewed move it is deliberately retrospective
187
+ — the line you could have played *instead* — because that is the question you
188
+ are asking while reading a review. Anywhere else it is the best line from
189
+ here.
190
+ - **Scroll the mouse wheel over the board** to step through the game — down
191
+ goes forward, up goes back. Every position is already in the browser, so this
192
+ is instant; it is the reason the board is drawn client-side rather than
193
+ fetched as an image. Trackpads work too: deltas are accumulated to a
194
+ threshold, so a flick is one move rather than ten. (The two sibling apps now
195
+ do this as well.)
196
+ - Arrow keys do the same; `f` flips; `Home`/`End` jump to the ends.
197
+ - Click the eval graph to jump to that moment.
198
+ - Each move wears its label as a badge on the destination square, and when you
199
+ did not play the engine's move, an arrow shows what it wanted.
200
+ - **Play any move on the board to explore.** The game itself is never touched:
201
+ your move becomes a variation, shown in the move list in brackets and a
202
+ lighter colour right after the move it replaces, the way Lichess and
203
+ Chess.com do it — `5...O-O (5...d5 6.Bb5 Bg4)`. Keep playing and it extends;
204
+ go back and try something else and you get a second variation beside the
205
+ first; try something inside a variation and it nests in its own brackets.
206
+ The engine follows you into them, so an exploratory move gets its own
207
+ evaluation. *Back to the game* returns to the mainline, *Clear variations*
208
+ throws them all away, and the review's labels and stored lines stay attached
209
+ to the moves that were actually played.
210
+
211
+ ## Live games
212
+
213
+ Five ways in, because the five situations are genuinely different.
214
+
215
+ | Mode | How it works |
216
+ |---|---|
217
+ | **Lichess** | A real push stream. `/api/stream/game/{id}` is public and sends a line per move — any game, yours or anyone's. Give it a game URL, an id, or just a username. |
218
+ | **Chess.com** | Polling `chess.com/callback/live/game/{id}` every two seconds and decoding the move list. **Undocumented** — see below. |
219
+ | **Follow along** | You click the moves as they are played. No network at all, so it works for a Chess.com blitz game, a stream you are watching, or a board in front of you. |
220
+ | **Arrange the board** | There is no URL and no PGN, only a position. Put the pieces where they are, say who is to move, and the engine evaluates it. See below. |
221
+ | **Paste PGN** | Paste, and re-paste as the game grows. Only ever moves forward: a short or scrambled paste cannot rewind the game. |
222
+
223
+ In every mode the eval bar and the engine's ranked lines keep up with the
224
+ current position, you can scroll back through the game without losing your
225
+ place when the players move, and **Save & review** freezes the game into the
226
+ library so you can run a full review on it.
227
+
228
+ ### Arranging a position
229
+
230
+ Pick **Arrange the board** and the board becomes an editor, outlined in green
231
+ so it is obvious that clicking it no longer plays moves. Choose a piece from
232
+ the palette and click squares; clicking the piece that is already there
233
+ removes it, and right-clicking any square clears it. **From the board** copies
234
+ whatever position you are already looking at, which beats building from empty
235
+ when you only need to move two pieces.
236
+
237
+ ![The board editor: the board outlined in green with a piece palette beside it, who is to move, the castling row, the FEN, and a line confirming the position is legal](https://raw.githubusercontent.com/spearb0lt/Lichess-Essentials/main/ChessAnalyzer/docs/setup.png)
238
+
239
+ *The castling row in that shot reads **none possible** on its own — no king and
240
+ rook are on their home squares in this position, so there is nothing to offer
241
+ and nothing left over from a previous arrangement to pass to the engine.*
242
+
243
+ Then say **White to play** or **Black to play**, and press *Analyse this
244
+ position*. From that point it behaves exactly like Follow along: the eval bar
245
+ and the engine's lines are live, and you click the moves as they happen.
246
+ **Save & review** turns it into a game in your library, with the arranged
247
+ position kept as the PGN's starting position.
248
+
249
+ Every click is validated, because most arrangements of pieces are not
250
+ positions. Two white kings, a pawn on the first rank, or Black in check while
251
+ White is to move cannot occur in a game, and an engine handed one either
252
+ refuses to start or produces a confident number about nothing. The editor says
253
+ which of those is wrong, in a sentence, before you can start.
254
+
255
+ Two details it fills in rather than asking about:
256
+
257
+ - **Castling rights** are offered only for a king and rook still on their home
258
+ squares — and a tick left over from a previous arrangement is dropped rather
259
+ than passed to the engine.
260
+ - **En passant** is offered only where a double pawn push could really just
261
+ have happened *and* the capture would be legal. (python-chess's
262
+ `has_legal_en_passant` is not enough on its own here: it asks whether a pawn
263
+ could capture *onto* the square and will say yes when there is no pawn there
264
+ to take. `status` is what checks a double push could have produced it. The
265
+ editor needs both, or it would offer you a square its own validator then
266
+ rejects.)
267
+
268
+ ### The honest part about Chess.com live games
269
+
270
+ Chess.com's documented public API cannot see a live game. `/pub/player/{u}/games`
271
+ is documented as "games in progress" and returns **daily/correspondence games
272
+ only** — a blitz game you are playing right now is not in it, and no documented
273
+ endpoint has it.
274
+
275
+ `chess.com/callback/live/game/{id}` does have it. It is undocumented, which
276
+ means it is not covered by Chess.com's API terms and can change or vanish
277
+ without notice. This app therefore treats it as optional: if it stops
278
+ answering, the session keeps the moves it already has and tells you to switch
279
+ to Paste PGN, which always works.
280
+
281
+ That endpoint returns moves in **TCN**, Chess.com's own two-characters-per-ply
282
+ encoding, which nothing in python-chess speaks. The decoder in
283
+ [`tcn.py`](https://github.com/spearb0lt/Lichess-Essentials/blob/main/ChessAnalyzer/chess_analyzer/tcn.py) is verified against 40 real games that
284
+ Chess.com shipped as *both* TCN and PGN — every move and every final position
285
+ matches, promotions and en passant included. The fixture is in the repository
286
+ so the check runs with no network.
287
+
288
+ **Also**: Chess.com sits behind Cloudflare and returns a 403 challenge page to
289
+ any request without a `User-Agent` header. Not a rate limit and not a ban —
290
+ just a missing header, which is a confusing hour to spend if you do not know.
291
+
292
+ ## Engines
293
+
294
+ The app uses whatever Stockfish it can find first — including the one in the
295
+ sibling app's `Lichess-Study-to-PDF/engine/` folder, so following this
296
+ repository's setup instructions means never being asked to download a second
297
+ copy.
298
+
299
+ Click the engine pill to see the picker. It reads each project's own GitHub
300
+ releases, so a new Stockfish appears the day it ships:
301
+
302
+ <img src="docs/engines.png" width="620" alt="The engine picker: the Stockfish already found on this machine at the top, then every Stockfish release available to download with its size and the CPU builds it ships, then Lc0">
303
+
304
+ - **Stockfish**, any recent version (~77 MB).
305
+ - **Lc0** (~24 MB) plus a network file. The **Maia** networks are the
306
+ interesting ones: they predict what a human *of a given rating* actually
307
+ plays rather than what is best. Use one to ask whether a move was findable —
308
+ never to judge one.
309
+
310
+ You can set the review engine and the analysis engine separately.
311
+
312
+ **About CPU builds.** Stockfish publishes one binary per instruction set —
313
+ `bmi2`, `avx2`, `avx512`, down to a plain `x86-64` — and running one your
314
+ processor cannot execute does not fail politely, it dies on an illegal
315
+ instruction. There is no portable way to read CPU feature flags from Python on
316
+ Windows, so the app does not guess: it downloads a build, **runs it**, waits
317
+ for it to answer `uci`, and on failure walks down to the next-safest build
318
+ automatically. What worked is remembered, so it happens at most once.
319
+
320
+ ## What is kept on disk
321
+
322
+ `ChessAnalyzer/games/` holds one JSON file per imported game — the record, the
323
+ PGN and the finished review — plus `positions.json`, a shared cache of analysed
324
+ positions keyed by position **and** engine settings. That cache is why
325
+ re-reviewing a game at the same settings is instant, and why a 0.1-second
326
+ answer never masquerades as a depth-22 one.
327
+
328
+ `ChessAnalyzer/data/openings.json` is the opening index, built once from
329
+ Lichess's own openings dataset (3,810 named positions, no token needed). It is
330
+ indexed by position rather than by move order, so a transposition into a named
331
+ line is still recognised.
332
+
333
+ Downloaded engines go in `ChessAnalyzer/engines/`. All three directories are
334
+ gitignored.
335
+
336
+ A Lichess API token is accepted in Settings and is **never written to disk** —
337
+ it lives in the process and dies with it. Nothing here needs one; every
338
+ endpoint the app uses is public. It only raises the rate limit for bulk
339
+ imports.
340
+
341
+ ## Command line
342
+
343
+ ```bash
344
+ python -m chess_analyzer.cli serve # the browser interface
345
+ python -m chess_analyzer.cli review <url|id|pgn> # review and print it
346
+ python -m chess_analyzer.cli review <url> --preset deep --depth 20
347
+ python -m chess_analyzer.cli engines # what is here, what is available
348
+ python -m chess_analyzer.cli engines --install stockfish:sf_18
349
+ python -m chess_analyzer.cli import <url> # add to the library
350
+ python -m chess_analyzer.cli library # list saved games
351
+ ```
352
+
353
+ ## Tests
354
+
355
+ ```bash
356
+ python -m pytest ChessAnalyzer/tests -q # 51 tests
357
+ node ChessAnalyzer/tests/test_variations.js # 10 more, if you have node
358
+ python ChessAnalyzer/tests/test_layout.py # 21 in a real browser
359
+ ```
360
+
361
+ No network and no engine required — a suite that needs both is a suite nobody
362
+ runs.
363
+
364
+ The second file covers the variation tree, which is browser code. Rather than
365
+ duplicating the logic, it lifts the functions out of the shipped `app.js` by
366
+ name and runs them against a `state` it controls; a copy would keep passing
367
+ after `app.js` changed underneath it, which is the one thing a test must not
368
+ do. (Checked by deliberately reintroducing the old behaviour, which failed
369
+ three of them.)
370
+
371
+ The third drives a real headless browser over the DevTools protocol, against a
372
+ server you have already started. It needs no new dependency — Edge or Chrome is
373
+ already on the machine and `websockets` came in with `uvicorn[standard]` — and
374
+ it skips if neither is there.
375
+
376
+ It exists because the app's worst bug could not be found by reading. The board
377
+ and the scrolling panel were resizing each other several times a second, so the
378
+ whole page shook; the stylesheet looked perfectly reasonable. One measurement
379
+ found it. It now checks that the board, the controls and the graph fit at seven
380
+ window sizes, that the board's size *settles* rather than drifting, that an
381
+ arriving evaluation does not move it, that trying a move leaves the game's
382
+ notation alone, and that a wheel notch and a trackpad flick each move exactly
383
+ one move.
384
+
385
+ ## Things worth knowing if you change this
386
+
387
+ **A warm engine stops the process from exiting.** python-chess runs each
388
+ engine's event loop on a *non-daemon* thread, and CPython joins non-daemon
389
+ threads *before* it runs `atexit` handlers. So a program that leaves an engine
390
+ open prints its last line and then blocks for ever, with no output and no
391
+ traceback to explain it. An `atexit` hook does not fix this — it runs too late.
392
+ Every entry point must call `engines.close()`; the server does it in its
393
+ shutdown handler and the CLI in a `finally`.
394
+
395
+ **Lichess's single-game export is not under `/api`.** It is
396
+ `https://lichess.org/game/export/{id}`, while everything else this app uses is
397
+ `https://lichess.org/api/...`. Getting it wrong returns a 404 HTML page.
398
+
399
+ **The opening explorer needs a token; the openings dataset does not.**
400
+ `explorer.lichess.org` now requires an authenticated request, which is why book
401
+ detection here uses the downloadable dataset instead — it never changes, so a
402
+ 380 KB download once beats an authenticated request per position.
403
+
404
+ **Scores are White's point of view everywhere**, matching `PovScore.white()`
405
+ and the sibling app's `Eval`. Only `classify.py` flips, and it does so once, at
406
+ the boundary. A 30-point drop in White's winning chances is a *gain* for Black,
407
+ and there is a test that keeps that sign honest.
408
+
409
+ ## Hosting it for free
410
+
411
+ Same profile as
412
+ [the other two apps](https://github.com/spearb0lt/Lichess-Essentials/blob/main/Lichess-Study-to-PDF/README.md#hosting-it-for-free) —
413
+ FastAPI/Uvicorn needing a real container, not a serverless host. Unlike
414
+ Repertoire-Creator, this app doesn't actually import the sibling package
415
+ anywhere in its code despite the `cloud` extra in `pyproject.toml` (that's an
416
+ unused hook for later), so [`Dockerfile`](https://github.com/spearb0lt/Lichess-Essentials/blob/main/ChessAnalyzer/Dockerfile) is self-contained in
417
+ this folder — no repo-root build context needed.
418
+
419
+ It installs Stockfish via `apt-get` rather than letting the app's own
420
+ GitHub-releases downloader (`chess_analyzer/engines.py`) fetch one at
421
+ runtime — simpler, and `apt` already picks the build matching the container's
422
+ actual CPU. `engines.py` checks `$STOCKFISH_PATH` directly
423
+ ([engines.py:214](https://github.com/spearb0lt/Lichess-Essentials/blob/main/ChessAnalyzer/chess_analyzer/engines.py#L214)), which the Dockerfile
424
+ sets, so it shows up in the engine picker as "Engine from $STOCKFISH_PATH"
425
+ with nothing else to configure. Lc0/Maia are left out — they're optional, and
426
+ downloadable from the same picker at runtime if you want them.
427
+
428
+ ### What does *not* persist on a free host
429
+
430
+ `games/`, `data/openings.json` and `engines/` are already gitignored locally
431
+ because they're regenerated on demand (see "What is kept on disk", above) —
432
+ but on a free container they also get wiped by every redeploy, and possibly
433
+ every wake from sleep. Unlike Repertoire-Creator, there is no git-autosave
434
+ here to fix that. In practice that means: the openings index rebuilds itself
435
+ on first use, Stockfish is already baked into the image so there's nothing to
436
+ re-download for the main engine, and **your reviewed-game library does not
437
+ survive a restart**. Treat a hosted instance as a live analysis tool, not a
438
+ permanent archive — export or note anything from `games/` you want to keep,
439
+ or do that curation against a local run instead.
440
+
441
+ ### Render
442
+
443
+ 1. Push this repo to GitHub.
444
+ 2. **New Web Service** → connect the repo → **Root Directory**:
445
+ `ChessAnalyzer` → Render auto-detects the Dockerfile → **Free** instance.
446
+ 3. Optional environment variables, marked **secret**: `ANALYZER_AUTH_USER` /
447
+ `ANALYZER_AUTH_PASS` — see "Locking it behind a password" below.
448
+ 4. Deploy. You get a URL like `https://<name>.onrender.com`.
449
+
450
+ ### Hugging Face Spaces
451
+
452
+ Spaces are their own separate git repo, so:
453
+
454
+ 1. **New Space** → **SDK: Docker** → **Hardware: CPU basic (free)**.
455
+ 2. Clone the Space's repo locally, then copy this folder's **contents**
456
+ (`Dockerfile`, `requirements.txt`, `chess_analyzer/`, etc.) into its
457
+ root — not the `ChessAnalyzer` folder itself, what's inside it.
458
+ 3. Add this to the top of the Space's `README.md`:
459
+ ```yaml
460
+ ---
461
+ title: Chess Analyzer
462
+ sdk: docker
463
+ app_port: 7860
464
+ ---
465
+ ```
466
+ 4. **Settings → Repository secrets**: `ANALYZER_AUTH_USER` /
467
+ `ANALYZER_AUTH_PASS`, if you want the password gate below.
468
+ 5. Commit and push (a Hugging Face access token as the git password).
469
+
470
+ ### Locking it behind a password
471
+
472
+ [`server.py`](https://github.com/spearb0lt/Lichess-Essentials/blob/main/ChessAnalyzer/chess_analyzer/server.py) already has an HTTP Basic Auth gate
473
+ that only activates when both `ANALYZER_AUTH_USER` and `ANALYZER_AUTH_PASS`
474
+ are set — leave them unset and local `chess-analyzer serve` is unaffected.
475
+ Set both as secrets on whichever host you use and every route, API included,
476
+ asks for that username and password first. It's one shared credential pair,
477
+ not per-user accounts, sent over the HTTPS both Render and Hugging Face
478
+ Spaces terminate by default.
479
+
480
+ ### One less thing to worry about here
481
+
482
+ This app never asks for a Lichess token via an environment variable at all —
483
+ the token field in Settings lives in memory only and every endpoint it calls
484
+ is public (see "What is kept on disk", above). The public-token caveat that
485
+ applies to the other two apps' `LICHESS_TOKEN` doesn't apply here.
486
+
487
+ ## Licence
488
+
489
+ MIT — see [LICENSE](https://github.com/spearb0lt/Lichess-Essentials/blob/main/LICENSE).