Building Minesweeper in JavaScript: Part 5

We’ve now got it so that when the player clicks a mine, we reveal all mines across the board, signaling the end of the game. But what happens if the player clicks a space that isn’t a mine? In the the previous post, we laid out the two possible outcomes depending on the spaces that surround the space itself. The first of those outcomes is easier than the second, so we’ll focus on that next. We can represent it as pseudocode like this:

  • If any of the spaces surrounding the space is a mine:
    • Show the space as uncovered
    • Count the surrounding mines and show them inside the space

Read over this, but don’t worry about fully deciphering this at the moment. For now, let’s store this information in minesweeper.js so we have a placeholder:

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

     if (isMine) {
       mines.push(space);
     }

     row.append(space);

     space.on("click", () => {
       if (isMine) {
         mines.forEach(mine => {
           mine.addClass("mine");
         });
-      }
+      } else {
+        // If any of the spaces surrounding the space is a mine:
+        //   Show the space as uncovered
+        //   Count the surrounding mines and show them inside the space
+      }
     });
   }
 }

Uncovering spaces

Before we dive into implementing these new changes, let’s talk about how they’ll affect the HTML and CSS.

So far, our board is made up of a series of td elements. Whenever the player clicks on a mine, a mine class is added to any spaces that are mines, and the space receives a black background.

Now we’re introducing this concept of “uncovering” a space that isn’t a mine. In a typical game of Minesweeper, these spaces are pushed in visually. We could recreate this with CSS, but we don’t want to go overboard. For right now, let’s say that these spaces receive a gray background instead of a black one. In order to apply this style, let’s give uncovered spaces an uncovered class. We’ll modify minesweeper.css like this:

 #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:not(.mine):hover {
+td:not(.uncovered):hover {
   background-color: #ddd;
 }

+.uncovered {
+  background-color: gray;
+  color: white;
+}
+
 .mine {
   background-color: black;
 }

Now, whenever we write code, one thing that we should do constantly is to put ourselves in the shoes of someone who is looking over our code for the first time and imagine how they might interpret it. While we wouldn’t expect them to see what’s in our head just by reading it, we should help them gain as much of an understanding as possible. Giving consistent and accurate names to various items in our code will go a long way in achieving this.

In this case, our CSS mentions two categories of things: “uncovered” things and “mines”. We never explicitly mention that these “mines” are also supposed to be uncovered, so our code paints an incomplete picture of reality and could be confusing. Let’s address that briefly:

 #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:not(.uncovered):hover {
   background-color: #ddd;
 }

-.uncovered {
+.uncovered:not(.mine) {
   background-color: gray;
   color: white;
 }

-.mine {
+.uncovered.mine {
   background-color: black;
 }

There – that’s a little better.

Finally, in uncovering a space where one or more mines surround it, we’ll need to add a count inside of the space. We don’t want to get too far ahead of ourselves, but it pays to think about what the HTML will look like. The simplest way to do this is to set the content of the td element to the number itself. When we implement the code for this later, we’ll discover that none of the tds (which are empty right now) have consistent dimensions and will stretch once more content is added to them. We need to freeze those dimensions so that they are exactly 1/9 of the board. In addition, the number that we add will need to be centered within the space. Let’s take care of both of those issues now:

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

 td {
   border: 1px solid black;
   cursor: pointer;
+  width: 11.1111111%;
+  height: 11.1111111%;
+  text-align: center;
+  vertical-align: center;
 }

 td:not(.uncovered):hover {
   background-color: #ddd;
 }

 .uncovered:not(.mine) {
   background-color: gray;
   color: white;
 }

 .uncovered.mine {
   background-color: black;
 }

Because we’ve changed the definition of the mine class and added a new class, our board is now broken. To fix this, we need to adjust our JavaScript code to match:

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

     if (isMine) {
       mines.push(space);
+      space.addClass("mine");
     }

     row.append(space);

     space.on("click", () => {
       if (isMine) {
         mines.forEach(mine => {
-          mine.addClass("mine");
+          mine.addClass("uncovered");
         });
       }
     });
   }
 }

We should see the same result as before, but this should set us up nicely for the changes we’re about to make:

Neighbors

Let’s take another look at the pseudocode:

  • If any of the spaces surrounding the space is a mine:
    • Show the space as uncovered
    • Count the surrounding mines and show them inside the space

There are a few different pieces that we need to implement, but the first thing that pops out is the idea of spaces that surround other spaces – or something we’ll call neighbors for short. For instance, all of the grey spaces here are neighbors of the red space:

Most spaces will have 9 neighbors, except for those that live on the edge of the board. For instance, a corner space has only 3 neighbors:

Okay, so how do we find the neighbors for any space on the board? Well, remember in part 3 how we learned that we have a way to denote the location of a space using X- and Y-coordinates?

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

This is made possible by the loop we use to draw the spaces:

for (let rowIndex = 0; rowIndex < 9; rowIndex++) {
  ...
  for (let columnIndex = 0; columnIndex < 9; columnIndex++) {
    const space = $("<td>");
    ...
  }
  ...
}

If we had a way to look up certain spaces by their coordinates, we could take a space’s coordinates and add or subtract a row or column from them in as many directions as were allowable. Then we’d have coordinates for that space’s neighbors and we could look up the spaces that belong to those coordinates.

How do we do this? We’ll have to put on our imagination hat.

Storing the board as data

What if we had an array of arrays of td elements, where the outer array represented the rows in the board and each inner array represented the column in a row?

[ 0 [ 0 <td> , 1 <td> , 2 <td> , 3 <td> , 4 <td> , 5 <td> , 6 <td> , 7 <td> , 8 <td> ] , 1 [ <td> , <td> , <td> , <td> , <td> , <td> , <td> , <td> , <td> ] , 2 [ <td> , <td> , <td> , <td> , <td> , <td> , <td> , <td> , <td> ] , 3 [ <td> , <td> , <td> , <td> , <td> , <td> , <td> , <td> , <td> ] , 4 [ <td> , <td> , <td> , <td> , <td> , <td> , <td> , <td> , <td> ] , 5 [ <td> , <td> , <td> , <td> , <td> , <td> , <td> , <td> , <td> ] , 6 [ <td> , <td> , <td> , <td> , <td> , <td> , <td> , <td> , <td> ] , 7 [ <td> , <td> , <td> , <td> , <td> , <td> , <td> , <td> , <td> ] , 8 [ <td> , <td> , <td> , <td> , <td> , <td> , <td> , <td> , <td> ] ]

With this array, we could drill down to a space by reusing the rowIndex and columnIndex of that space. For instance, if this imaginary array were stored in a variable called spaces, then we could use spaces[3][5] to refer to the td highlighted in red above. And we could use spaces[2][5] to refer to its northern neighbor, and spaces[3][6] to refer to its eastern neighbor, and so on.

So let’s turn this spaces array into reality. We’ll update minesweeper.js so that we define the array before the outer row loop starts. Then, each time we build a tr element, we add a new inner array to spaces to represent the row. We then add tds to this array as we build them.

 ...
 const mines = [];
+const spaces = [];

 for (let rowIndex = 0; rowIndex < 9; rowIndex++) {
   const row = $("<tr>");
+
   board.append(row);
+  spaces[rowIndex] = [];
+
   for (let columnIndex = 0; columnIndex < 9; columnIndex++) {
     const space = $("<td>");
     const possibleMineLocation = (rowIndex * 9) + columnIndex;
     const isMine = (mineLocations.indexOf(possibleMineLocation) !== -1);

     if (isMine) {
       mines.push(space);
       space.addClass("mine");
     }
+
+    spaces[rowIndex][columnIndex] = space;

     row.append(space);

     space.on("click", () => {
       if (isMine) {
         mines.forEach(mine => {
           mine.addClass("uncovered");
         });
       } else {
         // If any of the spaces surrounding the space is a mine:
         //   Show the space as uncovered
         //   Count the surrounding mines and show them inside the space
       }
     });
   }
 }

Gathering neighbors

Now that we have a data structure that mimics the board itself, we can use it to look up the neighbors for a space.

There’s code we need to write, but we’re not certain what it’s going to look like, so we’ll have to sketch something out. (Don’t put this in minesweeper.js just yet.) Let’s start by saying:

const north = spaces[rowIndex - 1][columnIndex];
const south = spaces[rowIndex + 1][columnIndex];
const west = spaces[rowIndex][columnIndex - 1];
const east = spaces[rowIndex][columnIndex + 1];
const northwest = spaces[rowIndex - 1][columnIndex - 1];
const northeast = spaces[rowIndex - 1][columnIndex + 1];
const southwest = spaces[rowIndex + 1][columnIndex - 1];
const southeast = spaces[rowIndex + 1][columnIndex + 1];
const neighbors = [
  north,
  south,
  west,
  east,
  northwest,
  northeast,
  southwest,
  southeast
];

That’s good, but as we mentioned earlier, a space may not always have 9 neighbors, as it depends on whether it is in the middle or at an edge; each time we access a different row or column, we need to consider that it may or may not exist. So let’s add in some checks:

const previousRow = spaces[rowIndex - 1];
const currentRow = spaces[rowIndex];
const nextRow = spaces[rowIndex + 1];
const neighbors = [];

const west = currentRow[columnIndex - 1];
const east = currentRow[columnIndex + 1];

if (west != null) {
  neighbors.push(west);
}

if (east != null) {
  neighbors.push(east);
}

if (previousRow != null) {
  const north = previousRow[columnIndex];
  const northwest = previousRow[columnIndex - 1];
  const northeast = previousRow[columnIndex + 1];

  neighbors.push(north);

  if (northwest != null) {
    neighbors.push(northwest);
  }

  if (northeast != null) {
    neighbors.push(northeast);
  }
}

if (nextRow != null) {
  const south = nextRow[columnIndex];
  const southwest = nextRow[columnIndex - 1];
  const southeast = nextRow[columnIndex + 1];

  neighbors.push(south);

  if (southwest != null) {
    neighbors.push(southwest);
  }

  if (southeast != null) {
    neighbors.push(southeast);
  }
}

Here we’re using value != null whenever we want to check that a row or column is present. In JavaScript, it’s generally best to use === whenever we want to check whether two things are equal, so why aren’t we doing that here? In this case, value != null is a shortcut for saying value !== undefined && value !== null.

Now our code works for spaces located anywhere. But that’s a lot of code just to look for neighbors, isn’t it? Isn’t there some way that we can make it shorter?

There is! The first thing we can do is to apply a technique involving the || operator (also called the “or” operator). Say that we are setting a variable to a value, but it is possible that the value is null. In that case, we want to assign an alternate value. We could use a conditional to accomplish this:

let someVariable = someValue;

if (someVariable == null) {
  someVariable = "someOtherValue";
}

But if we know that someVariable is not a number, we can also use || to achieve the same thing:

const someVariable = someValue || "someOtherValue";

So we can modify our code to ensure that previousRow and nextRow are at least set to empty arrays. That would obviate our need to check whether previousRow or nextRow exists before we use it:

const previousRow = spaces[rowIndex - 1] || [];
const currentRow = spaces[rowIndex];
const nextRow = spaces[rowIndex + 1] || [];
const west = currentRow[columnIndex - 1];
const east = currentRow[columnIndex + 1];
const north = previousRow[columnIndex];
const northwest = previousRow[columnIndex - 1];
const northeast = previousRow[columnIndex + 1];
const south = nextRow[columnIndex];
const southwest = nextRow[columnIndex - 1];
const southeast = nextRow[columnIndex + 1];
const neighbors = [];

if (west != null) {
  neighbors.push(west);
}

if (east != null) {
  neighbors.push(east);
}

if (north != null) {
  neighbors.push(north);
}

if (northwest != null) {
  neighbors.push(northwest);
}

if (northeast != null) {
  neighbors.push(northeast);
}

if (south != null) {
  neighbors.push(south);
}

if (southwest != null) {
  neighbors.push(southwest);
}

if (southeast != null) {
  neighbors.push(southeast);
}

The second thing we can do is to replace all of the checks for null we’re making with something more succinct. We’ll start by completely removing those checks:

const previousRow = spaces[rowIndex - 1] || [];
const currentRow = spaces[rowIndex];
const nextRow = spaces[rowIndex + 1] || [];
const north = previousRow[columnIndex];
const south = nextRow[columnIndex];
const west = currentRow[columnIndex - 1];
const east = currentRow[columnIndex + 1];
const northwest = previousRow[columnIndex - 1];
const northeast = previousRow[columnIndex + 1];
const southwest = nextRow[columnIndex - 1];
const southeast = nextRow[columnIndex + 1];
const neighbors = [
  north,
  south,
  west,
  east,
  northwest,
  northeast,
  southwest,
  southeast
];

Now we need to address the null values that may appear in neighbors. We don’t want a bunch of if statements like we had before, so what do we do? We could loop through all of the neighbors and build a new array that consist only of ones that are not null:

const previousRow = spaces[rowIndex - 1] || [];
const currentRow = spaces[rowIndex];
const nextRow = spaces[rowIndex + 1] || [];
const north = previousRow[columnIndex];
const south = nextRow[columnIndex];
const west = currentRow[columnIndex - 1];
const east = currentRow[columnIndex + 1];
const northwest = previousRow[columnIndex - 1];
const northeast = previousRow[columnIndex + 1];
const southwest = nextRow[columnIndex - 1];
const southeast = nextRow[columnIndex + 1];
const neighborsWithPossibleNulls = [
  north,
  south,
  west,
  east,
  northwest,
  northeast,
  southwest,
  southeast
];
const neighbors = [];
neighborsWithPossibleNulls.forEach(neighbor => {
  if (neighbor != null) {
    neighbors.push(neighbor);
  }
});

