Ending and Restarting Games

Implement win/loss detection and add a restart feature to complete the game loop
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
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:

Screenshot of our program

If we lose:

Screenshot of our program

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

Creating the New Game Button

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

Handling NEW_GAME within MinesweeperCell

Our MinesweeperCell class also has visibility of NEW_GAME events, so we could call Reset() from MinesweeperCell::HandleEvent instead of from the parent grid.

However, if taking this approach, we need to be mindful of the order the events will be processed in. We need to ensure the bombs are placed after the cells have been reset.

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

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);
  inline Uint32 GAME_WON =
    SDL_RegisterEvents(1);
  inline Uint32 GAME_LOST =
    SDL_RegisterEvents(1);
  inline Uint32 NEW_GAME =
    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 FOOTER_HEIGHT{60};

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

    inline constexpr int GRID_WIDTH{
    CELL_SIZE * GRID_COLUMNS
    + PADDING * (GRID_COLUMNS - 1)
  };

  inline constexpr int WINDOW_HEIGHT{
    GRID_HEIGHT + FOOTER_HEIGHT
    + PADDING * 2
  };
  inline constexpr int WINDOW_WIDTH{
    GRID_WIDTH + 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};
  inline constexpr SDL_Color
  BUTTON_SUCCESS_COLOR{
    210, 235, 210, 255};
  inline constexpr SDL_Color
  BUTTON_FAILURE_COLOR{
    235, 210, 210, 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);
  } 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);
}

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;
  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){
  if (isCleared) return;
  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();
}
#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;
};
#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
  };
};

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.

Was this lesson useful?

Next Lesson

Placing Flags

Implement flag placement and tracking to complete your Minesweeper project.
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
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
  • 60.GPUs and Rasterization
  • 61.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:

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

Placing Flags

Implement flag placement and tracking to complete your Minesweeper project.
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved