P2 TicTacToe Examples

The TicTacToe game is used in lectures 4, 5, 8 and 10. We iteratively develop a simple ascii game in lecture 4 (versions 1.0-1.2). In lecture 5 we then refactor the game and use inheritance to handle as well Gomoku, a similar game played on a Go board (versions 1.3-1.6). Next, in lecture 8 we develop a GUI interface using the Java 1.1 AWT (versions 2.0 and 2.1). Finally, we distribute the game in lecture 10 using RMI (version 3.0). During each iteration, we make sure all our regression tests run before freezing the version. The notes that follow below document the refactoring steps taken as new features are added. They are meant to complement the lecture notes, not replace them.

You can try out the multi-user RMI version (3.0) of TicTacToe/Gomoku online.

The javadoc of versions 1.6, 2.0 and 3.0 are also available on-line.


TicTacToe 1.0

See chapter 4. Here we iteratively develop classes to play the game of TicTacToe.

Each new version adds some basic functionality together with the tests for that functionality. Version 1.0 does nothing but introduce a GameDriver and a TicTacToe instance. Version 1.2 is the first usable game with a primitive ascii interface. Note that the version numbers refer to the complete game, not the individual classes, so there are no versions 1.0 or 1.1 of the class Player, for example.

TicTacToe 1.1

Version 1.1 allows us to set and get X and O values on the TicTacToe board. We introduce a TestDriver that simply tests if setting and getting works.

TicTacToe 1.2

This is the first usable game with a primitive ascii interface. We modify the GameDriver so that the TestDriver can also play a game. There is now a Player class that interacts with users to prompt them for moves. The Player is designed so that it can also be instantiated from a String containing prepackaged test moves. The TicTacToe class has been completed to maintain the game invariants, and we explicitly check pre-conditions and invariants at various points. Note that the logic of TicTacToe.checkWinner() is ugly — checking for rows, columns or diagonals should perhaps be factored out to a separate, more generic method.

TicTacToe 1.3

See chapter 5 on inheritance and refactoring. Versions 1.3 through 1.6 iteratively refactor TicTacToe so that it inherits from an AbstractBoardGame, which in turn implements a BoardGame interface. Gomoku is a similar game that shares the same interface and almost all of the same implementation. The refactoring steps change the TicTacToe design so that the two games can share as much as possible.

In version 1.3 we add the BoardGame interface and patch the GameDriver and TicTacToe to use that interface. We also patch the tests to be silent by default. To do this we must add a BoardGame/TicTacToe.currentPlayer() method and a verbose flag to GameDriver. Finally we patch TestDriver to use verbose=false.

TicTacToe 1.4

Now we introduce the AbstractBoardGame class which implements methods common to TicTacToe and Gomoku. In the end, it implements everything except the TicTacToe constructor. Methods that previously were private in TicTacToe 1.3 are now protected in AbstractBoardGame. We patch Player to use the BoardGame interface instead of the concrete TicTacToe class.

TicTacToe 1.5

Now we check which parts of AbstractBoardGame are generic and can be used for both TicTacToe and Gomoku. The number of rows and columns are fine. We introduce an init() method to initialize the board state. (This should be called by the game constructors.) We patch the set() and get() methods to be indexed by ints instead of chars. move() now is indexed by String coordinates which are parsed by getCol() and getRow(). toString() must be rewritten. We must similarly rewrite inRange(), invariant() and test(). Finally we patch the BoardGame interface and its clients, including the GameDriver.

TicTacToe 1.6

Finally we are ready to introduce Gomoku. Its implementation is trivial -- like TicTacToe, it just extends AbstractBoardGame with a constrcutor that calls init() with the right parameters. We add suitable tests for Gomoku and reorganize the TestDriver so all the test data for TicTacToe are kept together. The hardest part is to reimplement checkWinner() -- the algorithm to check if one of the players has won the game. We introduce the notion of a Runner -- an object that starts from the last position played and runs in both directions (horizontally, vertically and diagonally) to see if the winning number of squares have been occupied. To implement Runner, BoardGame.get() and inRange() must be public. We patch GameDriver to query the user for TicTacToe or Gomoku, run our tests, and we are done!


TicTacToe 2.0

See lecture 8. Now we add a GUI to TicTacToe and Gomoku.

We introduce GameApplet to bundle the GUI, Place to model a square on the board, and PlaceListener to handle mouse clicks on a place. A GUI architectures requires that the BoardGame and Player both be passive (since they passively wait for mouse events to happen, rather than actively querying fora String input). We consequently must shift some responsibilities between the BoardGame and Player classes. We remove BoardGame.update() and shift this responsibility to GameDriver.playGame(). (We run our tests and continue.)

Now we move getRow() and getCol() from BoardGame to Player; we change BoardGame.move() to take int parameters; we add assert to Player; we run our tests and move on. We factor out the assert() method to and Asserter mixin class, and run our tests again.

Next we change BoardGame.move() to take Player as arg (not a mark), and add an assertion to check if the Player is the currentPlayer (since it is no longer our responsibility but that of the GUI). We run our tests and continue.

We need to handle different kinds of Players, since we have the test driver, the ascii game and the GUI game. We therefore introduce a Player interface, with InactivePlayer and StreamPlayer classes implementing it. We run our tests again and continue. We introduce Player.setGame() so players know their game (set when they join the game); we run our tests.

Now we start to link the model to the view! We introduce AppletPlayer. We need Places to know their row & column so that mouse events can be correctly interpreted as moves. PlaceListener triggers AppletPlayer to move when it gets a mouse event. In order to propagate changes in the game state back to the GUI, we make AbstractBoardGame extend java.util.Observable. (Unfortunately this means it can no longer extend our Asserter mixin class!)

During testing we detect and fix a nasty bug in AbstractBoardGame -- _gameState was initialized with rows & cols instead of cols & rows -- but we never tested a game where rows != cols.

We introduce a new class Move to allow the game to bundle details of updates to the GUI. We add Place.setMove() to interpret which image to set (i.e., "X" or "O"). We extend PlaceListener to report when game is over, and who wins.

We add a button to the GameApplet to start new game. Finally we change BoardGame to hold a matrix of Players, not marks (affects many classes!). We run our tests and we are done!

TicTacToe 2.1

This version of the game is not directly used in the lecture notes.

We want to prepare the game for distribution, so we will separate the game into client, server and game, with two separate windows for the two players. We move all classes to either client or tictactoe packages, patch all imports ... and run.

We eliminate Player from the client package -- Players will only be modelled on the server. The GUI will pass mouse events to the server where they will be interpreted by the appropriate Player instance. We therefore introduce BoardGame.remoteMove().

Since the game must be started on the client side but instantiated on the server side, we introduce a server side GameFactory. We remove AppletPlayer and introduce PassivePlayer on the server side. Now only GameFactory, BoardGame, Move and AssertionException are imported from client.

We split the applet into a button to join a game and a separate game view window that pops up afterwards. There is one view for each player, so GameFactory returns two instances of a new game. Each player must join() the game to get a mark ("X" or "O") from the game, and wait to start until the game is ready (i.e., there are two players).


TicTacToe 3.0

This is by far the most ambitious step. See lecture 10 for details. In this last phase we migrate the game to a client server architecture, with the clients running as Applets and the server running as an RMI Remote object. In fact, the client applets also provide RMI interfaces since they must be updated by the server when the game state changes.

First we define a separate server package for the RMI server interfaces and classes: RemoteGameFactory, RemoteGame, and Move (the latter is serializable, not remote).

We must make GameFactory a true object so it can work over RMI. (Before it was only a class with static methods for creating game instances.) It implements the RemoteGameFactory interface.

We patch RemoteGame to not throw an AssertionException. (To keep our interfaces simple, the server will at worst throw a RemoteException.) We add a GameProxy wrapper around BoardGame to implement RemoteGame. (BoardGame throws AssertionException and some methods return a Player; GameProxy throws only RemoteExceptions, and its methods return a char mark of a Player.)

We find we must add BoardGame.player() to implement some GameProxy methods.

(We fix an obscure problem: the compiler complains about GameFactory and AbstractBoardGame not throwing RemoteException in their constructor; this is the default constructor inherited from UnicastRemoteObject; it is enough to declare it and invoke super().)

The client must be patched to use server interface. We add RemoteObserver to the server package to allow clients to be notified of updates remotely. We add WrappedObserver class to adapt RemoteObserver interface and catch RemoteExceptions.

We add GameFactory.main() to initialize the factory and register it with the RMI registry. The argument String we expect is the server:port (e.g., asterix.unibe.ch:2001). The name we register is "GameFactory".

Java doesn't support multiple inheritance. Since GameView already extends Frame, we must introduce a GameObserver that extends UnicastRemoteObject to handle observer updates.

Our server objects (GameFactory and GameProxy) are liable to receiving concurrent requests from clients, so we must declare the public methods as synchronized (this goes beyond what we can cover in this course!). (GameObserver.update() doesn't need to be synchronized, because it can never be called concurrently? Nothing else on client side might get concurrent updates since updates only come from server!) GameFactory.joinGame() must be synchronized since its state is modified here. GameProxy methods must be synchronized, but the BoardGame it wraps must not be (to avoid nested monitor problems).

TestDriver runs ok. Are we done?

The applet starts ok, but seems to deadlock. The problem is a cycle in the synchronized methods! [PlaceListener.mouseClicked() -> GameProxy.move() ... -> WrappedObserver.update() -> GameObserver.update() -> GameView.update() -> GameProxy.notOver() !!!] The solution is to make WrapperObserver.update() run in a separate Thread now the applet works ok!

NB: the concurrency aspects of the distributed version are at the very limits of what we can cover in this course. We try to avoid as much as possible problems of concurrency by choosing a simple architecture.


Last modified: 2001-02-21