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
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.Factory Pattern: The
CellFactory
class is used to create different types of cells (MineCell
andEmptyCell
). This abstracts the creation logic and makes it easier to manage cell types.Observer Pattern: The
GameView
class implements theObserver
interface and updates the display whenever the game state changes. TheGameBoard
class maintains a list of observers and notifies them of any changes.Command Pattern: The
RevealCellCommand
andFlagCellCommand
classes encapsulate the actions of revealing and flagging cells. These commands are executed by theGameController
class in response to user interactions.Strategy Pattern: The
RevealStrategy
interface and its implementationFloodFillStrategy
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)
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.
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.
- The
Scalability Issues:
- The design may not scale well with very large boards due to potential performance bottlenecks in the reveal and flood fill logic.
Single-Threaded Limitation:
- The design is single-threaded and might not handle more complex, multi-threaded scenarios or animations smoothly.
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.
Persistence - User Cannot save or load game later:
- Add functionality to save and load game states, allowing users to resume their games later.
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.
Multi-Board Support
Complete Working Code in IDE
Soon will add YouTube video on this channel - https://www.youtube.com/channel/UCgdIgkU_hq0VtGPhVPA0CPQ