Minesweeper

Everyone remembers the Windows game “Minesweeper”, and I thought it would make for a fun vanilla Javascript, HTML, and CSS project, especially since I can get it to run on WordPress here without any additional plugins or scripts.

Minesweeper Game

Right click to flag a cell. Left click to reveal a cell. Right click to remove a flag.
On mobile, tap and hold to place and remove flags.

     
Congrats! You win! Click a board size to play again.
Whoops! You died! Click a board size to try again.

Process

First, I started by building an HTML document with a grid structure, where each cell in the grid had a variable width and height.

/* Define the grid properties */
#grid {
    display: grid;
    gap: 1px;
    width: 60vw;
    height: 60vw;
    max-width: 60vh;
    max-height: 60vh;
    margin: 50px auto 0;
}

.cell {
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 16px;
    font-family: "Open Sans", sans-serif;
    background-color: #ddd;
    cursor: pointer;
    box-sizing: border-box;
    padding-top: 25px;
}
<div id="grid">
    <div class="cell"></div>
    <div class="cell"></div>
    <div class="cell"></div>
    <div class="cell"></div>
    <div class="cell"></div>
    <div class="cell"></div>
    <div class="cell"></div>
    <!-- add more cells... -->
</div>

Now, instead of manually adding each cell to the grid, which would make the game more static than I’d want, I used JavaScript to render each cell programmatically, and I adjusted the style of the cells and the grid to match the number of cells in the grid.

function resetBoard() {
    //get the board and # of cells
    const board = document.getElementById("grid");=
    board.innerHTML = "";
    const cellCount = 15 * 15;

    // Create an array to store the cell elements
    const cells = [];

    // Create the cell elements and add them to the array
    for (let i = 0; i < cellCount; i++) {
        const cell = document.createElement("div");
        cell.setAttribute('data-count', "0");
        cell.classList.add("cell");
        cell.dataset.index = i;
        board.appendChild(cell);
        cells.push(cell);
    }

    // Set the CSS grid properties for the board
    board.style.display = "grid";
    board.style.gridTemplateColumns = "repeat(15, 1fr)";
    board.style.gridTemplateRows = "repeat(15, 1fr)";


    //add cell styles
    cells.forEach(function (cell) {
        cell.style.display = "flex";
        cell.style.justifyContent = "center";
        cell.style.alignItems = "center";
    });
}

//load the grid after the DOM content is rendered.
document.addEventListener("DOMContentLoaded", function () {
    resetBoard();
});

Now that we have a grid, it’s time to add some bomb and flag styles, and to add the bombs to the board.

/* cell states */
.revealed {
    background-color: #f8f8f8;
    border: #ddd solid 1px;
}
.bomb {
    background-color: #f00;
}
.flagged {
    background-color: #008000;
    font-size: 0;
}
.hidden {
    background-color: #ddd;
    font-size: 0;
}
//set the bombs
for (let i = 0; i < 15; i++) {
    let randomIndex = Math.floor(Math.random() * cellCount);        
    cells[randomIndex].classList.add("bomb");
}

With the bomb classes added, if we look at the resulting webpage, we see a grid of grey and red cells, where the red cells are the bombs, and the greys are not.

By revealing the bombs in this stage, I can check that the code is working to add the bombs correctly, and I can later check the code that adds the neighbor-numbers to the grid.

Calculating the neighbor-numbers was the most difficult part of the development. In minesweeper, a cell that contains the number 1 is indicating that one of the neighbor cells is a bomb. A cell with a neighbor-number of 2 means that 2 of the surrounding cells are bombs.

Calculating the neighbor number itself is easy, you just need to iterate through the grid, getting the top, bottom, left, right, and diagonal neighbors, and counting up from 0 if there is a bomb. The resulting number should be the number of bombs in a square. like so:

However, we can’t just surround each bomb with 1’s and 2’s, because there are cases where bombs can be neighbors with other bombs, like so:

This means that the bomb on the left isn’t just surrounded by 1’s, it’s surrounded by 1’s, 2’s and 3’s, because of the neighboring bombs on the right. So, iterating through each cell in the list, I need to check if the cell is a bomb. If it is NOT a bomb, then I need to check the surrounding neighbors, and calculate the neighbor numbers for that cell:

if (!cell.classList.contains("bomb")) {
    let count = 0;
    const index = parseInt(cell.dataset.index);
    const x = index % 15;
    const y = Math.floor(index / 15);
    for (let dx = -1; dx <= 1; dx++) {
        for (let dy = -1; dy <= 1; dy++) {
            if (dx === 0 && dy === 0) {
                continue;
            }
            const neighborX = x + dx;
            const neighborY = y + dy;
            if (neighborX >= 0 && neighborX < 15 && neighborY >= 0 && neighborY < 15) {
                const neighborIndex = neighborY * 15 + neighborX;
                const neighborCell = cells[neighborIndex];
                if (neighborCell.classList.contains("bomb")) {
                    count++;
                }
            }
        }
    }
    if (count > 0) {
        cell.textContent = count;
        cell.setAttribute("data-count", count);
    }
} else {
    cell.setAttribute("data-count", "A");
}

