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.
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.
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.
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!
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).
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.