Developing, continued, and classes, objects, and methods

Welcome

Announcements

Are you a new add to the course? You need to get in touch with me immediately.

Back to TicTacToe

Note: corrected code (including the corrections described below) is on the schedule page. I am not going to do the corrections in class, to save time.

When we left off last class, we had the following code:

package tictactoe;

public class TicTacToeBoard {
    private enum Space {
        X, O, EMPTY;

        public String toString() {
            if (this == X) {
                return "X";
            } else if (this == O) {
                return "O";
            } else if (this == EMPTY) {
                return ".";
            }
            return "WTFBBQ"; // this should never happen
        }
    }

    private Space[] board;

    public String toString() {
        String string = "";

        for (int i = 0; i < board.length; i++) {
            string = string + board[i];
            if (i % 3 == 2) {
                string = string + '\n';             
            }
            else {
                string = string + ' ';
            }           
        }
        return string.substring(0, string.length() - 1);
    }

    public TicTacToeBoard() {
        board = new Space[9];
        for (int i = 0; i < board.length; i++) {
            board[i] = Space.EMPTY;
        }
    }


    public static void main(String[] args) {
    }

}

and test:

package tictactoe;

import static org.junit.Assert.*;

import org.junit.Test;

public class TestTicTacToeBoard {

    @Test
    public void testEmptyBoardPrinting() {
        // do some setup
        TicTacToeBoard board = new TicTacToeBoard();


        // test a value is what you expect

        // two arguments: first: what you expect
        // second: what you're testing ("actual")
        assertEquals(". . .\n" + 
                ". . .\n" + 
                ". . .", board.toString());
    }

}

This is a pretty common pattern, actually, where you write and test code in a driver class or method, then encode your test as a test case. This way, if you break something by accident later, you’ll discover it immediately before you move on.

Output legibility

Remember I made EMPTY toString itself as a single . (and update our test appropriately). Usually this sort of thing is a good idea – anything to make your life easier when reading your code (and its output) will pay dividends in the future, either to you or the programmer who’s reading your code in six months.

Schools, especially intro classes, are very misleading about the code lifecycle. You spend most of your time in school writing your own code. But in the real world, you spend far more time reading, modifying, and fixing others’ code. And once in a while, when you’re reading and debugging some particularly gnarly code, you’ll come to an awful realization: The programmer who wrote this mess was you, six months ago. Don’t let this happen to you!

More on tests

Good tests are generally small – testing just one thing.

They usually use assertEquals(), where the first argument is what’s expected and the second is what’s being tested. (They might use assertTrue or assertFalse when checking boolean results, and there are a few others, but we’ll come back to them later). It’s important to write tests this way, so that tools (like, for example Eclipse) show the right things to you in their UI.

In-class exercise

Is this a well-written test (x4)

Back to the driver

Let’s sketch out the driver so we know what we’ll need to do. How is a typical game played?

  • The board is empty
  • Players take turns until
    • one player wins
    • or it’s a draw (tie game)
  • The game ends, the result is announced

What might this look like in code? Here’s a first cut.

    public static void main(String[] args) {
        TicTacToeBoard board = new TicTacToeBoard();

        try (Scanner scanner = new Scanner(System.in)) {
            while (!board.isGameOver()) {
                System.out.println(board);
                System.out.println("Player " + board.getCurrentPlayer() + ", choose a move (0-8):");
                int move = scanner.nextInt();
                board.move(move);
            }
            System.out.println("Game result: " + board.getResult());
        }
    }

Notice that we don’t worry about various error conditions (like if the player chooses an invalid move); we’ll get to that later. Also notice we haven’t yet defined any of these methods. Eclipse will sketch them out for us if we want (demo in class) though notice that we of course need to implement them.

Also notice that we’ve divided the methods into observers, which return a fact about the underlying object but don’t change the object’s state, and mutators, which change something about the object. It’s good practice to put methods into one or the other of these groups, to help us reason about our code.

Let’s leave isGameOver() alone for now, and work on getCurrentPlayer() and move().

Ways to implement an observer

getCurrentPlayer() returns the current player as a string. We can either explicitly keep track of whose turn it is (in an instance variable), or compute it by looking at the board (if there are an equal number of Xs and Os, it’s X’s turn; if there are more Xs, it’s O’s turn). Let’s keep track explicitly for now by adding an instance variable and initializing it in the constructor.

    private String currentPlayer;
//...later, in the constructor
        currentPlayer = X;

Another observer

Let’s make sure we return it in our getter:

    public String getCurrentPlayer() {
        return currentPlayer;
    }

A mutator

And, at the end of every move, it should be the other player’s turn, so let’s encode that, too:

    public void move(int move) {
        if (currentPlayer == X) {
            currentPlayer = O;
        } else { // currentPlayer == O
            currentPlayer = X;
        }
    }

Note that this method, which changes the object’s state, does not return a value (it’s void). Often mutators are void, since you generally don’t want to mix mutation and observation.

Checking in the driver, it works, inasmuch as the players alternate.

Testing our mutator

We showed in the console that the player’s turn will swap back and forth after each move(). Let’s start by adding a test that captures this behavior:

    @Test
    public void testPlayerChangeAfterMove() {
        TicTacToeBoard board = new TicTacToeBoard();
        assertEquals("X", board.getCurrentPlayer());
        board.move(0);
        assertEquals("O", board.getCurrentPlayer());
    }

And while we’re at it, why not a bunch of moves?

    @Test
    public void testPlayerChangeAfterMultipleMove() {
        TicTacToeBoard board = new TicTacToeBoard();
        assertEquals("X", board.getCurrentPlayer());
        board.move(0);
        assertEquals("O", board.getCurrentPlayer());
        board.move(1);
        assertEquals("X", board.getCurrentPlayer());
        board.move(2);
        assertEquals("O", board.getCurrentPlayer());
        board.move(3);
        assertEquals("X", board.getCurrentPlayer());
        board.move(4);
        assertEquals("O", board.getCurrentPlayer());
        board.move(5);
        assertEquals("X", board.getCurrentPlayer());
    }

OK, and since we’re getting crazy, how about we get move() working now, too? It’s only a one-liner:

        board[move] = X;

And test it in the console. Huh, there’s a problem. Now, I get it, most of you see the problem immediately. But that’s kinda on purpose here; some problems are easier than others to spot (and then fix). Sometimes they’re simple problems you just can’t see yourself; sometimes they’re more subtle. Instead of caveman debugging this one (you know, adding “printlns” everywhere, let’s try the debugger) so we can see what’s going on.

First, we must set a breakpoint! Double-click the “gutter” and create a small blue dot.

Then run under the debugger.

Note that we can escape using the “perspective” buttons in the upper-right.

Values are displayed in the upper-right; everything in scope is here.

We can “step” forward line-by-line. “over” means stay in this method; “into” means follow any method call in that line into another method; “out” means return to the calling method.

The second time through, we see that the wrong value is assigned to the board entry – whoops, we hardcoded it! Let’s fix that:

        board[move] = currentPlayer;

And test it in the console with our driver.

And maybe some tests now too:

In-class exercise

    @Test
    public void testOneMove() {
        TicTacToeBoard board = new TicTacToeBoard();
        board.move(0);
        assertEquals("X . .\n. . .\n. . .", board.toString());
    }

    @Test
    public void testMultipleMoves() {
        TicTacToeBoard board = new TicTacToeBoard();
        board.move(0);
        assertEquals("X . .\n. . .\n. . .", board.toString());
        board.move(1);
        assertEquals("X O .\n. . .\n. . .", board.toString());
        board.move(2);
        assertEquals("X O X\n. . .\n. . .", board.toString());
        // and so on...
    }

Again, (one of) the reason(s) we encode this stuff into tests is so that if we break something later, we’ll spot it immediately and have a working test case to debug with.

How about checking for the end of the game? In English, in isGameOver() we check to see if either player has won or if it’s a draw. If any of those are true, the game is over, otherwise it’s a draw. Similarly, our getResult() method needs to determine which player won, or if it’s a draw.

Stopping and thinking for a second: we can express our draw check in terms of our winner check. If neither player has won, and the board is full, then it’s a draw.

What does this look like?

    public boolean isGameOver() {
        return (isWinner(Space.X) || isWinner(Space.O) || isDraw()); 
    }

How do we check if a player has won? If they have three in a row. We could hardcode this, for example:

    private String getWinner() {
        if (board[0][0].equals(X) && board[0][1].equals(X) && board[0][2].equals(X)) {
            return X;
        }
        else if //...
    }

