Adding Bombs to the Minesweeper Grid

Updating the game to to place bombs randomly in the grid and render them when cells are cleared.
This lesson is part of the course:

Building Minesweeper with C++ and SDL2

Apply what we learned to build an interactive, portfolio-ready capstone project using C++ and the SDL2 library

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 existing code to add a crucial element of the game: bombs.

We'll implement the ability to place bombs randomly within our grid and reveal them when cells containing bombs are cleared.

We'll start by updating our global configurations to include new variables for bomb-related events and settings. Then, we'll modify our grid and cell classes to handle bomb placement and rendering.

By the end of this section, you'll have a Minesweeper grid where bombs can be placed and revealed, setting the stage for implementing the game's core mechanics in future parts.

Updating Globals

To begin implementing bombs in our Minesweeper game, we'll first update our Globals.h file with some new variables. These additions will help us manage bomb-related events and configure the game's difficulty:

  1. UserEvents::BOMB_PLACED: This new event type will be dispatched whenever a bomb is placed within a cell. This will allow other cells to keep track of how many bombs are in adjacent cells, which we'll use later to display the correct number.
  2. Config::BOMB_COUNT: This variable will control how many bombs are placed in the grid. By adjusting this number, we can easily change the difficulty of the game - more bombs generally mean a more challenging experience for the player.
  3. Config::BOMB_IMAGE: This string specifies the file name of the icon we want to render in cells that contain a bomb. By defining this in our global configuration, we can easily change the bomb image across our entire game if needed.
// Globals.h

// ...

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

namespace Config{
  inline constexpr int BOMB_COUNT{6};
  // ...

  // Asset Paths
  inline const std::string BOMB_IMAGE{
    "Bomb.png"};
  // ...
}

// ...

Dealing with Invalid Configurations

When working with our game configuration, it's possible to inadvertently create an invalid setup. For example, imagine if we set BOMB_COUNT to be greater than the total number of cells in our grid:

inline constexpr int BOMB_COUNT{100};
inline constexpr int GRID_COLUMNS{8};
inline constexpr int GRID_ROWS{4};

In this case, we're trying to place 100 bombs in a grid with only 32 cells (8 * 4), which is impossible.

You might want to check for invalid configurations. One way of doing this is with static_assert. This is a compile-time check that ensures certain conditions are met before the program can be compiled.

Here's how static_assert works:

  1. It takes two arguments: a boolean condition and an error message.
  2. If the condition is false, compilation fails with the provided error message.
  3. The condition must be known at compile-time, which our constexpr config variables satisfy.

Here's an example of how you could use static_assert to prevent this invalid configuration:

// Globals.h

// ...

namespace Config{
  // Game Settings
  inline constexpr int BOMB_COUNT{100}; 
  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"
  );
  
  // ...
}
// ...
Error: static_assert failed: Cannot have more bombs than cells

Placing Bombs in Random Cells

To implement bomb placement in our grid, we'll update our MinesweeperGrid class with a new PlaceBombs() function. This function will be responsible for randomly selecting cells and placing bombs in them.

Here's how the PlaceBombs() function will work:

  1. We'll create a loop that continues until we've placed the required number of bombs (specified by Config::BOMB_COUNT).
  2. In each iteration, we'll select a random index from our Children vector, which contains all the cells in our grid.
  3. We'll call a new PlaceBomb() function (which we'll add to our MinesweeperCell class later) on the randomly selected cell.
  4. If PlaceBomb() returns true, indicating a successful bomb placement, we'll decrement our counter of bombs to place.
  5. If PlaceBomb() returns false, indicating the selected cell already contains a bomb, we'll continue the loop to try placing the bomb in a different random cell.

This approach ensures that we place the correct number of bombs, and that no cell contains more than one bomb.

// Minesweeper/Grid.h
#pragma once
#include "Globals.h"
#include "Minesweeper/Cell.h"
#include "Engine/Random.h" 

class MinesweeperGrid {
// ...

private:
  void PlaceBombs(){
    int BombsToPlace{Config::BOMB_COUNT};
    while (BombsToPlace > 0) {
      const size_t RandomIndex{
        Engine::Random::Int(
          0, Children.size() - 1
        )};
      if (Children[RandomIndex].PlaceBomb()) {
        --BombsToPlace;
      }
    }
  }