It turns out that the pattern that we’ve written – to select a portion of some array that matches a certain condition, discarding the rest that does not match that condition – is common to many programming languages. In JavaScript, we can do it with the filter method, which is available on any array. Let’s do that and take this opportunity to combine some of the other lines:

const previousRow = spaces[rowIndex - 1] || [];
const nextRow = spaces[rowIndex + 1] || [];
const currentRow = spaces[rowIndex];
const neighborsWithPossibleNulls = [
  currentRow[columnIndex - 1],
  currentRow[columnIndex + 1],
  previousRow[columnIndex - 1],
  previousRow[columnIndex],
  previousRow[columnIndex + 1],
  nextRow[columnIndex - 1],
  nextRow[columnIndex],
  nextRow[columnIndex + 1]
];
const neighbors = neighborsWithPossibleNulls.filter(neighbor => {
  return neighbor != null;
});

Isn’t that nice?

Now we can place it into minesweeper.js above the pseudocode we added earlier:

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

     if (isMine) {
       mines.push(space);
       space.addClass("mine");
     }

     row.append(space);

     space.on("click", () => {
       if (isMine) {
         mines.forEach(mine => {
           mine.addClass("uncovered");
         });
       } else {
+        const previousRow = spaces[rowIndex - 1] || [];
+        const nextRow = spaces[rowIndex + 1] || [];
+        const currentRow = spaces[rowIndex];
+        const neighborsWithPossibleNulls = [
+          currentRow[columnIndex - 1],
+          currentRow[columnIndex + 1],
+          previousRow[columnIndex - 1],
+          previousRow[columnIndex],
+          previousRow[columnIndex + 1],
+          nextRow[columnIndex - 1],
+          nextRow[columnIndex],
+          nextRow[columnIndex + 1]
+        ];
+        const neighbors = neighborsWithPossibleNulls.filter(neighbor => {
+          return neighbor != null;
+        });
         // If any of the spaces surrounding the space is a mine:
         //   Show the space as uncovered
         //   Count the surrounding mines and show them inside the space
       }
     });
   }
 }

Determining neighboring mines

Now that we have a way to get a list of neighbors for a space, let’s take another look at our pseudocode:

  • If any of the spaces surrounding the space (aka the neighbors) is a mine:
    • Show the space as uncovered
    • Count the surrounding mines and show them inside the space

Do you notice any similarities between the conditional in the first line and the last step? We are asking whether any neighbors is a mine, and yet later we are counting those exact neighbors. So if we establish a count first, we can use in the rest of the steps:

  • Count the neighboring mines, storing it as numNeighboringMines
  • If numNeighboringMines is greater than 0:
    • Show the space as uncovered
    • Show numNeighboringMines inside the space

Let’s update minesweeper.js to make sure this new perspective is close at hand:

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

     if (isMine) {
       mines.push(space);
       space.addClass("mine");
     }

     row.append(space);

     space.on("click", () => {
       if (isMine) {
         mines.forEach(mine => {
           mine.addClass("uncovered");
         });
       } else {
         const previousRow = spaces[rowIndex - 1] || [];
         const nextRow = spaces[rowIndex + 1] || [];
         const currentRow = spaces[rowIndex];
         const neighborsWithPossibleNulls = [
           currentRow[columnIndex - 1],
           currentRow[columnIndex + 1],
           previousRow[columnIndex - 1],
           previousRow[columnIndex],
           previousRow[columnIndex + 1],
           nextRow[columnIndex - 1],
           nextRow[columnIndex],
           nextRow[columnIndex + 1]
         ];
         const neighbors = neighborsWithPossibleNulls.filter(neighbor => {
           return neighbor != null;
         });
-        // If any of the spaces surrounding the space is a mine:
-        //   Show the space as uncovered
-        //   Count the surrounding mines and show them inside the space
+        // Count the neighboring mines, storing it as `numNeighboringMines`
+        // If `numNeighboringMines` is greater than 0
+        //   Show the space as uncovered
+        //   Show `numNeighboringMines` inside the space
       }
     });
   }
 }

All right – with that out of the way, we can start from the top. We need to count the neighboring spaces that are also mines, but to do that we need to find the neighboring spaces that are also mines. In other words, we need to start with neighbors and select a smaller portion of it. Doesn’t this seem like something we’ve done before? Yes – we can use filter again:

const neighboringMines = neighbors.filter(neighbor => {
  // is the neighbor a mine?
});

