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!
TABLE OF CONTENTS
JAVASCRIPT MINESWEEPER
All right, let us now get into the mechanics of the Minesweeper game.
JAVASCRIPT MINESWEEPER DEMO
PART 1) THE 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
/* (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
andflex-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
var mine = {
// (A) PROPERTIES
// (A1) GAME SETTINGS
total : 10, // total number of mines
height : 10, // number of rows
width : 5, // 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. Ifmine.rCell == mine.total
, then the player has successfully found all mines and wins.
PART 4) INITIALIZE/RESET GAME
// (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++) {
// (B3-1) 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
});
// (B3-2) 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 = " ";
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 andmine.rCell
toWIDTH X HEIGHT
. - (B2) Empties out the HTML container
<div id="mine-wrap">
, calculate the width of each cell. - (B3) Loop through
mine.width
andmine.height
to generate the cells. Take note of how the data for the cells are kept in the format ofmine.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
// (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
// (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 {
// (D3-1) 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]);
// (D3-2) 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();
}
// (D3-3) 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;
}
}
// (D3-4) 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.
DOWNLOAD & NOTE
Here is the download link to the example code, so you don’t have to copy-paste everything.
SORRY FOR THE ADS...
But someone has to pay the bills, and sponsors are paying for it. I insist on not turning Code Boxx into a "paid scripts" business, and I don't "block people with Adblock". Every little bit of support helps.
Buy Me A Coffee Code Boxx eBooks
EXAMPLE CODE DOWNLOAD
Click here for the source code on GitHub gist, just click on “download zip” or do a git clone. I have released it under the MIT license, so feel free to build on top of it or use it in your own project.
EXTRA BITS & LINKS
That’s all for the tutorial, and here is a small section on some extras and links that may be useful to you.
COMPATIBILITY CHECKS
- Arrow Functions – CanIUse
- Dataset – CanIUse
This example will work on all “Grade A” browsers.
LINKS & REFERENCES
- Example On CodePen – JS Minesweeper
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!
Hi! 😀
Was looking at your code and I believe I have noticed a bug. I am not that great at the game, so I might be completely wrong. If so, I apologize in advance 🙂 I have played through the game quite a few times, and I feel like the number of mines does not always correlate to the number in the cell. My friends and boyfriend have noticed the same.
Otherwise, it’s an amazing code!
Is this something you’ve spotted yourself? Would love to see the updated code if you are able to correct the bug!
Have a great day!
Kind regards,
Synne
Shall mark this as a “possible bug”. Thanks for reporting.