Logo
Chess Analyzer Pro
ReleasesDocsBlogDownload

Documentation

Getting StartedUsage GuideConfigurationArchitectureFiles & DataTroubleshootingHow We Calculate AnalysisChangelogFor Developers

Chess Analyzer Pro

Professional local-first chess analysis.

Project

  • GitHub Repository
  • Download
  • Documentation
  • Report Feedback/Bug

Resources

  • Stockfish Engine
  • Beekeeper Studio
  • My Lichess Profile
  • My Chess.com Profile

Developer

  • Portfolio
  • GitHub Profile
  • LinkedIn
  • Contact Me
Ā© 2025 Utkarsh Tiwari. Open Source.

This document details the end-to-end process of how a chess game is analyzed in Chess Analyzer Pro, from the moment a game is loaded to the final display of Move Classifications and Accuracy.


1. Game Ingestion (Loading & Parsing)

The process begins when a user loads a game via PGN File, API (Chess.com/Lichess), or text/link.

The PGN Parser

The PGNParser (src/backend/pgn_parser.py) converts raw game data into our internal format.

  1. Reading: Uses python-chess to read the game structure.
  2. Conversion: Converts PGN nodes into GameAnalysis and MoveAnalysis objects.
  3. Initialization: Captures FEN, UCI, and basic metadata.

Data Snapshot: Parsed Game

Right after parsing, the data looks like this structure (simplified):

GameAnalysis(
    game_id="uuid-1234...",
    metadata={
        "White": "Magnus Carlsen",
        "Black": "Hikaru Nakamura",
        "Result": "1-0",
        "Opening": "Sicilian Defense"
    },
    moves=[
        # Move 1 (White)
        MoveAnalysis(
            san="e4", 
            uci="e2e4", 
            fen_before="rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
            classification=None, # Not yet analyzed
            eval_before_cp=None
        ),
        # Move 1 (Black)
        MoveAnalysis(
            san="c5", 
            uci="c7c5", 
            fen_before="rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1",
            ...
        )
    ]
)

2. The Analysis Worker Pipeline

The AnalysisWorker (src/gui/analysis_worker.py) runs the analysis in a background thread to keep the UI responsive. It iterates through the moves and feeds them to the Analyzer.


3. Stockfish Engine Interaction

For each move, Analyzer (src/backend/analyzer.py) asks Stockfish for the "truth" of the position.

Engine Query

We call engine.analyse(board, limit=Depth 18, multipv=3).

Data Snapshot: Engine Output

The engine returns a list of dictionaries (one for each "PV" or principal variation asked for):

[
    # PV 1 (The Best Move)
    {
        "pv": [Move.from_uci('f3e5'), Move.from_uci('d6e5')], 
        "score": <PovScore: Cp(+35) white> # 0.35 advantage for White
    },
    # PV 2 (Second Best)
    {
        "pv": [Move.from_uci('d2d4'), Move.from_uci('c5d4')],
        "score": <PovScore: Cp(+10) white>
    },
    # PV 3 (Third Best)
    {
        "pv": [Move.from_uci('b1c3')],
        "score": <PovScore: Cp(-15) white> # Slight disadvantage
    }
]

4. Evaluation to Probability

We convert raw Centipawn (CP) scores into Win Probability (0.0 - 1.0) using a logistic regression formula to normalize human error perception.

$Win% = 50 + 50 \times (2 / (1 + e^{-0.00368208 \times CP}) - 1)$

Data Snapshot: Probability Conversion

For a move with Cp(+150) (White is better):

  • Input: +150 CP
  • Calculation: Logisitic curve result.
  • Output: 0.635 (63.5% Win Probability for White)

For a mate score #M3:

  • Input: Mate(+3)
  • Output: 1.0 (100% Win Probability)

5. Move Classification

We look at the change in Win Probability (WPL) from Before the move to After the move.

The Algorithm

AnalysisWorker compares the state before the move (where we could have played the 'Best' move) vs after the move (what we actually played).

Data Snapshot: Classification Example

Let's say White is winning (0.90 / 90%). White plays a bad move, and the evaluation drops to equal (0.50 / 50%).

MoveAnalysis(
    san="Bl5?", 
    uci="b2b4",
    
    # 1. We had a winning position
    eval_before_cp=450, 
    win_chance_before=0.90, 
    
    # 2. We played a bad move, now it's even
    eval_after_cp=0, 
    win_chance_after=0.50,
    
    # 3. The Loss
    wpl=0.40,  # 40% loss in win probability
    
    # 4. Resulting Classification
    classification="Blunder",
    explanation="Win chance dropped by 40.0%"
)

Common Classifications:

  • Best: Played the top engine move.
  • Great: Played the only good move (Win Prob difference > 15% to next best).
  • Excl: WPL < 1%
  • Good: WPL < 4%
  • Inac: WPL < 9%
  • Mist: WPL < 20%
  • Blund: WPL >= 20%

6. Accuracy Calculation

We calculate an aggregate "Accuracy Score" (0-100) based on Average Centipawn Loss (ACPL).

Data Snapshot: Final Summary

After all moves are processed, game_analysis.summary is populated:

{
    "white": {
        "acpl": 15.4,          # Average CP Loss per move
        "accuracy": 92.5,      # 100 * exp(-0.004 * 15.4)
        "Brilliant": 1,
        "Great": 2,
        "Best": 25,
        "Excellent": 5,
        "Good": 3,
        "Inaccuracy": 2,
        "Mistake": 1,
        "Blunder": 0,
        "Miss": 0,
        "Book": 5
    },
    "black": {
        "acpl": 45.2,
        "accuracy": 78.1,
        # ... counts ...
    }
}

7. Persistence & UI

Finally, the fully populated GameAnalysis object is sent to the UI.

  • Move List: Renders the moves, adding ?? for Blunders (Red) and !! for Brilliants (Teal) based on the classification field.
  • Graph: Plots the eval_before_cp for each move index.
  • Stats: Displays the Accuracy % from the summary dictionary.