0) Problem Restatement
Amazon asked: design an online Minesweeper platform. Players start games at different difficulty levels (e.g., Beginner 9×9 with 10 mines, Expert 30×16 with 99 mines), reveal and flag cells with low latency, can resume unfinished games later, and compete on leaderboards for the fastest wins.
1) Game Rules (quick recap)
- Hidden mines on a grid. Revealing a mine = lose.
- Revealing a safe cell shows the number of adjacent mines (0–8).
- Revealing a 0 automatically reveals its neighbors (a flood fill), since none of them can be mines.
- Win when all non-mine cells are revealed.
2) Who Holds the Truth? The Server
If the mine positions are sent to the client, a player can read them (cheat). So:
- The server generates and keeps the mine layout (never sent to the client until the game ends).
- The client sends actions (
reveal(r, c),flag(r, c)), and the server returns the newly revealed cells and numbers. - First click safety: generate the mines after the first click, excluding that cell (and often its neighbors).
3) Architecture
Architecture Diagram
flowchart LR
P["Player browser"] -->|"reveal / flag (HTTP or WebSocket)"| API["Game API - stateless"]
API --> ST[("Game state - Redis, active games")]
API --> DB[("Postgres - finished games, users")]
API --> LB[("Leaderboards - sorted sets per difficulty")]- Active game state is small (Expert: 480 cells, a few hundred bytes as bitsets), so keep it in Redis with a TTL (e.g., 7 days) for resuming, and write a snapshot to the DB on finish.
- The API is stateless, so any server can handle any move.
4) Data Model
game: game_id, user_id, difficulty, rows, cols, mines (bitset, server-only), revealed (bitset),
flagged (bitset), status (playing|won|lost), started_at, finished_at, moves_count, version
5) Reveal Algorithm (server side)
from collections import deque
def reveal(game, r, c):
if game.status != "playing" or game.revealed[r][c] or game.flagged[r][c]:
return []
if game.mines[r][c]:
game.status = "lost"
return [("mine", r, c)]
opened, q = [], deque([(r, c)])
game.revealed[r][c] = True
while q:
x, y = q.popleft()
n = game.adjacent_mines(x, y)
opened.append((n, x, y))
if n == 0: # flood fill through zeros
for dx in (-1, 0, 1):
for dy in (-1, 0, 1):
nx, ny = x + dx, y + dy
if game.inside(nx, ny) and not game.revealed[nx][ny] and not game.flagged[nx][ny]:
game.revealed[nx][ny] = True
q.append((nx, ny))
if game.safe_cells_left() == 0:
game.status = "won"
return opened
BFS avoids deep recursion on big empty areas. The adjacent-mine counts can be precomputed when mines are placed.
6) Concurrency, Timing and Leaderboards
- One active session per game: moves carry a
version, and the server applies them with a conditional update (compare-and-set in Redis via a Lua script), so double clicks or two tabs can't corrupt the state. - Timing: the server records the start time (on first reveal) and the finish time. The server clock is authoritative, which stops time manipulation.
- Leaderboards: on a win, add the time to a sorted set per difficulty (lower is better), plus daily and weekly boards.
- Anti-cheat: server-side mines already stop the obvious cheat. Also flag impossible times (faster than humanly possible) and bot-like click patterns.
7) Wrap-Up
Keep the mine layout only on the server (generated after a safe first click), and let clients send reveal and flag actions to a stateless API that updates compact bitset game state in Redis with version checks. Reveal uses BFS flood fill through zero cells, finished games are saved to the DB, and server-timed wins go into per-difficulty sorted-set leaderboards with basic anti-cheat checks.