CMPSCI 383: Artificial Intelligence

Fall 2014 (archived)

Assignment 03: In-class Tic-Tac-Toe, Updated

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
int unplayedCount = Collections.frequency(Arrays.asList(boardState, UNPLAYED)));

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.