Welcome to Chapter 7! In this chapter, we embark on building a classic game: Tic-Tac-Toe. This project moves beyond simple command-line utilities and introduces fundamental concepts of Object-Oriented Programming (OOP) in a practical context. We’ll design and implement the core game logic, focusing on how to represent the game board, players, and the overall game state using well-defined Java classes.

The importance of this step lies in applying OOP principles to create modular, maintainable, and extensible code. By encapsulating responsibilities within different objects, we make the codebase easier to understand, debug, and expand upon later (e.g., adding an AI opponent or a graphical user interface). This chapter will lay a solid foundation for more complex applications.

As a prerequisite, ensure you have completed the setup from Chapter 1, including having Java 25 and Maven installed and configured. While previous project chapters focused on single-file applications, this chapter will guide you through structuring a multi-class Java project. By the end of this chapter, you will have a fully functional, command-line-based Tic-Tac-Toe game that correctly handles player turns, validates moves, and determines win or draw conditions.

Planning & Design

Building a game, even one as simple as Tic-Tac-Toe, benefits greatly from upfront design. We’ll follow an Object-Oriented approach to model the different entities and their interactions.

Component Architecture

We will define several classes, each with a specific responsibility, to manage the game logic:

  1. GameStatus (Enum): A simple enumeration to represent the possible states of the game (e.g., IN_PROGRESS, X_WINS, O_WINS, DRAW). Using an enum provides type safety and makes the code more readable.
  2. GameException (Custom Exception): A custom runtime exception to handle invalid game operations, such as trying to place a mark on an occupied cell or an out-of-bounds move.
  3. Board Class:
    • Manages the 3x3 grid of the Tic-Tac-Toe board.
    • Responsible for placing player marks, checking if cells are empty, determining if the board is full, and printing the current state of the board.
    • It should not contain game-winning logic; its role is purely board management.
  4. Player Class:
    • Represents a player in the game.
    • Holds the player’s mark (e.g., ‘X’ or ‘O’) and potentially their name.
    • Simple data holder for player information.
  5. Game Class:
    • Orchestrates the overall game flow.
    • Manages the Board and Player objects.
    • Keeps track of the current player and the GameStatus.
    • Contains the core game logic: making moves, checking for win conditions (rows, columns, diagonals), and determining a draw.
    • It’s the central hub for game state management.
  6. TicTacToeGame Class (Main Entry Point):
    • Contains the main method.
    • Handles user interaction (input/output) and drives the game loop by interacting with the Game class.

File Structure

Our project will use Maven, so the standard directory structure will be followed:

tic-tac-toe-game/
├── pom.xml
└── src/
    └── main/
        └── java/
            └── com/
                └── example/
                    └── tictactoe/
                        ├── Board.java
                        ├── Game.java
                        ├── GameException.java
                        ├── GameStatus.java
                        ├── Player.java
                        └── TicTacToeGame.java

Step-by-Step Implementation

We’ll build our Tic-Tac-Toe game incrementally, starting with the project setup and then adding each class one by one.

a) Setup/Configuration

First, let’s create a new Maven project for our Tic-Tac-Toe game.

  1. Create Project Directory: Open your terminal or command prompt and navigate to your workspace.

    mkdir tic-tac-toe-game
    cd tic-tac-toe-game
    
  2. Initialize Maven Project: Use Maven to create a basic project structure.

    mvn archetype:generate -DgroupId=com.example.tictactoe -DartifactId=tic-tac-toe-game -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
    

    This command will create the pom.xml and the basic src/main/java/com/example/tictactoe/App.java file. We will rename App.java later.

  3. Update pom.xml: Open the pom.xml file in the tic-tac-toe-game directory. We need to configure it for Java 25 and add a plugin for easier execution.

    <!-- tic-tac-toe-game/pom.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.example.tictactoe</groupId>
        <artifactId>tic-tac-toe-game</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    
        <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <maven.compiler.source>25</maven.compiler.source> <!-- Target Java 25 -->
            <maven.compiler.target>25</maven.compiler.target> <!-- Target Java 25 -->
            <java.version>25</java.version>
        </properties>
    
        <dependencies>
            <!-- No external dependencies needed for this basic version -->
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.13.0</version> <!-- Use a recent version -->
                    <configuration>
                        <source>${maven.compiler.source}</source>
                        <target>${maven.compiler.target}</target>
                    </configuration>
                </plugin>
                <plugin>
                    <groupId>org.codehaus.mojo</groupId>
                    <artifactId>exec-maven-plugin</artifactId>
                    <version>3.2.0</version> <!-- Use a recent version -->
                    <executions>
                        <execution>
                            <goals>
                                <goal>java</goal>
                            </goals>
                        </execution>
                    </executions>
                    <configuration>
                        <mainClass>com.example.tictactoe.TicTacToeGame</mainClass> <!-- Our main class -->
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </project>
    

    Explanation:

    • We set maven.compiler.source and maven.compiler.target to 25 to ensure our project compiles and runs with Java 25 features.
    • The maven-compiler-plugin ensures the specified Java version is used.
    • The exec-maven-plugin allows us to run our application directly using mvn exec:java, specifying com.example.tictactoe.TicTacToeGame as our main class.
  4. Rename App.java: Delete the automatically generated src/main/java/com/example/tictactoe/App.java and src/test/java/com/example/tictactoe/AppTest.java files. We will create our own classes.

    rm src/main/java/com/example/tictactoe/App.java
    rm src/test/java/com/example/tictactoe/AppTest.java
    

