This document describes the full analysis pipeline from game loading to final accuracy calculation. All details are sourced from the application source code.
Game loaded (PGN / API / text)
│
▼
PGNParser -converts to GameAnalysis + MoveAnalysis objects
│
▼
AnalysisWorker (background QThread, keeps UI responsive)
│
▼
EngineManager -communicates with Stockfish via UCI protocol
│
▼
For each position: query engine → cache check → store multi-PV results
│
▼
Classify & Calculate Stats:
┌─ Win Probability (sigmoid from centipawns)
├─ Win Probability Loss per move
├─ Move Classification (10 categories via priority rules)
├─ Book Move detection (SQLite + Polyglot)
├─ Per-move Accuracy % (exponential decay)
└─ Game Accuracy (volatility-weighted + harmonic mean)
│
▼
Save to game_history database (SQLite)
│
▼
UI update -move list, eval graph, stats panel
File: src/backend/storage/pgn_parser.py
The parser uses python-chess to read raw PGN data and convert it into GameAnalysis and MoveAnalysis dataclass instances.
@dataclassclass GameAnalysis: game_id: str metadata: GameMetadata moves: List[MoveAnalysis] summary: Dict[str, Any] ai_summary: Optional[str] pgn_content: Optional[str]@dataclassclass GameMetadata: white, black, event, date, result, headers starting_fen, white_elo, black_elo, time_control eco, termination, opening, source, chess960@dataclassclass MoveAnalysis: move_number, ply, san, uci, fen_before eval_before_cp, eval_before_mate, best_move best_eval_cp, best_eval_mate, pv eval_after_cp, eval_after_mate win_chance_before, win_chance_after classification, explanation multi_pvs, summary time_left, time_spent, raw_clk is_book_move, book_move_count, book_exit_move eco, opening_name, candidate_continuations
Clock data is parsed from PGN [%clk] comments. Chess960 games are detected and the UCI_Chess960 engine option is set.
File: src/backend/analysis/engine.py
EngineManager manages the Stockfish process via the UCI protocol using chess.engine.SimpleEngine.
engine = chess.engine.SimpleEngine.popen_uci(engine_path)
On Windows, CREATE_NO_WINDOW flag is used to suppress the console window.
Priority order:
config.jsonshutil.which("stockfish"))Result is cached for the process lifetime. Call invalidate_engine_cache() if the config changes.
Default settings (defined in src/constants.py):
| Parameter | Default | Description |
|---|---|---|
| Threads | 1 | CPU cores (conservative -prevents laptop overheating) |
| Hash | 128 MB | Memory for position cache |
| Depth | 18 | Search depth in half-moves |
| Multi-PV | 2 | Number of candidate lines |
| Live Analysis Time | 0.5s | Time budget for live mode |
engine.analyse(board, limit=chess.engine.Limit(depth=depth), multipv=multi_pv)
Returns a list of info dictionaries, each containing:
pv: Principal variation (sequence of moves)cp: Centipawn evaluationmate: Mate distance (or None)File: src/backend/storage/cache.py
Before querying the engine, the cache is checked using a SHA-256 hash of the FEN and Multi-PV setting:
cache_key = SHA256(f"{fen}|multipv:{multi_pv}")
Cached results are depth-aware: a cached result is only used if its depth >= requested depth. The cache uses WAL journal mode for concurrent access.
File: src/backend/analysis/math_utils.py
Raw centipawn scores are converted to win probability (0.0–1.0) using a logistic sigmoid formula:
multiplier = -0.00368208 * cp
win_percent = 50 + 50 * (2 / (1 + exp(multiplier)) - 1)
wp = win_percent / 100.0
Clipping:
multiplier > 40 → wp = 0.0multiplier < -40 → wp = 1.0| Position | CP | Win Probability |
|---|---|---|
| Equal | 0 | 50% |
| Slight advantage | +150 | ~66% |
| Clear advantage | +350 | ~85% |
| Winning | +600 | ~97% |
| Mate in 3 | Mate(3) | 100% |
File: src/backend/analysis/move_classifier.py
Each move is classified based on Win Probability Loss (WPL) -the difference in win probability before and after the move, from the player's perspective:
if side == "white":
wpl = wp_before - wp_after
else:
wpl = wp_after - wp_before
if wpl < 0: wpl = 0
| Priority | Classification | Condition |
|---|---|---|
| 1 | Best | Move delivers checkmate (san.endswith('#')) |
| 2 | Miss | Had forced mate, failed to find it |
| 2.5 | Best | Position is ≥99% winning after move |
| 3 | Brilliant | Only winning move, >40% better than second-best, position improved >10%, not already winning |
| 3 | Great | Only good move, >15% better than second-best |
| 3 | Best | Move matches engine's top recommendation (uci == best_move) |
| 4 | Blunder | WPL ≥ 19%, WP after ≤ 50%, WP before > 35%, not extremely losing already |
| 5 | Miss | WP before > 70% and after < 50%, or similar thresholds |
| 6 | Mistake | WPL ≥ 8% |
| 6 | Inaccuracy | WPL ≥ 4.5% |
| 6 | Good | WPL ≥ 2% |
| 6 | Excellent | WPL < 2% |
File: src/backend/analysis/analyzer.py (method _check_book_move)
Moves are checked against two databases:
a.tsv through e.tsv).bin file, if configured)If a move matches known opening theory, it receives classification Book and 100% accuracy.
File: src/backend/analysis/math_utils.py
diff = wp_before_pct - wp_after_pct # both in [0, 100]
if diff <= 0: return 100.0
raw = 103.1668 * exp(-0.06 * diff) - 3.1669
clamp to [0.0, 100.0]
| Condition | Accuracy |
|---|---|
| Book move | 100% |
| Delivers checkmate | 100% |
| Leads to forced mate | 100% |
| Minimum floor | 5% (prevents harmonic mean collapse) |
File: src/backend/analysis/analyzer.py (method _calculate_final_accuracy)
Uses the Lichess-style algorithm combining volatility-weighted mean and harmonic mean.
A sliding window of size max(2, min(8, len(accuracies) // 10)) computes standard deviation of win percentages:
for each window: weight = max(0.5, min(12.0, std_dev(window)))
Higher volatility = more critical position = higher weight.
weighted_mean = sum(accuracy[i] * weight[i]) / sum(weights)
harmonic_mean = n / sum(1 / accuracy[i] for each move)
More sensitive to low accuracy moves than arithmetic mean.
final_accuracy = (weighted_mean + harmonic_mean) / 2clamp to [0, 100]
After analysis completes, results are saved to the local SQLite database via GameHistoryManager.save_game().
The games table stores:
summary_json)See Storage for the complete database schema.
| Platform | Primary Method | Key Difference |
|---|---|---|
| Chess.com | Proprietary CAP formula | Adaptive thresholds based on rating |
| Lichess | Volatility-weighted + harmonic mean | Open source, position-weighted |
| Chess Analyzer Pro | Same as Lichess | Decay constant 0.06 (vs ~0.0435) to better match Chess.com scale |
Source: Lichess AccuracyPercent.scala