← Object Oriented Design Interview 10Design a Tic Tac Toe Game

10Design a Tic Tac Toe Game

In this chapter, we will explore the object-oriented design of a Tic-Tac-Toe game. We aim to create an interactive platform where two players alternate turns, placing their symbols on a virtual board. We’ll design key components such as the game board, player move tracking, outcome determination, and a score tracker for managing player ratings. How Tic-Tac-Toe Works: Tic-Tac-Toe is a classic two-player game played on a 3x3 grid. Each player selects a symbol ("X" or "O") and takes turns placing it in an empty cell. The goal is to align three identical symbols horizontally, vertically, or diagonally. The game concludes with a win if a player achieves this alignment, or a draw if all nine cells are filled without a winner. Let’s gather the key requirements through a mock interview scenario. Image represents a 3x3 tic-tac-toe board showing a completed game.  The board is composed of nine equal-sized squares arranged in a three-by-three grid, delineated by thick black lines. Each square contains either a bold, uppercase 'X' or a bold, uppercase 'O,' representing the moves of the two players. The arrangement of the symbols shows that 'X' has won the game, having three 'X's in a diagonal line from the top-left to the bottom-right.  There is no other information, such as URLs or parameters, present in the image; it solely depicts the final state of a tic-tac-toe game. Tic-Tac-Toe

Requirements Gathering

Here’s an example of a typical prompt an interviewer might present: “Imagine you and a friend are sitting down for a quick game of Tic-Tac-Toe. You each choose a symbol (e.g., “X” or “O”) and take turns placing your symbol on a board. After each move, the game checks if someone has won or if the board is full, signaling a draw. Behind the scenes, the game tracks your moves, updates a scoreboard to reflect wins, and maintains player rankings for future matches. Let’s design a Tic-Tac-Toe game system that handles all this.”

Requirements clarification

Here is an example of how a conversation between a candidate and an interviewer might unfold: Candidate: Does the game support different board sizes?
Interviewer: No, let’s stick to the standard 3x3 board for simplicity. Candidate: How should the game handle outcomes like wins, losses, and draws?
Interviewer: The system must detect winning patterns and notify players of the result: win, draw, or ongoing. Candidate: Should the game track player ratings?
Interviewer: Yes, the game should maintain a score tracker that updates player ratings based on game outcomes (win, loss, or draw). Candidate: How does the game handle invalid moves?
Interviewer: If a player attempts a move in an occupied or invalid position, notify them and prompt them for a new move. Based on this discussion, let’s nail down the key functional requirements.

Requirements

Here are the key functional requirements we’ve identified.

  • The game is played on a 3x3 board.
  • The system determines the game’s status:
    • a win (three identical symbols aligned in a row, column, or diagonal)
    • a draw (a full board with no winner)
    • In progress.
  • A score tracker records player performance, updates ratings based on wins, and supports queries like rankings or top players.
  • Invalid moves (e.g., placing a symbol in an occupied cell) are rejected with feedback to the player.

Below are the non-functional requirements:

  • The user interface should be intuitive, providing clear feedback for invalid moves and game outcomes, with easily accessible gameplay instructions.
  • The system should support future enhancements, such as different board sizes or game modes, without major architectural changes.

Identify Core Objects

Before diving into the design, it’s important to identify the core objects.

  • Board: The Board class models the 3x3 game grid where players place their symbols (e.g., "X" or "O"). It handles updates to the grid, checks for a winner by examining rows, columns, and diagonals, and determines if the board is full.
  • Player: This class represents an individual playing the game.
  • Game: The central entity of the Tic-Tac-Toe game is the Game class. It coordinates turn-taking between players, validates moves (e.g., ensuring a cell isn’t occupied), and tracks the game’s status, whether it’s in progress or ended with a winner or draw.

