Adjacent Cells and Bomb Counting

Implement the techniques for detecting nearby bombs and clearing empty cells automatically.
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Posted

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 is calling the method.

// 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 or 1, and
  • A.Col - B.Col is either -1, 0 or 1

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, and
  • std::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 adding two new elements to our MinesweeperCell class:

  1. An AdjacentBombs integer to store the count of adjacent bombs.
  2. A HandleBombPlaced method to receive the event, and update the AdjacentBombs 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:

Screenshot of our program

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:

Screenshot of our program

Complete Code

Complete versions of the files we changed in this part are available below

#pragma once

#define SHOW_DEBUG_HELPERS

#include <iostream>
#include <SDL.h>
#include <string>
#include <vector>

namespace UserEvents{
  inline Uint32 CELL_CLEARED =
    SDL_RegisterEvents(1);
  inline Uint32 BOMB_PLACED =
    SDL_RegisterEvents(1);
}

namespace Config{
  // Game Settings
  inline const std::string GAME_NAME{
    "Minesweeper"};
  inline constexpr int BOMB_COUNT{6};
  inline constexpr int GRID_COLUMNS{8};
  inline constexpr int GRID_ROWS{4};
  static_assert(
    BOMB_COUNT < GRID_COLUMNS * GRID_ROWS,
    "Cannot have more bombs than cells"
  );

  // Size and Positioning
  inline constexpr int PADDING{5};
  inline constexpr int CELL_SIZE{50};

  inline constexpr int GRID_HEIGHT{
    CELL_SIZE * GRID_ROWS
    + PADDING * (GRID_ROWS - 1)
  };

  inline constexpr int WINDOW_HEIGHT{
    GRID_HEIGHT + PADDING * 2
  };
  inline constexpr int WINDOW_WIDTH{
    CELL_SIZE * GRID_COLUMNS
    + PADDING * (GRID_COLUMNS - 1)
    + PADDING * 2
  };

  // Colors
  inline constexpr SDL_Color BACKGROUND_COLOR{
    170, 170, 170, 255};
  inline constexpr SDL_Color BUTTON_COLOR{
    200, 200, 200, 255};
  inline constexpr SDL_Color BUTTON_HOVER_COLOR{
    220, 220, 220, 255};
  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
  inline const std::string BOMB_IMAGE{
    "Bomb.png"};
  inline const std::string FONT{
    "Rubik-SemiBold.ttf"};
}

namespace Utils{
#ifdef SHOW_DEBUG_HELPERS
  inline void CheckSDLError(
    const std::string& Msg){
    const char* error = SDL_GetError();
    if (*error != '\0') {
      std::cerr << Msg << " Error: " << error <<
        '\n';
      SDL_ClearError();
    }
  }
#endif
}
#pragma once
#include "Engine/Button.h"
#include "Engine/Image.h"
#include "Engine/Text.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;
  void Reset();
  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:
  void ClearCell();
  void ReportEvent(uint32_t EventType);
  void HandleCellCleared(
    const SDL_UserEvent& E);
  void HandleBombPlaced(const SDL_UserEvent& E);
  bool isAdjacent(MinesweeperCell* Other) const;

  int AdjacentBombs{0};
  int Row;
  int Col;
  bool hasBomb{false};
  bool isCleared{false};
  Engine::Image BombImage;
  Engine::Text Text;
};
#include <string>
#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{
      x, y, w, h,
      Config::BOMB_IMAGE},
    Text{
      x, y, w, h,
      std::to_string(AdjacentBombs),
      Config::TEXT_COLORS[AdjacentBombs]}{};

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);
}

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
}

void MinesweeperCell::Reset(){
  isCleared = false;
  hasBomb = false;
  AdjacentBombs = 0;
  SetIsDisabled(false);
  SetColor(Config::BUTTON_COLOR);
}

bool MinesweeperCell::PlaceBomb(){
  if (hasBomb) return false;
  hasBomb = true;
  ReportEvent(UserEvents::BOMB_PLACED);
  return true;
}

void MinesweeperCell::ClearCell(){
  if (isCleared) return;
  isCleared = true;
  SetIsDisabled(true);
  SetColor(Config::BUTTON_CLEARED_COLOR);
  ReportEvent(UserEvents::CELL_CLEARED);
}

void MinesweeperCell::ReportEvent(
  uint32_t EventType){
  SDL_Event event{EventType};
  event.user.data1 = this;
  SDL_PushEvent(&event);
}

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(); }
}

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]);
  }
}

bool MinesweeperCell::isAdjacent(
  MinesweeperCell* Other) const{
  return !(Other == this)
    && std::abs(Other->GetRow() - Row) <= 1
    && std::abs(Other->GetCol() - Col) <= 1;
}

void MinesweeperCell::HandleLeftClick(){
  ClearCell();
}

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.

Was this lesson useful?

Next Lesson

Ending and Restarting Games

Implement win/loss detection and add a restart feature to complete the game loop
Abstract art representing computer programming
New: AI-Powered AssistanceAI Assistance

Questions and HelpNeed Help?

Get instant help using our free AI assistant, powered by state-of-the-art language models.

Ryan McCombe
Ryan McCombe
Posted
Lesson Contents

Adjacent Cells and Bomb Counting

Implement the techniques for detecting nearby bombs and clearing empty cells automatically.

sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, Unlimited Access
Project: Making Minesweeper
  • 53.GPUs and Rasterization
  • 54.SDL Renderers
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, unlimited access

This course includes:

  • 55 Lessons
  • 100+ Code Samples
  • 91% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Ending and Restarting Games

Implement win/loss detection and add a restart feature to complete the game loop
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved