Assignment 3
Home ] Up ] Assignment 1 ] Assignment 2 ] [ Assignment 3 ] Assignment 4 ] Assignment 5 ]

 

 

Client/Server TicTacToe

In this assignment, we'll build on what we did in the earlier TicTacToe assignment. This time, we'll turn it into client/server version of TicTacToe!

Requirements

We would like to create a TicTacToe server, which provides the ability to play TicTacToe to a number of TicTacToe clients.

The TicTacToe server runs on some machine in the network, and each TicTacToe client connects to it from another (possibly the same) machine within the same network. The server pairs up the clients, and decides which one will play cross and which one nought. The server is capable of running multiple simultaneous TicTacToe games (subject to resource limitations, such as memory).

The TicTacToe Client

A single client for this version of the TicTacToe game should look something like this:


(that's an image, not a live applet, in case you tried to click on it!)

This should look very reminiscent of the way things were in the earlier TicTacToe assignment. The way the game is played is the same -- it's just that the two Boards are now totally separated from each other. You should be able to reuse lots of the code that you created earlier, with relatively minor modifications.

Note that the above image shows an applet version. However, you will almost certainly wish to use an application instead, to avoid the problems related to applet security restrictions. An unsigned applet is restricted to making network connections only to the machine from which it was downloaded. Unless you have a web server available to you on which you can place the TicTacToe server program, it will be difficult to make things work with applets. (Even if you do have such a web server available to you, it's still more of a pain to set up, so I still recommend you use applications). Of course, you can create applets that you intend only to run via appletviewer, which does not enforce the security restrictions that a true browser does.


The TicTacToe Server

The TicTacToe server has no graphical user interface. Its source code will initially look very much like one of the multi-threaded server examples in the networking section of this course. You can start off with one of those examples and work from there -- be sure to use a multi-threaded version of the server!

The TicTacToe Protocol (TTTP)

In order for the TicTacToe clients to communicate with the TicTacToe server, and vice versa, there must be a well-defined protocol that they must follow to ensure that the communication is reliable and properly understood. The use of a protocol is a common requirement for such client/server communication. For example, a mail client typically talks to a mail server using the SMTP (Simple Mail Transport Protocol), a browser communicates with a web server using the HTTP (HyperText Transport Protocol), and so on.

We'll use the TicTacToe Protocol (TTTP), which is a simple (probably too simple, but it's OK for this assignment!) protocol whose features are described in the TicTacToeProtocol interface:

package tictactoe;

/**
 *  Interface to define the TicTacToe protocol
 *  used by client/server TicTacToe.
 *
 *  @author Bryan J. Higgs, 24 February 2000
 *  @version 1.0
 */
public interface TicTacToeProtocol
{
    /////// Client to server commands ///////
    
    /**
     *  Client connect request.
     *  <p>
     *  Expected response: 
     *  <br>CONNECTED
     *  <br>When both players have connected,
     *  START_GAME to both players.
     */
    public static final int CONNECT         = 1;

    /**
     *  Client disconnect request.
     *  <p>
     *  Expected response: DISCONNECTED
     */
    public static final int DISCONNECT      = 2;
    
    /**
     *  Client Autoplay request.
     *  <p>
     *  Expected response: 
     *  <br>Autoplay events
     */
    public static final int AUTOPLAY        = 3;
    
    /**
     *  Client cross move request.
     *  <p>
     *  Followed by row and column of square.
     *  <p>
     *  Expected response: 
     *  <br>CROSS_IN_SQUARE, if valid move
     *  <br>nothing, otherwise.
     */
    public static final int CROSS_MOVE      = 4;
    
    /**
     *  Client nought move request.
     *  <p>
     *  Followed by row and column of square.
     *  <p>
     *  Expected response: 
     *  <br>NOUGHT_IN_SQUARE, if valid move
     *  <br>nothing, otherwise.
     */
    public static final int NOUGHT_MOVE     = 5;
    
    /**
     *  Client get game status request.
     *  <p>
     *  Expected response: 
     *  <br>IN_PROGRESS, CROSS_WINS, 
     *  NOUGHT_WINS, or STALEMATE
     */
    public static final int GET_STATUS      = 6;
    
    /**
     *  Client restart game request.
     *  <p>
     *  Expected response: CLEAR_BOARD
     */
    public static final int RESTART_GAME    = 7;
    
    
    /////// Server to client responses ///////
    
    /**
     *  Server response to client CONNECT request.
     */
    public static final int CONNECTED       = 101;
    
    /**
     *  Server response to client DISCONNECT request.
     */
    public static final int DISCONNECTED    = 102;
    
    /**
     *  Server response to start game.
     *  <p>
     *  Sent when both players in a game have connected.
     *  <p>
     *  Followed by a boolean value:
     *  <br>true if player plays cross
     *  <br>false if player plays nought
     *  <p>
     *  Client is expected to set itself to play X or O.
     */
    public static final int START_GAME      = 103;

    /**
     *  Cross selected square.
     *  <p>
     *  Followed by row and column of square.
     *  <p>
     *  Client is expected to display a cross in the
     *  specified square.
     */
    public static final int CROSS_IN_SQUARE = 104;

    /**
     *  Nought selected square.
     *  <p>
     *  Followed by row and column of square.
     *  <p>
     *  Client is expected to display a cross in the
     *  specified square.
     */
    public static final int NOUGHT_IN_SQUARE= 105;

    /**
     *  Game status response.
     *  <p>
     *  Followed by an int containing the status value
     *  (IN_PROGRESS, WIN, LOSE, STALEMATE)
     */
    public static final int RETURN_STATUS   = 106;

    /**
     *  Clear Board response (request to client).
     *  <p>
     *  Client is expected to clear the board.
     */
    public static final int CLEAR_BOARD     = 107;
    
    
    //////// Server to client errors ////////
    
    /**
     *  Error encountered.
     *  <p>
     *  Followed by explanatory string.
     */
    public static final int ERROR           = 901;
    
    /////////////////////////////////////////////////////

    /**
     *  Default port to use for client/server communications.
     */
	public static final int DEFAULT_PORT    = 5000;
}

Note that this interface simply defines a number of constants, and no methods whatsoever. While you could have one or more of your classes implement this protocol if you wish, it isn't necessary; you can simply have your code refer to the its constants directly using TicTacToeProtocol.CROSS_IN_SQUARE, etc.

Let's illustrate how the protocol works by following a typical TicTacToe game session between two clients and the server:

Client A Client B Server
Connects to server socket   Accepts client connection.
Sends a CONNECT request to the server.   Receives the CONNECT request, and sends a CONNECTED response to client A.
  Connects to server socket Accepts client connection.
  Sends a CONNECT request to the server. Receives the CONNECT request, and sends a CONNECTED response to client B.
Pairs up the two clients, creates a game for them to play, then sends a START_GAME to both clients, telling client A to play X and client B to play O.
Receives START_GAME, determines that is playing X, and enables the user interface for play. Receives START_GAME, determines that is playing O, and enables the user interface for play.  
User clicks on square. This causes a CROSS_MOVE (followed by row and column of square) to be sent to server.   Receives CROSS_MOVE and row and column information, and calls the game appropriately. The game determines whether the move is legal; if it is, a CROSS_IN_SQUARE message (with row and column) is sent to both clients.
Receives CROSS_IN_SQUARE (with row and column), and causes the cross image to display in the appropriate square. Receives CROSS_IN_SQUARE (with row and column), and causes the cross image to display in the appropriate square.  
  User clicks on square. This causes a NOUGHT_MOVE (followed by row and column of square) to be sent to server. Receives NOUGHT_MOVE and row and column information, and calls the game appropriately. The game determines whether the move is legal; if it is, a NOUGHT_IN_SQUARE message (with row and column) is sent to both clients.
Receives NOUGHT_IN_SQUARE (with row and column), and causes the nought image to display in the appropriate square. Receives NOUGHT_IN_SQUARE (with row and column), and causes the nought image to display in the appropriate square.  
  The process repeats until one of the clients wins, or stalemate occurs. Detects that X or O has won, or stalemate has occurred. Sends a RETURN_STATUS message to both clients, with the appropriate code.
Receives RETURN_STATUS message and displays the appropriate message in the text field. Receives RETURN_STATUS message and displays the appropriate message in the text field.  
User clicks on Restart button. This causes a RESTART_GAME message to be sent to the server. (Either client could do this.)   Receives the RESTART_GAME message. Tells the game to restart, which causes a CLEAR_BOARD message to be sent to each client.
Receives CLEAR_BOARD message, and clears its board. Receives CLEAR_BOARD message, and clears its board.  
  User clicks on Auto Play button. This causes an AUTOPLAY message to be sent to the server (either client could to this). Receives AUTOPLAY message, tells the game to start autoplay, which causes the appropriate CROSS_IN_SQUARE, NOUGHT_IN_SQUARE messages to be sent, to both clients until the game ends, and then sends the appropriate RETURN_STATUS message, indicating stalemate has occurred to both clients.
Receives the CROSS_IN_SQUARE, NOUGHT_IN_SQUARE, and RETURN_STATUS messages, and updates its user interface appropriately. Receives the CROSS_IN_SQUARE, NOUGHT_IN_SQUARE, and RETURN_STATUS messages, and updates its user interface appropriately.  

Threading Requirements

You will find that both the server and the client will need to be multi-threaded.

It should be clear why the server must be multi-threaded: As each client connects to the server's port, the server will accept the connection, and will then create an instance of a class (I called mine GamePlayer) to represent the client within the server program. It will start a thread for each of these instances, and the instance will be solely responsible for communicating with its client. After the first client connects, its GamePlayer instance is not yet ready to play, because it does not yet have an opponent, so it does not yet create a game. Once the second client connects, its GamePlayer instance takes responsibility for creating the game (it's a GameImpl on the server), and then associating the two GamePlayer instances together (I had an m_opponent field to keep track of who each GamePlayer's opponent was). Finally, once the GameImpl is created, and everything is set up between the two GamePlayer instances, the appropriate messages can be sent to each client to tell them that the game has started.

It may be less clear why you need the client to be multi-threaded, until you try to implement it. You will find that you will need to have code that stays in a loop, waiting for input from the client. If you do this in a straightforward way, you'll likely find that the GUI won't respond, because the loop, which mostly blocks waiting for a message from the server, doesn't allow events to be handled properly. I wrote my client so that the loop was running in a separate thread, and it worked pretty well.

Note: If you decide to use JFC/Swing for your client GUI, you need to be aware that, unlike AWT, JFC is not generally thread safe.For details, see the JavaSoft articles on this topic. My implementation used AWT.

Changes Required to Your Earlier Classes

You'll need to make some changes to your earlier classes:

First, we're changing who determines which player plays X and which plays O. Earlier, this was determined by passing a boolean as a parameter in the constructor for Board. Now, it's the server who decides, so we need to make some changes to accommodate that:

  • Change the constructor for the Board class to be:
        public Board(Game game, Image noughtImage, Image crossImage)

    That is, eliminate the playerIsCross parameter.

  • Add two new methods to the Board class:
        public void playsCross()
    
        public void playsNought()

    which set the value of a boolean instance variable to the appropriate state (you probably already have this boolean variable from earlier). These methods are used to set the state of the Board when the server tells the client which of X or O to play.

  • Add two more event types to the GameEvent class:
    • PLAYS_CROSS
    • PLAYS_NOUGHT
  • Add the two new methods
    public void playsCross()

    public void playsNought()

to the GameListener interface, to the GameAdapter class, to the GameEventMulticaster class, and to any other GameListeners you wrote for the earlier version.

  • Add the necessary code to the GameImpl class to dispatch these two new event types to their appropriate methods. (This is optional for the GameImpl class; I didn't find it necessary to do this.)

Second, you'll need to change the GUI from the earlier TicTacToe game to look show a single Board and text area (see the image above for how I did it).

Implementation

Here's how we're going to set things up:

  • Clearly, since the server will actually play the game on behalf of the clients, we'll move the GameImpl class to the server.
    So who will listen to GameEvents fired from GameImpl?
    Answer:Both instances of GamePlayer associated with that instance of GameImpl.
    (Remember that each pair of GamePlayers shares a single instance of GameImpl)

    Question: What requirements does this place on GamePlayer?

  • Since it's a GUI component, we keep the Board class in the client (although we now only have a single instance of it in each client, along with your other GUI code (appropriately modified, as stated above).
  • With GameImpl gone from the client (we moved it to the server, and removed it from the client), what will the Board instance listen to for GameEvents?
    Answer:We need to create a new class that represents the game on the client side, and so must implement Game. I called this class GameClient, and I found it necessary to make it capable of running in a thread.

    Question: What requirements does this place on GameClient?

So, here is the general strategy:

GUI code/Board
calls:
GameClient
which sends messages to:
GamePlayer
which calls:
GameImpl
Board
receives GameEvents from:
GameClient
which receives messages from:
GamePlayer
which receives GameEvents from:
GameImpl

In other words, we're replacing the previous interactions that Board (and other GUI code) had directly with GameImpl with a mechanism that looks the same as before to Board, and to GameImpl. However, the new mechanism uses a message-based protocol (TTTP) to communicate between the client and the server. (The mechanism is represented by the gray background cells, above.)

Note: The fact that this message-based protocol is being used should be totally transparent to the Board class (and to other GUI components), and also totally transparent to the GameImpl class!

This transparency is achieved by providing a GameClient class on the client side, and a GamePlayer class on the server side, and by having these two classes be totally responsible for the messages passed between them:

  • GameImpl fires GameEvents, which GamePlayer listens for.
  • GamePlayer responds to each GameEvent by sending its GameClient an appropriate message
  • GameClient receives the message, and fires the appropriate GameEvent, which Board listens for.
  • Board receives the GameEvent as if it had received it directly from the GameImpl. (It doesn't know the difference.)
  • Board (and other parts of the GUI), on detecting AWT events (such as a mouse or button click) invokes various methods in GameClient, which causes an appropriate message to be sent to its GamePlayer.
  • GamePlayer receives the message, and calls its GameImpl at the appropriate entry point.
  • GameImpl was invoked as if it was called directly from Board (It doesn't know the difference.)

The GameClient Class

Implementing GameClient is relatively easy, since it involves cutting and pasting, and then stripping out some code:

  • Make a copy of GameImpl.java into GameClient.java, and change every occurrence of the string "GameImpl" to "GameClient".
  • Change the GameClient constructor to:
    public GameClient(String host, int port)

    where the host and the port are used to connect to the server on that host, via that port.

  • Ensure that the two new events, PLAYS_CROSS and PLAYS_NOUGHT, are properly dispatched (use the original code as a guide).
  • Remove the isWon() and bestMove() methods.
  • Remove everything in the private data section, except m_gameListener.
  • For every method present in GameClient that is required because it implements the Game interface, remove the body of the method (the code between the open curly and close curly). Do not touch addGameEventListener or removeGameEventListener. You will have to retain some code (like return true;) to get GameClient to compile. (Note that the computedMove and status methods will not be used in GameClient, although they must be present to satisfy the requirements of implementing Game.)
  • Remove the body of the GameClient's run method.

This should give you a class that you can compile. However, it won't do anything yet, of course.

You will then have to add the necessary code to cause the GameClient to:

  • Connect to the server and send the CONNECT message (Where should this code go?)
  • Loop, waiting for messages from the server (Where should this code go?)
  • Cause appropriate messages to be sent to the server when certain actions are requested.

The GamePlayer Class

GamePlayer is the class that acts on behalf of a GameClient, on the server side.

Here, you'll have to do more original work. You can start by cloning the Board class, if you like (with appropriate name changes, of course), and stripping out unneeded code along similar lines to above. You won't need the Square class, nor any GUI-related stuff in GamePlayer.

  • GamePlayer should implement GameListener, and should be capable of running in a thread.
  • The GamePlayer constructor won't have the same parameters as Board's constructor, because GamePlayer doesn't deal with Images, and also the associated Game won't be in existence yet when the server creates the GamePlayer -- it gets created and associated later. You should add methods getGame() and setGame() so that the Game can be set later, and then retrieved.
  • Essentially, any code in Board that involves GUI changes should be converted to the appropriate sending of a message to the client.
  • It may just be simpler to start afresh, rather than doing major surgery on a copy of the Board class.

This should give you a class that will compile, but won't do anything, yet.

You will then have to add the necessary code to cause the GamePlayer to:

  • Be constructed with the appropriate parameters to allow an association with the opposing GamePlayer, and to create the associated GameImpl when both clients have sent their CONNECT requests.

Hint: I had the GameServer do the work to pair the GamePlayers up, and to pass in the appropriate parameters to their constructors.

  • Be fired up (by the GameServer) in a thread
  • Loop, waiting for messages from the associated client (Where should this code go?)
  • Cause appropriate messages to be sent to the client when certain events are encountered.

Hint: I suggest using DataInputStream and DataOutputStream to communicate between client and server.

Putting it All Together

The first warning I want to give you is:

Don't try to do everything at once!

To maintain your sanity, you should take small steps, get something working, and keep building on your successes until you're done. (There's nothing like a long string of failures -- or even one very long failure -- to increase your frustration level, raise your blood pressure, and generally make you want to hit something -- or someone!)

This may sound obvious, but it gets harder to do when you're working on both a client and a server, where you can't really test something until you've implemented the necessary support on both ends! So, don't try to add too much functionality at once, or you'll likely go crazy!

I suggest a development sequence like:

  1. Write the bare minimum server that will accept a connection from your bare minimum client, and get that working. This will probably involve getting both your GameClient and your GamePlayer classes working, in their appropriate threads, at a minimum level.
  2. Add the CONNECT -- CONNECTED functionality and get it working
  3. Add the START_GAME functionality and get it working.
  4. Try getting AUTOPLAY working. This is a bigger step, because it involves implementing support for the CROSS_IN_SQUARE, NOUGHT_IN_SQUARE, and RETURN_STATUS messages, and getting them to display the images in the client, and the appropriate text for the end of the game in the text area on the client.
  5. Add support for RESTART_GAME, and the resulting CLEAR_BOARD messages.
  6. Then add support for CROSS_MOVE and NOUGHT_MOVE; once Auto Play is working, this should be relatively simple to add.
  7. Finally, add support for GET_STATUS, which should be relatively trivial at this point.

Some more hints:

  • Using a debugger on both sides of a client/server link can be difficult. You may wish to use a debugger only on one side at a time, to avoid getting confused. It may be that your IDE can't support more than one executable running in its debugger at a time, anyway. You can always switch from running the client in a debugger to running the server in a debugger, as required.
  • Make a habit of using System.out.println(...) calls in appropriate parts of your code. They can be very helpful, and give you faster feedback than often is possible in a debugger.  This is especially beneficial when you're dealing with multi-threaded programs, where using the debugger is often not very practical.
  • I ran my server process from one MS/DOS window on my Windows NT system, one client from another MS/DOS window, and the second client from my Visual Cafe environment, with the debugger. (All on the same machine.) That, together with useful output on the MS/DOS windows from System.out.println() calls, worked well.

Results

At the end of this assignment, you should have a server which supports connections from pairs of clients. Each client should be independent of all other clients (other than interacting with the client's opponent). Each pair of clients should be playing a game independent of all other games.

I'd like to see some simple trace output (those System.out.println() statements) to show which clients connected, were paired up, and the various moves that were made.

I'd like to see that your server can support more than just one pair of clients playing a game.

 
The page was last updated February 19, 2008