Simple Javascript Minesweeper (Free Code Download)

Welcome to a tutorial on how to create a simple Minesweeper game with Javascript. Yes, Minesweeper is yet another of those classic games. There are plenty of “free-to-play” Minesweepers online, so here’s a slightly different one for those who want to learn – A minimal “no graphics” version, so you get to study the mechanics. Read on!

ⓘ I have included a zip file with all the source code at the start of this tutorial, so you don’t have to copy-paste everything… Or if you just want to dive straight in.

 

 

TABLE OF CONTENTS

Download & Demo JS Minesweeper Useful Bits & Links
The End

 

DOWNLOAD & DEMO

Firstly, here is the download link to the example code as promised.

 

QUICK NOTES

If you spot a bug, feel free to comment below. I try to answer short questions too, but it is one person versus the entire world… If you need answers urgently, please check out my list of websites to get help with programming.

 

EXAMPLE CODE DOWNLOAD

Click here to download all the example source code, I have released it under the MIT license, so feel free to build on top of it or use it in your own project.

 

JAVASCRIPT MINESWEEPER DEMO

 

 

JAVASCRIPT MINESWEEPER

All right, let us now get into the mechanics of the Minesweeper game.

 

PART 1) THE HTML

mine.html
<!-- GAME WRAPPER -->
<div id="mine-wrap"></div>

Yes, not kidding, a single <div> is all we need to get started. The “cells and mines” will be generated with Javascript, which will look something like this.

<div id="mine-wrap">
  <div class="cell"></div>
  <div class="cell"></div>
  <div class="cell"></div>
  ...
</div>

 

PART 2) THE CSS

mine.css
/* (A) MINESWEEPER WRAPPER */
#mine-wrap {
  display: flex;
  flex-wrap: wrap;
}
 
/* (B) MINESWEEPER CELLS */
#mine-wrap .cell {
  box-sizing: border-box;
  background: #ddd;
  border: 1px solid #ccc;
  font-size: 40px;
  text-align: center;
  cursor: pointer;
}
#mine-wrap .cell.mark { background: #fea; }
#mine-wrap .cell.reveal { background: #f7f7f7; }
#mine-wrap .cell.boom { background: #f00; }

Well, just some simple CSS styles to draw the board:

  • display: flex and flex-wrap: wrap are the ones that do the cells layout magic.
  • The width of each cell is automatically calculated in Javascript.
  • But otherwise, .cell controls the style of the cells.
    • .mark is attached when we right-click to mark a cell.
    • .reveal is a cell that is opened.
    • .boom when the player opens a cell with a mine in it…

 

 

PART 3) GAME PROPERTIES

mine.js
var mine = {
  // (A) PROPERTIES
  // (A1) GAME SETTINGS
  total : 10, // TOTAL NUMBER OF MINES
  height : 10, // NUMBER OF ROWS
  width : 8, // NUMBER OF COLUMNS
 
  // (A2) GAME FLAGS
  board : [], // CURRENT GAME BOARD
  rCell : 0 // NUMBER OF REMAINING HIDDEN CELLS
}

The Javascript is pretty brutal, so let’s split it into sections.

  • First, Captain Obvious – The var mine object contains all the minesweeper mechanics.
  • (A1) The game settings should be self-explanatory.
    • mine.total The total number of mines to set.
    • mine.height mine.width The number of rows and columns the game board has.
  • (A2) To keep track of the game process, we need to define some game flags.
    • mine.board An array to store every cell – If it is an empty cell, has a mine, and the number of adjacent mines.
    • mine.rCell The remaining number of hidden cells. If mine.rCell == mine.total, then the player has successfully found all mines and wins.

 

 

PART 4) INITIALIZE/RESET GAME