  std::vector<MinesweeperCell> Children;
};

We’ll call PlaceBombs() at the end of the MinesweeperGrid constructor.

// Minesweeper/Grid.h

// ...

class MinesweeperGrid {
public:
  MinesweeperGrid(int x, int y){
    // ...
    PlaceBombs();
  }
  
  // ...
};

Updating Cells to Receive Bombs

Now that we've implemented bomb placement in our grid, we need to update our MinesweeperCell class to handle bombs. We'll make the following changes:

  1. Add a new PlaceBomb() function that returns a bool. This function will be called by MinesweeperGrid when attempting to place a bomb in the cell.
  2. Add a hasBomb member variable to keep track of whether the cell contains a bomb.
  3. Create a GetHasBomb() getter function to allow other parts of our program to check if a cell contains a bomb without directly accessing the hasBomb variable.

Let’s update our header file with these three declarations:

// Minesweeper/Cell.h

// ...

class MinesweeperCell : public Engine::Button {
public:
  // ...
  bool PlaceBomb();

  [[nodiscard]]
  bool GetHasBomb() const{ return hasBomb; }
  // ...

private:
  bool hasBomb{false};
  // ...
};

Over in our MinesweeperCell.cpp file, we’ll implement the PlaceBomb() function. If the cell already has a bomb, we’ll immediately return false, prompting the PlaceBombs() loop back in MinesweeperGrid to choose another cell.

Otherwise, we’ll update our hasBomb member variable, report that a bomb has been placed, and return true to inform MinesweeperGrid that a bomb was successfully placed:

// Minesweeper/Cell.cpp

// ...

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

// ...

We'll also update our HandleEvent() function to react to bomb placement events.

For now, we'll just log that a bomb was placed, but in future parts, we'll use this to update the count of adjacent bombs for neighboring cells.

// 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) {
    // TODO
    std::cout << "A Bomb was Placed\n";
  }
  Button::HandleEvent(E);
}

// ...

We can now run our program and verify everything is connected correctly by looking at our terminal output. Every time a bomb is placed in a cell, every cell will react to the event, so we should see a long list of logs:

A Bomb was Placed
A Bomb was Placed
A Bomb was Placed
...

Naturally, we’ll replace this logging with more meaningful reactions later.

Rendering Bomb Images

Finally, lets update our program to render the bomb images in the appropriate cells. We’ll add a BombImage member variable:

// Minesweeper/Cell.h
#pragma once
#include "Engine/Button.h"
#include "Engine/Image.h"

class MinesweeperCell : public Engine::Button {
 // ...

private:
  Engine::Image BombImage;
};

Over in our constructor, we’ll initialize it. Our Engine::Image class needs to know the position (x and y) and size (w and h) we want the image to be rendered on the surface. It also needs to know the location of the image file,. We’ll pass all of this information to its constructor:

// 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}{};

// ...

Finally, we update the Render() method of our MinesweeperCell. If the player has cleared the cell and the cell contains a bomb, we render the image:

// Minesweeper/Cell.cpp

// ...

void MinesweeperCell::Render(
  SDL_Surface* Surface){
  Button::Render(Surface);
  if (isCleared && hasBomb) {
    BombImage.Render(Surface);
  }
}

If we run our game and clear cells, we should now eventually find a bomb:

Screenshot of our program

Development Helpers

To make future development easier, it would be helpful to know where the bombs are without having to find them by clearing cells. In our Globals.h file, we created a preprocessor definition called SHOW_DEBUG_HELPERS.

We can use this preprocessor definition in our MinesweeperCell::Render() method to display bombs in all cells during development, regardless of whether they've been cleared:

// Minesweeper/Cell.cpp

// ...

void MinesweeperCell::Render(
  SDL_Surface* Surface){
  Button::Render(Surface);
  if (isCleared && hasBomb) {
    BombImage.Render(Surface);
  }
#ifdef SHOW_DEBUG_HELPERS
  else if (hasBomb) {
    BombImage.Render(Surface);
  }
#endif
}

When our preprocessor definition is enabled, we will be able to see bombs without clearing cells, which will make future development more convenient:

Screenshot of our program

To enable these debug helpers, ensure the SHOW_DEBUG_HELPERS macro is defined in your Globals.h file. To disable them, simply comment out or remove the #define SHOW_DEBUG_HELPERS line:

// Globals.h

// #define SHOW_DEBUG_HELPERS 

// ...

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>

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

  // 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 "Globals.h"
#include "Minesweeper/Cell.h"
#include "Engine/Random.h"

class MinesweeperGrid {
public:
  MinesweeperGrid(int x, int y){
    using namespace Config;
    Children.reserve(GRID_COLUMNS * GRID_ROWS);
    for (int Col{1}; Col <= GRID_COLUMNS; ++
         Col) {
      for (int Row{1}; Row <= GRID_ROWS; ++
           Row) {
        constexpr int Spacing{
          CELL_SIZE + PADDING};
        Children.emplace_back(
          x + (Spacing) * (Col - 1),
          y + (Spacing) * (Row - 1),
          CELL_SIZE, CELL_SIZE, Row, Col
        );
      }
    }
    PlaceBombs();
  }

  void Render(SDL_Surface* Surface){
    for (auto& Child : Children) {
      Child.Render(Surface);
    }
  }

  void HandleEvent(const SDL_Event& E){
    for (auto& Child : Children) {
      Child.HandleEvent(E);
    }
  }

private:
  void PlaceBombs(){
    int BombsToPlace{Config::BOMB_COUNT};
    while (BombsToPlace > 0) {
      const size_t RandomIndex{
        Engine::Random::Int(
          0, Children.size() - 1
        )};
      if (Children[RandomIndex].PlaceBomb()) {
        --BombsToPlace;
      }
    }
  }

  std::vector<MinesweeperCell> Children;
};
#pragma once
#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:
  void ClearCell();
  void ReportEvent(uint32_t EventType);

  int Row;
  int Col;
  bool hasBomb{false};
  bool isCleared{false};
  Engine::Image BombImage;
};
#include <iostream>
#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}{};

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) {
    // TODO
    std::cout << "A Bomb was Placed\n";
  }
  Button::HandleEvent(E);
}

void MinesweeperCell::Render(
  SDL_Surface* Surface){
  Button::Render(Surface);
  if (isCleared && hasBomb) {
    BombImage.Render(Surface);
  }

#ifdef SHOW_DEBUG_HELPERS
  if (hasBomb) { BombImage.Render(Surface); }
#endif
}

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::HandleLeftClick(){
  ClearCell();
}

Files not listed above have not been changed since the previous section.

Summary

In this part of our Minesweeper tutorial, we've made significant progress in implementing one of the core elements of the game: bombs. We are now:

  1. Implementing a system for randomly placing bombs in the grid.
  2. Updating cells to track whether they contain a bomb.
  3. Rendering bomb images when cells are cleared.
  4. Adding a debug helper to visualize bomb placement during development.

In the next part of our tutorial, we'll build upon this foundation to implement the ability for cleared cells to display the number of bombs in neighboring cells. This feature is essential for providing players with the information they need to deduce the location of bombs and clear the grid safely.

We'll modify our cell class to keep track of neighboring bombs, update this count when bombs are placed, and display the count when a cell is cleared. This will involve working with events, updating our rendering logic, and introducing new graphical elements to represent the numbers.

Was this lesson useful?

Next Lesson

Adjacent Cells and Bomb Counting

Implement the techniques for detecting nearby bombs and clearing empty cells automatically.
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Posted
Lesson Contents

Adding Bombs to the Minesweeper Grid

Updating the game to to place bombs randomly in the grid and render them when cells are cleared.

3D art representing computer programming
This lesson is part of the course:

Building Minesweeper with C++ and SDL2

Apply what we learned to build an interactive, portfolio-ready capstone project using C++ and the SDL2 library

Free, Unlimited Access
3D art representing computer programming
This lesson is part of the course:

Building Minesweeper with C++ and SDL2

Apply what we learned to build an interactive, portfolio-ready capstone project using C++ and the SDL2 library

Free, unlimited access

This course includes:

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

Adjacent Cells and Bomb Counting

Implement the techniques for detecting nearby bombs and clearing empty cells automatically.
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved