Verified Board Games interactive verification tools

Each tab is a game whose perfect play we study with formal verification. One game per tab — more to come.
← woodingben.com/games/
Tic-Tac-Toe
Nim
Pig (dice)

Tic-Tac-Toe play the perfect solver

With perfect play this game is a forced draw. You are playing the optimal solver — it never loses. The best you can do is draw it.
Goal: get three of your marks in a row (horizontal, vertical, or diagonal) on the 3x3 grid. Players alternate placing one mark per turn in any empty cell, X going first; you play one mark and the perfect solver plays the other. The first to complete a line wins, and a full board with no line is a draw — and since this game is a forced draw under perfect play, the solver never loses, so the best you can do is draw it.
Outcome under perfect play certified exact
Perfect play → DRAW
PERFECT SOLVER · NEVER LOSES

Certified result

Perfect-play valueDRAW
<<X>> F x_winFALSE
<<O>> F o_winFALSE
Reachable positions5,478
Canonical (mod D4)765
Optimal X vs random O191/192 win · 0 loss
Optimal O vs random X887/945 win · 1/63 loss

How we verify Tic-Tac-Toe

Tic-tac-toe is finite, deterministic, perfect-information and zero-sum, so it has an exact game-theoretic value computed by backward-induction minimax on its transition system. We enumerate every reachable position, quotient by the board's symmetry, solve the game, and discharge the result as temporal-logic / reachability properties — the same pipeline the hidden-information games use, but with the hidden information removed, so the answer is exact.

1 · From rules to a transition system

A state is the 3×3 board (9 cells over {empty, X, O}); X moves first, so the side to move is fixed by the mark counts. Transitions are the legal moves (mark any empty cell); the game ends the instant a line completes or the board fills. Different move orders reaching the same board are the same state, so the game tree folds into a DAG. BFS from the empty board enumerates 5,478 reachable positions (626 X-wins, 316 O-wins, 16 draws, 4,520 non-terminal). The unmerged tree — distinct games counting move order, stopping at a win — has 255,168 leaves.

2 · Symmetry reduction (the dihedral group D4)

The 3×3 grid is invariant under D4 — 8 symmetries (4 rotations × 2 reflections). Boards related by a rotation or reflection are the same position strategically, so we quotient by D4 (the geometric analogue of a player-relabeling symmetry). That collapses 5,478 positions into 765 canonical positions — computed two independent ways (canonicalize every concrete board; BFS the quotient) that agree exactly.

3 · Perfect play is a forced draw

We solve the game by minimax (X maximises, O minimises a +1/0/−1 payoff). The value of the empty board is 0 — a draw. As alternating-time (ATL) reachability properties:

⟨⟨X⟩⟩ F x_win — "X can force a win" → FALSE ⟨⟨O⟩⟩ F o_win — "O can force a win" → FALSE

Neither side can force a win. We don't just read off the value: we extract the optimal strategy and re-verify it by reachability — playing the hero side optimally against every opponent reply, no line ever reaches a loss for the hero. That certificate is the temporal statement ⟨⟨hero⟩⟩ (¬lose U draw_or_win). The solver you play above runs exactly this strategy — which is why it never loses.

4 · Odds vs a fallible (random) player

A forced draw holds only between two perfect players. Against a uniform-random opponent the optimal side does far better — computed exactly by rational expectimax:

Match-upMover winsDrawMover loses
Optimal X vs random O191/192 ≈ 99.48%1/1920
Optimal O vs random X887/945 ≈ 93.86%43/9451/63 ≈ 1.59%

The optimal player never loses and wins almost always — the odds bar above shows this edge.

5 · SMT (z3): an inductive soundness invariant

"At most one player has a completed line" is not a static fact about arbitrary boards (a parity-respecting board can carry two lines — those boards are just unreachable). So we prove it as a reachability invariant by induction over the move relation: z3 discharges BASE (the empty board satisfies the invariant) and STEP (every legal move preserves it) as ∀-theorems (UNSAT of each negation). That certifies the model's "stop at the first win" handling is sound for all 5,478 reachable positions without enumerating them.

