I spent another forty minutes making a few more changes to complete the Minimax solver for 3x3 Tic-Tac-Toe. You can download the files here: TicTacToeInClassUpdated.tar.gz.
I also had to identify one bug that stumped me for a bit, details about which are below.
Generally, I broke methods down into smaller pieces. There’s a saying that things are “either obviously not wrong or not obviously wrong.” I find that simplifying methods such that they do only one thing at a time moves them toward the former category and away from the latter. There is some redundancy in the various checks for the end-game conditions now, but the code for each individual one is clearer, in my opinion. It also made catching the aforementioned bug easier.
I moved the test class(es) to their own directory — I think this will make your life easier if you use Eclipse. I also encoded three of the autograder test cases as unit tests on minimax()
. Add a few more, and you could start coding the more advanced optimizations with some confidence that you hadn’t broken anything.
I broke up isEndGame()
into a set of three checks: victory for each player, or a draw.
I refactored the victory check. Now allEqual()
checks whether the values in a series of cells, denoted by indices, are all equal to a given value, and isVictorious()
calls it to check for victory. The victoryIndices
are still hardcoded; you’d need to set them programmatically to handle variable board sizes.
I wrote isDraw()
, which checks to see if there’s no victor, and if the board is full (thus, a draw).
I replaced char
constants with public static final
constants; among other things, this lets minimax()
not have to know about X
and O
. You could continue to generalize by creating a type-parameterized interface on Board
that exposed this information. In fact, if you look up the Java reference code for the textbook, this is the approach that was taken.
I moved minValue()
and maxValue()
to a standalone Minimax
class, and added a minimax()
call which decides which of the two to start with.
I modified listSuccessors()
to return a List<Board>
rather than a Board[]
; it depends upon getCurrentPlayer()
, which I also made public to allow minimax()
to choose the right starting method.
Now, the bug: I originally attempted to use a statement like
1
|
|
to count the number of locations claimed by a particular player (and analogs for the number of plays each player had made). But, it turns out, it always returns zero. Primitive type auto-boxing appears to interfere with its equality checking (see BoardTest.testCount()
, and the general contract for frequency()
). Oops.
I zeroed in on this error quickly using some of the tests in BoardTest
; caveman debugging (System.err.println()
) also would work. But my experience is that if you don’t know where to start (and I didn’t: all boards were returning a minimax value of 0
), it’s best to check that methods behave the way you expect. Guessing at which variables are causing problems is less principled, and requires adding/removing/commenting-out println()
s. Fine for small programs, harder once things get bigger.
As a side bonus, the added unit tests provide further assurance that when you change things (like for alpha-beta pruning or transposition tables) you haven’t broken your code.