Design Choice Design choice: The Game class can become overloaded because it handles multiple operations. To keep it manageable, we delegate the Board to manage the grid and ScoreTracker to handle player ratings. This modularity enhances maintainability and scalability.

  • ScoreTracker: Tracks player ratings across games, updating them based on outcomes.

Now that we’ve identified the core objects, let’s design their relationships in a class diagram.

Design Class Diagram

In this section, we’ll define the class structure for a Tic-Tac-Toe game. The goal is to create a cohesive design that adheres to OOD principles, such as the Single Responsibility Principle (SRP), while remaining flexible for future extensions. We’ll also explain the reasoning behind design choices and consider alternatives to provide insight into the decision-making process.

Game

The Game class is the central coordinator of the Tic-Tac-Toe game. It manages the flow of gameplay, including initializing components, handling turns, and determining the outcome. To keep the design modular and manageable, certain responsibilities are delegated to other classes. For instance, the ScoreTracker class is solely responsible for tracking player performance, updating win counts based on game outcomes. The Board class manages the game grid, ensuring moves are valid by checking for empty spaces and staying within bounds. The Player class remains stateless. It does not store win counts directly. This separation allows the centralized ScoreTracker to monitor player performance across multiple games, setting the stage for a scalable ranking system, which we’ll explore in more depth later. Below is the representation of the Game class. Image represents a class diagram for a `Game` class in an object-oriented design.  The diagram shows the `Game` class, denoted by a 'C' in a circle, containing four private member variables: a `Board` object named `board`, a `ScoreTracker` object named `scoreTracker`, an array of `Player` objects named `players`, and an integer `currentPlayerIndex`.  Below these variables, the diagram lists five public member functions: `startNewGame\(\)`, which takes two `Player` objects as input; `makeMove\(\)`, which takes two integers and a `Player` object; `getGameStatus\(\)`, which returns a `GameCondition` object; `getCurrentPlayer\(\)`, which returns a `Player` object; and `getScoreTracker\(\)`, which returns a `ScoreTracker` object.  The arrangement clearly defines the internal state \(member variables\) and behavior \(member functions\) of the `Game` class, illustrating how it manages the game board, player scores, players themselves, and the current player's turn.  No external connections or data flows are depicted; the diagram focuses solely on the internal structure of the `Game` class.

Board

The Board class represents the 3x3 game grid, which is modeled as a two-dimensional array of Player objects. It is responsible for enforcing game rules at the board level, ensuring that moves are made within valid positions. It determines if a player has won by checking for three matching symbols in a row, column, or diagonal. Additionally, it provides functionality to reset the grid for a new game and allows retrieval of player symbols at specific grid positions. Here is the design of this class. Image represents a class diagram for a `Board` class in an object-oriented design.  The diagram shows the `Board` class, denoted by a 'C' in a circle, containing a private two-dimensional array `Player[][] grid` representing the game board's state.  The class further defines five public methods: `updateBoard\(int colIndex, int rowIndex, Player player\)`, which updates the board at a given location with a specified player; `Optional<Player> getWinner\(\)`, which returns an optional Player object representing the winner \(if any\); `boolean isFull\(\)`, which checks if the board is completely filled; `void reset\(\)`, which resets the board to its initial state; and `Player getPlayerAt\(int colIndex, int rowIndex\)`, which retrieves the player at a given location on the board.  No connections to other classes are explicitly shown in this diagram; it solely focuses on the internal structure and functionality of the `Board` class itself. Design Choice Design choice: The choice to include win-checking logic within the Board class, rather than the Game class, aligns with Single Responsibility Principle (SRP), as the Board is the owner of grid-related rules.

ScoreTracker

