Skip to content

Move Grading

Every move in every game is graded by comparing the position's win probability before and after the move. This produces a classification and an accuracy score. Both are displayed in the post-game review.

Win probability

The foundation of all grading is win probability, not raw centipawns. Centipawns from the engine are converted to win probability using the lichess formula — an S-curve clamped at ±1000 cp (at those extremes, win probability is effectively 100% or 0%). Mate evaluations are treated as ±1000 cp.

Win% is position-invariant in a way raw centipawns are not: a 200 cp advantage in a closed endgame is more decisive than 200 cp in a sharp tactical position. Win% captures that distinction.

POV normalisation

All evaluations are stored from White's perspective. When it is Black's turn to move, the engine's centipawn score (which is side-to-move-relative) is negated before storage.

This normalisation is applied in both UciEngineClient (production) and ScriptedEngineClient (tests), and covers every MultiPV line, not only the top line. The shared utility is src/shared/pov.js.

Classification

Move quality is determined in two stages:

Stage 1 — win% loss thresholds (applied first):

ClassificationThreshold
Blunder ??win% loss ≥ 30 points
Mistake ?win% loss ≥ 20 points
Inaccuracy ?!win% loss ≥ 10 points

Stage 2 — centipawn loss tiers (for moves that pass stage 1):

ClassificationThreshold
OKcp loss ≥ 50
Goodcp loss < 50
Great !cp loss < 25
Best !!engine's top choice

The colour palette used in the UI (from src/shared/quality.js):

TierColour
Blunder#dd7065
Mistake#b85a50
Inaccuracy#8f4a45
OK#6f6f69
Good#256abf
Great#3987e5
Best#6da7ec

The thresholds are defined in src/shared/balance.js: BLUNDER_WIN_PTS = 30, MISTAKE_WIN_PTS = 20, INACCURACY_WIN_PTS = 10, GREAT_CP_MAX = 25, GOOD_CP_MAX = 50.

Accuracy

Per-move accuracy: returns 100 when winAfter ≥ winBefore — good moves are not penalised for playing in an already-winning position. Below that threshold, the score follows a smooth 0–100 curve based on the magnitude of the win% loss.

Game accuracy: blends two means:

  • Harmonic mean — rewards consistency (penalises the worst moves heavily)
  • Volatility-weighted mean — weights critical positions more heavily

The final score is 1–100 and displayed as a tile on the review page.

Phase detection

Every ply is tagged with a game phase:

PhaseCondition
Openingply ≤ 20 AND castling rights still present for either side
Endgameno queens on the board AND ply ≥ 28
Middlegameeverything else

Phase is used to break down mistakes in the stats page (opening/middlegame/endgame breakdown bars).

Findability

Findability measures how discoverable the correct move is to a human player. It is computed in pass 3 of the analysis pipeline using Maia policy weights.

findability = P_maia(Stockfish best move)

The Maia model used for the probe is calibrated to play like the player's recent opponents. A move with findability = 0.8 is one that 80% of Maia players would find; findability = 0.02 is a computer move that essentially no human considers.

Puzzle gate: findability ≥ 0.04 → drillable puzzle. Below this threshold, the puzzle is tagged engine_only — visible in the review page's mistake list, but not added to the drill queue. There is no value in practising a move that cannot be learned through repetition.

Instructiveness: instructiveness = win_loss_pts × findability. A blunder that was findable (high instructiveness) is worth more practice time than a blunder on an obscure engine move (low instructiveness). Instructiveness is the primary sort key for the drill queue.

Temptation: temptation = P_maia(played move). When temptation is high, the puzzle is tagged common_trap — many players would make the same mistake. This tag is visible in the review and quiz views.

Released under the MIT License.