Breakout

Source: src/breakout.c · 383 lines · 14 functions.

Screen captures

Views of this part running on the Mini Arcade.

Breakout game screen on the Mini Arcade
Breakout showing the paddle, ball, and brick field.

Module entry points

These functions are exposed to the rest of the project. Open one to follow its decisions and the helpers it calls.

How to read these diagrams

Each function has its own expandable diagram using the actual expressions and statements from this file. Decisions split into True and False; loop bodies are indented. Returns end the function immediately; break and continue follow their C meanings. Arrows between statements show their order when execution reaches them.

These are structural source diagrams before preprocessing, not runtime traces. Conditional compilation and macros remain source expressions. Ternary choices remain inside their statement. Calls are linked separately; callback execution and task scheduling are not inferred.

Functions in this file

clampValue() · source line 59
static float clampValue(float value, float minimum, float maximum)
  1. Exit this functionreturn value < minimum ? minimum : value > maximum ? maximum : value;
Read original function source
static float clampValue(float value, float minimum, float maximum)
{
    return value < minimum ? minimum : value > maximum ? maximum : value;
}
remainingBricks() · source line 64
static unsigned remainingBricks(void)
  1. Executeunsigned count = 0U;
  2. FOR loopunsigned row = 0U; row < BRICK_ROWS; ++row

    Repeat body while the loop condition permits; then continue below.

      1. FOR loopunsigned column = 0U; column < BRICK_COLUMNS; ++column

        Repeat body while the loop condition permits; then continue below.

          1. Decisionbricks[row][column]
            True
            1. Execute++count;
            False

            Continue after this decision.

  3. Exit this functionreturn count;
Read original function source
static unsigned remainingBricks(void)
{
    unsigned count = 0U;
    for (unsigned row = 0U; row < BRICK_ROWS; ++row) {
        for (unsigned column = 0U; column < BRICK_COLUMNS; ++column) {
            if (bricks[row][column]) ++count;
        }
    }
    return count;
}
attachBall() · source line 75
static void attachBall(void)
  1. Executeball_x = paddle_x + (float)PADDLE_WIDTH * 0.5f;
  2. Executeball_y = (float)(PADDLE_Y - BALL_RADIUS - 1);
  3. Executeball_velocity_x = 0.0f;
  4. Executeball_velocity_y = 0.0f;
  5. Executeball_attached = true;
Read original function source
static void attachBall(void)
{
    ball_x = paddle_x + (float)PADDLE_WIDTH * 0.5f;
    ball_y = (float)(PADDLE_Y - BALL_RADIUS - 1);
    ball_velocity_x = 0.0f;
    ball_velocity_y = 0.0f;
    ball_attached = true;
}
beginWave() · source line 84
static void beginWave(void)

Calls into this documentation:

  1. FOR loopunsigned row = 0U; row < BRICK_ROWS; ++row

    Repeat body while the loop condition permits; then continue below.

      1. FOR loopunsigned column = 0U; column < BRICK_COLUMNS; ++column

        Repeat body while the loop condition permits; then continue below.

        1. Executebricks[row][column] = true;
  2. Executepaddle_x = (float)(LCD_WIDTH - PADDLE_WIDTH) * 0.5f;
  3. ExecuteattachBall();
  4. ExecuteESP_LOGI("breakout", "WAVE %u", (unsigned)wave);
Read original function source
static void beginWave(void)
{
    for (unsigned row = 0U; row < BRICK_ROWS; ++row) {
        for (unsigned column = 0U; column < BRICK_COLUMNS; ++column) bricks[row][column] = true;
    }
    paddle_x = (float)(LCD_WIDTH - PADDLE_WIDTH) * 0.5f;
    attachBall();
    ESP_LOGI("breakout", "WAVE %u", (unsigned)wave);
}
beginGame() · source line 94
static void beginGame(uint32_t now_ms)

Calls into this documentation:

  1. Executescore = 0U;
  2. Executewave = 1U;
  3. Executelives = 3U;
  4. Executeaccumulator = 0U;
  5. Executestate_started = now_ms;
  6. ExecutebeginWave();
  7. Executeai_next_decision = now_ms + aiSpeedDelay(ai_speed);
  8. Executestate = BREAKOUT_PLAYING;
  9. ExecuteaudioPlay(AUDIO_SFX_GAME_START);
Read original function source
static void beginGame(uint32_t now_ms)
{
    score = 0U;
    wave = 1U;
    lives = 3U;
    accumulator = 0U;
    state_started = now_ms;
    beginWave();
    ai_next_decision = now_ms + aiSpeedDelay(ai_speed);
    state = BREAKOUT_PLAYING;
    audioPlay(AUDIO_SFX_GAME_START);
}
launchBall() · source line 107
static void launchBall(void)
  1. Decision!ball_attached
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Executeconst float speed = 112.0f + (float)(wave > 8U ? 8U : wave) * 6.0f;
  3. Executeball_velocity_x = speed * 0.56f;
  4. Executeball_velocity_y = -speed * 0.83f;
  5. Executeball_attached = false;
Read original function source
static void launchBall(void)
{
    if (!ball_attached) return;
    const float speed = 112.0f + (float)(wave > 8U ? 8U : wave) * 6.0f;
    ball_velocity_x = speed * 0.56f;
    ball_velocity_y = -speed * 0.83f;
    ball_attached = false;
}
finishGame() · source line 116
static void finishGame(uint32_t now_ms)

Calls into this documentation:

  1. Executestate = BREAKOUT_FINISHED;
  2. Executestate_started = now_ms;
  3. Executeauto_restart_started = now_ms;
  4. ExecuteaudioPlay(AUDIO_SFX_GAME_OVER);
  5. Executeconst uint32_t gold = score / 1000U;
  6. Decisiongold != 0U && !(auto_mode ? addAIGold(gold) : addUserGoldWon(gold))
    True
      1. ExecuteESP_LOGE("breakout", "Failed to save %u Gold", (unsigned)gold);
    False

    Continue after this decision.

  7. ExecuteESP_LOGI("breakout", "GAME OVER: SCORE %u WAVE %u", (unsigned)score, (unsigned)wave);
Read original function source
static void finishGame(uint32_t now_ms)
{
    state = BREAKOUT_FINISHED;
    state_started = now_ms;
    auto_restart_started = now_ms;
    audioPlay(AUDIO_SFX_GAME_OVER);
    const uint32_t gold = score / 1000U;
    if (gold != 0U && !(auto_mode ? addAIGold(gold) : addUserGoldWon(gold))) {
        ESP_LOGE("breakout", "Failed to save %u Gold", (unsigned)gold);
    }
    ESP_LOGI("breakout", "GAME OVER: SCORE %u WAVE %u", (unsigned)score, (unsigned)wave);
}
hitBrick() · source line 129
static bool hitBrick(float previous_x, float previous_y)

Calls into this documentation:

  1. FOR loopunsigned row = 0U; row < BRICK_ROWS; ++row

    Repeat body while the loop condition permits; then continue below.

      1. FOR loopunsigned column = 0U; column < BRICK_COLUMNS; ++column

        Repeat body while the loop condition permits; then continue below.

          1. Decision!bricks[row][column]
            True
            1. Next loop iterationcontinue;
            False

            Continue after this decision.

          2. Executeconst float left = (float)(BRICK_LEFT + column * (BRICK_WIDTH + BRICK_GAP_X));
          3. Executeconst float top = (float)(BRICK_TOP + row * (BRICK_HEIGHT + BRICK_GAP_Y));
          4. Executeconst float right = left + (float)BRICK_WIDTH;
          5. Executeconst float bottom = top + (float)BRICK_HEIGHT;
          6. Decisionball_x + BALL_RADIUS <= left || ball_x - BALL_RADIUS >= right || ball_y + BALL_RADIUS <= top || ball_y - BALL_RADIUS >= bottom
            True
            1. Next loop iterationcontinue;
            False

            Continue after this decision.

          7. Executebricks[row][column] = false;
          8. Executescore = score > UINT32_MAX - brick_points[row] ? UINT32_MAX : score + brick_points[row];
          9. Decisionprevious_y + BALL_RADIUS <= top || previous_y - BALL_RADIUS >= bottom
            True
            1. Executeball_velocity_y = -ball_velocity_y;
            False
            1. Decisionprevious_x + BALL_RADIUS <= left || previous_x - BALL_RADIUS >= right
              True
              1. Executeball_velocity_x = -ball_velocity_x;
              False
              1. Executeball_velocity_y = -ball_velocity_y;
          10. ExecuteaudioPlay(AUDIO_SFX_HIT);
          11. Exit this functionreturn true;
  2. Exit this functionreturn false;
Read original function source
static bool hitBrick(float previous_x, float previous_y)
{
    for (unsigned row = 0U; row < BRICK_ROWS; ++row) {
        for (unsigned column = 0U; column < BRICK_COLUMNS; ++column) {
            if (!bricks[row][column]) continue;
            const float left = (float)(BRICK_LEFT + column * (BRICK_WIDTH + BRICK_GAP_X));
            const float top = (float)(BRICK_TOP + row * (BRICK_HEIGHT + BRICK_GAP_Y));
            const float right = left + (float)BRICK_WIDTH;
            const float bottom = top + (float)BRICK_HEIGHT;
            if (ball_x + BALL_RADIUS <= left || ball_x - BALL_RADIUS >= right ||
                ball_y + BALL_RADIUS <= top || ball_y - BALL_RADIUS >= bottom) continue;
            bricks[row][column] = false;
            score = score > UINT32_MAX - brick_points[row] ? UINT32_MAX : score + brick_points[row];
            if (previous_y + BALL_RADIUS <= top || previous_y - BALL_RADIUS >= bottom) ball_velocity_y = -ball_velocity_y;
            else if (previous_x + BALL_RADIUS <= left || previous_x - BALL_RADIUS >= right) ball_velocity_x = -ball_velocity_x;
            else ball_velocity_y = -ball_velocity_y;
            audioPlay(AUDIO_SFX_HIT);
            return true;
        }
    }
    return false;
}
simulateStep() · source line 152
static void simulateStep(uint32_t now_ms)

Calls into this documentation:

  1. Decisionball_attached
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Executeconst float previous_x = ball_x;
  3. Executeconst float previous_y = ball_y;
  4. Executeball_x += ball_velocity_x * (float)STEP_MS / 1000.0f;
  5. Executeball_y += ball_velocity_y * (float)STEP_MS / 1000.0f;
  6. Executebool bounced = false;
  7. Decisionball_x - BALL_RADIUS < 1.0f
    True
      1. Executeball_x = 1.0f + BALL_RADIUS;
      2. Executeball_velocity_x = -ball_velocity_x;
      3. Executebounced = true;
    False
    1. Decisionball_x + BALL_RADIUS >= (float)(LCD_WIDTH - 1)
      True
        1. Executeball_x = (float)(LCD_WIDTH - 1 - BALL_RADIUS);
        2. Executeball_velocity_x = -ball_velocity_x;
        3. Executebounced = true;
      False

      Continue after this decision.

  8. Decisionball_y - BALL_RADIUS < 14.0f
    True
      1. Executeball_y = 14.0f + BALL_RADIUS;
      2. Executeball_velocity_y = -ball_velocity_y;
      3. Executebounced = true;
    False

    Continue after this decision.

  9. Decisionbounced
    True
    1. ExecuteaudioPlay(AUDIO_SFX_BOUNCE);
    False

    Continue after this decision.

  10. Execute(void)hitBrick(previous_x, previous_y);
  11. Decisionball_velocity_y > 0.0f && ball_y + BALL_RADIUS >= (float)PADDLE_Y && ball_y - BALL_RADIUS <= (float)(PADDLE_Y + PADDLE_HEIGHT) && ball_x + BALL_RADIUS >= paddle_x && ball_x - BALL_RADIUS <= paddle_x + PADDLE_WIDTH
    True
      1. Executeconst float relative = (ball_x - (paddle_x + (float)PADDLE_WIDTH * 0.5f)) / ((float)PADDLE_WIDTH * 0.5f);
      2. Executeconst float speed = 124.0f + (float)(wave > 8U ? 8U : wave) * 6.0f;
      3. Executeball_y = (float)(PADDLE_Y - BALL_RADIUS);
      4. Executeball_velocity_x = relative * speed;
      5. Executeball_velocity_y = -(speed - (relative < 0.0f ? -relative : relative) * speed * 0.25f);
      6. ExecuteaudioPlay(AUDIO_SFX_BOUNCE);
    False

    Continue after this decision.

  12. Decisionball_y - BALL_RADIUS >= LCD_HEIGHT
    True
      1. Decisionlives == 0U
        True
        1. Exit this functionreturn;
        False

        Continue after this decision.

      2. Execute--lives;
      3. Decisionlives == 0U
        True
          1. ExecutefinishGame(now_ms);
          2. Exit this functionreturn;
        False

        Continue after this decision.

      4. ExecuteaudioPlay(AUDIO_SFX_LIFE_LOST);
      5. Executestate = BREAKOUT_LIFE_LOST;
      6. Executestate_started = now_ms;
      7. Executeball_attached = true;
      8. Exit this functionreturn;
    False

    Continue after this decision.

  13. DecisionremainingBricks() == 0U
    True
      1. ExecuteaudioPlay(AUDIO_SFX_WAVE_CLEAR);
      2. Decisionwave < UINT32_MAX
        True
        1. Execute++wave;
        False

        Continue after this decision.

      3. ExecutebeginWave();
    False

    Continue after this decision.

