Chapter 14. A Simple Game

This example involves refactoring and test-driven design.

Suppose we've decided to develop a system to play games in the tic-tac-toe family: squares occupied by different markers. In tic-tac-toe, you have a 3 x 3 grid, and you try to put your mark in three boxes in a row. In Connect Four by Hasbro, you have a rectangular grid and try to get four boxes in a row, but columns have to be filled from bottom to top. We'll start with a simplified version of tic-tac-toe and work our way up to the general case.

Here are some tests and the first version of the code (online at ).

 import junit.framework.*; public class GameTest extends TestCase {    public GameTest(String s) {super(s);}    public void testDefaultMove() {       Game game = new Game("XOXOX-OXO");       assertEquals(5, game.move('X'));       game = new Game("XOXOXOOX-");       assertEquals(8, game.move('O'));       game = new Game("---------");       assertEquals(0, game.move('X'));       game = new Game("XXXXXXXXX");       assertEquals(-1, game.move('X'));    }    public void testFindWinningMove() {       Game game = new Game("XO-XX-OOX");       assertEquals(5, game.move('X'));    }    public void testWinConditions() {       Game game = new Game("---XXX---");       assertEquals('X', game.winner());    } } public class Game {    public StringBuffer board;    public Game(String s) {board = new StringBuffer(s);}    public Game(StringBuffer s, int position, char player) {       board = new StringBuffer();       board.append(s);       board.setCharAt(position, player);    }    public int move(char player) {       for (int i = 0; i < 9; i++) {          if (board.charAt(i) == '-') {             Game game = play(i, player);             if (game.winner() == player)                return i;          }       }       for (int i = 0; i < 9; i++) {          if (board.charAt(i) == '-')             return i;       }       return -1;    }    public Game play(int i, char player) {       return new Game(this.board, i, player);    }    public char winner() {       if (board.charAt(0) != '-'             && board.charAt(0) == board.charAt(1)             && board.charAt(1) == board.charAt(2))          return board.charAt(0);       if (board.charAt(3) != '-'             && board.charAt(3) == board.charAt(4)             && board.charAt(4) == board.charAt(5))          return board.charAt(3);       if (board.charAt(6) != '-'             && board.charAt(6) == board.charAt(7)             && board.charAt(7) == board.charAt(8))          return board.charAt(6);       return '-';    } } 

Notice that the winner() routine is simplified: You win by getting three in a row horizontally. Notice also that the heuristics for what to play are primitive: Win if you can, play anything otherwise . We'll migrate toward something capable of more sophisticated strategies.

Exercise 60 Smells.

Go through this code and identify smells.

See Appendix A for solutions.

Exercise 61 Easy Changes.

It's not always easy to know what to do with code. Let's fix some of the easy things first. Fix them one at a time.

  • The variable i doesn't explain much either. Change it to move .

  • The value -1 is a flag value; create a constant NoMove to represent it.

  • The winner() function has a lot of duplication. Eliminate the duplication.

  • The check for a board character being a '-' is really a check that the square is unoccupied. Extract a method to do this, and name it appropriately.

We have two "for" loopsone to find a winning move, the other to find a default move. One way to handle this would be to extract each one into a method. As we add more strategies, we could see each strategy getting its own method. An alternative would be to merge the two loops and handle things in one pass through the possible moves. We'll take the latter approach.

One step along the way might be to assign a value to a temporary variable rather than to return it right away. You might make the second loop look like this:

 defaultMove = NoMove; for (int i = 0; i < 9; i++) {    if (board[i] == '-')       if (defaultMove == NoMove)          defaultMove = i; } 
Exercise 62 Fuse Loops.

The term Fuse Loops means combine two loops into one. Do so in small steps, in such a way that you maintain safety as you do it. When is it safe to merge two loops? ( Fuse Loops is a standard technique compilers use; it's not in Fowler's Refactoring catalog.)

See Appendix A for solutions.

Exercise 62. Fuse Loops
  • It's easiest if both loops have the same range.

  • It's important that the i th entry of the second loop not depend on anything past the i th entry in the first loop.

That is the safest approach, used because we do not want our refactoring to change behavior. To be equivalent to the original, we need the guard clause to make sure we haven't assigned a defaultMove yet. But let's put on a development hat: We don't really care which default move we make, so we could delete the defaultMove==NoMove test. It's not necessary to stop when we find a viable move; that is, there's no harm in trying each possible move, provided we prefer wins to defaults. So you can delete the break tests that exit early. Run the tests again and be sure you haven't changed anything important. (You may have to change the tests. What does this tell you?)

