Chapter 5  ·  Project

Square Invader

We've now got the two ingredients that any real game depends on. Chapter 2 gave us variables to remember what's going on, and Chapter 4 gave us flow control to react to it. Put them together and you can build something that actually plays like a game — a thing on screen that takes input, makes decisions, and changes state in response.

This chapter is that "put them together" moment. We're going to build a deliberately stripped-down homage to Space Invaders: a green player square at the bottom of the screen that you slide left and right, a yellow bullet you fire upward, and a red invader that marches across the top of the screen and drops down a row whenever it hits the edge. Shoot the invader, score a point, and a fresh one appears. Let the invader reach the bottom of the screen, and you lose a life. Run out of lives, and it's game over.

Every interesting line in the program is an if, a comparison, or a combination of the two. That's the entire point.

Project folder: SDL3 Projects/Square Invader — the complete source for this chapter lives here.

In this chapter, we will:

Let's get coding.

Setting Up the Project

You know the drill by now. The setup is the same as Chapter 1, condensed into a quick checklist:

  1. File > New > Project, Empty Project (C++), named SquareInvader.
  2. Add a main.cpp under Source Files.
  3. In Project Properties (with Configuration: All Configurations, Platform: All Platforms):
    • C/C++ > General > Additional Include Directories → your SDL include folder.
    • Linker > General > Additional Library Directories → your SDL lib\x64 folder.
    • Linker > Input > Additional Dependencies → add SDL3.lib.
    • Linker > System > SubSystemWindows.
  4. Copy SDL3.dll from SDL3\lib\x64 into the project's x64\Debug folder.

If this took longer than a minute, flip back to Chapter 1 and look at the bits that confused you. By the end of Act 1 you'll be doing this on autopilot.

Coding the Game

We'll build it up in the same step-by-step rhythm as the last two project chapters. Open main.cpp.

Includes and Constants

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>

const int   SCREEN_W      = 800;
const int   SCREEN_H      = 600;
const int   PLAYER_SIZE   = 40;
const int   BULLET_SIZE   = 10;
const int   INVADER_SIZE  = 40;
const float PLAYER_SPEED  = 5.0f;   // pixels per frame
const float BULLET_SPEED  = 8.0f;   // pixels per frame
const float INVADER_SPEED = 2.0f;   // pixels per frame
const int   STARTING_LIVES = 3;

In the preceding code, we set up our usual SDL includes and a small block of constants describing the window, the three objects, and the game's starting conditions. Marking these const means we cannot accidentally change them later — exactly the safety net we talked about in Chapter 2.

A small note on speed. In the Chapter 3 bouncing ball we used delta time to make movement frame-rate independent. Here we're going to keep things deliberately simple and measure speed in pixels per frame, then ask SDL to wait roughly the same amount of time between frames. It's less robust than delta time, but it keeps every line of code in this chapter focused on the new thing — flow control — rather than the math of timing.

main, SDL Setup, and the Game's Variables

int main(int argc, char* argv[])
{
    if (!SDL_Init(SDL_INIT_VIDEO))
    {
        SDL_Log("SDL_Init failed: %s", SDL_GetError());
        return 1;
    }

    SDL_Window* window = SDL_CreateWindow("Square Invader", SCREEN_W, SCREEN_H, 0);
    if (!window)
    {
        SDL_Log("SDL_CreateWindow failed: %s", SDL_GetError());
        SDL_Quit();
        return 1;
    }

    SDL_Renderer* renderer = SDL_CreateRenderer(window, nullptr);
    if (!renderer)
    {
        SDL_Log("SDL_CreateRenderer failed: %s", SDL_GetError());
        SDL_DestroyWindow(window);
        SDL_Quit();
        return 1;
    }

Same boilerplate as the last two projects. Start SDL, create a window, create a renderer, check for failure at each step. From here on, the new stuff begins.

    // Player (green, near the bottom)
    float playerX = (SCREEN_W - PLAYER_SIZE) / 2.0f;
    float playerY = SCREEN_H - PLAYER_SIZE - 10.0f;

    // Bullet (yellow, fired upward; only one allowed at a time)
    float bulletX      = 0.0f;
    float bulletY      = 0.0f;
    bool  bulletActive = false;

    // Invader (red, starts top-left and marches sideways)
    float invaderX      = 0.0f;
    float invaderY      = 10.0f;
    float invaderSpeedX = INVADER_SPEED;   // positive = moving right

    // Game state
    int  score    = 0;
    int  lives    = STARTING_LIVES;
    bool gameOver = false;

In the preceding code, we declare every variable our game needs. The player's position is calculated to sit centered horizontally and just above the bottom of the screen. The bullet's position is arbitrary for now — it doesn't matter where it starts, because we won't draw it until it's "active", which is exactly what the bulletActive bool is for. A bullet is either flying or it isn't. The invader has its own position and a horizontal speed variable so that we can flip its direction when it hits an edge, just like the bouncing ball did.

Then three game-state variables. score counts hits. lives starts at three and ticks down whenever the invader gets past us. gameOver is the flag that switches off gameplay when the game ends.

Eleven variables. That's the entire state of the game.

The Game Loop

    bool running = true;

    while (running)
    {

running controls the main loop, just as in every previous project. Inside the loop, the first job is events.

Handling Window and Key-Press Events with a switch

In Chapter 1 we handled window events with a couple of if statements. Now that we have switch in our toolkit, we can express the same idea more cleanly:

        SDL_Event event;
        while (SDL_PollEvent(&event))
        {
            switch (event.type)
            {
                case SDL_EVENT_QUIT:
                    running = false;
                    break;

                case SDL_EVENT_KEY_DOWN:
                    if (event.key.scancode == SDL_SCANCODE_ESCAPE)
                    {
                        running = false;
                    }
                    else if (event.key.scancode == SDL_SCANCODE_SPACE
                             && !bulletActive
                             && !gameOver)
                    {
                        // Spawn a bullet centered on top of the player
                        bulletX      = playerX + (PLAYER_SIZE - BULLET_SIZE) / 2.0f;
                        bulletY      = playerY;
                        bulletActive = true;
                    }
                    break;
            }
        }

In the preceding code, the outer while drains SDL's event queue. For each event, the switch jumps to whichever case matches the event's type. SDL_EVENT_QUIT (the window's X button) sets running to false. SDL_EVENT_KEY_DOWN (a key was just pressed this frame) leads into a small chain of if/else if decisions.

Press Escape and the game ends. Press Space and — if and only if no bullet is currently flying and the game isn't over — we spawn a new bullet centered on top of the player.

Look at that condition carefully:

event.key.scancode == SDL_SCANCODE_SPACE && !bulletActive && !gameOver

This is three separate questions joined together with && (AND). All three must be true for the body to run. We met this in Chapter 4 — and we also met the fact that && is short-circuit: if the first check is false, C++ doesn't even bother evaluating the second one. That's exactly the behavior we want here, both for efficiency and for safety.

The !bulletActive reads as "not bullet active" — true when there isn't a bullet on screen. The !gameOver reads the same way. Three small conditions, one combined question: "was Space pressed, and is there no bullet flying, and is the game still on?"

The break; at the end of each case is essential. Without it, execution would "fall through" into the next case — a subtle bug Chapter 4 warned about. Train your eye to spot a missing break; the moment you see a switch.

Moving the Player with Held Keys

Key-down events fire once when a key is pressed, which is right for "fire a bullet" but wrong for "slide left while held". For continuous movement we ask SDL for the current state of the keyboard each frame:

        if (!gameOver)
        {
            const bool* keys = SDL_GetKeyboardState(nullptr);

            if (keys[SDL_SCANCODE_A]) playerX -= PLAYER_SPEED;
            if (keys[SDL_SCANCODE_D]) playerX += PLAYER_SPEED;

            // Keep the player on screen
            if (playerX < 0.0f)                       playerX = 0.0f;
            if (playerX > SCREEN_W - PLAYER_SIZE)     playerX = SCREEN_W - PLAYER_SIZE;
        }

In the preceding code, the whole block is wrapped in if (!gameOver) so that none of this runs once the game has ended. We met this exact pattern in Chapter 4 — ! flips true and false, so !gameOver is true while the game is still on.

Inside, SDL_GetKeyboardState hands us a table of "is this key currently down?" values. If A is held, we subtract from playerX; if D is held, we add to it. Two more tiny if statements clamp the player to the window so they cannot wander offscreen. Notice how comparison operators (<, >) read like plain English the moment they're applied to a named variable.

Moving the Bullet and Checking for a Hit

Now the bullet, and the part of the program where flow control really earns its keep:

        if (bulletActive && !gameOver)
        {
            bulletY -= BULLET_SPEED;   // negative Y = moving UP

            // Did it fly off the top of the screen?
            if (bulletY + BULLET_SIZE < 0.0f)
            {
                bulletActive = false;
            }

            // Collision check — axis-aligned bounding box overlap
            bool overlapX = bulletX < invaderX + INVADER_SIZE &&
                            bulletX + BULLET_SIZE > invaderX;

            bool overlapY = bulletY < invaderY + INVADER_SIZE &&
                            bulletY + BULLET_SIZE > invaderY;

            if (overlapX && overlapY)
            {
                // Hit! Score a point, reset bullet and invader.
                score        += 1;
                bulletActive = false;

                invaderX      = 0.0f;
                invaderY      = 10.0f;
                invaderSpeedX = INVADER_SPEED;
            }
        }

In the preceding code, we only process the bullet at all if it's active and the game isn't over — another small && combined condition. If both are true, we move it upward by BULLET_SPEED each frame.

Then we ask whether it has flown off the top of the screen. If it has, we mark it inactive — gone, available to be fired again.

The next two lines are the most interesting in the chapter. Two rectangles overlap on the screen only when they overlap on the X axis and on the Y axis simultaneously. We work out each one as a bool and then combine them. overlapX is true when the left edge of the bullet is to the left of the right edge of the invader, and the right edge of the bullet is to the right of the left edge of the invader. That second clause might feel backwards — read it carefully and it'll click. The same idea handles Y, just rotated ninety degrees.

If overlapX && overlapY is true, the rectangles overlap, and the bullet has hit the invader. We bump the score up by one with += 1, mark the bullet inactive, and reset the invader back to its starting position with its original speed.

That's collision detection. Four bool values and an &&. The same pattern handles every rectangle-vs-rectangle hit you'll ever code, from bullets to boxes to player-vs-pickup. Variables and flow control, doing real game work.

The Invader's Movement and the Player's Lives

Now we move the invader and check the trickier consequences:

        if (!gameOver)
        {
            invaderX += invaderSpeedX;

            // Reached the right edge — drop down and head left
            if (invaderX + INVADER_SIZE >= SCREEN_W)
            {
                invaderX      = SCREEN_W - INVADER_SIZE;
                invaderY     += INVADER_SIZE;
                invaderSpeedX = -INVADER_SPEED;
            }
            // Reached the left edge — drop down and head right
            else if (invaderX <= 0.0f)
            {
                invaderX      = 0.0f;
                invaderY     += INVADER_SIZE;
                invaderSpeedX = INVADER_SPEED;
            }

            // Did the invader reach the player's row?
            if (invaderY + INVADER_SIZE >= playerY)
            {
                lives -= 1;

                // Reset the invader to the top
                invaderX      = 0.0f;
                invaderY      = 10.0f;
                invaderSpeedX = INVADER_SPEED;

                // No lives left? End the game.
                gameOver = (lives <= 0);
            }
        }

In the preceding code, we add the invader's horizontal speed to its X position to march it across the screen. If it has reached the right edge, we pin it there, drop it down a row, and flip its speed negative so the next frame it heads left. The mirrored block handles the left edge. We use else if here rather than two separate if blocks because the invader can't hit both walls in the same frame — the second check would be a waste of time.

The interesting bit is the lives. When the invader's bottom edge reaches the player's row, we lose a life with lives -= 1, reset the invader to the top, and decide whether the game is over.

Look at that last line:

gameOver = (lives <= 0);

(lives <= 0) is a comparison — it produces a bool. We assign that bool directly to gameOver. If lives have dropped to zero or below, gameOver becomes true; otherwise it stays false. No if. No else. Just an expression that evaluates to true-or-false and a variable to put the answer in. This is the kind of code that flow control lets you write — compact, intentional, and easy to read once your eye is trained for it.

Drawing the Game

Time to put pixels on screen:

        // Background — dark grey normally, dark red when game over
        if (gameOver)
            SDL_SetRenderDrawColor(renderer, 60, 0, 0, 255);
        else
            SDL_SetRenderDrawColor(renderer, 20, 20, 30, 255);
        SDL_RenderClear(renderer);

In the preceding code, we change the background color depending on whether the game is over. A small if/else over the call to SDL_SetRenderDrawColor. The reader doesn't need a "GAME OVER" text label to know it's happened — the whole screen turning red conveys it instantly. Bonus: we can do all this without ever loading a font, which is a topic for a much later chapter.

        // Player — green when alive, dimmer when game over
        int playerGreen = gameOver ? 80 : 200;
        SDL_SetRenderDrawColor(renderer, 0, playerGreen, 0, 255);
        SDL_FRect playerRect = { playerX, playerY, (float)PLAYER_SIZE, (float)PLAYER_SIZE };
        SDL_RenderFillRect(renderer, &playerRect);

That ? and : together are the ternary operator — the shorthand if/else from Chapter 4. Read it as: "if gameOver is true, use 80; otherwise, use 200." The result is stored in playerGreen and used as the green channel of the draw color. One line instead of four, and arguably easier to read once the ternary is familiar to you.

The (float)PLAYER_SIZE is a C-style cast — converting the int constant into a float because that's what SDL_FRect expects. The cleaner C++ way is static_cast<float>(PLAYER_SIZE), which we covered in Chapter 2 — feel free to use that form instead. The result is identical; the longer form is more explicit about what it's doing.

        // Bullet (only when active)
        if (bulletActive)
        {
            SDL_SetRenderDrawColor(renderer, 255, 255, 0, 255);
            SDL_FRect bulletRect = { bulletX, bulletY, (float)BULLET_SIZE, (float)BULLET_SIZE };
            SDL_RenderFillRect(renderer, &bulletRect);
        }

        // Invader
        SDL_SetRenderDrawColor(renderer, 200, 0, 0, 255);
        SDL_FRect invaderRect = { invaderX, invaderY, (float)INVADER_SIZE, (float)INVADER_SIZE };
        SDL_RenderFillRect(renderer, &invaderRect);

        // Show the finished frame
        SDL_RenderPresent(renderer);

        // Cap the frame rate at roughly 60 frames per second
        SDL_Delay(16);
    }

In the preceding code, we draw the bullet only if it's active — exactly the kind of guard the bulletActive flag exists for. The invader gets drawn unconditionally; even when the game is over, it's still visible at its last position. We present the frame, then SDL_Delay(16) pauses for sixteen milliseconds before the next iteration — roughly one sixtieth of a second, giving us a frame rate near 60 FPS without any of the delta-time arithmetic from Chapter 3.

Logging the Score and Cleaning Up

When the loop ends, log the final score so the player at least sees a number somewhere, and clean up SDL:

    SDL_Log("Game over! Final score: %d  Lives left: %d", score, lives);

    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();

    return 0;
}

SDL_Log writes to the Output window in Visual Studio. It's a quick, no-frills way to display a value while you're still learning, and we'll use it constantly throughout the book until we're ready to render text properly.

The Complete Program

Save the file. If you've followed every step, your main.cpp should look like this:

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>

const int   SCREEN_W       = 800;
const int   SCREEN_H       = 600;
const int   PLAYER_SIZE    = 40;
const int   BULLET_SIZE    = 10;
const int   INVADER_SIZE   = 40;
const float PLAYER_SPEED   = 5.0f;
const float BULLET_SPEED   = 8.0f;
const float INVADER_SPEED  = 2.0f;
const int   STARTING_LIVES = 3;

int main(int argc, char* argv[])
{
    if (!SDL_Init(SDL_INIT_VIDEO))
    {
        SDL_Log("SDL_Init failed: %s", SDL_GetError());
        return 1;
    }

    SDL_Window* window = SDL_CreateWindow("Square Invader", SCREEN_W, SCREEN_H, 0);
    if (!window)
    {
        SDL_Log("SDL_CreateWindow failed: %s", SDL_GetError());
        SDL_Quit();
        return 1;
    }

    SDL_Renderer* renderer = SDL_CreateRenderer(window, nullptr);
    if (!renderer)
    {
        SDL_Log("SDL_CreateRenderer failed: %s", SDL_GetError());
        SDL_DestroyWindow(window);
        SDL_Quit();
        return 1;
    }

    float playerX = (SCREEN_W - PLAYER_SIZE) / 2.0f;
    float playerY = SCREEN_H - PLAYER_SIZE - 10.0f;

    float bulletX      = 0.0f;
    float bulletY      = 0.0f;
    bool  bulletActive = false;

    float invaderX      = 0.0f;
    float invaderY      = 10.0f;
    float invaderSpeedX = INVADER_SPEED;

    int  score    = 0;
    int  lives    = STARTING_LIVES;
    bool gameOver = false;

    bool running = true;

    while (running)
    {
        SDL_Event event;
        while (SDL_PollEvent(&event))
        {
            switch (event.type)
            {
                case SDL_EVENT_QUIT:
                    running = false;
                    break;

                case SDL_EVENT_KEY_DOWN:
                    if (event.key.scancode == SDL_SCANCODE_ESCAPE)
                    {
                        running = false;
                    }
                    else if (event.key.scancode == SDL_SCANCODE_SPACE
                             && !bulletActive
                             && !gameOver)
                    {
                        bulletX      = playerX + (PLAYER_SIZE - BULLET_SIZE) / 2.0f;
                        bulletY      = playerY;
                        bulletActive = true;
                    }
                    break;
            }
        }

        if (!gameOver)
        {
            const bool* keys = SDL_GetKeyboardState(nullptr);

            if (keys[SDL_SCANCODE_A]) playerX -= PLAYER_SPEED;
            if (keys[SDL_SCANCODE_D]) playerX += PLAYER_SPEED;

            if (playerX < 0.0f)                   playerX = 0.0f;
            if (playerX > SCREEN_W - PLAYER_SIZE) playerX = SCREEN_W - PLAYER_SIZE;
        }

        if (bulletActive && !gameOver)
        {
            bulletY -= BULLET_SPEED;

            if (bulletY + BULLET_SIZE < 0.0f)
                bulletActive = false;

            bool overlapX = bulletX < invaderX + INVADER_SIZE &&
                            bulletX + BULLET_SIZE > invaderX;
            bool overlapY = bulletY < invaderY + INVADER_SIZE &&
                            bulletY + BULLET_SIZE > invaderY;

            if (overlapX && overlapY)
            {
                score        += 1;
                bulletActive = false;

                invaderX      = 0.0f;
                invaderY      = 10.0f;
                invaderSpeedX = INVADER_SPEED;
            }
        }

        if (!gameOver)
        {
            invaderX += invaderSpeedX;

            if (invaderX + INVADER_SIZE >= SCREEN_W)
            {
                invaderX      = SCREEN_W - INVADER_SIZE;
                invaderY     += INVADER_SIZE;
                invaderSpeedX = -INVADER_SPEED;
            }
            else if (invaderX <= 0.0f)
            {
                invaderX      = 0.0f;
                invaderY     += INVADER_SIZE;
                invaderSpeedX = INVADER_SPEED;
            }

            if (invaderY + INVADER_SIZE >= playerY)
            {
                lives -= 1;

                invaderX      = 0.0f;
                invaderY      = 10.0f;
                invaderSpeedX = INVADER_SPEED;

                gameOver = (lives <= 0);
            }
        }

        if (gameOver)
            SDL_SetRenderDrawColor(renderer, 60, 0, 0, 255);
        else
            SDL_SetRenderDrawColor(renderer, 20, 20, 30, 255);
        SDL_RenderClear(renderer);

        int playerGreen = gameOver ? 80 : 200;
        SDL_SetRenderDrawColor(renderer, 0, playerGreen, 0, 255);
        SDL_FRect playerRect = { playerX, playerY, (float)PLAYER_SIZE, (float)PLAYER_SIZE };
        SDL_RenderFillRect(renderer, &playerRect);

        if (bulletActive)
        {
            SDL_SetRenderDrawColor(renderer, 255, 255, 0, 255);
            SDL_FRect bulletRect = { bulletX, bulletY, (float)BULLET_SIZE, (float)BULLET_SIZE };
            SDL_RenderFillRect(renderer, &bulletRect);
        }

        SDL_SetRenderDrawColor(renderer, 200, 0, 0, 255);
        SDL_FRect invaderRect = { invaderX, invaderY, (float)INVADER_SIZE, (float)INVADER_SIZE };
        SDL_RenderFillRect(renderer, &invaderRect);

        SDL_RenderPresent(renderer);

        SDL_Delay(16);
    }

    SDL_Log("Game over! Final score: %d  Lives left: %d", score, lives);

    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();

    return 0;
}

Playing the Game

Hit F5 to build and run. An 800×600 window opens with a green player square near the bottom, and a red invader marching across the top. Use A and D to slide left and right. Tap Space to fire a bullet. Hit the invader and your score ticks up. Let the invader reach your row three times and the screen flashes dark red — the game is over. If waiting for the invader three times is too much, you already know what to do: change the value of STARTING_LIVES to 1 and/or speed the invader up a bit.

Square Invader gameplay screenshot
Square Invader in action: green player, yellow bullet mid-flight, red invader.
Square Invader game-over screen
Square Invader with the game-over state — background dark red, player dimmed.

Hit Escape (or close the window) to exit. Visual Studio's Output window will show the final score and lives count from the SDL_Log call at the end.

Understanding the Code

This is the biggest program we've written so far, but its shape is identical to the bouncing ball from Chapter 3. The outer layer is the same setup-and-teardown sandwich. The inner game loop still does input, update, render in that order. The only thing that has really changed is the amount of decision-making happening between "we got an event" and "we drew a pixel".

That decision-making is the whole point of flow control. Read down the body of the game loop and count the conditions:

Every single one of those is a question the program asks the world before deciding what to do next. That's the difference between a bouncing ball and a game. The ball reacts to one thing: walls. Our invader-shooter reacts to a dozen, and every one of them is built from the comparison operators and logical operators we learned in Chapter 4.

Experimenting

Try changing a value or two and see what happens.

Each one of those teaches you something about how the game's behavior follows from its variables. Tuning numbers is most of game design.

Common Errors and Fixes

The bullet fires, but never disappears or never hits anything. Most likely a typo in the collision check. Re-read the overlapX and overlapY lines. The < and > directions matter; getting one wrong gives nonsense.

The player slides off the screen. Check the clamp lines for the player. The right-side check uses SCREEN_W - PLAYER_SIZE, not just SCREEN_W. Otherwise the right edge of the player escapes.

Pressing space does nothing. Either the window doesn't have focus (click on it), or the !bulletActive part of the condition is keeping you locked out because the previous bullet is still flying. If the previous bullet is stuck at the top of the window forever, the bullet-off-screen check is wrong.

The game ends immediately. Almost certainly STARTING_LIVES is 0, or you've written gameOver = true; somewhere by mistake instead of gameOver = (lives <= 0);. The compiler will not save you from this — it's a logic bug, not a syntax error. Set a breakpoint on the lives -= 1 line and watch what happens.

The invader drops down forever once it hits a wall. Watch the else if on the left-edge check. If both edge checks are plain ifs, the invader can satisfy both in the same frame and bounce back and forth repeatedly. The else if ensures only one edge fires per frame.

AI Exercise (Optional)

If you want to take this further with AI help, here's a small challenge that pushes flow control. As always, skip it if you'd rather not — nothing later depends on it.

Open your AI chatbot of choice and try a prompt like this:

"I have a small C++ SDL 3 game with one player, one bullet, and one invader. I track three game-state variables: score, lives (currently 3), and gameOver (a bool). I have only learned variables and flow control — if, else, switch, comparison and logical operators, the ternary operator. No loops other than the main game loop. No functions other than main. No arrays, no vectors, no classes. Show me how to add a second invader that moves at a different speed and starts at a different X position. Use only the C++ features I have. Paste your complete code so I can compare line by line."

That prompt does a few useful things. It names the language and library precisely (so the AI doesn't reach for SDL 2 syntax). It names the constraints (no features you haven't met). And it asks for the whole file rather than a snippet, so you can see exactly where every change goes.

Look at the result carefully. Did the AI just copy-paste a second invader's variables and collision check, or did it find a way to keep them tidy? Did it accidentally introduce something you don't recognize? If it did, push back: "That uses a feature I haven't learned. Try again using only what I listed." This is genuinely how you should always work with AI — keep it inside your bubble of understanding, and grow the bubble deliberately rather than letting the AI bypass it.

Summary

You've built your first program that plays — a thing on screen with rules, with a goal, with a way to lose. Underneath the gameplay, every meaningful decision is a comparison or a logical combination of comparisons. That's what flow control gives you: the power to ask the world questions and act on the answers.

In the next chapter we'll cover loops — the proper, structured way to repeat work. We've already seen one loop (the game loop itself) without really understanding it, and another (the event-poll loop nested inside it). Once we get loops in our toolkit, we'll be ready for Chapter 7's project, where doing the same thing to lots of things at once finally becomes practical. Until then, take a moment to enjoy what you've made — a tiny game, built from variables, decisions, and not much else.