b) Core Implementation

Let’s start building our game components.

1. GameStatus Enum

This enum will clearly define the different states our game can be in.

File: src/main/java/com/example/tictactoe/GameStatus.java

package com.example.tictactoe;

/**
 * Represents the current status of the Tic-Tac-Toe game.
 * This enum provides a clear, type-safe way to manage game progression.
 */
public enum GameStatus {
    IN_PROGRESS, // The game is currently being played
    X_WINS,      // Player X has won the game
    O_WINS,      // Player O has won the game
    DRAW         // The game has ended in a draw
}

Explanation:

  • Enums are a special type of class that represent a group of constants. They are perfect for fixed sets of values like game states.
  • The status values are self-explanatory and cover all possible end conditions and the active state.

2. GameException Custom Exception

A custom exception helps us manage specific game-related errors, making our error handling more precise than generic RuntimeExceptions.

File: src/main/java/com/example/tictactoe/GameException.java

package com.example.tictactoe;

/**
 * Custom exception for Tic-Tac-Toe game-related errors,
 * such as invalid moves or game state issues.
 */
public class GameException extends RuntimeException {

    /**
     * Constructs a new GameException with the specified detail message.
     *
     * @param message the detail message (which is saved for later retrieval by the getMessage() method).
     */
    public GameException(String message) {
        super(message);
    }

    /**
     * Constructs a new GameException with the specified detail message and cause.
     *
     * @param message the detail message.
     * @param cause   the cause (which is saved for later retrieval by the getCause() method).
     *                (A null value is permitted, and indicates that the cause is nonexistent or unknown.)
     */
    public GameException(String message, Throwable cause) {
        super(message, cause);
    }
}

Explanation:

  • Extending RuntimeException means this is an unchecked exception, which is often suitable for programming errors or situations where immediate recovery isn’t expected (e.g., a user tries to make an illegal move, and we want to inform them and re-prompt).
  • It provides constructors to include a message and an optional cause, standard for exceptions.

3. Board Class

This class will manage the 3x3 grid and its operations.

File: src/main/java/com/example/tictactoe/Board.java

package com.example.tictactoe;

import java.util.Arrays; // For array manipulation, specifically filling the board.
import java.util.logging.Logger; // For logging board actions and state.

/**
 * Represents the Tic-Tac-Toe game board.
 * Manages the 3x3 grid, placing marks, and checking cell states.
 */
public class Board {
    private static final Logger LOGGER = Logger.getLogger(Board.class.getName());
    private static final int BOARD_SIZE = 3;
    private final char[][] cells; // 3x3 grid to store player marks.
    public static final char EMPTY_CELL = '-'; // Represents an empty cell on the board.

    /**
     * Constructs a new Board and initializes all cells to EMPTY_CELL.
     */
    public Board() {
        this.cells = new char[BOARD_SIZE][BOARD_SIZE];
        for (char[] row : cells) {
            Arrays.fill(row, EMPTY_CELL); // Efficiently fill each row with empty marks.
        }
        LOGGER.info("Tic-Tac-Toe board initialized.");
    }

    /**
     * Places a player's mark on the board at the specified row and column.
     *
     * @param row  The row index (0-2).
     * @param col  The column index (0-2).
     * @param mark The player's mark ('X' or 'O').
     * @throws GameException if the move is invalid (out of bounds or cell already occupied).
     */
    public void placeMark(int row, int col, char mark) {
        if (!isValidMove(row, col)) {
            LOGGER.warning(String.format("Attempted invalid move at (%d, %d).", row, col));
            throw new GameException("Invalid move: Row and column must be between 0 and 2.");
        }
        if (!isCellEmpty(row, col)) {
            LOGGER.warning(String.format("Attempted to place mark on occupied cell at (%d, %d).", row, col));
            throw new GameException("Invalid move: Cell already occupied.");
        }

        this.cells[row][col] = mark;
        LOGGER.info(String.format("Mark '%c' placed at (%d, %d).", mark, row, col));
    }