but we’d have to do 2 * (3 + 3 + 2) if clauses, mostly copy-pasted, making sure we didn’t screw up the board index each time. That’s OK, I guess, but it fails to capture the patterns we’re looking for: rows, columns, diagonals. Maybe something like this would be better:

    private boolean isWinner(Space player) {
        // check rows
        for (int i = 0; i < 7; i += 3) { // pick a row (starting at i)
            boolean won = true;
            for (int j = 0; j < 3; j++) { // check each square in that row
                if (board[i + j] != player) {
                    won = false; // any one is not this player
                }
            }
            if (won) return true;
        }

        // check cols
        for (int i = 0; i < 3; i++) { // pick a column (starting at i)
            boolean won = true;
            for (int j = 0; j < 7; j += 3) { // check each square in that column
                if (board[i + j] != player) {
                    won = false; // any one is not this player
                }
            }
            if (won) return true;
        }

        // check diagonals
        if (board[0] == player && board[4] == player && board[8] == player) {
            return true;
        }

        if (board[2] == player && board[4] == player && board[6] == player) {
            return true;
        }

        // maybe it would have been shorter just to do the eight cases individually!
        // context matters.

        return false;   
    }

Two notes: First, we could further condense the row/column checks with another inner for loop (though it’s not much cleaner); second, notice this is a version of the for loop pattern we talked about in a previous class, where we check a bunch of stuff: if it ever becomes true, then the method returns true, and if we make it to the end, the method returns false.

How about a draw? If either player has won, it’s not a draw. Or, if there are still empty spaces, it’s not a draw. Otherwise, it’s a draw:

    private boolean isDraw() {
        // has someone won?
        if (isWinner(Space.X) || isWinner (Space.O)) {
            return false;
        }

        // otherwise, is the board full?
        for (Space s : board) {
            // if any of them are empty, no, it's not full
            if (s == Space.EMPTY) {
                return false;
            }
        }

        return true;
    }

Notice I can freely call these “observer” methods (isDraw, isWinner, etc.): they only observe values, but they don’t change anything. Having methods like this means I don’t have to worry about “breaking” an object. A common beginner style error is to have a method that both answers a question about an object and changes its state (Imagine a method that, say, both makes a move and returns the current winner, if any. How do you then check winner without making a move, too? You can’t, unless you add another method. But then there are two methods that check winner, which means you need to keep them in sync, which means it’s easier to get later things wrong…).

Now we can also pretty easily return the game result, too:

    public String getResult() {
        if (isWinner(Space.X)) {
            return "Player X wins!";            
        }
        else if (isWinner(Space.O)) {
            return "Player O wins!";
        }
        else if (isDraw()) {
            return "It's a draw.";
        }
        else {
            return "Game ain't over yet.";
        }
    }

It appears to work (demo in console). One possible test might be:

    @Test
    public void testDiagonalXWinner() {
        TicTacToeBoard board = new TicTacToeBoard();
        board.move(0);
        board.move(1);
        board.move(4);
        board.move(3);
        board.move(8);
        assertEquals("Player X wins!", board.getResult());
    }

OK, enough tic-tac-toe. There’s more you could do (like make sure moves are in the right range, and not over an existing player’s move) but I’m sure you could figure it out (and test it!) yourself. Let’s go back to our Java review.

Methods

In Java, methods are always attached to a class. A method consists of a declaration and a body, which is just a sequence of statements. Let’s look at a declaration more closely:

public static void main(String[] args) {
  ...
}

WTF is going on here!?!?! is usually the reaction we get in 121 when people first start learning Java. But now, you probably know enough to understand most of it. Let’s tackle it inside-out.

String[] args is the parameter to this method. One or more parameters are passed in, and they look like (and in many respects behave as) variable declarations. The difference is that their values are provided by the calling method (or in the very special case of main, by the JVM). Here, main gets an array of Strings, which are exactly what is passed on the command line to the java interpreter, e.g.,

public class PrintArgs {
  public static void main(String[] args) {
    int i = 0;
    for (String a : args) {
      System.out.println(i + ": " + a);
      i += 1;
    }
  }
}
> javac PrintArgs.java
> java PrintArgs Hello Marc!
0: Hello
1: Marc!

Next is the name of the method, which is by convention “camelCased,” starting with a lowercase letter. Next is the method’s return type, or void if it does not return anything.

Important note: methods that return void typically do something, like print a value, or delete an item from a list, or the like. They affect state, or are stateful. Sometimes but not always, methods that return a value don’t do something (they are more like mathematical functions). The only way to be sure is to read the documentation (or the method code itself)! But a method’s public API should describe what it does.

Next comes one or more method modifiers: either abstract or one or more of static, final, and synchronized. static methods are associated with a class, but not a particular instance of that class (in other words, not with an object). We’ll talk more about the other modifiers later as they come up.

Finally there is at most one of public, protected, or private, which are member access modifiers. Which objects can invoke this method is determined by this modifier. public and private are probably familiar to you (any object and only objects of this class, respectively). protected and no modifier (“default access” or “package access”) have special meanings we’ll skip for now.