Read original function source
static void simulateStep(uint32_t now_ms)
{
    if (ball_attached) return;
    const float previous_x = ball_x;
    const float previous_y = ball_y;
    ball_x += ball_velocity_x * (float)STEP_MS / 1000.0f;
    ball_y += ball_velocity_y * (float)STEP_MS / 1000.0f;
    bool bounced = false;
    if (ball_x - BALL_RADIUS < 1.0f) {
        ball_x = 1.0f + BALL_RADIUS;
        ball_velocity_x = -ball_velocity_x;
        bounced = true;
    } else if (ball_x + BALL_RADIUS >= (float)(LCD_WIDTH - 1)) {
        ball_x = (float)(LCD_WIDTH - 1 - BALL_RADIUS);
        ball_velocity_x = -ball_velocity_x;
        bounced = true;
    }
    if (ball_y - BALL_RADIUS < 14.0f) {
        ball_y = 14.0f + BALL_RADIUS;
        ball_velocity_y = -ball_velocity_y;
        bounced = true;
    }
    if (bounced) audioPlay(AUDIO_SFX_BOUNCE);
    (void)hitBrick(previous_x, previous_y);
    if (ball_velocity_y > 0.0f && ball_y + BALL_RADIUS >= (float)PADDLE_Y &&
        ball_y - BALL_RADIUS <= (float)(PADDLE_Y + PADDLE_HEIGHT) &&
        ball_x + BALL_RADIUS >= paddle_x && ball_x - BALL_RADIUS <= paddle_x + PADDLE_WIDTH) {
        const float relative = (ball_x - (paddle_x + (float)PADDLE_WIDTH * 0.5f)) / ((float)PADDLE_WIDTH * 0.5f);
        const float speed = 124.0f + (float)(wave > 8U ? 8U : wave) * 6.0f;
        ball_y = (float)(PADDLE_Y - BALL_RADIUS);
        ball_velocity_x = relative * speed;
        ball_velocity_y = -(speed - (relative < 0.0f ? -relative : relative) * speed * 0.25f);
        audioPlay(AUDIO_SFX_BOUNCE);
    }
    if (ball_y - BALL_RADIUS >= LCD_HEIGHT) {
        if (lives == 0U) return;
        --lives;
        if (lives == 0U) {
            finishGame(now_ms);
            return;
        }
        audioPlay(AUDIO_SFX_LIFE_LOST);
        state = BREAKOUT_LIFE_LOST;
        state_started = now_ms;
        ball_attached = true;
        return;
    }
    if (remainingBricks() == 0U) {
        audioPlay(AUDIO_SFX_WAVE_CLEAR);
        if (wave < UINT32_MAX) ++wave;
        beginWave();
    }
}
startBreakout() · source line 206
void startBreakout(uint32_t now_ms)

Calls into this documentation:

  1. Executerunning = true;
  2. Executestate = BREAKOUT_READY;
  3. Executelast_update = now_ms;
  4. Executeaccumulator = 0U;
  5. Executescore = 0U;
  6. Executewave = 1U;
  7. Executelives = 3U;
  8. ExecutebeginWave();
  9. Executecontrols_ready = false;
  10. Executeauto_mode = false;
  11. Executeai_speed = AI_SPEED_DEFAULT_INDEX;
  12. Executeauto_target_x = paddle_x;
  13. ExecutemodeSelectionReset(&mode_input);
  14. ExecuteESP_LOGI("breakout", "START");
Read original function source
void startBreakout(uint32_t now_ms)
{
    running = true;
    state = BREAKOUT_READY;
    last_update = now_ms;
    accumulator = 0U;
    score = 0U;
    wave = 1U;
    lives = 3U;
    beginWave();
    controls_ready = false;
    auto_mode = false;
    ai_speed = AI_SPEED_DEFAULT_INDEX;
    auto_target_x = paddle_x;
    modeSelectionReset(&mode_input);
    ESP_LOGI("breakout", "START");
}
stopBreakout() · source line 224
void stopBreakout(void)

Calls into this documentation:

  1. ExecutecancelHighScoreEntry();
  2. Executerunning = false;
  3. ExecuteESP_LOGI("breakout", "STOP");
Read original function source
void stopBreakout(void)
{
    cancelHighScoreEntry();
    running = false;
    ESP_LOGI("breakout", "STOP");
}
breakoutAction() · source line 231
void breakoutAction(bool action, bool speed, uint32_t now_ms)

Calls into this documentation:

  1. Decision!running
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Decisionstate == BREAKOUT_READY
    True
      1. Decisionaction
        True
        1. ExecutebeginGame(now_ms);
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  3. Decisionauto_mode
    True
      1. Decisionspeed
        True
          1. ExecutecycleAISpeed(&ai_speed);
          2. Executeai_next_decision = now_ms;
        False

        Continue after this decision.

      2. Decisionaction
        True
          1. Executeauto_mode = false;
          2. ExecutemodeSelectionReset(&mode_input);
          3. Executestate = BREAKOUT_READY;
        False

        Continue after this decision.

      3. Exit this functionreturn;
    False

    Continue after this decision.

  4. Decisionaction && state == BREAKOUT_PLAYING && ball_attached
    True
    1. ExecutelaunchBall();
    False

    Continue after this decision.

Read original function source
void breakoutAction(bool action, bool speed, uint32_t now_ms)
{
    if (!running) return;
    if (state == BREAKOUT_READY) {
        if (action) beginGame(now_ms);
        return;
    }
    if (auto_mode) {
        if (speed) {
            cycleAISpeed(&ai_speed);
            ai_next_decision = now_ms;
        }
        if (action) {
            auto_mode = false;
            modeSelectionReset(&mode_input);
            state = BREAKOUT_READY;
        }
        return;
    }
    if (action && state == BREAKOUT_PLAYING && ball_attached) launchBall();
}
updateBreakout() · source line 253
void updateBreakout(int x_offset, int y_offset, bool joystick_valid, bool left, bool right,
                    uint32_t now_ms)