The ScoreTracker class is designed to monitor player performance across multiple games by maintaining a centralized scoreboard for a group of players. While real-world systems often employ complex methods that dynamically adjust scores based on performance distributions and other factors, a more straightforward approach suits an interview setting. Here, we can rate players based solely on their number of wins, keeping the logic straightforward yet effective. Instead of embedding ratings as an attribute within the Player class, we delegate this responsibility to ScoreTracker. This design choice stems from the nature of ratings: unlike a player’s name (an inherent trait), ratings are contextual, reflecting performance relative to others in a group. They shift as games are played, impacting multiple players simultaneously, and in advanced systems, they may even evolve over time. By isolating this logic in ScoreTracker, we also open the door to future enhancements, such as supporting players in multiple leagues with distinct ratings. To achieve this, ScoreTracker employs a HashMap<Player, Integer> named playerRatings to store win counts for all players. This centralized structure enables efficient management of the population’s ratings and supports key operations: updating scores after the game ends, identifying the top-ranked player, and determining any player’s rank. This modular approach not only encapsulates rating logic but also enhances maintainability and scalability. Here is the representation of this class. Image represents a class diagram for a `ScoreTracker` class.  The diagram shows the class name `ScoreTracker` enclosed in a rectangular box with rounded corners. Inside the box, a private member variable `playerRatings` is declared as a `HashMap` that stores `Player` objects as keys and their corresponding `Integer` scores as values.  Below this, three public methods are defined: `reportGameResult`, which takes two `Player` objects \(representing players in a game\) and an optional `Player` object \(the winning player\) as input; `getTopPlayers`, which returns a `Map` of `Player` objects and their scores, presumably representing the top-ranked players; and `getRank`, which takes a `Player` object as input and returns an integer representing that player's rank.  The diagram illustrates the internal structure and functionality of the `ScoreTracker` class, showing how it manages player scores and provides methods for reporting game results and retrieving player rankings. When a game ends, the reportGameResult method determines the winner and updates the score tracker. If the game results in a draw, no score changes are made.

Move

The Move class acts as a straightforward data structure designed to capture a player's move in the game. It stores the row and column indices where the player placed their symbol, along with a reference to the player who made the move. Image represents a class diagram depicting a class named 'Move'.  The class is represented by a rectangular box with a large 'C' in a circle at the top-left corner, indicating it's a class.  To the right of the 'C', the name 'Move' is written, signifying the class's name. Inside the rectangular box, three attributes are listed, each prefixed with a square checkbox:  'int rowlndex', 'int collndex', and 'Player player'. These attributes represent integer variables storing row and column indices, and an object of a class named 'Player', respectively.  There are no methods shown in this diagram, only the attributes of the class.  The diagram shows the internal structure of the 'Move' class, illustrating its data members \(attributes\) but not its behavior \(methods\).  No connections or information flow to or from other classes are depicted. By bundling these details (row, column, and player) into a single Move object, rather than passing them as separate parameters across various methods, the code becomes more readable and easier to maintain.

Player

The Player class encapsulates the core attributes of a player in the game: their name and assigned symbol (such as "X" or "O"). These attributes make it simple to identify which player is responsible for a given move. Image represents a class diagram depicting a `Player` class.  The diagram is a rectangular box.  In the top-left corner, inside a shaded circle, is the letter 'C', indicating this is a class. To the right of the 'C' is the class name, 'Player'. Below this, within the rectangular box, are two lines representing the class's attributes. The first line shows a checkbox followed by 'String name', indicating a string variable named `name`. The second line similarly shows a checkbox followed by 'char symbol', indicating a character variable named `symbol`.  There are no methods or relationships shown in this diagram; it solely describes the attributes of the `Player` class. While it might seem intuitive to include real-world actions like making moves or updating ratings within the Player class, this would violate the Single Responsibility Principle (SRP). In a well-designed system, responsibilities are clearly divided:

  • The Board class is the sole source of truth for validating and placing moves on the grid.
  • The ScoreTracker class tracks and updates player ratings, as ratings depend on the broader context of a group of players and require uniform updates across all players.

Complete Class Diagram

