Code a minesweeper game
Open in the interactive app: practice, quiz, mock interview →In one minute
- Minesweeper looks like a grid problem, but the interview is about three things: a clean split between the rules (
Game) and the data (Board), a reveal that floods empty regions without blowing the stack, and a game state machine that refuses every move after a win or a loss. - Precompute each cell's adjacent-mine count once, when mines are placed. Every later reveal is then a lookup, and the win check is one counter compared with
cells - mines, not a scan. - Make mine placement an injected strategy. Random placement with a seed, "first click is always safe" and "no guessing needed" generators are all placers; tests use a fixed list. Code that calls
random()inside the board cannot be tested or replayed. - Flood fill is a breadth-first search with an explicit queue. Recursive flood fill works on a 9 by 9 board and crashes on a large custom board, because recursion depth grows with the size of the empty region.
- The shape is finding things fast over a grid (neighbour lookups, connected regions). The same reveal-a-region code is the paint bucket in an image editor, fog-of-war in a strategy game and connected-component labelling in vision.
Requirements
Functional:
- A board of
rows x colscells withmmines. Presets: Beginner 9 by 9 with 10 mines, Intermediate 16 by 16 with 40, Expert 30 by 16 with 99, plus custom sizes. - The player can reveal a cell, toggle a flag on a hidden cell, and chord: on a revealed number whose flagged neighbours equal the number, reveal all other hidden neighbours at once.
- Revealing a mine loses. Revealing a cell with zero adjacent mines also reveals its neighbours, recursively, through the connected empty region and its numbered border.
- The first reveal is never a mine.
- The game is won when every safe cell is revealed. Flags are not required to win.
- Show a mine counter (mines minus flags) and a timer that starts at the first reveal.
Non-functional:
- Every move responds in well under a frame (16 ms), even on a 1,000 by 1,000 custom board.
- A game is reproducible from its seed and its move list, for bug reports and replays.
- Moves after the game ends are rejected and change nothing.
Out of scope: graphics, high-score servers, hexagonal grids, multiplayer. Mention them in Extensibility.
Entities and relationships
- Game is the façade the UI talks to. It owns the board, the status (
NOT_STARTED,PLAYING,WON,LOST), the timer and the observers. It enforces rules: no moves after the end, the first click is safe, chording needs the right flag count. - Board owns the cells and the geometry. It knows neighbours, adjacent counts, and how to flood a region. It has no idea about turns, timers or winning.
- Cell state is small: is it a mine, its adjacent count, and one of
HIDDEN,FLAGGED,REVEALED. In memory this is better as parallel arrays or one byte per cell than as a millionCellobjects (see Down the stack), but the concept stays the same. - MinePlacer is a strategy that returns mine positions given the board size, the mine count and the first click. Implementations:
FixedPlacer(tests, puzzles),RandomPlacer(seed)andNoGuessPlacer(generate, run a solver, retry). - Difficulty is a value: rows, cols and mines, with validation (
mines < rows * cols, or fewer if the first click must open a 3 by 3 area). - Clock supplies time for the timer; GameObserver receives events (cells revealed, status changed, counter changed) so the UI can repaint only what changed.
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 ..|> MinePlacerKey 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: PLAYINGDesign patterns used and why
- Façade for
Game. The UI, a test or a bot solver all call five methods. The board's array layout can change without touching them. - Strategy for mine placement. The rules about where mines may go (first click safe, first click opens a region, board must be solvable without guessing) change between versions and between difficulty settings. Simon Tatham's Mines, for example, generates boards that never require a guess by default. Each rule is a new placer;
Gamedoes not change. - State machine, kept as an enum with guarded transitions.
NOT_STARTEDtoPLAYINGon the first reveal,PLAYINGtoWONorLOST, and nothing out of the terminal states. A State-pattern class per status is overkill here, because only two methods behave differently per state and the rule is mostly "reject". - Observer for the UI and for sound or statistics. The flood returns the list of changed cells, and the UI repaints exactly those. That matters on big boards, where repainting everything after each click wastes most of a frame.
- Command, optional, for replay. Each move is a small record (
reveal 4 7,flag 2 3) appended to a log. Seed plus log reproduces any game, which turns "I lost on a cell that should have been safe" bug reports into a failing test.
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
- First click on a mine. Either place mines lazily after the first click, excluding that cell (or its 3 by 3 neighbourhood), or place them up front and move the clicked mine to another free cell. Lazy placement is cleaner; the practice uses the move rule because it keeps placement deterministic with a fixed list.
- Flags and floods. A flood must not reveal a flagged cell, even an empty one. The player put the flag there, and silently removing it is a bug report.
- Wrong flags and chording. Chording trusts the player's flags. If a flag is wrong, chording reveals a mine and the game is lost, exactly as the classic games behave. Chording on a number with the wrong flag count does nothing.
- Revealing a revealed or flagged cell is a no-op, not an error. Double clicks and stray taps are common, and the UI should not need to filter them.
- Moves after the end are rejected and leave everything unchanged, including the timer.
- Win and loss in one move. A chord can reveal a safe cell that would complete the board and a mine in the same call. Process neighbours in a fixed order and stop at the first mine, so the outcome is deterministic.
- Threads. The game is single-player, so the only concurrency is the UI thread and a timer tick. Keep all state changes on the UI thread (or one game actor), and have the timer only post "repaint the clock" events. A co-op multiplayer version puts one owner per game behind a queue, the single-owner approach from How to handle Concurrency Scenarios, and makes reveal idempotent so that two players clicking the same cell is harmless.
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
- Players click fast and boards can be large, so each move must cost in proportion to what it reveals, not to the board size.
- That rules out scanning, so counts are precomputed and the win check is a counter.
- 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.
- 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.
- 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
- Hexagonal or triangular grids: move neighbour calculation into a
Topologyinterface (square with 8 neighbours, hex with 6). Board, flood and counts stay the same, because they only ask for neighbours. - Guaranteed opening: a placer that excludes the first click's 3 by 3 neighbourhood, so the first click always reveals a region. Validate that
mines <= cells - 9. - No-guess mode: a
NoGuessPlacerwrapping a solver, with a retry budget and a fallback to random placement if generation is slow. - Undo for practice mode: because moves are commands and the game is deterministic from the seed, undo is "replay all but the last move", or a Memento of the byte array (480 bytes is cheap to snapshot).
- Hints and bots: a solver that reads only the public view and calls the same façade, so it cannot cheat by reading the mine array.
- Online leaderboards: submit seed, moves and the client's elapsed time; the server replays the game to verify the result before accepting the score.
Leadership lens
- Build first: the board with counts and the BFS flood, with tests on fixed boards. The UI can be a text grid for a week; a wrong flood or a first-click loss is what players notice.
- Ask in review: is there any
random()ornow()call outside the injected placer and clock; is the flood iterative; what does a move after game over do; what does a flood do at a flag. - Determinism pays for itself. A seed plus move log turns every crash report into a reproducible test and enables server-side score verification without trusting the client.
- Ownership: rules and board are one small module with exhaustive tests; rendering and input are another. Generators (no-guess mode) are a separate, slower-moving component that can be swapped without touching either.
- In the interview, get to the flood and the state machine within 15 minutes. Spending the first half on a
Cellinheritance tree is the common way to run out of time.
Check yourself
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).
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.
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.
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
- Coding Challenge #71: Minesweeper (The Coding Train): builds the game from scratch, including neighbour counts and the flood reveal.
- State Pattern, Design Patterns ep 17 (Christopher Okhravi): when a status enum is enough and when per-state classes earn their place.
Free resources
- Minesweeper (Wikipedia)): rules, chording, the classic difficulty presets and the NP-completeness result.
- Mines (Simon Tatham's Portable Puzzle Collection): a widely used implementation whose default generator never requires a guess.
- Flood fill (Wikipedia): recursive, queue-based and scanline variants, with their memory trade-offs.
- Grid parts and relationships (Red Blob Games): how to reason about cells, edges, corners and neighbours on square and hex grids.
- Observer pattern (Refactoring.Guru): how the UI subscribes to revealed cells and status changes.