Building the classics from scratch, one engineering problem at a time
Chess is a public-domain idea. So is eight-ball, so is the fifteen puzzle, so is noughts and crosses. Anyone can build them, which is why most people assume building them is easy.
It is not easy, and the difficulty is never where you would expect. The rules take an afternoon. What takes weeks is the thing underneath: making the opponent strong enough to be interesting on a five-year-old phone, making the physics unable to cheat, making the shuffle unable to hand someone a puzzle with no solution. Here are the problems that actually cost time.
Chess: the hundred-times performance cliff
The first decision was not to use a strong open-source engine. Not because they aren't excellent — they are far beyond anything one person will write — but because the strongest ones are licensed in a way that would put the terms of an ad-supported site in play. So the rules come from a permissively licensed library and the play is written here: a negamax search with alpha-beta pruning, iterative deepening, a quiescence search over captures, move ordering, and a transposition table. Standard equipment, and none of it is the interesting part.
The interesting part is a factor of a hundred hiding in the rules library.
That library has two ways to generate moves. The public one returns rich move objects and computes standard algebraic notation for every move — which internally requires a second, disambiguating generation pass to know whether to write Nf3 or Nbf3. It runs at roughly six thousand positions per second. The internal one skips the paperwork and runs at about three-quarters of a million.
At six thousand positions per second, a search is not a search. It is a random move generator with extra steps. So the search runs on the internal path and the interface keeps the public one, where the pretty notation is the entire point.
Swapping to an undocumented internal path is exactly the kind of clever move that ships a subtle bug, so both paths were verified against the standard published position counts before a single line of search was written — the number of legal positions reachable in five moves from the opening, and in four moves from a well-known tactical test position. Both matched exactly. That is what buys the right to use the fast path: not a hunch that it's the same, a proof that it is.
There was one genuine trap. The fast path does not maintain the position-repetition history that the slow path does. Miss that and the engine cannot see a threefold-repetition draw coming — it will happily repeat a position into a drawn game while believing it is winning. Repetition inside the search is therefore tracked separately and folded into the real game's history.
The last piece is about phones, not chess. Search depth is never fixed; the time budget is fixed. The engine completes a depth, keeps that move, and only then begins the next depth — abandoning any depth that runs past the deadline and playing the previous one's answer. The same code is therefore safe on a slow phone and a fast desktop: the phone simply finishes fewer iterations. And it all runs on a background thread, so even a long think never freezes the board.
Pool: proving a ball cannot pass through another ball
The table is a hundred long by fifty wide, and a ball is 2.25 across — which is a nine-foot table measured in inches, with a real pool ball in it. Every number in the physics is in those units, so a hard break is a genuine 330 inches per second, about nineteen miles an hour, rather than a number that only means something at one screen size. The renderer is the only part of the program that has ever heard of pixels.
Two properties had to be true, and "seems fine in testing" is not a standard for either.
It cannot tunnel. Every frame is cut into sub-steps small enough that no ball moves more than 0.45 units — two fifths of a ball's radius — within one of them. Two balls closing head-on therefore shrink the gap between them by at most 0.9 units per sub-step, and they touch at 2.25 apart. There is no step in which a pair can go from clear to clear through each other; the geometry forbids it. Cushions are position clamps rather than collisions, which cannot be passed at any speed, and pockets are tested against the swept segment of a ball's travel rather than its endpoint, so a fast ball cannot step over a pocket mouth.
It always settles. Friction removes a fixed proportion of speed from every moving ball every second, collisions lose energy at both the cushions and between balls, and anything slower than a threshold is parked outright. Total energy is therefore strictly decreasing with an absorbing floor, so the table coming to rest is not a hope — it is the only fixed point the system has. There is a governor that ramps friction after fifteen seconds as a belt-and-braces backstop, and a verification script that reports how often it fires across five thousand simulated breaks. It has never fired.
The other thing worth mentioning: a pool table is twice as long as it is wide and a phone is twice as tall as it is wide, so on a phone the table stands up, break end at the bottom under your thumb. The physics never learns which orientation it is in. Only the mapping from table coordinates to screen coordinates flips, so there is exactly one set of rules to reason about — and no chance of the two orientations quietly diverging.
The sliding puzzle: half of all shuffles are unsolvable
The fifteen puzzle has a famous property: exactly half of the possible arrangements cannot be reached from the solved state. Shuffle by picking a random arrangement and you hand half your players a puzzle with no solution, and they will spend twenty minutes finding that out.
The property is a parity invariant. Count the pairs of tiles that are out of order relative to the solved arrangement, add the row of the gap counted from the bottom, and whether that total is odd or even never changes under a legal move — sliding a tile swaps it with the gap, which flips the pair count and, when the move is vertical, also moves the gap's row. The two flips cancel. So every reachable arrangement has the same parity, and every arrangement with the other parity is unreachable.
There are two honest fixes. Shuffle by making a long sequence of random legal moves, which cannot leave the reachable set. Or generate an arrangement freely and check the invariant, repairing it with a single swap when it comes out wrong. This game checks, because checking is instant and also lets a test hook pose any position it likes and know immediately whether it is fair.
The general shape of that lesson recurs everywhere in this list: it is usually cheaper to verify a generated thing than to constrain the generator.
Hangman: a daily word that lasts for years
A shared daily puzzle has to satisfy two constraints that pull apart. Every device must compute the same word for the same date, with no server involved. And the sequence must not repeat for a very long time.
The first is solved by a small deterministic pseudorandom generator: the same seed produces the same sequence everywhere, so a fixed seed plus the day number gives every device the same answer with no network call.
The second was a real bug. The first version shuffled a permutation of the word list and walked it a day at a time — correct, elegant, and with a cycle equal to the size of the list. With seven themed sets of thirty-six words, that cycle was two hundred and fifty-two days. The daily repeated inside a year.
The fix was not more code. It was more words: the pool went to over a thousand, and the cycle went from under a year to nearly three. That is worth stating plainly because the instinct in that moment is to reach for cleverness — a hashing scheme, a date-mixed index, something that feels like engineering. None of it would have helped. The cycle length of a permutation walk is the size of the set, and the only lever is the size of the set.
Noughts and crosses: what perfect play is for
Three difficulty levels: one that plays loosely, one that plays well most of the time and occasionally doesn't, and one that runs a full minimax over the entire game tree with no pruning at all — because the whole tree is small enough that pruning would be a performance optimisation for a computation that already takes no time.
The top level plays perfectly, which means it cannot be beaten, only drawn. That sounds like a bad feature. It is the best one in the game, because it is the only setting that teaches: a player who keeps drawing against it has genuinely learned the game, and a player who loses has made a specific, findable mistake. A merely-strong opponent teaches nothing, because you can never tell whether you found the right move or it missed the punishment.
One detail: when several moves are exactly equally optimal, it picks among them at random. Without that, perfect play is also identical play, and the game becomes a memorisation exercise rather than a solved one.
Dots and boxes: the game is the endgame
The game most people misunderstand. Beginners take every box offered and discover that taking every box you are offered is precisely how you lose.
The strong computer level does two things beyond taking the free boxes. The first is the sacrifice that decides the game at every level of play: declining the last two boxes of a chain, so that the opponent is the one forced to open the next chain. It looks like giving away points and it is how the points get won.
The second is that once few enough lines remain — fourteen or under — it stops using heuristics and solves the position exactly, with a search memoised on which lines are drawn. That is at most sixteen thousand positions, so it returns instantly, and it plays loops, double-crosses and parity perfectly without any of those concepts being written down anywhere in the program. They fall out of the search. Every rule you can avoid encoding is a rule that cannot be encoded wrongly.
There is also an input problem worth mentioning because it is invisible until you get it wrong. A line is a few pixels thick, and giving each one a padded rectangle to be tapped either leaves dead gaps or makes neighbouring rectangles overlap near the dots. Instead the board has one pointer handler that converts a touch into board coordinates and picks the nearest undrawn line within a generous radius, so nearly every pixel of the board belongs to some line and none of them fight. And the choice is previewed on press and committed on release, so a finger that lands wrong can slide to the right line — which matters enormously here, because a misdrawn line hands over boxes and there is no taking it back.
What they have in common
Four different games, and the same lesson under all of them: the rules are the specification, not the work. The work is the property you have to guarantee — that the search finishes in time, that the balls cannot pass through each other, that the shuffle is solvable, that the daily does not repeat — and every one of those was cheaper to prove than to hope for.