player-prepper 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. player_prepper-0.1.0/LICENSE +21 -0
  2. player_prepper-0.1.0/PKG-INFO +637 -0
  3. player_prepper-0.1.0/README.md +600 -0
  4. player_prepper-0.1.0/player_prepper/__init__.py +3 -0
  5. player_prepper-0.1.0/player_prepper/board.py +223 -0
  6. player_prepper-0.1.0/player_prepper/book.py +507 -0
  7. player_prepper-0.1.0/player_prepper/bridge.py +110 -0
  8. player_prepper-0.1.0/player_prepper/cli.py +494 -0
  9. player_prepper-0.1.0/player_prepper/engine.py +306 -0
  10. player_prepper-0.1.0/player_prepper/exploit.py +199 -0
  11. player_prepper-0.1.0/player_prepper/export.py +279 -0
  12. player_prepper-0.1.0/player_prepper/fetch.py +407 -0
  13. player_prepper-0.1.0/player_prepper/jobs.py +155 -0
  14. player_prepper-0.1.0/player_prepper/openings.py +148 -0
  15. player_prepper-0.1.0/player_prepper/paths.py +82 -0
  16. player_prepper-0.1.0/player_prepper/pipeline.py +200 -0
  17. player_prepper-0.1.0/player_prepper/scout.py +454 -0
  18. player_prepper-0.1.0/player_prepper/server.py +547 -0
  19. player_prepper-0.1.0/player_prepper/store.py +197 -0
  20. player_prepper-0.1.0/player_prepper/tree.py +363 -0
  21. player_prepper-0.1.0/player_prepper/web/app.js +1432 -0
  22. player_prepper-0.1.0/player_prepper/web/index.html +303 -0
  23. player_prepper-0.1.0/player_prepper/web/style.css +425 -0
  24. player_prepper-0.1.0/player_prepper.egg-info/PKG-INFO +637 -0
  25. player_prepper-0.1.0/player_prepper.egg-info/SOURCES.txt +30 -0
  26. player_prepper-0.1.0/player_prepper.egg-info/dependency_links.txt +1 -0
  27. player_prepper-0.1.0/player_prepper.egg-info/entry_points.txt +2 -0
  28. player_prepper-0.1.0/player_prepper.egg-info/requires.txt +11 -0
  29. player_prepper-0.1.0/player_prepper.egg-info/top_level.txt +1 -0
  30. player_prepper-0.1.0/pyproject.toml +57 -0
  31. player_prepper-0.1.0/setup.cfg +4 -0
  32. player_prepper-0.1.0/tests/test_prepper.py +742 -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,637 @@
