Building a Minesweeper-Style Game with Flood-Fill Algorithm in JavaScript

Building a Minesweeper-Style Game with Flood-Fill Algorithm in JavaScript

In this article, we will explore how to implement a simple Minesweeper-like game in JavaScript using object-oriented design. We will use two main classes: Game and Player. The Game class manages the game logic, grid, traps, and UI rendering, while the Player class handles player interaction.

We will also apply the flood-fill algorithm to reveal cells intelligently, making the gameplay both engaging and challenging.


Overview of Classes and Methods

Game Class

The Game class encapsulates all the core logic and state of the game:

  • Initialize the grid and properties
  • Draw the board UI dynamically
  • Place traps randomly
  • Calculate numbers surrounding traps using flood-fill
  • Process player selections and detect win/loss conditions

Player Class

The Player class acts as an interface for the player, calling the Game methods upon user interaction.


Class Structure Overview

class Game {
  _createGrid() {}
  _draw() {}
  _addTraps() {}
  _addNumbers() {}
  _findTraps() {}
  checkPlayerSelection() {}
  playerLost() {}
  playerWon() {}
  start() {}
}

class Player {
  playerSelection() {}
}

Note: The underscore _ prefix is a convention indicating private methods, which are accessible but intended to be used internally within the class.


Step 1: Initializing the Game Class

We start by defining the properties for the game, such as the grid size, number of traps, and UI elements.

export class Game {
  constructor(rows, cols, maxNumberOfTraps) {
    this.rows = rows;              // Number of rows
    this.cols = cols;              // Number of columns
    this.grid = [];                // 2D array representing the grid
    this.traps = maxNumberOfTraps; // Number of traps to place
    this.target = "+";             // Trap identifier
    this.btns = null;              // Buttons covering the grid
    this.svg = "☠";                // Trap symbol
  }
}

Step 2: Generating the Grid

The grid is a 2D array filled initially with zeros:

_createGrid(rows, cols) {
  return Array.from(Array(rows), () => new Array(cols).fill(0));
}

This grid acts as the logical representation of the game board.


Step 3: Drawing the Board UI

We dynamically create a grid layout using DOM elements:

_draw() {
  const body = document.querySelector("body");
  this.grid = this._createGrid(this.rows, this.cols);

  const gameContainer = document.createElement("div");
  gameContainer.classList.add("grid");

  for (let i = 0; i < this.rows; i++) {
    const row = document.createElement("div");
    row.classList.add("row");

    for (let j = 0; j < this.cols; j++) {
      const cell = document.createElement("div");
      cell.id = `${i}${j}`;
      cell.classList.add("cell");

      const btn = document.createElement("button");
      btn.classList.add("btn");

      const cellContainer = document.createElement("div");
      cellContainer.classList.add("cellContainer");

      cellContainer.append(cell);
      cellContainer.append(btn);

      row.append(cellContainer);
    }
    gameContainer.append(row);
  }
  body.append(gameContainer);
}

Step 4: Adding Traps Randomly

Traps are randomly placed on the grid, marked visually with a skull symbol and logically with a + in the grid array:

_addTraps() {
  for (let i = 0; i < this.traps; i++) {
    const x = Math.floor(Math.random() * this.rows);
    const y = Math.floor(Math.random() * this.cols);
    const cell = document.getElementById(`${x}${y}`);
    cell.innerHTML = `<span>${this.svg}</span>`;
    this.grid[x][y] = "+";
  }
}

Step 5: Adding Numbers Around Traps Using Flood-Fill

We use a recursive method _addNumbers that visits adjacent cells around each trap and increments numbers to indicate how many traps are nearby.

_addNumbers(x, y, target) {
  if (
    x < 0 || y < 0 ||
    x >= this.grid.length || y >= this.grid[0].length
  ) {
    return;
  }

  if (this.grid[x][y] !== target) {
    if (this.grid[x][y] === "x") {
      return;
    }
    this.grid[x][y] += 1;
    const cell = document.getElementById(`${x}${y}`);
    cell.innerHTML = `<span>${this.grid[x][y]}</span>`;
    return;
  }

  this.grid[x][y] = "x";

  this._addNumbers(x - 1, y, target);
  this._addNumbers(x + 1, y, target);
  this._addNumbers(x, y - 1, target);
  this._addNumbers(x, y + 1, target);
  this._addNumbers(x - 1, y - 1, target);
  this._addNumbers(x + 1, y + 1, target);
  this._addNumbers(x - 1, y + 1, target);
  this._addNumbers(x + 1, y - 1, target);
}

Step 6: Finding All Traps and Applying Numbering

To process the entire grid, we scan all cells and invoke _addNumbers for every trap found:

_findTraps() {
  for (let x = 0; x < this.grid.length; x++) {
    for (let y = 0; y < this.grid[x].length; y++) {
      if (this.grid[x][y] === this.target) {
        this._addNumbers(x, y, this.target);
      }
    }
  }
}

Step 7: Processing Player Selection

When a player clicks a cell, we determine whether they clicked a trap, a number, or an empty cell, revealing appropriate cells accordingly:

checkPlayerSelection(x, y, target) {
  if (
    x < 0 || y < 0 ||
    x >= this.grid.length || y >= this.grid[0].length
  ) {
    return;
  }

  if (this.grid[x][y] !== target) {
    if (this.grid[x][y] === "x") {
      this.playerLost("x");
    }
    this.grid[x][y] = "o";
    const cell = document.getElementById(`${x}${y}`);
    cell.nextSibling.style.display = "none";
    return;
  }

  const cell = document.getElementById(`${x}${y}`);
  cell.nextSibling.classList.add("hidden");
  this.grid[x][y] = "o";

  this.checkPlayerSelection(x - 1, y, target);
  this.checkPlayerSelection(x + 1, y, target);
  this.checkPlayerSelection(x, y - 1, target);
  this.checkPlayerSelection(x, y + 1, target);
}

Step 8: Handling Game Over and Win Conditions

Player Lost

Disable all buttons and reveal remaining traps:

playerLost(target) {
  this.btns.forEach(btn => btn.disabled = true);
  for (let i = 0; i < this.grid.length; i++) {
    for (let j = 0; j < this.grid[i].length; j++) {
      if (this.grid[i][j] === target) {
        const cell = document.getElementById(`${i}${j}`);
        cell.nextSibling.style.display = "none";
      }
    }
  }
}

Player Won

If only traps remain hidden, disable buttons and declare victory:

playerWon() {
  for (let i = 0; i < this.grid.length; i++) {
    for (let j = 0; j < this.grid[i].length; j++) {
      if (!isNaN(this.grid[i][j])) {
        return;
      }
    }
  }
  this.btns.forEach(btn => btn.disabled = true);
}

Step 9: Starting the Game

The start method draws the grid, adds traps, and calculates numbers:

start() {
  this._draw();
  this.btns = document.querySelectorAll(".btn");
  this._addTraps();
  this._findTraps();
}

Step 10: Player Class Implementation

The Player class interacts with the game:

export class Player {
  constructor(game) {
    this.game = game;
  }

  playerSelection(x, y) {
    this.game.checkPlayerSelection(+x, +y, 0);
    this.game.playerWon();
  }
}

Step 11: Connecting Everything in start.js

We initialize the game and player, start the game, and listen for user input:

import { Game } from "./game.js";
import { Player } from "./player.js";

window.addEventListener("load", () => {
  const game = new Game(10, 10, 15);
  const player = new Player(game);

  game.start();

  game.btns.forEach(btn => {
    btn.addEventListener("click", e => {
      const x = e.target.previousElementSibling.id[0];
      const y = e.target.previousElementSibling.id[1];
      player.playerSelection(x, y);
    });
  });
});

Final Notes

  • This game can be enhanced with animations, difficulty levels, scoring, and better UI.
  • The current implementation uses simple DOM manipulation and event handling.
  • Feel free to extend the logic or integrate with frameworks like React or Vue.