โš™๏ธ My First C++ Game ยท Episode 2 of 7 ยท See All Episodes
๐Ÿ Episode 2 ยท Beginner+ ยท Data Structures

Grow Long!
Snake in C++

The 1997 classic rebuilt with modern C++: a std::deque body, grid movement on a timer, food, self-collision and speed-up. This episode is secretly about data structures, the fun way.

๐Ÿ‘ถ Ages 12+ โฑ๏ธ ~1.5 Hours โš™๏ธ C++ & SFML โœ“ Free
๐Ÿ Snake logic ๐Ÿ“š std::deque ๐Ÿงญ Direction state ๐ŸŽ Food spawning ๐Ÿ’€ Self-collision โฑ๏ธ Tick timing
โญ
0 XP
Level 1
๐Ÿ”ฅ0
Your Progress 0 / 6 steps
โš ๏ธ
Heads up: SFML 3 renamed a few things. The install step gives you SFML 3 today, but the short snippets in the steps below are written in the classic SFML 2 style. The differences are small and mechanical: sf::VideoMode({800, 500})sf::VideoMode({800, 500}) (curly braces), the new event loop while (const std::optional e = window.pollEvent())while (const std::optional e = window.pollEvent()) with e->is<sf::Event::Closed>()e->is<sf::Event::Closed>(), sf::Keyboard::Key::Leftsf::Keyboard::Key::Left (add ::Key::Key), and setPosition({x, y})setPosition({x, y}) / move({dx, dy})move({dx, dy}) (curly braces). The โœ… complete finished code at the bottom of this page is full SFML 3 and compiles on a fresh install, use it as your reference.
๐Ÿงฉ
Need the game setup? Each episode builds a new game on the same C++/SFML skeleton: the sf::RenderWindowsf::RenderWindow, the game loop and event polling. If you're starting here or need that boilerplate, grab it in one click: Episode 1 setup โ†’ ยท C++ Cheatsheet โ†’
1
๐Ÿงญ
The Grid & Snake Head
A 25ร—25 cell world with one moving segment
Active
๐ŸŽฏ
Goal for this step

Set up the grid world and a head cell that steps on a timer.

  • 1New project snake.cpp: 500ร—500 window, framerate 60. The world is 25ร—25 cells of 20 px.
  • 2A cell is a coordinate pair: struct Cell { int x, y; };, your first struct!
  • 3Movement is TICK-based: an sf::Clock accumulates time; every 0.12 s the snake takes one grid step.
  • 4Direction is a Cell too: {1, 0} means right. Head position += direction each tick.
snake.cpp
#include <SFML/Graphics.hpp>
#include <deque>

struct Cell { int x, y; };

int main() {
    sf::RenderWindow window(sf::VideoMode({500, 500}), "C++ Snake");
    window.setFramerateLimit(60);

    Cell head{12, 12};
    Cell dir{1, 0};
    sf::Clock tick;

    while (window.isOpen()) {
        // events...
        if (tick.getElapsedTime().asSeconds() > 0.12f) {
            tick.restart();
            head.x += dir.x;
            head.y += dir.y;
        }
        // draw head as a 20x20 rectangle at (head.x*20, head.y*20)
    }
}#include <SFML/Graphics.hpp>
#include <deque>

struct Cell { int x, y; };

int main() {
    sf::RenderWindow window(sf::VideoMode({500, 500}), "C++ Snake");
    window.setFramerateLimit(60);

    Cell head{12, 12};
    Cell dir{1, 0};
    sf::Clock tick;

    while (window.isOpen()) {
        // events...
        if (tick.getElapsedTime().asSeconds() > 0.12f) {
            tick.restart();
            head.x += dir.x;
            head.y += dir.y;
        }
        // draw head as a 20x20 rectangle at (head.x*20, head.y*20)
    }
}
โœ๏ธ
Fill in the Blanks
+15 XP
A bundles related data, Cell holds an x and y. The snake steps once per (0.12 s), not once per frame.
๐Ÿง 
Knowledge Check
+15 XP
Why does Snake use tick-based movement instead of smooth per-frame movement?
AC++ is too slow for smooth movement
BSnake is a GRID game, whole-cell steps at fixed intervals ARE the classic feel
Csf::Clock only ticks slowly
2
๐Ÿ
The Body: std::deque
Grow at the front, shrink at the back
Locked
๐ŸŽฏ
Goal for this step

