I recently finished building a small suite of puzzle and game solvers that all run entirely in the browser — no backend, no API calls, no machine-learning models. You paste in a Sudoku, a chess position, or a crossword pattern, and the answer comes back instantly, computed on your own device.
The fun part wasn't the UI. It was that each puzzle turned out to be a textbook excuse to reach for a different classic algorithm. Nine solvers, and I got to use constraint propagation, adversarial search, heuristic search, brute-force scanning, and plain old pattern matching — the stuff that shows up in an algorithms course and then, in most day jobs, never again.
This is a tour of which algorithm fits which puzzle, and a few of the potholes I hit along the way. Everything here is vanilla JavaScript running in a Web Worker.
The one design constraint: no server
Before the algorithms, the rule that shaped all of them: it has to run client-side. That's a privacy choice (your puzzle never leaves the tab) and a cost choice (no compute bill), but it's also a fun forcing function. You can't lean on a beefy backend or a hosted model — you get one browser thread (well, a Worker thread) and whatever you can compute in a few hundred milliseconds.
That budget is exactly why classic algorithms shine here. They're fast, deterministic, and small enough to ship as a script. Let's group the solvers by the technique each one leans on.
Family 1: Constraint propagation
Sudoku
Sudoku is the poster child for constraint propagation. A cell that can only be one value forces that value; that in turn shrinks its neighbours' options, which forces more cells, and so on. Most "easy" and "medium" boards fall over from propagation alone (naked singles + hidden singles), and only the hard ones need a backtracking search on top.
The nice property: the same engine that solves the board also powers the hint feature (find the next forced cell and explain why it's forced) and a uniqueness check — count solutions, stop at two, and you know instantly whether a puzzle is well-formed.
solve(board):
propagate singles until stable
if solved: return board
pick the cell with the fewest candidates
for each candidate:
place it, recurse
if it leads to a solution: return it
otherwise undo
Minesweeper
Minesweeper is the same idea wearing different clothes. Each revealed number is a constraint: "exactly N of my hidden neighbours are mines." Treat the frontier as a system of those constraints and you can deduce two things exactly:
- Cells that are provably safe in every valid arrangement.
- Cells that are certainly mines in every valid arrangement.
And for everything the logic can't settle, you enumerate the valid mine placements over the frontier and report an exact probability per cell. So instead of "looks risky," you get "this cell is a mine 25% of the time." That exact-probability step is the difference between a toy and something you'd actually trust mid-game.
Family 2: Adversarial search (minimax / negamax + alpha–beta)
Three of the solvers are two-player games, and they all run on the same engine idea: look ahead, assume the opponent plays their best reply, and pick the move that's best for you after they do. That's minimax, and its tidier sibling negamax, with alpha–beta pruning to skip branches that can't change the outcome.
Tic-Tac-Toe
The game tree is tiny (at most 9! leaves, far fewer in practice), so you can search it exhaustively. That means the solver is perfect: from an empty board it correctly reports that every move leads to a draw with best play, and it never misses a win or a block. Tic-Tac-Toe is where you go to convince yourself your minimax is actually correct before you trust it anywhere harder.
Connect 4
Same algorithm, much bigger tree — so representation matters. I store each position as a pair of bitboards (using BigInt), which makes "is this a win?" a couple of bit shifts and masks instead of a loop over cells. With a 7-bit column stride, win detection is four shifts:
function hasWon(bb) {
// 1 = vertical, 7 = horizontal, 6 and 8 = the two diagonals
for (const shift of [1n, 7n, 6n, 8n]) {
const m = bb & (bb >> shift);
if ((m & (m >> (2n * shift))) !== 0n) return true;
}
return false;
}
With that speed, an iterative-deepening negamax with a time budget gets you strong play from any legal position.
Chess
Same family again, one honesty note. The chess solver uses negamax + alpha–beta with a hand-written evaluation (material, position, king safety, mate detection) and shows an eval bar, the best move, the likely line, and mate-in-N. It plays at a solid club level.
It is emphatically not Stockfish. I think it's worth saying that out loud in the tool itself, because "chess engine" carries Stockfish-sized expectations, and a few-hundred-line negamax is a different (and, honestly, more readable) thing. It's a great way to actually see how positional search works, not a bench-topping engine.
Family 3: Heuristic & shortest-path search
Maze
Draw a maze, mark start and end, and you want the shortest path. That's breadth-first search — the textbook shortest-path algorithm for an unweighted grid. BFS explores in rings outward from the start, so the first time it reaches the goal, it's reached it by a shortest route. No heuristics needed; the grid is small and BFS is exact.
Sliding puzzle (8- and 15-puzzle)
This is the one that fought back. The state space of the 15-puzzle is enormous, so plain BFS is hopeless. The right tool is IDA* (iterative-deepening A*) with an admissible heuristic — I use Manhattan distance plus linear-conflict.
Two things I learned the hard way:
Optimal 15-puzzle solving is genuinely slow on hard scrambles. For the 4×4 I switched to a weighted IDA* (inflate the heuristic slightly): it returns a near-optimal solution in milliseconds instead of a perfectly optimal one in minutes. For the 3×3 it stays fully optimal and instant.
My backtracking was subtly wrong and it cost me an evening. When you make a move you swap the blank with a tile; to undo it you have to restore both squares. My restore reused a variable I'd already reassigned to the new blank position, so it zeroed the wrong square and corrupted the search — which happily returned invalid "solutions." The tell: 1-move scrambles worked (the winning branch never backtracks), harder ones didn't. Lesson: always replay-verify a search solver on hard instances, not just the easy ones that pass by luck.
Family 4: Brute-force scanning & pattern matching
Two word tools round things out, and neither needs a search tree — sometimes the right move is to just try everything.
Word Search
Given a grid of letters and a list of words to find, the solver checks every starting cell and scans outward in all eight compass directions — N, S, E, W and the four diagonals — until it either matches a word or runs off the grid. It's unashamedly brute force, but for a normal-sized puzzle the search space is tiny, and covering all eight directions catches every way a word can be hidden, including backwards and diagonal. Sometimes the right algorithm really is "try them all."
Crossword
The newest one, and the simplest algorithmically — a nice palate cleanser after IDA*. You type a pattern like c_o___o_d (known letters, _ for blanks). Turn that into a regex (^c.o...o.d$), filter a large English word list to same-length matches, and rank common words first:
const re = new RegExp('^' + pattern.replace(/_/g, '[a-z]') + '$');
const matches = words.filter(w => w.length === pattern.length && re.test(w));
The whole thing leans on a ~172,000-word list that was already bundled for the site's word-unscrambler tools, so the "engine" is a dozen lines. c_o___o_d → crossroad, crossword. Not every puzzle needs a search tree.
The takeaway
The thing I keep coming back to: classic algorithms are underrated for this kind of work. Nine puzzles, and between them constraint propagation, minimax/negamax + alpha–beta, BFS, IDA*, an eight-direction scan, and a regex covered every case — no model, no server, all fast enough to feel instant in a browser tab.
If you're learning these techniques, puzzles are a fantastic sandbox, because the correctness bar is unforgiving: a solver that's almost right produces an obviously wrong answer, so you find your bugs fast (see: my sliding-puzzle evening).
If you want to poke at the finished versions, they're all free and browser-only here: lkforge.com/tools/solvers.
Happy to go deeper on any one of these in the comments — the Minesweeper exact-probability step and the weighted-IDA* tradeoff are both good rabbit holes.
Top comments (0)