Ending and Restarting Games

Implement win/loss detection and add a restart feature to complete the game loop

Ryan McCombe
Updated

In this lesson, we'll update our game to detect and react to the player winning or losing.

  • To win, the player must clear all the cells that do not have bombs
  • If the player clears a cell that has a bomb, they lose

Let's get this working!

Updating Globals

When the player wins or loses, we'll disable all the cells and reveal where the bombs are. If the player won, we'll highlight those cells in green, but if the player lost, we'll highlight them in red.

Let's add those colours to our Globals.h. We'll also register events that we can use to communicate when a win or loss happens:

// Globals.h

// ...

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

namespace Config{
  // ...
  inline constexpr SDL_Color BUTTON_SUCCESS_COLOR{
    210, 235, 210, 255};
  inline constexpr SDL_Color BUTTON_FAILURE_COLOR{
    235, 210, 210, 255};
  // ...
}

Triggering GAME_WON and GAME_LOST Events

The game is won when the player clears all the cells, excluding those that have bombs. Currently, the MinesweeperGrid class is managing the cells and placing bombs, so this is the natural place to trigger the GAME_WON event.

GAME_LOST could reasonably be triggered from the MinesweeperCell class, but we'll do it from the MinesweeperGrid too, just so both conditions are in the same place.

Let's update our MinesweeperGrid class with a CellsToClear integer. When this value reaches 0, the player has won.

In our PlaceBombs function, we set this variable to the correct value. The player has to clear all the cells in the grid, excluding those that have bombs, so we do some arithmetic to work out the required value:

// Minesweeper/Grid.h

// ...

class MinesweeperGrid {
// ...

private:
  // ...

  void PlaceBombs(){
    int BombsToPlace{Config::BOMB_COUNT};
    CellsToClear = Config::GRID_COLUMNS
      * Config::GRID_ROWS - 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;
  int CellsToClear;
};

We'll add a HandleCellCleared function, which we'll call every time a UserEvents::CELL_CLEARED event is received. Similar to the previous lesson, we'll static_cast the event's data1 void pointer, so we can call functions on the cell that was cleared.

If the cell contained a bomb, we'll push a UserEvents::GAME_LOST event. Otherwise, we'll decrement the CellsToClear variable, moving the user one step closer to victory.

If this causes CellsToClear to reach 0, the game is won and we push a UserEvents::GAME_WON event:

// Minesweeper/Grid.h

// ...

class MinesweeperGrid {

// ...

private:
  void HandleCellCleared(
    const SDL_UserEvent& E){
    auto* Cell{
      static_cast<MinesweeperCell*>(
        E.data1
      )};

    if (Cell->GetHasBomb()) {
      SDL_Event Event{UserEvents::GAME_LOST};
      SDL_PushEvent(&Event);
    } else {
      --CellsToClear;
      if (CellsToClear == 0) {
        SDL_Event Event{UserEvents::GAME_WON};
        SDL_PushEvent(&Event);
      }
    }
  }
  
  // ...
};

Finally, let's update HandleEvent() to forward any CELL_CLEARED events to this function:

// Minesweeper/Grid.h

// ...

class MinesweeperGrid {
public:
  // ...
  void HandleEvent(const SDL_Event& E){
    if (E.type == UserEvents::CELL_CLEARED) {
      HandleCellCleared(E.user);
    }
    
    for (auto& Child : Children) {
      Child.HandleEvent(E);
    }
  }
  // ...
};

Reacting to the Game Ending

Let's have our MinesweeperCell objects react to the GAME_WON and GAME_LOST events being dispatched from our grid.

We'll update the HandleEvent() function to check for them. When the game is won, we'll change the color of the cell to the green value we set in our Globals.h if it has a bomb.

When the game is lost, we'll reveal where the bombs are by setting isCleared to true, and we'll set those cells to red.

We'll also disable all the cells, preventing them from responding to future mouse events:

// 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);
  } else if (E.type == UserEvents::GAME_WON) {
    if (hasBomb) {
      SetColor(Config::BUTTON_SUCCESS_COLOR);
    }
    SetIsDisabled(true);
  } else if (E.type == UserEvents::GAME_LOST) {
    if (hasBomb) {
      isCleared = true;
      SetColor(Config::BUTTON_FAILURE_COLOR);
    }
    SetIsDisabled(true);
  }
  Button::HandleEvent(E);
}

// ...

If we run our game, we should now see the correct behavior. If we win:

If we lose:

Note that these screenshot were taken with SHOW_DEBUG_HELPERS temporarily disabled to ensure the bombs are hidden if we win, and shown if we lose.

Restarting the Game

Let's add a button to the UI that allows the player to start a new game. We'll add a footer to our current interface that will store this button, as well as a flag counter we'll add in the next part.

Let's add a global to control the height of this footer, and also update our WINDOW_HEIGHT to accommodate it. We'll also register a new event that the button can trigger when it is clicked:

// Globals.h

// ...

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

namespace Config{
  // ...
  
  inline constexpr int FOOTER_HEIGHT{60};
  
  // ...

  inline constexpr int WINDOW_HEIGHT{
    GRID_HEIGHT + FOOTER_HEIGHT
    + PADDING * 2
  };
  
  // ...
}

Creating the New Game Button

Let's create our NewGameButton class. It will inherit from Engine::Button, and will additionally own a Engine::Text object to render the "New Game" text.

