Win/Loss Logic for Snake
Add game-ending logic to our Snake game with custom events, state management, and visual cues for player feedback.
Now that we have a working Snake game with basic movement and apple collection, it's time to add win-and-loss conditions to create a complete gaming experience.
In this lesson, we'll implement collision detection for game over states, add a winning condition when the snake reaches maximum length, and provide visual feedback for both outcomes.
The GAME_WON
and GAME_LOST
Events
Let's begin by creating new event types that we can dispatch to notify our components that a game has been won or lost:
// GameConfig.h
// ...
namespace UserEvents{
inline Uint32 GAME_WON =
SDL_RegisterEvents(1);
inline Uint32 GAME_LOST =
SDL_RegisterEvents(1);
}
// ...
Winning the Game
The only way to win in Snake is to get our snake to some target length. We'll create a variable in our GameConfig.h
to set what this should be. We're free to set it however we like, and we can derive the value based on the grid size if we want:
// GameConfig.h
// ...
namespace Config{
inline constexpr int MAX_LENGTH{
GRID_COLUMNS * GRID_ROWS};
// ...
}
// ...
Currently, the length of our snake is managed within the GameState
class, where it is incremented every time an APPLE_EATEN
event is encountered. Let's expand that logic to dispatch a GAME_WON
event if our snake's length reaches the maximum length:
// GameState.h
// ...
class GameState {
public:
// ...
void HandleEvent(SDL_Event& E) {
using namespace UserEvents;
using namespace Config;
if (E.type == SDL_KEYDOWN) {
HandleKeyEvent(E.key);
} else if (E.type == APPLE_EATEN) {
++Snake.Length;
if (Snake.Length == MAX_LENGTH) {
SDL_Event Event{GAME_WON};
SDL_PushEvent(&Event);
}
} else if (E.type == RESTART_GAME) {
RestartGame();
}
}
// ...
};
Losing the Game
There are two failure conditions in our Snake game:
- Boundary Collision: When the snake hits the edge of the grid
- Self-Collision: When the snake runs into its own body
Let's implement both of these conditions.
Implementing Boundary Collision
We'll add boundary collision detection in the UpdateSnake()
method of our GameState
class. After calculating the new head position, we'll check if it lies outside the valid grid coordinates.
If it does, we'll dispatch a GAME_LOST
event, otherwise, we'll dispatch an ADVANCE
event as before:
// GameState.h
// ...
class GameState {
// ...
private:
void UpdateSnake() {
Snake.Direction = NextDirection;
switch (NextDirection) {
case Up:
Snake.HeadRow--;
break;
case Down:
Snake.HeadRow++;
break;
case Left:
Snake.HeadCol--;
break;
case Right:
Snake.HeadCol++;
break;
}
if (
Snake.HeadRow < 0 ||
Snake.HeadRow >= Config::GRID_ROWS ||
Snake.HeadCol < 0 ||
Snake.HeadCol >= Config::GRID_COLUMNS
) {
SDL_Event Event{UserEvents::GAME_LOST};
SDL_PushEvent(&Event);
} else {
// If we're not going outside the bounds
// of the grid, advance as before
SDL_Event Event{UserEvents::ADVANCE};
Event.user.data1 = &Snake;
SDL_PushEvent(&Event);
}
}
// ...
};
Implementing Self Collision
For detecting when the snake collides with itself, we'll use our Cell
class. Within the Cell::Advance()
method, we'll check if the snake is moving into a cell that already contains a snake segment. If it is, we'll dispatch a GAME_LOST
event:
// Cell.h
// ...
class Cell {
// ...
private:
void Advance(SDL_UserEvent& E) {
SnakeData* Data{
static_cast<SnakeData*>(E.data1)};
bool isThisCell{
Data->HeadRow == Row &&
Data->HeadCol == Column
};
if (isThisCell) {
if (CellState == Snake) {
SDL_Event Event{UserEvents::GAME_LOST};
SDL_PushEvent(&Event);
return;
}
if (CellState == Apple) {
SDL_Event Event{
UserEvents::APPLE_EATEN};
SDL_PushEvent(&Event);
}
CellState = Snake;
SnakeDuration = Data->Length;
} else if (CellState == Snake) {
SnakeDuration--;
if (SnakeDuration <= 0) {
CellState = Empty;
}
}
}
// ...
};
Reacting to Victory and Loss
Now that we're dispatching GAME_WON
and GAME_LOST
events at the correct time, we need to update our components to react to these events. We'll do three things when the game is over:
- Our
GameState
will have aGameOver
state which pauses the game and ignores keyboard input until the user presses the restart button. - The
RestartButton
will be highlighted, indicating that the user needs to press it. - The snake's color will change to indicate whether the user won or lost.
Let's update these components step by step.
1. Updating the Game State
We'll add an IsGameOver
boolean to our GameState
class. We'll initialize it to false
, and reset it back to false
every time we restart the game:
// GameState.h
// ...
class GameState {
// ...
private:
bool IsGameOver{false};
void RestartGame() {
IsPaused = true;
IsGameOver = false;
ElapsedTime = 0;
Snake = {Config::GRID_ROWS / 2, 3, 2, Right};
NextDirection = Right;
}
// ...
};
When our GameState
object receives a GAME_LOST
or GAME_WON
event, we'll update this IsGameOver
variable to true
:
// GameState.h
// ...
class GameState {
public:
// ...
void HandleEvent(SDL_Event& E) {
using namespace UserEvents;
using namespace Config;
if (E.type == SDL_KEYDOWN) {
HandleKeyEvent(E.key);
} else if (E.type == APPLE_EATEN) {
++Snake.Length;
if (Snake.Length == MAX_LENGTH) {
SDL_Event Event{GAME_WON};
SDL_PushEvent(&Event);
}
} else if (E.type == RESTART_GAME) {
RestartGame();
} else if (E.type == GAME_LOST ||
E.type == GAME_WON) {
IsGameOver = true;
}
}
// ...
};
When the game is over, we'll replicate a similar behavior to the initial IsPaused
state. First, we'll disable our ticking logic:
// GameState.h
// ...
class GameState {
public:
// ...
void Tick(Uint32 DeltaTime) {
if (IsPaused || IsGameOver) return;
ElapsedTime += DeltaTime;
if (ElapsedTime >= Config::ADVANCE_INTERVAL) {
ElapsedTime = 0;
UpdateSnake();
}
}
// ...
};
Additionally, when the game is over, we'll ignore keyboard input until the game is restarted:
// GameState.h
// ...
class GameState {
// ...
private:
void HandleKeyEvent(SDL_KeyboardEvent& E) {
if (IsGameOver) return;
}
}
};
If we run our game, we should now note that our snake stops moving and our keyboard input is ignored once we win or lose the game. Note that the IsGameOver
and IsPaused
states are subtly different.
Both of these states disable our GameState
object's Tick()
behavior, but the logic in our HandleKeyEvent()
function continues to be evaluated if the game is only paused. Players can press the right arrow or D key to unpause the game by setting IsPaused
to false
, but to set IsGameOver
to false
, they need to restart the game.
2. Highlighting the Restart Button
To indicate to the user that they need to press the restart button when the game is over, let's highlight that button. We'll first create a variable to control the highlighted color of our button:
// GameConfig.h
// ...
namespace Config{
inline constexpr SDL_Color BUTTON_COLOR{
73, 117, 46, 255};
inline constexpr SDL_Color BUTTON_HIGHLIGHT_COLOR{
67, 117, 234, 255};
}
// ...
In our RestartButton
class, we'll update our ButtonColor
member based on GAME_LOST
, GAME_WON
, and RESTART_GAME
events:
// RestartButton.h
// ...
class RestartButton {
public:
// ...
void HandleEvent(SDL_Event& E) {
using namespace UserEvents;
using namespace Config;
if (E.type == SDL_MOUSEBUTTONDOWN) {
HandleClick(E.button);
} else if (
E.type == GAME_LOST ||
E.type == GAME_WON
) {
CurrentColor = BUTTON_HIGHLIGHT_COLOR;
} else if (E.type == RESTART_GAME) {
CurrentColor = BUTTON_COLOR;
}
}
// ...
};
Running our code, we should now see that the restart button uses a different color when the game is over:

