Vectors to the King: Detecting Check Mathematically

Cover Image for Vectors to the King: Detecting Check Mathematically
William Forty
William Forty

The problem: given an 8x8 board with a king somewhere on it, decide whether the king is in check. That's it. One bit of output.

The obvious approach is to do what a chess engine does. Walk every enemy piece, generate its legal move set, ask whether the king's square shows up in any of those sets. It works. It's also the kind of solution that makes you feel like you've written a chess engine when all you wanted was a yes-or-no answer. You're computing thousands of squares the king will never occupy in order to learn one fact about the square it currently does.

There's a better question hiding in that.

Invert the question

Instead of "where can every enemy piece go?", ask "who can reach me?" The king is fixed. Every attacker's threat geometry, relative to the king, is a vector — a direction and a distance. There are only a handful of directions that matter, and for the pieces that care about distance (knights, pawns) there's only one distance that matters.

So stop simulating moves. Treat the board as a 2D plane, compute each piece's vector to the king, and read off the answer.

The vector

Flatten the board into a 64-cell array and find the king's index. Modulo 8 gives the file, integer-divide by 8 gives the rank:

const flat = board.flat();
const kingIdx = flat.indexOf("K");
const kingX = kingIdx % 8;
const kingY = Math.floor(kingIdx / 8);

Now walk every other square and compute, for each piece, the displacement to the king plus two scalars derived from it: the angle (using atan2, converted to degrees) and the magnitude (Pythagoras, or Math.hypot if you want to be polite about it).

const vectors = board.flatMap((row, y) =>
  row.map((piece, x) => {
    const dx = x - kingX;
    const dy = kingY - y;
    const angle = (Math.atan2(dy, dx) * 180) / Math.PI;
    const magnitude = Math.hypot(dx, dy);
    return { piece, angle, magnitude };
  })
).filter(v => v.piece !== " " && v.piece !== "K");

That's five numbers per square: x, y, dx, dy, then collapse the last two into (angle, magnitude). The board is now a flat list of vectors pointing at the king.

Line of sight is just a sort

Sliding pieces — bishops, rooks, queens — only matter if nothing's in the way. The naive version of "line of sight" is a loop that walks each square between attacker and king. You don't need it. If two pieces share an angle to the king, the closer one obscures the further one. Period.

So sort by magnitude descending, then reduce into a Map keyed by angle. Each rewrite overwrites the further piece. What's left is the nearest piece on every occupied ray:

const nearest = vectors
  .sort((a, b) => b.magnitude - a.magnitude)
  .reduce((map, v) => map.set(v.angle, v), new Map());

The Map now holds, for each angle, the only piece on that ray that can possibly check the king. No raycasting. No walking squares. The sort did the line-of-sight work.

The check tests collapse to arithmetic

With the candidates filtered, the per-piece-type tests are one-liners against angle and magnitude.

Bishop — must sit on a diagonal. A diagonal is any angle whose absolute value mod 90 equals 45:

if (piece === "B" && (angle + 180) % 90 === 45) return true;

Rook — must sit on an orthogonal. Same trick, mod 90 equals 0:

if (piece === "R" && (angle + 180) % 90 === 0) return true;

Queen — diagonal or orthogonal, so mod 45 equals 0:

if (piece === "Q" && (angle + 180) % 45 === 0) return true;

Knight — angle is irrelevant. A knight attacks the king if and only if its distance is exactly √5 (two squares one way, one the other: √(4+1)). One number:

if (piece === "N" && magnitude === Math.sqrt(5)) return true;

Pawn — distance √2 (one diagonal), with the angle constrained to one of two diagonals depending on which colour you're checking. Same logic, narrower angle filter.

That's the whole detector. No move generation, no raycasting, no per-piece-type direction tables. The geometry does the work.

The lesson

This is what I mean by "five numbers." Once you've reframed the board as vectors to the king, every chess rule that previously felt like a procedure becomes a predicate on two scalars. Bishop = mod 90 is 45. Rook = mod 90 is 0. Knight = magnitude is √5. The branching disappears.

The move that unlocked it wasn't clever. It was inverting the question. The naive solution asks each piece "where can you go?" and crosses its fingers that the king's square turns up. The vector solution asks the king "who's pointing at you, and is anything in the way?" — and the second half of that question collapses into a sort.

Most "elegant" solutions I've stumbled into look like this in retrospect. There's a question being asked in the obvious direction, and a much smaller question hiding behind it in the inverse direction. The simulation approach iterates the world. The vector approach interrogates a single point. One bit of output deserves one point of inquiry.

If you find yourself generating every possibility to check one fact, stop and flip the arrow. The answer is usually waiting on the other side.