Now we have a single loop, but the condition to decide what to return is still a little complicated:

 if (winningMove != NoMove)    return winningMove; if (defaultMove != NoMove)    return defaultMove; return NoMove; 
Exercise 63 Result.

How would you simplify this?

Exercise 64 Next .

What refactorings would you tackle next?

There are still a lot of magic numbers floating around. The winner() routine is full of them, and we still have a "9" in the main loop of bestMoveFor() .

Exercise 65 Constants.

What is "9"? Name some constants and rewrite it.

I struggled over the names , and ended up using rows and columns . (I'm also experimenting with different naming conventions; some styles might use ROWS and COLUMNS.)

Exercise 66 Duplication in winner().

Here's the winner() routine as it is now.

 public char winner() {    if (!canPlay(0)        && board.charAt(0) == board.charAt(1)        && board.charAt(1) == board.charAt(2))        return board.charAt(0);        if (!canPlay(3)            && board.charAt(3) == board.charAt(4)            && board.charAt(4) == board.charAt(5))            return board.charAt(3);            if (!canPlay(6)                && board.charAt(6) == board.charAt(7)                && board.charAt(7) == board.charAt(8))                return board.charAt(6);            return '-'; } 

Eliminate the duplication in this routine.

I imagine you used a loop over the rows and extracted some sort of method that decided if there was a winning combination.

To assess your refactoring of this, switch back to a development hat. There are two changes we might make, given our vision. The first is that we're not playing tic-tac-toe yet because we're only allowing horizontal three-in-a-row wins. Extend the routine to allow vertical and diagonal wins. Was it easy to change given your refactoring?

There's another hidden constant: the number in a row that it takes to win. (Recall that we mentioned Connect Four as one of the variations we eventually want to support.) Suppose we change to a 5 x 5 grid and want four in a row to win. How easy is that to put into the code? You needn't add this feature yet; this is more of a thought question. (I gave myself a B on this one: I defined a winningCombination() routine that took three positions as arguments, and I created an array that explicitly called out the winning squares. This works fine for tic-tac-toe but doesn't cover n -in-a-row.)

Most of the refactorings we've applied so far have been obvious improvements. Now I want to grow and improve my program through a combination of refactoring and new implementation, but I'm not sure what's best to do next.

I think of this as subjunctive programming . The subjunctive tense is the one used to talk about possible worlds ("If I were a rich man"). My stance is that I'll try some ideas and see where they lead, but if they don't work out, that's OK.

Two things make subjunctive programming bearable: a partner, so you can kick around ideas, and a source control system, so you can back out anything you don't like.

The general direction is that I want to allow more sophisticated strategies than "win if you can and play anything otherwise." My thought is to create a Move object and let it evaluate how good the move is.

Exercise 67 Iterator.

We're running a "for" loop over the integers representing possible moves. Turn this into an iterator over the moves. This is not one of the refactorings in Fowler's Refactoring catalog, so let's take it in small steps.

  1. Convert from int to Integer first; that will get us into the domain of objects.

  2. Make an iterator, where the next() method returns an Integer. Remove all references to int from the bestMoveFor() method.

  3. Introduce a Move class.

Now, the main loop looks something like this:

 for (Iterator iter = new MoveIterator(); iter.hasNext();) {    Move move = (Move);    if (!canPlay(move)) continue; 
Exercise 68 Legal Moves Only.

Our iterator delivers all moves, legal or not. Move the canPlay test into the iterator so it delivers only legal moves.

Currently, we're just looping through possible moves, trying to select the best one, following a simple rule: Wins are best, anything else is acceptable. But wins are rare, so we'd like to pick a good intermediate move (some moves are better than others). We can think of each move as having a score: How good is it? Just to have something to work with, we'll say a win is worth 100 points and any other move is worth 0 points. (We could also think of wins by the opposing player as being worth minus 100 points (to the home player), but we won't check for thoseyet.)

Note that we're out of the domain of refactoring; we're making a semantic change to our program. That's the way development works. Because refactoring makes things cleaner, we can see better ways to do them.

Refactoring Workbook
Refactoring Workbook
ISBN: 0321109295
EAN: 2147483647
Year: 2003
Pages: 146

Similar book on Amazon © 2008-2017.
If you may any questions please contact us: