Create a blank play-board that will contain the weight values we calculate by trying possible moves (line 111). Determine the valid winner subset for the current player (line 113), then determine the valid winner subset for the opponent (line 114). Calculate the weights for the current player (line 116), then add on the weights of the opponent (that is blocking the opponent) (line 117).
At this point I have to point out a flaw in the current algorithm. There is one particular case where the algorithm fails to pick an appropriate move, and that is if player X starts with any of the outer middle squares.
By default, the weight algorithm will give the highest weight to the center, because this position appears in the most winner combinations than any other position (4 out of 8), so it gets calculated more than any other, artificially making it a higher weight than we actually want. If we ignore this, you would see by experimenting that the following combination would have X allowed a win!
To avoid this I add some code to detect this special case and hard code a preferable move that deals with this. We'll see this in a minute.
Now we setup some variables to hold various state, including a array of the highest weight values, as there may be more than one with the same value.
For each element in our play-board (line 126), if the current element is higher than our current highest value (line 128), then we reset the highest array, push a new index, and store the new highest value (line 131). Otherwise if the value is equal to our current highest value (line 134), then we add that to the highest array (line 136). Add the current value to a running total (line 138). This is just an easy way to confirm that there are any possible moves. If not, then the game must be over.
Now, since we are already looping through the board positions, we’ll look for our special case, and flag it’s state. If the current board’s position has a player (line 140), increment our total-moves counter (line 142) and see if it is one of the four special locations we care about (line 143). If so, increment a special case counter (line 145), and store its position (line 146).
At this point we have an array of the highest weight indices, and our special case state.
Make sure we actually have a possible move (we aren’t finished) (line 153). If we only have one move so far, and our special case counter has only one case, and the highest position we calculated is in the center of the board (line 155), default to index zero (line 157) and, for indices 1 and 3, set the highest index to position 0 (line 162), or for 5 and 7, set it to position 8 (line 166). Otherwise if we have more than one index to choose from (line 170), we randomly pick an index in our highList array and store that (line 173). (There’s some controversy over how the random function works, so just to be doubly sure, I make sure that the index does not extend beyond a valid range (lines 175-177)). Otherwise, if there is only one highest value, default to index zero (line 181).
Now we just check that we have an index to choose from (line 184), and get that value (line 186). We return that move position (189).
Now skip down to line 209 and we set up storage for our winner array and our current-board array.
Pop down to line 282. This is our exposed function SetMoveX(), which just stores the given index to the Player X values. SetMoveO() does the same for player O.
Down to 295, GetMoveO() is the main function that calls the whole AI mechanism and gets the index of the next best move.
GetDataAt just gets the value at a given index.
Finished Game with AI
This is by no means the best implementation of Tic-Tac-Toe, but hopefully there's something useful here in having a whole game detailed from various levels of implementation.
Continue to the next page for an addendum with some bug fixes since this tutorial's initial release.