mine.js
// (B) RESET & INITIALIZE GAME
reset : () => {
  // (B1) RESET GAME FLAGS
  mine.board = [];
  mine.rCell = mine.height * mine.width;
 
  // (B2) GET + RESET HTML WRAPPER
  let wrap = document.getElementById("mine-wrap"),
  cssWidth = 100 / mine.width;
  wrap.innerHTML = "";
 
  // (B3) LOOP THROUGH ROWS & COLUMNS - GENERATE CELLS
  for (let row=0; row<mine.height; row++) {
    mine.board.push([]); 
    for (let col=0; col<mine.width; col++) {
      // ADD CELL TO MINE.BOARD[]
      mine.board[row].push({ 
        r : false, // CELL IS REVEALED?
        x : false, // CELL IS MARKED?
        m : false, // CELL HAS A MINE?
        a : 0, // NUMBER OF MINES IN ADJACENT CELLS
        c : document.createElement("div") // HTML REFERENCE
      });
 
      // ADD CELL TO HTML <DIV>
      let cell = mine.board[row][col].c;
      cell.classList.add("cell");
      cell.id = "mine-" + row + "-" + col;
      cell.dataset.row = row;
      cell.dataset.col = col;
      cell.onclick = () => { mine.open(cell); };
      cell.oncontextmenu = (e) => {
        e.preventDefault();
        mine.mark(cell);
      };
      cell.style.width = cssWidth + "%";
      cell.innerHTML = "&nbsp;";
      wrap.appendChild(cell);
    }
  }
 
  // (B4) RANDOMLY LAY MINES
  let mRow = mine.height - 1,
      mCol = mine.width - 1,
      mToLay = mine.total;
  while (mToLay > 0) {
    let row = Math.floor(Math.random() * mRow);
    let col = Math.floor(Math.random() * mCol);
    if (!mine.board[row][col].m) {
      mine.board[row][col].m = true;
      // CHEAT - SHOW MINE ON THE BOARD
      // mine.board[row][col].c.innerHTML = "*";
      mToLay--;
    }
  }
 
  // (B5) CALCULATE NUMBER OF ADJACENT MINES FOR EACH CELL
  // LOOP THROUGH ROW-BY-ROW
  for (let row=0; row<mine.height; row++) {
    let lastRow = row - 1,
        nextRow = row + 1;
    if (nextRow == mine.height) { nextRow = -1; }
 
    // LOOP THROUGH CELLS OF EACH ROW
    for (let col=0; col<mine.width; col++) {
      let lastCol = col - 1,
          nextCol = col + 1;
      if (nextCol == mine.width) { nextCol = -1; }
 
      // CALCULATE ONLY IF CELL DOES NOT CONTAIN MINE
      if (!mine.board[row][col].m) {
        // ADD NUMBER OF MINES IN LAST ROW
        if (lastRow != -1) {
          if (lastCol != -1) { if (mine.board[lastRow][lastCol].m) { mine.board[row][col].a++; } }
          if (mine.board[lastRow][col].m) { mine.board[row][col].a++; }
          if (nextCol != -1) { if (mine.board[lastRow][nextCol].m) { mine.board[row][col].a++; } }
        }
 
        // ADD NUMBER OF MINES IN CURRENT ROW
        if (lastCol != -1) { if (mine.board[row][lastCol].m) { mine.board[row][col].a++; } }
        if (nextCol != -1) { if (mine.board[row][nextCol].m) { mine.board[row][col].a++; } }
 
        // ADD NUMBER OF MINES IN NEXT ROW
        if (nextRow != -1) {
          if (lastCol != -1) { if (mine.board[nextRow][lastCol].m) { mine.board[row][col].a++; } }
          if (mine.board[nextRow][col].m) { mine.board[row][col].a++; }
          if (nextCol != -1) { if (mine.board[nextRow][nextCol].m) { mine.board[row][col].a++; } }
        }
      }
 
      // CHEAT - SHOW NUMBER OF ADJACENT MINES ON BOARD
      // if (mine.board[row][col].a != 0) { mine.board[row][col].c.innerHTML = mine.board[row][col].a ; }
    }
  }
}
window.addEventListener("DOMContentLoaded", mine.reset);

Not going to explain line-by-line, but what mine.reset() does is essentially do the calculations and set the game up:

  • (B1) Reset mine.board to an empty array and mine.rCell to WIDTH X HEIGHT.
  • (B2) Empties out the HTML container <div id="mine-wrap">, calculate the width of each cell.
  • (B3) Loop through mine.width and mine.height to generate the cells. Take note of how the data for the cells are kept in the format of mine.board[ROW][COL] = {DATA}, and the corresponding HTML cell is <div class="cell" data-row="ROW" data-col="COL">.
  • (B4) Randomly place the mines.
  • (B5) Lastly, loop through the cells again, calculate the number of adjacent mines each cell has.

 

 

PART 5) RIGHT-CLICK TO MARK CELL

mine.js
// (C) RIGHT CLICK TO MARK CELL
mark : (cell) => {
  // (C1) GET COORDS OF SELECTED CELL
  let row = cell.dataset.row,
      col = cell.dataset.col;

  // (C2) MARK/UNMARK ONLY IF CELL IS STILL HIDDEN
  if (!mine.board[row][col].r) {
    cell.classList.toggle("mark");
    mine.board[row][col].x = !mine.board[row][col].x;
  }
}

This is straightforward – mine.mark() is called when the player right-clicks on a cell. Basically, we just toggle the .mark CSS class on the cell.

 

 

PART 6) LEFT-CLICK TO OPEN CELL

