Creating a Minesweeper Game with JavaScript

JavaScriptJavaScriptBeginner
Practice Now

Introduction

I believe everyone has played the classic game of Minesweeper before. It has simple rules but is highly addictive. Have you ever thought about developing one yourself? Today, we will create a web-based version of Minesweeper. First, let's take a look at a screenshot of the interface.

👀 Preview

Minesweeper

🎯 Tasks

In this project, you will learn:

  • How to design the game algorithm for the Minesweeper game
  • How to create the file structure for the project
  • How to implement the page layout using HTML and CSS
  • How to draw the grid using JavaScript
  • How to add click events to the cells to handle gameplay
  • How to implement game control functions such as game start and end

🏆 Achievements

After completing this project, you will be able to:

  • Design and implement game algorithms
  • Create file structure for a web project
  • Use HTML and CSS to create page layouts
  • Use JavaScript to draw grids and handle events
  • Implement game control functions

Development Preparation

Before starting development, let's first design the game algorithm.

The rules of the Minesweeper game are simple:

  • There are several squares on the game board, each square contains a number (blank indicates the number is 0) or a bomb. The number in the square represents the number of bombs in the surrounding squares. The player's task is to find the number squares with as little time as possible.
  • Except for the squares on the edges, each square has 8 neighboring squares: up, down, left, right, and 4 diagonal squares. Therefore, the number range is from 0 to 8.

So, our algorithm is as follows:

  • Based on the difficulty level selected by the user (there are three levels: beginner, intermediate, and advanced, with more bombs and squares as the level increases), randomly generate a certain number of bombs and place them randomly on the squares. Then, traverse the squares, calculate the number in each square, and mark it on the square. When the player left-clicks on a square, the square's content is displayed (if the square contains a bomb, the challenge fails and the game ends), and when the player right-clicks on a square, the square is marked as a bomb. The challenge is successful only when all bombs are correctly marked and all non-bomb squares are opened, and the game ends.

Handy tip: Since the number range in the squares is from 0 to 8, we can consider marking the numbers in the squares where the bombs are located as 9 to facilitate calculation.

Project File Structure

First, we need to create the following file structure under the ~/project path:

~/project
 |__ index.html
 |__ index.css
 |__ index.js
 |__ jms.js
 |__ mine.png
 |__ flag.png

The mine.png and flag.png in the project directory are images for the mine and flag respectively.

Page Layout

First, we need to have a panel to display game information, including the number of remaining mines, elapsed time, difficulty level, etc. Since the number of squares is not fixed, we will not draw the squares for now and instead draw them in the JS code.

Create an index.html file and add the following code:

<!-- index.html -->
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Minesweeper with JavaScript</title>
    <link
      href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css"
      rel="stylesheet"
    />
    <link rel="stylesheet" type="text/css" href="index.css" />
  </head>
  <body class="bg-gray-100">
    <div
      id="JMS_main"
      class="container mx-auto p-4 bg-white shadow-lg rounded-lg"
    >
      <table id="landmine" class="mx-auto"></table>
      <div id="operation" class="text-center">
        <div class="text-lg text-red-600">
          Number of remaining mines:
          <span class="font-semibold" id="landMineCount">0</span>
        </div>
        <div class="text-lg text-orange-500">
          Time of duration: <span class="font-semibold" id="costTime">0</span> s
        </div>
        <fieldset class="my-4">
          <legend class="text-lg font-semibold">Difficulty selection:</legend>
          <input
            type="radio"
            name="level"
            id="llevel"
            checked="checked"
            value="10"
            class="mr-2"
          />
          <label for="llevel">Primary (10*10) </label><br />
          <input
            type="radio"
            name="level"
            id="mlevel"
            value="15"
            class="mr-2"
          />
          <label for="mlevel">Intermediate (15*15) </label><br />
          <input
            type="radio"
            name="level"
            id="hlevel"
            value="20"
            class="mr-2"
          />
          <label for="hlevel">Advanced (20*20) </label><br />
        </fieldset>
        <button
          id="begin"
          class="px-4 py-2 bg-blue-500 text-white font-semibold rounded-lg hover:bg-blue-700"
        >
          Start Game</button
        ><br />
        <div class="text-left mt-4">
          <div class="font-semibold">Hint:</div>
          <ul class="list-disc ml-6">
            <li>1. Click "Start Game" to start the game timer.</li>
            <li>
              2. Clicking "Start Game" during the game will start a new game.
            </li>
          </ul>
        </div>
      </div>
    </div>
    <script src="jms.js"></script>
    <script src="index.js"></script>
  </body>
</html>
✨ Check Solution and Practice

Page Layout Styles

Next, we need to adjust the position of the game information on the panel and add some styles. Add the following code to index.css and save:

@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

.container {
  max-width: 600px;
}
table {
  background: #ccc;
  margin: 0 auto;
}
td {
  border: 2px outset #eee;
  font-size: 20px;
  width: 32px;
  height: 32px;
  text-align: center;
  cursor: pointer;
}
button {
  padding: 2px 10px;
  margin: 5px;
  font-size: 20px;
  cursor: pointer;
}

.clicked-cell {
  background-color: #a0aec0;
  color: #fff;
  font-weight: bold;
}

.landMine {
  background-image: url(mine.png);
  background-position: center;
  background-repeat: no-repeat;
}

.flag {
  background-image: url(flag.png);
  background-position: center;
  background-repeat: no-repeat;
}

After completing this step, open index.html in your browser.

Minesweeper
Minesweeper
✨ Check Solution and Practice

Drawing Grid

After completing the previous steps, we need to draw the grid. In order to make the code clearer, we separate the game implementation part and the calling part. The game implementation part is placed in jms.js, which is in the same directory as index.html, and the game calling part is placed in index.js, also in the same directory.

To draw the grid, we need to pass in some parameters, such as the id of the table where the grid will be placed, and the number of cells (represented by the number of rows and columns). In addition, other game-related data needs to be initialized.

jms.js Part

Add the following code to jms.js and save it:

(function () {
  // Initialize the Minesweeper object and initialize the data
  var JMS = function (
    id,
    rowCount,
    colCount,
    minLandMineCount,
    maxLandMineCount
  ) {
    if (!(this instanceof JMS))
      return new JMS(
        id,
        rowCount,
        colCount,
        minLandMineCount,
        maxLandMineCount
      );
    this.doc = document;
    this.table = this.doc.getElementById(id); // Table for drawing the grid
    this.cells = this.table.getElementsByTagName("td"); // Cells
    this.rowCount = rowCount || 10; // Number of rows in the grid
    this.colCount = colCount || 10; // Number of columns in the grid
    this.landMineCount = 0; // Number of mines
    this.markLandMineCount = 0; // Number of marked mines
    this.minLandMineCount = minLandMineCount || 10; // Minimum number of mines
    this.maxLandMineCount = maxLandMineCount || 20; // Maximum number of mines
    this.arrs = []; // Array corresponding to the cells
    this.beginTime = null; // Start time of the game
    this.endTime = null; // End time of the game
    this.currentSetpCount = 0; // Number of steps taken
    this.endCallBack = null; // Callback function when the game ends
    this.landMineCallBack = null; // Callback function to update the remaining number of mines when a mine is marked
    this.doc.oncontextmenu = function () {
      // Disable right-click menu
      return false;
    };
    this.drawMap();
  };

  // Create cells in the prototype of JMS
  JMS.prototype = {
    // Draw the grid
    drawMap: function () {
      var tds = [];
      // For browser compatibility
      if (
        window.ActiveXObject &&
        parseInt(navigator.userAgent.match(/msie ([\d.]+)/i)[1]) < 8
      ) {
        // Create a new CSS style file
        var css = "#JMS_main table td{background-color:#888;}",
          // Get the head tag
          head = this.doc.getElementsByTagName("head")[0],
          // Create a style tag
          style = this.doc.createElement("style");
        style.type = "text/css";
        if (style.styleSheet) {
          // Assign the CSS style to the style tag
          style.styleSheet.cssText = css;
        } else {
          // Create a node in the style tag
          style.appendChild(this.doc.createTextNode(css));
        }
        // Append the style tag as a child tag of the head tag
        head.appendChild(style);
      }
      // Loop to create the table
      for (var i = 0; i < this.rowCount; i++) {
        tds.push("<tr>");
        for (var j = 0; j < this.colCount; j++) {
          tds.push("<td id='m_" + i + "_" + j + "'></td>");
        }
        tds.push("</tr>");
      }
      this.setTableInnerHTML(this.table, tds.join(""));
    },
    // Add HTML to the table
    setTableInnerHTML: function (table, html) {
      if (navigator && navigator.userAgent.match(/msie/i)) {
        // Create a div within the owner document of the table
        var temp = table.ownerDocument.createElement("div");
        // Create the content of the table's tbody
        temp.innerHTML = "<table><tbody>" + html + "</tbody></table>";
        if (table.tBodies.length == 0) {
          var tbody = document.createElement("tbody");
          table.appendChild(tbody);
        }
        table.replaceChild(temp.firstChild.firstChild, table.tBodies[0]);
      } else {
        table.innerHTML = html;
      }
    }
  };

  window.JMS = JMS;
})();

The above code includes some code to make it compatible with IE browsers, which can be ignored.

index.js Part

In the calling code in index.js, we need to bind the event of the difficulty selection buttons, and then call the JMS we defined above to start drawing the grid.

Add the following code to index.js and save it:

var jms = null,
  timeHandle = null;
window.onload = function () {
  var radios = document.getElementsByName("level");
  for (var i = 0, j = radios.length; i < j; i++) {
    radios[i].onclick = function () {
      if (jms != null)
        if (jms.landMineCount > 0)
          if (!confirm("Are you sure you want to end the current game?"))
            return false;
      var value = this.value;
      init(value, value, (value * value) / 5 - value, (value * value) / 5);
      document.getElementById("JMS_main").style.width =
        value * 40 + 180 + 60 + "px";
    };
  }
  init(10, 10);
};

function init(rowCount, colCount, minLandMineCount, maxLandMineCount) {
  var doc = document,
    landMineCountElement = doc.getElementById("landMineCount"),
    timeShow = doc.getElementById("costTime"),
    beginButton = doc.getElementById("begin");
  if (jms != null) {
    clearInterval(timeHandle);
    timeShow.innerHTML = 0;
    landMineCountElement.innerHTML = 0;
  }
  jms = JMS("landmine", rowCount, colCount, minLandMineCount, maxLandMineCount);
}

Then open index.html in the browser, and the grid should be displayed. The effect is as follows:

Minesweeper

Click on the difficulty selection on the right to see the number of cells change.

✨ Check Solution and Practice

Game Initialization

Now, let's begin initializing the game, which mainly involves three steps:

  1. Initialize all cells (represented by an array in the code) to 0.
  2. Generate a random number of landmines and place them randomly in the array, setting the value of the array item to 9.
  3. Calculate the numbers in other cells and store the values in the array.

Add the following code inside JMS.prototype in jms.js:

// Initialization: set the default value of the array to 0 and determine the number of landmines
init: function () {
    for (var i = 0; i < this.rowCount; i++) {
        this.arrs[i] = [];
        for (var j = 0; j < this.colCount; j++) {
            this.arrs[i][j] = 0;
        }
    }
    this.landMineCount = this.selectFrom(this.minLandMineCount, this.maxLandMineCount);
    this.markLandMineCount = 0;
    this.beginTime = null;
    this.endTime = null;
    this.currentSetpCount = 0;
},
// Set the value of array items that contain landmines to 9
landMine: function () {
    var allCount = this.rowCount * this.colCount - 1,
        tempArr = {};
    for (var i = 0; i < this.landMineCount; i++) {
        var randomNum = this.selectFrom(0, allCount),
            rowCol = this.getRowCol(randomNum);
        if (randomNum in tempArr) {
            i--;
            continue;
        }
        this.arrs[rowCol.row][rowCol.col] = 9;
        tempArr[randomNum] = randomNum;
    }
},
// Calculate the numbers in other cells
calculateNoLandMineCount: function () {
    for (var i = 0; i < this.rowCount; i++) {
        for (var j = 0; j < this.colCount; j++) {
            if (this.arrs[i][j] == 9)
                continue;
            if (i > 0 && j > 0) {
                if (this.arrs[i - 1][j - 1] == 9)
                    this.arrs[i][j]++;
            }
            if (i > 0) {
                if (this.arrs[i - 1][j] == 9)
                    this.arrs[i][j]++;
            }
            if (i > 0 && j < this.colCount - 1) {
                if (this.arrs[i - 1][j + 1] == 9)
                    this.arrs[i][j]++;
            }
            if (j > 0) {
                if (this.arrs[i][j - 1] == 9)
                    this.arrs[i][j]++;
            }
            if (j < this.colCount - 1) {
                if (this.arrs[i][j + 1] == 9)
                    this.arrs[i][j]++;
            }
            if (i < this.rowCount - 1 && j > 0) {
                if (this.arrs[i + 1][j - 1] == 9)
                    this.arrs[i][j]++;
            }
            if (i < this.rowCount - 1) {
                if (this.arrs[i + 1][j] == 9)
                    this.arrs[i][j]++;
            }
            if (i < this.rowCount - 1 && j < this.colCount - 1) {
                if (this.arrs[i + 1][j + 1] == 9)
                    this.arrs[i][j]++;
            }
        }
    }
},
// Get a random number
selectFrom: function (iFirstValue, iLastValue) {
    var iChoices = iLastValue - iFirstValue + 1;
    return Math.floor(Math.random() * iChoices + iFirstValue);
},
// Find the row and column numbers based on the value
getRowCol: function (val) {
    return {
        row: parseInt(val / this.colCount),
        col: val % this.colCount
    };
},
✨ Check Solution and Practice

Add Click Events to Cells

Now, it is necessary to add click events to the cells. When left-clicked, display the number in the cell (if it is a mine, the game will end). When right-clicked, mark it as a mine.

In addition, when the cell is clicked for the first time (usually involving luck), if there is a blank area around it, it should be directly expanded.

Add the following code to the JMS.prototype in jms.js:

// Get element
$: function (id) {
    return this.doc.getElementById(id);
},
// Bind click events (left and right) to each cell
bindCells: function () {
    var self = this;
    for (var i = 0; i < this.rowCount; i++) {
        for (var j = 0; j < this.colCount; j++) {
            (function (row, col) {
                self.$("m_" + i + "_" + j).onmousedown = function (e) {
                    e = e || window.event;
                    var mouseNum = e.button;
                    var className = this.className;
                    if (mouseNum == 2) {
                        if (className == "flag") {
                            this.className = "";
                            self.markLandMineCount--;
                        } else {
                            this.className = "flag";
                            self.markLandMineCount++;
                        }
                        if (self.landMineCallBack) {
                            self.landMineCallBack(self.landMineCount - self.markLandMineCount);
                        }
                    } else if (className != "flag") {
                        self.openBlock.call(self, this, row, col);
                    }
                };
            })(i,j);
        }
    }
},
// Expand the area with no mines
showNoLandMine: function (x, y) {
    for (var i = x - 1; i < x + 2; i++)
        for (var j = y - 1; j < y + 2; j++) {
            if (!(i == x && j == y)) {
                var ele = this.$("m_" + i + "_" + j);
                if (ele && ele.className == "") {
                    this.openBlock.call(this, ele, i, j);
                }
            }
        }
},
// Display
openBlock: function (obj, x, y) {
    if (this.arrs[x][y] != 9) {
        this.currentSetpCount++;
        if (this.arrs[x][y] != 0) {
            obj.innerHTML = this.arrs[x][y];
        }
        obj.className = "clicked-cell";
        if (this.currentSetpCount + this.landMineCount == this.rowCount * this.colCount) {
            this.success();
        }
        obj.onmousedown = null;
        if (this.arrs[x][y] == 0) {
            this.showNoLandMine.call(this, x, y);
        }
    } else {
        this.failed();
    }
},
// Display mines
showLandMine: function () {
    for (var i = 0; i < this.rowCount; i++) {
        for (var j = 0; j < this.colCount; j++) {
            if (this.arrs[i][j] == 9) {
                this.$("m_" + i + "_" + j).className = "landMine";
            }
        }
    }
},
// Show information of all cells
showAll: function () {
    for (var i = 0; i < this.rowCount; i++) {
        for (var j = 0; j < this.colCount; j++) {
            if (this.arrs[i][j] == 9) {
                this.$("m_" + i + "_" + j).className = "landMine";
            } else {
                var ele=this.$("m_" + i + "_" + j);
                if (this.arrs[i][j] != 0)
                    ele.innerHTML = this.arrs[i][j];
                ele.className = "normal";
            }
        }
    }
},
// Clear displayed cell information
hideAll: function () {
    for (var i = 0; i < this.rowCount; i++) {
        for (var j = 0; j < this.colCount; j++) {
            var tdCell = this.$("m_" + i + "_" + j);
            tdCell.className = "";
            tdCell.innerHTML = "";
        }
    }
},
// Remove the events bound to the cells
disableAll: function () {
    for (var i = 0; i < this.rowCount; i++) {
        for (var j = 0; j < this.colCount; j++) {
            var tdCell = this.$("m_" + i + "_" + j);
            tdCell.onmousedown = null;
        }
    }
},
✨ Check Solution and Practice