    /**
     * Checks if a cell at the given row and column is empty.
     *
     * @param row The row index.
     * @param col The column index.
     * @return true if the cell is empty, false otherwise.
     */
    public boolean isCellEmpty(int row, int col) {
        // Basic boundary check for internal use, assuming placeMark handles public validation
        if (!isValidMove(row, col)) {
            // Log this as an internal issue if it happens, as public methods should prevent it
            LOGGER.warning(String.format("Internal call to isCellEmpty with invalid coordinates (%d, %d).", row, col));
            return false; // Or throw an IllegalArgumentException if strict internal validation is desired
        }
        return cells[row][col] == EMPTY_CELL;
    }

    /**
     * Checks if the entire board is full (no empty cells remaining).
     *
     * @return true if the board is full, false otherwise.
     */
    public boolean isBoardFull() {
        for (int i = 0; i < BOARD_SIZE; i++) {
            for (int j = 0; j < BOARD_SIZE; j++) {
                if (cells[i][j] == EMPTY_CELL) {
                    return false; // Found an empty cell, board is not full.
                }
            }
        }
        LOGGER.info("Board is full.");
        return true; // No empty cells found.
    }

    /**
     * Prints the current state of the board to the console.
     * Includes row and column headers for better readability.
     */
    public void printBoard() {
        LOGGER.fine("Printing current board state.");
        System.out.println("  0 1 2"); // Column headers
        for (int i = 0; i < BOARD_SIZE; i++) {
            System.out.print(i + " "); // Row header
            for (int j = 0; j < BOARD_SIZE; j++) {
                System.out.print(cells[i][j] + (j == BOARD_SIZE - 1 ? "" : " "));
            }
            System.out.println();
        }
        System.out.println(); // Add an extra newline for spacing
    }

    /**
     * Checks if the given row and column coordinates are within the board boundaries.
     *
     * @param row The row index.
     * @param col The column index.
     * @return true if the coordinates are valid, false otherwise.
     */
    public boolean isValidMove(int row, int col) {
        return row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE;
    }

    /**
     * Provides direct access to the board's cells.
     * This is useful for the Game class to check win conditions.
     *
     * @return The 2D character array representing the board cells.
     */
    public char[][] getCells() {
        // Return a defensive copy if the board state should be immutable from outside.
        // For this simple game, direct access is acceptable for performance and simplicity,
        // but in a larger system, a clone or immutable view would be preferred.
        return cells;
    }
}

Explanation:

  • LOGGER: We use java.util.logging.Logger for internal logging of board operations. This is crucial for production-ready applications to trace execution and debug issues.
  • BOARD_SIZE & EMPTY_CELL: Constants for clarity and easy modification.
  • cells: A char[][] array represents the 3x3 grid.
  • Constructor: Initializes all cells to EMPTY_CELL. Arrays.fill is an efficient way to do this.
  • placeMark:
    • Performs critical input validation: checks bounds and if the cell is already occupied.
    • Throws GameException for invalid moves, allowing the Game class to handle user feedback.
    • Logs successful and failed attempts.
  • isCellEmpty, isBoardFull: Utility methods to query the board state.
  • printBoard: Formats and prints the board to System.out.
  • isValidMove: A private helper method for boundary checks.
  • getCells: Provides the Game class access to the raw board for win condition checks. A more robust solution might return a deep copy or an unmodifiable view to prevent external modification of the internal state.

4. Player Class

A simple class to hold player information.

File: src/main/java/com/example/tictactoe/Player.java

package com.example.tictactoe;

import java.util.logging.Logger;

/**
 * Represents a player in the Tic-Tac-Toe game.
 * Each player has a name and a unique mark ('X' or 'O').
 */
public class Player {
    private static final Logger LOGGER = Logger.getLogger(Player.class.getName());
    private final String name;
    private final char mark;

    /**
     * Constructs a new Player.
     *
     * @param name The name of the player.
     * @param mark The mark associated with the player ('X' or 'O').
     * @throws IllegalArgumentException if the mark is not 'X' or 'O'.
     */
    public Player(String name, char mark) {
        if (mark != 'X' && mark != 'O') {
            LOGGER.severe("Invalid mark provided for player: " + mark);
            throw new IllegalArgumentException("Player mark must be 'X' or 'O'.");
        }
        this.name = name;
        this.mark = mark;
        LOGGER.info(String.format("Player '%s' created with mark '%c'.", name, mark));
    }

    /**
     * Returns the player's name.
     * @return The name of the player.
     */
    public String getName() {
        return name;
    }

    /**
     * Returns the player's mark ('X' or 'O').
     * @return The mark of the player.
     */
    public char getMark() {
        return mark;
    }

    @Override
    public String toString() {
        return String.format("Player{name='%s', mark='%c'}", name, mark);
    }
}

Explanation:

  • LOGGER: For logging player creation.
  • name & mark: Fields to store player data. final to ensure immutability after creation.
  • Constructor: Basic validation for the mark character.
  • Getters: Standard accessor methods.
  • toString(): Provides a helpful string representation for logging or debugging.

5. Game Class

This is the central class that ties everything together, managing the game state and logic.

File: src/main/java/com/example/tictactoe/Game.java

package com.example.tictactoe;

import java.util.logging.Logger;

/**
 * Manages the Tic-Tac-Toe game logic, including turns, moves,
 * and determining win or draw conditions.
 */
public class Game {
    private static final Logger LOGGER = Logger.getLogger(Game.class.getName());

    private final Board board;
    private final Player playerX;
    private final Player playerO;
    private Player currentPlayer;
    private GameStatus status;

    /**
     * Constructs a new Tic-Tac-Toe game.
     * Initializes the board, creates two players, and sets the initial game status.
     */
    public Game() {
        this.board = new Board();
        this.playerX = new Player("Player X", 'X');
        this.playerO = new Player("Player O", 'O');
        this.currentPlayer = playerX; // Player X always starts.
        this.status = GameStatus.IN_PROGRESS;
        LOGGER.info("Tic-Tac-Toe game initialized. Player X starts.");
    }

    /**
     * Processes a player's move.
     * Places the current player's mark on the board, checks for win/draw,
     * and switches to the next player if the game is still in progress.
     *
     * @param row The row index for the move.
     * @param col The column index for the move.
     * @throws GameException if the game is not in progress or the move is invalid.
     */
    public void makeMove(int row, int col) {
        if (status != GameStatus.IN_PROGRESS) {
            LOGGER.warning("Attempted move when game is not IN_PROGRESS. Current status: " + status);
            throw new GameException("Game is already over. Current status: " + status);
        }

        try {
            board.placeMark(row, col, currentPlayer.getMark());
            LOGGER.info(String.format("%s made a move at (%d, %d).", currentPlayer.getName(), row, col));
        } catch (GameException e) {
            LOGGER.warning("Invalid move attempt: " + e.getMessage());
            throw e; // Re-throw to inform the caller (TicTacToeGame main)
        }

        updateGameStatus(); // Check for win or draw after each move.

        if (status == GameStatus.IN_PROGRESS) {
            switchCurrentPlayer(); // Only switch if the game is still active.
        } else {
            LOGGER.info("Game has ended. Final status: " + status);
        }
    }

    /**
     * Checks the board for win conditions (rows, columns, diagonals)
     * and draw conditions, then updates the game status accordingly.
     */
    private void updateGameStatus() {
        char[][] cells = board.getCells();
        char currentMark = currentPlayer.getMark();

        // Check rows and columns for a win
        for (int i = 0; i < 3; i++) {
            if ((cells[i][0] == currentMark && cells[i][1] == currentMark && cells[i][2] == currentMark) || // Row win
                (cells[0][i] == currentMark && cells[1][i] == currentMark && cells[2][i] == currentMark)) { // Column win
                status = (currentMark == 'X') ? GameStatus.X_WINS : GameStatus.O_WINS;
                LOGGER.info(String.format("Player %c wins!", currentMark));
                return;
            }
        }

        // Check diagonals for a win
        if ((cells[0][0] == currentMark && cells[1][1] == currentMark && cells[2][2] == currentMark) || // Diagonal 1
            (cells[0][2] == currentMark && cells[1][1] == currentMark && cells[2][0] == currentMark)) { // Diagonal 2
            status = (currentMark == 'X') ? GameStatus.X_WINS : GameStatus.O_WINS;
            LOGGER.info(String.format("Player %c wins!", currentMark));
            return;
        }

        // Check for draw
        if (board.isBoardFull()) {
            status = GameStatus.DRAW;
            LOGGER.info("Game is a draw!");
            return;
        }

        // If no win or draw, game is still in progress
        status = GameStatus.IN_PROGRESS;
    }

    /**
     * Switches the current player from 'X' to 'O' or 'O' to 'X'.
     */
    private void switchCurrentPlayer() {
        currentPlayer = (currentPlayer == playerX) ? playerO : playerX;
        LOGGER.fine("Switched current player to: " + currentPlayer.getName());
    }

    /**
     * Returns the current player.
     * @return The Player whose turn it is.
     */
    public Player getCurrentPlayer() {
        return currentPlayer;
    }

    /**
     * Returns the current status of the game.
     * @return The GameStatus enum value.
     */
    public GameStatus getStatus() {
        return status;
    }

    /**
     * Returns the game board.
     * @return The Board object.
     */
    public Board getBoard() {
        return board;
    }
}

Explanation:

  • LOGGER: Logs game events, moves, and status changes.
  • board, playerX, playerO, currentPlayer, status: These fields encapsulate the entire state of the game. final for board, playerX, playerO as they don’t change after initialization.
  • Constructor: Initializes the board and players. playerX always starts.
  • makeMove:
    • Crucially checks if the GameStatus is IN_PROGRESS before allowing a move.
    • Delegates mark placement to the Board object.
    • Handles GameException from Board.placeMark by re-throwing it, allowing TicTacToeGame to present it to the user.
    • Calls updateGameStatus after each valid move.
    • Calls switchCurrentPlayer only if the game is still ongoing.
  • updateGameStatus: This is the core logic for determining game outcomes.
    • It retrieves the board cells directly from the Board object.
    • It checks all 8 possible win lines (3 rows, 3 columns, 2 diagonals) for the currentMark.
    • If no win, it checks for a draw using board.isBoardFull().
    • Updates the status field accordingly.
  • switchCurrentPlayer: A simple ternary operator toggles the currentPlayer.
  • Getters: Allow TicTacToeGame to query the game state.

6. TicTacToeGame (Main Class)

This is the entry point for our application, responsible for user interaction and driving the game loop.

File: src/main/java/com/example/tictactoe/TicTacToeGame.java

package com.example.tictactoe;

import java.util.InputMismatchException; // For handling non-integer input.
import java.util.Scanner; // For reading user input.
import java.util.logging.Level; // For logging level configuration.
import java.util.logging.Logger; // For logging.

/**
 * Main class to run the Tic-Tac-Toe game.
 * Handles user input, displays the board, and manages the game loop.
 */
public class TicTacToeGame {
    private static final Logger LOGGER = Logger.getLogger(TicTacToeGame.class.getName());
    private final Scanner scanner; // Scanner to read user input.

    public TicTacToeGame() {
        this.scanner = new Scanner(System.in);
        // Configure logging for the console (optional, but good for production-readiness)
        // By default, console handler logs INFO and above.
        // To see FINE/FINER/FINEST, you'd configure logging.properties or programmatically.
        // For this tutorial, we'll keep it simple and let INFO/WARNING/SEVERE show up.
        LOGGER.setLevel(Level.INFO); // Set default level for this logger
        LOGGER.info("Tic-Tac-Toe Game application started.");
    }

    /**
     * Runs the main game loop.
     */
    public void startGame() {
        LOGGER.info("Starting new Tic-Tac-Toe game instance.");
        Game game = new Game(); // Create a new game instance.

        while (game.getStatus() == GameStatus.IN_PROGRESS) {
            game.getBoard().printBoard(); // Display the current board.
            Player currentPlayer = game.getCurrentPlayer();
            System.out.printf("%s (%c), enter your move (row and column, e.g., '0 1'): ",
                    currentPlayer.getName(), currentPlayer.getMark());

            int row = -1;
            int col = -1;
            boolean validInput = false;

            while (!validInput) {
                try {
                    String line = scanner.nextLine();
                    String[] parts = line.trim().split("\\s+"); // Split by one or more spaces
                    if (parts.length != 2) {
                        System.out.println("Invalid input format. Please enter two numbers separated by a space (e.g., '0 1').");
                        LOGGER.warning("Invalid input format received: " + line);
                        continue;
                    }

                    row = Integer.parseInt(parts[0]);
                    col = Integer.parseInt(parts[1]);

                    // Basic validation to ensure numbers are within expected range before passing to Game.makeMove
                    if (row < 0 || row > 2 || col < 0 || col > 2) {
                        System.out.println("Coordinates out of bounds. Row and column must be between 0 and 2.");
                        LOGGER.warning(String.format("User entered out-of-bounds coordinates: (%d, %d)", row, col));
                        continue;
                    }
                    validInput = true; // Input is valid (format and range)
                } catch (NumberFormatException e) {
                    System.out.println("Invalid input. Please enter numbers for row and column.");
                    LOGGER.warning("NumberFormatException during input parsing: " + e.getMessage());
                } catch (Exception e) { // Catch any other unexpected input issues
                    System.out.println("An unexpected error occurred during input. Please try again.");
                    LOGGER.log(Level.SEVERE, "Unexpected error in input reading loop.", e);
                    scanner.nextLine(); // Consume the invalid line to prevent infinite loop
                }
            }

            try {
                game.makeMove(row, col); // Attempt to make the move.
            } catch (GameException e) {
                System.out.println("Error: " + e.getMessage());
                LOGGER.info("GameException caught: " + e.getMessage());
                // No need to switch player or update status, just re-prompt current player.
            }
        }

        // Game has ended, print final board and result
        game.getBoard().printBoard();
        printGameResult(game.getStatus());
        LOGGER.info("Tic-Tac-Toe game finished. Status: " + game.getStatus());
        scanner.close(); // Close the scanner to release resources.
        LOGGER.info("Tic-Tac-Toe Game application ended.");
    }

