Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

chessprep-generator

Generate an opening-preparation PGN: for every opponent move worth covering, Stockfish's best reply — recursively, from a real games database.

You give it a starting line and how deep to go. It asks the Lichess Opening Explorer what your opponents actually play there, keeps the moves that matter, has Stockfish answer each one, and recurses. The result is a single PGN with recursive variations, ready to import into En Croissant, Chess Prep Pro, chess.com, a Lichess study or any database.

$ chessprep-generator --moves "e4 c5" --side white --depth 2

[Event "White prep - Sicilian Defense"]
[White "Prep"]
[Black "Sicilian Defense"]
[ECO "B20"]
[Opening "Sicilian Defense"]
[Annotator "chessprep-generator 0.1.0 | Stockfish 18 d20 | k4 cov0.9 depth2"]
[Orientation "white"]

1. e4 c5 2. Nf3 {[%eval 0.39] +0.39/d20} 2... d6
{36% | 24257153 games | W48 =6 B46} (2... Nc6
{35% | 23862888 games | W49 =6 B45} 3. d4 {[%eval 0.33] +0.33/d20}) (2... e6
{20% | 13311955 games | W46 =6 B48} 3. d4 {[%eval 0.48] +0.48/d20}) 3. d4
{[%eval 0.38] +0.38/d20} *

Each comment says how often the move is played, how it scores, and what the engine thinks. Headers name the line so a repertoire file's games are told apart at a glance.

Why the trees stay small

The tree is deliberately asymmetric: on the opponent's side it keeps the k moves worth covering, on your side it keeps one — the engine's best, the move you will actually play. Width grows as k^n rather than (k·k)^n, which is what makes a five-move-deep repertoire practical.

--coverage shrinks it further. Moves are taken most-played first until they account for a share of the games, so a position where one reply is 98% of the database costs one branch instead of four.

Install

Needs a Stockfish binary and a Rust toolchain (edition 2024).

cargo build --release
export STOCKFISH_PATH=/path/to/stockfish   # or pass --engine

LICHESS_TOKEN is optional and raises the explorer's rate limit. It is read from the environment only, never from a flag, so it stays out of your shell history and the process list.

Quick start

# One line, four of your moves deep.
chessprep-generator --moves "e4 c5" --side white --depth 4 -o sicilian.pgn

# Check what a run would cost before paying for it — no engine, no network.
chessprep-generator --moves "e4" --side white --depth 4 --dry-run

# No network at all: opponent moves come from the engine instead of the database.
chessprep-generator --source engine --depth 3

--side follows the side to move by default, so it is only needed when --moves stops on the other side's turn — after 1.e4 it is Black to move, so a White repertoire needs --side white. --dry-run resolves everything and prints the plan without starting an engine or sending a request, which is the cheapest way to find out:

line                          side depth  ~nodes  from
Sicilian                     white     4     169  e4 c5
Caro-Kann                    white     3      41  e4 c6

While it runs, a single status line is rewritten in place — or a periodic one when stderr is redirected, so log files stay readable:

[2/6] Caro-Kann 412/~1364 nodes | 128 searches (61% cached) | 1m12s | e4 c6 d4 d5 Nc3

A whole repertoire in one command

Copy chessprep.example.toml to chessprep.toml. It carries the settings and the lines, because a repertoire is exactly "those lines, with those settings".

[defaults]
depth = 4
ratings = "1600,1800,2000"      # aim at the opponents you actually face
speeds = "blitz,rapid"

[profiles.quick]                # --profile quick, for iterating
depth = 2
engine-depth = 14

[[lines]]
name = "Sicilian"               # what the game is called in your PGN reader
moves = "e4 c5"
side = "white"

[[lines]]
name = "Caro-Kann"
moves = "e4 c6"
side = "white"
depth = 3                       # per-line override
chessprep-generator -o white.pgn            # every line, one game each
chessprep-generator --split-output prep/    # or one file per line

Keys are the flag names, hyphenated. Precedence runs lowest to highest: built-in defaults, [defaults], [profiles.<name>], [[lines]], command line — so --depth 2 on the command line flattens everything for one run, and --moves builds just that line, ignoring the repertoire.

Every line shares one engine pool and one warm explorer cache, which is what makes this meaningfully cheaper than one invocation per line. A [[lines]] entry may therefore only override what shapes its own tree (name, fen, moves, side, depth, engine-depth, max-nodes, nags); anything else is refused by name rather than silently applied to all lines.

Flags worth knowing

Flag Default
--depth 2 Number of your moves per line — not plies.
--opponent-moves 4 The k: how many opponent replies to cover.
--coverage 0.9 Share of games the kept moves must account for.
--source hybrid hybrid, lichess (stop out of theory) or engine (no network).
--ratings 2000,2200,2500 Which rated players the database statistics come from.
--speeds blitz,rapid,classical Which time controls they come from.
--engines one per core Stockfish processes. --threads/--hash size each one.
--max-nodes 5000 Hard stop against combinatorial explosion.
--nags off Annotate engine-found inferior replies ($6/$2/$4).

Prefer raising --engines over --threads: Stockfish scales far better across processes than across threads on independent positions. Run --help for the full list and worked examples.

How it works

  • Two sources. The Lichess explorer says what opponents play; the engine says what they could play. In hybrid mode a branch uses the database until it runs out of games, then falls back to the engine. Leaving the book is monotone — every descendant of an out-of-book position is out of book too — so those subtrees never touch the network again.
  • Parallel across branches, not layers. The builder expands breadth-first only until there are as many independent subtrees as engines, then hands each engine one subtree to walk depth-first. A layer-by-layer build was tried first and measured slower: depth-first keeps each position one ply from the one it descends from, so Stockfish finds its own work in the transposition table.
  • Two on-disk caches under .chessprep-cache/, keyed on the position with the move counters dropped so transpositions share an entry. Explorer entries are keyed by rating and speed filters; engine searches are tagged with the Stockfish version. Both are written after every line, so interrupting a long run never loses the network and CPU already paid for. Deleting the directory only costs time, never correctness.

Evaluations in the PGN are written from White's point of view, as every reader expects, in both the [%eval] tag and the human-readable comment.

Notes

  • A multi-engine run is not bit-reproducible: at fixed depth Stockfish answers from whatever its transposition table holds, so two processes can return slightly different scores for the same position. Every value stored is a legitimate search. --engines 1 is reproducible.
  • --max-nodes truncates whichever branches are walked last, so a truncated tree is uneven rather than uniformly shallower. The run says so when it happens.

Development

cargo test          # unit tests, all inline; no Stockfish or network needed
cargo clippy --all-targets
cargo fmt

License

Not yet chosen.

About

Generate an opening-preparation PGN: for every opponent move worth covering, Stockfish's best reply — recursively, from a real games database.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages