How To Measure Chess Position Complexity — ChessCheaterDetector
← Blog2026-08-22<br>How To Measure Chess Position Complexity<br>This is the first of several posts digging into the internals of ChessCheaterDetector. We'll cover one signal at a time, explain what it measures, why it matters and how we calculate it.
This post is about chess position complexity: how difficult a position is to play accurately.
Why complexity matters
Suppose the engine says there's only one sensible move in a position: recapturing a queen, getting out of check or taking a free piece.
A player finding that move isn't particularly informative. A beginner and a grandmaster may both see it immediately.
Now consider a position where the engine's top five moves might all look reasonable, but after calculating deeper, one move gradually separates itself from the others. Perhaps one move, that initially looks excellent, turns out to allow a devasting hidden tactic for the opponent at the end.
A user finding the best move in that position tells us much more.
A simple way to think about it
Imagine watching Stockfish think about the same position over and over.
At a shallow search, it might rank the moves like this:
1. Move A<br>2. Move B<br>3. Move C<br>4. Move D
After searching deeper:
1. Move A<br>2. Move C<br>3. Move B<br>4. Move D
The rankings are fairly stable. Stockfish basically knew what it liked from the beginning.
Now imagine this:
Depth 5
1. Move A<br>2. Move B<br>3. Move C<br>4. Move D
Depth 10
1. Move C<br>2. Move A<br>3. Move D<br>4. Move B
Depth 15
1. Move B<br>2. Move D<br>3. Move A<br>4. Move C
Here the engine keeps changing its mind as it calculates further.
That's a useful proxy for complexity. A position where the strongest moves remain stable as the search gets deeper is generally easier to resolve. A position where the ranking changes substantially requires more calculation and is therefore more difficult to evaluate correctly.
Position complexity measures how much the engine's view of a position changes as it calculates more.
The approach: Biswas and Regan
This is based on a method described by Biswas & Regan, "Measuring Level-K Reasoning, Satisficing, and Human Error in Game-Play Data" (ICAART 2015). The paper gives us a way to turn this intuition into a number.
We run Stockfish at a series of search depths and record the moves it considers strongest at each depth. We then compare the rankings between consecutive depths.
The more stable those rankings are, the lower the complexity. The more they change, the higher the complexity.
Generalized Kendall tau
To compare two rankings, we use a generalized version of Kendall tau distance. The idea behind it is fairly simple: look at pairs of moves and see whether their ordering has changed.
Our version takes the engine evaluations into account. A small ranking change between two moves that are almost equal matters more than the same ranking change between moves where one is already much better.
The calculation
For each position we run the Stockfish engine with multipv=50 at every depth from 1 through 19. This gives us a ranked list of the 50 best candidate moves at each depth.
We compare each depth with the next one, calculate how similar their rankings are, and average those results:
def _compute_complexity_from_scored_moves(self, scored_moves: dict) -> float:<br>summed = sum(<br>self._kendall_tau(scored_moves[depth], scored_moves[depth + 1])<br>for depth in range(1, self._max_depth)<br>return 1 - ((1 / (self._max_depth - 1)) * summed)
Stable rankings → low complexity
Unstable rankings → high complexity
Speed/Accuracy Tradeoff
There is a practical tradeoff between how deeply we search and how quickly we can analyse a position.
The depth of the Stockfish search has a large effect on analysis time. Going from depth 10 to depth 19 requires substantially more computation. Since complexity is based on how the engine's rankings change as it searches deeper, higher depths generally give us a more complete picture of the position, but at a significant cost in processing time.
The number of candidate moves we consider has a surprisingly small effect by comparison. Increasing multipv from a small number to 50 adds relatively little analysis time compared with increasing the search depth. This makes it practical to examine a large set of candidate moves without sacrificing much performance.
For ChessCheaterDetector, this means depth is the main parameter we need to balance against analysis speed, while keeping a relatively high multipv gives us more information about the stability of the engine's rankings at comparatively little additional cost.
Our current implementation searches to depth 19 with multipv=50, giving us a reasonably detailed measure of complexity while keeping analysis time mostly manageable.
A few implementation details
There are a few details worth mentioning for completeness.
We clear Stockfish's hash table between depths. This prevents a search from influencing a later calculation...