1
+ Metadata-Version: 2.4
2
+ Name: player-prepper
3
+ Version: 0.1.0
4
+ Summary: Scout a chess opponent from their own games, and measure your preparation against them.
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/Player-Prepper/README.md
11
+ Keywords: chess,lichess,opponent,scouting,preparation,openings
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: prep
33
+ Requires-Dist: lichess-study-to-pdf>=0.1.0; extra == "prep"
34
+ Provides-Extra: dev
35
+ Requires-Dist: pytest>=8.0; extra == "dev"
36
+ Dynamic: license-file
37
+
38
+ # Player Prepper
39
+
40
+ [![PyPI](https://img.shields.io/pypi/v/player-prepper?logo=pypi&logoColor=white)](https://pypi.org/project/player-prepper/)
41
+ [![Python](https://img.shields.io/pypi/pyversions/player-prepper)](https://pypi.org/project/player-prepper/)
42
+ [![Downloads](https://static.pepy.tech/badge/player-prepper)](https://pepy.tech/project/player-prepper)
43
+ [![Downloads](https://static.pepy.tech/badge/player-prepper/month)](https://pepy.tech/project/player-prepper)
44
+ [![License](https://img.shields.io/pypi/l/player-prepper)](LICENSE)
45
+
46
+
47
+ Scout an opponent from their own games, and find out what you have no answer
48
+ for.
49
+
50
+ Give it a Lichess or Chess.com username. It pulls a few hundred of their
51
+ games, folds them into an opening tree keyed by position, and answers three
52
+ questions:
53
+
54
+ - **What do they play?** Per colour, counted, named, with how they score.
55
+ - **Where do they leak points?** Their own results, ranked by how many points
56
+ a move has actually cost them. No engine, no opinion.
57
+ - **What have you got nothing for?** Every position their games reach where it
58
+ is *your* move and your book is empty — weighted by how many of their games
59
+ would put you there.
60
+ - **Where should you aim?** Their real choices crossed with what the engine
61
+ says you get against each, ranked by an opportunity score you control.
62
+
63
+ That third one is the whole product, and it needs to know what you play. It
64
+ can read that from a Repertoire-Creator repertoire, a Lichess study, or your
65
+ own games — or all three at once.
66
+
67
+ ```powershell
68
+ cd Player-Prepper
69
+ & "..\.lichess\Scripts\python.exe" -m player_prepper.cli serve # port 8780
70
+ ```
71
+
72
+ Then open <http://127.0.0.1:8780>.
73
+
74
+ ![A finished scout: their record and coverage across the top, the gap list ranked by how many of their games reach each one, and the selected gap on the right with the engine's suggestion drawn on the board](https://raw.githubusercontent.com/spearb0lt/Lichess-Essentials/main/Player-Prepper/docs/report.png)
75
+
76
+ *Above: this opponent meets 1.e4 with the Scandinavian in 30 of their 60 games
77
+ as Black, and the repertoire being measured has no move written down for it.*
78
+
79
+ ---
80
+
81
+ ## Install
82
+
83
+ ```bash
84
+ pip install player-prepper
85
+ prepper serve
86
+ ```
87
+
88
+ **Optional.** Engine suggestions for gaps, prep-sheet PDFs and private
89
+ studies: `pip install "player-prepper[prep]"`.
90
+
91
+ Or take all five at once with `pip install lichess-essentials`. Installed this
92
+ way your files live in the usual per-user folder for your platform, and the
93
+ app prints the path in its startup banner. To run it from a checkout instead,
94
+ see [the repository README](https://github.com/spearb0lt/Lichess-Essentials/blob/main/README.md#setup-from-a-checkout).
95
+
96
+ ## What a gap actually is
97
+
98
+ A gap is not "a position you have not studied" — there are billions of those.
99
+ It is a position that
100
+
101
+ 1. **this opponent actually steers into**, in games they really played,
102
+ 2. **your own repertoire actually reaches**, so it is a line you will get, and
103
+ 3. **you have written nothing about**.
104
+
105
+ The count beside it is the number of their games that would have put you
106
+ there. That is the number worth working through in order.
107
+
108
+ Getting there means walking each of their games ply by ply, up to the scouting
109
+ depth, and doing one of three things at every one of your turns:
110
+
111
+ | At your turn | |
112
+ |---|---|
113
+ | Your book has a move, and they played into it | Carry on. |
114
+ | Your book has a move, and the game went somewhere else | **Drop the game.** Their real opponent played something you never would, so this game is about somebody else's repertoire. Counting it would punish you for a line you cannot reach. |
115
+ | Your book has nothing here | **A gap.** Record the position, stop the game, and add one to that position's count. |
116
+
117
+ Their own moves are never checked against anything — whatever they played, you
118
+ have to meet it.
119
+
120
+ So a colour's coverage comes out as three numbers that add up:
121
+
122
+ ```
123
+ their 60 games as Black
124
+ = 18 that never reach your repertoire (their opponent opened 1.d4; you play 1.e4)
125
+ + 42 that do
126
+ = 0 that stay inside it to move 12
127
+ + 42 that run into one of 6 gap positions
128
+ ```
129
+
130
+ The middle number is the honest denominator. A tool that measured coverage
131
+ against all 60 would report 70% "coverage" for a repertoire that answers
132
+ nothing, purely because most of their games were unreachable.
133
+
134
+ ---
135
+
136
+ ## Setting up
137
+
138
+ Everything shares the one virtualenv at the repository root. From the
139
+ repository root:
140
+
141
+ ```powershell
142
+ # Windows PowerShell
143
+ python -m venv .lichess
144
+ .\.lichess\Scripts\python.exe -m pip install -r Player-Prepper\requirements.txt
145
+ .\.lichess\Scripts\python.exe -m pip install -e Lichess-Study-to-PDF
146
+ ```
147
+
148
+ ```bash
149
+ # Git Bash on Windows
150
+ python -m venv .lichess
151
+ ./.lichess/Scripts/python.exe -m pip install -r Player-Prepper/requirements.txt
152
+ ./.lichess/Scripts/python.exe -m pip install -e Lichess-Study-to-PDF
153
+
154
+ # macOS / Linux
155
+ python -m venv .lichess
156
+ ./.lichess/bin/python -m pip install -r Player-Prepper/requirements.txt
157
+ ./.lichess/bin/python -m pip install -e Lichess-Study-to-PDF
158
+ ```
159
+
160
+ That second install is the sibling app, and it is **optional**. Without it the
161
+ app still scouts, still measures coverage and still lists every gap; what you
162
+ lose is the engine's suggestion for a gap, the PDF export, and the ability to
163
+ read a *private* study as your book. The startup banner tells you which of
164
+ those are available.
165
+
166
+ A [Stockfish](https://stockfishchess.org/download/) binary in
167
+ `Lichess-Study-to-PDF/engine/` is what turns on the engine suggestions. A LaTeX
168
+ install (MiKTeX or TeX Live) unlocks the typeset book layout for the PDF.
169
+
170
+ **No API token is needed.** Every endpoint this app uses is public. A Lichess
171
+ token raises the rate limit when you are pulling hundreds of games, and lets a
172
+ private study be used as your book — paste one into the Token dialog and it is
173
+ kept in memory for that session only, never written to disk.
174
+
175
+ ## Running it
176
+
177
+ ```powershell
178
+ # Windows PowerShell
179
+ cd Player-Prepper
180
+ & "..\.lichess\Scripts\python.exe" -m player_prepper.cli serve
181
+ ```
182
+
183
+ ```bash
184
+ # Git Bash on Windows
185
+ cd Player-Prepper
186
+ ../.lichess/Scripts/python.exe -m player_prepper.cli serve
187
+
188
+ # macOS / Linux
189
+ cd Player-Prepper
190
+ ../.lichess/bin/python -m player_prepper.cli serve
191
+ ```
192
+
193
+ Open <http://127.0.0.1:8780>. `Ctrl+C` stops it. `--port 8781` if 8780 is
194
+ taken; `--host 0.0.0.0` exposes it to your network, which has no
195
+ authentication unless you set the password gate below — only do that on a
196
+ network you trust.
197
+
198
+ ---
199
+
200
+ ## Your book: the three sources
201
+
202
+ Coverage is measured against a **book**, which is `{position: moves you play
203
+ from it}` — the same shape as Repertoire-Creator's universal mode, and for the
204
+ same reason. Keying on the position rather than the line is what makes a scout
205
+ survive move orders: if their pet order transposes into a normal Sicilian, a
206
+ line-keyed book would call it uncovered and simply be wrong.
207
+
208
+ Click the **book** pill in the top bar. You can stack as many sources as you
209
+ like; they fold into one book.
210
+
211
+ | Source | |
212
+ |---|---|
213
+ | **A Repertoire-Creator repertoire** | Read straight off `Repertoire-Creator/repertoires/`. That folder is plain PGN plus a small JSON manifest, so this reads the files rather than importing the app — the two never have to be installed together. Set `REPERTOIRE_DIR` if yours live elsewhere; it is the same variable the sibling app honours. |
214
+ | **A Lichess study** | Paste a study or chapter URL. With the sibling exporter installed, private studies work through its chapter-by-chapter route with no token. Without it, public studies still work. |
215
+ | **Your own games** | What you actually play, as opposed to what you wrote down. Only your moves are recorded, and how often you played each, so the book knows your main move from something you tried once. |
216
+
217
+ **Only your side's moves are ever recorded.** A white repertoire lists Black's
218
+ tries too, and counting those as your own answers would report coverage you do
219
+ not have — so a repertoire contributes only moves made by the colour it is
220
+ for, and a book built from your games contributes only the moves *you* made.
221
+
222
+ Colour is never stored, because it does not need to be: a move recorded from a
223
+ position where White is to move *is* a white move. Coverage asks about one
224
+ colour at a time and looks at whose turn it is.
225
+
226
+ The one case with no answer is a **study chapter with no `Orientation` tag** —
227
+ there is nothing to say which side it is for. The default reads the tag when
228
+ Lichess wrote one and records both sides when it did not, and the book panel
229
+ says so, because guessing wrong is exactly the difference between "you have an
230
+ answer" and "you do not". Set the colour explicitly to settle it.
231
+
232
+ **Check it** in the book dialog builds the book and reports what is in it, so
233
+ you can see how many positions a source actually contributes before measuring
234
+ anything against it.
235
+
236
+ <img src="docs/book.png" width="620" alt="The book dialog with two sources stacked -- a Repertoire-Creator repertoire and the user's own Chess.com games -- and the built book reported underneath as positions, moves and branch points">
237
+
238
+ ---
239
+
240
+ ## Reading the report
241
+
242
+ ### Gaps
243
+
244
+ Ranked by how many of their games arrive there. Click one for the position,
245
+ their record from it, links to the games that got there, and — if you have an
246
+ engine — a suggested move drawn on the board.
247
+
248
+ ### Where they leak points
249
+
250
+ Their own moves, ranked by **games × (0.5 − their score)**: the number of
251
+ points they have dropped below an even score in that line. That is the honest
252
+ way to combine "how badly it goes for them" with "how often it happens". A
253
+ move they lost with once is not a plan; a move they score 35% with over thirty
254
+ games is.
255
+
256
+ Two things this is not. It is **not a verdict on the move** — it is a fact
257
+ about their results, and you are playing them, not the move. And it is **not
258
+ significance-tested**: the raw record and the sample size travel with every
259
+ row so you can see a 100%-over-two-games for what it is, and the *smallest
260
+ sample* setting on the left is the only filter.
261
+
262
+ ![The Where they leak tab: two moves in the Scandinavian ranked by points dropped, each with its full line, the opening name, the percentage and the raw win-draw-loss record](https://raw.githubusercontent.com/spearb0lt/Lichess-Essentials/main/Player-Prepper/docs/weak-spots.png)
263
+
264
+ ### Exploit
265
+
266
+ The other tabs answer separate questions and leave you to join them up. *Where
267
+ they leak* says a move has gone badly for them; *Every choice* says how often
268
+ they play it; neither says what to actually do about it. This tab does the
269
+ joining.
270
+
271
+ For each of their real choices it takes the position **after** their move —
272
+ where you are to move — asks the engine for the best reply, and scores it on
273
+ three numbers **you can switch on and off independently**:
274
+
275
+ | Factor | |
276
+ |---|---|
277
+ | **frequency** | How many of their games play into it, against their busiest choice. A brilliant refutation of something they played twice is not preparation. |
278
+ | **record** | How badly it has actually gone for them, as `1 − their score`. The empirical half; needs no engine. |
279
+ | **edge** | How good your best reply leaves you, as winning chances from *your* side, through the curve Lichess uses. |
280
+
281
+ The opportunity score is the **product** of whichever factors are enabled,
282
+ which is the honest combination: a product says every enabled factor has to be
283
+ true at once, so a common line they do fine in and a rare line you crush both
284
+ rank below a common line they are already losing. Turn *frequency* off and the
285
+ rare brilliancies rise; turn *edge* off and it becomes a pure results ranking
286
+ that works with no engine at all.
287
+
288
+ ![The Exploit tab: their choices ranked by opportunity score, each with the reply the engine wants, how many of their games reach it, how they score there and the winning chances you get](https://raw.githubusercontent.com/spearb0lt/Lichess-Essentials/main/Player-Prepper/docs/exploit.png)
289
+
290
+ Every number in a row is from one point of view and it says which: *they score
291
+ 55%* is theirs, *you get 47%* is yours. The raw evaluation in pawns stays in
292
+ the engine box under the board, where the White's-point-of-view convention the
293
+ rest of the repository uses still applies.
294
+
295
+ This is the one part of the app that needs a lot of engine time, so it runs
296
+ **when you first open the tab**, as a background job with a progress bar, and
297
+ the answers are saved into the report. Opening it again is instant. *Run
298
+ again* re-analyses, and `prepper exploit` does the same thing from a script.
299
+
300
+ ### Their openings
301
+
302
+ Every game grouped by the deepest named opening it reached, using Lichess's own
303
+ openings dataset indexed by position — so a transposition is grouped with the
304
+ opening it became, not the one it started as.
305
+
306
+ ### Every choice
307
+
308
+ One row per (position, move): what they played, how often out of the times
309
+ they reached that position, and how they scored. This is the raw material the
310
+ other tabs are ranked from.
311
+
312
+ ### Explore
313
+
314
+ Walk their tree by hand from the starting position. At a position where it was
315
+ their move you get their choices; at a position where it was not, you get every
316
+ reply their opponents actually made. Both carry counts and scores, so you can
317
+ follow a line down to where it gets thin.
318
+
319
+ ![The Explore tab one move into their tree: after 1.e4, every reply their opponents played, with how many games and how the scouted player scored against each](https://raw.githubusercontent.com/spearb0lt/Lichess-Essentials/main/Player-Prepper/docs/explore.png)
320
+
321
+ ---
322
+
323
+ ## The board
324
+
325
+ Whatever you click — a gap, one of their moves, an exploit row, a position in
326
+ Explore — lands on the board beside the report, and the board is not a picture.
327
+
328
+ <img src="docs/board.png" width="380" alt="The board panel: an eval bar down the left, the position with the engine's move drawn on it, first/back/forward/last and flip controls with the evaluation beside them, the move strip, and the engine's ranked lines underneath">
329
+
330
+ - **Play on from it.** Click a piece and its legal moves light up; click a
331
+ destination and you are off down your own line. Play at a point you have
332
+ stepped back to and the rest is rewritten, which is what taking a move back
333
+ and trying something else should do. *Reset* returns to the row you clicked.
334
+ Legality is decided on the server, so there is no chess library in the page.
335
+ - **The mouse wheel steps the line** — down goes forward, up goes back, and a
336
+ trackpad flick is one move rather than ten. Arrow keys do the same,
337
+ `Home`/`End` jump to the ends, and `F` flips.
338
+ - **The eval bar is live** and follows every move you make, including moves you
339
+ invent. The light block is always White's share and sits on White's side of
340
+ the board, so it still reads correctly after a flip. The number beside the
341
+ controls says the depth and whether it came from the local engine or the
342
+ Lichess cloud.
343
+ - **The move strip** shows the line you are standing in; moves you played
344
+ yourself are in a different colour. Click any move to jump to it.
345
+ - **Arrows** mean one thing each: **blue** is the engine's move, **yellow** is
346
+ the next move of the line you are looking at.
347
+
348
+ ### The engine toggle
349
+
350
+ Asking for the engine one position at a time gets old. The **engine** pill in
351
+ the top bar turns it on for everything: every position you look at, step to, or
352
+ play into gets its ranked lines automatically, debounced so that spinning the
353
+ wheel is not a storm of searches. The setting is remembered.
354
+
355
+ With the toggle off, a gap still shows the suggestion computed when it was
356
+ scouted, and *Ask the engine* asks for one position on demand.
357
+
358
+ ### On a phone
359
+
360
+ The layout collapses to one column at 860px, and the order changes with it:
361
+ the report first, then the position you tapped, then the scout list and
362
+ settings. Settings collapse into a fold on narrow screens. Everything works by
363
+ touch — tap a piece, tap a square — and the forward and back buttons stand in
364
+ for the wheel.
365
+
366
+ <img src="docs/phone.png" width="300" alt="Player Prepper on a phone: a two-row header, the report with its scrolling tab bar, then the board with its eval bar and engine lines, then the scout list and collapsed settings">
367
+
368
+ ---
369
+
370
+ ## Exporting
371
+
372
+ **PGN** gives you the whole prep sheet as a multi-chapter study: what they play
373
+ as each colour, where they leak points, and your gaps — each as a merged move
374
+ tree with the numbers in the comments. It opens in any chess GUI.
375
+
376
+ **PDF** hands that PGN to the sibling app's layout engine, so you get the same
377
+ four styles the study exporter has. Grid is the one to use: a scouting report
378
+ is a contact sheet, not a book.
379
+
380
+ ![A page of the exported prep sheet: twelve diagrams to a page in reading order, each with the move, how many games they played it in and how they scored, with sidelines colour-coded and named](https://raw.githubusercontent.com/spearb0lt/Lichess-Essentials/main/Player-Prepper/docs/prep-sheet.png)
381
+
382
+ Lines are merged into a tree rather than written one per chapter on purpose.
383
+ Twenty gaps as twenty chapters is twenty nearly-empty pages, because every
384
+ chapter starts a fresh one; merged, they share their opening moves and fit on
385
+ two pages of diagrams — which is also how you actually think about them.
386
+
387
+ Player Prepper **never writes into your repertoires folder.** It reads it and
388
+ nothing else, so there is no chance of two apps writing the same files and
389
+ Repertoire-Creator's git auto-commit stays the only writer there.
390
+
391
+ ---
392
+
393
+ ## What the two sites will and will not give you
394
+
395
+ **Lichess streams.** `/api/games/user/{u}` is one ndjson request whatever the
396
+ count, so the fetch reads it line by line and stops when it has enough rather
397
+ than asking for a number and hoping. `moves` there is SAN, so it is walked
398
+ through a board once to become UCI — which is what the tree wants, and a
399
+ fraction of the size to cache.
400
+
401
+ **Chess.com does not stream, and has no "last N games" endpoint at all** —
402
+ only whole months. So the fetch walks backwards through the monthly archives,
403
+ newest first, and stops as soon as it has enough. It is bounded at 36 months so
404
+ one click on a dormant account cannot turn into a hundred requests. Chess.com
405
+ also sits behind Cloudflare and returns a 403 to any request with no
406
+ `User-Agent`; that is a missing header, not a ban, and this app always sends
407
+ one.
408
+
409
+ Both are the documented public APIs. Neither needs a token. Variant games are
410
+ dropped rather than guessed at.
411
+
412
+ ---
413
+
414
+ ## What is kept on disk
415
+
416
+ ```
417
+ prep/ (PREPPER_DIR, or ./prep)
418
+ settings.json your book sources, remembered
419
+ games/
420
+ lichess-drnykterstein.json their games, compact, reusable
421
+ scouts/
422
+ lichess-drnykterstein.json the last report and what produced it
423
+ books/
424
+ study-i7hMEq7h.pgn a study fetched as a reference book
425
+ ```
426
+
427
+ Games are cached as UCI move lists rather than PGN: a tenth the size, no
428
+ re-parsing, and all the tree ever looks at. The identifying tags are kept
429
+ beside them so a row still reads like a game.
430
+
431
+ A cached fetch is reused only when it was made with filters at least as wide as
432
+ the ones you are asking for now — otherwise "give me 500 games" would be
433
+ silently answered with the 50 fetched last week. Tick **Re-fetch** to force it.
434
+
435
+ `data/openings.json` is the openings index, built once from Lichess's
436
+ downloadable openings dataset (no token, unlike the opening explorer, which now
437
+ requires an authenticated request). Both `prep/` and `data/` are gitignored.
438
+
439
+ ---
440
+
441
+ ## Command line
442
+
443
+ Everything the browser does is also a command, so a night-before-the-tournament
444
+ script can scout four opponents and print four PDFs.
445
+
446
+ ```bash
447
+ prepper scout <username> [--site lichess|chesscom] # scout and save
448
+ prepper scout hikaru --site chesscom --limit 500 --speed blitz --days 180
449
+ prepper scout them --repertoire white-ruy-lopez --suggest 8
450
+ prepper scout them --study https://lichess.org/study/abcd1234
451
+ prepper scout them --my-games chesscom:you
452
+ prepper exploit <username> --color both # best counters, ranked
453
+ prepper exploit them --no-frequency --top 20 # same toggles as the tab
454
+ prepper show <username> [--json] [--verbose] # print a saved report
455
+ prepper list # every saved scout
456
+ prepper forget <username> # report and cached games
457
+ prepper repertoires # what the folder holds
458
+ prepper book --repertoire white-ruy-lopez # build a book, say what is in it
459
+ prepper games <username> --limit 500 # fetch and cache only
460
+ prepper pdf <username> --mode grid --out prep.pdf
461
+ prepper serve [--host H] [--port P]
462
+ ```
463
+
464
+ Run them through the venv the same way as `serve`, e.g.
465
+ `../.lichess/Scripts/python.exe -m player_prepper.cli list`.
466
+
467
+ Book sources are repeatable and combine: `--repertoire a --repertoire b
468
+ --study <url> --my-games lichess:you` builds one book from all four.
469
+
470
+ ---
471
+
472
+ ## How the pieces fit
473
+
474
+ | Module | |
475
+ |---|---|
476
+ | `fetch.py` | bulk games from both sites, and the SAN/PGN to UCI conversion |
477
+ | `store.py` | the prep folder, atomic writes, what is cached where |
478
+ | `book.py` | your preparation from a repertoire, a study or your own games |
479
+ | `tree.py` | their games as a position-keyed tree with results attached |
480
+ | `scout.py` | coverage, gaps, rankings, the report — no network, no engine |
481
+ | `exploit.py` | candidate positions, the three factors and the opportunity score |
482
+ | `pipeline.py` | the five steps of one scout, shared by the CLI and the server |
483
+ | `openings.py` | Lichess's openings dataset, indexed by position |
484
+ | `engine.py` | the eval bar, the suggestion for a gap, the exploit pass |
485
+ | `export.py` | the prep sheet as PGN, and as a PDF through the sibling app |
486
+ | `board.py` | board SVGs, legality, and the positions along a line |
487
+ | `jobs.py` | background work with progress and cancellation |
488
+ | `bridge.py` | what the sibling app provides, and degrading without it |
489
+ | `server.py` / `web/` | the HTTP layer and the browser interface |
490
+
491
+ `scout.py` is deliberately pure: everything that can fail for an external
492
+ reason lives in fetching, book building and the engine, which is what makes the
493
+ part that produces the numbers testable with no network, no engine and no disk.
494
+
495
+ ## Tests
496
+
497
+ ```bash
498
+ ../.lichess/Scripts/python.exe -m pytest tests -q # 52 tests
499
+ ```
500
+
501
+ No network, no engine, no Stockfish. The bias is towards things that break
502
+ *quietly*: a scouting report is a page of confident numbers and there is no way
503
+ to eyeball whether 62% was computed from the right point of view, so those get
504
+ exact assertions on hand-built games where the answer is countable by hand.
505
+
506
+ Two of the tests exist because the bug happened during the build:
507
+
508
+ - python-chess's `StringExporter` **accumulates**, so one exporter reused
509
+ across chapters emits each chapter with every earlier chapter glued in front
510
+ of it. The only visible symptom was a PDF with ten chapters instead of four.
511
+ - `all_gaps()` returning copies instead of the report's own dictionaries meant
512
+ engine suggestions were computed, stored nowhere, and silently missing from
513
+ the export.
514
+
515
+ A third came out of a test failing honestly: the coverage walk originally only
516
+ checked positions it was about to play a move from, so a game that *ended* on
517
+ your turn — someone resigned — was silently counted as covered rather than
518
+ checked. Every position reached inside the horizon is now checked.
519
+
520
+ A fourth is the one stylesheet assertion. `.dialog input` and `.check input`
521
+ have identical specificity, so whichever is written last wins; the later one
522
+ set `width: 100%`, which turned every checkbox in the export dialog into a
523
+ full-width slab that shouldered its own label out through the right-hand edge.
524
+ Nothing in Python could see it and the stylesheet reads correctly. The rule now
525
+ says `:not([type="checkbox"])` outright, and the test asks the general question
526
+ — does any selector setting `width: 100%` still reach a bare `input` — rather
527
+ than checking that today's `:not()` is still spelled right.
528
+
529
+ The exploit ranking is tested the same way — on hand-built rows where the
530
+ product of the factors can be worked out by hand — because "which line is the
531
+ best to prepare" is exactly the kind of claim nobody can check by eye. One of
532
+ those tests exists because *the test* was wrong first: it assumed a rare
533
+ disaster would outrank a common even line, and the arithmetic says otherwise.
534
+ That is the intended behaviour, and it is now written down as an assertion
535
+ rather than an assumption.
536
+
537
+ ---
538
+
539
+ ## Hosting it for free
540
+
541
+ Same profile as the three sibling apps — FastAPI/Uvicorn needing a real
542
+ container, not a serverless host — with one thing that makes this app's setup
543
+ different: like Repertoire-Creator, it wants the sibling package, so the
544
+ Docker build context has to be the **repository root**, not this folder.
545
+
546
+ [`Dockerfile`](https://github.com/spearb0lt/Lichess-Essentials/blob/main/Player-Prepper/Dockerfile) lives here, same as every other app in this repo,
547
+ but must be built with the repo root as its context so it can `COPY` the
548
+ sibling. Docker keeps those two concerns separate — "where is the Dockerfile"
549
+ and "what can `COPY` see" — which is what Render's separate **Root Directory**
550
+ and **Dockerfile Path** fields are for. `.dockerignore` is read from the
551
+ context root regardless, so it stays at [the repository root](https://github.com/spearb0lt/Lichess-Essentials/blob/main/dockerignore).
552
+
553
+ It installs Stockfish via `apt-get` and sets `STOCKFISH_PATH`, so gap
554
+ suggestions work on a hosted instance with nothing to configure. LaTeX is
555
+ deliberately left out: a full texlive install is several GB, and the grid PDF
556
+ does not need it.
557
+
558
+ ### What does not persist
559
+
560
+ `prep/` and `data/` are gitignored locally because they are regenerated on
561
+ demand, and on a free container they also get wiped by every redeploy. In
562
+ practice: the openings index rebuilds itself on first use, and **your saved
563
+ scouts do not survive a restart**. That is a smaller loss here than in the
564
+ sibling apps — a scout is a few minutes of network away from being rebuilt, and
565
+ a report about somebody's recent games goes stale anyway.
566
+
567
+ ### Render
568
+
569
+ 1. Push this repo to GitHub.
570
+ 2. **New Web Service** → connect the repo → **Root Directory**: leave blank
571
+ (repo root, so the build context can see both apps) → **Dockerfile Path**:
572
+ `Player-Prepper/Dockerfile` → **Free** instance.
573
+ 3. Optional environment variables, marked **secret**: `PREPPER_AUTH_USER` /
574
+ `PREPPER_AUTH_PASS` — see below.
575
+ 4. Deploy. You get a URL like `https://<name>.onrender.com`.
576
+
577
+ ### Hugging Face Spaces
578
+
579
+ Spaces are their own separate git repo, so:
580
+
581
+ 1. **New Space** → **SDK: Docker** → **Hardware: CPU basic (free)**.
582
+ 2. Clone the Space's repo locally. Copy in, preserving folder names:
583
+ `Lichess-Study-to-PDF/` and `Player-Prepper/` — then copy
584
+ `Player-Prepper/Dockerfile` up to the **Space repo's own root** as well,
585
+ since Spaces always build whatever is literally named `Dockerfile` there.
586
+ 3. Add this to the top of the Space's `README.md`:
587
+ ```yaml
588
+ ---
589
+ title: Player Prepper
590
+ sdk: docker
591
+ app_port: 7860
592
+ ---
593
+ ```
594
+ 4. **Settings → Repository secrets**: the same variables as Render.
595
+ 5. Commit and push (a Hugging Face access token as the git password).
596
+
597
+ ### Locking it behind a password
598
+
599
+ [`server.py`](https://github.com/spearb0lt/Lichess-Essentials/blob/main/Player-Prepper/player_prepper/server.py) has an HTTP Basic Auth gate that only
600
+ activates when both `PREPPER_AUTH_USER` and `PREPPER_AUTH_PASS` are set —
601
+ leave them unset and local use is never asked for credentials. Set both as
602
+ secrets and every route, API included, asks for that pair first. One shared
603
+ credential pair, not per-user accounts, over the HTTPS both hosts terminate by
604
+ default.
605
+
606
+ Do **not** set `LICHESS_TOKEN` on a public deployment: it would be shared by
607
+ every visitor. Paste a token into the UI per session instead — nothing here
608
+ needs one anyway.
609
+
610
+ ---
611
+
612
+ ## If something goes wrong
613
+
614
+ | Symptom | Cause and fix |
615
+ |---|---|
616
+ | `No module named player_prepper` | You are in the wrong directory. `cd` into `Player-Prepper` first. |
617
+ | `Port 8780 is already in use` | It is probably already running — open the browser, or `--port 8781`. |
618
+ | "No standard games found ... with those filters" | Rated-only is on by default and they may play casual; the speed filter may exclude everything they play. Widen both. |
619
+ | Every line says "coverage was not measured" | No book. Click the **book** pill and add a source. |
620
+ | Coverage is 0% and every game is a gap at move 1 | Your book is for the other colour. A white repertoire has nothing to say when *they* have White — that is a correct answer, not a bug. |
621
+ | Gaps have no suggested move | No engine. Install the sibling app and put Stockfish in `Lichess-Study-to-PDF/engine/`; the banner confirms it at startup. |
622
+ | Lines are unnamed | The openings dataset has not downloaded. It is fetched once on first use and needs no token; check the network and reload. |
623
+ | "That study is private" | Install the sibling exporter, or supply a token with `study:read`. |
624
+ | Lichess says 429 | You pulled a lot of games. Wait a minute; a token raises the limit considerably. |
625
+ | A scout seems stale | The games cache is reused when it is wide enough. Tick **Re-fetch** or pass `--refresh`. |
626
+ | The Exploit tab says there is nothing to analyse | Nothing they play meets the minimum sample. Lower *smallest sample* on the left, or scout more of their games. |
627
+ | Exploit rows show "no engine" | No engine, so there is no best reply to report. The ranking still works on *frequency* and *record*; turn *edge* off to stop it counting. |
628
+ | The eval bar sits in the middle and says nothing | That position is not in the Lichess cloud and there is no local Stockfish. Add a binary to `Lichess-Study-to-PDF/engine/`. |
629
+ | The board will not let me move a piece | It is not that side's turn, or the move is illegal — legality is decided on the server, and only legal destinations light up. |
630
+
631
+ ## Licence
632
+
633
+ MIT — see [LICENSE](https://github.com/spearb0lt/Lichess-Essentials/blob/main/LICENSE). Chess piece artwork in the board diagrams
634
+ comes from python-chess (Colin M.L. Burnett's Cburnett set, CC BY-SA 3.0);
635
+ opening names come from
636
+ [lichess-org/chess-openings](https://github.com/lichess-org/chess-openings)
637
+ (CC0).