Ha-ha, you fool! You fell victim to one of the classic blunders, the most famous of which is “Never get involved in a land war in Asia,” but only slightly less well known is this: “Never go in a blog post expecting a game related article, because it may be a math related article!”
I am reading Probability and Computing: Randomization and Probabilistic Techniques in Algorithms and Data Analysis by Mitzenmacher and Upfal.
The exercises section of Chapter 1 presents the following tic-tac-toe question:
1.26(a): Each of the nine squares is labeled either or according to an independent and uniform coin flip. If only one of the players has one (or more) winning tic-tac-toe combinations, that player wins. Otherwise. the game is a tie. Determine the probability that wins. (You may want to use a computer program to help run through the configurations.)
Not wanting to underutilize the capabilities of my website, I decided to write up a solution. I did promise a math post, so the Probability comes first, and Computing second.
Math
This being a math post, let’s define a few sets to make the solution more concise.
Let the set of possible boards be , and the set of boards where wins be .
First, let’s determine the total number of possible boards, . Note that this isn’t a typical tic-tac-toe board, where the game stops after a single three-in-a-row.
There are outcomes for a coin flip (or vs ), and since each flip is independent, flips to determine the labels for the board result in possible boards.
Secondly, let’s determine the number of boards where has one or more winning combinations, . This is a much tougher problem, and to work out the solution I split the problem into cases depending on the number of s on the board that result in a win.
-
We can easily enumerate the cases here, resulting in a total of .
✗✗✗✗✗✗✗✗✗✗✗✗✗✗✗✗✗✗✗✗✗✗✗✗ -
Since we cannot have two winning lines with only 4 , we can think of the problem as the problem, but also choosing one of the 6 remaining cells to add another , resulting in cases.
-
This requires a bit more enumeration. We can think of the number of winning cases as the difference between the total number of cases and the number of non-winning cases, so
The total number of boards with 5 of is , as we are choosing 5 out of the 9 available cells to place an in. Therefore, .
There are 5 unique boards that are non-winning.
✗✗✗✗✗✗✗✗✗✗✗✗✗✗✗✗✗✗✗✗✗✗✗✗✗Of these 5 boards, the first two are unique across all 8 transformations (4 rotations and 4 reflections), while the other three only have 4 unique transformations due to the symmetry shown by the line.
-
There are only two boards with 6 that are non-winning.
✗✗✗✗✗✗✗✗✗✗✗✗This leaves winning boards.
-
All boards are winning, therefore there are boards.
-
All boards are winning, therefore there are boards.
-
All boards are winning, therefore there is board.
Adding them all up, we get winning boards out of boards total. Now it should be smooth sailing to find the probability that wins, which is !
ono
Fixing The Mistake
In hindsight, seeing winning with should raise some alarm bells, given that should win with the same probability. In another variant of this game, we could say that everyone wins and go home happy campers. Today though, there is only one spot on the podium for the luckiest tic-tac-toer (ticer-tacer-toer?).
Luckily, we did not do a whole bunch of work for nothing. From our winning boards, we need to prune ones that result in also winning. We go again!
-
The new total for this is now , as the remaining 6 boards result in a win for .
-
Similar to the previous section, the total is now .
-
Here we have another kerfuffle. Because we previously calculated the number of winning boards by subtracting non-winning boards from the total number of boards, we cannot simply subtract the winning boards that also result in an win.
The situation to notice here is that where , . We know that in all the boards, there are cases where wins, since .
Going back to the non-winning boards:
✗✗✗✗✗✗✗✗✗✗✗✗✗✗✗✗✗✗✗✗✗✗✗✗✗I’ve added the s onto two boards, and highlighted their respective wins. Including possible transformations, this means that out of the non-winning boards for , there are winning boards for .
Therefore, the number of winning boards for where is also winning is . This gives the total number of actual wins, ie. wins for where does not win, as boards.
-
We can utilize a similar, but simpler logic here. The two non-winning boards for :
✗✗✗✗✗✗✗✗✗✗✗✗is winning for , which leaves the remaining winning boards for in the winning boards for . So, using the total from the previous section, the new total is boards.
-
Onto the home stretch! These cases make it impossible for to win, so the totals remain the same at , and respectively.
Our new and improved total is boards, giving a probability of . That’s much more reasonable!
I can fix that! Let’s write some code. Although I’m not confident my code’s any better than my math…
CS
I’ll be using Python today, for no reason other than I’ve been writing a lot of Rust lately and have been neglecting my first programming language. Let’s do it two slightly different ways, one by enumerating all possible tic-tac-toe boards, and another by randomly generating boards and calculating a running win percentage.
Combinatorics
To generate all possible boards without duplication, we can use the handy itertools.product
.
My oxidized iron brain can’t help introducing more type-safety into the duck-typed snake language, so we’ll use enumerations.
import enum
import itertools
class Player(enum.Enum):
X = enum.auto()
O = enum.auto()
def __repr__(self):
return f"{self.name}"
PLAYERS = list(Player)
for board in itertools.product(PLAYERS, repeat=9):
# Prints:
# (X, X, X, X, X, X, X, X, X)
# (X, X, X, X, X, X, X, X, O)
# (X, X, X, X, X, X, X, O, X)
# (X, X, X, X, X, X, X, O, O)
# (X, X, X, X, X, X, O, X, X)
# ...
print(board)
We can then write a function to test for a win from a particular player:
def is_win_for(board: tuple[Player, ...], player: Player) -> bool:
INDICES = (
((0, 0), (0, 1), (0, 2)),
((1, 0), (1, 1), (1, 2)),
((2, 0), (2, 1), (2, 2)),
((0, 0), (1, 0), (2, 0)),
((0, 1), (1, 1), (2, 1)),
((0, 2), (1, 2), (2, 2)),
((0, 0), (1, 1), (2, 2)),
((0, 2), (1, 1), (2, 0)),
)
for indices in INDICES:
if all(board[col * 3 + row] == player for (row, col) in indices):
return True
return False
Putting it all together:
total_boards = 0
wins = 0
PLAYERS = list(Player)
for board in itertools.product(PLAYERS, repeat=9):
print(board)
if is_win_for(board, Player.X) and not is_win_for(board, Player.O):
wins += 1
total_boards += 1
print(f"{wins}/{total_boards} = {wins / total_boards}")
And drumroll please…
$ python solver.py
98/512 = 0.38671875
Tough customer!
Simulation
Now, let’s do it more in the spirit of the question!
import random
total_boards = 0
wins = 0
PLAYERS = list(Player)
for i in range(10000001):
# Show incremental progress
if i % 1000000 == 0 and total_boards != 0:
print(f"{i} iterations: {wins}/{total_boards} = {wins / total_boards}")
# Our coin flip generated board
board = tuple(random.choice(PLAYERS) for _ in range(9))
if is_win_for(board, Player.X) and not is_win_for(board, Player.O):
wins += 1
total_boards += 1
Second drumroll please…
$ python solver.py
98/512 = 0.38671875
1000000 iterations: 387490/1000000 = 0.38749
2000000 iterations: 774802/2000000 = 0.387401
3000000 iterations: 1161301/3000000 = 0.3871003333333333
4000000 iterations: 1547835/4000000 = 0.38695875
5000000 iterations: 1934959/5000000 = 0.3869918
6000000 iterations: 2321540/6000000 = 0.38692333333333334
7000000 iterations: 2707923/7000000 = 0.38684614285714286
8000000 iterations: 3094900/8000000 = 0.3868625
9000000 iterations: 3482005/9000000 = 0.38688944444444445
10000000 iterations: 3868924/10000000 = 0.3868924
Close enough!
Conclusion
The math part of this post took a lot longer than the CS part of this post.
I will stuff my fingers into my ears and chalk that up to me not being a math major.
Please do let me know if there’s a more rigorous, less “proof-by-enumeration” math solution.
I can be reached at feedback@<this website's domain name>
.
The Exercise For The Reader is part (b) of the same problem:
1.26(b): and take turns, with the player going first. On the player’s turn, an is placed on a square chosen independently and uniformly at random from the squares that are still vacant; plays similarly. The first player to have a winning tic-tac-toe combination wins the game, and a tie occurs if neither player achieves a winning combination. Find the probability that each player wins. (Again, you may want to write a program to help you.)
Eat your veggies, and I’ll see you in the next post.