Building Minesweeper in JavaScript: Part 3

In the last post we wrote code so that the visuals for Minesweeper are generated via JavaScript when the page loads. This will be helpful later as it will allow us to update the board visually as the player interacts with the game.

Let’s review what we have in our project so far:

  • minesweeper/
    • minesweeper.html
    • minesweeper.css
    • jquery.js
    • minesweeper.js

Our minesweeper.js looks like this:

const board = $("<table>").attr("id", "board");
const body = $(document.body);
body.append(board);

for (let rowIndex = 0; rowIndex < 9; rowIndex++) {
  const row = $("<tr>");
  board.append(row);
  for (let columnIndex = 0; columnIndex < 9; columnIndex++) {
    const cell = $("<td>");
    row.append(cell);
  }
}

And so far the game looks like this:

Now for something a little more interesting.

Drawing mines

Since the game is all about revealing hidden mines, it seems that we ought to be able to draw those mines when the appropriate time comes. There are a couple of different ways to do this, but ultimately we need to use HTML, CSS, or both. To make things simple for right now, we’ll choose the CSS method. When we draw a space on the board, if a mine is inside of a space, then we’ll add a mine class to the table cell associated with the space.

How do we add a class to a cell (or any HTML element, for that matter)? Fortunately, jQuery has a built-in solution with the addClass method. We can use it like so:

element.addClass("someClass");

So we’ll begin by modifying our JavaScript code like this:

 const board = $("<table>").attr("id", "board");
 const body = $(document.body);
 body.append(board);

 for (let rowIndex = 0; rowIndex < 9; rowIndex++) {
   const row = $("<tr>");
   board.append(row);
   for (let columnIndex = 0; columnIndex < 9; columnIndex++) {
     const cell = $("<td>");
+    cell.addClass("mine");
     row.append(cell);
   }
 }

This is a diff, a common way to represent changes to a piece of text. We’re adding a line to minesweeper.js, so we’ve indicated that by placing a plus sign (+) at the beginning of the line. You should add this line to your version of this file too.

Now we’ll update minesweeper.css so that mines appear as black cells, and so that the hover effect that we’d previously placed on all cells only applies to non-mine cells.

 #board {
   height: 200px;
   left: 50%;
   margin-left: -100px;
   margin-top: -100px;
   position: absolute;
   top: 50%;
   width: 200px;
 }

 td {
   border: 1px solid black;
   cursor: pointer;
 }

