Engineering Atlas › Low-Level Design

Code a minesweeper game

13 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

Requirements

Functional:

Non-functional:

Out of scope: graphics, high-score servers, hexagonal grids, multiplayer. Mention them in Extensibility.

Entities and relationships

Relationships: a game composes one board; a board composes its cells. The game uses a placer and a clock, both injected. Observers are aggregated and outlive nothing.

Class diagram

classDiagram
    class Game {
      -board: Board
      -status: Status
      -placer: MinePlacer
      -clock: Clock
      -observers: GameObserver[]
      +reveal(r, c) Status
      +toggleFlag(r, c) bool
      +chord(r, c) Status
      +minesLeft() int
      +elapsed() int
    }
    class Board {
      +rows: int
      +cols: int
      -mine: bool[]
      -count: byte[]
      -state: CellState[]
      -opened: int
      +placeMines(cells)
      +flood(index) int[]
      +neighbours(index) int[]
    }
    class CellState {
      <<enumeration>>
      HIDDEN
      FLAGGED
      REVEALED
    }
    class MinePlacer {
      <<interface>>
      +place(rows, cols, mines, firstClick) int[]
    }
    class GameObserver {
      <<interface>>
      +onCells(indices)
      +onStatus(status)
    }
    class Clock {
      <<interface>>
      +now() int
    }
    Game "1" *-- "1" Board
    Board --> CellState
    Game --> MinePlacer
    Game --> Clock
    Game "1" o-- "many" GameObserver
    RandomPlacer ..|> MinePlacer
    FixedPlacer ..|> MinePlacer
    NoGuessPlacer ..|> MinePlacer

Key flows

The interesting flow is the first reveal, because it places the mines lazily and starts the clock.

sequenceDiagram
    participant U as UI
    participant G as Game
    participant P as MinePlacer
    participant B as Board
    participant O as Observer
    U->>G: reveal(4, 7)
    G->>G: status NOT_STARTED, so start
    G->>P: place(16, 30, 99, first=(4,7))
    P-->>G: 99 cells, none at or around (4,7)
    G->>B: placeMines(cells), compute counts
    G->>G: startTime = clock.now(), status PLAYING
    G->>B: flood(4*30+7)
    B-->>G: 37 newly revealed cells
    G->>G: opened == 480 - 99 ? no
    G-->>O: onCells(37 cells)
    G-->>U: PLAYING

Design patterns used and why

Avoid the tempting Cell class hierarchy (MineCell, NumberCell, EmptyCell). A cell's kind is fixed by data, and the only behaviour that differs is what reveal does, which the board handles with one branch.

Concurrency and edge cases

Down the stack

Layer 1The contract

reveal on a hidden, unflagged cell during play either loses (the cell is a mine) or reveals exactly the cell, plus, if its count is zero, the connected region of zero cells and their non-mine neighbours, never crossing a flag. It returns the new status. It is idempotent: a second call on the same cell changes nothing. The number of revealed safe cells equals rows * cols - mines if and only if the status is WON. The first reveal never loses.

Layer 2The mechanism

Store the board in row-major order: cell (r, c) is index r * cols + c. Neighbours are the eight offsets -cols-1, -cols, -cols+1, -1, +1, cols-1, cols, cols+1, with bounds checks at the edges. A common trick removes the checks: pad the board with a one-cell border of sentinel cells that are "already revealed, count non-zero", so the flood stops there naturally. Chess programs use the same idea in mailbox boards.

Counts are computed once after placement: for each mine, increment its eight neighbours. That is 8m increments, under 1,000 for Expert. The flood is a BFS: push the start cell, pop a cell, and if its count is zero, push each hidden, unflagged neighbour after marking it revealed. Marking on push rather than on pop guarantees each cell enters the queue once, so the flood is O(cells revealed). The win check is a counter of opened safe cells, incremented by the flood.

A no-guess generator places mines, then runs a solver from the first click that uses only deductions (single-cell rules, then subset rules between pairs of numbers). If the solver gets stuck, it rerolls or locally moves mines. Deciding in general whether a partly revealed board is consistent is NP-complete, which Kaye proved in 2000, so practical generators use bounded deduction plus retries rather than exact reasoning.

Layer 3The machine

Pack each cell into one byte: four bits of count (0 to 8), one bit for mine, two bits for hidden, flagged or revealed. Expert is 480 bytes, which fits in eight 64-byte cache lines; a 1,000 by 1,000 board is 1 MB and fits in a modern L2 or L3 cache. A Python object per cell costs 100 bytes or more once its attribute storage is counted, plus an 8-byte pointer in a list, so a million Cell objects is over 100 MB before you have done anything. Row-major order also makes horizontal neighbours adjacent in memory and vertical ones cols bytes away, so a BFS that spreads row by row mostly hits warm lines.

Recursion is the classic failure. A recursive flood on a 1,000 by 1,000 empty board recurses up to a million frames deep. CPython stops at a default recursion limit of 1,000 frames. A browser's JavaScript stack typically allows on the order of ten thousand frames before "Maximum call stack size exceeded". Go grows goroutine stacks on demand up to a 1 GB maximum on 64-bit systems, so it survives longer but copies the stack each time it doubles. An explicit queue lives on the heap, costs 8 bytes or less per entry, and never crashes.

Why it's built this way

  1. Players click fast and boards can be large, so each move must cost in proportion to what it reveals, not to the board size.
  2. That rules out scanning, so counts are precomputed and the win check is a counter.
  3. The flood can reveal most of the board in one click, so it must not use the call stack; it uses a heap-allocated queue.
  4. Mine placement rules vary across versions and difficulty, and tests need fixed boards, so placement is a strategy and randomness is injected through a seed.
  5. Bug reports are about specific games, so a seed plus a move log must replay the game exactly, which forbids hidden sources of randomness or time.

Extensibility

Leadership lens

Check yourself

  1. Why is the win check "opened safe cells equals cells minus mines" rather than "every mine is flagged"?

    Show answer

    Flags are the player's guesses and can be wrong or absent; only revealed safe cells prove the board is cleared. The counter also makes the check O(1).

  2. A recursive flood fill passes every test on a 9 by 9 board. What will break first in production, and how do you fix it?

    Show answer

    A large custom board with a big empty region overflows the call stack. Use an explicit queue (BFS) or stack on the heap.

  3. Why mark a cell as revealed when you push it onto the BFS queue, not when you pop it?

    Show answer

    Otherwise a cell reachable from several zero neighbours is enqueued several times, which wastes work and can double-count the opened counter.

  4. Where should the "first click is safe" rule live?

    Show answer

    In the mine placer (lazy placement that excludes the first click) or a game-level relocation on the first reveal. The board itself should not know about turns.

Watch

Free resources

Open in the interactive app: practice, quiz, mock interview →