Design (LLD) Tic-Tac-Toe - Machine Coding

Design (LLD) Tic-Tac-Toe - Machine Coding

Features Required:

  1. Initialization: Ability to initialize a new game.

  2. Move Validation: Check if a move is valid.

  3. Board Update: Update the board with the player's move.

  4. Win Condition: Check for a win condition.

  5. Draw Condition: Check for a draw condition.

  6. Player Switching: Switch between players after each move.

Design Patterns Involved:

  1. Singleton Pattern: To ensure there is only one instance of the game controller.

  2. Factory Pattern: To create player instances.

  3. Observer Pattern: To notify players of the game state changes.

  4. Strategy Pattern: To encapsulate the algorithm for move validation and win/draw condition checking.

Diagram

Detailed Implementation:

Singleton Pattern:

We will use the Singleton pattern to ensure that there is only one instance of the game controller.

public class GameController {
    private static GameController instance;

    private GameController() {
        // private constructor to prevent instantiation
    }

    public static GameController getInstance() {
        if (instance == null) {
            instance = new GameController();
        }
        return instance;
    }
}

Factory Pattern:

We will use the Factory pattern to create player instances.

public abstract class Player {
    protected char symbol;

    public char getSymbol() {
        return symbol;
    }

    public abstract void makeMove(Board board);
}

public class PlayerFactory {
    public static Player createPlayer(char symbol) {
        return new HumanPlayer(symbol);
    }
}

public class HumanPlayer extends Player {
    public HumanPlayer(char symbol) {
        this.symbol = symbol;
    }

    @Override
    public void makeMove(Board board) {
        // Implementation for making a move
    }

    public void update(Board board) {
        // Implementation to update player with board state
    }
}

Observer Pattern:

We will use the Observer pattern to notify players of the game state changes.

import java.util.ArrayList;
import java.util.List;

public class Board {
    private char[][] board;
    private List<Player> observers = new ArrayList<>();

    public Board(int size) {
        board = new char[size][size];
    }

    public void addObserver(Player player) {
        observers.add(player);
    }

    public void notifyObservers() {
        for (Player player : observers) {
            player.update(this);
        }
    }

    public void updateBoard(int x, int y, char symbol) {
        board[x][y] = symbol;
        notifyObservers();
    }

    public char getCell(int x, int y) {
        return board[x][y];
    }

    public int getSize() {
        return board.length;
    }
}

Strategy Pattern:

We will use the Strategy pattern to encapsulate the algorithms for move validation and win/draw condition checking.

public interface MoveStrategy {
    boolean isValidMove(Board board, int x, int y);
}

public interface WinStrategy {
    boolean checkWin(Board board, char symbol);
}

public class DefaultMoveStrategy implements MoveStrategy {
    @Override
    public boolean isValidMove(Board board, int x, int y) {
        return board.getCell(x, y) == '\0';
    }
}

public class DefaultWinStrategy implements WinStrategy {
    @Override
    public boolean checkWin(Board board, char symbol) {
        int size = board.getSize();
        // Check rows and columns
        for (int i = 0; i < size; i++) {
            if (checkRow(board, symbol, i) || checkColumn(board, symbol, i)) {
                return true;
            }
        }
        // Check diagonals
        return checkDiagonal(board, symbol) || checkAntiDiagonal(board, symbol);
    }

    private boolean checkRow(Board board, char symbol, int row) {
        for (int i = 0; i < board.getSize(); i++) {
            if (board.getCell(row, i) != symbol) {
                return false;
            }
        }
        return true;
    }

    private boolean checkColumn(Board board, char symbol, int col) {
        for (int i = 0; i < board.getSize(); i++) {
            if (board.getCell(i, col) != symbol) {
                return false;
            }
        }
        return true;
    }

    private boolean checkDiagonal(Board board, char symbol) {
        for (int i = 0; i < board.getSize(); i++) {
            if (board.getCell(i, i) != symbol) {
                return false;
            }
        }
        return true;
    }