Give the snake a body using the perfect container for the job.

  • 1The body is a std::deque<Cell>, a list that is fast at BOTH ends.
  • 2The movement trick that powers every Snake ever: each tick push_front(new head) and pop_back().
  • 3The middle never moves! Only the ends change, that is why deque (double-ended queue) is the right tool.
  • 4Draw every segment; make the head brighter.
snake.cpp
std::deque<Cell> snake = { {12,12}, {11,12}, {10,12} };

// each tick:
Cell newHead{ snake.front().x + dir.x,
              snake.front().y + dir.y };
snake.push_front(newHead);
snake.pop_back();            // remove the tail

// drawing:
sf::RectangleShape seg(sf::Vector2f(18, 18));
for (auto& c : snake) {
    seg.setPosition({c.x * 20.f + 1, c.y * 20.f + 1});
    seg.setFillColor(&c == &snake.front()
        ? sf::Color(6, 214, 160) : sf::Color(4, 160, 120));
    window.draw(seg);
}std::deque<Cell> snake = { {12,12}, {11,12}, {10,12} };

// each tick:
Cell newHead{ snake.front().x + dir.x,
              snake.front().y + dir.y };
snake.push_front(newHead);
snake.pop_back();            // remove the tail

// drawing:
sf::RectangleShape seg(sf::Vector2f(18, 18));
for (auto& c : snake) {
    seg.setPosition({c.x * 20.f + 1, c.y * 20.f + 1});
    seg.setFillColor(&c == &snake.front()
        ? sf::Color(6, 214, 160) : sf::Color(4, 160, 120));
    window.draw(seg);
}
๐Ÿ’ก
Choosing the right container IS the skill here: vector (end-only), deque (both ends), map (lookups). Snake naturally teaches deque.
โŒจ๏ธ
Code Challenge
+20 XP
Move the snake one step using the two-ended trick:
snake.cpp
snake.snake.(newHead);
snake.(newHead);
snake.();();
๐Ÿ’ก Hint: The new head is added at the front; the tail is removed from the back.
๐Ÿง 
Knowledge Check
+15 XP
What happens if you push_front WITHOUT pop_back?
AThe snake teleports
BThe snake grows by one segment, which is exactly how eating will work!
CA compile error
3
๐ŸŽฎ
Steering & the 180ยฐ Rule
Arrow keys change direction, but no instant reversal
Locked
๐ŸŽฏ
Goal for this step

Steer the snake, and block the fatal reverse-into-yourself move.

  • 1On arrow KeyPressed events, set dir, but ONLY if the new direction is not the exact opposite of the current one.
  • 2Opposite check: newDir.x != -dir.x || newDir.y != -dir.y.
  • 3Subtle classic bug: pressing two keys within one tick can still reverse you. Fix: store nextDir and apply it once per tick.
  • 4That two-step input buffer is real production-grade thinking.
snake.cpp
Cell nextDir = dir;

// in the event loop:
if (const auto* kp = event->getIf<sf::Event::KeyPressed>()) {
    Cell want = dir;
    if (kp->code == sf::Keyboard::Key::Up)    want = { 0,-1};
    if (kp->code == sf::Keyboard::Key::Down)  want = { 0, 1};
    if (kp->code == sf::Keyboard::Key::Left)  want = {-1, 0};
    if (kp->code == sf::Keyboard::Key::Right) want = { 1, 0};
    if (want.x != -dir.x || want.y != -dir.y)
        nextDir = want;
}

// at each tick, before moving:
dir = nextDir;Cell nextDir = dir;

// in the event loop:
if (const auto* kp = event->getIf<sf::Event::KeyPressed>()) {
    Cell want = dir;
    if (kp->code == sf::Keyboard::Key::Up)    want = { 0,-1};
    if (kp->code == sf::Keyboard::Key::Down)  want = { 0, 1};
    if (kp->code == sf::Keyboard::Key::Left)  want = {-1, 0};
    if (kp->code == sf::Keyboard::Key::Right) want = { 1, 0};
    if (want.x != -dir.x || want.y != -dir.y)
        nextDir = want;
}

// at each tick, before moving:
dir = nextDir;
โœ๏ธ
Fill in the Blanks
+15 XP
Reversing 180ยฐ would mean instant (head into neck). We buffer input in and apply it once per tick to kill the double-press bug.
๐Ÿง 
Knowledge Check
+15 XP
The double-key-press bug happens becauseโ€ฆ
AKeyboards are slow
BTwo direction changes can land between ticks: upโ†’leftโ†’down reads as legal twice but reverses overall
CSFML drops events
4
๐ŸŽ
Food & Growing
Random spawning that avoids the snake body
Locked
๐ŸŽฏ
Goal for this step

Spawn food in valid cells and grow when eaten.

  • 1Food is one Cell, drawn red. Eating: new head equals food position โ†’ skip pop_back (grow!) and respawn food.
  • 2Respawn must NOT land on the snake: loop random positions until one is free.
  • 3Seed randomness once at the top of main: srand(time(0));.
  • 4Score = snake length โˆ’ 3. Show it in the title bar with window.setTitle, no font file needed!
snake.cpp
#include <cstdlib>
#include <ctime>

Cell food{18, 12};

Cell spawnFood(const std::deque<Cell>& snake) {
    while (true) {
        Cell f{ rand() % 25, rand() % 25 };
        bool onSnake = false;
        for (auto& c : snake)
            if (c.x == f.x && c.y == f.y) onSnake = true;
        if (!onSnake) return f;
    }
}

// in the tick:
snake.push_front(newHead);
if (newHead.x == food.x && newHead.y == food.y)
    food = spawnFood(snake);     // grew! no pop_back
else
    snake.pop_back();#include <cstdlib>
#include <ctime>

Cell food{18, 12};

Cell spawnFood(const std::deque<Cell>& snake) {
    while (true) {
        Cell f{ rand() % 25, rand() % 25 };
        bool onSnake = false;
        for (auto& c : snake)
            if (c.x == f.x && c.y == f.y) onSnake = true;
        if (!onSnake) return f;
    }
}

// in the tick:
snake.push_front(newHead);
if (newHead.x == food.x && newHead.y == food.y)
    food = spawnFood(snake);     // grew! no pop_back
else
    snake.pop_back();
โŒจ๏ธ
Code Challenge
+20 XP
Grow only when food is eaten:
snake.cpp
snake.push_front(newHead);
if (newHead.x == food.x && newHead.y == snake.push_front(newHead);
if (newHead.x == food.x && newHead.y == )
    food = )
    food = (snake);
else
    snake.(snake);
else
    snake.();();
๐Ÿ’ก Hint: Compare both coordinates; on a hit respawn the food and skip the tail removal; otherwise remove the tail as usual.
๐Ÿง 
Knowledge Check
+15 XP
Why must spawnFood check the snake body?
AFood on the snake looks bad
BFood inside the body can be unreachable or auto-eaten, on a long snake the game breaks
Crand() requires it
5
๐Ÿ’€
Death: Walls & Self-Bite
Two ways to die, one game-over state
Locked
๐ŸŽฏ
Goal for this step

End the game on wall hits and self-collision.

  • 1Wall death: new head outside 0-24 in either axis.
  • 2Self-bite: new head equals ANY body cell, loop and compare (check BEFORE pushing the new head).
  • 3On death: set a gameOver flag, stop ticking, tint the snake grey, show the score, R restarts.
  • 4The self-bite check is why Snake gets hard: the hazard is your own success.
snake.cpp
bool gameOver = false;

// in the tick, before push_front:
if (newHead.x < 0 || newHead.x > 24 ||
    newHead.y < 0 || newHead.y > 24)
    gameOver = true;

for (auto& c : snake)
    if (c.x == newHead.x && c.y == newHead.y)
        gameOver = true;

if (!gameOver) {
    snake.push_front(newHead);
    // ... food / pop_back ...
}bool gameOver = false;

// in the tick, before push_front:
if (newHead.x < 0 || newHead.x > 24 ||
    newHead.y < 0 || newHead.y > 24)
    gameOver = true;

for (auto& c : snake)
    if (c.x == newHead.x && c.y == newHead.y)
        gameOver = true;

if (!gameOver) {
    snake.push_front(newHead);
    // ... food / pop_back ...
}
โœ๏ธ
Fill in the Blanks
+15 XP
The grid runs from 0 to in both axes. Self-collision compares the new head against every cell before it is pushed.
๐Ÿง 
Knowledge Check
+15 XP
What makes Snake harder the better you play?
AThe walls close in
BYour own body is the obstacle, every food eaten makes the hazard longer
CThe food gets smaller
6
โฑ๏ธ
Speed-Up & Polish
Faster ticks as you grow, plus juicy touches
Locked
๐ŸŽฏ
Goal for this step

