Skip to main content

Minesweeper

A grid, some hidden mines, and one reveal operation that has to cascade outward on its own. The class model is small; the trap is writing that cascade recursively and finding out on a 30x30 board that the call stack disagrees.

Requirements

Functional

  • An R x C board is seeded with K mines at random positions before the first move.
  • Revealing a cell that holds a mine ends the game as a loss.
  • Revealing a cell with zero adjacent mines automatically reveals its neighbors, and theirs, and so on, stopping at the first ring of cells that do have an adjacent mine.
  • Revealing every non-mine cell wins the game. Cells can be flagged and unflagged, and a flagged cell cannot be revealed until unflagged.

Non-functional

  • The cascade in requirement three must not recurse - a board with thousands of empty cells chained together should not risk a stack overflow.
  • Checking "has the player won yet" should not re-scan the whole board on every single reveal once the board is large.

Design

Cell is deliberately dumb - a bag of four booleans/counters (isMine, revealed, flagged, adjacentMines) with no idea what a neighbor is. All the graph-shaped work - computing adjacency counts, walking the flood-fill frontier - lives in Board, using an explicit queue instead of recursion so the non-functional requirement is structural, not a matter of remembering to be careful.

PlayerGameBoardCellreveal(row, col)1reveal(row, col)2isMine()3floodFill(row, col)4reveal()5isWon()6
  1. 1The player only ever calls reveal on the game facade.
  2. 2Game forwards the coordinates without inspecting board internals itself.
  3. 3If this one cell is a mine, the board can return LOSE immediately.
  4. 4Otherwise the board walks an explicit queue of zero-adjacency cells outward.
  5. 5Each cell touched by the flood fill flips its own revealed flag - it does not know why it was reached.
  6. 6After the reveal settles, the game asks whether every non-mine cell is now revealed.

reveal returns a result rather than mutating some shared "game over" flag - the caller (Game) decides what a LOSE or WIN means for the UI; Board just reports what happened.

Class diagram

Game- board: Board+ reveal(row, col): RevealResult+ toggleFlag(row, col): voidBoard- cells: Cell[][]- rows: int- cols: int- mineCount: int+ reveal(row, col): RevealResult+ isWon(): boolCell- isMine: bool- revealed: bool- flagged: bool- adjacentMines: int+ reveal(): void+ toggleFlag(): voidRevealResultCONTINUEWINLOSE
creates
Cell holds state only. Board owns every rule that needs to look at more than one cell.

Code

import java.util.*;
 
enum RevealResult { CONTINUE, WIN, LOSE }
 
class Cell {
boolean isMine;
boolean revealed;
boolean flagged;
int adjacentMines;
}
 
class Board {
private final Cell[][] cells;
private final int rows;
private final int cols;
private final int mineCount;
private int revealedCount = 0;
 
Board(int rows, int cols, int mineCount) {
this.rows = rows;
this.cols = cols;
this.mineCount = mineCount;
this.cells = new Cell[rows][cols];
for (int r = 0; r < rows; r++)
for (int c = 0; c < cols; c++)
cells[r][c] = new Cell();
placeMines();
computeAdjacency();
}
 
private void placeMines() {
Random random = new Random();
int placed = 0;
while (placed < mineCount) {
int r = random.nextInt(rows);
int c = random.nextInt(cols);
if (!cells[r][c].isMine) {
cells[r][c].isMine = true;
placed++;
}
}
}
 
private void computeAdjacency() {
for (int r = 0; r < rows; r++)
for (int c = 0; c < cols; c++)
cells[r][c].adjacentMines = countAdjacentMines(r, c);
}
 
private int countAdjacentMines(int row, int col) {
int count = 0;
for (int dr = -1; dr <= 1; dr++)
for (int dc = -1; dc <= 1; dc++) {
int r = row + dr, c = col + dc;
if (inBounds(r, c) && cells[r][c].isMine) count++;
}
return count;
}
 
private boolean inBounds(int r, int c) {
return r >= 0 && r < rows && c >= 0 && c < cols;
}
 
void toggleFlag(int row, int col) {
Cell cell = cells[row][col];
if (!cell.revealed) cell.flagged = !cell.flagged;
}
 
RevealResult reveal(int row, int col) {
Cell start = cells[row][col];
if (start.flagged || start.revealed) return RevealResult.CONTINUE;
if (start.isMine) {
start.revealed = true;
return RevealResult.LOSE;
}
floodFill(row, col);
return isWon() ? RevealResult.WIN : RevealResult.CONTINUE;
}
 
private void floodFill(int startRow, int startCol) {
Deque<int[]> queue = new ArrayDeque<>();
queue.add(new int[]{startRow, startCol});
while (!queue.isEmpty()) {
int[] pos = queue.poll();
Cell cell = cells[pos[0]][pos[1]];
if (cell.revealed || cell.flagged) continue;
cell.revealed = true;
revealedCount++;
if (cell.adjacentMines == 0) {
for (int dr = -1; dr <= 1; dr++)
for (int dc = -1; dc <= 1; dc++) {
int r = pos[0] + dr, c = pos[1] + dc;
if (inBounds(r, c) && !cells[r][c].revealed) queue.add(new int[]{r, c});
}
}
}
}
 
boolean isWon() {
return revealedCount == rows * cols - mineCount;
}
}
 
class Game {
private final Board board;
 
Game(Board board) {
this.board = board;
}
 
RevealResult reveal(int row, int col) {
return board.reveal(row, col);
}
 
void toggleFlag(int row, int col) {
board.toggleFlag(row, col);
}
}

Design decisions

  • Flood fill is an explicit BFS queue, not recursion. A recursive revealNeighbors reads cleaner, but its call depth is bounded by however many empty cells happen to chain together
    • on a large board that's an unbounded, data-dependent stack depth. A queue keeps memory on the heap where it's supposed to be.
  • Cell has zero neighbor-awareness. It would be tempting to give a cell a list of neighbor references so it can compute its own adjacentMines. That couples every cell to the board's dimensions and makes Cell impossible to unit-test in isolation. Board computing offsets and counting is one method, done once, in one place.
  • reveal returns a RevealResult enum instead of throwing or setting a flag. Loss and win are ordinary outcomes of this operation, not exceptional ones, and a caller checking a return value doesn't need to know about Game's internal state to react correctly.
  • What's missing for a real system: a running "cells remaining" counter maintained incrementally (instead of a linear win-check scan) is the obvious optimization once boards get large, and a real client also wants first-click safety - guaranteeing the very first reveal is never a mine, which means deferring mine placement until after that first click instead of seeding the board up front.
0%0 of 122 pages studied