Build a Minesweeper game using JavaScript


Minesweeper is a single-player puzzle game. The goal of the player is to clear a rectangular board containing hidden "mines" or bombs without detonating any of them, with help from clues about the number of neighboring mines in each cell.

In this article, we'll learn how to build a basic minesweeper game using JavaScript. This is how it will look:

Minesweeper

Tech stack: The tech stack that will be used is:

Yes, no frameworks or library is to be used, we'll develop the game using Vanilla JS only.

Implementation rule and algorithm:

The rules based on which the game needs to be implemented are-

  1. In the first step, the player has to click on a random square and just hope it's not a bomb.

  2. If the player clicks on a safe area, the square will either open up to be blank (which is mine) or will contain a number from 1 to 8.

    These numbers specify the number of bombs that are adjacent to that square, i.e. n means there are n bombs adjacent to that square.

  3. This way the player needs to go ahead with calculating which square can contain the bombs. These calculations are to be performed based on multiple squares which can determine the probability of a square having bombs.

  4. If the player clicks on a unsafe square which contains bomb, the game gets over.

Colour code of different elements-

  • Background: #626262/rgb(98,98,98)

  • Blank Tiles: #C3C3C3/rgb(195,195,195)

  • Fonts: #F5F5F5/rgb(245,245,245)

  • Numbers: Multiple rainbow colors

Unicode for bomb emoji-

  • Character: 💣
  • Codepoints: U+1F4A3
  • Shortcodes: :bomb:

Setting up the structure:

Let's set the basic structure of the game using HTML.

First, we need to include the static files in our index.html file. So including these following links will set the path to our static CSS and JavaScript files, i.e. index.css and index.js.

<link rel="stylesheet" href="index.css" type="text/css">
<script type="text/javascript" src="./index.js"></script>

Now, let's set the body part of html file.

<body>
      <div id="box">
         <h2>MINESWEEPER!</h2><hr><br><br>
            <div id="field"></div>
            <br>
          <div id="lost" style="display: none;">
            <h3>You got bombed!</h3>
            <button id="new-game-button" type="button" onclick="reload()">Start Again</button>
          </div>         
      </div>
</body>

As shown in the above code, a heading is set as 'MINESWEEPER' by the h2 tag. the a div with id = 'field' element has been created for the table to show up. We'll see how to do that in the JavaScript section. Also a div section is created fow showing the message when the user loses the game. A button is created with id = 'new-game-button', upon clicking that the game will restart.

Here's the full HTML code-

<!DOCTYPE html>
<html>
  <head>
    <title>Minesweeper</title>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="index.css" type="text/css">
    <script type="text/javascript" src="./index.js"></script>
  </head>
  <body>
      <div id="box">
         <h2>MINESWEEPER!</h2><hr><br><br>
            <div id="field"></div>
            <br>
          <div id="lost" style="display: none;">
            <h3>You got bombed!</h3>
            <button id="new-game-button" type="button" onclick="reload()">Start Again</button>
          </div>         
      </div>
  </body>
</html>

Styling the elements:

Now, let's add CSS to our HTML structure.

body{
    background-color: rgb(98, 98, 98);
}

h2, h3{
    color: whitesmoke;
}

#box {
    text-align: center;
}

As you can see, the background colour has been set to the value of rgb(98, 98, 98) by the background-color property. Also the heading tags are coloured as whitesmoke colour. the minesweeper table is centered to the page by the text-align property.

Next, let' style the table with background colour as rgb(195, 195, 195), height and width as 30px and border with 1px width and solid black style.

#field table {
    border-collapse: collapse;
    font-size: 150%;
    font-family: san-serif;
    font-weight: bold;
    display: inline-block;
}

#field table td {
    background-color: rgb(195, 195, 195);
    width: 30px;
    min-width: 30px;
    height: 30px;
    min-height: 30px;
    border: 1px solid black;    
}

Now, for styling the button, the colour is kept constant according to the theme. The widht is given as 100px and height as 30px, while the border radius are 20px in all side.

button {
    width: 100px;
    height: 30px;
    background-color:rgb(195, 195, 195);
    color: black;
    font-weight: bolder;
    border-radius: 20px 20px 20px 20px;
}

