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.