-td:hover {
+td:not(.mine):hover {
   background-color: #ddd;
 }
+
+.mine {
+  background-color: black;
+}

Here we have lines beginning with +, but we also have a line beginning with -. You should remove these kinds of lines from your version of the file.

Here’s what we get:

Mapping out the next steps

Now every cell is a mine, but of course what we really need is to scatter them across the board, putting them in random spots so that each gameplay is different. How will that work exactly?

Say that we had a way of marking the spaces on the board in some way so that we could identify one space from another. The simplest thing to do would be to assign a number to each space:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81

Here we’ve numbered the spaces starting at 1, but for reasons we’ll see in a bit, it will actually make more sense to start with 0:

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80

What does that give us? Well, once we have a set of numbers, we can place those numbers in a virtual “hat” and draw ten of them. Each number we draw from the “hat” will correspond to a space and designate a mine that is present at that location:

0 2 3 4 13 20 6 18 19 7 16 8 26 9 23 10 11 12 14 15 60 43 1 5 17 21 22 24 36 25 42 27 28 29 30 31 32 35 37 38 39 47 40 51 41 44 53 45 46 34 62 59 48 33 49 50 52 55 58 78 70 63 66 68 64 73 65 54 76 57 69 67 61 71 72 74 56 75 77 79 80 52 70 21 1 31 28 10 58 13 46

We’ll talk about drawing the mines in a bit, but for now, let’s see if we can capture our picture above in pseudocode. We need to:

  • Make an empty list of numbers called mineLocations
  • Make a “hat” of numbers between 0 and 80
  • Then, repeating for 10 times:
    • Draw a number from the “hat” (removing it in the process)
    • Add the number to mineLocations

That brings us to two related questions:

  1. How we make the “hat”?
  2. How we do we draw numbers from the “hat”?

Generating a random number

Let’s start with the second question. When you draw a number from a hat, what are you doing? You’re choosing a number at random. So we need some way to achieve randomness.

JavaScript gives us a function to do just this: Math.random. This function returns a number between 0 and 1 (excluding 1 itself). Here’s a sampling of how it works:

> Math.random()
< 0.5057105947315927
> Math.random()
< 0.43924692114566244
> Math.random()
< 0.5794767991781664

Remember in the last post how we learned about the Developer Tools? Any time you run across a method that you haven’t seen before, you can always test it out there. Go ahead and bring it up now with Cmd-Option-I (make sure you’re on the Console tab). Type Math.random() and press Enter. What do you get?

Generating a number between 0 and 1 may not seem helpful now, but it will prove to be a useful tool in a moment.

Creating the “hat”

Now we can properly address the first question. Before we draw a number, we have to create a set of numbers to draw from. It makes the most sense to use an array to store this list. But how do we build it? There’s not an elegant way to do this in JavaScript, so we’ll have to use a loop and build the array by hand. As our pseudocode instructs, we’ll start from 0 and end at 80:

const hatOfMineLocations = [];
for (let n = 0; n <= 80; n++) {
  hatOfMineLocations.push(n);
}

Since we also need an empty list to store the mine locations, we can define that as an array as well:

const mineLocations = [];

Now we can see that we need some way to take random numbers out of hatOfMineLocations and add them to mineLocations.

Pulling from the “hat”

Since hatOfMineLocations is an array, each number has an index, the position of that number inside the array:

0 0 1 1 2 2 3 3 4 4 5 5 75 75 76 76 77 77 78 78 79 79 80 80

So in order to pick a random number from hatOfMineLocations, we really need to choose a random index. Note how our indices start from 0 and end at 80, just like the numbers do. So we need to generate a random number between 0 and 80 (including 80 itself).

This is where our newfound friend Math.random comes into play. Remember that it returns a number between 0 and 1 (excluding 1 itself) – but we can fix this with a little math. Since 0 × any number is 0, and 1 × any number is that number, we can multiply the random number we get by 81 to get a number between 0 and 81 (excluding 81 itself):

> Math.random() * 81
< 34.609916103091635
> Math.random() * 81
< 71.31560224890487
> Math.random() * 81
< 22.572173263630866

Of course, that gives us decimal numbers when we really want whole numbers. So we can round down the result by using another builtin, the Math.floor function:

> Math.floor(Math.random() * 81)
< 34
> Math.floor(Math.random() * 81)
< 71
> Math.floor(Math.random() * 81)
< 22

Cool! Now we have a random index in hatOfMineLocations. We can use it to access the number at that index and add the number to mineLocations:

const mineLocationIndex = Math.floor(Math.random() * 81);
const mineLocation = hatOfMineLocations[mineLocationIndex];
mineLocations.push(mineLocations);

Once we’ve updated mineLocations, we need to remove the number from hatOfMineLocations so we don’t pick it again. We’ll do this using splice, a method available on all arrays:

const mineLocationIndex = Math.floor(Math.random() * 81);
const mineLocation = hatOfMineLocations[mineLocationIndex];
mineLocations.push(mineLocations);
hatOfMineLocations.splice(mineLocationIndex, 1);

Now we need to repeat the whole thing 10 times:

for (let i < 0; i < 10; i++) {
  const mineLocationIndex = Math.floor(Math.random() * 81);
  const mineLocation = hatOfMineLocations[mineLocationIndex];
  mineLocations.push(mineLocations);
  hatOfMineLocations.splice(mineLocationIndex, 1);
}

Watch out, though! This won’t quite work: once we remove a number from hatOfMineLocations, the indices assigned to each number will change. Why? Because now there’s one fewer number in our array:

0 1 2 0 1 2 4 5 3 4 75 76 77 78 79 80 74 75 76 77 78 79

So in the next iteration of our loop, instead of choosing a random index between 0 and 81, we’ll need to choose an index between 0 and 80. And in the next iteration after that, we’ll have to shrink the range again. And so forth and so on. What’s a good way to code this? If we think about it, what we really want is for our range to change based on the number of items in hatOfMineLocations. So we’ll say this:

for (let i < 0; i < 10; i++) {
  const mineLocationIndex = Math.floor(Math.random() * hatOfMineLocations.length);
  const mineLocation = hatOfMineLocations[mineLocationIndex];
  mineLocations.push(mineLocation);
  hatOfMineLocations.splice(mineLocationIndex, 1);
}

And there we go – now we have our mine locations! Let’s collect what we have so far and add it to minesweeper.js:

 const board = $("<table>").attr("id", "board");
 const body = $(document.body);
 body.append(board);

+const hatOfMineLocations = [];
+for (let n = 0; n <= 80; n++) {
+  hatOfMineLocations.push(n);
+}
+
+const mineLocations = [];
+for (let i = 0; i < 10; i++) {
+ const mineLocationIndex = Math.floor(Math.random() * hatOfMineLocations.length);
+ const mineLocation = hatOfMineLocations[mineLocationIndex];
+ mineLocations.push(mineLocation);
+ hatOfMineLocations.splice(mineLocationIndex, 1);
+}
+
 for (let rowIndex = 0; rowIndex < 9; rowIndex++) {
   const row = $("<tr>");
   board.append(row);
   for (let columnIndex = 0; columnIndex < 9; columnIndex++) {
     const cell = $("<td>");
     cell.addClass("mine");
     row.append(cell);
   }
 }

Populating the board with mines

Next we need to modify the loop that draws cells. We need to take the ten mine locations we’ve generated and ask each cell if it corresponds to one of those locations. If the answer is yes, we’ll add the mine class to the cell, otherwise we’ll leave the cell empty. In pseudocode, that looks like:

  • For each cell:
    • If the cell’s location is present within mineLocations
      • cell.addClass("mine")

However, in order to determine whether a cell’s location is one of the mineLocations, we have to determine the cell’s location first. It turns out we already know how to do this. Remember that in order to build the rows and columns of the board, we use a loop within a loop. Since we’re using for to do that, we have two counters that we are incrementing along the way: rowIndex and columnIndex.

for (let rowIndex = 0; rowIndex < 9; rowIndex++) {
  // ...
  for (let columnIndex = 0; columnIndex < 9; columnIndex++) {
    // ...
  }
}

You can think of rowIndex as the Y-coordinate and columnIndex as the X-coordinate of a given cell:

0 1 2 3 4 5 6 7 8 0 1 2 3 4 5 6 7 8 (5,4) (2,2) (4,8)

Note, however, that this way to locate cells is different from the way we previously located mines. Here we use two numbers, whereas before we used one number:

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80

Is there a relationship between the two systems? There is! Look at cell number 41 in the second image. Notice how it is labeled (5,4) in the first image. How many cells are in a row? 9. So to get to cell 41, we have to iterate through 4 rows’ worth of cells, which puts us at 36, and then add 5 columns, which puts us at 41. Let’s store the result of this calculation in a variable:

const possibleMineLocation = (rowIndex * 9) + columnIndex;

Now we can determine whether mineLocations is present within possibleMineLocation. JavaScript doesn’t give us a way to ask this question exactly, but we can get the answer indirectly. Remember how we talked about arrays and how each value in an array has an index position associated with it? mineLocations is an array, too. So what we can do is to use the indexOf method to look for possibleMineLocation in mineLocations and get its index position there.

Of course, we need to know whether possibleMineLocation is present, not its index. So how does this help us? Well, if the value we’re looking for is not present, then the index we get back from the method will be -1. So what we want to do is make sure that the index is not -1:

if (mineLocations.indexOf(possibleMineLocation) !== -1) {
  cell.addClass("mine");
}

Now we’ll update minesweeper.js accordingly:

 const board = $("<table>").attr("id", "board");
 const body = $(document.body);
 body.append(board);

 const hatOfMineLocations = [];
 for (let n = 0; n <= 80; n++) {
   hatOfMineLocations.push(n);
 }

 const mineLocations = [];
 for (let i = 0; i < 10; i++) {
  const mineLocationIndex = Math.floor(Math.random() * hatOfMineLocations.length);
  const mineLocation = hatOfMineLocations[mineLocationIndex];
  mineLocations.push(mineLocation);
  hatOfMineLocations.splice(mineLocationIndex, 1);
 }

 for (let rowIndex = 0; rowIndex < 9; rowIndex++) {
   const row = $("<tr>");
   board.append(row);
   for (let columnIndex = 0; columnIndex < 9; columnIndex++) {
     const cell = $("<td>");
+    const possibleMineLocation = (rowIndex * 9) + columnIndex;
+    if (mineLocations.indexOf(possibleMineLocation) !== -1) {
+      cell.addClass("mine");
+    }
     row.append(cell);
   }
 }

Now if we take a look we should see the board dotted with mines. Feel free to refresh the page!

What’s next

Now that we have a way to distribute mines across the board and display them, the next step is to add interactivity so that clicking on the board reveals those mines.