Calls into this documentation:

  1. Decision!running
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Executeconst uint32_t elapsed = now_ms - last_update;
  3. Executelast_update = now_ms;
  4. Decisionstate == BREAKOUT_FINISHED
    True
      1. Decisionauto_mode
        True
          1. Decisionnow_ms - auto_restart_started >= aiRestartDelay()
            True
            1. ExecutebeginGame(now_ms);
            False

            Continue after this decision.

        False
        1. Decisionnow_ms - state_started >= 1000U
          True
            1. ExecutestartHighScoreEntry(HIGH_SCORE_BREAKOUT, score, now_ms);
            2. Executestate = BREAKOUT_SCORES;
          False

          Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  5. Decisionstate == BREAKOUT_SCORES
    True
      1. DecisionupdateHighScoreEntry(x_offset, y_offset, joystick_valid, now_ms)
        True
        1. Executestate = BREAKOUT_READY;
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  6. Decisionstate == BREAKOUT_LIFE_LOST
    True
      1. Decisionnow_ms - state_started >= LIFE_LOST_MS
        True
          1. ExecuteattachBall();
          2. Executeai_next_decision = now_ms + aiSpeedDelay(ai_speed);
          3. Executestate = BREAKOUT_PLAYING;
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  7. Decisionstate == BREAKOUT_READY
    True
      1. DecisionmodeSelectionNext(&mode_input, x_offset, y_offset, joystick_valid, left, right, now_ms)
        True
          1. Executeauto_mode = !auto_mode;
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  8. Decisionstate != BREAKOUT_PLAYING
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  9. Decision!joystick_valid
    True
    1. Executecontrols_ready = false;
    False
    1. Decisionx_offset >= -350 && x_offset <= 350
      True
      1. Executecontrols_ready = true;
      False

      Continue after this decision.

  10. Executeconst float delta_seconds = (float)(elapsed > 50U ? 50U : elapsed) / 1000.0f;
  11. Decisionauto_mode
    True
      1. Decisionnow_ms >= ai_next_decision
        True
          1. Executeauto_target_x = clampValue(ball_x - (float)PADDLE_WIDTH * 0.5f, 1.0f, (float)(LCD_WIDTH - 1 - PADDLE_WIDTH));
          2. Executeai_next_decision = now_ms + aiSpeedDelay(ai_speed);
          3. Decisionball_attached
            True
            1. ExecutelaunchBall();
            False

            Continue after this decision.

        False

        Continue after this decision.

      2. Executeconst float distance = auto_target_x - paddle_x;
      3. Executeconst float maximum_step = 205.0f * delta_seconds;
      4. Executeconst float step = clampValue(distance, -maximum_step, maximum_step);
      5. Executepaddle_x = clampValue(paddle_x + step, 1.0f, (float)(LCD_WIDTH - 1 - PADDLE_WIDTH));
    False
      1. Executeconst int direction = left == right ? 0 : left ? -1 : 1;
      2. Executeconst float joystick_speed = joystick_valid && controls_ready && !left && !right ? clampValue((float)x_offset / 1600.0f, -1.0f, 1.0f) * 205.0f : 0.0f;
      3. Executeconst float button_speed = (float)direction * 205.0f;
      4. Executepaddle_x = clampValue(paddle_x + (left || right ? button_speed : joystick_speed) * delta_seconds, 1.0f, (float)(LCD_WIDTH - 1 - PADDLE_WIDTH));
  12. Decisionball_attached
    True
      1. Executeball_x = paddle_x + (float)PADDLE_WIDTH * 0.5f;
      2. Exit this functionreturn;
    False

    Continue after this decision.

  13. Executeaccumulator += elapsed > 100U ? 100U : elapsed;
  14. WHILE loopaccumulator >= STEP_MS && state == BREAKOUT_PLAYING

    Repeat body while the loop condition permits; then continue below.

      1. Executeaccumulator -= STEP_MS;
      2. ExecutesimulateStep(now_ms);
Read original function source
void updateBreakout(int x_offset, int y_offset, bool joystick_valid, bool left, bool right,
                    uint32_t now_ms)
{
    if (!running) return;
    const uint32_t elapsed = now_ms - last_update;
    last_update = now_ms;
    if (state == BREAKOUT_FINISHED) {
        if (auto_mode) {
            if (now_ms - auto_restart_started >= aiRestartDelay()) beginGame(now_ms);
        } else if (now_ms - state_started >= 1000U) {
            startHighScoreEntry(HIGH_SCORE_BREAKOUT, score, now_ms);
            state = BREAKOUT_SCORES;
        }
        return;
    }
    if (state == BREAKOUT_SCORES) {
        if (updateHighScoreEntry(x_offset, y_offset, joystick_valid, now_ms)) state = BREAKOUT_READY;
        return;
    }
    if (state == BREAKOUT_LIFE_LOST) {
        if (now_ms - state_started >= LIFE_LOST_MS) {
            attachBall();
            ai_next_decision = now_ms + aiSpeedDelay(ai_speed);
            state = BREAKOUT_PLAYING;
        }
        return;
    }
    if (state == BREAKOUT_READY) {
        if (modeSelectionNext(&mode_input, x_offset, y_offset, joystick_valid, left, right,
                              now_ms)) {
            auto_mode = !auto_mode;
        }
        return;
    }
    if (state != BREAKOUT_PLAYING) return;
    if (!joystick_valid) controls_ready = false;
    else if (x_offset >= -350 && x_offset <= 350) controls_ready = true;
    const float delta_seconds = (float)(elapsed > 50U ? 50U : elapsed) / 1000.0f;
    if (auto_mode) {
        if (now_ms >= ai_next_decision) {
            auto_target_x = clampValue(ball_x - (float)PADDLE_WIDTH * 0.5f, 1.0f,
                                       (float)(LCD_WIDTH - 1 - PADDLE_WIDTH));
            ai_next_decision = now_ms + aiSpeedDelay(ai_speed);
            if (ball_attached) launchBall();
        }
        const float distance = auto_target_x - paddle_x;
        const float maximum_step = 205.0f * delta_seconds;
        const float step = clampValue(distance, -maximum_step, maximum_step);
        paddle_x = clampValue(paddle_x + step, 1.0f, (float)(LCD_WIDTH - 1 - PADDLE_WIDTH));
    } else {
        const int direction = left == right ? 0 : left ? -1 : 1;
        const float joystick_speed = joystick_valid && controls_ready && !left && !right
            ? clampValue((float)x_offset / 1600.0f, -1.0f, 1.0f) * 205.0f : 0.0f;
        const float button_speed = (float)direction * 205.0f;
        paddle_x = clampValue(paddle_x + (left || right ? button_speed : joystick_speed) *
                              delta_seconds, 1.0f,
                              (float)(LCD_WIDTH - 1 - PADDLE_WIDTH));
    }
    if (ball_attached) {
        ball_x = paddle_x + (float)PADDLE_WIDTH * 0.5f;
        return;
    }
    accumulator += elapsed > 100U ? 100U : elapsed;
    while (accumulator >= STEP_MS && state == BREAKOUT_PLAYING) {
        accumulator -= STEP_MS;
        simulateStep(now_ms);
    }
}
drawBreakout() · source line 322
void drawBreakout(uint16_t *frame)

