In this lesson, we’ll update our game to detect and react to the player winning or losing.
Let’s get this working!
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};
// ...
}
GAME_WON
and GAME_LOST
EventsThe 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);
}
}
// ...
};
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.
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:
{}
to use it// 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
};
};
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);
}
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:
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.
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:
MinesweeperGrid
classNewGameButton
class for game restartsIn 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.
Implement win/loss detection and add a restart feature to complete the game loop
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games