A devlog by Neeraj Kotwani, who builds every game on Elipar.
Elipar used to have chess. It ran Stockfish, which is somebody else's engine, and when I decided every game here should be one I'd written myself, it went. Rebuilding it meant writing the rules and the opponent from nothing.
Chess rules are deceptive. Moving a knight is easy. The rest of it is a pile of special cases that all interact: en passant, castling and the four separate ways to lose the right to do it, promotion, and the fact that a move which leaves your own king in check isn't a move at all. You can get all of that ninety-five per cent right and have a game that plays fine for twenty minutes and then does something illegal in front of someone who knows chess better than you.
I didn't want to ship on ninety-five per cent.
Counting instead of judging
There's a standard test for this and it's beautifully dumb. You don't ask the engine whether a move is legal. You ask it how many positions exist after N moves from a given start, and you compare that single number against the number everyone else gets.
From the opening position, four moves deep, there are 197,281. If your engine says 197,280, something is wrong and you don't get to argue about it.
The real value is in the awkward positions the chess programming community has collected over decades, each one built to break a specific thing. There's one everybody calls Kiwipete that is stuffed with castling rights, pins and captures; four moves deep it should give 4,085,603. Others exist to catch en passant capture that exposes your own king along a rank, promotion that captures on the same move, and the case where a rook gets taken on its home square and castling rights have to disappear with it.
I ran five of those positions, twenty-one counts in total, and they all matched first time. I'd be lying if I said I expected that. But the point isn't that I got lucky, it's that if I'd got it wrong I would have known within a minute rather than a month, and I'd have known roughly where to look, because each position fails for its own reason.
If you ever touch that engine's move generation, run those counts again. Nothing else finds these.
An upside-down table plays badly and looks fine
The opponent scores positions partly with tables that say "a knight here is worth a bit more than a knight there". I wrote them out visually, top row first, the way a board looks in a diagram, which means they have to be flipped when loaded.
Get that flip backwards and the engine still plays chess. Legal moves, sensible-looking development, no errors. It just quietly prefers bad squares. You would not spot it by watching. So there are assertions on seven named cells: a pawn on d2 is worth -20, the same pawn on d4 is +20, a knight in the corner is -50, a castled king on g1 is +30. If the table ever flips, those fail immediately.
The difficulty setting I had to throw away
I wanted an easy mode, and my first attempt was to add a bit of random noise to the engine's scores. Wobble the numbers, get a weaker player. It seemed obvious.
It made the engine look broken. In the opening, every reasonable move sits within about 40 centipawns of every other reasonable move, so noise of plus or minus 35 is enough to promote a genuinely silly move over a good one. My easy mode opened by shuffling a knight to the edge of the board. That isn't a weaker opponent, that's an opponent with something wrong with it, and a beginner can tell the difference even if they can't say why.
What works is limiting how deep it looks, plus occasionally having it pick its second-best move on purpose. Same underlying judgement, less of it. Easy still opens with a real book move. And I checked it's a genuine ladder rather than trusting the setting names: hard beat easy six games to nil playing both colours, and was 1,420 centipawns up after forty half-moves.
Two other things that matter more than they sound. The search works to a clock rather than a fixed depth, so a slow phone thinks shallower instead of freezing. And it always resolves captures before it stops looking, because without that it will happily win a queen and then lose its own on the very next move, purely because it stopped counting in between.
My tests were wrong twice
Worth admitting. When I tested checkmate and promotion, two cases failed, and both times the engine was right and I wasn't.
One was a stalemate position where I'd put the white king in front of its own queen, blocking the file, so it wasn't the position I thought I'd built. The other was a "mate in one" that simply wasn't mate. If you hand-write a chess position for a test, verify the position before you trust the assertion, because a constructed board that's subtly illegal or subtly wrong will fail against perfectly good code and send you hunting.
I also had a debug hook that played moves by calling the engine directly, which meant it skipped the whole pointer path, which is exactly where promotion gets triggered. So the promotion tests were testing a route no player ever takes. The hook now does nothing but set up a position, and everything else happens through real clicks on real squares.
Chess 3D is free in a browser, three difficulties, no account. The rules are right. I can show you the numbers.