Calls into this documentation:

  1. Decision!running || frame == NULL
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Decisionstate == BREAKOUT_SCORES
    True
      1. ExecutedrawHighScoreEntry(frame);
      2. Exit this functionreturn;
    False

    Continue after this decision.

  3. Executeclear(frame, COLOR_BLACK);
  4. Executechar text[40];
  5. Decisionstate == BREAKOUT_READY
    True
      1. Executedraw_text_centred(frame, 160, 3, "BREAKOUT", 1, COLOR_CYAN);
    False
      1. Executesnprintf(text, sizeof(text), "SCORE %u", (unsigned)score);
      2. Executedraw_text(frame, 4, 3, text, 1, COLOR_WHITE);
      3. Executesnprintf(text, sizeof(text), "LIVES %u", (unsigned)lives);
      4. Executedraw_text_centred(frame, 160, 3, text, 1, COLOR_CYAN);
      5. Executesnprintf(text, sizeof(text), "WAVE %u", (unsigned)wave);
      6. Executedraw_text(frame, 222, 3, text, 1, COLOR_YELLOW);
  6. Executedraw_line(frame, 0, 13, LCD_WIDTH - 1, 13, COLOR_DARKGRAY);
  7. FOR loopunsigned row = 0U; row < BRICK_ROWS; ++row

    Repeat body while the loop condition permits; then continue below.

      1. FOR loopunsigned column = 0U; column < BRICK_COLUMNS; ++column

        Repeat body while the loop condition permits; then continue below.

          1. Decision!bricks[row][column]
            True
            1. Next loop iterationcontinue;
            False

            Continue after this decision.

          2. Executeconst int x = BRICK_LEFT + (int)column * (BRICK_WIDTH + BRICK_GAP_X);
          3. Executeconst int y = BRICK_TOP + (int)row * (BRICK_HEIGHT + BRICK_GAP_Y);
          4. Executefill_rect(frame, x, y, BRICK_WIDTH, BRICK_HEIGHT, brick_colours[row]);
          5. Executedraw_line(frame, x, y, x + BRICK_WIDTH - 1, y, COLOR_WHITE);
  8. Decisionstate != BREAKOUT_READY
    True
      1. Executefill_rect(frame, (int)paddle_x, PADDLE_Y, PADDLE_WIDTH, PADDLE_HEIGHT, COLOR_LIGHTGREEN);
      2. Executefill_circle(frame, (int)ball_x, (int)ball_y, BALL_RADIUS, COLOR_WHITE);
    False

    Continue after this decision.

  9. Decisionstate == BREAKOUT_READY
    True
      1. Executedraw_text_centred(frame, 100, 97, "PLAY", 2, auto_mode ? COLOR_LIGHTGRAY : COLOR_YELLOW);
      2. Executedraw_text_centred(frame, 220, 97, "AUTO", 2, auto_mode ? COLOR_YELLOW : COLOR_LIGHTGRAY);
      3. Executedraw_text(frame, 49, 121, "RED 60", 1, COLOR_RED);
      4. Executedraw_text(frame, 124, 121, "ORANGE 50", 1, COLOR_ORANGE);
      5. Executedraw_text(frame, 218, 121, "YELLOW 40", 1, COLOR_YELLOW);
      6. Executedraw_text(frame, 52, 132, "LIME 30", 1, COLOR_LIME);
      7. Executedraw_text(frame, 132, 132, "CYAN 20", 1, COLOR_CYAN);
      8. Executedraw_text(frame, 212, 132, "PURPLE 10", 1, COLOR_MEDIUMPURPLE);
      9. Executedraw_text_centred(frame, 160, 153, "D/B CHANGE A/K START", 1, COLOR_YELLOW);
    False
    1. Decisionstate == BREAKOUT_LIFE_LOST
      True
        1. Executedraw_text_centred(frame, 160, 89, "BALL LOST", 2, COLOR_RED);
      False
      1. Decisionstate == BREAKOUT_FINISHED
        True
          1. Executedraw_text_centred(frame, 160, 89, "GAME OVER", 2, COLOR_RED);
          2. Decisionauto_mode
            True
              1. Executeconst uint32_t elapsed = last_update - auto_restart_started;
              2. Executeconst uint32_t delay = aiRestartDelay();
              3. Executeconst unsigned seconds = elapsed >= delay ? 0U : (unsigned)((delay - elapsed + 999U) / 1000U);
              4. Executesnprintf(text, sizeof(text), "NEXT %u", seconds);
              5. Executedraw_text_centred(frame, 160, 114, text, 1, COLOR_YELLOW);
            False

            Continue after this decision.

        False
        1. Decisionauto_mode
          True
            1. Executesnprintf(text, sizeof(text), "AUTO %s E SPEED A/K STOP", aiSpeedShortText(ai_speed));
            2. Executedraw_text_centred(frame, 160, 136, text, 1, COLOR_LIGHTGRAY);
          False
          1. Decisionball_attached
            True
              1. Executedraw_text_centred(frame, 160, 136, "A/K LAUNCH", 1, COLOR_LIGHTGRAY);
            False

            Continue after this decision.

Read original function source
void drawBreakout(uint16_t *frame)
{
    if (!running || frame == NULL) return;
    if (state == BREAKOUT_SCORES) {
        drawHighScoreEntry(frame);
        return;
    }
    clear(frame, COLOR_BLACK);
    char text[40];
    if (state == BREAKOUT_READY) {
        draw_text_centred(frame, 160, 3, "BREAKOUT", 1, COLOR_CYAN);
    } else {
        snprintf(text, sizeof(text), "SCORE %u", (unsigned)score);
        draw_text(frame, 4, 3, text, 1, COLOR_WHITE);
        snprintf(text, sizeof(text), "LIVES %u", (unsigned)lives);
        draw_text_centred(frame, 160, 3, text, 1, COLOR_CYAN);
        snprintf(text, sizeof(text), "WAVE %u", (unsigned)wave);
        draw_text(frame, 222, 3, text, 1, COLOR_YELLOW);
    }
    draw_line(frame, 0, 13, LCD_WIDTH - 1, 13, COLOR_DARKGRAY);
    for (unsigned row = 0U; row < BRICK_ROWS; ++row) {
        for (unsigned column = 0U; column < BRICK_COLUMNS; ++column) {
            if (!bricks[row][column]) continue;
            const int x = BRICK_LEFT + (int)column * (BRICK_WIDTH + BRICK_GAP_X);
            const int y = BRICK_TOP + (int)row * (BRICK_HEIGHT + BRICK_GAP_Y);
            fill_rect(frame, x, y, BRICK_WIDTH, BRICK_HEIGHT, brick_colours[row]);
            draw_line(frame, x, y, x + BRICK_WIDTH - 1, y, COLOR_WHITE);
        }
    }
    if (state != BREAKOUT_READY) {
        fill_rect(frame, (int)paddle_x, PADDLE_Y, PADDLE_WIDTH, PADDLE_HEIGHT, COLOR_LIGHTGREEN);
        fill_circle(frame, (int)ball_x, (int)ball_y, BALL_RADIUS, COLOR_WHITE);
    }
    if (state == BREAKOUT_READY) {
        draw_text_centred(frame, 100, 97, "PLAY", 2,
                          auto_mode ? COLOR_LIGHTGRAY : COLOR_YELLOW);
        draw_text_centred(frame, 220, 97, "AUTO", 2,
                          auto_mode ? COLOR_YELLOW : COLOR_LIGHTGRAY);
        draw_text(frame, 49, 121, "RED 60", 1, COLOR_RED);
        draw_text(frame, 124, 121, "ORANGE 50", 1, COLOR_ORANGE);
        draw_text(frame, 218, 121, "YELLOW 40", 1, COLOR_YELLOW);
        draw_text(frame, 52, 132, "LIME 30", 1, COLOR_LIME);
        draw_text(frame, 132, 132, "CYAN 20", 1, COLOR_CYAN);
        draw_text(frame, 212, 132, "PURPLE 10", 1, COLOR_MEDIUMPURPLE);
        draw_text_centred(frame, 160, 153, "D/B CHANGE  A/K START", 1, COLOR_YELLOW);
    } else if (state == BREAKOUT_LIFE_LOST) {
        draw_text_centred(frame, 160, 89, "BALL LOST", 2, COLOR_RED);
    } else if (state == BREAKOUT_FINISHED) {
        draw_text_centred(frame, 160, 89, "GAME OVER", 2, COLOR_RED);
        if (auto_mode) {
            const uint32_t elapsed = last_update - auto_restart_started;
            const uint32_t delay = aiRestartDelay();
            const unsigned seconds = elapsed >= delay ? 0U : (unsigned)((delay - elapsed + 999U) / 1000U);
            snprintf(text, sizeof(text), "NEXT %u", seconds);
            draw_text_centred(frame, 160, 114, text, 1, COLOR_YELLOW);
        }
    } else if (auto_mode) {
        snprintf(text, sizeof(text), "AUTO %s  E SPEED  A/K STOP", aiSpeedShortText(ai_speed));
        draw_text_centred(frame, 160, 136, text, 1, COLOR_LIGHTGRAY);
    } else if (ball_attached) {
        draw_text_centred(frame, 160, 136, "A/K LAUNCH", 1, COLOR_LIGHTGRAY);
    }
}
Complete original file, including constants and data
/* Coded By JD(2026) */
#error "Reference listing only — not provided as build-ready source."
#include "breakout.h"

#include "audio.h"
#include "ai_speed.h"
#include "game_graphics.h"
#include "gold.h"
#include "high_scores.h"
#include "mode_selection.h"
#include "PET2001_font.h"
#include "esp_log.h"

#include <stdio.h>
#include <string.h>

#define BRICK_ROWS 6U
#define BRICK_COLUMNS 10U
#define BRICK_LEFT 20
#define BRICK_TOP 23
#define BRICK_WIDTH 26
#define BRICK_HEIGHT 9
#define BRICK_GAP_X 2
#define BRICK_GAP_Y 3
#define PADDLE_Y 151
#define PADDLE_WIDTH 48
#define PADDLE_HEIGHT 5
#define BALL_RADIUS 3
#define STEP_MS 10U
#define LIFE_LOST_MS 900U

typedef enum { BREAKOUT_READY, BREAKOUT_PLAYING, BREAKOUT_LIFE_LOST, BREAKOUT_FINISHED, BREAKOUT_SCORES } breakout_state_t;

static const uint16_t brick_colours[BRICK_ROWS] = {
    COLOR_RED, COLOR_ORANGE, COLOR_YELLOW, COLOR_LIME, COLOR_CYAN, COLOR_MEDIUMPURPLE
};
static const uint16_t brick_points[BRICK_ROWS] = {60U, 50U, 40U, 30U, 20U, 10U};

static bool bricks[BRICK_ROWS][BRICK_COLUMNS];
static breakout_state_t state;
static float paddle_x;
static float ball_x;
static float ball_y;
static float ball_velocity_x;
static float ball_velocity_y;
static uint32_t score;
static uint32_t wave;
static uint32_t last_update;
static uint32_t accumulator;
static uint32_t state_started;
static uint32_t ai_next_decision;
static uint32_t auto_restart_started;
static uint8_t lives;
static uint8_t ai_speed;
static bool ball_attached;
static bool running;
static bool controls_ready;
static bool auto_mode;
static float auto_target_x;
static mode_selection_input_t mode_input;

static float clampValue(float value, float minimum, float maximum)
{
    return value < minimum ? minimum : value > maximum ? maximum : value;
}

static unsigned remainingBricks(void)
{
    unsigned count = 0U;
    for (unsigned row = 0U; row < BRICK_ROWS; ++row) {
        for (unsigned column = 0U; column < BRICK_COLUMNS; ++column) {
            if (bricks[row][column]) ++count;
        }
    }
    return count;
}

static void attachBall(void)
{
    ball_x = paddle_x + (float)PADDLE_WIDTH * 0.5f;
    ball_y = (float)(PADDLE_Y - BALL_RADIUS - 1);
    ball_velocity_x = 0.0f;
    ball_velocity_y = 0.0f;
    ball_attached = true;
}

static void beginWave(void)
{
    for (unsigned row = 0U; row < BRICK_ROWS; ++row) {
        for (unsigned column = 0U; column < BRICK_COLUMNS; ++column) bricks[row][column] = true;
    }
    paddle_x = (float)(LCD_WIDTH - PADDLE_WIDTH) * 0.5f;
    attachBall();
    ESP_LOGI("breakout", "WAVE %u", (unsigned)wave);
}

static void beginGame(uint32_t now_ms)
{
    score = 0U;
    wave = 1U;
    lives = 3U;
    accumulator = 0U;
    state_started = now_ms;
    beginWave();
    ai_next_decision = now_ms + aiSpeedDelay(ai_speed);
    state = BREAKOUT_PLAYING;
    audioPlay(AUDIO_SFX_GAME_START);
}

static void launchBall(void)
{
    if (!ball_attached) return;
    const float speed = 112.0f + (float)(wave > 8U ? 8U : wave) * 6.0f;
    ball_velocity_x = speed * 0.56f;
    ball_velocity_y = -speed * 0.83f;
    ball_attached = false;
}

static void finishGame(uint32_t now_ms)
{
    state = BREAKOUT_FINISHED;
    state_started = now_ms;
    auto_restart_started = now_ms;
    audioPlay(AUDIO_SFX_GAME_OVER);
    const uint32_t gold = score / 1000U;
    if (gold != 0U && !(auto_mode ? addAIGold(gold) : addUserGoldWon(gold))) {
        ESP_LOGE("breakout", "Failed to save %u Gold", (unsigned)gold);
    }
    ESP_LOGI("breakout", "GAME OVER: SCORE %u WAVE %u", (unsigned)score, (unsigned)wave);
}

static bool hitBrick(float previous_x, float previous_y)
{
    for (unsigned row = 0U; row < BRICK_ROWS; ++row) {
        for (unsigned column = 0U; column < BRICK_COLUMNS; ++column) {
            if (!bricks[row][column]) continue;
            const float left = (float)(BRICK_LEFT + column * (BRICK_WIDTH + BRICK_GAP_X));
            const float top = (float)(BRICK_TOP + row * (BRICK_HEIGHT + BRICK_GAP_Y));
            const float right = left + (float)BRICK_WIDTH;
            const float bottom = top + (float)BRICK_HEIGHT;
            if (ball_x + BALL_RADIUS <= left || ball_x - BALL_RADIUS >= right ||
                ball_y + BALL_RADIUS <= top || ball_y - BALL_RADIUS >= bottom) continue;
            bricks[row][column] = false;
            score = score > UINT32_MAX - brick_points[row] ? UINT32_MAX : score + brick_points[row];
            if (previous_y + BALL_RADIUS <= top || previous_y - BALL_RADIUS >= bottom) ball_velocity_y = -ball_velocity_y;
            else if (previous_x + BALL_RADIUS <= left || previous_x - BALL_RADIUS >= right) ball_velocity_x = -ball_velocity_x;
            else ball_velocity_y = -ball_velocity_y;
            audioPlay(AUDIO_SFX_HIT);
            return true;
        }
    }
    return false;
}

static void simulateStep(uint32_t now_ms)
{
    if (ball_attached) return;
    const float previous_x = ball_x;
    const float previous_y = ball_y;
    ball_x += ball_velocity_x * (float)STEP_MS / 1000.0f;
    ball_y += ball_velocity_y * (float)STEP_MS / 1000.0f;
    bool bounced = false;
    if (ball_x - BALL_RADIUS < 1.0f) {
        ball_x = 1.0f + BALL_RADIUS;
        ball_velocity_x = -ball_velocity_x;
        bounced = true;
    } else if (ball_x + BALL_RADIUS >= (float)(LCD_WIDTH - 1)) {
        ball_x = (float)(LCD_WIDTH - 1 - BALL_RADIUS);
        ball_velocity_x = -ball_velocity_x;
        bounced = true;
    }
    if (ball_y - BALL_RADIUS < 14.0f) {
        ball_y = 14.0f + BALL_RADIUS;
        ball_velocity_y = -ball_velocity_y;
        bounced = true;
    }
    if (bounced) audioPlay(AUDIO_SFX_BOUNCE);
    (void)hitBrick(previous_x, previous_y);
    if (ball_velocity_y > 0.0f && ball_y + BALL_RADIUS >= (float)PADDLE_Y &&
        ball_y - BALL_RADIUS <= (float)(PADDLE_Y + PADDLE_HEIGHT) &&
        ball_x + BALL_RADIUS >= paddle_x && ball_x - BALL_RADIUS <= paddle_x + PADDLE_WIDTH) {
        const float relative = (ball_x - (paddle_x + (float)PADDLE_WIDTH * 0.5f)) / ((float)PADDLE_WIDTH * 0.5f);
        const float speed = 124.0f + (float)(wave > 8U ? 8U : wave) * 6.0f;
        ball_y = (float)(PADDLE_Y - BALL_RADIUS);
        ball_velocity_x = relative * speed;
        ball_velocity_y = -(speed - (relative < 0.0f ? -relative : relative) * speed * 0.25f);
        audioPlay(AUDIO_SFX_BOUNCE);
    }
    if (ball_y - BALL_RADIUS >= LCD_HEIGHT) {
        if (lives == 0U) return;
        --lives;
        if (lives == 0U) {
            finishGame(now_ms);
            return;
        }
        audioPlay(AUDIO_SFX_LIFE_LOST);
        state = BREAKOUT_LIFE_LOST;
        state_started = now_ms;
        ball_attached = true;
        return;
    }
    if (remainingBricks() == 0U) {
        audioPlay(AUDIO_SFX_WAVE_CLEAR);
        if (wave < UINT32_MAX) ++wave;
        beginWave();
    }
}

void startBreakout(uint32_t now_ms)
{
    running = true;
    state = BREAKOUT_READY;
    last_update = now_ms;
    accumulator = 0U;
    score = 0U;
    wave = 1U;
    lives = 3U;
    beginWave();
    controls_ready = false;
    auto_mode = false;
    ai_speed = AI_SPEED_DEFAULT_INDEX;
    auto_target_x = paddle_x;
    modeSelectionReset(&mode_input);
    ESP_LOGI("breakout", "START");
}

void stopBreakout(void)
{
    cancelHighScoreEntry();
    running = false;
    ESP_LOGI("breakout", "STOP");
}

void breakoutAction(bool action, bool speed, uint32_t now_ms)
{
    if (!running) return;
    if (state == BREAKOUT_READY) {
        if (action) beginGame(now_ms);
        return;
    }
    if (auto_mode) {
        if (speed) {
            cycleAISpeed(&ai_speed);
            ai_next_decision = now_ms;
        }
        if (action) {
            auto_mode = false;
            modeSelectionReset(&mode_input);
            state = BREAKOUT_READY;
        }
        return;
    }
    if (action && state == BREAKOUT_PLAYING && ball_attached) launchBall();
}

void updateBreakout(int x_offset, int y_offset, bool joystick_valid, bool left, bool right,
                    uint32_t now_ms)
{
    if (!running) return;
    const uint32_t elapsed = now_ms - last_update;
    last_update = now_ms;
    if (state == BREAKOUT_FINISHED) {
        if (auto_mode) {
            if (now_ms - auto_restart_started >= aiRestartDelay()) beginGame(now_ms);
        } else if (now_ms - state_started >= 1000U) {
            startHighScoreEntry(HIGH_SCORE_BREAKOUT, score, now_ms);
            state = BREAKOUT_SCORES;
        }
        return;
    }
    if (state == BREAKOUT_SCORES) {
        if (updateHighScoreEntry(x_offset, y_offset, joystick_valid, now_ms)) state = BREAKOUT_READY;
        return;
    }
    if (state == BREAKOUT_LIFE_LOST) {
        if (now_ms - state_started >= LIFE_LOST_MS) {
            attachBall();
            ai_next_decision = now_ms + aiSpeedDelay(ai_speed);
            state = BREAKOUT_PLAYING;
        }
        return;
    }
    if (state == BREAKOUT_READY) {
        if (modeSelectionNext(&mode_input, x_offset, y_offset, joystick_valid, left, right,
                              now_ms)) {
            auto_mode = !auto_mode;
        }
        return;
    }
    if (state != BREAKOUT_PLAYING) return;
    if (!joystick_valid) controls_ready = false;
    else if (x_offset >= -350 && x_offset <= 350) controls_ready = true;
    const float delta_seconds = (float)(elapsed > 50U ? 50U : elapsed) / 1000.0f;
    if (auto_mode) {
        if (now_ms >= ai_next_decision) {
            auto_target_x = clampValue(ball_x - (float)PADDLE_WIDTH * 0.5f, 1.0f,
                                       (float)(LCD_WIDTH - 1 - PADDLE_WIDTH));
            ai_next_decision = now_ms + aiSpeedDelay(ai_speed);
            if (ball_attached) launchBall();
        }
        const float distance = auto_target_x - paddle_x;
        const float maximum_step = 205.0f * delta_seconds;
        const float step = clampValue(distance, -maximum_step, maximum_step);
        paddle_x = clampValue(paddle_x + step, 1.0f, (float)(LCD_WIDTH - 1 - PADDLE_WIDTH));
    } else {
        const int direction = left == right ? 0 : left ? -1 : 1;
        const float joystick_speed = joystick_valid && controls_ready && !left && !right
            ? clampValue((float)x_offset / 1600.0f, -1.0f, 1.0f) * 205.0f : 0.0f;
        const float button_speed = (float)direction * 205.0f;
        paddle_x = clampValue(paddle_x + (left || right ? button_speed : joystick_speed) *
                              delta_seconds, 1.0f,
                              (float)(LCD_WIDTH - 1 - PADDLE_WIDTH));
    }
    if (ball_attached) {
        ball_x = paddle_x + (float)PADDLE_WIDTH * 0.5f;
        return;
    }
    accumulator += elapsed > 100U ? 100U : elapsed;
    while (accumulator >= STEP_MS && state == BREAKOUT_PLAYING) {
        accumulator -= STEP_MS;
        simulateStep(now_ms);
    }
}

void drawBreakout(uint16_t *frame)
{
    if (!running || frame == NULL) return;
    if (state == BREAKOUT_SCORES) {
        drawHighScoreEntry(frame);
        return;
    }
    clear(frame, COLOR_BLACK);
    char text[40];
    if (state == BREAKOUT_READY) {
        drawPet2001TextCentredTransparent(frame, 160, 3, "BREAKOUT", COLOR_CYAN, 1);
    } else {
        snprintf(text, sizeof(text), "SCORE %u", (unsigned)score);
        drawPet2001TextTransparent(frame, 4, 3, text, COLOR_WHITE, 1);
        snprintf(text, sizeof(text), "LIVES %u", (unsigned)lives);
        drawPet2001TextCentredTransparent(frame, 160, 3, text, COLOR_CYAN, 1);
        snprintf(text, sizeof(text), "WAVE %u", (unsigned)wave);
        drawPet2001TextTransparent(frame, 222, 3, text, COLOR_YELLOW, 1);
    }
    draw_line(frame, 0, 13, LCD_WIDTH - 1, 13, COLOR_DARKGRAY);
    for (unsigned row = 0U; row < BRICK_ROWS; ++row) {
        for (unsigned column = 0U; column < BRICK_COLUMNS; ++column) {
            if (!bricks[row][column]) continue;
            const int x = BRICK_LEFT + (int)column * (BRICK_WIDTH + BRICK_GAP_X);
            const int y = BRICK_TOP + (int)row * (BRICK_HEIGHT + BRICK_GAP_Y);
            fill_rect(frame, x, y, BRICK_WIDTH, BRICK_HEIGHT, brick_colours[row]);
            draw_line(frame, x, y, x + BRICK_WIDTH - 1, y, COLOR_WHITE);
        }
    }
    if (state != BREAKOUT_READY) {
        fill_rect(frame, (int)paddle_x, PADDLE_Y, PADDLE_WIDTH, PADDLE_HEIGHT, COLOR_LIGHTGREEN);
        fill_circle(frame, (int)ball_x, (int)ball_y, BALL_RADIUS, COLOR_WHITE);
    }
    if (state == BREAKOUT_READY) {
        drawPet2001TextCentredTransparent(frame, 100, 97, "PLAY", auto_mode ? COLOR_LIGHTGRAY : COLOR_YELLOW, 2);
        drawPet2001TextCentredTransparent(frame, 220, 97, "AUTO", auto_mode ? COLOR_YELLOW : COLOR_LIGHTGRAY, 2);
        drawPet2001TextTransparent(frame, 49, 121, "RED 60", COLOR_RED, 1);
        drawPet2001TextTransparent(frame, 124, 121, "ORANGE 50", COLOR_ORANGE, 1);
        drawPet2001TextTransparent(frame, 218, 121, "YELLOW 40", COLOR_YELLOW, 1);
        drawPet2001TextTransparent(frame, 52, 132, "LIME 30", COLOR_LIME, 1);
        drawPet2001TextTransparent(frame, 132, 132, "CYAN 20", COLOR_CYAN, 1);
        drawPet2001TextTransparent(frame, 212, 132, "PURPLE 10", COLOR_MEDIUMPURPLE, 1);
        drawPet2001TextCentredTransparent(frame, 160, 153, "D/B CHANGE  A/K START", COLOR_YELLOW, 1);
    } else if (state == BREAKOUT_LIFE_LOST) {
        drawPet2001TextCentredTransparent(frame, 160, 89, "BALL LOST", COLOR_RED, 2);
    } else if (state == BREAKOUT_FINISHED) {
        drawPet2001TextCentredTransparent(frame, 160, 89, "GAME OVER", COLOR_RED, 2);
        if (auto_mode) {
            const uint32_t elapsed = last_update - auto_restart_started;
            const uint32_t delay = aiRestartDelay();
            const unsigned seconds = elapsed >= delay ? 0U : (unsigned)((delay - elapsed + 999U) / 1000U);
            snprintf(text, sizeof(text), "NEXT %u", seconds);
            drawPet2001TextCentredTransparent(frame, 160, 114, text, COLOR_YELLOW, 1);
        }
    } else if (auto_mode) {
        snprintf(text, sizeof(text), "AUTO %s  E SPEED  A/K STOP", aiSpeedShortText(ai_speed));
        drawPet2001TextCentredTransparent(frame, 160, 136, text, COLOR_LIGHTGRAY, 1);
    } else if (ball_attached) {
        drawPet2001TextCentredTransparent(frame, 160, 136, "A/K LAUNCH", COLOR_LIGHTGRAY, 1);
    }
}