Which results in a nice grid of bombs with the neighbor numbers calculated and displayed:

Now for the truly difficult part. When playing minesweeper, if you click on a cell that doesn’t have a number, that entire area is revealed. Related numbers will reveal themselves but only if they directly neighbor a 0 cell that has already been revealed. This part is tricky because it’s not just the neighbors we have to calculate, it’s the neighbors of the neighbors to the nth degree, until there are no more revealable tiles.

So I start by adding the hidden class to all the cells, making each cell “unknown” to the user (though, if you used inspect element on the browser, you can clearly see the “bomb” classes and cheat that way, but it’s less fun).

.hidden {
    background-color: #ddd;
    font-size: 0;
}
// Create the cells
for (let i = 0; i < cellCount; i++) {
    const cell = document.createElement("div");
    cell.setAttribute('data-count', "0");
    cell.classList.add("cell");
    //hide all the cells
    cell.classList.add("hidden");
    cell.dataset.index = i;
    board.appendChild(cell);
    cells.push(cell);
}

And now we have a fully hidden minesweeper board:

To handle the revealing, we need to add a listener to each cell that listens for a click. For starters, I added an event listener that only listened for left-clicks, and when a given cell is clicked, the hidden class is removed and the revealed class is applied:

This method doesn’t give us any actual gameplay. If you click on a bomb, you don’t lose, and if you click on a 0 cell, nothing else is revealed. So the next step is to calculate how many cells to reveal and to set the loss condition for the game.

I wrote a function called revealCell(cell) that runs on each cell’s click event.

cells.forEach(function () {
    cell.addEventListener("click", function () {
        cell.classList.remove('hidden');
        cell.classList.add('revealed');
    });
});
function revealCell(cell) {
    //if it is a bomb, reveal all cells
    //and end the game
    if (cell.classList.contains('bomb')) {
        cells.forEach(function (cell) {
            cell.classList.remove('hidden');
        });
        //set timeout function to alert user that they lost
        setTimeout(function () {
            alert('you lost!');
        }, 100);
    } 
    //if it is not a bomb and is still hidden,
    //reveal the cell
    else if (cell.classList.contains("hidden")) {
        cell.classList.remove("hidden");
        cell.classList.add('revealed');
    }
}

Now, with the revealCell function partially functional, it’s time to loop through the cells to reveal zero spaces:

cells.forEach(function () {
    cell.addEventListener("click", function () {
        revealCell(cell)
    });
});
function revealCell(cell) {
    if (cell.classList.contains('bomb')) {
        cells.forEach(function (cell) {
            cell.classList.remove('hidden');
        });
        //set timeout function to alert user that they lost
        setTimeout(function () {
            document.getElementById('loss').classList.remove('d-none');
        }, 100);
    } else if (cell.classList.contains("hidden")) {
        cell.classList.remove("hidden");
        cell.classList.add('revealed');
        if (cell.getAttribute("data-count") === "0") {
            const index = parseInt(cell.dataset.index);
            const x = index % 15;
            const y = Math.floor(index / 15);
            for (let dx = -1; dx <= 1; dx++) {
                for (let dy = -1; dy <= 1; dy++) {
                    if (dx === 0 && dy === 0) {
                        continue;
                    }
                    const neighborX = x + dx;
                    const neighborY = y + dy;
                    if (neighborX >= 0 && neighborX < 15 && neighborY >= 0 && neighborY < 15) {
                        const neighborIndex = neighborY * 15 + neighborX;
                        const neighborCell = cells[neighborIndex];
                        if (neighborCell.classList.contains("hidden")) {
                            revealCell(neighborCell);
                        } else if (!neighborCell.classList.contains("bomb") && neighborCell.getAttribute("data-count") !== "0") {
                            neighborCell.classList.remove("hidden");
                            neighborCell.classList.add('revealed');
                        }
                    }
                }
            }
        }
        if (checkWin() === true) {
            setTimeout(function () {
                document.getElementById('win').classList.remove('d-none');
            }, 100);
        }
    }
}

Now, the cells will reveal themselves properly, and on each click, the game will check for win conditions and loss conditions.

After a little bit of formatting, adding alert divs instead of using the default javascript alert, I was able to replace the 15×15 grid with a resizable grid and adjustable bomb numbers defined by the formula numBombs = SIZE + (Math.floor(SIZE / 3)), and add buttons to reset the board with each size.

Scroll back to the top and give the final game a try!