    /**
     * Prints the final result of the game based on its status.
     * @param status The final GameStatus.
     */
    private void printGameResult(GameStatus status) {
        switch (status) {
            case X_WINS:
                System.out.println("Player X wins! Congratulations!");
                break;
            case O_WINS:
                System.out.println("Player O wins! Congratulations!");
                break;
            case DRAW:
                System.out.println("It's a draw! Good game!");
                break;
            case IN_PROGRESS:
                // This case should ideally not be reached if the loop condition is correct.
                System.out.println("Game unexpectedly ended while still in progress.");
                LOGGER.warning("printGameResult called with IN_PROGRESS status.");
                break;
        }
    }

    /**
     * Main method to start the Tic-Tac-Toe application.
     * @param args Command line arguments (not used).
     */
    public static void main(String[] args) {
        // Basic logging configuration for console output (optional, but good practice)
        // By default, java.util.logging outputs to console at INFO level.
        // For more advanced logging, you'd use a logging.properties file or frameworks like Log4j/Logback.
        System.setProperty("java.util.logging.SimpleFormatter.format",
                "[%1$tF %1$tT] [%4$-7s] %3$s - %5$s%n");

        TicTacToeGame app = new TicTacToeGame();
        app.startGame();
    }
}

Explanation:

  • LOGGER: Configured for general application logging. System.setProperty is used to format java.util.logging output for better readability in the console.
  • Scanner: Used for reading user input from the console. It’s initialized once and closed at the end of startGame to prevent resource leaks.
  • startGame():
    • Creates a Game instance.
    • The while (game.getStatus() == GameStatus.IN_PROGRESS) loop drives the game until a win or draw occurs.
    • Inside the loop:
      • The board is printed.
      • The current player is prompted for a move.
      • Robust Input Handling: A nested while (!validInput) loop with try-catch blocks handles various input errors:
        • NumberFormatException: If non-numeric input is provided.
        • InputMismatchException: (Handled implicitly by NumberFormatException after Integer.parseInt).
        • Custom validation for out-of-bounds coordinates (0-2).
        • General Exception for any other unexpected input issues, consuming the line to prevent infinite loops.
      • game.makeMove(row, col) is called. This is wrapped in a try-catch block to handle GameException thrown by the Game and Board classes (e.g., trying to place a mark on an occupied cell).
  • printGameResult: A helper method to display the final outcome.
  • main method:
    • Sets up the logging format.
    • Creates an instance of TicTacToeGame and calls startGame().

c) Testing This Component

At this point, we have a complete, runnable command-line Tic-Tac-Toe game. Let’s compile and run it to test the core logic.

  1. Compile the Project: Navigate to the tic-tac-toe-game directory in your terminal and compile the Java code using Maven:

    mvn clean install
    

    This command cleans previous builds, compiles the source code, and packages it into a JAR file. You should see BUILD SUCCESS if everything is correct.

  2. Run the Game: After successful compilation, run the game using the exec-maven-plugin:

    mvn exec:java
    

    Expected Behavior: The game should start, print an empty 3x3 board, and prompt Player X for a move.

    [2025-12-04 10:00:00] [INFO   ] com.example.tictactoe.TicTacToeGame - Tic-Tac-Toe Game application started.
    [2025-12-04 10:00:00] [INFO   ] com.example.tictactoe.Game - Tic-Tac-Toe game initialized. Player X starts.
    [2025-12-04 10:00:00] [INFO   ] com.example.tictactoe.Board - Tic-Tac-Toe board initialized.
      0 1 2
    0 - - -
    1 - - -
    2 - - -
    
    Player X (X), enter your move (row and column, e.g., '0 1'):
    

    Debugging Tips:

    • If you get compilation errors, double-check your pom.xml (especially Java version and plugin versions) and ensure all file paths and class names match exactly.
    • Check for missing imports in your Java files.
    • If the game doesn’t start, ensure your mainClass in pom.xml (for exec-maven-plugin) points to com.example.tictactoe.TicTacToeGame.
    • If input isn’t working, verify the Scanner usage and try-catch blocks in TicTacToeGame.java. The logging messages will be very helpful here.

