The most critical bug in the chess analysis system was the Perspective Error, also called the "Winning Blunder" bug.
When Black made a move that increased their advantage, the system would classify it as a blunder or mistake.
Stockfish always returns evaluations from White's perspective:
- Positive values (+100cp) = White is winning
- Negative values (-100cp) = Black is winning
The old code didn't account for this when calculating the move quality.
- Black is already winning: -90cp (from White's perspective)
- Black promotes a pawn to Queen
- Position after move: -98cp (Black winning even more!)
# OLD CODE (BROKEN)
eval_before = -90 # Black winning
eval_after = -98 # Black winning MORE
# Calculate difference
diff = eval_before - eval_after
diff = -90 - (-98)
diff = -90 + 98
diff = +8 # Positive difference!
# Classification logic thinks:
# "Evaluation changed by +8cp, that's good... wait, no..."
# "Actually, eval went from -90 to -98, that's MORE negative..."
# "The position got WORSE! This is a BLUNDER!"
# ❌ WRONG CLASSIFICATIONThe old code would see -90 → -98 as getting worse (more negative = worse in absolute terms) and classify it as a blunder even though Black's position actually improved.
# NEW CODE (FIXED)
is_white_turn = False # Black's turn
# Get raw evaluation from Stockfish (White's perspective)
eval_raw_before = -90 # Black winning
eval_raw_after = -98 # Black winning more
# CRITICAL FIX: Flip sign for Black
eval_before = -eval_raw_before if not is_white_turn else eval_raw_before
eval_after = -eval_raw_after if not is_white_turn else eval_raw_after
# Now from Black's perspective:
eval_before = -(-90) = +90 # Positive = good for Black!
eval_after = -(-98) = +98 # MORE positive = even better!# Calculate centipawn LOSS (should be 0 or positive)
loss = max(0, eval_before - eval_after)
loss = max(0, 90 - 98)
loss = max(0, -8)
loss = 0 # No loss! Actually GAINED advantage!
# Classification: loss = 0 → "best" move ✅ CORRECT!Black's Turn
Eval: -90cp → -98cp
Old System Logic:
❌ "Evaluation got more negative"
❌ "Position got worse"
❌ Classification: BLUNDER
Result: Winning moves incorrectly marked as blunders!
Black's Turn
Eval: -90cp → -98cp
New System Logic:
✓ Normalize: -90cp → +90cp (Black's perspective)
✓ Normalize: -98cp → +98cp (Black's perspective)
✓ Change: +90 → +98 (+8cp improvement!)
✓ Classification: BEST
Result: Winning moves correctly recognized!
File: backend/app/engine/analyzer.py
Lines: 77-95
# OLD CODE
best_eval_cp = get_cp_evaluation(engine, fen_before,
perspective_white=(side == "white"))
played_eval_cp = get_cp_evaluation(engine, fen_after,
perspective_white=(side == "white"))
# This didn't properly normalize - it just returned different signs
# but didn't flip them correctly for classification logic
eval_diff_cp = abs(best_eval_cp - played_eval_cp) # abs() hides the problem!# NEW CODE (FIXED)
is_white_turn = (side == "white")
# Get raw evaluation (always from White's perspective)
best_eval_raw = get_cp_evaluation(engine, fen_before, perspective_white=True)
played_eval_raw = get_cp_evaluation(engine, fen_after, perspective_white=True)
# Normalize to current player's perspective
# For White: use as-is (positive = good)
# For Black: flip sign (so negative becomes positive = good)
best_eval_cp = best_eval_raw if is_white_turn else -best_eval_raw
played_eval_cp = played_eval_raw if is_white_turn else -played_eval_raw
# Now calculate LOSS correctly
# Higher eval = better, so best_eval - played_eval = loss
eval_diff_cp = max(0, best_eval_cp - played_eval_cp)Input:
- Side: Black
- Best eval (White's perspective): -90cp
- Played eval (White's perspective): -98cp
Processing:
- Normalized best (Black's perspective): +90cp
- Normalized played (Black's perspective): +98cp
- Loss: max(0, 90 - 98) = 0cp
- Win% loss: 58.9% → 59.7% = -0.8% (gained!)
Result: Classification = "best" ✅Input:
- Side: Black
- Best eval (White's perspective): -300cp (Black winning)
- Played eval (White's perspective): +200cp (White winning now!)
Processing:
- Normalized best (Black's perspective): +300cp
- Normalized played (Black's perspective): -200cp
- Loss: max(0, 300 - (-200)) = 500cp (huge loss!)
- Win% loss: 76.9% → 31.0% = 45.9% (massive loss!)
Result: Classification = "blunder" ✅Input:
- Side: White
- Best eval (White's perspective): +300cp
- Played eval (White's perspective): +350cp
Processing:
- Normalized best (White's perspective): +300cp (no change)
- Normalized played (White's perspective): +350cp (no change)
- Loss: max(0, 300 - 350) = 0cp
- Win% loss: 76.9% → 80.2% = -3.3% (gained!)
Result: Classification = "best" ✅- ❌ Black's good moves marked as blunders
- ❌ Confusing and frustrating user experience
- ❌ Analysis was actively misleading
- ❌ Made learning from analysis impossible
- ✅ All moves correctly classified regardless of player color
- ✅ Black's improvements recognized as good moves
- ✅ Consistent logic for both White and Black
- ✅ Analysis is now trustworthy and educational
The perspective error was fixed by:
- Always getting raw evaluation from White's perspective (consistent baseline)
- Normalizing to current player's perspective (flip sign for Black)
- Calculating loss correctly (higher = better for current player)
- Using win% for classification (more accurate than raw centipawns)
This ensures that:
- Positive evaluations always mean "good for current player"
- Loss calculations are always meaningful
- Classifications are consistent across both colors
- The "Winning Blunder" bug is completely eliminated
Status: ✅ FIXED AND TESTED