mine.js
// (D) LEFT CLICK TO OPEN CELL
open : (cell) => {
  // (D1) GET COORDS OF SELECTED CELL
  let row = cell.dataset.row,
      col = cell.dataset.col;

  // (D2) SELECTED CELL HAS MINE = LOSE
  if (!mine.board[row][col].x && mine.board[row][col].m) {
    cell.classList.add("boom");
    setTimeout(() => {
      alert("Opps. You lost.");
      mine.reset();
    }, 1);
  }
 
  // (D3) REVEAL SELECTED CELL + ALL EMPTY ADJACENT CELLS
  else {
    // (D3A) FLAGS - WHICH CELLS SHOULD WE AUTOMATICALLY REVEAL?
    let toReveal = [], // ALL CELLS TO REVEAL
        toCheck = [], // ALL CELLS TO CHECK
        checked = []; // ALL CELL THAT HAVE ALREADY BEEN CHECKED
    for (let i=0; i<mine.height; i++) { checked.push({}); }
    toCheck.push([row, col]);
 
    // (D3B) LOOP & CHECK - ADD CELLS TO REVEAL
    while (toCheck.length>0) {
      // CURRENT "CHECK CELL" COORDINATES
      let thisRow = parseInt(toCheck[0][0]),
          thisCol = parseInt(toCheck[0][1]);

      // HAS MINE, ALREADY REVEALED, MARKED - DO NOTHING
      if (mine.board[thisRow][thisCol].m || mine.board[thisRow][thisCol].r || mine.board[thisRow][thisCol].x) {}
      else {
        // ADD CELL TO REVEAL
        if (!checked[thisRow][thisCol]) { toReveal.push([thisRow, thisCol]); }
 
        // CELL DOES NOT HAVE ADJ MINES - CHECK NEIGHBOURS
        if (mine.board[thisRow][thisCol].a == 0) {
          // PREVIOUS & NEXT CELL COORDINATES
          let lastRow = thisRow - 1,
              nextRow = thisRow + 1,
              lastCol = thisCol - 1,
              nextCol = thisCol + 1;
          if (nextRow == mine.height) { nextRow = -1; }
          if (nextCol == mine.width) { nextCol = -1; }
 
          // LAST ROW
          if (lastRow != -1) {
            if (lastCol != -1 && !checked[lastRow][lastCol]) { toCheck.push([lastRow, lastCol]); }
            if (!checked[lastRow][thisCol]) { toCheck.push([lastRow, thisCol]); }
            if (nextCol != -1 && !checked[lastRow][nextCol]) { toCheck.push([lastRow, nextCol]); }
          }
 
          // CURRENT ROW
          if (lastCol != -1 && !checked[thisRow][lastCol]) { toCheck.push([thisRow, lastCol]); }
          if (nextCol != -1 && !checked[thisRow][nextCol]) { toCheck.push([thisRow, nextCol]); }
 
          // NEXT ROW
          if (nextRow != -1) {
            if (lastCol != -1 && !checked[nextRow][lastCol]) { toCheck.push([nextRow, lastCol]); }
            if (!checked[nextRow][thisCol]) { toCheck.push([nextRow, thisCol]); }
            if (nextCol != -1 && !checked[nextRow][nextCol]) { toCheck.push([nextRow, nextCol]); }
          }
        }
      }
 
      // MOVE ON - CHECK NEXT CELL
      checked[thisRow][thisCol] = true;
      toCheck.shift();
    }
 
    // (D3C) AUTOMATICALLY REVEAL CELLS (IF ANY)
    if (toReveal.length > 0) {
      for (let cell of toReveal) {
        let thisRow = parseInt(cell[0]);
        let thisCol = parseInt(cell[1]);
        mine.board[thisRow][thisCol].r = true;
        if (mine.board[thisRow][thisCol].a != 0) { 
          mine.board[thisRow][thisCol].c.innerHTML = mine.board[thisRow][thisCol].a;
        }
        mine.board[thisRow][thisCol].c.classList.add("reveal");
        mine.rCell = mine.rCell - 1;
      }
    }
 
    // (D3D) NO CELLS LEFT TO OPEN - WIN!
    if (mine.rCell == mine.total) {
      alert("YOU WIN!");
      mine.reset();
    }
  }
}

Lastly, opening a cell also involves some grim calculations.

  • (D1) Get the coordinates of the selected cell (that is clicked on).
  • (D2) The player clicks on a mine and loses. Easy.
  • (D3) The not-so-easy part where the player clicks on an empty cell. Some heavy calculations are required to also automatically reveal all adjacent empty cells. If there are no more cells left to open mine.rCell == mine.total the player wins the game.

 

THE END

Thank you for reading, and we have come to the end. I hope that it has helped you to better understand, and if you want to share anything with this guide, please feel free to comment below. Good luck and happy coding!

Leave a Comment

Your email address will not be published. Required fields are marked *