Low Level Design: Tic-Tac-Toe Game

December 13, 2025

Design a Tic-Tac-Toe game for two players that allows them to play on a 3x3 grid, take alternate turns, validate moves, and determine the winner or a draw based on the game state.

tic-tac-toe


Functional Requirements

  1. Assign each player a unique symbol (e.g., X and O) at the start of the game.

  2. Enforce alternating turns between players and always indicate the current player's turn.

  3. Allow players to make a move by selecting a cell (row, column), ensuring the cell is not already occupied, and update the board accordingly.

  4. Allow players to undo the last move, restoring the previous board state and switching the turn back to the respective player.

  5. Check after every move whether the current player has won by completing a row, column, or diagonal.

  6. Detect a draw when all cells are filled and no player has won.

  7. Keep track of the score for each player.

XPlayer 10
OPlayer 20
Player X's turn

Primary Use Cases

The primary use cases involved in the Tic-Tac-Toe game include players making moves on the board and optionally undoing previous moves, with the system responsible for maintaining board state, enforcing turn order, and determining game outcomes such as win or draw.

Use Case 1: Move a New Move

  1. A player makes a move by selecting a cell using a [row, column] coordinate on the board.

  2. The system validates if the selected cell is not already occupied. If valid, it updates the cell with the player's symbol.

  3. Next, the system evaluates whether the move results in a win or draw. If the game is still in progress, the turn is switched to the next player.

  4. If a player wins, the system updates the player's score to reflect the victory.

Use Case 2: Undo a Previous Move

  1. A player requests to undo the last move.

  2. The system retrieves the most recent move from the move history and reverts the board state by clearing that move.

  3. Next, the system switches the turn back to the player who made the undone move and updates the player's score if necessary.


Object-Model Design

The primary entities in the system include:

Game

The Game class is responsible for validating and coordinating each player's move, swithing turns, updating the board state, and tracking each player's wins.

It maintains the core state required to run the game, include the list of players, the current player's turn, the board instance, and a history of moves to support undo functionality;

In order to adhere to the Single Responsibility Principle, it should not handle low-level operations such as validating moves and updating the board state. Instead, it should delegate these responsibilities to the Board class, ensuring a clean separation of concerns and better maintainability.

class Game {

    Player[] players;
    Board board;
    int currentPlayerIndex;
    Stack<Move> moveHistory;

    Game() {
        this.players = new Player[]{new Player("Bob", "O"), new Player("Alice", "X")};
        this.board = new Board();
        this.currentPlayerIndex = 0;
        this.moveHistory = new ArrayDeque<>();
    }

    public void playMove(int row, int column) {
        Player player = players[currentPlayerIndex];

        // Step 1: Validate Move
        if(!board.isValidMove(row, column)) {
            System.out.println("Invalid Move!");
            return;
        }

        // Step 2: Update board state
        board.makeMove(row, column, player.symbol);

        // Step 3: Record move in move history
        moveHistory.push(new Move(row, column, player));

        // Step 4: Check for a win
        if(board.isWinningMove(row, column, player.symbol)) {
            player.incrementWins();
            return;
        }

        // Step 5: Check for a draw
        if(board.isfull()) {
            System.out.println("Draw!");
            return;
        }

        // Step 6: Switch turn to next player
        switchTurn();
    }

    public void undo() {
        if(moveHistory.isEmpty()) {
            System.out.println("No moves to undo!");
            return;
        }

        // Step 1: Retrieve the last move
        Move lastMove = moveHistory.pop();
        
        // Step 2: Revert the board state
        board.undoMove(lastMove.row, lastMove.column);

        // Step 3: Switch turn back to the previous player
        switchTurn();
    }

    private void switchTurn() {
        currentPlayerIndex = (currentPlayerIndex + 1) % 2;
    }

}

Player

The Player class represents a participant in the game. It serves as a data holder with limited behavior, typically including attributes such as the player's name, assigned symbol (e.g., X or O), and score (number of wins).

class Player {

    String name;
    Symbol symbol;
    int wins;

    Person(String name, Symbol symbol) {
        this.name = name;
        this.symbol = symbol;
    }

    public void incrementWins() {
        wins++;
    }

}

Board

The Board class represents the core data structure of the game. It encapsulates all board-related operations, such as validating whether a move is allowed, updating the state of a cell when a move is made, and reverting a move when an undo is requested.

NOTE: Since each cell only needs to store a Symbol, we can use a Symbol[][] for simplicity and efficiency. We can introduce a Cell abstraction only if additional metadata or behavior per cell is required.

enum Symbol {
    X,
    O,
    EMPTY // represents unoccupied cells
}

class Board {

    private static final int SIZE = 3;
    private Symbol[][] grid;


    public Board() {
        this.grid = new Symbol[SIZE][SIZE];

        // Initialize all cells to EMPTY
        for(int i = 0; i < SIZE; i++) {
            for(int j = 0; j < SIZE; j++) {
                grid[i][j] = Symbol.EMPTY;
            }
        }
    }

    // Validate if cell is empty
    public boolean isValidMove(int row, int col) {
        return grid[row][col] == Symbol.EMPTY;
    }

    // Apply move
    public boolean makeMove(int row, int col, Symbol symbol) {
        grid[row][col] = symbol;
    }

    // Undo move
    public void undoMove(int row, int col) {
        grid[row][col] = Symbol.EMPTY;
    }

    // Check if board is full
    public boolean isFull() {
        for (int i = 0; i < SIZE; i++) {
            for (int j = 0; j < SIZE; j++) {
                if (grid[i][j] == Symbol.EMPTY) return false;
            }
        }
        return true;
    }

    // Check if the move is a winning move
    public boolean isWinningMove(int row, int col, Symbol symbol) {

        // Check row
        boolean isWinningRow = true;
        for (int j = 0; j < SIZE; j++) {
            if (grid[row][j] != symbol) {
                isWinningRow = false;
                break;
            }
        }

        // Check column
        boolean isWinningColumn = true;
        for (int i = 0; i < SIZE; i++) {
            if (grid[i][col] != symbol) {
                isWinningColumn = false;
                break;
            }
        }

        // Check diagonal
        boolean isWinningDiagonal = true;
        if (row == col) {
            for (int i = 0; i < SIZE; i++) {
                if (grid[i][i] != symbol) {
                    isWinningDiagonal = false;
                    break;
                }
            }
        } else {
            isWinningDiagonal = false;
        }

        // Check Anti-Diagonal
        boolean isWinningAntiDiagonal = true;
        if (row + col == SIZE - 1) {
            for(int i = 0; i < SIZE; i++) {
                if (grid[i][SIZE - i - 1] != symbol) {
                    isWinningAntiDiagonal = false;
                    break;
                }
            }
        } else {
            isWinningAntiDiagonal = false;
        }

        return isWinningRow || isWinningColumn || isWinningDiagonal || isWinningAntiDiagonal;

    }

}

Move

The Move class represents a single action taken by a player during the game. It encapsulates the minimal information required to describe an action: the row and column of the selected cell, and the Player who made the move.

One of the primary reasons for introducing the Move class is to support undo functionality.

class Move {
    
    int row;
    int col;
    Player player; // what is the need for Player object here...

    Move(int row, int col, Player player) {
        this.row = row;
        this.col = col;
        this.player = player;
    }

}

Final UML Diagram