Simulating 10 000 Tic Tac Toe Games

Tic Tac Toe game is amongst the very first paper-and-pencil game one learns. Its principle is rather very simple; two players put their marks (X or O) on a $latex 3X3$ board. Any player that manages to put three marks either vertically, horizontally or diagonally wins the game. If play optimally by two professional players, this game can go on forever. However, in this article, we will identify the outcome of games when those marks are put on the board randomly. We will set up two experiments using R. In the first variation of the experiment, we make the marks appear randomly on the board all at once.  On the second variation of our Tic Tac Toe game, we will make the marks appear one after another.

Tic Tac Toe Function Set up

In order for us to build the different models for evaluating the variations in games outcome in Tic Tac Toe.  We will first use a function that will allow us to evaluate the winner of a particular Tic Tac Toe game. We will assign to player number 1(X) the value 1 and to player number 2 (O) the value -1.

For this experiment, we will consider the playground as a matrix. Player  1 will win if the sum of one or more columns, diagonals, or rows is 3, and if there aren’t rows, columns, or diagonals that sum to -3. In other words, player 1 wins if the sum of one row, column or diagonal is equal to 3, and player 2 wins when that summation is equal to -3. If the result does not satisfy those conditions, we will record the game as a draw.


winner <- function(board) {
square <- matrix(board, nrow = 3)
horsum <- rowSums(square)
versum <- colSums(square)
diag1 <- sum(diag(square))
diag2 <- square[1,3] + square[2,2] + square[3,1]
if (3 %in% c(horsum, versum, diag1, diag2) &! -3  <br>%in% c(horsum, versum, diag1, diag2)) return (1)
if (-3 %in% c(horsum, versum, diag1, diag2) &! 3  <br>%in% c(horsum, versum, diag1, diag2)) return (-1)
else
return(0)
}



Variation 1

The first variation of our tic tac toe game is the one in which the board is automatically generated. In this Tic Tac Toe variation,  we will create a function that will take in the number of games n that we would like to simulate. Set the seed, Reset the outcome of the games (player 1 win, player 2 wins or draw), call a variable x that represents the moves. Create a loop that will represent each game up to n. Then we will sample x on a board of 9 tiles. That means that a matrix of 9 random values of -1 and 1 is created. From those values, we will call the win function to determine the winner of the game.


variation1 <- function(n=100){set.seed(23)
#reset the values
countPlayer1<- 0
countplayer0 <-0
Drawss<- 0
x<- c(-1,1)
#variation 1
for (i in 1:n) {
game <- sample(x,9, replace = TRUE) # fill board with random X and O
win<-winner(game)
if (1 %in% win){countPlayer1=countPlayer1+1}
if (-1 %in% win){countplayer0=countplayer0+1}
if (0 %in% win){Drawss=Drawss+1}
}
return(c(countPlayer1, countplayer0, Drawss))
}



Let’s run a simulation of 100 games


result <- variation1(100) print (paste("player X wins :", result[1], " ; ", (result[1]/100)*100 ," %" ))
## [1] "player X wins : 44  ;  44  %"
print (paste("player 0 wins :", result[2], " ; ", (result[2]/100)*100 ," %" ))
## [1] "player 0 wins : 32  ;  32  %"
print (paste("Draw :", result[3], " ; ", (result[3]/100)*100 ," %" ))
## [1] "Draw : 24  ;  24  %"
1
barplot(result,names.arg = c("player X wins", "player O wins", "Draw"), ylim = c(0,100))



With a seed set to 23, the probability of the player X to win are 44%. It seems that player 1 or X has a higher probability of winning.  Obviously, this is due to the number of simulations fixed to 100: if we want a more accurate probability, we have to do more simulations.

Let’s apply the same function 10000 times.


result <- variation1(10000) print (paste("player X wins :", result[1], " ; ", (result[1]/10000)*100 ," %" ))
## [1] "player X wins : 3900  ;  39  %"
print (paste("player 0 wins :", result[2], " ; ", (result[2]/10000)*100 ," %" ))
## [1] "player 0 wins : 3851  ;  38.51  %"
print (paste("Draw :", result[3], " ; ", (result[3]/10000)*100 ," %" ))
## [1] "Draw : 2249  ;  22.49  %"
barplot(result,names.arg = c("player X wins", "player O wins", "Draw"), ylim = c(0,10000))



As we intuitively expected, if we fill each square with a random X or O with equal probability (0.5), the two gamers have approximately the same probability of winning (near 0.4), and a probability of drawing of 20%.

Variation 2

In this variation, we have the variable representing the counts for each winner. We have a for loop (representing the number of games to be played) that will contain the board game made of 9 tiles.  Finally, we put the game process within a “while” loop. It will run as long as the board is not full or until we have a winner. Firstly, It figures out which squares are empty, then when the program finds all empty squares it randomly places an X or O, and changes the board matrix. Once the board changes, the program evaluates if there is a winner and changes the player.


#variation 2
variation2 <- function(n=100){
countPlayer1 <- 0
countplayer0 <-0
Drawss<- 0
for (i in 1:n) {
game <- rep(0, 9)
win <- 0
player <- 1
while (0 %in% game & win == 0) { # Keep playing until win or full board
empty <- which(game == 0) # find empty tiles
move <- empty[sample(length(empty), 1)] # players move
game[move] <- player # Change board
win <- winner(game)
player <- player * -1
}

if (1 %in% win){countPlayer1=countPlayer1+1}
if (-1 %in% win){countplayer0=countplayer0+1}
if (0 %in% win){Drawss=Drawss+1}
}

return(c(countPlayer1, countplayer0, Drawss))
}



Now let’s run 100 games using this simulation to figure out the various outcome of the games.


result <- variation2(100)

print (paste("player X wins :", result[1], " ; ", (result[1]/100)*100 ," %" ))
## [1] "player X wins : 61  ;  61  %"
print (paste("player 0 wins :", result[2], " ; ", (result[2]/100)*100 ," %" ))
## [1] "player 0 wins : 26  ;  26  %"
print (paste("Draw :", result[3], " ; ", (result[3]/100)*100 ," %" ))
## [1] "Draw : 13  ;  13  %"
barplot(result,names.arg = c("player X wins", "player 0 wins", "Draw"), ylim = c(0,100))



In the second variation, we realize that player X wins about 60 % of the time, player O 30%, and draws occur around 10%. Let’s run a simulation of 10 000 games to double-check our results.


result <- variation2(10000) print (paste("player X wins :", result[1], " ; ", (result[1]/10000)*100 ," %" ))
## [1] "player X wins : 5758  ;  57.58  %"
print (paste("player 0 wins :", result[2], " ; ", (result[2]/10000)*100 ," %" ))
## [1] "player 0 wins : 2979  ;  29.79  %"
print (paste("Draw :", result[3], " ; ", (result[3]/10000)*100 ," %" ))
## [1] "Draw : 1263  ;  12.63  %"
barplot(result,names.arg = c("player X wins", "player 0 wins", "Draw"), ylim = c(0,10000))


This time the Player $X$ has a higher probability of a win. This makes sense because he plays first, thus he can put 5 X, instead of 4 O. In this case, the probability that X wins are approximately 60%.  Player 2 or O has a 30% chance of winning. We record draws 10% of the time.

If there are any improvements, suggestions, questions, you might have, feel free to express them in the comment section down below.