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:
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.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.BoardClass:- 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.
PlayerClass:- 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.
GameClass:- Orchestrates the overall game flow.
- Manages the
BoardandPlayerobjects. - 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.
TicTacToeGameClass (Main Entry Point):- Contains the
mainmethod. - Handles user interaction (input/output) and drives the game loop by interacting with the
Gameclass.
- Contains the
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.
Create Project Directory: Open your terminal or command prompt and navigate to your workspace.
mkdir tic-tac-toe-game cd tic-tac-toe-gameInitialize 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=falseThis command will create the
pom.xmland the basicsrc/main/java/com/example/tictactoe/App.javafile. We will renameApp.javalater.Update
pom.xml: Open thepom.xmlfile in thetic-tac-toe-gamedirectory. 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.sourceandmaven.compiler.targetto25to ensure our project compiles and runs with Java 25 features. - The
maven-compiler-pluginensures the specified Java version is used. - The
exec-maven-pluginallows us to run our application directly usingmvn exec:java, specifyingcom.example.tictactoe.TicTacToeGameas our main class.
- We set
Rename
App.java: Delete the automatically generatedsrc/main/java/com/example/tictactoe/App.javaandsrc/test/java/com/example/tictactoe/AppTest.javafiles. 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
RuntimeExceptionmeans 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 usejava.util.logging.Loggerfor 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: Achar[][]array represents the 3x3 grid.- Constructor: Initializes all cells to
EMPTY_CELL.Arrays.fillis an efficient way to do this. placeMark:- Performs critical input validation: checks bounds and if the cell is already occupied.
- Throws
GameExceptionfor invalid moves, allowing theGameclass to handle user feedback. - Logs successful and failed attempts.
isCellEmpty,isBoardFull: Utility methods to query the board state.printBoard: Formats and prints the board toSystem.out.isValidMove: A private helper method for boundary checks.getCells: Provides theGameclass 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.finalto ensure immutability after creation.- Constructor: Basic validation for the
markcharacter. - 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.finalforboard,playerX,playerOas they don’t change after initialization.- Constructor: Initializes the board and players.
playerXalways starts. makeMove:- Crucially checks if the
GameStatusisIN_PROGRESSbefore allowing a move. - Delegates mark placement to the
Boardobject. - Handles
GameExceptionfromBoard.placeMarkby re-throwing it, allowingTicTacToeGameto present it to the user. - Calls
updateGameStatusafter each valid move. - Calls
switchCurrentPlayeronly if the game is still ongoing.
- Crucially checks if the
updateGameStatus: This is the core logic for determining game outcomes.- It retrieves the board cells directly from the
Boardobject. - 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
statusfield accordingly.
- It retrieves the board cells directly from the
switchCurrentPlayer: A simple ternary operator toggles thecurrentPlayer.- Getters: Allow
TicTacToeGameto 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.setPropertyis used to formatjava.util.loggingoutput for better readability in the console.Scanner: Used for reading user input from the console. It’s initialized once and closed at the end ofstartGameto prevent resource leaks.startGame():- Creates a
Gameinstance. - 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 withtry-catchblocks handles various input errors:NumberFormatException: If non-numeric input is provided.InputMismatchException: (Handled implicitly byNumberFormatExceptionafterInteger.parseInt).- Custom validation for out-of-bounds coordinates (0-2).
- General
Exceptionfor any other unexpected input issues, consuming the line to prevent infinite loops.
game.makeMove(row, col)is called. This is wrapped in atry-catchblock to handleGameExceptionthrown by theGameandBoardclasses (e.g., trying to place a mark on an occupied cell).
- Creates a
printGameResult: A helper method to display the final outcome.mainmethod:- Sets up the logging format.
- Creates an instance of
TicTacToeGameand callsstartGame().
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.
Compile the Project: Navigate to the
tic-tac-toe-gamedirectory in your terminal and compile the Java code using Maven:mvn clean installThis command cleans previous builds, compiles the source code, and packages it into a JAR file. You should see
BUILD SUCCESSif everything is correct.Run the Game: After successful compilation, run the game using the
exec-maven-plugin:mvn exec:javaExpected 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
mainClassinpom.xml(forexec-maven-plugin) points tocom.example.tictactoe.TicTacToeGame. - If input isn’t working, verify the
Scannerusage andtry-catchblocks inTicTacToeGame.java. The logging messages will be very helpful here.
- If you get compilation errors, double-check your
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
GameExceptionfor domain-specific errors (e.g., invalid moves). This makes error handling clearer than using genericRuntimeException. - Input Validation:
TicTacToeGamethoroughly validates user input (numeric, range, format) before attempting to process it. This prevents crashes due to malformed input. - Boundary Checks: The
Boardclass performs boundary checks (isValidMove) to preventArrayIndexOutOfBoundsException. - Game State Checks: The
Gameclass checksGameStatusbefore 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.fillmethod 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:
Playerobjects are largely immutable (name and mark arefinal), which can prevent accidental modification of player data during the game.
Logging and Monitoring
java.util.logging: We’ve integratedjava.util.logging.Loggerinto all core classes (Board,Player,Game,TicTacToeGame).- Logging Levels: We use
INFOfor significant events (game start/end, player moves),WARNINGfor recoverable issues (invalid user input, invalid move attempts),SEVEREfor critical errors, andFINEfor detailed internal operations (like board printing). - Structured Logging (Basic): The
System.setPropertyinmainprovides 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:
GameStatusEnum: 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 andexec-maven-plugin)src/main/java/com/example/tictactoe/GameStatus.javasrc/main/java/com/example/tictactoe/GameException.javasrc/main/java/com/example/tictactoe/Board.javasrc/main/java/com/example/tictactoe/Player.javasrc/main/java/com/example/tictactoe/Game.javasrc/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.
ArrayIndexOutOfBoundsException:- Issue: Occurs if you try to access
cells[row][col]whereroworcolis outside the0-2range. - Debugging: Check the values of
rowandcoljust before the exception. In ourBoard.java, theisValidMovemethod and theplaceMarkmethod’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
TicTacToeGameclass includes this client-side validation, andBoard.placeMarkhas server-side validation. - Prevention: Always validate user input and use helper methods like
isValidMovefor boundary checks.
- Issue: Occurs if you try to access
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 inGame.java. - Print the
cellsarray state at each step of theupdateGameStatusmethod, 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.
- Carefully review the
- Solution: Break down the
updateGameStatusmethod into smaller, testable private methods (e.g.,checkRowsForWin(),checkColumnsForWin(),checkDiagonalsForWin()). This makes it easier to isolate bugs. Ensure all 8 win conditions are covered.
InputMismatchExceptionorNumberFormatExceptionduring 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. TheLOGGER.warningmessages will indicate the exact input that caused the problem. - Solution: Our code already handles this robustly using a
try-catchblock aroundInteger.parseIntandscanner.nextLine(), prompting the user to re-enter. Thesplit("\\s+")also helps handle varying spaces. - Prevention: Always wrap user input parsing in
try-catchblocks and provide clear error messages to the user.
Testing & Verification
Let’s ensure our Tic-Tac-Toe game works as expected through various scenarios.
Compile and Run: First, ensure you have compiled and can run the application:
mvn clean install mvn exec:javaVerify Game Start:
- The board should print empty cells (
-). - Player X should be prompted for their first move.
- The board should print empty cells (
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.
- Enter
Test Invalid Moves:
- Out of bounds: Enter
3 0or-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
abcor0or0a 1. The game should print “Invalid input format…” or “Invalid input. Please enter numbers…” and re-prompt.
- Out of bounds: Enter
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!”
- X:
- 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!”
- X:
- Player X Wins (Diagonal):
- X:
0 0 - O:
0 1 - X:
1 1 - O:
0 2 - X:
2 2 - Expected: “Player X wins! Congratulations!”
- X:
- Player X Wins (Row):
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!”
- X:
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.