Chapter 209
Can You Win Tic-Tac-Toe Blindfolded?
Riddler Express
How many possible games of tic-tac-toe are there? A game is a sequence of moves alternating and , starting with , played until either a player completes three in a row or the board fills.
The Riddler, FiveThirtyEight, December 14, 2018(original post)
Solution
A game is a sequence of to moves on the board, with moving on odd turns and on even, ending the moment someone has three in a row or the board fills. Two distinct move orders give distinct games even if the final board is the same.
Why not just ? A naïve upper bound treats each game as a permutation of the nine squares, giving . But many such permutations describe boards where the game would have ended earlier (a player completed three in a row before all squares were filled), so the move sequence after that point is not a real game.
Decompose by ending turn. A game ends on turn , , , , or . Count each case.
Turn (X wins). has made moves, has made . The three marks lie on one of the winning lines ( rows, columns, diagonals), in some order ( ways), and ’s two marks occupy two of the remaining cells in order (). Total: .
Turn (O wins). has marks on a winning line ( orderings), has marks on the remaining cells in order (). Gross count . From this subtract the cases where would already have won on turn : ’s three marks form a winning line and ’s three marks form a different winning line, and the two lines must share no cell. The only pairs of disjoint winning lines are the row-row pairs and the column-column pairs, giving unordered ordered-pair pairs invalid sequences. Net: .
Turn (X wins). ’s three marks lie on a winning line in some order (), interleaved with a "wasted" move on one of the off-line cells at one of possible turns (i.e. the -on-line moves occupy of the -turns), and ’s three marks fill three of the remaining off-line cells in order (). Gross count: . Subtract the cases in which would have already won on turn : ’s three marks form a winning line and ’s line is disjoint, ’s wasted square takes one of the remaining cells (one of at one of turns), giving invalid. Net: .
Turn (O wins). ’s line in some order (), ’s wasted move on one of off-line cells at one of slots, ’s four marks fill the remaining cells in order (). Gross: . Subtract premature wins (analogous accounting): The cleanest way is direct enumeration; the count is .
Turn (a fill, X plays last). Total fill orderings minus the orderings that would have ended on turn with that final-board placement. This subtraction is messy by hand, but a brute-force enumeration handles it cleanly.
A complete tree search confirms the totals , with games filling the board (cat’s games and ninth-move wins).
The computation
Enumerate the tree of plays from the empty board, terminating each branch on a win or a fill, and count leaves.
State: the -cell board and whose turn it is.
For each empty cell, place the current player’s mark and recurse.
Terminate if the new board has a three-in-a-row, or if the board is full.
Sum the leaves.
LINES = [(0,1,2),(3,4,5),(6,7,8),(0,3,6),(1,4,7),
(2,5,8),(0,4,8),(2,4,6)]
def winner(b):
for a, c, d in LINES:
if b[a] != ' ' and b[a] == b[c] == b[d]:
return b[a]
return None
def count(board, turn):
if winner(board) is not None or ' ' not in board:
return 1
total = 0
for i in range(9):
if board[i] == ' ':
nb = board[:i] + turn + board[i+1:]
total += count(nb, 'O' if turn == 'X' else 'X')
return total
print("Total games:", count(' ' * 9, 'X'))
The script prints , matching the boxed answer.
Riddler Classic
You play tic-tac-toe with one advantage and one big disadvantage. The advantage is that you go first. The disadvantage is that you are blindfolded and never told your opponent’s moves. The squares are numbered through . On your turn you call out a number. If your call lands on an empty square, your is placed there. If it lands on a square already occupied by an , you are told the square is occupied and you call again, repeating until you place an . You learn nothing about your opponent’s moves except via these collisions. Can you guarantee that you never lose?
The Riddler, FiveThirtyEight, December 14, 2018(original post)
Solution
Yes. A blindfolded , given that moves first and that occupied-square collisions reveal ’s cell, can choose a strategy guaranteeing not-loss against every possible play.
Information available to . After any sequence of moves, knows exactly:
the cells has placed (call this set );
the cells has occupied that has personally collided with (call this set , the confirmed cells).
does not know about cells that have not yet been bumped. A strategy is therefore a function from each information state to the next cell attempts. The same information state must produce the same attempt regardless of the unseen cells.
Game-tree certificate. The game has at most moves per side, the information state takes at most values, and the game tree is finite. An exhaustive backward induction over the information-state graph decides whether a not-loss strategy exists. Concretely, declare information state safe when there exists a cell such that, for every consistent , attempting leads to a safe successor information state. Here , and a successor is one of two cases:
Collision (if ): same game, new info state against the same .
Success (if ): places , then moves adversarially, returning to info state .
Backward induction from terminal positions (win for if contains a winning line; loss if contains one; tie at board-fill) returns
The induction therefore certifies a not-loss strategy. The official column gives three concrete examples: one starts at the centre (cell ) and follows a decision tree, one starts at a corner (cell ), one starts at an edge (cell ). All three certify the existence claim, and the backward induction above reproduces a witness.
The computation
Run the backward induction over information states and confirm that an attempt cell exists at the empty initial info state.
For each information state , enumerate consistent sets (supersets of disjoint from , with at ’s turn).
For each candidate attempt and each consistent , simulate one attempt: collision recurses with against the same ; success recurses on ’s adversarial reply.
Mark safe at if no consistent forces a loss.
Return safe iff some such exists; report the safe at the empty info state.
from functools import lru_cache
from itertools import combinations
LINES = [(0,1,2),(3,4,5),(6,7,8),(0,3,6),(1,4,7),
(2,5,8),(0,4,8),(2,4,6)]
def line_in(mask):
for a, b, c in LINES:
if (mask >> a) & 1 and (mask >> b) & 1 and (mask >> c) & 1:
return True
return False
def consistent(x, conf):
nx = bin(x).count('1')
need = nx - bin(conf).count('1')
free = [i for i in range(9)
if not ((x >> i) & 1 or (conf >> i) & 1)]
if need < 0 or need > len(free):
return
for extra in combinations(free, need):
m = conf
for b in extra:
m |= 1 << b
yield m
@lru_cache(maxsize=None)
def after_x(x, o, conf):
if line_in(x):
return True
if (x | o) == 0x1FF:
return True
for i in range(9):
if not ((x >> i) & 1 or (o >> i) & 1):
no = o | (1 << i)
if line_in(no):
return False
if not info_safe(x, conf):
return False
return True
@lru_cache(maxsize=None)
def info_safe(x, conf):
free = [c for c in range(9)
if not ((x >> c) & 1 or (conf >> c) & 1)]
if not free:
return True
cons = list(consistent(x, conf))
if not cons:
return True
for c in free:
ok = True
for o in cons:
if (o >> c) & 1:
if not info_safe(x, conf | (1 << c)):
ok = False
break
else:
if not after_x(x | (1 << c), o, conf):
ok = False
break
if ok:
return True
return False
print("Not-loss strategy exists:", info_safe(0, 0))
The script prints True, certifying the boxed claim.