Below is the complete class diagram of the Tic-Tac-Toe game: Image represents a class diagram illustrating the object-oriented design of a game.  The central class, `Game`, contains a `Board`, a `ScoreTracker`, an array of `Player` objects, and an integer `currentPlayerIndex`.  `Game` has methods to start a new game \(`startNewGame`\), make a move \(`makeMove`\), get the game status \(`getGameStatus`\), get the current player \(`getCurrentPlayer`\), and get the `ScoreTracker`. The `Board` class has a 2D array \(`grid`\) of `Player` objects, methods to update the board \(`updateBoard`\), get the winner \(`getWinner`\), check if the board is full \(`isFull`\), reset the board \(`reset`\), and get a player at a specific position \(`getPlayerAt`\). The `ScoreTracker` class maintains a `HashMap` called `playerRatings` mapping `Player` objects to their scores \(integers\). It includes methods to report game results \(`reportGameResult`\), get the top players \(`getTopPlayers`\), and get a player's rank \(`getRank`\).  The `Move` class stores information about a single move: the row and column indices \(`rowIndex`, `colIndex`\), and the player who made the move \(`player`\).  The `Player` class has a `name` \(String\) and a `symbol` \(char\).  The `Game` class has a composition relationship with `Board`, `ScoreTracker`, and `Move`, indicated by solid lines.  The `ScoreTracker` and `Move` classes have an association with the `Player` class, also shown with solid lines.  Finally, an enumeration `GameCondition` with states `IN_PROGRESS` and `ENDED` is used by the `Game` class to represent the game's state. Class Diagram of Tic-Tac-Toe Let’s now bring our design to life with the code implementation!

Code - Tic-Tac-Toe Game

In this section, we’ll implement the core functionalities of the Tic-Tac-Toe game, focusing on key areas such as managing the game board, handling player turns, determining the winner, and tracking player ratings through a score tracker.

Game

The Game class manages the flow of a Tic-Tac-Toe game. It manages the game board, player turns, and score tracker updates. After each move, the game checks for win conditions (three matching symbols in a row, column, or diagonal) and for a draw (when the board is full, but no winner exists). Check out the code implementation of the Game class below.

public class Game {
    // Core game components
    private final Board board; // Manages the game board state
    private final ScoreTracker scoreTracker; // Keeps track of player scores
    private Player[] players; // Array of players in the game
    private int currentPlayerIndex; // Index of the current player's turn

    // Constructor initializes game components and starts a new game
    public Game(Player playerX, Player playerY) {
        board = new Board();
        scoreTracker = new ScoreTracker();
        startNewGame(playerX, playerY);
    }

    // Resets the game state and initializes players for a new game
    public void startNewGame(Player playerX, Player playerY) {
        board.reset();
        players = new Player[] `{playerX, playerY}`;
        currentPlayerIndex = 0;
    }

    // Processes a player's move, validates it, and updates game state
    public void makeMove(int colIndex, int rowIndex, Player player) {
        if (getGameStatus().equals(GameCondition.ENDED)) {
            throw new IllegalStateException("game ended");
        }
        if (players[currentPlayerIndex] != player) {
            throw new IllegalArgumentException("not the current player");
        }
        if (board.getPlayerAt(colIndex, rowIndex) != null) {
            throw new IllegalArgumentException("board position is taken");
        }
        board.updateBoard(colIndex, rowIndex, player);
        final Move newMove = new Move(colIndex, rowIndex, player);
        currentPlayerIndex = (currentPlayerIndex + 1) % players.length;
        if (getGameStatus().equals(GameCondition.ENDED)) {
            scoreTracker.reportGameResult(players[0], players[1], board.getWinner());
        }
    }

    // Determines if the game is in progress or has ended
    public GameCondition getGameStatus() {
        Optional<Player> winner = board.getWinner();
        if (winner.isPresent()) {
            return GameCondition.ENDED;
        }
        return board.isFull() ? GameCondition.ENDED : GameCondition.IN_PROGRESS;
    }

    // Returns the player whose turn it is
    public Player getCurrentPlayer() {
        return players[currentPlayerIndex];
    }

    // Returns the score tracker for accessing game statistics
    public ScoreTracker getScoreTracker() {
        return scoreTracker;
    }
}

  • makeMove(int colIndex, int rowIndex, Player player) : This method handles the player’s move by checking if the game is still ongoing, whether the correct player is making a move, and if the selected board position is empty. If all checks pass, the board is updated, and the turn is passed to the next player. If the move results in a win or a draw, the score tracker is updated accordingly.
  • getGameStatus() : Determines the game’s status by checking for a winner or a full board. Returns GameCondition.ENDED if either condition is met, otherwise GameCondition.IN_PROGRESS.

Implementation choice: The Game class uses a state machine-like approach to manage game flow, tracking the current player and game status (INPROGRESS or ENDED) with an enum (GameCondition). This was chosen for its simplicity and clarity in handling turn-based logic, as it ensures only one player moves at a time and the game stops when a win or draw occurs.

Board

The Board class represents the game board for a two-player Tic-Tac-Toe game. It uses a 3x3 grid to encapsulate the state of play within a 3x3 grid, where each position holds either a Player object or null if it's empty. The definition of the Board class is given below.

public class Board {
    // 3x3 grid to store player moves
    private final Player[][] grid = new Player[3][3];

    // Updates the board with a player's move at the specified position
    public void updateBoard(int colIndex, int rowIndex, Player player) {
        if (grid[colIndex][rowIndex] == null) {
            grid[colIndex][rowIndex] = player;
        }
    }

    // Checks for a winner by examining rows, columns, and diagonals
    public Optional<Player> getWinner() {
        // Check rows for three in a row
        for (int i = 0; i < grid.length; i++) {
            Player first = grid[i][0];
            if (first != null && Arrays.stream(grid[i]).allMatch(p -> p == first)) {
                return Optional.of(first);
            }
        }

        // Check columns for three in a column
        for (int j = 0; j < grid[0].length; j++) {
            final Player first = grid[0][j];
            int finalJ = j; // streams require a final object
            if (first != null && Arrays.stream(grid).allMatch(row -> row[finalJ] == first)) {
                return Optional.of(first);
            }
        }

        // Check main diagonal (top-left to bottom-right)
        Player topLeft = grid[0][0];
        if (topLeft != null
                && IntStream.range(0, grid.length).allMatch(i -> grid[i][i] == topLeft)) {
            return Optional.of(topLeft);
        }

        // Check anti-diagonal (top-right to bottom-left)
        Player topRight = grid[0][grid[0].length - 1];
        if (topRight != null
                && IntStream.range(0, grid.length)
                        .allMatch(i -> grid[i][grid[0].length - 1 - i] == topRight)) {
            return Optional.of(topRight);
        }

        // No winner found
        return Optional.empty();
    }

    // Checks if all positions on the board are filled
    public boolean isFull() {
        return Arrays.stream(grid).flatMap(Arrays::stream).noneMatch(Objects::isNull);
    }

    // Resets the board by clearing all positions
    public void reset() {
        for (Player[] players : grid) {
            Arrays.fill(players, null);
        }
    }

    // Returns the player at the specified position, or null if empty
    public Player getPlayerAt(int colIndex, int rowIndex) {
        return grid[colIndex][rowIndex];
    }
}

The class provides several methods to manage the game, including:

  • updateBoard(int colIndex, int rowIndex, Player player): Updates the board by placing the specified player's symbol at the given colIndex and rowIndex, provided the position is empty.
  • Optional getWinner(): This method checks for a winner by looking at the rows, columns, and diagonals. If any row, column, or diagonal contains the same symbol, the player is declared the winner.
  • Player getPlayerAt(int colIndex, int rowIndex): Returns the Player object at the specified board position, or null if the position is unoccupied.

Implementation choice: The Board uses a 2D array (Player[][]) to represent the 3x3 grid, chosen for its direct mapping to the game’s spatial structure and O(1) access time for reading or updating positions.

Player

The Player class represents a player in the game, with key attributes like their name and symbol. These attributes help identify the player and their move on the board.

public class Player {
    private final String name;
    private final char symbol;

    public Player(String name, char symbol) {
        this.name = name;
        this.symbol = symbol;
    }

    public String getName() {
        return name;
    }

    public char getSymbol() {
        return symbol;
    }
}

ScoreTracker

The ScoreTracker class manages player rankings across multiple Tic-Tac-Toe games by tracking and updating scores for a group of players. It uses a HashMap<Player, Integer> to store player ratings, where each Player is mapped to an integer score based on a simple victory count system. Here is the implementation of this class.

class ScoreTracker {
    // Stores player ratings in a map where key is player and value is their score
    private HashMap<Player, Integer> playerRatings = new HashMap<>();

    // This logic is customizable and, in reality, will use a complex ranking algorithm. For the
    // interview, we use a simple victory count system where the winner gets one point, the loser
    // loses a point, and no changes occur for a draw.
    public void reportGameResult(Player player1, Player player2, Optional<Player> winningPlayer) {
        if (winningPlayer.isPresent()) {
            Player winner = winningPlayer.get();
            Player loser = player1 == winner ? player2 : player1;
            playerRatings.putIfAbsent(winner, 0);
            playerRatings.put(winner, playerRatings.get(winner) + 1);
            playerRatings.putIfAbsent(loser, 0);
            playerRatings.put(loser, playerRatings.get(loser) - 1);
        }
    }

    // Returns a map of players sorted by their ratings in descending order
    public Map<Player, Integer> getTopPlayers() {
        return playerRatings.entrySet().stream()
                .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
                .map(Map.Entry::getKey)
                .collect(Collectors.toMap(player -> player, player -> playerRatings.get(player)));
    }

    // Returns the rank of a player based on their rating
    public int getRank(Player player) {
        List<Player> sortedPlayers =
                playerRatings.entrySet().stream()
                        .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
                        .map(Map.Entry::getKey)
                        .collect(Collectors.toList());
        return sortedPlayers.indexOf(player) + 1;
    }

    // getters are omitted for brevity
}

Implementation choice: The ScoreTracker uses a HashMap<Player, Integer> to store player ratings, which is chosen for its O(1) average-case lookup and update performance, making it ideal for frequent score updates and queries (e.g., top-ranked player).

Deep Dive Topic

At this point, you have met the basic requirements of the question. In this section, we will explore some potential extensions in more detail.

Implement undo functionality in Tic-Tac-Toe

Imagine you’re playing Tic-Tac-Toe, and you accidentally place your ‘X’ in the wrong spot. Wouldn’t it be great to hit an undo button and try again? Let’s dive into how we can implement undo functionality in a Tic-Tac-Toe game, step by step. Step 1: Track move history

  • Every time a player makes a move, we store that move so it can be undone later.
  • The Move class (which we've already designed as part of the main game logic) serves this purpose by capturing details such as rowIndex, colIndex, and the player.

Below is the representation of the Move class. Image represents a class diagram depicting a class named 'Move'.  The class is represented by a rectangular box with a large 'C' in a circle at the top-left corner, indicating it's a class.  To the right of the 'C', the name 'Move' is written, signifying the class's name. Inside the rectangular box, three attributes are listed, each prefixed with a square checkbox:  'int rowlndex', 'int collndex', and 'Player player'. These attributes represent integer variables storing row and column indices, respectively, and an object of a class named 'Player'.  There are no methods shown in this diagram, only the attributes of the class.  The diagram shows the structure of the 'Move' class, indicating that each instance of this class will hold information about a move in a game, specifying its row and column coordinates and the player who made the move.  No connections or information flow to or from other classes is depicted in this specific diagram. Step 2: Store Moves with a Stack

  • Since moves occur in a last-in, first-out (LIFO) order (i.e., the last move is undone first), we can use a stack (ArrayDeque).
  • When a move is made, we push it onto the stack.
  • When undo is requested, we pop the most recent move from the stack and revert the board to its previous state.

Below is the representation of the MoveHistory class, which records the move and implements the undo functionality. Image represents a class diagram for a `MoveHistory` class.  The diagram shows the class name 'MoveHistory' enclosed in a rectangular box, preceded by a 'C' symbol indicating it's a class. Inside the box, a private member variable `history` of type `ArrayDeque<Move>` is declared, suggesting it stores a sequence of 'Move' objects using an array deque data structure. Below this, three public methods are listed: `recordMove\(Move\)`, which presumably adds a new `Move` object to the `history`; `undoMove\(\)`, which returns and removes the last `Move` object from the `history`; and `clearHistory\(\)`, which likely empties the `history`.  No connections to other classes are shown, implying `MoveHistory` is self-contained, managing its internal `history` of `Move` objects through its defined methods. Here’s the code for the MoveHistory class:

class MoveHistory {
    // Stack-like structure to store moves in chronological order
    private final ArrayDeque<Move> history = new ArrayDeque<>();

    // Adds a new move to the history stack
    public void recordMove(Move move) {
        history.push(move);
    }

    // Removes and returns the most recent move from the history
    public Move undoMove() {
        return history.pop();
    }

    // Clears all moves from the history
    public void clearHistory() {
        history.clear();
    }
}

Step 3: Reverse the Board State The final step is to clear that spot on the board, and switch the current player back to whoever’s turn it was before. Here’s the updated code from the Game class:

public void makeMove(int colIndex, int rowIndex, Player player) {
    // Validate that game hasn't ended
    if (getGameStatus().equals(GameCondition.ENDED)) {
        throw new IllegalStateException("game ended");
    }
    // Validate that it's the correct player's turn
    if (players[currentPlayerIndex] != player) {
        throw new IllegalArgumentException("not the current player");
    }
    // Validate that the position is not already taken
    if (board.getPlayerAt(colIndex, rowIndex) != null) {
        throw new IllegalArgumentException("board position is taken");
    }
    // Update the board with the player's move
    board.updateBoard(colIndex, rowIndex, player);
    // Record the move in history
    final Move newMove = new Move(colIndex, rowIndex, player);
    moveHistory.recordMove(newMove);
    // Switch to the next player
    currentPlayerIndex = (currentPlayerIndex + 1) % players.length;
    // If game has ended, update the score
    if (getGameStatus().equals(GameCondition.ENDED)) {
        scoreTracker.reportGameResult(players[0], players[1], board.getWinner());
    }
}

// Reverts the last move made in the game
public void undoMove() {
    // Check if game has ended to prevent undoing after winner is reported
    if (getGameStatus().equals(GameCondition.ENDED)) {
        throw new IllegalStateException("game ended and winner already reported");
    }
    // Get the last move from history
    final Move lastMove = moveHistory.undoMove();

    // Update current player index to previous player
    if (currentPlayerIndex == 0) {
        currentPlayerIndex = players.length - 1;
    } else {
        currentPlayerIndex--;
    }

    // Clear the board position of the undone move
    board.updateBoard(lastMove.getColIndex(), lastMove.getRowIndex(), null);
}

What we discussed above is the essence of a well-known software design pattern called the Memento Pattern. Definition Definition: The Memento Pattern is a behavioral design pattern that allows an object to save and restore its previous state without exposing the details of its implementation. This pattern is useful in scenarios where undo or rollback functionality is needed, as it maintains a complete event history. In this pattern:

  • The Memento is an object that stores the state of another object at a specific point in time, acting as a snapshot. In our design, the Move class served as the Memento, capturing the state of a single move in the Tic-Tac-Toe game.
  • The Caretaker is responsible for storing and managing Memento objects, typically keeping them in a collection and providing mechanisms to save or retrieve them. The MoveHistory class played the role of the Caretaker, maintaining a stack of Move objects (Mementos) and offering an undoMove() method to retrieve the last move for reversal.
  • The Originator is the object whose state is being captured and restored. It creates Memento objects to save its state and can use them to restore a previous state. The Game class acted as the Originator, creating Move objects during each makeMove() call to capture the move’s state and using the undoMove() method to restore the game state by clearing the board position of the last move.

The UML diagram below illustrates this structure. Image represents a class diagram illustrating the design of a game using the Command and Memento design patterns.  The central class is `Game`, which contains a `Board`, a `MoveHistory`, an array of `Player` objects, and an integer representing the current player's index.  The `Game` class uses `makeMove` and `undoMove` methods to interact with the game state.  `MoveHistory` stores a history of `Move` objects using an `ArrayDeque`.  Each `Move` object contains the row and column index and the `Player` who made the move.  The `MoveHistory` class has methods to record moves, undo moves, and clear the history. The `Board` class has a 2D array of `Player` objects representing the game board and methods to update the board and retrieve players at specific positions.  The `Player` class has a name and a symbol.  A `Caretaker` \(represented as a note\) manages the `MoveHistory`, and a `Memento` \(also a note\) represents a snapshot of the game state.  The `Originator` \(a note\) is implied to be the `Game` class, responsible for creating and restoring game states.  Arrows indicate relationships between classes, showing how data flows and methods are called.  For example, `Game` uses `MoveHistory` and `Board`, and `MoveHistory` uses `Move`.  The `Move` class is linked to `Memento` suggesting it's used to store game state snapshots. Undo functionality using the memento pattern However, one potential challenge of using the Memento Pattern is memory overhead. Every time a move is made, we store the entire move in the history. In Tic-Tac-Toe, this isn't much of a problem because the board is small, but in more complex games with larger states, the memory required to store each memento could become a bottleneck. With our Tic-Tac-Toe game designed and implemented, let’s wrap up with key takeaways.

Wrap Up

In this chapter, we designed the Tic-Tac-Toe game. We started by gathering and clarifying the requirements through a series of questions and answers. Next, we identified the core objects involved, designed the class diagram, and implemented the key components of the game. A key takeaway from this design is the importance of modularity and clear separation of concerns. Each component, such as the Board, Game, Player, and ScoreTracker classes, focuses on a specific responsibility, ensuring the system is maintainable and easy to extend. In the deep dive section, we explored advanced topics, including using the Memento Pattern to handle undo functionality, enabling players to revert their moves while maintaining the integrity of the game state. Congratulations on getting this far! Now give yourself a pat on the back. Good job! Mark as Complete ask alexask alex expend In this chapter, we will explore the object-oriented design of a Tic-Tac-Toe game. We aim to create an interactive platform where two players alternate turns, placing their symbols on a virtual board. We’ll design key components such as the game board, player move tracking, outcome determination, and a score tracker for managing player ratings. How Tic-Tac-Toe Works: Tic-Tac-Toe is a classic two-player game played on a 3x3 grid. Each player selects a symbol ("X" or "O") and takes turns placing it in an empty cell. The goal is to align three identical symbols horizontally, vertically, or diagonally. The game concludes with a win if a player achieves this alignment, or a draw if all nine cells are filled without a winner. Let’s gather the key requirements through a mock interview scenario. Image represents a 3x3 tic-tac-toe board showing a completed game.  The board is composed of nine equal-sized squares arranged in a three-by-three grid, delineated by thick black lines. Each square contains either a bold, uppercase 'X' or a bold, uppercase 'O,' representing the moves of the two players. The arrangement of the symbols shows that 'X' has won the game, having three 'X's in a diagonal line from the top-left to the bottom-right.  There is no other information, such as URLs or parameters, present in the image; it solely depicts the final state of a tic-tac-toe game. Tic-Tac-Toe