6 · Honesty & limits

  • Every figure here is exact — the game is small enough to enumerate completely (5,478 positions, 765 up to symmetry, 255,168 games).
  • The forced-draw value and both ATL properties are rigorously established (minimax + reachability certificate); the in-browser solver runs the same optimal strategy.
  • The odds-vs-random figures are exact rationals (191/192, 887/945, …).
  • Note: the often-quoted "549,946" is not the stop-at-a-win game count; the well-attested exact figure is 255,168, which we compute three ways.

Nim exactly solved · perfect bot

Remove any number of objects from a single row. Last to take an object wins. The bot plays Bouton's certified strategy — beat it only if it starts from a losing position.
Goal: take the last object to win. The board is three rows ({3,4,5} objects); on your turn you remove any number (1 or more) of objects from a single row, then the perfect bot replies. Whoever takes the very last object wins — and with perfect play the player to move wins iff the nim-sum (XOR of the row sizes) is non-zero.
Win odds exact — not a guess
You 100%
Bot 0%

Live nim-sum (XOR)

The nim-sum is the bitwise XOR of the heap sizes. You win iff it is ≠ 0 on your turn. The bot always hands you XOR 0, from which every move you make leaves XOR ≠ 0 — so it never loses from a winning position.

The certified property

Property. In normal-play Nim the player to move wins under perfect play iff the nim-sum (XOR of the heap sizes) is non-zero; the losing (P-) positions are exactly those with nim-sum 0. This is Bouton's theorem (1901), the Nim instance of the Sprague–Grundy theorem.

P-positions (mover loses) = { positions : h₁ ⊕ h₂ ⊕ … ⊕ hₖ = 0 } winning move from XOR = S ≠ 0 : pick a heap h with (h ⊕ S) < h, reduce it to h ⊕ S ⇒ new XOR = 0

How it is certified

  • Sprague–Grundy value g = mex of the successors' values, computed by recursion (grundy.py); a single heap of size n has g = n, and Bouton's theorem gives g(h₁…hₖ) = h₁ ⊕ … ⊕ hₖ.
  • Brute-force cross-check. An independent backward-induction game-tree solver (no nim-sum used) confirms grundy == XOR and XOR = 0 ⇔ mover loses on all 400 configs with ≤3 heaps of size ≤6.
  • Machine-checked lemmas (z3). smt_proof.py discharges, as ∀-theorems over all positions (bit-vector XOR, UNSAT-of-negation): L1 (progress) nim-sum ≠ 0 ⇒ ∃ a move to nim-sum 0, and L2 (closure) nim-sum = 0 ⇒ every move yields nim-sum ≠ 0. Together they are the winning-strategy certificate the bot plays.

Everything here is exact — there is no estimation anywhere. The odds bar shows the true value: from a non-zero nim-sum the side to move wins with certainty.

Pig (dice) optimal play & win-odds

Race to 100. Roll to add to your turn total — but a 1 wipes it and ends your turn. Hold to bank. Play the certified optimal bot and watch the value-iteration odds.
Under optimal play by both players the first player wins 53.06% — the certified Neller-Presser (2004) value, computed here by value iteration to a Bellman fixpoint (residual 0). The optimal policy beats “hold at 20” head-to-head from both seats.
Pig — first to 100 wins. On your turn, repeatedly roll a die: a 2–6 adds to your turn total, but a 1 wipes the turn total and ends your turn. Hold any time to bank the turn total into your score and pass the dice to your opponent.
Win odds certified value-iteration table
You 50%
Bot 50%

You

0
human

Bot

0
certified optimal policy
Turn total: 0

Analysis — the optimal call

P(you win) — certified V(state)50.0%
Optimal hold threshold here≥ 21
State (my · opp · turn)you 0 · bot 0 · turn 0

How we verify Pig

Pig is a push-your-luck race: each turn you roll a die, banking a growing turn total, but rolling a 1 wipes it and ends your turn. We model two-player Pig as a stochastic game / Markov Decision Process, solve it by value iteration to the win-probability fixpoint, and certify the result — reproducing the Neller-Presser (2004) optimal-play solution.

1 · Pig as a stochastic game / MDP

Pig is fully observable (no hidden information), zero-sum (exactly one player wins), and symmetric (both face identical rules), so its value collapses to a single function for the player to move:

V(i, j, k) = P(player to move eventually wins | my score = i, opp score = j, turn total this turn = k), under OPTIMAL play by both.

A state is just (myScore, oppScore, turnTotal) — a sufficient statistic: nothing else in the history affects legal future play. Scores run 0–99; the turn total runs 0–99. The two actions are roll and hold.

2 · The Bellman optimality equation

The opponent’s turn is the mover’s loss (zero-sum), so a turn-handover flips the value to 1 − V:

HOLD: bank k. win now if i+k ≥ 100, else hand over: hold = 1 − V(j, i+k, 0) ROLL: roll a d6. a 1 (prob 1/6) ends the turn, total lost: → 1 − V(j, i, 0) a face f in 2..6 (prob 1/6 each): win if i+k+f ≥ 100, else → V(i, j, k+f) roll = (1/6) · [ (1 − V(j,i,0)) + Σ_f branch(f) ] V(i, j, k) = max( roll, hold )

3 · Value iteration discharges P=? [F win]

“Who wins under optimal play?” is a probabilistic-reachability property — the probability of eventually reaching a win state — written PCTL-style as P=? [F win] from the start state (0,0,0). For an MDP this is the optimal reachability value, the fixpoint of the Bellman operator, computed by value iteration exactly as PRISM/Storm do.

There is no discount factor, so convergence is not automatic. It holds because every state cedes the turn with positive probability (a bust, or the always-available hold), which resets the turn total and hands control over; the fresh-turn values over the finite 100×100 grid form a monotone, bounded Bellman system with a unique fixpoint. Iteration reaches it in ~150 sweeps.

We certify the converged value by recomputing its Bellman residual max_s |V*(s) − (backup of V*)(s)| and checking it is 0 — exactly the fixpoint check a probabilistic model checker performs. So the headline number is a certified value, not a simulation; a Monte-Carlo run only cross-checks it.

QuantityValueStatus
First-player optimal win prob0.5306 (53.06%)certified (exact, fixpoint)
Second-player optimal win prob0.4694certified (exact)
“Hold at 20” self-play (1st)0.5347certified (exact)
Optimal vs hold-20 (1st / 2nd)0.587 / 0.522certified (exact)
Bellman residual of V*0.0fixpoint certificate

4 · The optimal policy vs “hold at 20”

The optimal policy is the argmax of the Bellman equation — a roll/hold boundary “roll until the turn total reaches t, then hold”. The folk heuristic “hold at 20” is close from the start (optimal is “hold at 21” at 0–0) but wrong in general:

  • Behind, the threshold rises (≈30 at 70–70): you must gamble to catch up.
  • All-in when the opponent threatens to win (opp = 90): never hold short of winning — keep rolling until you reach 100.
  • Hold early when you are close (90–0): bank small turn totals to lock the win.

A truly-optimal player beats a hold-at-20 player head-to-head from both seats (0.587 going first, 0.522 going second; seat-average 0.555 > 0.5). The edge is small because hold-at-20 is a strong heuristic — but the state-dependent optimum strictly dominates the flat threshold. (Note: optimal self-play 0.5306 is below hold-20 self-play 0.5347 — these are different games; optimality is defined against an optimal opponent.)

5 · How the live odds bar is computed

The odds bar reads the certified value table directly: for the current state it shows V(myScore, oppScore, turnTotal) reframed as P(you win). The “optimal call” line shows the solver’s exact roll/hold decision and threshold for whoever is to move. The 16-bit table embedded here reproduces the Python value iteration to ~1e-4 — the same numbers, in the browser.

6 · Honesty & limits

  • The first-player optimal win probability (0.5306), the hold-at-20 value, and the optimal-vs-hold-20 head-to-head are exact value-iteration fixpoints (Bellman residual 0).
  • The odds bar and the optimal call are read from the certified table (a 16-bit copy, ~1e-4 faithful) — treat the live percentages as estimates of the certified values.
  • The bot plays the certified optimal policy; the rigorous numbers live in the Python verification code (mdp.py / verify.py).