Ending and Restarting Games
Implement win/loss detection and add a restart feature to complete the game loop
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
};
// ...
}
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.
Placing Flags
Implement flag placement and tracking to complete your Minesweeper project.