hardReact Native#132

Minesweeper Grid (Booscaminas)

Prompt

Implement the core logic functions for Minesweeper.

Write three pure functions:

  1. createBoard() — returns an 8x8 board with 10 bombs placed randomly. Each cell: { bomb: boolean, adjacent: number, revealed: boolean }
  2. revealCell(board, r, c) — returns a NEW board with cell (r,c) revealed. If adjacent=0, auto-reveal neighbors (flood fill). Bomb cells are never revealed by this function — the UI detects the bomb tap and handles game over.
  3. checkWin(board) — returns true if all non-bomb cells are revealed.

Solution

const GRID_SIZE = 8
const BOMB_COUNT = 10

function createBoard() {
  const board = Array.from({ length: GRID_SIZE }, () =>
    Array.from({ length: GRID_SIZE }, () => ({ bomb: false, adjacent: 0, revealed: false }))
  )
  let placed = 0
  while (placed < BOMB_COUNT) {
    const r = Math.floor(Math.random() * GRID_SIZE)
    const c = Math.floor(Math.random() * GRID_SIZE)
    if (!board[r][c].bomb) { board[r][c] = { ...board[r][c], bomb: true }; placed++ }
  }
  for (let r = 0; r < GRID_SIZE; r++) for (let c = 0; c < GRID_SIZE; c++) {
    if (board[r][c].bomb) continue
    let count = 0
    for (let dr = -1; dr <= 1; dr++) for (let dc = -1; dc <= 1; dc++) {
      const nr = r + dr, nc = c + dc
      if (nr >= 0 && nr < GRID_SIZE && nc >= 0 && nc < GRID_SIZE && board[nr][nc].bomb) count++
    }
    board[r][c] = { ...board[r][c], adjacent: count }
  }
  return board
}

function revealCell(board, r, c) {
  const rows = board.length
  const cols = board[0].length
  const newBoard = board.map(row => row.map(cell => ({ ...cell })))
  const reveal = (rr, cc) => {
    if (rr < 0 || rr >= rows || cc < 0 || cc >= cols) return
    if (newBoard[rr][cc].revealed || newBoard[rr][cc].bomb) return
    newBoard[rr][cc].revealed = true
    if (newBoard[rr][cc].adjacent === 0) {
      for (let dr = -1; dr <= 1; dr++) for (let dc = -1; dc <= 1; dc++) reveal(rr + dr, cc + dc)
    }
  }
  reveal(r, c)
  return newBoard
}

function checkWin(board) {
  return board.flat().every(cell => cell.revealed || cell.bomb)
}
Mentor's take

Minesweeper is the strongest test of quarantining impurity. Math.random() lives only in createBoard, a factory called once per game; revealCell and checkWin are deterministic board-in/board-out functions. That split is what makes the game logic unit-testable and what makes React happy: the component holds the board in state and every tap is setBoard(revealCell(board, r, c)).

The subtle parts:

  • Deep-copy before flood fill. revealCell clones every row and every cell object up front, then lets the recursive reveal mutate the clone freely. Local mutation of a fresh copy is a legitimate performance pattern — purity is about the function's contract (inputs untouched, new output), not about never using assignment internally.
  • Flood-fill termination: the recursion stops at bounds, at already-revealed cells, and at bombs. The revealed-check is the visited-set — without it, two adjacent zero cells recurse into each other forever.
  • Bounds come from board.length, not the constant, so the function works on any board size — which is also what makes it testable with tiny hand-built boards.
  • checkWin is a derived predicateflat().every(revealed || bomb) — never a flag you maintain by hand.

Red flag: mutating the board held in state and calling setBoard(board). Same reference — React skips the re-render and taps "stop working." Every reveal must return a new board.

Say it: "Randomness is quarantined in the factory; reveal is board-in, new-board-out with the revealed flag doubling as the flood-fill visited set."