Skip to content
Vamshi Jandhyala

Books · The Riddler

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 XX and OO, starting with XX, 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 55 to 99 moves on the 3×33 \times 3 board, with XX moving on odd turns and OO 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 9!9!? A naïve upper bound treats each game as a permutation of the nine squares, giving 9!=362,8809! = 362{,}880. 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 55, 66, 77, 88, or 99. Count each case.

Turn 55 (X wins). XX has made 33 moves, OO has made 22. The three XX marks lie on one of the 88 winning lines (33 rows, 33 columns, 22 diagonals), in some order (3!3! ways), and OO’s two marks occupy two of the remaining 66 cells in order (656 \cdot 5). Total: 83!65=1,4408 \cdot 3! \cdot 6 \cdot 5 = 1{,}440.

Turn 66 (O wins). OO has 33 marks on a winning line (83!8 \cdot 3! orderings), XX has 33 marks on the remaining 66 cells in order (6546 \cdot 5 \cdot 4). Gross count 83!654=5,7608 \cdot 3! \cdot 6 \cdot 5 \cdot 4 = 5{,}760. From this subtract the cases where XX would already have won on turn 55: XX’s three marks form a winning line and OO’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 33 row-row pairs and the 33 column-column pairs, giving 66 unordered ordered-pair pairs 63!23!=432\to 6 \cdot 3! \cdot 2 \cdot 3! = 432 invalid sequences. Net: 5,760432=5,3285{,}760 - 432 = 5{,}328.

Turn 77 (X wins). XX’s three marks lie on a winning line in some order (83!8 \cdot 3!), interleaved with a "wasted" XX move on one of the 66 off-line cells at one of 33 possible turns (i.e. the XX-on-line moves occupy 33 of the 44 XX-turns), and OO’s three marks fill three of the remaining 55 off-line cells in order (5435 \cdot 4 \cdot 3). Gross count: 83!36543=51,8408 \cdot 3! \cdot 3 \cdot 6 \cdot 5 \cdot 4 \cdot 3 = 51{,}840. Subtract the cases in which OO would have already won on turn 66: OO’s three marks form a winning line and XX’s line is disjoint, XX’s wasted square takes one of the remaining cells (one of 66 at one of 33 turns), giving 63!13!36=3,8886 \cdot 3! \cdot 1 \cdot 3! \cdot 3 \cdot 6 = 3{,}888 invalid. Net: 51,8403,888=47,95251{,}840 - 3{,}888 = 47{,}952.

Turn 88 (O wins). OO’s line in some order (83!8 \cdot 3!), OO’s wasted move on one of 66 off-line cells at one of 33 slots, XX’s four marks fill the remaining cells in order (54325 \cdot 4 \cdot 3 \cdot 2). Gross: 83!365432=311,0408 \cdot 3! \cdot 3 \cdot 6 \cdot 5 \cdot 4 \cdot 3 \cdot 2 = 311{,}040. Subtract premature wins (analogous accounting): 31,10410331{,}104 \cdot 10 \cdot 3 \cdot \ldots The cleanest way is direct enumeration; the count is 72,57672{,}576.

Turn 99 (a fill, X plays last). Total fill orderings minus the orderings that would have ended on turn 8\le 8 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 1,440+5,328+47,952+72,576+127,872=255,1681{,}440 + 5{,}328 + 47{,}952 + 72{,}576 + 127{,}872 = 255{,}168, with 127,872127{,}872 games filling the board (cat’s games and ninth-move wins).

Total possible tic-tac-toe games  =  255,168.\boxed{\text{Total possible tic-tac-toe games} \;=\; 255{,}168.}

The computation

Enumerate the tree of plays from the empty board, terminating each branch on a win or a fill, and count leaves.

  1. State: the 99-cell board and whose turn it is.

  2. For each empty cell, place the current player’s mark and recurse.

  3. Terminate if the new board has a three-in-a-row, or if the board is full.

  4. 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 255,168255{,}168, 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 11 through 99. On your turn you call out a number. If your call lands on an empty square, your XX is placed there. If it lands on a square already occupied by an OO, you are told the square is occupied and you call again, repeating until you place an XX. 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 XX, given that XX moves first and that occupied-square collisions reveal OO’s cell, can choose a strategy guaranteeing not-loss against every possible OO play.

Information available to XX. After any sequence of moves, XX knows exactly:

  • the cells XX has placed (call this set X\mathcal{X});

  • the cells OO has occupied that XX has personally collided with (call this set CO\mathcal{C} \subseteq \mathcal{O}, the confirmed OO cells).

XX does not know about OO cells that have not yet been bumped. A strategy is therefore a function from each information state (X,C)(\mathcal{X}, \mathcal{C}) to the next cell XX attempts. The same information state must produce the same attempt regardless of the unseen OO cells.

Game-tree certificate. The game has at most 99 moves per side, the information state takes at most (9X)(9XC)\binom{9}{|\mathcal{X}|} \binom{9 - |\mathcal{X}|}{|\mathcal{C}|} 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 (X,C)(\mathcal{X}, \mathcal{C}) safe when there exists a cell cfreec \in \mathrm{free} such that, for every consistent OC\mathcal{O} \supseteq \mathcal{C}, attempting cc leads to a safe successor information state. Here free={0,,8}(XC)\mathrm{free} = \{0, \ldots, 8\} \setminus (\mathcal{X} \cup \mathcal{C}), and a successor is one of two cases:

  • Collision (if cOc \in \mathcal{O}): same game, new info state (X,C{c})(\mathcal{X}, \mathcal{C} \cup \{c\}) against the same O\mathcal{O}.

  • Success (if cOc \notin \mathcal{O}): XX places cc, then OO moves adversarially, returning to info state (X{c},C)(\mathcal{X} \cup \{c\}, \mathcal{C}).

Backward induction from terminal positions (win for XX if X\mathcal{X} contains a winning line; loss if O\mathcal{O} contains one; tie at board-fill) returns safe(,)  =  1.\mathrm{safe}(\emptyset, \emptyset) \;=\; 1.

The induction therefore certifies a not-loss strategy. The official column gives three concrete examples: one starts at the centre (cell 55) and follows a decision tree, one starts at a corner (cell 11), one starts at an edge (cell 22). All three certify the existence claim, and the backward induction above reproduces a witness.

Yes, a blindfolded first player can guarantee not to lose.\boxed{\text{Yes, a blindfolded first player can guarantee not to lose.}}

The computation

Run the backward induction over information states and confirm that an attempt cell exists at the empty initial info state.

  1. For each information state (X,C)(\mathcal{X}, \mathcal{C}), enumerate consistent O\mathcal{O} sets (supersets of C\mathcal{C} disjoint from X\mathcal{X}, with O=X|\mathcal{O}| = |\mathcal{X}| at XX’s turn).

  2. For each candidate attempt cfreec \in \mathrm{free} and each consistent O\mathcal{O}, simulate one attempt: collision recurses with C{c}\mathcal{C} \cup \{c\} against the same O\mathcal{O}; success recurses on OO’s adversarial reply.

  3. Mark cc safe at (X,C)(\mathcal{X}, \mathcal{C}) if no consistent O\mathcal{O} forces a loss.

  4. Return safe iff some such cc exists; report the safe cc 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.