    private boolean checkAntiDiagonal(Board board, char symbol) {
        int size = board.getSize();
        for (int i = 0; i < size; i++) {
            if (board.getCell(i, size - 1 - i) != symbol) {
                return false;
            }
        }
        return true;
    }
}

Complete Implementation:

import java.util.Scanner;

public class TicTacToe {
    private Board board;
    private Player currentPlayer;
    private Player player1;
    private Player player2;
    private MoveStrategy moveStrategy;
    private WinStrategy winStrategy;

    public TicTacToe() {
        GameController gameController = GameController.getInstance();
        board = new Board(3);
        player1 = PlayerFactory.createPlayer('X');
        player2 = PlayerFactory.createPlayer('O');
        currentPlayer = player1;
        moveStrategy = new DefaultMoveStrategy();
        winStrategy = new DefaultWinStrategy();
        board.addObserver(player1);
        board.addObserver(player2);
    }

    public void playGame() {
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.println("Player " + currentPlayer.getSymbol() + "'s turn");
            System.out.println("Enter row (0, 1, or 2): ");
            int x = scanner.nextInt();
            System.out.println("Enter column (0, 1, or 2): ");
            int y = scanner.nextInt();

            // Validate move
            if (moveStrategy.isValidMove(board, x, y)) {
                // Update board
                board.updateBoard(x, y, currentPlayer.getSymbol());

                // Check for win
                if (winStrategy.checkWin(board, currentPlayer.getSymbol())) {
                    System.out.println("Player " + currentPlayer.getSymbol() + " wins!");
                    break;
                }

                // Check for draw
                if (isDraw()) {
                    System.out.println("Game is a draw!");
                    break;
                }

                // Switch player
                switchPlayer();
            } else {
                System.out.println("Invalid move! Try again.");
            }
        }
        scanner.close();
    }

    private void switchPlayer() {
        currentPlayer = (currentPlayer == player1) ? player2 : player1;
    }

    private boolean isDraw() {
        for (int i = 0; i < board.getSize(); i++) {
            for (int j = 0; j < board.getSize(); j++) {
                if (board.getCell(i, j) == '\0') {
                    return false;
                }
            }
        }
        return true;
    }

    public static void main(String[] args) {
        TicTacToe game = new TicTacToe();
        game.playGame();
    }
}

Explanation:

  1. GameController: Ensures a single instance of the game controller using the Singleton pattern.

  2. Player and PlayerFactory: Creates player instances using the Factory pattern. HumanPlayer extends Player and implements the makeMove method.

  3. Board: Manages the game board and uses the Observer pattern to notify players of state changes.

  4. MoveStrategy and WinStrategy: Encapsulate move validation and win condition checking using the Strategy pattern. DefaultMoveStrategy checks if a move is valid, and DefaultWinStrategy checks for win conditions.

  5. TicTacToe: The main game class initializes the board, players, and strategies, and contains the game loop to handle player moves, check for wins/draws, and switch players.

Issues in the Above Design (Covered in our premium course)

  1. Limited to Human Players:

    • The current design only supports human players. It lacks the flexibility to easily incorporate AI players or networked multiplayer functionality.
  2. Hardcoded Board Size:

    • The board size is hardcoded to 3x3. This restricts the game to traditional Tic-Tac-Toe and does not support variations like 4x4 or larger grids.
  3. Game Modes:

    • Above design does not support multiple game modes like human vs AI or AI vs AI etc.
  4. Multiple Game:

    • Above design does not support multiple Tic-Tac-Toe games simultaneously.
  5. Lack of Clear Separation of Concerns:

    • The TicTacToe class handles multiple responsibilities, including game control, input handling, and player switching, which can make the code harder to maintain. Similarly Some Other classes also.

Soon will add YouTube video on this channel - https://www.youtube.com/channel/UCgdIgkU_hq0VtGPhVPA0CPQ

Did you find this article valuable?

Support Subhahu Jain by becoming a sponsor. Any amount is appreciated!