Scale difficulty with length and add the finishing feel.

  • 1Speed from length: tickTime = std::max(0.05f, 0.12f - snake.size() * 0.002f), every meal shaves 2 ms, floored at 50 ms.
  • 2Flash the food (alternate brightness with a frame counter), and darken each body segment slightly toward the tail (index-based colour).
  • 3High score: keep a static best across restarts and show both in the title.
  • 4Ship it. You have used structs, deques, const references and range-for, real modern C++.
snake.cpp
float tickTime = std::max(0.05f,
    0.12f - (float)snake.size() * 0.002f);

if (tick.getElapsedTime().asSeconds() > tickTime) {
    // ... the whole tick ...
}

// tail fade while drawing (i = index):
int shade = 160 - std::min(100, (int)i * 4);
seg.setFillColor(sf::Color(6, shade + 54, 120 + shade/3));float tickTime = std::max(0.05f,
    0.12f - (float)snake.size() * 0.002f);

if (tick.getElapsedTime().asSeconds() > tickTime) {
    // ... the whole tick ...
}

// tail fade while drawing (i = index):
int shade = 160 - std::min(100, (int)i * 4);
seg.setFillColor(sf::Color(6, shade + 54, 120 + shade/3));
โŒจ๏ธ
Code Challenge
+20 XP
Make the game speed up with length, safely floored:
snake.cpp
float tickTime = std::float tickTime = std::(0.05f,
    0.12f - (float)snake.(0.05f,
    0.12f - (float)snake.() * 0.002f);() * 0.002f);
๐Ÿ’ก Hint: A floor needs the bigger-of-two function; the snakeโ€™s length comes from its container.
๐Ÿง 
Knowledge Check
+15 XP
The tick floor of 0.05 s exists becauseโ€ฆ
ASFML cannot go faster
BPast ~20 ticks/second humans cannot react, the floor keeps maximum difficulty humanly playable
CThe deque overflows
โœ… See the complete finished game , full SFML 3, compiles today

The whole game in one file, written in current SFML 3 (what the install step gives you today) and compiled with g++ + SFML 3.0 to confirm it builds. The step snippets above use classic SFML 2 names; this is your working reference. Build it with g++ snake.cpp -o snake -lsfml-graphics -lsfml-window -lsfml-systemg++ snake.cpp -o snake -lsfml-graphics -lsfml-window -lsfml-system then run ./snake./snake (on Windows, from the MSYS2 MinGW terminal so it finds the SFML DLLs).

snake.cpp
// Complete C++ Snake for SFML 3. Build:
//   g++ snake.cpp -o snake -lsfml-graphics -lsfml-window -lsfml-system
#include <SFML/Graphics.hpp>
#include <optional>
#include <deque>
#include <cstdlib>

int main() {
    const int GRID = 25, CELL = 20;
    sf::RenderWindow window(sf::VideoMode({500, 500}), "C++ Snake");
    window.setFramerateLimit(60);

    std::deque<sf::Vector2i> snake = {{12,12},{11,12},{10,12}};
    sf::Vector2i dir = {1, 0};
    sf::Vector2i food = {5, 5};
    bool dead = false;
    sf::Clock clock;
    float acc = 0.f;

    auto spawnFood = [&]() {
        while (true) {
            sf::Vector2i f = {std::rand() % GRID, std::rand() % GRID};
            bool onSnake = false;
            for (auto& c : snake) if (c == f) onSnake = true;
            if (!onSnake) { food = f; return; }
        }
    };
    spawnFood();

    auto reset = [&]() {
        snake = {{12,12},{11,12},{10,12}}; dir = {1,0}; dead = false; spawnFood();
    };

    while (window.isOpen()) {
        while (const std::optional event = window.pollEvent()) {
            if (event->is<sf::Event::Closed>()) window.close();
            if (const auto* k = event->getIf<sf::Event::KeyPressed>()) {
                using K = sf::Keyboard::Key;
                if (k->code == K::Up    && dir.y != 1)  dir = {0,-1};
                if (k->code == K::Down  && dir.y != -1) dir = {0, 1};
                if (k->code == K::Left  && dir.x != 1)  dir = {-1,0};
                if (k->code == K::Right && dir.x != -1) dir = {1, 0};
                if (k->code == K::Space && dead) reset();
            }
        }

        acc += clock.restart().asSeconds();
        if (!dead && acc >= 0.12f) {
            acc = 0.f;
            sf::Vector2i head = snake[0] + dir;
            if (head.x < 0 || head.x >= GRID || head.y < 0 || head.y >= GRID) dead = true;
            for (auto& c : snake) if (c == head) dead = true;
            if (!dead) {
                snake.push_front(head);
                if (head == food) spawnFood();
                else snake.pop_back();
            }
        }

        window.clear(sf::Color(10, 14, 20));
        sf::CircleShape f(8.f);
        f.setPosition({food.x * (float)CELL + 2, food.y * (float)CELL + 2});
        f.setFillColor(sf::Color(255, 84, 112));
        window.draw(f);
        for (auto& c : snake) {
            sf::RectangleShape cell({CELL - 2.f, CELL - 2.f});
            cell.setPosition({c.x * (float)CELL + 1, c.y * (float)CELL + 1});
            cell.setFillColor(sf::Color(51, 209, 122));
            window.draw(cell);
        }
        window.display();
    }
}// Complete C++ Snake for SFML 3. Build:
//   g++ snake.cpp -o snake -lsfml-graphics -lsfml-window -lsfml-system
#include <SFML/Graphics.hpp>
#include <optional>
#include <deque>
#include <cstdlib>

