This chapter is a live transcript of an AI-assisted coding session. We're going to build a complete Snake game using SDL 3, working with an AI in a series of prompt rounds. The goal is not just the game — it's watching the describe-generate-review-run-refine loop in action, seeing where it works cleanly and where human judgment is essential.
The Rules of Vibe-Coding
"Vibe-coding" is the informal name for using AI to generate most of the code for a project, steering with prompts rather than writing every line yourself. It's faster than writing everything from scratch, but it only works well if you follow some rules:
- Front-load constraints. Tell the AI what you're using before it starts — SDL 3, C++17, RAII, no deprecated functions. An unconstrained AI will make choices you'll regret later.
- One feature at a time. Adding movement, adding food, adding score, adding speed increase — these are four separate prompts, not one. Smaller rounds produce reviewable output.
- Read every line. Don't paste code you don't understand. If something looks suspicious or unfamiliar, ask about that line specifically before including it.
- Run after every round. Verify that what you have works before adding more. Compiling and running a broken state is valuable — you know exactly which round introduced the problem.
Project Setup
Create a new Visual Studio SDL 3 project called Vibe Snake. Delete the generated stub code. Our first prompt handles the skeleton.
Round 1 — The Skeleton
Prompt:
I am writing C++17 with SDL 3 (not SDL 2). Do not use any deprecated SDL 3 functions. Use RAII for all SDL resources. No external libraries beyond SDL3. Create a single-file Snake game skeleton in main.cpp. The window is 640×480. The game grid is 20×15 cells, each cell 32×32 pixels. Define a Point struct (int x, int y). The game runs in a fixed-timestep loop using SDL_GetTicks() — advance the snake every 150 ms. For now, draw a 5-segment snake (horizontal, facing right) as green squares. No food, no input, no collision yet.
What the AI produces:
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <deque>
#include <cstdlib>
static constexpr int CELL = 32;
static constexpr int COLS = 20;
static constexpr int ROWS = 15;
static constexpr int WIN_W = COLS * CELL;
static constexpr int WIN_H = ROWS * CELL;
static constexpr Uint64 STEP_MS = 150;
struct Point { int x, y; };
int main(int, char**)
{
SDL_Init(SDL_INIT_VIDEO);
SDL_Window* window = SDL_CreateWindow("Vibe Snake", WIN_W, WIN_H, 0);
SDL_Renderer* renderer = SDL_CreateRenderer(window, nullptr);
std::deque<Point> snake = {{4,7},{3,7},{2,7},{1,7},{0,7}};
Uint64 lastStep = SDL_GetTicks();
bool running = true;
while (running)
{
SDL_Event ev;
while (SDL_PollEvent(&ev))
if (ev.type == SDL_EVENT_QUIT) running = false;
Uint64 now = SDL_GetTicks();
if (now - lastStep >= STEP_MS)
{
lastStep = now;
// TODO: move snake
}
SDL_SetRenderDrawColor(renderer, 20, 20, 20, 255);
SDL_RenderClear(renderer);
SDL_SetRenderDrawColor(renderer, 60, 200, 60, 255);
for (auto& seg : snake)
{
SDL_FRect r{ (float)(seg.x * CELL), (float)(seg.y * CELL),
(float)CELL, (float)CELL };
SDL_RenderFillRect(renderer, &r);
}
SDL_RenderPresent(renderer);
}
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}
Review: This is clean. std::deque<Point> is the right data structure for a snake — push to the front, pop from the back. It's using SDL 3's SDL_FRect for rendering. The fixed-timestep structure is correct. The only thing worth noting is the raw pointer to SDL_Window and SDL_Renderer — ideally these would be in a smart-pointer RAII wrapper, but for a single-file demo this is acceptable. Run it: you should see a dark window with five green squares.
Round 2 — Movement and Input
Prompt:
Good. Now add movement and input. The snake should move one cell per step in its current direction. The player steers with WASD or arrow keys. Changing to the opposite direction (e.g. pressing left while moving right) should be ignored. Use a pending-direction buffer so a direction change queued just before a step fires isn't dropped.
What the AI produces (key additions):
enum class Dir { Up, Down, Left, Right };
// In game state:
Dir dir = Dir::Right;
Dir pendingDir = Dir::Right;
// In the event loop:
case SDL_EVENT_KEY_DOWN:
{
Dir req = pendingDir;
switch (ev.key.scancode)
{
case SDL_SCANCODE_W: case SDL_SCANCODE_UP: req = Dir::Up; break;
case SDL_SCANCODE_S: case SDL_SCANCODE_DOWN: req = Dir::Down; break;
case SDL_SCANCODE_A: case SDL_SCANCODE_LEFT: req = Dir::Left; break;
case SDL_SCANCODE_D: case SDL_SCANCODE_RIGHT: req = Dir::Right; break;
default: break;
}
pendingDir = req;
break;
}
// In the step update:
auto opposite = [](Dir a, Dir b) {
return (a == Dir::Up && b == Dir::Down) ||
(a == Dir::Down && b == Dir::Up) ||
(a == Dir::Left && b == Dir::Right) ||
(a == Dir::Right && b == Dir::Left);
};
if (!opposite(dir, pendingDir)) dir = pendingDir;
Point head = snake.front();
switch (dir)
{
case Dir::Up: --head.y; break;
case Dir::Down: ++head.y; break;
case Dir::Left: --head.x; break;
case Dir::Right: ++head.x; break;
}
snake.push_front(head);
snake.pop_back();
Review: The pendingDir pattern is the right approach here. Without it, a very fast player who presses two keys between steps can input a 180° reversal in two moves. By buffering into pendingDir and only committing at step time, that bug is closed. The lambda for opposite is clean. Run it: the snake should move and steer.
Round 3 — Food and Growth
Prompt:
Add food. A single red square appears at a random grid cell that the snake does not occupy. When the snake's head reaches the food cell, the snake grows by one segment (don't pop the tail that step) and new food is placed at another random empty cell. Use rand() for placement — no need for a better RNG here.
What the AI produces (key addition — food spawn):
Point spawnFood(const std::deque<Point>& snake)
{
Point food;
do {
food.x = rand() % COLS;
food.y = rand() % ROWS;
} while (std::any_of(snake.begin(), snake.end(),
[&](const Point& p){ return p.x == food.x && p.y == food.y; }));
return food;
}
// In step update (after moving head):
if (head.x == food.x && head.y == food.y)
{
// Grow: push new head but keep tail
food = spawnFood(snake);
score += 10;
}
else
{
snake.pop_back(); // Normal move: remove tail
}
Review: This is correct. The key insight is that "growing" just means skipping the pop_back call. The do-while loop for food placement guarantees no overlap with any segment — good. Run it: food appears, eating it grows the snake.
Round 4 — Collision Detection
Prompt:
Add collision detection. The game ends if the snake's head goes outside the grid, or if it collides with any segment of its own body. When the game ends, stop movement and display "GAME OVER" centred on screen using SDL_RenderDebugText. Reset the game if the player presses R.
What the AI produces (key parts):
bool gameOver = false;
// After computing new head, before push_front:
if (head.x < 0 || head.x >= COLS ||
head.y < 0 || head.y >= ROWS)
{
gameOver = true;
}
else
{
// Self-collision: check all segments *except* the tail
// (the tail is about to be removed, so head can move into its spot)
auto bodyEnd = snake.end();
if (!growing) --bodyEnd; // exclude tail if not growing
if (std::any_of(snake.begin(), bodyEnd,
[&](const Point& p){ return p.x == head.x && p.y == head.y; }))
{
gameOver = true;
}
}
Review — spot the bug: There's an off-by-one lurking here. The comment says "exclude the tail if not growing" — but the tail is at snake.back(), and the check is --bodyEnd which excludes the last element. This is actually correct for non-growth steps, because the tail is about to move away, so the head is allowed to step into that cell. But the AI's comment is slightly misleading — it says "head can move into its spot" when it really means "the check correctly models future state." If you simply omit the tail exclusion (checking the whole snake), you'll get false game-overs at longer lengths when the head tries to step into where the tail currently is. This is the subtlety to understand in self-collision: check what the body will look like after the step, not what it looks like before.
Run it. The game now ends on wall and self-collision, and resets with R.
Round 5 — Score and Speed Increase
Prompt:
Add a visible score counter in the top-left using SDL_RenderDebugText. Every 3 foods eaten, decrease the step interval by 10 ms (minimum 60 ms) to speed the snake up. Show the current score.
What the AI produces:
int score = 0;
int foodsEaten = 0;
Uint64 stepMs = 150;
// In food-eaten branch:
score += 10;
foodsEaten += 1;
if (foodsEaten % 3 == 0 && stepMs > 60)
stepMs -= 10;
// In render:
char buf[32];
SDL_snprintf(buf, sizeof(buf), "Score: %d", score);
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
SDL_RenderDebugText(renderer, 8.0f, 8.0f, buf);
Review: Clean. SDL_snprintf is the safe SDL-provided string formatting function. SDL_RenderDebugText is SDL 3's quick-and-dirty text renderer — it uses a built-in 8×8 pixel font, no TTF required. Perfect for scores and debug output.
Round 6 — Polish and Wrap-Up
Prompt:
Polish pass: draw a darker green head segment (so the direction is visible), draw grid lines as very dark grey, and add a brief invincibility window of 3 frames after respawn so the player doesn't instantly die on reset.
The AI adds the head colour (drawing snake.front() separately after the rest), draws a grid with a loop of SDL_RenderLine calls, and adds a spawnGrace counter that counts down from 3 and suppresses collision checks while positive. All straightforward.
The Finished Game
The complete game was built in six prompt rounds and roughly 200 lines of C++. The session took about 25 minutes. Key moments where human judgment was essential:
- Specifying
pendingDirin Round 2 — a naive AI prompt for "input" would produce direct direction mutation, which breaks on fast input. - Catching the self-collision off-by-one in Round 4 — the code was correct, but the comment was misleading. Understanding what it actually does required reasoning about the data structure, not just reading the comment.
- Choosing what to ask for. All six rounds had tightly-scoped single-feature prompts. A single "build me Snake" prompt would have produced something longer, less structured, and harder to review.
What You've Learned
The game is less important than the workflow. You've seen:
- How to structure a vibe-coding session as a series of small, reviewable rounds
- How front-loading constraints shapes the AI's output from the start
- How to spot and evaluate subtle bugs in AI-generated logic (the self-collision timing)
- That the AI does the typing but you provide the design decisions
Next chapter we apply the same workflow to a more complex project: Asteroids.