Here's the full CSS code-

body{
    background-color: rgb(98, 98, 98);
}

h2, h3{
    color: whitesmoke;
}

#box {
    text-align: center;
}

#field table {
    border-collapse: collapse;
    font-size: 150%;
    font-family: san-serif;
    font-weight: bold;
    display: inline-block;
}

#field table td {
    background-color: rgb(195, 195, 195);
    width: 30px;
    min-width: 30px;
    height: 30px;
    min-height: 30px;
    border: 1px solid black;    
}

button {
    width: 100px;
    height: 30px;
    background-color:rgb(195, 195, 195);
    color: black;
    font-weight: bolder;
    border-radius: 20px 20px 20px 20px;
}

Setting up the logic:

Now, it's time to set up the logic and functionality of the game using JavaScript.

First of all, let;s set the different components as standard values in dictionary form. We'll use these components repeatedly throughout the code.

var components = {
    num_of_rows : 12,
    num_of_cols : 24,
    num_of_bombs : 55,
    bomb : '💣',
    alive : true,
    colors : {1: 'blue', 2: 'green', 3: 'red', 4: 'purple', 5: 'maroon', 6: 'turquoise', 7: 'black', 8: 'grey'}
}

Number of rows and coloumns of the table are set as 12 nad 24. Also number of bombs is set to 55. This numbers can be changed. The bomb will be shown as special character '💣' which is set to the 'bomb' variable in the dictionary. A flag variable is set as 'true', i.e. alive: true. Also, different colours are set to different numbers that will be shown in the table.

window.addEventListener('load', function() {
    document.getElementById('lost').style.display="none";
    startGame();
});

The goal of the above code is to invoke the startGame() function whenever the page is loaded. This is done by the addEventListener() method of the window DOM with 'load' parameter. The message section which is to be shown after losing the game is set as hidden here.

function startGame() {
    components.bombs = placeBombs();
    document.getElementById('field').appendChild(createTable());
}

This function is used to create the table and initialize the bombs.
placeBombs() and createTable() functions are used for creating the table and initializing the bombs. The table is created and the section is added to the 'field' division in the DOM by appendChild() method.

Now, let's see the createTable() function first.

function cellID(i, j) {
    return 'cell-' + i + '-' + j;
}

function createTable() {
    var table, row, td, i, j;
    table = document.createElement('table');
    
    for (i=0; i<components.num_of_rows; i++) {
        row = document.createElement('tr');
        for (j=0; j<components.num_of_cols; j++) {
            td = document.createElement('td');
            td.id = cellID(i, j);
            row.appendChild(td);
            addCellListeners(td, i, j);
        }
        table.appendChild(row);
    }
    return table;
}

As shown in the code, 'table' element is created and stored to the table variable. Now by running a loop, table rows and coloumns are created according to the number of rows anf coloumns as initialized before in the dictionary. CellID() function returns the current cell id which is added to the row element.

Now, let's see how to place the bombs.

placeBombs() runs a loop throughout the table, cell by cell and initialize the bombs in certain cells by calling the placeSingleBomb() function.

function placeBombs() {
    var i, rows = [];
    
    for (i=0; i<components.num_of_bombs; i++) {
        placeSingleBomb(rows);
    }
    return rows;
} 

In the placeSingleBomb function, at first the probability of having a bomb in a certain row and coloumn number is selected randomly by the Math.random() function.

Now, if the particular cell is not clicked before or is not mine or is not bomb, we recursively call the placeSingleBomb functions again and when the particular probaility condition is met, the bomb is set.

function placeSingleBomb(bombs) {

    var nrow, ncol, row, col;
    nrow = Math.floor(Math.random() * components.num_of_rows);
    ncol = Math.floor(Math.random() * components.num_of_cols);
    row = bombs[nrow];
    
    if (!row) {
        row = [];
        bombs[nrow] = row;
    }
    
    col = row[ncol];
    
    if (!col) {
        row[ncol] = true;
        return
    } 
    else {
        placeSingleBomb(bombs);
    }
}

