Try Our Course for 4 Hours @99rs

Try our course for 4 Hours and if you like it, you can go for one year or lifetime access. If you buy our (1yr or lifetime) course 99rs will be refunded !

Design (LLD) Minesweeper - Machine Coding

Design (LLD) Minesweeper - Machine Coding

GitHub - Pierre-Monier/minesweeper: You can play the game here

Features Required

  1. Game Board Initialization

    • Initialize a game board with a given number of rows, columns, and mines.

    • Ensure that mines are placed randomly without clustering too many in one area.

  2. User Interaction

    • Allow the user to select a cell to reveal.

    • Allow the user to flag or unflag a cell as a suspected mine.

  3. Game Logic

    • Check the status of the game (win/loss) after each move.

    • If a mine is revealed, the game is lost.

    • If all non-mine cells are revealed, the game is won.

    • Reveal all adjacent non-mine cells if an empty cell is selected.

  4. Display

    • Display the game board to the user, updating it after each move.

Design Patterns Involved

  1. Singleton Pattern

    • Used to manage the single instance of the game controller.
  2. Factory Pattern

    • Used to create different types of cells (mine, empty) on the game board.
  3. Observer Pattern

    • Used to notify the game view to update the display after any change in the game state.
  4. Command Pattern

    • Used to handle user actions such as reveal cell and flag/unflag cell.
  5. Strategy Pattern

    • Used to handle different algorithms for revealing cells.

Algorithms Involved

  1. Random Mine Placement

    • Algorithm to place mines randomly on the board.
  2. Flood Fill Algorithm

    • Used to reveal all adjacent non-mine cells when an empty cell is selected.

Diagram

Code (Java)

Singleton Pattern for Game Controller

public class GameController {
    private static GameController instance;
    private GameBoard gameBoard;
    private GameView gameView;

    private GameController(int rows, int cols, int mines) {
        gameBoard = new GameBoard(rows, cols, mines);
        gameView = new GameView(gameBoard);
    }

    public static GameController getInstance(int rows, int cols, int mines) {
        if (instance == null) {
            instance = new GameController(rows, cols, mines);
        }
        return instance;
    }

    public void startGame() {
        gameView.displayBoard();
        // Handle user interactions (Example, this can be extended as needed)
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.println("Enter command (r for reveal, f for flag) and coordinates (row col): ");
            String command = scanner.next();
            int row = scanner.nextInt();
            int col = scanner.nextInt();

            if (command.equals("r")) {
                new RevealCellCommand(this, row, col).execute();
            } else if (command.equals("f")) {
                new FlagCellCommand(this, row, col).execute();
            }

            if (gameBoard.isGameOver()) {
                System.out.println("Game Over");
                break;
            }
        }
        scanner.close();
    }

    public void revealCell(int row, int col) {
        gameBoard.revealCell(row, col);
        gameView.update();
    }

    public void flagCell(int row, int col) {
        gameBoard.flagCell(row, col);
        gameView.update();
    }
}

Factory Pattern for Creating Cells

abstract class Cell {
    protected boolean isRevealed;
    protected boolean isFlagged;

    public abstract void reveal();

    public void flag() {
        isFlagged = !isFlagged;
    }

    public boolean isRevealed() {
        return isRevealed;
    }

    public boolean isFlagged() {
        return isFlagged;
    }
}

class MineCell extends Cell {
    @Override
    public void reveal() {
        isRevealed = true;
        // Game over logic (can be extended as needed)
    }
}

class EmptyCell extends Cell {
    private int adjacentMines;

    public EmptyCell(int adjacentMines) {
        this.adjacentMines = adjacentMines;
    }

    @Override
    public void reveal() {
        isRevealed = true;
        if (adjacentMines == 0) {
            RevealStrategy strategy = new FloodFillStrategy();
            strategy.revealAdjacentCells(this);
        }
    }

    public int getAdjacentMines() {
        return adjacentMines;
    }
}

class CellFactory {
    public static Cell createCell(boolean isMine, int adjacentMines) {
        if (isMine) {
            return new MineCell();
        } else {
            return new EmptyCell(adjacentMines);
        }
    }
}

