Play it first
This is the solver from the session, running in your browser. Nothing goes to a server. You are red. Pick how far it looks ahead, then drop a piece.
Your move. You are red.
At 3 moves ahead it is easy to trap. Turn on "Show what it sees for you" to see each of your columns marked as a win, a loss, or even, with how many moves it takes.
How it started: agents with a mouse
c4 began as a benchmark for hand, my tool that lets an agent use a Mac: it reads the screen and clicks, and it checks every click. The game ran in Chrome in a macOS virtual machine, on a page with 7 buttons. Each turn the agent read the page, picked a column and clicked. I played the other side.
Those numbers come from the game files the server saved: 10 games where an LLM made at least one move, from 2026.11.08 to 2026.11.10. The named models were Claude Sonnet, Claude Opus and GPT; the first games did not record the model. Half the games I stopped early. The other half I won.
Tuning the agent with a judge
On the first day, 2026.11.08, I tried to make the agents better with a loop. Two hand-driven agents played each other. A judge model looked at the results, changed one thing, and played a fresh batch of 4 games. It kept the change only if moves got 10% faster or blunders fell, with no new illegal moves. A solver decided what counted as a blunder: a missed forced win, or a move into a forced loss when a safe move existed.
The prompt it kept:
You play Connect 4: 7 columns numbered 1-7, 6 rows.
From the page outline, reconstruct the board and count the pieces
in every column; a column with 6 pieces is full and cannot be played.
Pick your move: take an immediate win if one exists, otherwise block
an opponent's immediate win, otherwise prefer the centre.
Before answering, re-check that your chosen column is not full.
Reply with ONLY the column number, a single digit 1-7, no other text. Two things stayed with me. The first change made moves 6 times faster, because it stopped the model from writing out its reasoning. And the two "stronger" models thought longer and made more illegal moves. Blunders never fell below 1 a game. That loop cost $2.50.
The opening eval
That evening I noticed the agents only played offense. So I gave them the opening I use against everyone I play. It is three moves long:
- Red goes down the middle, column 4.
- Most people stack on top of it. The agents did too.
- Red plays right next to its first piece, left or right.
Now yellow must block one end of the bottom pair. If it does not, red adds a third piece with both ends open and wins next turn, whatever yellow does. Here it is against the solver:
red 4 · yellow 4 · red 3
At 1 or 3 moves ahead the solver stacks on column 4 again and loses. From 5 moves ahead it blocks, on both sides. The trap needs a 5-move look to see.
The LLMs did worse than depth 3. Three setups, each tried 6 times, blocked 0 times, and every answer was column 4: glm-5.3 with thinking on, glm-5.3 with thinking off, and kimi-k2.6 with thinking off. The same models were better at plain one-move puzzles, but still uneven: blocking an immediate win, glm-5.3 got 4 of 12 right and kimi-k2.6 got 11 of 12.
I then pointed the judge loop at this opening alone. One prompt line about "an opponent open two on the bottom row" took glm-5.3 from 0 to 8 of 8 blocks. But that was through the direct model call. In the live game in the browser, with thinking on, it blocked 2 of 4 times. The other 2 times it wrote about 40,000 characters of reasoning for 98 to 133 seconds, gave no answer, and the game fell back to column 4.
A prompt line fixed one trap in one setting. Moving the solver from depth 3 to depth 5 fixes this trap and every other trap that fits in 5 moves.
The solver
Every one of those prompt lines is a rule, and rules are what code is for. The benchmark already had the rules as code: the grader was a minimax search with alpha-beta pruning, written in the first hour, before any agent played. On 2026.11.11 I let it play too. It tries every column, then every reply, a fixed number of moves deep, and skips branches that cannot change the answer. Here is the core of it:
const search = (board, toMove, depth, alpha, beta) => {
if (winner(board) === other(toMove)) return -depth;
if (isFull(board) || depth === 0) return 0;
let best = -Infinity;
for (const col of orderMoves(board, legal(board), toMove)) {
const score = -search(drop(board, col, toMove), other(toMove), depth - 1, -beta, -alpha);
if (score > best) best = score;
if (best > alpha) alpha = best;
if (alpha >= beta) break;
}
return best;
}; A win that comes sooner scores higher, and a loss that comes later scores higher, so it takes fast wins and delays losses. Moves are tried centre first, with wins and blocks at the front of the list, which makes the pruning cut much more.
In the one full game I played against it at depth 9, it won in 34 moves. Its median reply was
471 ms and its slowest was 2.2 s. The same file also grades moves, which is how the judge loop
above counted blunders. A 9-case check, solver-check.ts,
covers wins, blocks, full columns and grading. One of those cases came from a bug in the first
version, at positions where the game was already over.
Watch two solvers play
With a solver on both sides, I could ask a cleaner question: how much does looking further ahead matter? Set the depths and press play. Drag the slider to replay the game move by move.
I ran each pairing 7 times, once for each opening column. When the weaker side moves first, the deeper side wins most of the time:
Depth 3 against depth 7, with red opening in column 1, ends in 12 moves. That is the game loaded above. Depth 7 as red against depth 3 is less one-sided than I expected: 5 draws and 1 loss. The deeper side goes first, takes the centre, and the shallow side defends well enough to fill the board. At depth 9 on both sides, every opening is a draw at 42 moves.
Two depth-9 solvers, for comparison:
Connect 4 is solved: with perfect play, red wins by starting in the centre. A draw here means depth 9 cannot see far enough to find that win, not that the game is even.
One more try: a solver with no if
I also tried to build a smaller player in Handwired, Cheng Lou's style of writing a program as a fixed list of number operations. It looks one move ahead only: win now if you can, block if you must, else play the centre. The circuit is 1,627 operations and runs in 0.07 ms.
While writing this post I tested it on 300 random positions, and it was broken. It took every win, but it missed 34 of 41 blocks. The block was weighted zero, and a different wire looked at the board before the move:
const oppWin = virtualWin(vbOpp, them);
const oppCurrentThreat = hasWin(2);
const score = circuit.affine(
[[myWin, 10000], [oppWin, 0], [oppCurrentThreat, 5000], [center, 10], [notFull, 1]],
0,
); The fix was to weight the right wire:
const score = circuit.affine(
[[myWin, 10000], [oppWin, 5000], [center, 10], [notFull, 1]],
0,
); After that it missed 2 wins. Two winning lines in one move summed to 2 instead of 1, so a block could tie with a win. Clamping the sum to 1 fixed that. Now, on the same 300 positions: 0 illegal moves, 70 of 70 wins taken, 41 of 41 blocks made, and the same column as the depth-9 solver 269 times. It is not embedded here because it depends on a local clone of Handwired.
This is the same lesson again, one level down. Reading the circuit, I thought it blocked. Only a test against the search solver showed that it did not.
What I took from it
- The grader was the better player. 69 LLM moves cost $1.50 and won nothing. The code that graded them costs nothing a move and beat me.
- A prompt line is a rule with no tests. "Block open twos" fixed my opening in one setting and half-failed in the live game. Depth 5 blocks it every time.
- A bigger model is not a fix. Both stronger models made more illegal moves than the one I had, at 10 to 15 times the time a move.
- Keep the model for the parts that are not rules. Reading a page and clicking the right button is where hand and a model still help. Picking the column is not.
- Check the checker. The Handwired player looked right and was not. A deterministic reference made the bug cheap to find.
What if
Standard Connect 4 was solved in 1988: with perfect play, red wins by starting in the centre. Starting in either column next to the centre draws, and the outer four columns lose. I ended the session sad about that, and with a list of questions. Change one rule and most of them have no answer I know of.
A match of 5, winner goes first. I asked this one first. Perfect play makes it 5-0, so the coin flip decides the whole match. With "loser goes first" the players take turns, and the coin-flip winner takes it 3-2. With the depth-9 solver above, every game is a 42-move draw, so a match is 0-0 and the rule does not say who goes first after a draw.
Two of these I want to build. The first is the 1988 proof itself. It used nine strategy rules, not brute force. Can a model find rules like those and write them as code? That is the same job the grader did here, one step further.
The second is a search for the fairest variant. Make the board width, the height, the line length and gravity into settings, and let the solver play thousands of rule sets and score each one for balance. The solver becomes the judge, and what comes out is a new game. Because depth 9 is not perfect, the scores would describe good players, not prove anything.
Fine print
What this does not prove
- LLM numbers are from 10 games against one person, me, with prompts that changed between games. They are not a benchmark of the models.
- The LLM time includes reading the page through hand in a macOS virtual machine, not only thinking. The judge table splits see, think and act; thinking was most of it before the prompt change.
- Depth 9 is not perfect play. It cannot find red's known forced win from the centre.
- The solver timings are from my M-series laptop with Bun. Your browser will differ; the board above shows the real time for each reply.
- Connect 4 is a small, fully known game. Many tasks that agents do have no solver this clean.
- The solved results in "What if" (Connect 4 in 1988, Gomoku, Qubic, the nine rules) are from the published literature as I remember it. I did not check them in this session.
Run it yourself
The solver and board on this page are in src/lib/c4, with tests. The game server, judge loop and notes are not published yet.
bun test src/lib/c4 # 6 cases
The judge loop's best agent still made one blunder a game and took 3.9 s a move. The solver on this page plays the same game in the time it takes the piece to fall.