package netgame.tictactoe; import javafx.application.Platform; import javafx.stage.Stage; import javafx.scene.Scene; import javafx.scene.control.Label; import javafx.scene.control.Alert; import javafx.scene.layout.BorderPane; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.paint.Color; import javafx.scene.text.Font; import javafx.scene.text.FontWeight; import javafx.geometry.Pos; import javafx.geometry.Insets; import java.io.IOException; import netgame.common.*; /** * This window represents one player in a two-player networked * game of TicTacToe. The window is meant to be created by * the program netgame.tictactoe.Main. */ public class TicTacToeWindow extends Stage { /** * The state of the game. This state is a copy of the official * state, which is stored on the server. When the state changes, * the state is sent as a message to this window. (It is actually * sent to the TicTacToeClient object that represents the connection * to the server.) When that happens, the state that was received * in the message replaces the value of this variable, and the * board and UI is updated to reflect the changed state. This * is done in the newState() method, which is called by the * TicTacToeClient object. */ private TicTacToeGameState state; private volatile boolean connecting; // Set to true until connection is established to hub. private Canvas board; // A panel that displays the board. The user // makes moves by clicking on this panel. private Label message; // Displays messages to the user about the status of the game. private int myID; // The ID number that identifies the player using this window. private TicTacToeClient connection; // The Client object for sending and receiving // network messages. /** * Creates, configures, and opens the window. Also creates a thread that attempts to * open a connection to the server. * @param hostName the name or IP address of the host where the server is running. * @param serverPortNumber the port number on the server computer where * the Hub is listening for connections. */ public TicTacToeWindow(String hostName, int serverPortNumber) { connecting = true; message = new Label("Waiting for connection."); message.setFont( Font.font("Arial", FontWeight.BOLD, 16) ); message.setPadding( new Insets(10)); board = new Canvas(400,400); board.setOnMousePressed( e -> doMouseClick(e.getX(), e.getY()) ); drawBoard(); BorderPane content = new BorderPane(board); content.setBottom(message); BorderPane.setAlignment(message, Pos.CENTER); setScene( new Scene(content) ); setTitle("Net TicTacToe"); setResizable(false); setOnHidden( evt -> { // When the user clicks the window's close box, this listener will // send a disconnect message to the Hub and will end the program. // The other player will then be notified that this player has disconnected. if (connection != null) { connection.disconnect(); // Send a disconnect message to the hub. try { Thread.sleep(333); // Wait one-third second to allow the message to be sent. } catch (InterruptedException e) { } } System.exit(0); // In case connecting thread is still around, make sure it dies. }); setX(100 + 50*Math.random()); setY(100 + 50*Math.random()); show(); Thread connector = new Thread( () -> connect(hostName, serverPortNumber) ); connector.start(); } // end constructor /** * This class defines the client object that handles communication with the Hub. */ private class TicTacToeClient extends Client { /** * Connect to the hub at a specified host name and port number. */ public TicTacToeClient(String hubHostName,int hubPort) throws IOException { super(hubHostName, hubPort); } /** * Responds to a message received from the Hub. The only messages that * are supported are TicTacToeGameState objects. When one is received, * the newState() method in the TicTacToeWindow class is called. That * method is called using Platform.runLater() so that it will run on * the JavaFX application thread. */ protected void messageReceived(Object message) { if (message instanceof TicTacToeGameState) { Platform.runLater( () -> newState( (TicTacToeGameState)message ) ); } } /** * If a shutdown message is received from the Hub, the user is notified * and the program ends. */ protected void serverShutdown(String message) { Platform.runLater( () -> { Alert alert = new Alert(Alert.AlertType.INFORMATION, "Your opponent has disconnected.\nThe game is ended."); TicTacToeWindow.this.hide(); alert.showAndWait(); System.exit(0); }); } } // end nested class TicTacToeClient /** * When the window is created, this method is called in a separate * thread to make the connection to the server. If an error * occurs, the program is terminated. */ private void connect(String hostName, int serverPortNumber) { TicTacToeClient c; int id; try { c = new TicTacToeClient(hostName, serverPortNumber); id = c.getID(); Platform.runLater( () -> { connecting = false; connection = c; myID = id; drawBoard(); message.setText("Waiting for two players to connect."); }); } catch (Exception e) { Platform.runLater( () -> { Alert alert = new Alert(Alert.AlertType.INFORMATION, "Sorry, could not connect to\n" + hostName + " on port " + serverPortNumber + "\nShutting down."); alert.showAndWait(); System.exit(0); }); } } private void drawBoard() { GraphicsContext g = board.getGraphicsContext2D(); g.setFill(Color.WHITE); g.fillRect(0,0,board.getWidth(),board.getHeight()); g.setStroke(Color.BLACK); g.setLineWidth(6); g.strokeRect(0,0,board.getWidth(),board.getHeight()); g.setFill(Color.BLACK); if (connecting) { g.fillText("Connecting...", 20, 35); return; } if (state == null || state.board == null) { g.fillText("Starting up.", 20, 35); return; } g.setLineWidth(10); g.setStroke(Color.BLACK); g.strokeLine(150,50,150,350); g.strokeLine(250,50,250,350); g.strokeLine(50,150,350,150); g.strokeLine(50,250,350,250); for (int row = 0; row < 3; row++) { for (int col = 0; col < 3; col++) { if (state.board[row][col] == 'X') { g.setStroke(Color.RED); g.strokeLine(70+col*100, 70+row*100, 130+col*100, 130+row*100); g.strokeLine(70+col*100, 130+row*100, 130+col*100, 70+row*100); } else if (state.board[row][col] == 'O') { g.setStroke(Color.BLUE); g.strokeOval(65+col*100,65+row*100, 70, 70); } } } } /** * This method is called when the user clicks the tictactoe board. If the * click represents a legal move at a legal time, then a message is sent * to the Hub to inform it of the move. The Hub will change the game * state and send the new state to both players. It is very important that * the game clients do not change the game state directly, since the * "official" game state is maintained by the Hub. Doing things this * way guarantees that both players see the same board. */ private void doMouseClick(double x, double y) { if (state == null || state.board == null) return; if (!state.gameInProgress) { // After a game ends, the winning player -- or either // player in the event of a tie -- can start a new // game by clicking the board. When this happens, // the String "newgame" is sent as a message to Hub. if (state.gameEndedInTie || myID == state.winner) connection.send("newgame"); // Start a new game. return; } if (myID !=state.currentPlayer) { return; // it's not this player's turn. } int row = (int)((y-50) / 100); int col = (int)((x-50) / 100); if (row >= 0 && row < 3 && col >= 0 && col < 3 && state.board[row][col] == ' ' ) { // User has clicked an empty square. Send the move to the Hub // as an array of two ints containing the row number and column // number of the square where the user clicked. connection.send( new int[] { row, col } ); } } /** * This method is called when a new game state is received from the hub. * It stores the new state in the instance variable that represents the * game state and updates the user interface to reflect the state. * Note that this method is called on the application thread (using * Platform.runLater()) to avoid synchronization problems. */ private void newState(TicTacToeGameState state) { if ( state.playerDisconnected ) { Alert alert = new Alert(Alert.AlertType.INFORMATION, "Your opponent has disconnected.\nThe game is ended."); alert.showAndWait(); System.exit(0); } this.state = state; drawBoard(); if ( state.board == null ) { return; // haven't started yet -- waiting for 2nd player } else if ( ! state.gameInProgress ) { setTitle("Game Over"); if ( state.gameEndedInTie ) message.setText("Game ended in tie. Click to start again."); else if (myID == state.winner) message.setText("You won! Click to start a new game."); else message.setText("You lost. Waiting for new game..."); } else { if (myID == state.playerPlayingX) setTitle("You are playing X's"); else setTitle("You are playing O's"); if (myID == state.currentPlayer) message.setText("Your move"); else message.setText("Waiting for opponent's move"); } } } // end TicTacToeWindow