Observer Pattern for Updating the Game View

interface Observer {
    void update();
}

class GameView implements Observer {
    private GameBoard gameBoard;

    public GameView(GameBoard gameBoard) {
        this.gameBoard = gameBoard;
        gameBoard.addObserver(this);
    }

    public void displayBoard() {
        // Display the initial game board (can be extended as needed)
    }

    @Override
    public void update() {
        // Update the display after any change in the game state (can be extended as needed)
    }
}

Command Pattern for Handling User Actions

interface Command {
    void execute();
}

class RevealCellCommand implements Command {
    private GameController gameController;
    private int row, col;

    public RevealCellCommand(GameController gameController, int row, int col) {
        this.gameController = gameController;
        this.row = row;
        this.col = col;
    }

    @Override
    public void execute() {
        gameController.revealCell(row, col);
    }
}

class FlagCellCommand implements Command {
    private GameController gameController;
    private int row, col;

    public FlagCellCommand(GameController gameController, int row, int col) {
        this.gameController = gameController;
        this.row = row;
        this.col = col;
    }

    @Override
    public void execute() {
        gameController.flagCell(row, col);
    }
}

Strategy Pattern for Revealing Adjacent Cells

interface RevealStrategy {
    void revealAdjacentCells(Cell cell);
}

class FloodFillStrategy implements RevealStrategy {
    @Override
    public void revealAdjacentCells(Cell cell) {
        // Implement flood fill algorithm to reveal adjacent cells
    }
}

Game Board to Manage the State of the Game

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

class GameBoard {
    private Cell[][] board;
    private List<Observer> observers = new ArrayList<>();
    private boolean gameOver;

    public GameBoard(int rows, int cols, int mines) {
        initializeBoard(rows, cols, mines);
    }

    private void initializeBoard(int rows, int cols, int mines) {
        board = new Cell[rows][cols];
        placeMines(rows, cols, mines);
        calculateAdjacentMines(rows, cols);
    }

    private void placeMines(int rows, int cols, int mines) {
        Random random = new Random();
        int placedMines = 0;

        while (placedMines < mines) {
            int row = random.nextInt(rows);
            int col = random.nextInt(cols);
            if (board[row][col] == null) {
                board[row][col] = CellFactory.createCell(true, 0);
                placedMines++;
            }
        }

        for (int row = 0; row < rows; row++) {
            for (int col = 0; col < cols; col++) {
                if (board[row][col] == null) {
                    board[row][col] = CellFactory.createCell(false, 0);
                }
            }
        }
    }

    private void calculateAdjacentMines(int rows, int cols) {
        int[] directions = {-1, 0, 1};

        for (int row = 0; row < rows; row++) {
            for (int col = 0; col < cols; col++) {
                if (board[row][col] instanceof EmptyCell) {
                    int adjacentMines = 0;
                    for (int dr : directions) {
                        for (int dc : directions) {
                            if (dr == 0 && dc == 0) continue;
                            int newRow = row + dr;
                            int newCol = col + dc;
                            if (newRow >= 0 && newRow < rows && newCol >= 0 && newCol < cols) {
                                if (board[newRow][newCol] instanceof MineCell) {
                                    adjacentMines++;
                                }
                            }
                        }
                    }
                    ((EmptyCell) board[row][col]).setAdjacentMines(adjacentMines);
                }
            }
        }
    }

    public void revealCell(int row, int col) {
        if (!gameOver && !board[row][col].isRevealed()) {
            board[row][col].reveal();
            if (board[row][col] instanceof MineCell) {
                gameOver = true;
            }
            notifyObservers();
        }
    }

    public void flagCell(int row, int col) {
        if (!gameOver && !board[row][col].isRevealed()) {
            board[row][col].flag();
            notifyObservers();
        }
    }

    public void addObserver(Observer observer) {
        observers.add(observer);
    }

    private void notifyObservers() {
        for (Observer observer : observers) {
            observer.update();
        }
    }

    public boolean isGameOver() {
        return gameOver;
    }

