25.9. (Optional) Case Studies: Distributed TicTacToe Games |
In §16.7, "Case Study: TicTacToe," you developed an applet for the TicTacToe game that enables two players to play on the same machine. In this section, you will learn how to develop a distributed TicTacToe game using multithreads and networking with socket streams. A distributed TicTacToe game enables users to play on different machines from anywhere on the Internet.
You need to develop a server for multiple clients . The server creates a server socket, and accepts connections from every two players to form a session. Each session is a thread that communicates with the two players and determines the status of the game. The server can establish any number of sessions, as shown in Figure 25.15.
For each session, the first client connecting to the server is identified as Player 1 with token 'X', and the second client connecting to the server is identified as Player 2 with token 'O'. The server notifies the players of their respective tokens. Once two clients are connected to it, the server starts a thread to facilitate the game between the two players by performing the steps repeatedly, as shown in Figure 25.16.
The server does not have to be a graphical component, but creating it as a frame in which game information can be viewed is user -friendly. You can create a scroll pane to hold a text area in the frame and display game information in the text area. The server creates a thread to handle a game session when two players are connected to the server.
The client is responsible for interacting with the players. It creates a user interface with nine cells , and displays the game title and status to the players in the labels. The client class is very similar to the TicTacToe class presented in §16.7, "Case Study: TicTacToe." However, the client in this example does not determine the game status (win or draw), it simply passes the moves to the server and receives the game status from the server.
Based on the foregoing analysis, you can create the following classes:
TicTacToeServer serves all the clients in Listing 25.13.
HandleASession facilitates the game for two players in Listing 25.13. It is in the same file with TicTacToeServer.java.
TicTacToeClient models a player in Listing 25.14.
Cell models a cell in the game in Listing 25.14. It is an inner class in TicTacToeClient .
TicTacToeConstants is an interface that defines the constants shared by all the classes in the example in Listing 25.12.
The relationships of these classes are shown in Figure 25.17.
1 public interface TicTacToeConstants { 2 public static int PLAYER1 = 1 ; // Indicate player 1 3 public static int PLAYER2 = 2 ; // Indicate player 2 4 public static int PLAYER1_WON = 1 ; // Indicate player 1 won 5 public static int PLAYER2_WON = 2 ; // Indicate player 2 won 6 public static int DRAW = 3 ; // Indicate a draw 7 public static int CONTINUE = 4 ; // Indicate to continue 8 } |
1 import java.io.*; 2 import java.net.*; 3 import javax.swing.*; 4 import java.awt.*; 5 import java.util.Date; 6 7 public class TicTacToeServer extends JFrame 8 implements TicTacToeConstants { 9 public static void main(String[] args) { 10 TicTacToeServer frame = new TicTacToeServer(); 11 } 12 13 public TicTacToeServer() { 14 JTextArea jtaLog = new JTextArea(); 15 16 // Create a scroll pane to hold text area 17 JScrollPane scrollPane = new JScrollPane(jtaLog); 18 19 // Add the scroll pane to the frame 20 add(scrollPane, BorderLayout.CENTER); 21 22 setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 23 setSize( 300 , 300 ); 24 setTitle( "TicTacToeServer" ); 25 setVisible( true ); 26 27 try { 28 // Create a server socket 29 ServerSocket serverSocket = new ServerSocket( 8000 ); 30 jtaLog.append( new Date() + 31 ": Server started at socket 8000\n" ); 32 33 // Number a session 34 int sessionNo = 1 ; 35 36 // Ready to create a session for every two players 37 while ( true ) { 38 jtaLog.append( new Date() + 39 ": Wait for players to join session " + sessionNo + '\n' ); 40 41 // Connect to player 1 42 Socket player1 = serverSocket.accept(); 43 44 jtaLog.append( new Date() + ": Player 1 joined session " + 45 sessionNo + '\n' ); 46 jtaLog.append( "Player 1's IP address" + 47 player1.getInetAddress().getHostAddress() + '\n' ); 48 49 // Notify that the player is Player 1 50 new DataOutputStream( 51 player1.getOutputStream()).writeInt(PLAYER1); 52 53 // Connect to player 2 54 Socket player2 = serverSocket.accept(); 55 56 jtaLog.append( new Date() + 57 ": Player 2 joined session " + sessionNo + '\n' ); 58 jtaLog.append( "Player 2's IP address" + 59 player2.getInetAddress().getHostAddress() + '\n' ); 60 61 // Notify that the player is Player 2 62 new DataOutputStream( 63 player2.getOutputStream()).writeInt(PLAYER2); 64 65 // Display this session and increment session number 66 jtaLog.append( new Date() + ": Start a thread for session " + 67 sessionNo++ + '\n' ); 68 69 // Create a new thread for this session of two players 70 HandleASession task = new HandleASession(player1, player2); 71 72 // Start the new thread 73 new Thread(task).start(); 74 } 75 } 76 catch (IOException ex) { 78 System.err.println(ex); 79 } 80 } 81 } 82 83 // Define the thread class for handling a new session for two players 84 class HandleASession implements Runnable, TicTacToeConstants { 85 private Socket player1; 86 private Socket player2; 87 88 // Create and initialize cells 89 private char [][] cell = new char [ 3 ][ 3 ]; 90 91 private DataInputStream fromPlayer1; 92 private DataOutputStream toPlayer1; 93 private DataInputStream fromPlayer2; 94 private DataOutputStream toPlayer2; 95 96 // Continue to play 97 private boolean continueToPlay = true ; 98 99 /** Construct a thread */ 100 public HandleASession(Socket player1, Socket player2) { 101 this .player1 = player1; 102 this .player2 = player2; 103 104 // Initialize cells 105 for ( int i = ; i < 3 ; i++) 106 for ( int j = ; j < 3 ; j++) 107 cell[i][j] = ' ' ; 108 } 109 110 /** Implement the run() method for the thread */ 111 public void run() { 112 try { 113 // Create data input and output streams 114 DataInputStream fromPlayer1 = new DataInputStream( 115 player1.getInputStream()); 116 DataOutputStream toPlayer1 = new DataOutputStream( 117 player1.getOutputStream()); 118 DataInputStream fromPlayer2 = new DataInputStream( 119 player2.getInputStream()); 120 DataOutputStream toPlayer2 = new DataOutputStream( 121 player2.getOutputStream()); 122 123 // Write anything to notify player 1 to start 124 // This is just to let player 1 know to start 125 toPlayer1.writeInt( 1 ); 126 127 // Continuously serve the players and determine and report 128 // the game status to the players 129 while ( true ) { 130 // Receive a move from player 1 131 int row = fromPlayer1.readInt(); 132 int column = fromPlayer1.readInt(); 133 cell[row][column] = 'X' ; 134 135 // Check if Player 1 wins 136 if (isWon( 'X' )) { 137 toPlayer1.writeInt(PLAYER1_WON); 138 toPlayer2.writeInt(PLAYER1_WON); 139 sendMove(toPlayer2, row, column); 140 break ; // Break the loop 141 } 142 else if (isFull()) { // Check if all cells are filled 143 toPlayer1.writeInt(DRAW); 144 toPlayer2.writeInt(DRAW); 145 sendMove(toPlayer2, row, column); 146 break ; 147 } 148 else { 149 // Notify player 2 to take the turn 150 toPlayer2.writeInt(CONTINUE); 151 152 // Send player 1's selected row and column to player 2 153 sendMove(toPlayer2, row, column); 154 } 155 156 // Receive a move from Player 2 157 row = fromPlayer2.readInt(); 158 column = fromPlayer2.readInt(); 159 cell[row][column] = 'O' ; 160 161 // Check if Player 2 wins 162 if (isWon( 'O' )) { 163 toPlayer1.writeInt(PLAYER2_WON); 164 toPlayer2.writeInt(PLAYER2_WON); 165 sendMove(toPlayer1, row, column); 166 break ; 167 } 168 else { 169 // Notify player 1 to take the turn 170 toPlayer1.writeInt(CONTINUE); 171 172 // Send player 2's selected row and column to player 1 173 sendMove(toPlayer1, row, column); 174 } 175 } 176 } 177 catch (IOException ex) { 178 System.err.println(ex); 179 } 180 } 181 182 /** Send the move to other player */ 183 private void sendMove(DataOutputStream out, int row, int column) 184 throws IOException { 185 out.writeInt(row); // Send row index 186 out.writeInt(column); // Send column index 187 } 188 189 /** Determine if the cells are all occupied */ 190 private boolean isFull() { 191 for ( int i = ; i < 3 ; i++) 192 for ( int j = ; j < 3 ; j++) 193 if (cell[i][j] == ' ' ) 194 return false ; // At least one cell is not filled 195 196 // All cells are filled 197 return true ; 198 } 199 200 /** Determine if the player with the specified token wins */ 201 private boolean isWon( char token) { 202 // Check all rows 203 for ( int i = ; i < 3 ; i++) 204 if ((cell[i][ ] == token) 205 && (cell[i][ 1 ] == token) 206 && (cell[i][ 2 ] == token)) { 207 return true ; 208 } 209 210 /** Check all columns */ 211 for ( int j = ; j < 3 ; j++) 212 if ((cell[ ][j] == token) 213 && (cell[ 1 ][j] == token) 214 && (cell[ 2 ][j] == token)) { 215 return true ; 216 } 217 218 /** Check major diagonal */ 219 if ((cell[ ][ ] == token) 220 && (cell[ 1 ][ 1 ] == token) 221 && (cell[ 2 ][ 2 ] == token)) { 222 return true ; 223 } 224 225 /** Check subdiagonal */ 226 if ((cell[ ][ 2 ] == token) 227 && (cell[ 1 ][ 1 ] == token) 228 && (cell[ 2 ][ ] == token)) { 229 return true ; 230 } 231 232 /** All checked, but no winner */ 233 return false ; 234 } 235 } |
1 import java.awt.*; 2 import java.awt.event.*; 3 import javax.swing.*; 4 import javax.swing.border.LineBorder; 5 import java.io.*; 6 import java.net.*; 7 8 public class TicTacToeClient extends JApplet 9 implements Runnable, TicTacToeConstants { 10 // Indicate whether the player has the turn 11 private boolean myTurn = false ; 12 13 // Indicate the token for the player 14 private char myToken = ' ' ; 15 16 // Indicate the token for the other player 17 private char otherToken = ' ' ; 18 19 // Create and initialize cells 20 private Cell[][] cell = new Cell[ 3 ][ 3 ]; 21 22 // Create and initialize a title label 23 private JLabel jlblTitle = new JLabel(); 24 25 // Create and initialize a status label 26 private JLabel jlblStatus = new JLabel(); 27 28 // Indicate selected row and column by the current move 29 private int rowSelected; 30 private int columnSelected; 31 32 // Input and output streams from/to server 33 private DataInputStream fromServer; 34 private DataOutputStream toServer; 35 36 // Continue to play? 37 private boolean continueToPlay = true ; 38 39 // Wait for the player to mark a cell 40 private boolean waiting = true ; 41 42 // Indicate if it runs as application 43 private boolean isStandAlone = false ; 44 45 // Host name or ip 46 private String host = "localhost" ; 47 48 /** Initialize UI */ 49 public void init() { 50 // Panel p to hold cells 51 JPanel p = new JPanel(); 52 p.setLayout( new GridLayout( 3 , 3 , , )); 53 for ( int i = ; i < 3 ; i++) 54 for ( int j = ; j < 3 ; j++) 55 p.add(cell[i][j] = new Cell(i, j)); 56 57 // Set properties for labels and borders for labels and panel 58 p.setBorder( new LineBorder(Color.black, 1 )); 59 jlblTitle.setHorizontalAlignment(JLabel.CENTER); 60 jlblTitle.setFont( new Font( "SansSerif" , Font.BOLD, 16 )); 61 jlblTitle.setBorder( new LineBorder(Color.black, 1 )); 62 jlblStatus.setBorder( new LineBorder(Color.black, 1 )); 63 64 // Place the panel and the labels to the applet 65 add(jlblTitle, BorderLayout.NORTH); 66 add(p, BorderLayout.CENTER); 67 add(jlblStatus, BorderLayout.SOUTH); 68 69 // Connect to the server 70 connectToServer(); 71 } 72 73 private void connectToServer() { 74 try { 75 // Create a socket to connect to the server 76 Socket socket; 77 if (isStandAlone) 78 socket = new Socket(host, 8000 ); 79 else 80 socket = new Socket(getCodeBase().getHost(), 8000 ); 81 82 // Create an input stream to receive data from the server 83 fromServer = new DataInputStream(socket.getInputStream()); 84 85 // Create an output stream to send data to the server 86 toServer = new DataOutputStream(socket.getOutputStream()); 87 } 88 catch (Exception ex) { 89 System.err.println(ex); 90 } 91 92 // Control the game on a separate thread 93 Thread thread = new Thread( this ); 94 thread.start(); 95 } 96 97 public void run() { 98 try { 99 // Get notification from the server 100 int player = fromServer.readInt(); 101 102 // Am I player 1 or 2? 103 if (player == PLAYER1) { 104 myToken = 'X' ; 105 otherToken = 'O' ; 106 jlblTitle.setText( "Player 1 with token 'X'" ); 107 jlblStatus.setText( "Waiting for player 2 to join" ); 108 109 // Receive startup notification from the server 110 fromServer.readInt(); // Whatever read is ignored 111 112 // The other player has joined 113 jlblStatus.setText( "Player 2 has joined. I start first" ); 114 115 // It is my turn 116 myTurn = true ; 117 } 118 else if (player == PLAYER2) { 119 myToken = 'O' ; 120 otherToken = 'X' ; 121 jlblTitle.setText( "Player 2 with token 'O'" ); 122 jlblStatus.setText( "Waiting for player 1 to move" ); 123 } 124 125 // Continue to play 126 while (continueToPlay) { 127 if (player == PLAYER1) { 128 waitForPlayerAction(); // Wait for player 1 to move 129 sendMove(); // Send the move to the server 130 receiveInfoFromServer(); // Receive info from the server 131 } 132 else if (player == PLAYER2) { 133 receiveInfoFromServer(); // Receive info from the server 134 waitForPlayerAction(); // Wait for player 2 to move 135 sendMove() ; // Send player 2's move to the server 136 } 137 } 138 } 139 catch (Exception ex) { 140 } 141 } 142 143 /** Wait for the player to mark a cell */ 144 Private void waitForPlayerAction() throws InterruptedException { 145 while (waiting) { 146 Thread.sleep( 100 ); 147 } 148 149 waiting = true ; 150 } 151 152 /** Send this player's move to the server */ 153 private void sendMove() throws IOException { 154 toServer.writeInt(rowSelected); // Send the selected row 155 toServer.writeInt(columnSelected); // Send the selected column 156 } 157 158 /** Receive info from the server */ 159 private void receiveInfoFromServer() throws IOException { 160 // Receive game status 161 int status = fromServer.readInt(); 162 163 if (status == PLAYER1_WON) { 164 // Player 1 won, stop playing 165 continueToPlay = false ; 166 if (myToken == 'X' ) { 167 jlblStatus.setText( "I won! (X)" ); 168 } 169 else if (myToken == 'O' ) { 170 jlblStatus.setText( "Player 1 (X) has won!" ); 171 receiveMove(); 172 } 173 } 174 else if (status == PLAYER2_WON) { 175 // Player 2 won, stop playing 176 continueToPlay = false ; 177 if (myToken == 'O' ) { 178 jlblStatus.setText( "I won! (O)" ); 179 } 180 else if (myToken == 'X' ) { 181 jlblStatus.setText( "Player 2 (O) has won!" ); 182 receiveMove(); 183 } 184 } 185 else if (status == DRAW) { 186 // No winner, game is over 187 continueToPlay = false ; 188 jlblStatus.setText( "Game is over, no winner!" ); 189 190 if (myToken == 'O' ) { 191 receiveMove(); 192 } 193 } 194 else { 195 receiveMove(); 196 jlblStatus.setText( "My turn" ); 197 myTurn = true ; // It is my turn 198 } 199 } 200 201 private void receiveMove() throws IOException { 202 // Get the other player's move 203 int row = fromServer.readInt(); 204 int column = fromServer.readInt(); 205 cell[row][column].setToken(otherToken); 206 } 207 208 // An inner class for a cell 209 public class Cell extends JPanel { 210 // Indicate the row and column of this cell in the board 211 private int row; 212 private int column; 213 214 // Token used for this cell 215 private char token = ' ' ; 216 217 public Cell( int row, int column) { 218 this .row = row; 219 this .column = column; 220 setBorder( new LineBorder(Color.black, 1 )); // Set cell's border 221 addMouseListener( new ClickListener()); // Register listener 222 } 223 224 /** Return token */ 225 public char getToken() { 226 return token; 227 } 228 229 /** Set a new token */ 230 public void setToken( char c) { 231 token = c; 232 repaint(); 233 } 234 235 /** Paint the cell */ 236 protected void paintComponent(Graphics g) { 237 super .paintComponent(g); 238 239 if (token == 'X' ) { 240 g.drawLine( 10 , 10 , getWidth() - 10 , getHeight() - 10 ); 241 g.drawLine(getWidth() - 10 , 10 , 10 , getHeight() - 10 ); 242 } 243 else if (token == 'O' ) { 244 g.drawOval( 10 , 10 , getWidth() - 20 , getHeight() - 20 ); 245 } 246 } 247 248 /** Handle mouse click on a cell */ 249 private class ClickListener extends MouseAdapter { 250 public void mouseClicked(MouseEvent e) { 251 // If cell is not occupied and the player has the turn 252 if ((token == ' ' ) && myTurn) { 253 setToken(myToken); // Set the player's token in the cell 254 myTurn = false ; 255 rowSelected = row; 256 columnSelected = column; 257 jlblStatus.setText( "Waiting for the other player to move" ); 258 waiting = false ; // Just completed a successful move 259 } 260 } 261 } 262 } 263 } |
The server can serve any number of sessions. Each session takes care of two players. The client can be a Java applet or a Java application. To run a client as a Java applet from a Web browser, the server must run from a Web server. Figures 25.18 and 25.19 show sample runs of the server and the clients.
The TicTacToeConstants interface defines the constants shared by all the classes in the project. Each class that uses the constants needs to implement the interface. Centrally defining constants in an interface is a common practice in Java. For example, all the constants shared by Swing classes are defined in java.swing.SwingConstants .
Once a session is established, the server receives moves from the players in alternation . Upon receiving a move from a player, the server determines the status of the game. If the game is not finished, the server sends the status ( CONTINUE ) and the player's move to the other player. If the game is won or drawn, the server sends the status ( PLAYER1_WON , PLAYER2_WON , or DRAW ) to both players.
The implementation of Java network programs at the socket level is tightly synchronized. An operation to send data from one machine requires an operation to receive data from the other machine. As shown in this example, the server and the client are tightly synchronized to send or receive data.