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.
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.
Needs a Stockfish binary and a Rust toolchain (edition 2024).
cargo build --release
export STOCKFISH_PATH=/path/to/stockfish # or pass --engineLICHESS_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.
# 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
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 overridechessprep-generator -o white.pgn # every line, one game each
chessprep-generator --split-output prep/ # or one file per lineKeys 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.
| 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.
- Two sources. The Lichess explorer says what opponents play; the engine says what they
could play. In
hybridmode 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.
- 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 1is reproducible. --max-nodestruncates whichever branches are walked last, so a truncated tree is uneven rather than uniformly shallower. The run says so when it happens.
cargo test # unit tests, all inline; no Stockfish or network needed
cargo clippy --all-targets
cargo fmtNot yet chosen.