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