JavaScript Game Tutorial

How to Make Minesweeper in JavaScript: Mines, Number Clues and Flood Fill

Minesweeper is a small game with a few genuinely interesting algorithms: placing mines fairly, counting neighbours, and opening a whole empty region with one click. Each one fits in a short function.

BeginnerJavaScriptAlgorithms16 min read

Production context

This guide studies Minesweeper, a published Supagames game. Repository source: games/lvl01/09-minesweeper.html.

Open the Supagames Minesweeper and try all three sizes: 8x8 with 10 mines, 10x10 with 15 and 12x12 with 25. The code below is taken from that game and explains why your first click can never hit a mine.

The board is rendered as a CSS grid of div cells. You need a container element, a mine counter and a status line; everything else is created by JavaScript.

1. Keep Three Grids: Board, Revealed and Flagged

Separate what a cell is from what the player knows about it. The board grid stores -1 for a mine or the number of neighbouring mines. Two boolean grids track which cells are revealed and which are flagged.

Difficulty is just data. Each preset sets rows, columns and the mine count, and resetGame rebuilds all three grids from it.

const diffs = { E: { r: 8, c: 8, m: 10 }, M: { r: 10, c: 10, m: 15 }, H: { r: 12, c: 12, m: 25 } };
let rows = 8, cols = 8, mines = 10;
let board = [], revealed = [], flagged = [];
let gameOver = false, firstClick = true;

const makeGrid = (value) => Array.from({ length: rows }, () => Array(cols).fill(value));

function resetGame() {
  board = makeGrid(0);
  revealed = makeGrid(false);
  flagged = makeGrid(false);
  gameOver = false;
  firstClick = true;
  render();
}

2. Place Mines After the First Click

Losing on the very first click feels unfair, because the player had no information. The fix is to wait: do not place mines until the first click, then exclude the clicked cell and its eight neighbours.

Keeping the whole 3x3 area clear does more than prevent a loss. The first cell is guaranteed to be a 0, so the first click always opens a region and gives the player numbers to work with.

function placeMines(safeRow, safeCol) {
  let placed = 0;
  while (placed < mines) {
    const r = Math.floor(Math.random() * rows);
    const c = Math.floor(Math.random() * cols);
    const nearStart = Math.abs(r - safeRow) <= 1 && Math.abs(c - safeCol) <= 1;
    if (board[r][c] === -1 || nearStart) continue;
    board[r][c] = -1;
    placed++;
  }
  countNumbers();
}

3. Count Neighbouring Mines

Every safe cell shows how many of its up to eight neighbours are mines. A small neighbours helper keeps the bounds checks in one place, and both the counting and the flood fill can use it.

Edge and corner cells simply have fewer neighbours. The helper skips coordinates outside the board, so no special cases are needed elsewhere.

function neighbours(r, c) {
  const list = [];
  for (let dr = -1; dr <= 1; dr++) {
    for (let dc = -1; dc <= 1; dc++) {
      const nr = r + dr, nc = c + dc;
      if ((dr || dc) && nr >= 0 && nr < rows && nc >= 0 && nc < cols) list.push([nr, nc]);
    }
  }
  return list;
}

function countNumbers() {
  for (let r = 0; r < rows; r++)
    for (let c = 0; c < cols; c++)
      if (board[r][c] !== -1)
        board[r][c] = neighbours(r, c).filter(([nr, nc]) => board[nr][nc] === -1).length;
}

4. Open Empty Regions With Flood Fill

Clicking a 0 should open every connected empty cell plus the numbered border around it. The Supagames version does this recursively: reveal the cell, and if it is a 0, call reveal on all neighbours. Already revealed and flagged cells stop the recursion.

Recursion is clear and fine for small boards. On very large custom boards an iterative version with an explicit stack avoids hitting the browser's call stack limit, and it is only a few lines longer.

function reveal(startRow, startCol) {
  const stack = [[startRow, startCol]];
  while (stack.length) {
    const [r, c] = stack.pop();
    if (revealed[r][c] || flagged[r][c]) continue;
    revealed[r][c] = true;
    if (board[r][c] === -1) {
      gameOver = true;
      statusEl.textContent = "Boom! You hit a mine!";
      break;
    }
    if (board[r][c] === 0) stack.push(...neighbours(r, c));
  }
  render();
  checkWin();
}

