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
🎯 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:
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:
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:
Click on the difficulty selection on the right to see the number of cells change.
Now, let's begin initializing the game, which mainly involves three steps:
Initialize all cells (represented by an array in the code) to 0.
Generate a random number of landmines and place them randomly in the array, setting the value of the array item to 9.
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
};
},
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;
}
}
},
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:
Add a click event listener to the start button to reset the game parameters.
Start counting time when the game starts.
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);
};
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.
We use cookies for a number of reasons, such as keeping the website reliable and secure, to improve your experience on our website and to see how you interact with it. By accepting, you agree to our use of such cookies. Privacy Policy