Methodology

Proving It's Actually Unbeatable: How We Benchmark a Game AI Before Publishing a Number

"Unbeatable" is a testable claim, not a marketing word. Before that word goes anywhere near the tic-tac-toe page, the shipped AI has to survive a headless self-play harness — the same functions the browser runs, played thousands of times with no human at the keyboard. Here is what that harness measured, including the part that didn't look good at first.

 ·  8 min read  ·  all numbers from re-runnable headless benchmarks

The Numbers

1,200
tic-tac-toe games simulated
0
losses recorded
250
2048 self-play runs
36,528
nodes/opening (α-β)

Win / draw / loss, two opponents

825 175 0 vs random (1,000 games) 0 200 0 vs perfect (200 games)

Win Draw Loss — the bar that matters is the one that never appears. Zero losses in 1,000 games against a random opponent, zero losses in 200 games against a copy of itself playing perfectly.

Completed 3x3 tic-tac-toe board from a game against the Hard AI, filled with circles and squares and no three-in-a-row, ending in a draw
A live game against the Hard AI (full-strength minimax) played out to a draw — every line blocked, no three in a row. Force perfect play and a draw is the best either side can get.

What "Unbeatable" Has to Mean

"Unbeatable" is a claim about the worst case, not the average case, so testing it against one kind of opponent proves nothing. We ran two. Against 1,000 games of a random-move opponent, the AI went 825 wins, 175 draws, 0 losses — the wins come from a weak opponent occasionally handing over a forced win. Against 200 games of the AI playing a perfect copy of itself, every single game ended in a draw: 0 wins, 0 losses, 200 draws. That is the correct result for perfect tic-tac-toe play, and it is the harder claim to satisfy — a program that merely beats random players a lot could still have a hole a perfect opponent would find.

Across both runs — 1,200 games total — the loss count is 0. That is the number the word "unbeatable" is actually standing on.

The Harness

The benchmark is headless self-play: the exact move-selection functions from the shipped page, imported into a script with no DOM and no human, playing thousands of games back to back. That distinction matters more than it sounds. A benchmark written against a re-implementation of the algorithm proves the algorithm is sound in theory; it says nothing about the code a browser actually executes. Pulling the real functions out of the production page and driving them from a script means the numbers describe the shipped build, not a clean-room stand-in that happens to share a name with it.

It also has to be re-runnable. A number that came from one afternoon of manual play-testing is an anecdote; a script that plays 1,000 games against a random opponent and 200 games against itself, and produces the same win/draw/loss counts every time it's re-run, is a measurement. Mean move time came out at about 0.3 ms across 500 sampled positions — cheap enough that running the full 1,200-game suite costs seconds, not minutes, which is what makes it realistic to re-run on every change instead of trusting the last time someone checked.

Reproduce this benchmark Run it yourself →
  • Tic-tac-toe, vs random: 1,000 headless games, shipped minimax + α-β at full depth 9. Expected: 825 W / 175 D / 0 L.
  • Tic-tac-toe, vs self: 200 games, engine vs a perfect copy. Expected: 0 W / 200 D / 0 L.
  • Depth sweep: repeat the 200-game vs-random run with search capped at each depth 1–9. Expected: win% between 77–90.5%, with 2 losses at depth 2 and 0 losses only at depth 9.
  • 2048, reach-rate: 250 self-play games, shipped expectimax + corner-snake. Expected: ~69.6% reach 2048, ~30% reach 4096, 0% reach 8192.

Every number on this page comes from these four runs against the exact move-selection functions the browser ships — no re-implementation. Counts are deterministic per seed and stable across re-runs.

Depth Is the Proof

The shipped engine searches to depth 9 — the full game tree for a 3x3 board — which is what guarantees the zero-loss result above. To find out whether that depth is actually necessary, the harness re-ran the same 200-game vs-random test with the search artificially capped at each depth from 1 to 9:

0% 25% 50% 75% 100% 1 2 3 4 5 6 7 8 9 lost 2/200 0 losses · shipped search depth → lkforge.com

The honest finding is in the depth-2 column: at a search depth of 2, the AI lost 2 of 200 games. A shallow search is not automatically safe just because it wins most of the time — 87% win rate at depth 2 looks fine on its own, until you notice the two losses hiding next to it.

Win percentage also doesn't rise monotonically with depth (it peaks at depth 3, at 90.5%, then drifts down before settling at 81% at full depth 9), because deeper search doesn't just chase more wins — past the point where a win is already forced, it starts steering into the safest won-or-drawn line instead of the flashiest one, which trades a few wins-against-random for the zero-loss guarantee. Depth 9 is the only depth in the table with a 0-loss guarantee behind it; every shallower depth is a different, weaker program that happens to share the same UI.

That is the entire argument for benchmarking instead of reasoning from the algorithm's name. "It's minimax, so it's unbeatable" is true only at one specific depth setting, and the only way to know which setting that is, is to run all of them and look for the losses.

The 2048 Case

Tic-tac-toe has a small enough state space that "unbeatable" is checkable in an absolute sense. Most games don't — 2048 has no realistic path to "solved," so the only honest claim is a measured reach-rate. The same harness pattern applies: 250 headless self-play games using the shipped expectimax-plus-corner-snake solver, no human involved.

The result is 69.6% of games reach the 2048 tile, with reach-rate falling off sharply after that (30% reach 4096, 0% reach 8192 in this run) — a strength profile, not a single number, and one that would be indistinguishable from a coin flip if we'd only run it 10 times instead of 250. The solver internals are covered separately in How the 2048 AI Solver Works; this post is only about the measurement discipline that number came from.

0% 25% 50% 75% 100% 69.6% 2048 tile 30% 4096 tile 0% 8192 tile lkforge.com

The Rule This Enforces

This is the same rule the 26-repos build retrospective landed on after a month of shipping pages with an AI pipeline: benchmark before you publish a number. Any performance or strength claim on this site has to come from a measurement that can be re-run, not a description of what the algorithm should do in theory. The depth-2 result above is exactly the kind of thing that rule is for — a plausible-sounding claim ("it's minimax, it can't lose") that turns out to be depth-dependent, and would have shipped wrong without a script that actually played the games.

Related reading: the three search algorithms these numbers come from are broken down in Six Games, Three Classic Algorithms, and the frame-budget work that keeps them running client-side is in Search Algorithms Inside One Browser Tab.

Try to Beat It

1,200 self-play games came up with zero losses. See if a human can do better against the same full-depth engine.

Play Tic-Tac-Toe →