Adding Game Control Function

Up to now, the main part of the game has been completed. The next step is to add game control functions to make the game run smoothly. The main steps are as follows:

  1. Add a click event listener to the start button to reset the game parameters.
  2. Start counting time when the game starts.
  3. Stop counting time and prompt when the game ends.

jms.js Section

Add the game entry and start function to the JMS.prototype in jms.js:

// Game start
begin: function() {
    this.currentSetpCount = 0; // Reset the step count to zero
    this.markLandMineCount = 0;
    this.beginTime = new Date(); // Game start time
    this.hideAll();
    this.bindCells();
},
// Game end
end: function() {
    this.endTime = new Date(); // Game end time
    if (this.endCallBack) { // Call the callback function if it exists
        this.endCallBack();
    }
},
// Game success
success: function() {
    this.end();
    this.showAll();
    this.disableAll();
    alert("Congratulation!");
},
// Game failed
failed: function() {
    this.end();
    this.showAll();
    this.disableAll();
    alert("GAME OVER!");
},
// Entry point
play: function() {
    this.init();
    this.landMine();
    this.calculateNoLandMineCount();
},

index.js Section

In index.js, add an event listener to the start button to start the game, and display the game time and remaining number of landmines (after jms = JMS("landmine", rowCount, colCount, minLandMineCount, maxLandMineCount); in the init function of index.js):

jms.endCallBack = function () {
  clearInterval(timeHandle);
};
jms.landMineCallBack = function (count) {
  landMineCountElement.innerHTML = count;
};

// Bind event to "Start Game" button
beginButton.onclick = function () {
  jms.play(); // Initialize the game

  // Display the number of landmines
  landMineCountElement.innerHTML = jms.landMineCount;

  // Start the game
  jms.begin();

  // Update the elapsed time
  timeHandle = setInterval(function () {
    timeShow.innerHTML = parseInt((new Date() - jms.beginTime) / 1000);
  }, 1000);
};
✨ Check Solution and Practice

Running and Testing

At this point, our web version of Minesweeper is complete. Open the index.html file in your browser to start playing.

Click on Go Live button in the bottom right corner of WebIDE, and switch to the Web 8080 tab.

Minesweeper
✨ Check Solution and Practice

Summary

This experiment mainly uses JavaScript to implement a web version of the classic game Minesweeper. It is believed that through this experiment, users can improve their understanding and application skills of JavaScript. In addition, users can also learn how to use the JavaScript language to abstract and encapsulate objects in the game.

Other JavaScript Tutorials you may like