    public Cell getCell(int row, int col) {
        return board[row][col];
    }
}

Main Class to Start the Game

import java.util.Scanner;

public class MinesweeperGame {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("Enter the number of rows, columns, and mines:");
        int rows = scanner.nextInt();
        int cols = scanner.nextInt();
        int mines = scanner.nextInt();

        GameController gameController = GameController.getInstance(rows, cols, mines);
        gameController.startGame();
    }
}

Additional Methods for EmptyCell

class EmptyCell extends Cell {
    private int adjacentMines;

    public EmptyCell(int adjacentMines) {
        this.adjacentMines = adjacentMines;
    }

    @Override
    public void reveal() {
        isRevealed = true;
        if (adjacentMines == 0) {
            RevealStrategy strategy = new FloodFillStrategy();
            strategy.revealAdjacentCells(this);
        }
    }

    public int getAdjacentMines() {
        return adjacentMines;
    }

    public void setAdjacentMines(int adjacentMines) {
        this.adjacentMines = adjacentMines;
    }
}

Implementation of Flood Fill Strategy

class FloodFillStrategy implements RevealStrategy {
    @Override
    public void revealAdjacentCells(Cell cell) {
        // Assuming we have access to the GameBoard and its methods
        GameBoard gameBoard = GameController.getInstance(0, 0, 0).get

GameBoard();
        int[] directions = {-1, 0, 1};

        for (int dr : directions) {
            for (int dc : directions) {
                if (dr == 0 && dc == 0) continue;
                int newRow = row + dr;
                int newCol = col + dc;
                if (newRow >= 0 && newRow < gameBoard.getRows() && newCol >= 0 && newCol < gameBoard.getCols()) {
                    Cell adjacentCell = gameBoard.getCell(newRow, newCol);
                    if (!adjacentCell.isRevealed() && !adjacentCell.isFlagged()) {
                        adjacentCell.reveal();
                    }
                }
            }
        }
    }
}

Detailed Implementation Explanation

  1. Singleton Pattern: The GameController class ensures that only one instance of the controller exists. It initializes the game board and the view, and provides methods for starting the game, revealing cells, and flagging cells.

  2. Factory Pattern: The CellFactory class is used to create different types of cells (MineCell and EmptyCell). This abstracts the creation logic and makes it easier to manage cell types.

  3. Observer Pattern: The GameView class implements the Observer interface and updates the display whenever the game state changes. The GameBoard class maintains a list of observers and notifies them of any changes.

  4. Command Pattern: The RevealCellCommand and FlagCellCommand classes encapsulate the actions of revealing and flagging cells. These commands are executed by the GameController class in response to user interactions.

  5. Strategy Pattern: The RevealStrategy interface and its implementation FloodFillStrategy handle the logic for revealing adjacent cells. This allows for flexibility in changing the algorithm if needed.

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

  1. Incomplete Game Over Handling:

    • The current implementation of game over logic is rudimentary. If a mine is revealed, the game is set to over, but there is no mechanism to reveal all mines or provide a game-over message to the user.
  2. Revealing Adjacent Cells Logic:

    • The FloodFillStrategy class does not correctly access the game board and lacks the implementation to handle the flood fill logic properly. It needs proper access to the cell's coordinates and the game board.
  3. Scalability Issues:

    • The design may not scale well with very large boards due to potential performance bottlenecks in the reveal and flood fill logic.
  4. Single-Threaded Limitation:

    • The design is single-threaded and might not handle more complex, multi-threaded scenarios or animations smoothly.
  5. User Cannot Select Difficulty Levels:

    • Introduce different difficulty levels with varying board sizes and the number of mines.

    • Allow the user to select the difficulty level at the start of the game.

  6. Persistence - User Cannot save or load game later:

    • Add functionality to save and load game states, allowing users to resume their games later.
  7. Enhanced User Feedback:

    • Provide more detailed feedback to the user, such as the number of remaining mines, a timer, and a move counter.

    • Display hints or tips for new players.

  8. Multi-Board Support

Complete Working Code in IDE

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