Production Considerations

While Tic-Tac-Toe is a simple game, applying production-ready principles from the start is crucial for any real-world application.

Error Handling

  • Custom Exceptions: We’ve used GameException for domain-specific errors (e.g., invalid moves). This makes error handling clearer than using generic RuntimeException.
  • Input Validation: TicTacToeGame thoroughly validates user input (numeric, range, format) before attempting to process it. This prevents crashes due to malformed input.
  • Boundary Checks: The Board class performs boundary checks (isValidMove) to prevent ArrayIndexOutOfBoundsException.
  • Game State Checks: The Game class checks GameStatus before allowing moves, preventing actions on an already finished game.

Performance Optimization

For a simple command-line Tic-Tac-Toe game, performance is not a critical concern. The operations are minimal (array access, simple loops).

  • Efficiency: The Arrays.fill method for board initialization is efficient. Win condition checks are direct and involve minimal iterations.
  • Object Creation: Object creation (Board, Player, Game) happens once per game, minimizing overhead.

Security Considerations

  • Input Sanitization: For a command-line game, the primary security concern is malicious input. Our robust input validation (Integer.parseInt, range checks, split("\\s+")) handles common issues like non-numeric input or attempts to overflow buffers (though less of a concern in Java than C++).
  • No External Dependencies: This version has no network calls, database interactions, or file I/O, significantly reducing the attack surface. If we were to add features like saving game state to a file, we’d need to consider file path traversal vulnerabilities.
  • Immutability: Player objects are largely immutable (name and mark are final), which can prevent accidental modification of player data during the game.

Logging and Monitoring

  • java.util.logging: We’ve integrated java.util.logging.Logger into all core classes (Board, Player, Game, TicTacToeGame).
  • Logging Levels: We use INFO for significant events (game start/end, player moves), WARNING for recoverable issues (invalid user input, invalid move attempts), SEVERE for critical errors, and FINE for detailed internal operations (like board printing).
  • Structured Logging (Basic): The System.setProperty in main provides a basic structured format for console logs, including timestamp, level, logger name, and message. In a larger production system, you’d integrate a more powerful logging framework like Log4j2 or SLF4J/Logback and configure it to output to files, central logging systems (e.g., ELK stack), or cloud monitoring services.

Code Review Checkpoint

At this stage, you have successfully built the core logic for a Tic-Tac-Toe game using an Object-Oriented approach.

Summary of what was built:

  • GameStatus Enum: Defines the possible states of the game.
  • GameException: A custom exception for game-specific errors.
  • Board.java: Manages the 3x3 game grid, including placing marks, checking empty cells, and printing the board.
  • Player.java: Represents a player with a name and a mark.
  • Game.java: The central game logic orchestrator, managing players, board, turns, and determining win/draw conditions.
  • TicTacToeGame.java: The main application entry point, handling user interaction and driving the game loop.

Files created/modified:

  • pom.xml (configured for Java 25 and exec-maven-plugin)
  • src/main/java/com/example/tictactoe/GameStatus.java
  • src/main/java/com/example/tictactoe/GameException.java
  • src/main/java/com/example/tictactoe/Board.java
  • src/main/java/com/example/tictactoe/Player.java
  • src/main/java/com/example/tictactoe/Game.java
  • src/main/java/com/example/tictactoe/TicTacToeGame.java

How it integrates with existing code: This is a standalone project, so it doesn’t directly integrate with previous chapters. However, it demonstrates the evolution of our coding practices from single-file scripts to a multi-class, OOP-driven application, which is a crucial step for future, more complex projects.

Common Issues & Solutions