Now the problem is what exactly to put inside the function. How do we know whether a space is a mine?

To answer this appropriately, we need some way to distinguish mines from non-mines. In fact, it turns out we already do: we mark mines visually using a CSS class. So in theory, we could ask whether the space has a mine class (using the hasClass method available in jQuery):

const neighboringMines = neighbors.filter(neighbor => {
  return neighbor.hasClass("mine");
});

While this works – and it is not unusual to see this approach out in the wild – it isn’t the best idea to use CSS classes in this manner. Why? Because this isn’t how CSS was intended to be used. CSS exists solely to change the appearance of elements on the page. The class we added to each mine is a tag we attach to the td element so that the browser knows to draw it using a black background. We just so happened to call this tag “mine”, but we could have called it anything. In addition, if we make use of CSS like this, it may create problems for us. We may decide down the road that we want to change how we style mines, and if we tangle CSS and JavaScript together, then that kind of change may be harder to make.

So we still need to tag our element somehow as being a mine, but only do so within JavaScript. There are a couple different methods we can use, but the one that fits best with our code at present is to use jQuery’s data method. This method allows us to take a key-value pair and associate it with an element on the page so that we can look up the value by the key later. With that in mind, we can modify minesweeper.js like this:

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

     if (isMine) {
       mines.push(space);
       space.addClass("mine");
+      space.data("isMine", true);
     }

     ...
   }
 }

Now we can revise how we are building the list of neighboring mines like this:

const neighboringMines = neighbors.filter(neighbor => {
  return neighbor.data("isMine");
});

Lastly, we can fit it into minesweeper.js:

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

     if (isMine) {
       mines.push(space);
       space.addClass("mine");
       space.data("isMine", true);
     }

     row.append(space);

     space.on("click", () => {
       if (isMine) {
         mines.forEach(mine => {
           mine.addClass("uncovered");
         });
       } else {
         const previousRow = spaces[rowIndex - 1] || [];
         const nextRow = spaces[rowIndex + 1] || [];
         const currentRow = spaces[rowIndex];
         const neighborsWithPossibleNulls = [
           currentRow[columnIndex - 1],
           currentRow[columnIndex + 1],
           previousRow[columnIndex - 1],
           previousRow[columnIndex],
           previousRow[columnIndex + 1],
           nextRow[columnIndex - 1],
           nextRow[columnIndex],
           nextRow[columnIndex + 1]
         ];
         const neighbors = neighborsWithPossibleNulls.filter(neighbor => {
           return neighbor != null;
         });
+        const neighboringMines = neighbors.filter(neighbor => {
+          return neighbor.data("isMine");
+        });
         // Count the neighboring mines, storing it as `numNeighboringMines`
         // If `numNeighboringMines` is greater than 0
         //   Show the space as uncovered
         //   Show `numNeighboringMines` inside the space
       }
     });
   }
 }

Finishing up

We’ve made a lot of progress in this post, and we can combine everything we’ve done here to finish up this piece of the game:

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

     if (isMine) {
       mines.push(space);
       space.addClass("mine");
       space.data("isMine", true);
     }

     row.append(space);

     space.on("click", () => {
       if (isMine) {
         mines.forEach(mine => {
           mine.addClass("uncovered");
         });
       } else {
         const previousRow = spaces[rowIndex - 1] || [];
         const nextRow = spaces[rowIndex + 1] || [];
         const currentRow = spaces[rowIndex];
         const neighborsWithPossibleNulls = [
           currentRow[columnIndex - 1],
           currentRow[columnIndex + 1],
           previousRow[columnIndex - 1],
           previousRow[columnIndex],
           previousRow[columnIndex + 1],
           nextRow[columnIndex - 1],
           nextRow[columnIndex],
           nextRow[columnIndex + 1]
         ];
         const neighbors = neighborsWithPossibleNulls.filter(neighbor => {
           return neighbor != null;
         });
         const neighboringMines = neighbors.filter(neighbor => {
           return neighbor.data("isMine");
         });
-        // Count the neighboring mines, storing it as `numNeighboringMines`
+        const numNeighboringMines = neighboringMines.length;
-        // If `numNeighboringMines` is greater than 0
+
+        if (numNeighboringMines > 0) {
-        //   Show the space as uncovered
+          space.addClass("uncovered");
-        //   Show `numNeighboringMines` inside the space
+          space.text(numNeighboringMines);
+        }
       }
     });
   }
 }

Time to finally take a look at what this produces! Try clicking on a space that’s close to a mine:

What’s next

Our next task is to finish up the core logic of the game by implementing what happens when a space that does not neighbor any mines is clicked.