Design (LLD) Minesweeper - Machine Coding

Features Required
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.
User Interaction
Allow the user to select a cell to reveal.
Allow the user to flag or unflag a cell as a suspected mine.
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.
Display
- Display the game board to the user, updating it after each move.
Design Patterns Involved
Singleton Pattern
- Used to manage the single instance of the game controller.
Factory Pattern
- Used to create different types of cells (mine, empty) on the game board.
Observer Pattern
- Used to notify the game view to update the display after any change in the game state.
Command Pattern
- Used to handle user actions such as reveal cell and flag/unflag cell.
Strategy Pattern
- Used to handle different algorithms for revealing cells.
Algorithms Involved
Random Mine Placement
- Algorithm to place mines randomly on the board.
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
Singleton Pattern: The
GameControllerclass 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.Factory Pattern: The
CellFactoryclass is used to create different types of cells (MineCellandEmptyCell). This abstracts the creation logic and makes it easier to manage cell types.Observer Pattern: The
GameViewclass implements theObserverinterface and updates the display whenever the game state changes. TheGameBoardclass maintains a list of observers and notifies them of any changes.Command Pattern: The
RevealCellCommandandFlagCellCommandclasses encapsulate the actions of revealing and flagging cells. These commands are executed by theGameControllerclass in response to user interactions.Strategy Pattern: The
RevealStrategyinterface and its implementationFloodFillStrategyhandle the logic for revealing adjacent cells. This allows for flexibility in changing the algorithm if needed.