// Minesweeper/NewGameButton.h
#pragma once

#include "Engine/Button.h"
#include "Engine/Text.h"

class NewGameButton : public Engine::Button {
private:
  Engine::Text Text;
};

Our constructor will accept the usual x and y arguments to set the button position, and w and h arguments to set the size. We'll forward these to the base Button constructor, as well as to the Text constructor.

Text will also receive 3 extra parameters:

  • The text to render - "NEW GAME" in this case
  • The color to use. The constructor has a default value for this argument, so we pass {} to use it
  • The size to render the text at - we'll use 20
// Minesweeper/NewGameButton.h
#pragma once
#include "Engine/Button.h"
#include "Engine/Text.h"

class NewGameButton : public Engine::Button {
public:
  NewGameButton(int x, int y, int w, int h)
    : Button{x, y, w, h},
      Text{x, y, w, h, "NEW GAME", {}, 20}{}

private:
  Engine::Text Text;
};

We'll override the base Button's Render() method to ensure our Text gets rendered too, and we'll override the HandleLeftClick() event to push our NEW_GAME event when the user clicks the button:

// Minesweeper/NewGameButton.h
#pragma once
#include "Globals.h"

#include "Engine/Button.h"
#include "Engine/Text.h"

class NewGameButton : public Engine::Button {
public:
  NewGameButton(int x, int y, int w, int h)
    : Button{x, y, w, h},
      Text{x, y, w, h, "NEW GAME", {}, 20}{}

  void Render(SDL_Surface* Surface) override{
    Button::Render(Surface);
    Text.Render(Surface);
  }

  void HandleLeftClick() override{
    SDL_Event E{UserEvents::NEW_GAME};
    SDL_PushEvent(&E);
  }

private:
  Engine::Text Text;
};

Finally, lets add an instance of this class to our MinesweeperUI. We'll do some arithmetic using our Config variables to ensure it has the correct position and size, and we'll add it to our Render() and HandleEvent() functions to ensure it renders and receives events:

// Minesweeper/UI.h

#pragma once
#include "Globals.h"
#include "Minesweeper/Grid.h"
#include "Minesweeper/NewGameButton.h"

class MinesweeperUI {
public:
  void Render(SDL_Surface* Surface){
    Grid.Render(Surface);
    Button.Render(Surface);
  }

  void HandleEvent(const SDL_Event& E){
    Grid.HandleEvent(E);
    Button.HandleEvent(E);
  };

private:
  MinesweeperGrid Grid{
    Config::PADDING, Config::PADDING
  };
  NewGameButton Button{
    Config::PADDING,
    Config::GRID_HEIGHT + Config::PADDING * 2,
    Config::WINDOW_WIDTH - Config::PADDING * 2,
    Config::FOOTER_HEIGHT - Config::PADDING
  };
};

Implementing the New Game Logic

When the player wants to start a new game, we need to reset all of our cells, and place a new set of bombs.

Let's implement this in the HandleEvent() function of MinesweeperGrid. When we receive a NEW_GAME event, we'll call Reset() on all of the MinesweeperCell objects, and then invoke PlaceBombs().

Note that MinesweeperCell::Reset() doesn't exist yet - we'll create it in the next section.

With these changes, HandleEvent() in MinesweeperGrid looks like this:

// Minesweeper/Grid.h

// ...

class MinesweeperGrid {
public:
  // ...
  void HandleEvent(const SDL_Event& E){
    if (E.type == UserEvents::CELL_CLEARED) {
      HandleCellCleared(E.user);
    } else if (E.type == UserEvents::NEW_GAME) {
      for (auto& Child : Children) {
        Child.Reset();
      }
      PlaceBombs();
    }
    for (auto& Child : Children) {
      Child.HandleEvent(E);
    }
  }

// ...

};

Over in MinesweeperCell, lets add the Reset() method, which sets the key properties back to their initial values:

// Minesweeper/Cell.h

// ...

class MinesweeperCell : public Engine::Button {
public:
  // ...
  void Reset();
  // ...
};
// Minesweeper/Cell.cpp

// ...

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

Running our program, we should now be able to click the button in the footer to start a new game at any time:

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 lesson, we enhanced our Minesweeper game by implementing win and loss conditions.

We added new events to signal game outcomes, updated the UI to reflect these states, and created a "New Game" button for restarting. Key additions include:

  • Implementing win/loss detection in the MinesweeperGrid class
  • Updating cell appearance based on game outcome
  • Creating a NewGameButton class for game restarts
  • Resetting the game state when starting a new game

In the next lesson, we'll finish off our project by letting the player right click to flag cells they think contain bombs.

We'll also add a counter to our footer, keeping track of how many flags they have remaining.

Next Lesson
Lesson 38 of 129

Placing Flags

Implement flag placement and tracking to complete your Minesweeper project.

Questions & Answers

Answers are generated by AI models and may not have been reviewed. Be mindful when running any code on your device.

Customizing the New Game Button
How can we customize the appearance of the "New Game" button?
Resetting Cells to Clear
Why do we need to reset the CellsToClear variable in PlaceBombs()?
Using Inline Constants in Config Namespace
Why do we use inline for some of the constants in the Config namespace?
Adding a Hint Feature
Is it possible to add a "hint" feature that reveals a safe cell?
Or Ask your Own Question
Purchase the course to ask your own questions