3. Changing Snake Color
Finally, let's update the color of our snake when the user has won or lost the game. We'll add new configuration variables for these colors:
// GameConfig.h
// ...
namespace Config{
inline constexpr SDL_Color SNAKE_COLOR{
67, 117, 234, 255};
inline constexpr SDL_Color SNAKE_LOST_COLOR{
227, 67, 97, 255};
inline constexpr SDL_Color SNAKE_VICTORY_COLOR{
255, 140, 0, 255};
// ...
}
In our Cell
class, we'll add a SnakeColor
variable to keep track of what color our snake segments should be. We'll set this to the default SNAKE_COLOR
variable every time our cell is initialized:
// Cell.h
// ...
class Cell {
// ...
private:
// ...
SDL_Color SnakeColor{Config::SNAKE_COLOR};
void Initialize() {
CellState = Empty;
SnakeColor = Config::SNAKE_COLOR;
SnakeDuration = 0;
int MiddleRow{Config::GRID_ROWS / 2};
if (Row == MiddleRow && Column == 2) {
CellState = Snake;
SnakeDuration = 1;
} else if (Row == MiddleRow && Column == 3) {
CellState = Snake;
SnakeDuration = 2;
} else if (Row == MiddleRow && Column == 11) {
CellState = Apple;
}
}
};
Within the HandleEvent()
function, we'll change this color depending on whether the player won or lost. Our Initialize()
function is already being called when the game is restarted, so we're already handling that scenario:
// Cell.h
// ...
class Cell {
public:
// ...
void HandleEvent(SDL_Event& E) {
using namespace UserEvents;
if (E.type == ADVANCE) {
Advance(E.user);
} else if (E.
type == APPLE_EATEN) {
if (CellState == Snake) {
++SnakeDuration;
}
} else if (E.type == GAME_LOST) {
SnakeColor = Config::SNAKE_LOST_COLOR;
} else if (E.type == GAME_WON) {
SnakeColor = Config::SNAKE_VICTORY_COLOR;
} else if (E.type == RESTART_GAME) {
Initialize();
}
}
// ...
};
Finally, let's update our Render()
method to use this new SnakeColor
variable, rather than the static Config::SNAKE_COLOR
value it was previously using:
// Cell.h
// ...
class Cell {
public:
// ...
void Render(SDL_Surface* Surface) {
SDL_FillRect(Surface, &BackgroundRect,
SDL_MapRGB(
Surface->format,
BackgroundColor.r,
BackgroundColor.g,
BackgroundColor.b
)
);
if (CellState == Apple) {
Assets.Apple.Render(
Surface, &BackgroundRect);
} else if (CellState == Snake) {
SDL_FillRect(Surface, &BackgroundRect,
SDL_MapRGB(
Surface->format,
SnakeColor.r,
SnakeColor.g,
SnakeColor.b
)
);
}
}
// ...
};
Now, when we lose, our snake should be rendered in the SNAKE_LOSS_COLOR
we defined:

And, when we win, it should use the SNAKE_VICTORY_COLOR
. To test this, it can be helpful to temporarily reduce the MAX_LENGTH
value to make winning easier:

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've implemented win-and-loss conditions. We've used the familiar event-based techniques to signal changes in one component, and react to those events throughout our application to keep our game state synchronised and provide visual feedback to players.
Next, we'll update our UI to include a score tracker, keeping track of how many apples the player has collected.
Building the Score Display
Build a dynamic score counter that tracks the player's progress and displays it with custom graphics and text.