Now, let's see the code for handling the mouse click event is the cell and see what happens to the adjacent cells due to that.

adjacentBombs() and adjacentFlags() function returns the states of the adjacent cells, i.e. returns the number of flags in the adjacent cells or the bombs.

function adjacentBombs(row, col) {
    var i, j, num_of_bombs;
    num_of_bombs = 0;

    for (i=-1; i<2; i++) {
        for (j=-1; j<2; j++) {
            if (components.bombs[row + i] && components.bombs[row + i][col + j]) {
                num_of_bombs++;
            }
        }
    }
    return num_of_bombs;
}

function adjacentFlags(row, col) {
    var i, j, num_flags;
    num_flags = 0;

    for (i=-1; i<2; i++) {
        for (j=-1; j<2; j++) {
            cell = document.getElementById(cellID(row + i, col + j));
            if (!!cell && cell.flagged) {
                num_flags++;
            }
        }

Now, let's see the function to handle the click events in the unclicked cells.

function handleCellClick(cell, i, j) {
    if (!components.alive) {
        return;
    }

    if (cell.flagged) {
        return;
    }

    cell.clicked = true;

    if (components.bombs[i][j]) {
        cell.style.color = 'red';
        cell.textContent = components.bomb;
        gameOver();
        
    }
    else {
        cell.style.backgroundColor = 'lightGrey';
        num_of_bombs = adjacentBombs(i, j);
        if (num_of_bombs) {
            cell.style.color = components.colors[num_of_bombs];
            cell.textContent = num_of_bombs;
        } 
        else {
            clickAdjacentBombs(i, j);
        }
    }
}

At first, it is checked if the game is over or the cell is already clicked by the checking components.alive and cell.flagged value. Then if the condition of bombing the cell is met, i.e. bomb array has the threshold probability of bombing in that particular cell, bomb is placed in the cell and gameOver() function is called.

Else, the cell backgound colour is set as lightgrey and then adjacentBombs() is called. If the number of bombs surrounding the cell is not zero, then flags are set accordingly as 1, 2, 3, ....

Otherwise, clickAdjacenBombs() function is invoked.

function clickAdjacentBombs(row, col) {
    var i, j, cell;
    
    for (i=-1; i<2; i++) {
        for (j=-1; j<2; j++) {
            if (i === 0 && j === 0) {
                continue;
            }
            cell = document.getElementById(cellID(row + i, col + j));
            if (!!cell && !cell.clicked && !cell.flagged) {
                handleCellClick(cell, row + i, col + j);
            }
        }
    }
}

function performMassClick(cell, row, col) {
    if (adjacentFlags(row, col) === adjacentBombs(row, col)) {
        clickAdjacentBombs(row, col);
    }
}

Sometimes when the flag is set as the highest value, adjacent cells are automatically set as the flag values accordingly. This is done by the performMassClick() function.

If the adjacent flags and adjacent bombs value are same the clickAdjacentBombs() function is called.

At last the function for losing the game and restarting the game is set.

function gameOver() {
    components.alive = false;
    document.getElementById('lost').style.display="block";
    
}

function reload(){
    window.location.reload();
}

As you can see, the alive flag of the components is set as false and message section is shown.

In the reload() function, window.location.reload() method is invoked to restart the game.

Here's the full JavaScript code-

var components = {
    num_of_rows : 12,
    num_of_cols : 24,
    num_of_bombs : 55,
    bomb : '💣',
    alive : true,
    colors : {1: 'blue', 2: 'green', 3: 'red', 4: 'purple', 5: 'maroon', 6: 'turquoise', 7: 'black', 8: 'grey'}
}

function startGame() {
    components.bombs = placeBombs();
    document.getElementById('field').appendChild(createTable());
}

function placeBombs() {
    var i, rows = [];
    
    for (i=0; i<components.num_of_bombs; i++) {
        placeSingleBomb(rows);
    }
    return rows;
} 

function placeSingleBomb(bombs) {

    var nrow, ncol, row, col;
    nrow = Math.floor(Math.random() * components.num_of_rows);
    ncol = Math.floor(Math.random() * components.num_of_cols);
    row = bombs[nrow];
    
    if (!row) {
        row = [];
        bombs[nrow] = row;
    }
    
    col = row[ncol];
    
    if (!col) {
        row[ncol] = true;
        return
    } 
    else {
        placeSingleBomb(bombs);
    }
}

function cellID(i, j) {
    return 'cell-' + i + '-' + j;
}

function createTable() {
    var table, row, td, i, j;
    table = document.createElement('table');
    
    for (i=0; i<components.num_of_rows; i++) {
        row = document.createElement('tr');
        for (j=0; j<components.num_of_cols; j++) {
            td = document.createElement('td');
            td.id = cellID(i, j);
            row.appendChild(td);
            addCellListeners(td, i, j);
        }
        table.appendChild(row);
    }
    return table;
}

function addCellListeners(td, i, j) {
    td.addEventListener('mousedown', function(event) {
        if (!components.alive) {
            return;
        }
        components.mousewhiches += event.which;
        if (event.which === 3) {
            return;
        }
        if (this.flagged) {
            return;
        }
        this.style.backgroundColor = 'lightGrey';
    });

    td.addEventListener('mouseup', function(event) {
      
        if (!components.alive) {
            return;
        }

        if (this.clicked && components.mousewhiches == 4) {
            performMassClick(this, i, j);
        }

        components.mousewhiches = 0;
        
        if (event.which === 3) {
           
            if (this.clicked) {
                return;
            }
            if (this.flagged) {
                this.flagged = false;
                this.textContent = '';
            } else {
                this.flagged = true;
                this.textContent = components.flag;
            }

            event.preventDefault();
            event.stopPropagation();
          
            return false;
        } 
        else {
            handleCellClick(this, i, j);
        }
    });

    td.oncontextmenu = function() { 
        return false; 
    };
}

function handleCellClick(cell, i, j) {
    if (!components.alive) {
        return;
    }

    if (cell.flagged) {
        return;
    }

    cell.clicked = true;

    if (components.bombs[i][j]) {
        cell.style.color = 'red';
        cell.textContent = components.bomb;
        gameOver();
        
    }
    else {
        cell.style.backgroundColor = 'lightGrey';
        num_of_bombs = adjacentBombs(i, j);
        if (num_of_bombs) {
            cell.style.color = components.colors[num_of_bombs];
            cell.textContent = num_of_bombs;
        } 
        else {
            clickAdjacentBombs(i, j);
        }
    }
}

function adjacentBombs(row, col) {
    var i, j, num_of_bombs;
    num_of_bombs = 0;

    for (i=-1; i<2; i++) {
        for (j=-1; j<2; j++) {
            if (components.bombs[row + i] && components.bombs[row + i][col + j]) {
                num_of_bombs++;
            }
        }
    }
    return num_of_bombs;
}

function adjacentFlags(row, col) {
    var i, j, num_flags;
    num_flags = 0;

    for (i=-1; i<2; i++) {
        for (j=-1; j<2; j++) {
            cell = document.getElementById(cellID(row + i, col + j));
            if (!!cell && cell.flagged) {
                num_flags++;
            }
        }
    }
    return num_flags;
}

function clickAdjacentBombs(row, col) {
    var i, j, cell;
    
    for (i=-1; i<2; i++) {
        for (j=-1; j<2; j++) {
            if (i === 0 && j === 0) {
                continue;
            }
            cell = document.getElementById(cellID(row + i, col + j));
            if (!!cell && !cell.clicked && !cell.flagged) {
                handleCellClick(cell, row + i, col + j);
            }
        }
    }
}

function performMassClick(cell, row, col) {
    if (adjacentFlags(row, col) === adjacentBombs(row, col)) {
        clickAdjacentBombs(row, col);
    }
}

function gameOver() {
    components.alive = false;
    document.getElementById('lost').style.display="block";
    
}

function reload(){
    window.location.reload();
}

window.addEventListener('load', function() {
    document.getElementById('lost').style.display="none";
    startGame();
});

Output:

Minesweeper

Demo link: Click here

With this article at OpenGenus, you must have the complete idea of building a MineSweeper game in JavaScript. Enjoy.