Ultimate Tic Tac Toe Player

advertisement
Ultimate Tic Tac Toe Player
Manasvi Vaidyula
2019101012
The goal is to design a tic tac toe software that never loses a human, given that it
always plays second. The algorithm described below ensures that the software always
plays the optimal move given the state of the game board at any particular point in time.
First, we define the state of the board at a given point in time as the tic tac toe board
with 9 positions and a certain number of Xs and Os in those positions such that each
position is occupied by at most one symbol and the number of Os is either equal to the
number of Xs or less than the number of Xs by 1. The tic tac toe game can be described
as a series of states where we go from one state to the next by placing a new symbol
on the board such that the conditions mentioned above are not violated. This means
that each player takes turns placing symbols on the board until there are no free spaces
left or one of the players wins. We also define an optimal move for a player as follows:
The player will first try to move such that she is guaranteed a win. If this is not possible,
she will move such that she is guaranteed a draw. If even this is not possible, she will
move such that she is guaranteed a loss.
The crux of our algorithm will be a single function that takes the current state as input
and returns the optimal move for the current player. We adopt a recursive strategy for
this function because we will determine the current optimal move based on future
moves which the recursive function will compute for us. As with all recursive algorithms,
we will describe a base case and recursive step below.
The idea of looking at future steps comes from the fact that, in the worst case for us, we
can expect the opponent to make optimal moves. We can predict those optimal moves
and try to counter them. We first try placing the current player’s symbol in some empty
position and further see what moves the other player can make given the new state. If
Ultimate Tic Tac Toe Player
1
the other player has a guaranteed win, it doesn’t make sense for the current player to
place the symbol in this position. On the other hand, if it results in a guaranteed loss for
the other player, it makes perfect sense to put it in this position.
Let us now go into detail about the functionality. We are given the current state of the
board. We can trivially find whose turn it is by counting the number of Xs and Os
present on the board.
We will first look at the base case, as it will be important in understanding the recursive
step. There are three base cases here:
1. There are three Xs in a row, which means player 1 wins
2. There are three Os in a row, which means player 2 wins,
3. Conditions 1 and 2 are not satisfied and there are no more empty positions left in
the current state
In the first case, we return that the human has won. In the second case, we return that
we have won. And in the third case, we return that the game has ended in a draw.
Now for the recursive step, let’s say we have some current state that is not a base case.
We will loop through every possible empty position on the board. Once we reach an
empty position, we will place the current symbol here which will give rise to a new state.
We call the function again for this new state, but now we know it’s the other player’s
turn. Without loss of generality, let us assume it is currently the software’s turn and we
place O in some empty position. Now, we call the function recursively for the new state,
but now it’s the human’s turn. In that recursive call, the human will first try to win. If that
is not possible, draw. So given multiple possibilities of guaranteed wins, draws, and
losses, we can assume the human will pick moves that make her win. The main point
here is that if there exists even a single possibility to win, we can assume the human will
take it. Hence, if a win is possible for the human, our recursive call will return that the
current move will allow the human to win. If it is not possible for the human to win, she
will look for a draw. If there is even a single possibility of a draw, she will move in such a
way that it happens and our recursive call will return that the current move will end in a
draw at best. Similarly, if there is no way for the human to win, our recursive call will
Ultimate Tic Tac Toe Player
2
return that the current move is perfect and this is the best move. Notice that the human
would think about optimal moves the same way, and hence we can use a single function
to determine optimal moves for both. We do this for every possible empty position and
check if even a single one guarantees a win and choose it. If not we settle for a draw. If
even that is not possible, we lose. This recursive call happens until we hit a base case,
which returns whether someone won or if it ends in a draw. An example of a few
recursive calls for a given state is given below:
Ultimate Tic Tac Toe Player
3
The base case return values are shown in red. In the first move, we see that there is
one move where the human will win for sure, and so we return that the human will win in
the current state.
It can be seen from exhaustive testing that, with this strategy, the software can never
lose
Ultimate Tic Tac Toe Player
4
That is the idea for the main algorithm. We still need to design how the board will be
stored and the game will be handled. We can have a class GameBoard which will have
methods to display the board and update the current state. The initial state is where all
the positions are empty. We will also have a boolean variable that lets us know if it is the
human’s turn or the software’s turn. If it is the human’s turn, it will simply take the input
symbol. If it is the software’s turn, it will run the function to compute the optimal move as
described above and update the state accordingly. After every update, we must also run
a function to check if the game has ended based on the rules of the game. If it has
ended, display the winner or declare that the game has ended in a draw.
Ultimate Tic Tac Toe Player
5
Download