Gambit, a chess engine written from nothing
Gambit is a chess engine written from an empty file: the board, every legal move, and a search that picks one. It has no dependencies and thinks in a worker in your browser, so the page stays smooth while it does. Play it below. Its move generator is held against the published perft counts for six standard positions, and it gives the same count in every one.
Drag a piece, or click it and then a square. The panel shows what Gambit is weighing while it thinks: how many moves ahead it has looked, the score in pawns from white’s side, and the line it expects both sides to play. The game is kept in this browser when you leave.
Held against the published counts
A move generator is easy to get almost right. Castling through an attacked square, an en passant capture that leaves the king open, a pawn that takes and promotes in one move: each is a rule that a few positions in a million depend on, and a game can go on for hours without meeting one.
Perft is the standard test. From a position it counts every sequence of legal moves to a given depth, and the totals for a handful of well-known positions are published. One missing or extra move anywhere in the tree changes a total. Press Count to run it here, with the same code the board above plays with.
The page stops where a count would take more than a few seconds. The command runs every depth that is published up to a few hundred million, and this is its output:
$ node web/notes/gambit/perft.mjs --deep same Start depth 1 20 published 20 1 ms same Start depth 2 400 published 400 1 ms same Start depth 3 8902 published 8902 26 ms same Start depth 4 197281 published 197281 63 ms same Start depth 5 4865609 published 4865609 849 ms same Start depth 6 119060324 published 119060324 19736 ms same Kiwipete depth 1 48 published 48 0 ms same Kiwipete depth 2 2039 published 2039 3 ms same Kiwipete depth 3 97862 published 97862 28 ms same Kiwipete depth 4 4085603 published 4085603 720 ms same Kiwipete depth 5 193690690 published 193690690 33022 ms same Position 3 depth 1 14 published 14 0 ms same Position 3 depth 2 191 published 191 0 ms same Position 3 depth 3 2812 published 2812 1 ms same Position 3 depth 4 43238 published 43238 10 ms same Position 3 depth 5 674624 published 674624 136 ms same Position 3 depth 6 11030083 published 11030083 2401 ms same Position 4 depth 1 6 published 6 0 ms same Position 4 depth 2 264 published 264 0 ms same Position 4 depth 3 9467 published 9467 1 ms same Position 4 depth 4 422333 published 422333 77 ms same Position 4 depth 5 15833292 published 15833292 2610 ms same Position 5 depth 1 44 published 44 0 ms same Position 5 depth 2 1486 published 1486 0 ms same Position 5 depth 3 62379 published 62379 11 ms same Position 5 depth 4 2103487 published 2103487 352 ms same Position 5 depth 5 89941194 published 89941194 16449 ms same Position 6 depth 1 46 published 46 0 ms same Position 6 depth 2 2079 published 2079 0 ms same Position 6 depth 3 89890 published 89890 14 ms same Position 6 depth 4 3894594 published 3894594 1021 ms same Position 6 depth 5 164075551 published 164075551 24439 ms every count is the published one; 610,195,852 positions in 102.0 s
The board has twice as many squares as it shows
The board is an array of sixteen squares per rank, where only the left eight of each rank are real. A square’s index is its rank times sixteen plus its file. Stepping off the edge of the board gives an index where index & 0x88 is not zero, so the edge test is a single AND, the same for every piece and every direction. The format is called 0x88 after that mask.
Here is the knight’s part of that as a script you can run. It is the same test the engine makes, written out in a few lines. Step walks through it the way the engine reads it, one part of an expression at a time, with every variable beside the code. Change the square or the steps and run it again.
const KNIGHT = [33, 31, 18, 14, -33, -31, -18, -14];
const name = (s) => 'abcdefgh'[s & 7] + '87654321'[s >> 4];
function knightMoves(from) {
const moves = [];
for (const step of KNIGHT) {
const to = from + step;
if (to < 0 || to & 0x88) continue; // off the board
moves.push(name(to));
}
return moves;
}
console.log(knightMoves(0x00)); // a8, in the corner
console.log(knightMoves(0x44)); // e4, in the middle
Every legal move, by trying it
The generator first lists the moves the pieces can make without asking whether their own king is left in check. Each one is then played on the board, the king’s square is tested for attack, and the move is taken back. A move is a single integer that holds the two squares, the piece it promotes to and whether it is a capture, an en passant, a castle or a pawn’s double step.
Taking a move back restores the board, the castling rights, the en passant square and the position’s hash exactly. The tests check the hash against one computed from scratch after every move in every perft position, two moves deep.
Looking ahead, and skipping what cannot matter
Gambit searches with alpha-beta: once it has found one good line, any branch that the opponent can make worse than that line is cut without being read to the end. It searches one move deep, then two, then three, until its time is up, and keeps every position it has scored in a table keyed by the position’s hash. Each deeper pass starts with the move the last pass found best, which is what makes the cuts come early.
At the end of a line it keeps playing captures until no capture is left, so it never scores a position halfway through a trade. It also checks whether passing would still be good enough, and searches the later, quieter moves less deep unless they surprise it. The panel beside the board shows how deep that gets on your device.
What a position is worth
The score is the material on the board plus where each piece stands. Every piece has two tables of bonuses, one for the middlegame and one for the endgame, and Gambit blends them by how much material is left. A knight is worth most in the center. A king scores best behind its pawns while the queens are on, and in the middle once they are gone. Choose a piece to see its table.
What it does not have
Gambit has no opening book and no endgame tables, and it thinks on one thread. Its strength has not been measured against a rated engine, so this page gives it no rating. What is measured is the move generator, above, and the tests: mates in one and two, a hanging queen taken, notation, the ends of a game, and two copies playing a full game where every move is checked for legality.
The source is in the portfolio’s repository under web/notes/gambit/: engine.js for the board and the search, perft.mjs for the counts, engine.test.mjs for the tests.