Adjacent Cells and Bomb Counting
Implement the techniques for detecting nearby bombs and clearing empty cells automatically.
In this part of our Minesweeper tutorial, we'll build upon our previous code by adding the ability to track and display the count of bombs in adjacent cells.
This feature provides players with the information they need to make informed decisions during gameplay.
We'll also allow cells to be cleared when there are no adjacent bombs, allowing players to clear large sections of the grid automatically.
Determining Cell Adjacency
To implement the adjacent bomb count feature, we first need to determine if a MinesweeperCell
is adjacent to another MinesweeperCell
.
We'll add a new method to our MinesweeperCell
class called isAdjacent
. This method will take another MinesweeperCell
pointer as an argument and return a boolean indicating whether that cell is adjacent to the cell that this method is called on.
// Minesweeper/Cell.h
// ...
class MinesweeperCell : public Engine::Button {
// ...
private:
bool isAdjacent(MinesweeperCell* Other) const;
// ...
};
Our MinesweeperCell
objects already know where they are in the grid, thanks to the Row
and Col
values (and associated GetRow()
and GetCol()
getters) provided when the MinesweeperGrid
constructs them.
We can use this to implement our isAdjacent
method:
// Minesweeper/Cell.cpp
// ...
bool MinesweeperCell::isAdjacent(
MinesweeperCell* Other) const{
return !(Other == this)
&& std::abs(GetRow() - Other->GetRow()) <= 1
&& std::abs(GetCol() - Other->GetCol()) <= 1;
}
If it's not clear what this logic is doing, we can imagine that two cells, A
and B
, are adjacent if they are within one row and one column of each other - that is:
A.Row - B.Row
is either-1
,0
or1
, andA.Col - B.Col
is either-1
,0
or1
We can use the concept of absolute value to simplify this. The absolute value of a number is how far away the number is from 0
. Typically, we can imagine this as simply removing the negative sign from a number, if it had one.
The std::abs
function receives a number and returns its absolute value - for example, std::abs(-1)
will return 1
.
So, equivalently, two cells are adjacent if:
std::abs(A.Row - B.Row)
is<= 1
, andstd::abs(A.Col - B.Col)
is<= 1
Note that if std::abs(A.Row - B.Row)
and std::abs(A.Col - B.Col)
are both 0
, that means A
and B
are in the exact same position in the grid.
In the context of our game, that means the the two cells are the same object. We don't want a cell to be considered adjacent to itself so, if Other == this
, isAdjacent()
will return false
.
Keeping Track of Adjacent Bombs
In the previous part, our MinesweeperCell
objects were pushing an SDL_Event
to the event queue whenever they received a bomb. Every cell is notified of this event, so they can use them to keep track of the number of adjacent bombs.
To implement this, we'll add two new elements to our MinesweeperCell
class:
- An
AdjacentBombs
integer to store the count of adjacent bombs. - A
HandleBombPlaced
method to receive the event, and update theAdjacentBombs
count if the event was created by an adjacent cell.
// Minesweeper/Cell.h
// ...
class MinesweeperCell : public Engine::Button {
// ...
private:
void HandleBombPlaced(const SDL_UserEvent& E);
int AdjacentBombs{0};
// ...
};
We'll call the HandleBombPlaced()
method whenever a bomb is placed on the board. The ReportEvent()
method our our MinesweeperCell
class is attaching a pointer to the cell that created each event. This pointer is in the data1
variable of the SDL_UserEvent
.
data1
is technically a void pointer (void*
), but we know it's pointing to a MinesweeperCell
, so we can statically cast it:
MinesweeperCell* Cell{
static_cast<MinesweeperCell*>(E.data1)
};
We'll pass this MinesweeperCell*
off to our isAdjacent
function. If isAdjacent()
returns true, that means a bomb was placed in an adjacent cell, and we'll update our AdjacentBombs
counter accordingly:
// Minesweeper/Cell.cpp
// ...
void MinesweeperCell::HandleBombPlaced(
const SDL_UserEvent& E){
MinesweeperCell* Cell{
static_cast<MinesweeperCell*>(E.data1)
};
if (isAdjacent(Cell)) {
++AdjacentBombs;
}
}
Finally, we need to call HandleBombPlaced
at the appropriate time, so we'll update the HandleEvent
method. Previously, this method was simply logging to the terminal when a bomb was placed. Now, it will call our new HandleBombPlaced
function:
// Minesweeper/Cell.cpp
// ...
void MinesweeperCell::HandleEvent(
const SDL_Event& E){
if (E.type == UserEvents::CELL_CLEARED) {
// TODO
std::cout << "A Cell Was Cleared\n";
} else if (E.type == UserEvents::BOMB_PLACED) {
HandleBombPlaced(E.user);
}
Button::HandleEvent(E);
}
// ...
Rendering Adjacent Bomb Count
To visually represent the number of adjacent bombs, we'll update our game to render this count on cleared cells that have at least one adjacent bomb.
We want the count to have a different color depending on the number of adjacent bombs, so let's add a TEXT_COLORS
array to our Config
namespace in Globals.h
.
This array maps the number of adjacent bombs to a specific color. We won't render any text if there are no adjacent bombs, so TEXT_COLORS[0]
will technically not be used, but we'll include it anyway as indices must start at 0
:
// Globals.h
// ...
namespace Config{
// ...
// Text color based on number of surrounding bombs
inline const std::vector<SDL_Color> TEXT_COLORS{
/* 0 */ {0, 0, 0, 255}, // Unused
/* 1 */ {0, 1, 249, 255},
/* 2 */ {1, 126, 1, 255},
/* 3 */ {250, 1, 2, 255},
/* 4 */ {1, 0, 128, 255},
/* 5 */ {129, 1, 0, 255},
/* 6 */ {0, 128, 128, 255},
/* 7 */ {0, 0, 0, 255},
/* 8 */ {128, 128, 128, 255}
};
// ...
}
// ...
We're using the Engine::Text
class to handle the rendering of the adjacent bomb count. This class takes care of creating and rendering text surfaces for us. Let's add an Engine::Text
member to each of our MinesweeperCell
objects:
// Minesweeper/Cell.h
#pragma once
#include "Engine/Button.h"
#include "Engine/Image.h"
#include "Engine/Text.h"
class MinesweeperCell : public Engine::Button {
// ...
private:
// ...
Engine::Text Text;
};
In the MinesweeperCell
constructor, we initialize the Text
object with its position (x
and y
), size (w
and h
), the text to render (which is "0" for now) and its corresponding color.
// Minesweeper/Cell.cpp
// ...
MinesweeperCell::MinesweeperCell(
int x, int y, int w, int h, int Row, int Col)
: Button{x, y, w, h}, Row{Row}, Col{Col},
BombImage{
x, y, w, h,
Config::BOMB_IMAGE},
Text{
x, y, w, h,
std::to_string(AdjacentBombs),
Config::TEXT_COLORS[AdjacentBombs]}{};
// ...
Whenever a bomb is placed adjacent to the cell, we'll call the SetText()
method of Engine::Text
, passing the new value, and the corresponding colour:
// Minesweeper/Cell.cpp
// ...
void MinesweeperCell::HandleBombPlaced(
const SDL_UserEvent& E){
MinesweeperCell* Cell{
static_cast<MinesweeperCell*>(E.data1)
};
if (isAdjacent(Cell)) {
++AdjacentBombs;
Text.SetText(std::to_string(AdjacentBombs),
Config::TEXT_COLORS[AdjacentBombs]);
}
}
Finally, we'll updated the Render
method to render the text when a cell is cleared and has adjacent bombs:
// Minesweeper/Cell.cpp
// ...
void MinesweeperCell::Render(
SDL_Surface* Surface){
Button::Render(Surface);
if (isCleared && hasBomb) {
BombImage.Render(Surface);
} else if (isCleared && AdjacentBombs > 0) {
Text.Render(Surface);
}
#ifdef SHOW_DEBUG_HELPERS
else if (hasBomb) { BombImage.Render(Surface); }
#endif
}
If we run our application and clear cells that are adjacent to bombs, we should now see the correct number display, in the correct color:

Remember, the reason we can see the bombs is because of the preprocessor directive enabling debug helpers. If we want to test the game as a player would, we can comment out that declaration:
// Globals.h
#pragma once
// #define SHOW_DEBUG_HELPERS
// ...
Automatically Clearing Adjacent Cells
In Minesweeper, when a cell with no adjacent bombs is cleared, all adjacent cells are automatically cleared as well. This creates a cascading effect that can clear large areas of the board at once.
To implement this feature, we'll follow a similar pattern as before. Our MinesweeperCell
objects are already being notified when any cell is cleared.
We'll add a HandleCellCleared
method to implement the automatic-clearing behaviour where appropriate:
// Minesweeper/Cell.h
// ...
class MinesweeperCell : public Engine::Button {
// ...
private:
void HandleCellCleared(const SDL_UserEvent& E);
// ...
};
As before, our implementation will static_cast
the data1
void pointer to a MinesweeperCell*
.
We check if the cell that was cleared had a bomb. If it does, we don't want to clear any adjacent cells, as the game is effectively over. We'll implement the game-over logic in the next part.
If the cell that was cleared did not have a bomb, is adjacent to this cell, and it had no adjacent bombs, we'll clear this cell too:
// Minesweeper/Cell.cpp
// ...
void MinesweeperCell::HandleCellCleared(
const SDL_UserEvent& E){
MinesweeperCell* Cell{
static_cast<MinesweeperCell*>(E.data1)
};
if (Cell->hasBomb) return;
if (
isAdjacent(Cell) &&
Cell->AdjacentBombs == 0
) {
ClearCell();
}
}
Remember, the ClearCell()
method creates further UserEvents::CELL_CLEARED
events, causing the chain reaction that allows large areas of the board to be cleared automatically.
Finally, let's update our HandleEvent()
method to forward UserEvents::CELL_CLEARED
to our new HandleCellCleared()
method:
//
// ...
void MinesweeperCell::HandleEvent(
const SDL_Event& E){
if (E.type == UserEvents::CELL_CLEARED) {
HandleCellCleared(E.user);
} else if (E.type ==
UserEvents::BOMB_PLACED) {
HandleBombPlaced(E.user);
}
Button::HandleEvent(E);
}
With these changes, we should now be able to click on an area of our grid that does not have any nearby bombs, and see a chain reaction clearing a large area of the board automatically:

Complete Code
Complete versions of the files we changed in this part are available below
Files not listed above have not been changed since the previous section.
Summary
In this part of the tutorial, we've significantly enhanced our Minesweeper game by implementing the adjacent bomb count feature.
We've added methods to determine cell adjacency, keep track of adjacent bombs, render the bomb count, and automatically clear adjacent empty cells.
These additions bring our game closer to a fully functional Minesweeper implementation.
In the next part, we'll focus on detecting and reacting to win and lose conditions, which will complete the core gameplay loop of our Minesweeper game.
Ending and Restarting Games
Implement win/loss detection and add a restart feature to complete the game loop