From the many examples in this and earlier chapters you have learned about objects, classes, arrays, class inheritance, GUI, and event-driven programming. Now it is time to put what you have learned to work in developing comprehensive projects. In this section, we will develop a JavaFX program with which to play the popular game of tic-tac-toe.
Two players take turns marking an available cell in a 3 * 3 grid with their respective tokens (either X or O). When one player has placed three tokens in a horizontal, vertical, or diagonal row on the grid, the game is over and that player has won. A draw (no winner) occurs when all the cells on the grid have been filled with tokens and neither player has achieved a win. Figure below shows the representative sample runs of the game.
All the examples you have seen so far show simple behaviors that are easy to model with classes. The behavior of the tic-tac-toe game is somewhat more complex. To define classes that model the behavior, you need to study and understand the game.
Assume that all the cells are initially empty, and that the first player takes the X token and the second player the O token. To mark a cell, the player points the mouse to the cell and clicks it. If the cell is empty, the token (X or O) is displayed. If the cell is already filled, the player’s action is ignored.
From the preceding description, it is obvious that a cell is a GUI object that handles the mouse-click event and displays tokens. There are many choices for this object. We will use a pane to model a cell and to display a token (X or O). How do you know the state of the cell (empty, X, or O)? You use a property named token of the char type in the Cell class. The Cell class is responsible for drawing the token when an empty cell is clicked, so you need to write the code for listening to the mouse-clicked action and for painting the shapes for tokens X and O. The Cell class can be defined as shown in Figure below.
The tic-tac-toe board consists of nine cells, created using new Cell[3][3]. To determine which player’s turn it is, you can introduce a variable named whoseTurn of the char type. whoseTurn is initially ‘X’, then changes to ‘O’, and subsequently changes between ‘X’ and ‘O’ whenever a new cell is occupied. When the game is over, set whoseTurn to ‘ ‘.
How do you know whether the game is over, whether there is a winner, and who the winner, if any? You can define a method named isWon(char token) to check whether a specified token has won and a method named isFull() to check whether all the cells are occupied.
Clearly, two classes emerge from the foregoing analysis. One is the Cell class, which handles operations for a single cell; the other is the TicTacToe class, which plays the whole game and deals with all the cells. The relationship between these two classes is shown in Figure below.
Since the Cell class is only to support the TicTacToe class, it can be defined as an inner class in TicTacToe. The complete program is given in the code below.
package application;
import javafx.application.Application;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.scene.shape.Ellipse; public class TicTacToe extends Application { // Indicate which player has a turn, initially it is the X player private char whoseTurn = 'X'; // Create and initialize cell private Cell[][] cell = new Cell[3][3]; // Create and initialize a status label private Label lblStatus = new Label("X's turn to play"); @Override // Override the start method in the Application class public void start(Stage primaryStage) { // Pane to hold cell GridPane pane = new GridPane(); for(int i = 0; i < 3; i++) for(int j = 0; j < 3; j++) pane.add(cell[i][j] = new Cell(), i, j); BorderPane borderPane = new BorderPane(); borderPane.setCenter(pane); borderPane.setBottom(lblStatus); // Create a scene and place it in the stage Scene scene = new Scene(borderPane, 450, 170); primaryStage.setTitle("TicTacToe"); // Set the stage title primaryStage.setScene(scene); // Place the scene in the stage primaryStage.show(); // Display the stage } public static void main(String[] args) { Application.launch(args); } /** Determine if the cell are all occupied */ public boolean isFull() { for(int i = 0; i < 3; i++) for(int j = 0; j < 3; j++) if(cell[i][j].getToken() == ' ') return false; return true; } /** Determine if the player with the specified token wins */ public boolean isWon(char token) { for(int i = 0; i < 3; i++) if(cell[i][0].getToken() == token && cell[i][1].getToken() == token && cell[i][2].getToken() == token) { return true; } for(int j = 0; j < 3; j++) if(cell[0][j].getToken() == token && cell[1][j].getToken() == token && cell[2][j].getToken() == token) { return true; } if(cell[0][0].getToken() == token && cell[1][1].getToken() == token && cell[2][2].getToken() == token) { return true; } if(cell[0][2].getToken() == token && cell[1][1].getToken() == token && cell[2][0].getToken() == token) { return true; } return false; } // An inner class for a cell public class Cell extends Pane{ // Token used for this cell private char token = ' '; public Cell() { setStyle("-fx-border-color: black"); this.setPrefSize(2000, 2000); this.setOnMouseClicked(e -> handleMouseClick()); } /** Return token */ public char getToken() { return token; } /** Set a new token */ public void setToken(char c) { token = c; if(token == 'X') { Line line1 = new Line(10, 10, this.getWidth() - 10, this.getHeight() - 10); line1.endXProperty().bind(this.widthProperty().subtract(10)); line1.endYProperty().bind(this.heightProperty().subtract(10)); Line line2 = new Line(10, this.getHeight() - 10, this.getWidth() - 10, 10); line2.startYProperty().bind(this.heightProperty().subtract(10)); line2.endXProperty().bind(this.widthProperty().subtract(10)); // Add the lines to the pane this.getChildren().addAll(line1, line2); } else if(token == 'O') { Ellipse ellipse = new Ellipse(this.getWidth() / 2, this.getHeight() / 2, this.getWidth() / 2 - 10, this.getHeight() / 2 - 10); ellipse.centerXProperty().bind(this.widthProperty().divide(2)); ellipse.centerYProperty().bind(this.heightProperty().divide(2)); ellipse.radiusXProperty().bind(this.widthProperty().divide(2).subtract(10)); ellipse.radiusYProperty().bind(this.heightProperty().divide(2).subtract(10)); ellipse.setStroke(Color.BLACK); ellipse.setFill(Color.WHITE); getChildren().add(ellipse); // Add the ellipse to the pane } } /** Handle a mouse click event */ private void handleMouseClick() { // If cell is empty and game is not over if(token == ' ' && whoseTurn != ' ') { setToken(whoseTurn); // Set token in the cell // Check game status if(isWon(whoseTurn)) { lblStatus.setText(whoseTurn + " won! The game is over"); whoseTurn = ' '; // Game is over } else if(isFull()) { lblStatus.setText("Draw! he game is over"); whoseTurn = ' '; // Game is over } else { // Change the turn whoseTurn = (whoseTurn == 'X') ? 'O' : 'X'; // Display whose turn lblStatus.setText(whoseTurn + "'s turn"); } } } }
}
The TicTacToe class initializes the user interface with nine cells placed in a grid pane (lines 26–29). A label named lblStatus is used to show the status of the game (line 21). The variable whoseTurn (line 15) is used to track the next type of token to be placed in a cell. The methods isFull (lines 47–54) and isWon (lines 57–77) are for checking the status of the game.
Since Cell is an inner class in TicTacToe, the variable (whoseTurn) and methods (isFull and isWon) defined in TicTacToe can be referenced from the Cell class. The inner class makes programs simple and concise. If Cell were not defined as an inner class of TicTacToe, you would have to pass an object of TicTacToe to Cell in order for the variables and methods in TicTacToe to be used in Cell.
The listener for the mouse-click action is registered for the cell (line 87). If an empty cell is clicked and the game is not over, a token is set in the cell (line 126). If the game is over, whoseTurn is set to ‘ ‘ (lines 132, 136). Otherwise, whoseTurn is alternated to a new turn (line 140).