5. Flag Mines With Right Click or a Flag Mode

On desktop, flags go on the right mouse button. Listen for contextmenu, call preventDefault so the browser menu does not open, and toggle the flag. The remaining mine counter is the total minus the number of flags.

Phones have no right click, so the game adds a flag mode button. When it is on, a tap places or removes a flag instead of revealing. It is more reliable than long-press detection and players understand it immediately.

let flagMode = false;

function toggleFlag(r, c) {
  if (gameOver || revealed[r][c]) return;
  flagged[r][c] = !flagged[r][c];
  mineCountEl.textContent = mines - flagged.flat().filter(Boolean).length;
  render();
}

function onCellClick(r, c) {
  if (gameOver) return;
  if (flagMode) return toggleFlag(r, c);
  if (firstClick) { placeMines(r, c); firstClick = false; }
  reveal(r, c);
}

cell.addEventListener("click", () => onCellClick(r, c));
cell.addEventListener("contextmenu", (e) => { e.preventDefault(); toggleFlag(r, c); });

6. Check for a Win by Counting Revealed Cells

The player wins when every safe cell is revealed. You do not need to check flags at all: count revealed cells and compare with rows times columns minus mines. Flags are a memory aid for the player, not a requirement.

Run the check after every reveal. It is a quick loop over at most 144 cells on the hardest Supagames board.

function checkWin() {
  if (gameOver) return;
  let opened = 0;
  for (let r = 0; r < rows; r++)
    for (let c = 0; c < cols; c++)
      if (revealed[r][c]) opened++;

  if (opened === rows * cols - mines) {
    gameOver = true;
    statusEl.textContent = "You won!";
    render();
  }
}

7. Render Cells From the Three Grids

Rendering reads the grids and builds one div per cell. Revealed cells show a mine or their number, with a class per number so CSS can colour 1s blue, 2s green and so on, as in classic Minesweeper. Hidden flagged cells show a flag.

Setting gridTemplateColumns from the column count lets one CSS grid handle every difficulty without separate layouts.

function render() {
  gridEl.style.gridTemplateColumns = "repeat(" + cols + ", 34px)";
  gridEl.innerHTML = "";
  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      const cell = document.createElement("div");
      cell.className = "mine-cell";
      if (revealed[r][c]) {
        cell.classList.add("revealed");
        if (board[r][c] === -1) cell.textContent = "💣";
        else if (board[r][c] > 0) { cell.textContent = board[r][c]; cell.classList.add("n" + board[r][c]); }
      } else if (flagged[r][c]) {
        cell.textContent = "🚩";
      }
      cell.addEventListener("click", () => onCellClick(r, c));
      gridEl.appendChild(cell);
    }
  }
}

8. Build checklist

  • Store mines and numbers separately from what the player has revealed or flagged.
  • Place mines after the first click and keep the 3x3 area around it clear.
  • Use one neighbours helper for counting and for flood fill.
  • Prefer an explicit stack for flood fill on large boards.
  • Support right click on desktop and a flag mode on touch screens.
  • Win when all safe cells are revealed; flags are optional.

9. FAQ

Why is the first click in Minesweeper always safe?

Because mines are placed only after the first click, and the generator skips the clicked cell and its neighbours. Many implementations do this, including the Windows versions, which moved a mine away from the first clicked cell.

Is every Minesweeper board solvable without guessing?

No. Random mine placement sometimes creates positions where two cells are equally likely to be mines. Guaranteeing a no-guess board requires running a solver during generation and rerolling layouts it cannot solve.

What do the numbers in Minesweeper mean?

A number shows how many of the eight surrounding cells contain mines. A 1 next to a single hidden cell means that cell is a mine; a number whose mines are already flagged means the other neighbours are safe.

What are the classic Minesweeper board sizes?

The Windows classic used Beginner 9x9 with 10 mines, Intermediate 16x16 with 40 mines and Expert 30x16 with 99 mines. The Supagames presets are smaller so they fit comfortably on a phone.

Previous: How to make 2048 Next: Tic-Tac-Toe with minimax AI