Verifying Every Chess Puzzle on My Home Page with a Tiny Solver
By Amine Amine
My home page opens with a chess puzzle: White to play, mate in 2. It's the first thing visitors see, so it can't be wrong. A puzzle with two solutions, or one that's secretly a mate in 1, makes the whole site look sloppy.
I didn't want to check positions by hand, so I made the computer do it.
The definition, written as code
A mate-in-2 puzzle has exactly one first move for White such that, whatever Black replies, White can then deliver checkmate. Written down, that's three small functions on top of the chess engine that already powers Chess Master:
export function isCheckmate(pos: ChessPosition): boolean {
return generateLegalMoves(pos).length === 0 && isInCheck(pos, pos.turn);
}
/** After White's first move: does every Black reply allow mate in one? */
export function forcesMateInOne(pos: ChessPosition): boolean {
if (isCheckmate(pos)) return true;
const replies = generateLegalMoves(pos);
if (replies.length === 0) return false; // stalemate
return replies.every((reply) => {
const after = makeMove(pos, reply);
return generateLegalMoves(after).some((m) => isCheckmate(makeMove(after, m)));
});
}
A smoke test then checks every puzzle in the list:
- it's White to move;
- no legal move is an immediate checkmate (otherwise it's really a mate in 1);
- exactly one legal move passes
forcesMateInOne.
It's brute force, and that's fine. Puzzle positions are small: a few dozen first moves, a few dozen replies, a few dozen follow-ups. All 35 puzzles verify in about three seconds. In the browser, the same check runs when you make a move and takes around 2ms.
There's a nice side effect: the UI never compares your move against a stored answer. It asks the engine whether your move works. If a position ever had a second solution, it would still accept it.
Getting real puzzles, without downloading 300MB
Hand-made positions look artificial, so I wanted positions from real games. The Lichess puzzle database is released under CC0 and tags puzzles with themes like mateIn2. The catch is size: around 300MB compressed, millions of rows.
I only needed a few hundred candidates, so I streamed it and stopped early:
curl -s https://database.lichess.org/lichess_db_puzzle.csv.zst \
| zstd -dc | grep -m 600 mateIn2 > mate2.csv
grep -m 600 exits after 600 matches, which closes the pipe, which stops the download. A few seconds instead of a few minutes.
Then a selection script:
- Replays the opponent's move. Lichess stores the position before the opponent's last move, so I apply it to get the actual puzzle position.
- Filters. White to move, rating 900–2000, well-liked and played often, at most 22 pieces so it reads well on a phone.
- Verifies with my engine, and also checks that my engine's single key move matches Lichess's solution. Two independent sources agreeing is a good sign that both are right.
95 positions survived. I took every third one across the rating range, added two I'd composed myself, and shuffled them with a fixed seed so easy and hard days alternate.
What I like about this
The solver is a few dozen lines, and it replaces a checklist I would eventually have got wrong. When I add puzzles, the test either passes or tells me exactly which one is broken.
Update: weekends now get a mate in 3, verified by a generalised version of the same solver, and every past puzzle is in the archive.
If you haven't yet, go solve today's puzzle. Your streak lives in your browser, and the share button gives you a little 🟩 to brag with.