Adjacent Cells and Bomb Counting
Implement the techniques for detecting nearby bombs and clearing empty cells automatically.
In this part of our Minesweeper project, 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.
We'll continue from where we left off in the previous lesson. Our project already has a grid of interactive cells where bombs are placed randomly.
For this lesson, we will need to render numbers on our cells. To do this, we will use the Engine::Text
class that we created in the lesson.
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.
src/Minesweeper/Cell.h
#pragma once
#include <SDL3/SDL.h>
#include <memory>
#include "Engine/Button.h"
#include "Engine/Image.h"
class MinesweeperCell : public Engine::Button {
public:
MinesweeperCell(
int X, int Y, int W, int H, int Row, int Col
);
void HandleEvent(const SDL_Event& E) override;
void Render(SDL_Surface* Surface) override;
bool PlaceBomb();
[[nodiscard]]
bool GetHasBomb() const{ return hasBomb; }
[[nodiscard]]
int GetRow() const{ return Row; }
[[nodiscard]]
int GetCol() const{ return Col; }
protected:
void HandleLeftClick() override;
private:
bool isAdjacent(const MinesweeperCell* Other) const;
void ClearCell();
void ReportEvent(Uint32 EventType);
int Row;
int Col;
bool hasBomb{false};
bool isCleared{false};
std::unique_ptr<Engine::Image> BombImage;
};
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:
src/Minesweeper/Cell.cpp
#include <iostream>
#include <memory>
#include <SDL3/SDL.h>
#include "Minesweeper/Cell.h"
#include "Globals.h"
// ... constructor ...
bool MinesweeperCell::isAdjacent(
const MinesweeperCell* Other) const{
return !(Other == this)
&& std::abs(GetRow() - Other->GetRow()) <= 1
&& std::abs(GetCol() - Other->GetCol()) <= 1;
}
// ... rest of file ...
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 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.
src/Minesweeper/Cell.h
// ...
class MinesweeperCell : public Engine::Button {
// ...
private:
void HandleBombPlaced(const SDL_UserEvent& E);
bool isAdjacent(const MinesweeperCell* Other) const;
void ClearCell();
void ReportEvent(Uint32 EventType);
int AdjacentBombs{0};
int Row;
int Col;
bool hasBomb{false};
bool isCleared{false};
std::unique_ptr<Engine::Image> BombImage;
};
We'll call the HandleBombPlaced()
method whenever a bomb is placed on the board. The ReportEvent()
method of 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:
src/Minesweeper/Cell.cpp
// ... constructor ...
void MinesweeperCell::HandleBombPlaced(
const SDL_UserEvent& E){
const auto* Cell{
static_cast<MinesweeperCell*>(E.data1)};
if (isAdjacent(Cell)) {
++AdjacentBombs;
}
}
bool MinesweeperCell::isAdjacent(
// ...
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:
src/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
:
src/Globals.h
#pragma once
#include <vector>
// ...
namespace Config{
// ...
// Colors
// ...
inline constexpr SDL_Color
BUTTON_CLEARED_COLOR{
240, 240, 240, 255};
// 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}
};
// Asset Paths
// ...
}
// ...
We're using the Engine::Text
class to handle the rendering of the adjacent bomb count. Let's add a std::unique_ptr<Engine::Text>
member to each of our MinesweeperCell
objects:
src/Minesweeper/Cell.h
#pragma once
#include <SDL3/SDL.h>
#include <memory>
#include "Engine/Button.h"
#include "Engine/Image.h"
#include "Engine/Text.h"
class MinesweeperCell : public Engine::Button {
// ...
private:
// ...
std::unique_ptr<Engine::Image> BombImage;
std::unique_ptr<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.
src/Minesweeper/Cell.cpp
#include <iostream>
#include <memory>
#include <string>
#include <SDL3/SDL.h>
#include "Minesweeper/Cell.h"
#include "Globals.h"
MinesweeperCell::MinesweeperCell(
int x, int y, int w, int h, int Row, int Col)
: Button{x, y, w, h}, Row{Row}, Col{Col} {
BombImage = std::make_unique<Engine::Image>(
x, y, w, h,
Config::BOMB_IMAGE
);
Text = std::make_unique<Engine::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:
src/Minesweeper/Cell.cpp
// ...
void MinesweeperCell::HandleBombPlaced(
const SDL_UserEvent& E){
const auto* 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:
src/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:
src/Globals.h
// ...
// #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:
src/Minesweeper/Cell.h
// ...
class MinesweeperCell : public Engine::Button {
// ...
private:
void HandleCellCleared(const SDL_UserEvent& E);
void HandleBombPlaced(const SDL_UserEvent& E);
bool isAdjacent(const MinesweeperCell* Other) const;
// ...
};
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 and had no adjacent bombs either, it's neighboring cells should automatically be cleared.
Remember, we can check if a MinesweeperCell
is adjacent to this
cell using the isAdjacent()
function:
src/Minesweeper/Cell.cpp
// ...
void MinesweeperCell::HandleCellCleared(
const SDL_UserEvent& E
){
// Get the cell that was just cleared
const auto* Cell{
static_cast<MinesweeperCell*>(E.data1)};
// If the cell had a bomb, we don't need to
// take any further action
if (Cell->GetHasBomb()) return;
// If the cell is adjacent to this cell and
// if it had no adjacent bombs, we should
// clear this cell too
if (
isAdjacent(Cell) &&
Cell->AdjacentBombs == 0
) {
ClearCell();
}
}
// ...
Finally, let's update our HandleEvent()
method to forward UserEvents::CELL_CLEARED
to our new HandleCellCleared()
method:
src/Minesweeper/Cell.cpp
// ...
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:

It may be a little confusing why this works, so let's go over the two key points.
When One Cell is Cleared, Every Cell is Notified
When any cell gets cleared, every cell gets notified. The cell that was cleared pushs an event and attaches a pointer to itself to the data1
member of that event.
Then, every cell gets notified of that event via their HandleEvent()
function, which calls HandleCellCleared()
. This function is called on every cell in our grid, and lets each of those cells decide how they should react to what just happened.
Chain Reactions
If one of those cells decides that they need to clear themselves too, they call their own ClearCell()
method.
This creates a further UserEvents::CELL_CLEARED
event, which again gets reported to every other cell. And this process repeats as often as necessary, causing the cascading effect where large areas of the board to can be cleared automatically.
Complete Code
Complete versions of the files we changed in this part are available below.
Files
Files not listed above have not been changed since the previous section.
Summary
In this part of the tutorial, we've enhanced our 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.
In the next part, we'll focus on detecting and reacting to win and lose conditions, which will complete the core gameplay loop.
Ending and Restarting Games
Implement win/loss detection and add a restart feature to complete the game loop