WickedLines is a command-line tool for analyzing chess opening lines using the Lichess game database. It has five primary modes:
line: A deep-dive analysis of a single, specific opening variation.hunt: An automatic, recursive search to discover statistically significant opportunities and blunders.plot: Visualizes an opening's performance and popularity across different ELO levels, with the ability to compare multiple time controls.compare: Plots multiple different openings on the same charts for direct comparison.batchplot: Automatically generates plots for a predefined list of major openings.
The tool helps you prioritize your study by focusing on variations that are reachable, statistically sound, and offer the greatest practical impact on your rating.
git clone https://github.com/RemiFabre/WickedLines.git
cd WickedLines
pip install -e .[dev]
python wickedlines.py line e4 c5 # deep-dive on the Sicilian
python wickedlines.py hunt e4 e5 Nf3 # hunt for opportunities after 1.e4 e5 2.Nf3
python wickedlines.py plot e4 c6 --speeds blitz,rapid,classical # plot the Caro-Kann across time controlsAll numbers come from the Lichess opening explorer. Two facts to keep in mind when interpreting any plot:
- Buckets are by average of the two players' ratings. A point at "1600" means games where the two players' Lichess ratings averaged 1600-1799 (the 2200 bucket spans 2200-2499). Verified in
lila-openingexplorer. - Lichess ratings are independent per speed. A "1600 Blitz" rating and a "1600 Classical" rating are not the same chess skill — the player pools and their distributions differ. The chart below shows how lopsided the game-volume comparison is across speeds.
The Blitz curve sits roughly an order of magnitude above Classical at every rating bucket. Useful context whenever a "Blitz vs Classical" comparison looks surprising — some of the gap is the data, not the time on the clock.
All reports generated by the hunt command are saved in the hunt_results/ directory.
Write-ups from running the tool on real data:
- The Shape of Time: How Openings Evolve from Blitz to Classical
- Royal Gambits
- Performance vs Elo
- Hunts: discovering opening blunders automatically
This metric calculates the probability of reaching a position assuming one player actively tries to steer the game towards it, while their opponent's moves follow the database's overall frequencies. It answers, "How often can I realistically get this position on the board?"
This is a simpler metric that shows what percentage of all games starting from the initial position follow the specified sequence of moves. It answers, "How common is this line in general practice?" and is plotted on a separate chart for clarity.
A metric to judge a position's statistical value, calculated from the win/draw/loss percentages. A positive EV favors White; a negative EV favors Black.
Shows how the EV changes after a specific move, indicating its statistical quality. A positive ΔEV is good for White; a negative ΔEV is good for Black.
This is a crucial filter. It answers the question: "Could this move's high win rate be due to pure random chance?" A low p-value (typically < 0.05) suggests the result is statistically significant and trustworthy, not just a fluke from a small sample size. The hunt mode will only report on moves that meet this significance threshold.
This is the ultimate metric for practicality. It combines a move's reachability with its statistical impact (ΔEV) to estimate the concrete rating point gain you could expect over 100 games by learning and playing this opportunity:
Reachability × |ΔEV| × 7.5
The constant 7.5 is K/2 with Lichess's Glicko-2 K ≈ 15 — i.e., the slope of "score surplus → rating change" for an established player. Both the hunt and the plot use this same factor.
Plotted on the "Surprise" chart. Always ≥ 1 by construction: popularity multiplies the move-frequencies of both sides, while reachability multiplies only the opponent's. Lines with a high surprise factor are easy for one side to force but rarely seen in the wild — potent preparation weapons.
-
Ensure you have Python 3 installed.
-
Install the required libraries:
pip install -e .[dev]
WickedLines now operates with two main commands: line and hunt.
Use this for a detailed breakdown of one specific variation.
Syntax:
python wickedlines.py line [moves...] [options...]
Example:
python wickedlines.py line e4 e5 Nf3 Nc6 Bb5
Use this to automatically search for high-impact moves.
Syntax:
python wickedlines.py hunt [initial_moves...] [options...]
Examples:
# Hunt for opportunities right from the start
python wickedlines.py hunt
# Hunt for opportunities within the Ruy Lopez
python wickedlines.py hunt e4 e5 Nf3 Nc6 Bb5
# Run a quick hunt that stops after finding 5 lines
python wickedlines.py hunt --max-finds 5
Use this to see how an opening's effectiveness changes across skill levels. You can also compare multiple time controls on the same chart.
Syntax:
python wickedlines.py plot [moves...] [options...]
Examples:
# Plot the performance of the Caro-Kann in rapid games
python wickedlines.py plot e4 c6 --speeds rapid
# Compare Blitz vs Rapid performance for the Caro-Kann on the same plots
python wickedlines.py plot e4 c6 --speeds blitz,rapid
Directly compare the statistics of two or more openings on the same set of plots.
Syntax:
python wickedlines.py compare "[moves_1...]" "[moves_2...]" [options...]
Example:
# Compare the Sicilian Defense vs the French Defense in rapid games
python wickedlines.py compare "e4 c5" "e4 e6" --speed rapid
Automatically generates a full set of plots for a predefined list of common openings.
Syntax:
python wickedlines.py batchplot [options...]
Example:
# Generate plots for all major openings using rapid stats
python wickedlines.py batchplot --speeds rapid
# Generate comparison plots (e.g. Blitz vs Rapid) for all major openings
python wickedlines.py batchplot --speeds blitz,rapid
moves/move_strings: A sequence of moves in SAN.--ratings: (Optional,line/hunt) Comma-separated rating buckets. Allowed values:0, 1000, 1200, 1400, 1600, 1800, 2000, 2200. Each bucket is the lower bound of an average-rating range:1600means games where the two players' ratings averaged 1600-1799. The2200bucket spans 2200-2499. Default:1600.--speeds(multi-value, used byline/hunt/plot/batchplot) and--speed(single-value, used bycompare): time controls. Allowed values:blitz,rapid,classical. Default varies by mode.--max-finds N: (Forhuntmode) Stop the search after findingNinteresting lines.--force-refresh: (For all modes) Ignore the local cache and fetch fresh data from the Lichess API. Useful for getting the most up-to-date stats.
API responses are cached on disk in .wickedlines_cache/ so subsequent runs are fast and offline-friendly. Pass --force-refresh to bypass the cache when you want fresh numbers.
This mode provides a full breakdown of a single line, including:
- Line Reachability Stats: A move-by-move table showing how reachability and EV evolve. The key move that was found in a hunt will be marked with
<-- Interesting. - Final Position Summary: A snapshot of the final position's stats and a link to the Lichess analysis board.
- Next Move Statistics: A table of all legal replies from the final position, including their
ΔEVandp-valueto help you choose the best response.
The hunt mode is designed for discovery and has three phases:
1. Initial Line Analysis (if applicable)
If you provide an initial move sequence, the hunt will first run a full line analysis on it.
2. Live Search Log
You will see a real-time log of the search progress. When an interesting line is found, its full line analysis is printed immediately inside a colored banner.
3. Final Hunt Summary
At the end of the hunt (or when you press Ctrl+C), a final summary is printed. It ranks all found opportunities by their Expected ELO Gain, providing a prioritized list of what to study.
The summary is presented as a series of "cards" for readability:
--- Hunt Summary ---
Top opportunities ranked by expected ELO gain over 100 games:
1. ELO Gain/100: +9.42 | Reachable: 10.07% | Move: f5 | ΔEV: -12.4 (good for Black)
Line: e4 e5 Nf3 Nc6 Bb5 f5 (Ruy Lopez: Schliemann Defense)
URL: https://lichess.org/analysis/pgn/1.e4%20e5%202.Nf3%20Nc6%203.Bb5%20f5
2. ELO Gain/100: +3.54 | Reachable: 23.06% | Move: d4 | ΔEV: +6.4 (good for White)
Line: e4 e5 Nf3 Nc6 Bb5 d6 d4 (Ruy Lopez: Steinitz Defense)
URL: https://lichess.org/analysis/pgn/1.e4%20e5%202.Nf3%20Nc6%203.Bb5%20d6%204.d4
...
This format allows you to quickly identify the most profitable lines to add to your repertoire.
These modes generate a series of PNG charts saved to the plots/ directory (under plots/white/ or plots/black/ depending on which side is the forcing player), organized by opening. Four distinct charts are created for each analysis:
- Performance: Expected Elo gain per 100 games when the forcing player plays this opening. Computed as
EV × 7.5(see "Expected ELO Gain / 100 Games" above). Unlike the hunt's metric, this is not multiplied by reachability — the chart shows what happens given you reach the position. A baseline line shows the same metric for the average game in that rating bucket; above the baseline means the opening outperforms the average opener. - Reachability: Shows the percentage chance of reaching the position if one player wants it. This is a key metric for practicality.
- Popularity: Shows the raw percentage of games that feature this line. This helps you understand how mainstream the opening is.
- Surprise: A calculated metric (Reachability / Popularity, always ≥ 1) that highlights lines that are easy for one player to force but are not well-known by the general player base. High surprise factor lines can be potent weapons.
When comparing multiple speeds (e.g., plot e4 c6 --speeds blitz,rapid) or multiple openings (e.g., compare "e4 c5" "e4 e6"), the lines will be drawn on the same chart with a legend for easy comparison.
Reading speed comparisons: remember that Lichess maintains independent Glicko-2 ratings per time control. The same x-axis bucket label points at a different player population in each speed pool — see "Reading the Data Behind the Plots" above for the population-volume context.