int main() {
    const int GRID = 25, CELL = 20;
    sf::RenderWindow window(sf::VideoMode({500, 500}), "C++ Snake");
    window.setFramerateLimit(60);

    std::deque<sf::Vector2i> snake = {{12,12},{11,12},{10,12}};
    sf::Vector2i dir = {1, 0};
    sf::Vector2i food = {5, 5};
    bool dead = false;
    sf::Clock clock;
    float acc = 0.f;

    auto spawnFood = [&]() {
        while (true) {
            sf::Vector2i f = {std::rand() % GRID, std::rand() % GRID};
            bool onSnake = false;
            for (auto& c : snake) if (c == f) onSnake = true;
            if (!onSnake) { food = f; return; }
        }
    };
    spawnFood();

    auto reset = [&]() {
        snake = {{12,12},{11,12},{10,12}}; dir = {1,0}; dead = false; spawnFood();
    };

    while (window.isOpen()) {
        while (const std::optional event = window.pollEvent()) {
            if (event->is<sf::Event::Closed>()) window.close();
            if (const auto* k = event->getIf<sf::Event::KeyPressed>()) {
                using K = sf::Keyboard::Key;
                if (k->code == K::Up    && dir.y != 1)  dir = {0,-1};
                if (k->code == K::Down  && dir.y != -1) dir = {0, 1};
                if (k->code == K::Left  && dir.x != 1)  dir = {-1,0};
                if (k->code == K::Right && dir.x != -1) dir = {1, 0};
                if (k->code == K::Space && dead) reset();
            }
        }

        acc += clock.restart().asSeconds();
        if (!dead && acc >= 0.12f) {
            acc = 0.f;
            sf::Vector2i head = snake[0] + dir;
            if (head.x < 0 || head.x >= GRID || head.y < 0 || head.y >= GRID) dead = true;
            for (auto& c : snake) if (c == head) dead = true;
            if (!dead) {
                snake.push_front(head);
                if (head == food) spawnFood();
                else snake.pop_back();
            }
        }

        window.clear(sf::Color(10, 14, 20));
        sf::CircleShape f(8.f);
        f.setPosition({food.x * (float)CELL + 2, food.y * (float)CELL + 2});
        f.setFillColor(sf::Color(255, 84, 112));
        window.draw(f);
        for (auto& c : snake) {
            sf::RectangleShape cell({CELL - 2.f, CELL - 2.f});
            cell.setPosition({c.x * (float)CELL + 1, c.y * (float)CELL + 1});
            cell.setFillColor(sf::Color(51, 209, 122));
            window.draw(cell);
        }
        window.display();
    }
}
๐ŸŽ‰๐Ÿ†๐ŸŽฎโœจ๐ŸŽ‰
Workshop Complete!
Deques, grids and tick-based movement, Snake is a data-structures lesson in disguise and you aced it. Episode 3: Breakout!
0
Total XP
1
Level
0
Best Streak
0%
Accuracy
โ–ถ Episode 3: Breakout โ†’
โญ View My Progress & Certificates

This workshop was free and took many hours to build. If it helped you learn something new, consider supporting the project.

☕ Support on Ko-fi