Loading...

12-Hour Money-Back Guarantee

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

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

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

29 Jun 20246 min read

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.