Developers often encounter a few common issues when building a game like Tic-Tac-Toe.

  1. ArrayIndexOutOfBoundsException:

    • Issue: Occurs if you try to access cells[row][col] where row or col is outside the 0-2 range.
    • Debugging: Check the values of row and col just before the exception. In our Board.java, the isValidMove method and the placeMark method’s initial check are designed to prevent this.
    • Solution: Ensure all user inputs are validated for range before they are passed to array access methods. Our TicTacToeGame class includes this client-side validation, and Board.placeMark has server-side validation.
    • Prevention: Always validate user input and use helper methods like isValidMove for boundary checks.
  2. Logical Errors in Win Condition Checks:

    • Issue: The game might declare a winner incorrectly, or fail to declare a winner when it should, or declare a draw prematurely.
    • Debugging:
      • Carefully review the updateGameStatus() method in Game.java.
      • Print the cells array state at each step of the updateGameStatus method, especially when checking rows, columns, and diagonals.
      • Manually trace specific game scenarios (e.g., X wins on top row, O wins on middle column, diagonal win) on paper and compare with your code’s logic.
    • Solution: Break down the updateGameStatus method into smaller, testable private methods (e.g., checkRowsForWin(), checkColumnsForWin(), checkDiagonalsForWin()). This makes it easier to isolate bugs. Ensure all 8 win conditions are covered.
  3. InputMismatchException or NumberFormatException during input:

    • Issue: The user enters text when numbers are expected, or enters too few/many numbers.
    • Debugging: The TicTacToeGame.java’s input loop catches these exceptions. The LOGGER.warning messages will indicate the exact input that caused the problem.
    • Solution: Our code already handles this robustly using a try-catch block around Integer.parseInt and scanner.nextLine(), prompting the user to re-enter. The split("\\s+") also helps handle varying spaces.
    • Prevention: Always wrap user input parsing in try-catch blocks and provide clear error messages to the user.

Testing & Verification

Let’s ensure our Tic-Tac-Toe game works as expected through various scenarios.

  1. Compile and Run: First, ensure you have compiled and can run the application:

    mvn clean install
    mvn exec:java
    
  2. Verify Game Start:

    • The board should print empty cells (-).
    • Player X should be prompted for their first move.
  3. Test Valid Moves:

    • Enter 0 0. The board should update, and Player O should be prompted.
    • Enter 1 1. The board should update, and Player X should be prompted.
    • Continue making valid moves.
  4. Test Invalid Moves:

    • Out of bounds: Enter 3 0 or -1 1. The game should print an error (“Coordinates out of bounds…”) and re-prompt the current player.
    • Occupied cell: Try to place a mark on a cell that already has an ‘X’ or ‘O’. The game should print an error (“Error: Invalid move: Cell already occupied.”) and re-prompt the current player.
    • Invalid input format: Enter abc or 0 or 0a 1. The game should print “Invalid input format…” or “Invalid input. Please enter numbers…” and re-prompt.
  5. Test Win Conditions:

    • Player X Wins (Row):
      • X: 0 0
      • O: 1 0
      • X: 0 1
      • O: 1 1
      • X: 0 2
      • Expected: “Player X wins! Congratulations!”
    • Player O Wins (Column):
      • X: 0 0
      • O: 0 1
      • X: 1 1
      • O: 1 2
      • X: 2 0
      • O: 0 2
      • Expected: “Player O wins! Congratulations!”
    • Player X Wins (Diagonal):
      • X: 0 0
      • O: 0 1
      • X: 1 1
      • O: 0 2
      • X: 2 2
      • Expected: “Player X wins! Congratulations!”
  6. Test Draw Condition:

    • X: 0 0
    • O: 0 1
    • X: 0 2
    • O: 1 0
    • X: 1 2
    • O: 1 1
    • X: 2 1
    • O: 2 0
    • X: 2 2
    • Expected: “It’s a draw! Good game!”

After running through these scenarios, you should be confident that your Tic-Tac-Toe game logic is sound and robust.

Summary & Next Steps

In this comprehensive chapter, we successfully built a production-ready core for a Tic-Tac-Toe game using modern Java (version 25) and applying strong Object-Oriented Design principles. We meticulously crafted classes for GameStatus, GameException, Board, Player, and Game, encapsulating responsibilities and promoting modularity. The TicTacToeGame class served as our robust application entry point, handling user interaction with thorough input validation and logging.

We emphasized production considerations such as robust error handling, basic performance awareness, security in input processing, and comprehensive logging using java.util.logging. This project serves as an excellent foundation for understanding how to structure a larger Java application and manage its state effectively.

In the next chapter, we will enhance our Tic-Tac-Toe game by exploring options for a graphical user interface (GUI). We’ll investigate frameworks like JavaFX or Swing to transform our command-line game into a more visually appealing and interactive experience, while retaining the core logic we’ve built in this chapter.