Win/Loss Logic for Snake
Add game-ending logic with custom events, state management, and visual cues for player feedback.
Now that we have a working game with basic movement and apple collection, it's time to add win-and-loss conditions to create a complete 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.
Starting Point
We'll continue from where we left off in the previous lesson. Our project already has a grid of interactive cells, a controllable snake, the ability for the snake to eat apples and grow, and a restart button. The starting code for the most relevant files is provided below.
Files
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:
src/Globals.h
#pragma once
#define CHECK_ERRORS
#include <iostream>
#include <SDL3/SDL.h>
#include <string>
namespace UserEvents{
inline const Uint32 ADVANCE{SDL_RegisterEvents(1)};
inline const Uint32 APPLE_EATEN{SDL_RegisterEvents(1)};
inline const Uint32 RESTART_GAME{SDL_RegisterEvents(1)};
inline const Uint32 GAME_WON{
SDL_RegisterEvents(1)};
inline const 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 Globals.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:
src/Globals.h
// ...
namespace Config{
// ...
inline constexpr int GRID_COLUMNS{16};
static_assert(
GRID_COLUMNS >= 12,
"Grid must be at least 12 columns wide");
inline constexpr int GRID_ROWS{5};
static_assert(
GRID_ROWS >= 5,
"Grid must be at least 5 rows tall");
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:
src/GameState.h
#pragma once
#include <SDL3/SDL.h>
#include "Globals.h"
#include "Snake/SnakeData.h"
class GameState {
public:
void HandleEvent(const SDL_Event& E) {
if (E.type == SDL_EVENT_KEY_DOWN) {
HandleKeyEvent(E.key);
} else if (E.type == UserEvents::APPLE_EATEN) {
++Snake.Length;
if (Snake.Length == Config::MAX_LENGTH) {
SDL_Event Event{};
Event.type = UserEvents::GAME_WON;
SDL_PushEvent(&Event);
}
} else if (E.type == UserEvents::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:
src/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{};
Event.type = UserEvents::GAME_LOST;
SDL_PushEvent(&Event);
} else {
// If we're not going outside the bounds
// of the grid, advance as before
SDL_Event Event{};
Event.type = 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:
src/Snake/Cell.cpp
// ...
void Cell::Advance(const SDL_UserEvent& E) {
auto* Data{static_cast<SnakeData*>(E.data1)};
bool isThisCell{
Data->HeadRow == Row &&
Data->HeadCol == Column
};
if (isThisCell) {
if (State == CellState::Snake) {
SDL_Event Event{};
Event.type = UserEvents::GAME_LOST;
SDL_PushEvent(&Event);
return;
}
if (State == CellState::Apple) {
SDL_Event Event{.type = UserEvents::APPLE_EATEN};
SDL_PushEvent(&Event);
}
State = CellState::Snake;
SnakeDuration = Data->Length;
} else if (State == CellState::Snake) {
--SnakeDuration;
if (SnakeDuration <= 0) {
State = 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
GameStatewill have aGameOverstate which pauses the game and ignores keyboard input until the user presses the restart button. - The
RestartButtonwill 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:
src/GameState.h
#pragma once
#include <SDL3/SDL.h>
#include "Globals.h"
#include "Snake/SnakeData.h"
class GameState {
// ...
private:
void RestartGame() {
IsPaused = true;
IsGameOver = false;
ElapsedTime = 0;
Snake = {
.HeadRow = Config::GRID_ROWS / 2,
.HeadCol = 3,
.Length = 2,
.Direction = Right
};
NextDirection = Right;
}
bool IsPaused{true};
bool IsGameOver{false};
// ...
};When our GameState object receives a GAME_LOST or GAME_WON event, we'll update this IsGameOver variable to true:
src/GameState.h
#pragma once
#include <SDL3/SDL.h>
#include "Globals.h"
#include "Snake/SnakeData.h"
class GameState {
public:
void HandleEvent(const SDL_Event& E) {
if (E.type == SDL_EVENT_KEY_DOWN) {
HandleKeyEvent(E.key);
} else if (E.type == UserEvents::APPLE_EATEN) {
++Snake.Length;
if (Snake.Length == Config::MAX_LENGTH) {
SDL_Event Event{};
Event.type = UserEvents::GAME_WON;
SDL_PushEvent(&Event);
}
} else if (E.type == UserEvents::RESTART_GAME) {
RestartGame();
} else if (E.type == UserEvents::GAME_LOST ||
E.type == UserEvents::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:
src/GameState.h
#pragma once
#include <SDL3/SDL.h>
#include "Globals.h"
#include "Snake/SnakeData.h"
class GameState {
public:
// ...
void Tick(Uint64 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:
src/GameState.h
// ...
class GameState {
// ...
private:
void HandleKeyEvent(const 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:
src/Globals.h
// ...
namespace Config{
// ...
// Colors
// ...
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 HandleEvent() function to change the CurrentColor member based on GAME_LOST, GAME_WON, and RESTART_GAME events:
src/Snake/RestartButton.h
#pragma once
#include <SDL3/SDL.h>
#include "Globals.h"
#include "Engine/Text.h"
class RestartButton {
public:
// ...
void HandleEvent(const SDL_Event& E) {
if (E.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
HandleClick(E.button);
} else if (
E.type == UserEvents::GAME_LOST ||
E.type == UserEvents::GAME_WON
) {
CurrentColor = Config::BUTTON_HIGHLIGHT_COLOR;
} else if (E.type == UserEvents::RESTART_GAME) {
CurrentColor = Config::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:
src/Globals.h
// ...
namespace Config{
// ...
// Colors
// ...
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 reset:
src/Snake/Cell.h
// ...
class Cell {
// ...
private:
// ...
SDL_Color SnakeColor{Config::SNAKE_COLOR};
};src/Snake/Cell.cpp
// ...
void Cell::Reset() {
State = CellState::Empty;
SnakeColor = Config::SNAKE_COLOR;
SnakeDuration = 0;
int MiddleRow{Config::GRID_ROWS / 2};
if (Row == MiddleRow && Column == 2) {
State = CellState::Snake;
SnakeDuration = 1;
} else if (Row == MiddleRow && Column == 3) {
State = CellState::Snake;
SnakeDuration = 2;
} else if (Row == MiddleRow && Column == 11) {
State = CellState::Apple;
}
}
// ...Within the HandleEvent() function, we'll change this color depending on whether the player won or lost. Our Reset() function is already being called when the game is restarted, so we're already handling that scenario:
src/Snake/Cell.cpp
// ...
void Cell::HandleEvent(const SDL_Event& E) {
if (E.type == UserEvents::ADVANCE) {
Advance(E.user);
} else if (E.type == UserEvents::APPLE_EATEN) {
if (State == CellState::Snake) {
++SnakeDuration;
}
} else if (E.type == UserEvents::GAME_LOST) {
SnakeColor = Config::SNAKE_LOST_COLOR;
} else if (E.type == UserEvents::GAME_WON) {
SnakeColor = Config::SNAKE_VICTORY_COLOR;
} else if (E.type == UserEvents::RESTART_GAME) {
Reset();
}
}
// ...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:
src/Snake/Cell.cpp
// ...
void Cell::Render(SDL_Surface* Surface) {
SDL_FillSurfaceRect(
Surface, &BackgroundRect,
SDL_MapRGB(
SDL_GetPixelFormatDetails(Surface->format),
nullptr,
BackgroundColor.r,
BackgroundColor.g,
BackgroundColor.b
)
);
if (State == CellState::Apple) {
AssetList.Apple.Render(
Surface, &BackgroundRect);
} else if (State == CellState::Snake) {
SDL_FillSurfaceRect(Surface, &BackgroundRect,
SDL_MapRGB(
SDL_GetPixelFormatDetails(Surface->format),
nullptr,
SnakeColor.r,
SnakeColor.g,
SnakeColor.b
)
);
}
}
// ...Now, when we lose, our snake should be rendered in the SNAKE_LOST_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
Files not listed above have not been changed since the previous section.
Summary
In this lesson, we implemented the win and loss conditions for our Snake game, bringing it closer completion. Key steps from this lesson include:
- Win/Loss Events: We created custom SDL events (
GAME_WONandGAME_LOST) to signal the end of the game. - Collision Detection: We implemented logic to detect both boundary and self-collisions, triggering a game loss.
- Win Condition: We added a check for when the snake reaches its maximum possible length, triggering a game win.
- Visual Feedback: We updated the game to change the snake's color and highlight the restart button based on the game's outcome.
- Game State Control: We enhanced the
GameStateclass to pause the game and disable input when the game is over.
With these features in place, our game is nearly complete. The next lessons will add the final touches, including a score display and smoother snake movement.
Building the Score Display
Build a dynamic score counter that tracks the player's progress and displays it with custom graphics and text.