Galaga

Source: src/galaga.c · 368 lines · 19 functions.

Screen captures

Views of this part running on the Mini Arcade.

Galaga enemy formation on the Mini Arcade
Galaga showing an active enemy formation.
Galaga gameplay on the Mini Arcade
Galaga during a later moment of play.

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

clampf() · source line 65
static float clampf(float value, float low, float high)
  1. Exit this functionreturn value < low ? low : value > high ? high : value;
Read original function source
static float clampf(float value, float low, float high)
{
    return value < low ? low : value > high ? high : value;
}
enemyColour() · source line 70
static uint16_t enemyColour(EnemyType type, EnemyMotion motion)
  1. Decisionmotion == DIVING
    True
      1. Exit this functionreturn type == BOSS ? COLOR_RED : type == BUTTERFLY ? COLOR_LIME : COLOR_ORANGE;
    False

    Continue after this decision.

  2. Exit this functionreturn type == BOSS ? COLOR_MAGENTA : type == BUTTERFLY ? COLOR_CYAN : COLOR_YELLOW;
Read original function source
static uint16_t enemyColour(EnemyType type, EnemyMotion motion)
{
    if (motion == DIVING) {
        return type == BOSS ? COLOR_RED : type == BUTTERFLY ? COLOR_LIME : COLOR_ORANGE;
    }
    return type == BOSS ? COLOR_MAGENTA : type == BUTTERFLY ? COLOR_CYAN : COLOR_YELLOW;
}
enemyBitmap() · source line 78
static const uint8_t *enemyBitmap(EnemyType type)
  1. Exit this functionreturn type == BOSS ? galaga_boss_bitmap : type == BUTTERFLY ? galaga_butterfly_bitmap : galaga_bee_bitmap;
Read original function source
static const uint8_t *enemyBitmap(EnemyType type)
{
    return type == BOSS ? galaga_boss_bitmap : type == BUTTERFLY ? galaga_butterfly_bitmap : galaga_bee_bitmap;
}
enemyPoints() · source line 83
static unsigned enemyPoints(EnemyType type, bool diving)
  1. Executeconst unsigned base = type == BOSS ? 100U : type == BUTTERFLY ? 80U : 50U;
  2. Exit this functionreturn diving ? base * 2U : base;
Read original function source
static unsigned enemyPoints(EnemyType type, bool diving)
{
    const unsigned base = type == BOSS ? 100U : type == BUTTERFLY ? 80U : 50U;
    return diving ? base * 2U : base;
}
addScore() · source line 89
static void addScore(unsigned points)
  1. Executescore = score > UINT32_MAX - points ? UINT32_MAX : score + points;
Read original function source
static void addScore(unsigned points)
{
    score = score > UINT32_MAX - points ? UINT32_MAX : score + points;
}
prepareWave() · source line 94
static void prepareWave(void)
  1. Executechallenge_stage = wave > 0 && wave % 3U == 0;
  2. Executememset(player_shots, 0, sizeof(player_shots));
  3. Executememset(enemy_shots, 0, sizeof(enemy_shots));
  4. Executeunsigned index = 0;
  5. FOR loopunsigned row = 0; row < sizeof(formation_count); ++row

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

      1. Executeconst unsigned count = formation_count[row];
      2. Executeconst float spacing = 22.0f;
      3. Executeconst float first_x = (LCD_WIDTH - (16.0f + (count - 1U) * spacing)) / 2.0f;
      4. FOR loopunsigned column = 0; column < count; ++column

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

          1. ExecuteEnemyType type = row == 0 ? BOSS : row < 3 ? BUTTERFLY : BEE;
          2. Executeconst float home_x = first_x + column * spacing;
          3. Executeconst float home_y = 29.0f + row * 18.0f;
          4. Executeconst unsigned group = index / 4U;
          5. Executeenemies[index] = (Enemy){home_x, home_y, 0.0f, -16.0f, -(float)group * 0.18f, type, ENTERING, true};
          6. Execute++index;
  6. Executedive_delay = 1000U;
  7. Executeenemy_fire_delay = 800U;
  8. Executebanner_until = last_update + 1200U;
Read original function source
static void prepareWave(void)
{
    challenge_stage = wave > 0 && wave % 3U == 0;
    memset(player_shots, 0, sizeof(player_shots));
    memset(enemy_shots, 0, sizeof(enemy_shots));
    unsigned index = 0;
    for (unsigned row = 0; row < sizeof(formation_count); ++row) {
        const unsigned count = formation_count[row];
        const float spacing = 22.0f;
        const float first_x = (LCD_WIDTH - (16.0f + (count - 1U) * spacing)) / 2.0f;
        for (unsigned column = 0; column < count; ++column) {
        EnemyType type = row == 0 ? BOSS : row < 3 ? BUTTERFLY : BEE;
        const float home_x = first_x + column * spacing;
        const float home_y = 29.0f + row * 18.0f;
        // Galaga enemies arrive in staggered groups along long, curved paths
        // before locking into their assigned formation position.
        const unsigned group = index / 4U;
        enemies[index] = (Enemy){home_x, home_y, 0.0f, -16.0f,
                                 -(float)group * 0.18f, type, ENTERING, true};
        ++index;
        }
    }
    dive_delay = 1000U;
    enemy_fire_delay = 800U;
    banner_until = last_update + 1200U;
}
beginGame() · source line 121
static void beginGame(uint32_t now_ms)

Calls into this documentation:

  1. Executescore = 0;
  2. Executelives = 3;
  3. Executewave = 1;
  4. Executecontrols_ready = false;
  5. Executeplayer = (Object){152.0f, PLAYER_Y, 0.0f, 0.0f, true};
  6. Executefire_delay = 0;
  7. Executeaccumulator = 0;
  8. Executelast_update = now_ms;
  9. ExecuteprepareWave();
  10. Executestate = PLAYING;
Read original function source
static void beginGame(uint32_t now_ms)
{
    score = 0; lives = 3; wave = 1; controls_ready = false;
    player = (Object){152.0f, PLAYER_Y, 0.0f, 0.0f, true};
    fire_delay = 0; accumulator = 0; last_update = now_ms;
    prepareWave();
    state = PLAYING;
}
firePlayerShot() · source line 130
static void firePlayerShot(void)

Calls into this documentation:

  1. Decision!player.active || fire_delay != 0
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. FOR loopunsigned i = 0; i < MAX_PLAYER_SHOTS; ++i

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

    1. Decision!player_shots[i].active
      True
        1. Executeplayer_shots[i] = (Object){player.x + PLAYER_W / 2.0f - 1.0f, PLAYER_Y - 2.0f, 0.0f, -245.0f, true};
        2. Executefire_delay = FIRE_DELAY_MS;
        3. ExecuteaudioPlay(AUDIO_SFX_PLAYER_SHOT);
        4. Exit this functionreturn;
      False

      Continue after this decision.

Read original function source
static void firePlayerShot(void)
{
    if (!player.active || fire_delay != 0) return;
    for (unsigned i = 0; i < MAX_PLAYER_SHOTS; ++i) if (!player_shots[i].active) {
        player_shots[i] = (Object){player.x + PLAYER_W / 2.0f - 1.0f, PLAYER_Y - 2.0f, 0.0f, -245.0f, true};
        fire_delay = FIRE_DELAY_MS;
        audioPlay(AUDIO_SFX_PLAYER_SHOT);
        return;
    }
}
fireEnemyShot() · source line 141
static void fireEnemyShot(void)

Calls into this documentation:

  1. Decisionenemy_fire_delay != 0 || challenge_stage
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. FOR loopunsigned attempts = 0; attempts < MAX_ENEMIES; ++attempts

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

      1. ExecuteEnemy *enemy = &enemies[esp_random() % MAX_ENEMIES];
      2. Decision!enemy->active || enemy->motion != FORMATION
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      3. FOR loopunsigned i = 0; i < MAX_ENEMY_SHOTS; ++i

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

        1. Decision!enemy_shots[i].active
          True
            1. Executeconst float dx = player.x + PLAYER_W / 2.0f - enemy->x;
            2. Executeenemy_shots[i] = (Object){enemy->x + 7.0f, enemy->y + 13.0f, clampf(dx * 0.45f, -80.0f, 80.0f), 115.0f + wave * 5.0f, true};
            3. Executeenemy_fire_delay = ENEMY_FIRE_MS > wave * 45U ? ENEMY_FIRE_MS - wave * 45U : 260U;
            4. ExecuteaudioPlay(AUDIO_SFX_ENEMY_SHOT);
            5. Exit this functionreturn;
          False

          Continue after this decision.

Read original function source
static void fireEnemyShot(void)
{
    if (enemy_fire_delay != 0 || challenge_stage) return;
    for (unsigned attempts = 0; attempts < MAX_ENEMIES; ++attempts) {
        Enemy *enemy = &enemies[esp_random() % MAX_ENEMIES];
        if (!enemy->active || enemy->motion != FORMATION) continue;
        for (unsigned i = 0; i < MAX_ENEMY_SHOTS; ++i) if (!enemy_shots[i].active) {
            const float dx = player.x + PLAYER_W / 2.0f - enemy->x;
            enemy_shots[i] = (Object){enemy->x + 7.0f, enemy->y + 13.0f, clampf(dx * 0.45f, -80.0f, 80.0f), 115.0f + wave * 5.0f, true};
            enemy_fire_delay = ENEMY_FIRE_MS > wave * 45U ? ENEMY_FIRE_MS - wave * 45U : 260U;
            audioPlay(AUDIO_SFX_ENEMY_SHOT);
            return;
        }
    }
}
startDive() · source line 157
static void startDive(void)
  1. Decisionchallenge_stage || dive_delay != 0
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. FOR loopunsigned attempts = 0; attempts < MAX_ENEMIES; ++attempts

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

      1. ExecuteEnemy *enemy = &enemies[esp_random() % MAX_ENEMIES];
      2. Decision!enemy->active || enemy->motion != FORMATION
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      3. Executeenemy->motion = DIVING;
      4. Executeenemy->dive_phase = 0.0f;
      5. Executedive_delay = DIVE_DELAY_MS > wave * 55U ? DIVE_DELAY_MS - wave * 55U : 360U;
      6. Exit this functionreturn;
Read original function source
static void startDive(void)
{
    if (challenge_stage || dive_delay != 0) return;
    for (unsigned attempts = 0; attempts < MAX_ENEMIES; ++attempts) {
        Enemy *enemy = &enemies[esp_random() % MAX_ENEMIES];
        if (!enemy->active || enemy->motion != FORMATION) continue;
        enemy->motion = DIVING;
        enemy->dive_phase = 0.0f;
        dive_delay = DIVE_DELAY_MS > wave * 55U ? DIVE_DELAY_MS - wave * 55U : 360U;
        return;
    }
}
hitPlayer() · source line 170
static void hitPlayer(void)

Calls into this documentation:

  1. Decision!player.active
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. ExecuteaudioPlay(AUDIO_SFX_PLAYER_EXPLOSION);
  3. Decision--lives == 0
    True
      1. Executeplayer.active = false;
      2. Executestate = FINISHED;
      3. Executestate_started = last_update;
      4. Exit this functionreturn;
    False

    Continue after this decision.

  4. Executeplayer.active = false;
  5. Executestate_started = last_update;
  6. Executememset(player_shots, 0, sizeof(player_shots));
  7. Executememset(enemy_shots, 0, sizeof(enemy_shots));
Read original function source
static void hitPlayer(void)
{
    if (!player.active) return;
    audioPlay(AUDIO_SFX_PLAYER_EXPLOSION);
    if (--lives == 0) {
        player.active = false;
        state = FINISHED;
        state_started = last_update;
        return;
    }
    player.active = false;
    state_started = last_update;
    memset(player_shots, 0, sizeof(player_shots));
    memset(enemy_shots, 0, sizeof(enemy_shots));
}
enemiesRemaining() · source line 186
static unsigned enemiesRemaining(void)
  1. Executeunsigned count = 0;
  2. FOR loopunsigned i = 0; i < MAX_ENEMIES; ++i

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

    1. Decisionenemies[i].active
      True
      1. Execute++count;
      False

      Continue after this decision.

  3. Exit this functionreturn count;
Read original function source
static unsigned enemiesRemaining(void)
{
    unsigned count = 0;
    for (unsigned i = 0; i < MAX_ENEMIES; ++i) if (enemies[i].active) ++count;
    return count;
}
updateEnemies() · source line 193
static void updateEnemies(void)

Calls into this documentation:

  1. FOR loopunsigned i = 0; i < MAX_ENEMIES; ++i

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

      1. ExecuteEnemy *enemy = &enemies[i];
      2. Decision!enemy->active
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      3. Decisionenemy->motion == ENTERING
        True
          1. Executeenemy->dive_phase += STEP_MS / 1350.0f;
          2. Decisionenemy->dive_phase < 0.0f
            True
            1. Next loop iterationcontinue;
            False

            Continue after this decision.

          3. Executeconst float t = enemy->dive_phase;
          4. Decisiont >= 1.0f
            True
              1. Executeenemy->x = enemy->home_x;
              2. Executeenemy->y = enemy->home_y;
              3. Executeenemy->motion = FORMATION;
              4. Next loop iterationcontinue;
            False

            Continue after this decision.

          5. Executeconst float curve = sinf(PI * t);
          6. Executeconst float start_x = ((unsigned)(enemy - enemies) / 4U & 1U) == 0U ? -18.0f : (float)LCD_WIDTH + 2.0f;
          7. Executeconst float direction = start_x < 0.0f ? 1.0f : -1.0f;
          8. Executeenemy->x = start_x + (enemy->home_x - start_x) * t + direction * curve * 42.0f;
          9. Executeenemy->y = -16.0f + (enemy->home_y + 16.0f) * t + curve * 36.0f;
          10. Next loop iterationcontinue;
        False

        Continue after this decision.

      4. Decisionenemy->motion != DIVING
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      5. Executeenemy->dive_phase += STEP_MS / 1800.0f;
      6. Decisionenemy->dive_phase >= 1.0f
        True
          1. Executeenemy->motion = FORMATION;
          2. Executeenemy->x = enemy->home_x;
          3. Executeenemy->y = enemy->home_y;
          4. Next loop iterationcontinue;
        False

        Continue after this decision.

      7. Executeconst float curve = sinf(PI * enemy->dive_phase);
      8. Executeconst float direction = ((unsigned)(enemy - enemies) & 1U) == 0U ? -1.0f : 1.0f;
      9. Executeconst float depth = (float)(PLAYER_Y - 16) - enemy->home_y;
      10. Executeenemy->x = enemy->home_x + direction * (curve * 88.0f + sinf(PI * enemy->dive_phase * 2.0f) * 28.0f);
      11. Executeenemy->y = enemy->home_y + curve * depth;
      12. Decisionplayer.active && fabsf(enemy->x + 8.0f - (player.x + PLAYER_W / 2.0f)) < 12.0f && fabsf(enemy->y + 8.0f - (PLAYER_Y + PLAYER_H / 2.0f)) < 10.0f
        True
        1. ExecutehitPlayer();
        False

        Continue after this decision.

Read original function source
static void updateEnemies(void)
{
    for (unsigned i = 0; i < MAX_ENEMIES; ++i) {
        Enemy *enemy = &enemies[i];
        if (!enemy->active) continue;
        if (enemy->motion == ENTERING) {
            enemy->dive_phase += STEP_MS / 1350.0f;
            if (enemy->dive_phase < 0.0f) continue;
            const float t = enemy->dive_phase;
            if (t >= 1.0f) {
                enemy->x = enemy->home_x;
                enemy->y = enemy->home_y;
                enemy->motion = FORMATION;
                continue;
            }
            const float curve = sinf(PI * t);
            const float start_x = ((unsigned)(enemy - enemies) / 4U & 1U) == 0U ? -18.0f : (float)LCD_WIDTH + 2.0f;
            const float direction = start_x < 0.0f ? 1.0f : -1.0f;
            enemy->x = start_x + (enemy->home_x - start_x) * t + direction * curve * 42.0f;
            enemy->y = -16.0f + (enemy->home_y + 16.0f) * t + curve * 36.0f;
            continue;
        }
        if (enemy->motion != DIVING) continue;
        enemy->dive_phase += STEP_MS / 1800.0f;
        if (enemy->dive_phase >= 1.0f) {
            enemy->motion = FORMATION;
            enemy->x = enemy->home_x;
            enemy->y = enemy->home_y;
            continue;
        }
        const float curve = sinf(PI * enemy->dive_phase);
        const float direction = ((unsigned)(enemy - enemies) & 1U) == 0U ? -1.0f : 1.0f;
        const float depth = (float)(PLAYER_Y - 16) - enemy->home_y;
        enemy->x = enemy->home_x + direction * (curve * 88.0f + sinf(PI * enemy->dive_phase * 2.0f) * 28.0f);
        enemy->y = enemy->home_y + curve * depth;
        if (player.active && fabsf(enemy->x + 8.0f - (player.x + PLAYER_W / 2.0f)) < 12.0f &&
                   fabsf(enemy->y + 8.0f - (PLAYER_Y + PLAYER_H / 2.0f)) < 10.0f) hitPlayer();
    }
}
stepGame() · source line 233
static void stepGame(float input_x, bool fire)

Calls into this documentation:

  1. Executefire_delay = fire_delay > STEP_MS ? fire_delay - STEP_MS : 0;
  2. Executeenemy_fire_delay = enemy_fire_delay > STEP_MS ? enemy_fire_delay - STEP_MS : 0;
  3. Executedive_delay = dive_delay > STEP_MS ? dive_delay - STEP_MS : 0;
  4. Decision!player.active
    True
      1. Decisionlast_update - state_started >= RESPAWN_MS
        True
        1. Executeplayer = (Object){152.0f, PLAYER_Y, 0.0f, 0.0f, true};
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  5. Executeplayer.vx += input_x * 1000.0f * STEP_MS / 1000.0f;
  6. Executeplayer.vx *= 0.92f;
  7. Executeplayer.vx = clampf(player.vx, -140.0f, 140.0f);
  8. Executeplayer.x = clampf(player.x + player.vx * STEP_MS / 1000.0f, 4.0f, LCD_WIDTH - PLAYER_W - 4.0f);
  9. Decisionfire
    True
    1. ExecutefirePlayerShot();
    False

    Continue after this decision.

  10. ExecutestartDive();
  11. ExecutefireEnemyShot();
  12. ExecuteupdateEnemies();
  13. FOR loopunsigned s = 0; s < MAX_PLAYER_SHOTS; ++s

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

      1. ExecuteObject *shot = &player_shots[s];
      2. Decision!shot->active
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      3. Executeshot->y += shot->vy * STEP_MS / 1000.0f;
      4. Decisionshot->y < PLAY_TOP
        True
          1. Executeshot->active = false;
          2. Next loop iterationcontinue;
        False

        Continue after this decision.

      5. FOR loopunsigned e = 0; e < MAX_ENEMIES; ++e

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

          1. ExecuteEnemy *enemy = &enemies[e];
          2. Decision!enemy->active || fabsf(shot->x - (enemy->x + 7.0f)) >= 9.0f || fabsf(shot->y - (enemy->y + 7.0f)) >= 10.0f
            True
            1. Next loop iterationcontinue;
            False

            Continue after this decision.

          3. ExecuteaddScore(enemyPoints(enemy->type, enemy->motion == DIVING));
          4. Executeenemy->active = false;
          5. Executeshot->active = false;
          6. ExecuteaudioPlay(AUDIO_SFX_HIT);
          7. Exit enclosing loop or switchbreak;
  14. FOR loopunsigned s = 0; s < MAX_ENEMY_SHOTS; ++s

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

      1. ExecuteObject *shot = &enemy_shots[s];
      2. Decision!shot->active
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      3. Executeshot->x += shot->vx * STEP_MS / 1000.0f;
      4. Executeshot->y += shot->vy * STEP_MS / 1000.0f;
      5. Decisionshot->y > LCD_HEIGHT
        True
          1. Executeshot->active = false;
          2. Next loop iterationcontinue;
        False

        Continue after this decision.

      6. Decisionfabsf(shot->x - (player.x + PLAYER_W / 2.0f)) < 10.0f && fabsf(shot->y - (PLAYER_Y + 5.0f)) < 9.0f
        True
          1. Executeshot->active = false;
          2. ExecutehitPlayer();
        False

        Continue after this decision.

  15. DecisionenemiesRemaining() == 0
    True
      1. ExecuteaudioPlay(AUDIO_SFX_WAVE_CLEAR);
      2. Execute++wave;
      3. ExecuteprepareWave();
    False

    Continue after this decision.

Read original function source
static void stepGame(float input_x, bool fire)
{
    fire_delay = fire_delay > STEP_MS ? fire_delay - STEP_MS : 0;
    enemy_fire_delay = enemy_fire_delay > STEP_MS ? enemy_fire_delay - STEP_MS : 0;
    dive_delay = dive_delay > STEP_MS ? dive_delay - STEP_MS : 0;
    if (!player.active) {
        if (last_update - state_started >= RESPAWN_MS) player = (Object){152.0f, PLAYER_Y, 0.0f, 0.0f, true};
        return;
    }
    player.vx += input_x * 1000.0f * STEP_MS / 1000.0f;
    player.vx *= 0.92f;
    player.vx = clampf(player.vx, -140.0f, 140.0f);
    player.x = clampf(player.x + player.vx * STEP_MS / 1000.0f, 4.0f, LCD_WIDTH - PLAYER_W - 4.0f);
    if (fire) firePlayerShot();
    startDive();
    fireEnemyShot();
    updateEnemies();

    for (unsigned s = 0; s < MAX_PLAYER_SHOTS; ++s) {
        Object *shot = &player_shots[s];
        if (!shot->active) continue;
        shot->y += shot->vy * STEP_MS / 1000.0f;
        if (shot->y < PLAY_TOP) { shot->active = false; continue; }
        for (unsigned e = 0; e < MAX_ENEMIES; ++e) {
            Enemy *enemy = &enemies[e];
            if (!enemy->active || fabsf(shot->x - (enemy->x + 7.0f)) >= 9.0f || fabsf(shot->y - (enemy->y + 7.0f)) >= 10.0f) continue;
            addScore(enemyPoints(enemy->type, enemy->motion == DIVING));
            enemy->active = false;
            shot->active = false;
            audioPlay(AUDIO_SFX_HIT);
            break;
        }
    }
    for (unsigned s = 0; s < MAX_ENEMY_SHOTS; ++s) {
        Object *shot = &enemy_shots[s];
        if (!shot->active) continue;
        shot->x += shot->vx * STEP_MS / 1000.0f;
        shot->y += shot->vy * STEP_MS / 1000.0f;
        if (shot->y > LCD_HEIGHT) { shot->active = false; continue; }
        if (fabsf(shot->x - (player.x + PLAYER_W / 2.0f)) < 10.0f && fabsf(shot->y - (PLAYER_Y + 5.0f)) < 9.0f) {
            shot->active = false;
            hitPlayer();
        }
    }
    if (enemiesRemaining() == 0) {
        audioPlay(AUDIO_SFX_WAVE_CLEAR);
        ++wave;
        prepareWave();
    }
}
startGalaga() · source line 284
void startGalaga(uint32_t now_ms)

Calls into this documentation:

  1. Executehigh_score_table_t scores;
  2. Executehigh_score = high_scores_load(HIGH_SCORE_GALAGA, &scores) == HIGH_SCORES_OK && scores.count != 0 ? scores.entries[0].score : 0;
  3. Executestarfield_initialize(&stars, 80, 0, 0, LCD_WIDTH, LCD_HEIGHT, STARFIELD_COLOUR);
  4. Executerunning = true;
  5. ExecutebeginGame(now_ms);
  6. Executestate = READY;
Read original function source
void startGalaga(uint32_t now_ms)
{
    high_score_table_t scores;
    high_score = high_scores_load(HIGH_SCORE_GALAGA, &scores) == HIGH_SCORES_OK && scores.count != 0 ? scores.entries[0].score : 0;
    starfield_initialize(&stars, 80, 0, 0, LCD_WIDTH, LCD_HEIGHT, STARFIELD_COLOUR);
    running = true;
    beginGame(now_ms);
    state = READY;
}
stopGalaga() · source line 293
void stopGalaga(void)

Calls into this documentation:

  1. ExecutecancelHighScoreEntry();
  2. Executerunning = false;
Read original function source
void stopGalaga(void) { cancelHighScoreEntry(); running = false; }
galagaAction() · source line 294
void galagaAction(bool start, uint32_t now_ms)

Calls into this documentation:

  1. Decisionrunning && start && state == READY
    True
      1. ExecutebeginGame(now_ms);
      2. ExecuteaudioPlay(AUDIO_SFX_GAME_START);
    False

    Continue after this decision.

Read original function source
void galagaAction(bool start, uint32_t now_ms)
{
    if (running && start && state == READY) {
        beginGame(now_ms);
        audioPlay(AUDIO_SFX_GAME_START);
    }
}
updateGalaga() · source line 302
void updateGalaga(int x_offset, int y_offset, bool joystick_valid, bool fire, 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 == FINISHED && now_ms - state_started >= 1000U
    True
      1. ExecutestartHighScoreEntry(HIGH_SCORE_GALAGA, score, now_ms);
      2. Executestate = SCORES;
    False

    Continue after this decision.

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

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  6. Executestarfield_update(&stars, elapsed);
  7. Decisionstate != PLAYING
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  8. 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.

  9. Executefloat input_x = joystick_valid && controls_ready ? clampf((float)x_offset / 1500.0f, -1.0f, 1.0f) : 0.0f;
  10. Executeaccumulator += elapsed > 100U ? 100U : elapsed;
  11. WHILE loopaccumulator >= STEP_MS && state == PLAYING

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

      1. Executeaccumulator -= STEP_MS;
      2. ExecutestepGame(input_x, fire);
Read original function source
void updateGalaga(int x_offset, int y_offset, bool joystick_valid, bool fire, uint32_t now_ms)
{
    if (!running) return;
    const uint32_t elapsed = now_ms - last_update;
    last_update = now_ms;
    if (state == FINISHED && now_ms - state_started >= 1000U) {
        startHighScoreEntry(HIGH_SCORE_GALAGA, score, now_ms);
        state = SCORES;
    }
    if (state == SCORES) { if (updateHighScoreEntry(x_offset, y_offset, joystick_valid, now_ms)) state = READY; return; }
    starfield_update(&stars, elapsed);
    if (state != PLAYING) return;
    if (!joystick_valid) controls_ready = false;
    else if (x_offset >= -350 && x_offset <= 350) controls_ready = true;
    float input_x = joystick_valid && controls_ready ? clampf((float)x_offset / 1500.0f, -1.0f, 1.0f) : 0.0f;
    accumulator += elapsed > 100U ? 100U : elapsed;
    while (accumulator >= STEP_MS && state == PLAYING) { accumulator -= STEP_MS; stepGame(input_x, fire); }
}
drawGalaga() · source line 321
void drawGalaga(uint16_t *frame)

Calls into this documentation:

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

    Continue after this decision.

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

    Continue after this decision.

  3. Executeclear(frame, COLOR_BLACK);
  4. Executestarfield_draw(frame, &stars);
  5. FOR loopunsigned i = 0; i < MAX_ENEMIES; ++i

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

    1. Decisionenemies[i].active
      True
        1. Executeconst Enemy *enemy = &enemies[i];
        2. Executedraw_bitmap_scaled(frame, (int)enemy->x, (int)enemy->y, enemyBitmap(enemy->type), 8, 8, 16, 14, enemyColour(enemy->type, enemy->motion));
      False

      Continue after this decision.

  6. FOR loopunsigned i = 0; i < MAX_PLAYER_SHOTS; ++i

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

    1. Decisionplayer_shots[i].active
      True
        1. Executefill_rect(frame, (int)player_shots[i].x, (int)player_shots[i].y, 2, 8, COLOR_WHITE);
        2. Executefill_rect(frame, (int)player_shots[i].x - 1, (int)player_shots[i].y + 2, 4, 3, COLOR_CYAN);
      False

      Continue after this decision.

  7. FOR loopunsigned i = 0; i < MAX_ENEMY_SHOTS; ++i

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

    1. Decisionenemy_shots[i].active
      True
        1. Executefill_rect(frame, (int)enemy_shots[i].x, (int)enemy_shots[i].y, 2, 5, COLOR_RED);
      False

      Continue after this decision.

  8. Decisionplayer.active
    True
    1. Executedraw_bitmap_scaled(frame, (int)player.x, PLAYER_Y, galaga_player_bitmap, 8, 8, PLAYER_W, PLAYER_H, COLOR_CYAN);
    False

    Continue after this decision.

  9. FOR loopunsigned i = 1; i < lives; ++i

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

    1. Executedraw_bitmap_scaled(frame, 6 + (int)(i - 1U) * 20, LCD_HEIGHT - 13, galaga_player_bitmap, 8, 8, 16, 12, COLOR_CYAN);
  10. Executefill_rect(frame, 0, 0, LCD_WIDTH, PLAY_TOP, COLOR_BLACK);
  11. Executechar text[32];
  12. Executedraw_text(frame, 52, 1, "1UP", 1, COLOR_RED);
  13. Executesnprintf(text, sizeof(text), "%u", (unsigned)score);
  14. Executedraw_text_centred(frame, 68, 9, text, 1, COLOR_WHITE);
  15. Executedraw_text_centred(frame, 190, 1, "HIGH SCORE", 1, COLOR_RED);
  16. Executesnprintf(text, sizeof(text), "%u", (unsigned)high_score);
  17. Executedraw_text_centred(frame, 190, 9, text, 1, COLOR_WHITE);
  18. Decisionstate == READY
    True
      1. Executeclear(frame, COLOR_BLACK);
      2. Executestarfield_draw(frame, &stars);
      3. Executedraw_text_centred(frame, 160, 14, "-- GALAGA --", 3, COLOR_CYAN);
      4. Executedraw_text_centred(frame, 160, 49, "SCORE", 1, COLOR_WHITE);
      5. Executedraw_bitmap_scaled(frame, 82, 63, galaga_boss_bitmap, 8, 8, 16, 14, COLOR_MAGENTA);
      6. Executedraw_text(frame, 148, 66, "100", 1, COLOR_MAGENTA);
      7. Executedraw_text(frame, 214, 66, "200", 1, COLOR_RED);
      8. Executedraw_bitmap_scaled(frame, 82, 81, galaga_butterfly_bitmap, 8, 8, 16, 14, COLOR_CYAN);
      9. Executedraw_text(frame, 148, 84, "80", 1, COLOR_CYAN);
      10. Executedraw_text(frame, 214, 84, "160", 1, COLOR_LIME);
      11. Executedraw_bitmap_scaled(frame, 82, 99, galaga_bee_bitmap, 8, 8, 16, 14, COLOR_YELLOW);
      12. Executedraw_text(frame, 148, 102, "50", 1, COLOR_YELLOW);
      13. Executedraw_text(frame, 214, 102, "100", 1, COLOR_ORANGE);
      14. Executedraw_text_centred(frame, 160, 124, "JOYSTICK MOVE A FIRE", 1, COLOR_CYAN);
      15. Executedraw_text_centred(frame, 160, 145, "BOOT BACK", 1, COLOR_LIGHTGRAY);
    False
    1. Decisionstate == FINISHED
      True
      1. Executedraw_text_centred(frame, 160, 78, "GAME OVER", 2, COLOR_RED);
      False
      1. Decisionlast_update < banner_until
        True
        1. Executedraw_text_centred(frame, 160, 23, challenge_stage ? "CHALLENGING STAGE" : "READY", 1, challenge_stage ? COLOR_YELLOW : COLOR_WHITE);
        False

        Continue after this decision.

Read original function source
void drawGalaga(uint16_t *frame)
{
    if (!running || frame == NULL) return;
    if (state == SCORES) { drawHighScoreEntry(frame); return; }
    clear(frame, COLOR_BLACK);
    starfield_draw(frame, &stars);
    for (unsigned i = 0; i < MAX_ENEMIES; ++i) if (enemies[i].active) {
        const Enemy *enemy = &enemies[i];
        draw_bitmap_scaled(frame, (int)enemy->x, (int)enemy->y, enemyBitmap(enemy->type), 8, 8, 16, 14,
                           enemyColour(enemy->type, enemy->motion));
    }
    for (unsigned i = 0; i < MAX_PLAYER_SHOTS; ++i) if (player_shots[i].active) {
        fill_rect(frame, (int)player_shots[i].x, (int)player_shots[i].y, 2, 8, COLOR_WHITE);
        fill_rect(frame, (int)player_shots[i].x - 1, (int)player_shots[i].y + 2, 4, 3, COLOR_CYAN);
    }
    for (unsigned i = 0; i < MAX_ENEMY_SHOTS; ++i) if (enemy_shots[i].active) {
        fill_rect(frame, (int)enemy_shots[i].x, (int)enemy_shots[i].y, 2, 5, COLOR_RED);
    }
    if (player.active) draw_bitmap_scaled(frame, (int)player.x, PLAYER_Y, galaga_player_bitmap, 8, 8, PLAYER_W, PLAYER_H, COLOR_CYAN);
    for (unsigned i = 1; i < lives; ++i) draw_bitmap_scaled(frame, 6 + (int)(i - 1U) * 20, LCD_HEIGHT - 13,
        galaga_player_bitmap, 8, 8, 16, 12, COLOR_CYAN);
    fill_rect(frame, 0, 0, LCD_WIDTH, PLAY_TOP, COLOR_BLACK);
    char text[32];
    draw_text(frame, 52, 1, "1UP", 1, COLOR_RED);
    snprintf(text, sizeof(text), "%u", (unsigned)score); draw_text_centred(frame, 68, 9, text, 1, COLOR_WHITE);
    draw_text_centred(frame, 190, 1, "HIGH SCORE", 1, COLOR_RED);
    snprintf(text, sizeof(text), "%u", (unsigned)high_score); draw_text_centred(frame, 190, 9, text, 1, COLOR_WHITE);
    if (state == READY) {
        clear(frame, COLOR_BLACK);
        starfield_draw(frame, &stars);
        draw_text_centred(frame, 160, 14, "-- GALAGA --", 3, COLOR_CYAN);
        draw_text_centred(frame, 160, 49, "SCORE", 1, COLOR_WHITE);
        draw_bitmap_scaled(frame, 82, 63, galaga_boss_bitmap, 8, 8, 16, 14, COLOR_MAGENTA);
        draw_text(frame, 148, 66, "100", 1, COLOR_MAGENTA);
        draw_text(frame, 214, 66, "200", 1, COLOR_RED);
        draw_bitmap_scaled(frame, 82, 81, galaga_butterfly_bitmap, 8, 8, 16, 14, COLOR_CYAN);
        draw_text(frame, 148, 84, "80", 1, COLOR_CYAN);
        draw_text(frame, 214, 84, "160", 1, COLOR_LIME);
        draw_bitmap_scaled(frame, 82, 99, galaga_bee_bitmap, 8, 8, 16, 14, COLOR_YELLOW);
        draw_text(frame, 148, 102, "50", 1, COLOR_YELLOW);
        draw_text(frame, 214, 102, "100", 1, COLOR_ORANGE);
        draw_text_centred(frame, 160, 124, "JOYSTICK MOVE  A FIRE", 1, COLOR_CYAN);
        draw_text_centred(frame, 160, 145, "BOOT BACK", 1, COLOR_LIGHTGRAY);
    } else if (state == FINISHED) draw_text_centred(frame, 160, 78, "GAME OVER", 2, COLOR_RED);
    else if (last_update < banner_until) draw_text_centred(frame, 160, 23,
        challenge_stage ? "CHALLENGING STAGE" : "READY", 1, challenge_stage ? COLOR_YELLOW : COLOR_WHITE);
}
Complete original file, including constants and data
/* Coded By JD(2026) */
#error "Reference listing only — not provided as build-ready source."
#include "galaga.h"
#include "audio.h"
#include "esp_random.h"
#include "game_graphics.h"
#include "high_scores.h"
#include "PET2001_font.h"
#include "starfield.h"
#include <math.h>
#include <stdio.h>
#include <string.h>

#define STEP_MS 10U
#define PI 3.14159265f
#define PLAY_TOP 17
#define PLAYER_Y 148
#define PLAYER_W 16
#define PLAYER_H 12
#define MAX_ENEMIES 49
#define MAX_PLAYER_SHOTS 2
#define MAX_ENEMY_SHOTS 6
#define FIRE_DELAY_MS 120U
#define ENEMY_FIRE_MS 850U
#define DIVE_DELAY_MS 1050U
#define RESPAWN_MS 900U

// GALAGA ARTWORK: each byte is one 8-pixel row, bit 7 is the left edge.
// Change these arrays to replace the player, Boss Galaga, butterflies, and bees.
static const uint8_t galaga_player_bitmap[8] = {
    0x18, 0x3C, 0x7E, 0xDB, 0xFF, 0x7E, 0x24, 0x42,
};
static const uint8_t galaga_boss_bitmap[8] = {
    0x18, 0x7E, 0xDB, 0xFF, 0xBD, 0x5A, 0x24, 0x42,
};
static const uint8_t galaga_butterfly_bitmap[8] = {
    0x81, 0x5A, 0x3C, 0xFF, 0x7E, 0x3C, 0x5A, 0x81,
};
static const uint8_t galaga_bee_bitmap[8] = {
    0x24, 0x5A, 0xFF, 0x3C, 0x7E, 0xDB, 0x24, 0x42,
};

typedef enum { READY, PLAYING, FINISHED, SCORES } State;
typedef enum { BOSS, BUTTERFLY, BEE } EnemyType;
typedef enum { ENTERING, FORMATION, DIVING } EnemyMotion;
typedef struct { float x, y, vx, vy; bool active; } Object;
typedef struct {
    float home_x, home_y, x, y, dive_phase;
    EnemyType type;
    EnemyMotion motion;
    bool active;
} Enemy;

static State state;
static Object player, player_shots[MAX_PLAYER_SHOTS], enemy_shots[MAX_ENEMY_SHOTS];
static Enemy enemies[MAX_ENEMIES];
static uint32_t score, last_update, accumulator, fire_delay, enemy_fire_delay;
static uint32_t dive_delay, state_started, banner_until;
static uint32_t high_score;
static uint8_t lives, wave;
static bool running, controls_ready, challenge_stage;

// The original Galaga formation is deliberately uneven: 5 Boss Galagas,
// 10 butterflies in each middle row, then 12 bees in each lower row.
static const uint8_t formation_count[] = {5, 10, 10, 12, 12};
static starfield_t stars;

static float clampf(float value, float low, float high)
{
    return value < low ? low : value > high ? high : value;
}

static uint16_t enemyColour(EnemyType type, EnemyMotion motion)
{
    if (motion == DIVING) {
        return type == BOSS ? COLOR_RED : type == BUTTERFLY ? COLOR_LIME : COLOR_ORANGE;
    }
    return type == BOSS ? COLOR_MAGENTA : type == BUTTERFLY ? COLOR_CYAN : COLOR_YELLOW;
}

static const uint8_t *enemyBitmap(EnemyType type)
{
    return type == BOSS ? galaga_boss_bitmap : type == BUTTERFLY ? galaga_butterfly_bitmap : galaga_bee_bitmap;
}

static unsigned enemyPoints(EnemyType type, bool diving)
{
    const unsigned base = type == BOSS ? 100U : type == BUTTERFLY ? 80U : 50U;
    return diving ? base * 2U : base;
}

static void addScore(unsigned points)
{
    score = score > UINT32_MAX - points ? UINT32_MAX : score + points;
}

static void prepareWave(void)
{
    challenge_stage = wave > 0 && wave % 3U == 0;
    memset(player_shots, 0, sizeof(player_shots));
    memset(enemy_shots, 0, sizeof(enemy_shots));
    unsigned index = 0;
    for (unsigned row = 0; row < sizeof(formation_count); ++row) {
        const unsigned count = formation_count[row];
        const float spacing = 22.0f;
        const float first_x = (LCD_WIDTH - (16.0f + (count - 1U) * spacing)) / 2.0f;
        for (unsigned column = 0; column < count; ++column) {
        EnemyType type = row == 0 ? BOSS : row < 3 ? BUTTERFLY : BEE;
        const float home_x = first_x + column * spacing;
        const float home_y = 29.0f + row * 18.0f;
        // Galaga enemies arrive in staggered groups along long, curved paths
        // before locking into their assigned formation position.
        const unsigned group = index / 4U;
        enemies[index] = (Enemy){home_x, home_y, 0.0f, -16.0f,
                                 -(float)group * 0.18f, type, ENTERING, true};
        ++index;
        }
    }
    dive_delay = 1000U;
    enemy_fire_delay = 800U;
    banner_until = last_update + 1200U;
}

static void beginGame(uint32_t now_ms)
{
    score = 0; lives = 3; wave = 1; controls_ready = false;
    player = (Object){152.0f, PLAYER_Y, 0.0f, 0.0f, true};
    fire_delay = 0; accumulator = 0; last_update = now_ms;
    prepareWave();
    state = PLAYING;
}

static void firePlayerShot(void)
{
    if (!player.active || fire_delay != 0) return;
    for (unsigned i = 0; i < MAX_PLAYER_SHOTS; ++i) if (!player_shots[i].active) {
        player_shots[i] = (Object){player.x + PLAYER_W / 2.0f - 1.0f, PLAYER_Y - 2.0f, 0.0f, -245.0f, true};
        fire_delay = FIRE_DELAY_MS;
        audioPlay(AUDIO_SFX_PLAYER_SHOT);
        return;
    }
}

static void fireEnemyShot(void)
{
    if (enemy_fire_delay != 0 || challenge_stage) return;
    for (unsigned attempts = 0; attempts < MAX_ENEMIES; ++attempts) {
        Enemy *enemy = &enemies[esp_random() % MAX_ENEMIES];
        if (!enemy->active || enemy->motion != FORMATION) continue;
        for (unsigned i = 0; i < MAX_ENEMY_SHOTS; ++i) if (!enemy_shots[i].active) {
            const float dx = player.x + PLAYER_W / 2.0f - enemy->x;
            enemy_shots[i] = (Object){enemy->x + 7.0f, enemy->y + 13.0f, clampf(dx * 0.45f, -80.0f, 80.0f), 115.0f + wave * 5.0f, true};
            enemy_fire_delay = ENEMY_FIRE_MS > wave * 45U ? ENEMY_FIRE_MS - wave * 45U : 260U;
            audioPlay(AUDIO_SFX_ENEMY_SHOT);
            return;
        }
    }
}

static void startDive(void)
{
    if (challenge_stage || dive_delay != 0) return;
    for (unsigned attempts = 0; attempts < MAX_ENEMIES; ++attempts) {
        Enemy *enemy = &enemies[esp_random() % MAX_ENEMIES];
        if (!enemy->active || enemy->motion != FORMATION) continue;
        enemy->motion = DIVING;
        enemy->dive_phase = 0.0f;
        dive_delay = DIVE_DELAY_MS > wave * 55U ? DIVE_DELAY_MS - wave * 55U : 360U;
        return;
    }
}

static void hitPlayer(void)
{
    if (!player.active) return;
    audioPlay(AUDIO_SFX_PLAYER_EXPLOSION);
    if (--lives == 0) {
        player.active = false;
        state = FINISHED;
        state_started = last_update;
        return;
    }
    player.active = false;
    state_started = last_update;
    memset(player_shots, 0, sizeof(player_shots));
    memset(enemy_shots, 0, sizeof(enemy_shots));
}

static unsigned enemiesRemaining(void)
{
    unsigned count = 0;
    for (unsigned i = 0; i < MAX_ENEMIES; ++i) if (enemies[i].active) ++count;
    return count;
}

static void updateEnemies(void)
{
    for (unsigned i = 0; i < MAX_ENEMIES; ++i) {
        Enemy *enemy = &enemies[i];
        if (!enemy->active) continue;
        if (enemy->motion == ENTERING) {
            enemy->dive_phase += STEP_MS / 1350.0f;
            if (enemy->dive_phase < 0.0f) continue;
            const float t = enemy->dive_phase;
            if (t >= 1.0f) {
                enemy->x = enemy->home_x;
                enemy->y = enemy->home_y;
                enemy->motion = FORMATION;
                continue;
            }
            const float curve = sinf(PI * t);
            const float start_x = ((unsigned)(enemy - enemies) / 4U & 1U) == 0U ? -18.0f : (float)LCD_WIDTH + 2.0f;
            const float direction = start_x < 0.0f ? 1.0f : -1.0f;
            enemy->x = start_x + (enemy->home_x - start_x) * t + direction * curve * 42.0f;
            enemy->y = -16.0f + (enemy->home_y + 16.0f) * t + curve * 36.0f;
            continue;
        }
        if (enemy->motion != DIVING) continue;
        enemy->dive_phase += STEP_MS / 1800.0f;
        if (enemy->dive_phase >= 1.0f) {
            enemy->motion = FORMATION;
            enemy->x = enemy->home_x;
            enemy->y = enemy->home_y;
            continue;
        }
        const float curve = sinf(PI * enemy->dive_phase);
        const float direction = ((unsigned)(enemy - enemies) & 1U) == 0U ? -1.0f : 1.0f;
        const float depth = (float)(PLAYER_Y - 16) - enemy->home_y;
        enemy->x = enemy->home_x + direction * (curve * 88.0f + sinf(PI * enemy->dive_phase * 2.0f) * 28.0f);
        enemy->y = enemy->home_y + curve * depth;
        if (player.active && fabsf(enemy->x + 8.0f - (player.x + PLAYER_W / 2.0f)) < 12.0f &&
                   fabsf(enemy->y + 8.0f - (PLAYER_Y + PLAYER_H / 2.0f)) < 10.0f) hitPlayer();
    }
}

static void stepGame(float input_x, bool fire)
{
    fire_delay = fire_delay > STEP_MS ? fire_delay - STEP_MS : 0;
    enemy_fire_delay = enemy_fire_delay > STEP_MS ? enemy_fire_delay - STEP_MS : 0;
    dive_delay = dive_delay > STEP_MS ? dive_delay - STEP_MS : 0;
    if (!player.active) {
        if (last_update - state_started >= RESPAWN_MS) player = (Object){152.0f, PLAYER_Y, 0.0f, 0.0f, true};
        return;
    }
    player.vx += input_x * 1000.0f * STEP_MS / 1000.0f;
    player.vx *= 0.92f;
    player.vx = clampf(player.vx, -140.0f, 140.0f);
    player.x = clampf(player.x + player.vx * STEP_MS / 1000.0f, 4.0f, LCD_WIDTH - PLAYER_W - 4.0f);
    if (fire) firePlayerShot();
    startDive();
    fireEnemyShot();
    updateEnemies();

    for (unsigned s = 0; s < MAX_PLAYER_SHOTS; ++s) {
        Object *shot = &player_shots[s];
        if (!shot->active) continue;
        shot->y += shot->vy * STEP_MS / 1000.0f;
        if (shot->y < PLAY_TOP) { shot->active = false; continue; }
        for (unsigned e = 0; e < MAX_ENEMIES; ++e) {
            Enemy *enemy = &enemies[e];
            if (!enemy->active || fabsf(shot->x - (enemy->x + 7.0f)) >= 9.0f || fabsf(shot->y - (enemy->y + 7.0f)) >= 10.0f) continue;
            addScore(enemyPoints(enemy->type, enemy->motion == DIVING));
            enemy->active = false;
            shot->active = false;
            audioPlay(AUDIO_SFX_HIT);
            break;
        }
    }
    for (unsigned s = 0; s < MAX_ENEMY_SHOTS; ++s) {
        Object *shot = &enemy_shots[s];
        if (!shot->active) continue;
        shot->x += shot->vx * STEP_MS / 1000.0f;
        shot->y += shot->vy * STEP_MS / 1000.0f;
        if (shot->y > LCD_HEIGHT) { shot->active = false; continue; }
        if (fabsf(shot->x - (player.x + PLAYER_W / 2.0f)) < 10.0f && fabsf(shot->y - (PLAYER_Y + 5.0f)) < 9.0f) {
            shot->active = false;
            hitPlayer();
        }
    }
    if (enemiesRemaining() == 0) {
        audioPlay(AUDIO_SFX_WAVE_CLEAR);
        ++wave;
        prepareWave();
    }
}

void startGalaga(uint32_t now_ms)
{
    high_score_table_t scores;
    high_score = high_scores_load(HIGH_SCORE_GALAGA, &scores) == HIGH_SCORES_OK && scores.count != 0 ? scores.entries[0].score : 0;
    starfield_initialize(&stars, 80, 0, 0, LCD_WIDTH, LCD_HEIGHT, STARFIELD_COLOUR);
    running = true;
    beginGame(now_ms);
    state = READY;
}
void stopGalaga(void) { cancelHighScoreEntry(); running = false; }
void galagaAction(bool start, uint32_t now_ms)
{
    if (running && start && state == READY) {
        beginGame(now_ms);
        audioPlay(AUDIO_SFX_GAME_START);
    }
}

void updateGalaga(int x_offset, int y_offset, bool joystick_valid, bool fire, uint32_t now_ms)
{
    if (!running) return;
    const uint32_t elapsed = now_ms - last_update;
    last_update = now_ms;
    if (state == FINISHED && now_ms - state_started >= 1000U) {
        startHighScoreEntry(HIGH_SCORE_GALAGA, score, now_ms);
        state = SCORES;
    }
    if (state == SCORES) { if (updateHighScoreEntry(x_offset, y_offset, joystick_valid, now_ms)) state = READY; return; }
    starfield_update(&stars, elapsed);
    if (state != PLAYING) return;
    if (!joystick_valid) controls_ready = false;
    else if (x_offset >= -350 && x_offset <= 350) controls_ready = true;
    float input_x = joystick_valid && controls_ready ? clampf((float)x_offset / 1500.0f, -1.0f, 1.0f) : 0.0f;
    accumulator += elapsed > 100U ? 100U : elapsed;
    while (accumulator >= STEP_MS && state == PLAYING) { accumulator -= STEP_MS; stepGame(input_x, fire); }
}

void drawGalaga(uint16_t *frame)
{
    if (!running || frame == NULL) return;
    if (state == SCORES) { drawHighScoreEntry(frame); return; }
    clear(frame, COLOR_BLACK);
    starfield_draw(frame, &stars);
    for (unsigned i = 0; i < MAX_ENEMIES; ++i) if (enemies[i].active) {
        const Enemy *enemy = &enemies[i];
        draw_bitmap_scaled(frame, (int)enemy->x, (int)enemy->y, enemyBitmap(enemy->type), 8, 8, 16, 14,
                           enemyColour(enemy->type, enemy->motion));
    }
    for (unsigned i = 0; i < MAX_PLAYER_SHOTS; ++i) if (player_shots[i].active) {
        fill_rect(frame, (int)player_shots[i].x, (int)player_shots[i].y, 2, 8, COLOR_WHITE);
        fill_rect(frame, (int)player_shots[i].x - 1, (int)player_shots[i].y + 2, 4, 3, COLOR_CYAN);
    }
    for (unsigned i = 0; i < MAX_ENEMY_SHOTS; ++i) if (enemy_shots[i].active) {
        fill_rect(frame, (int)enemy_shots[i].x, (int)enemy_shots[i].y, 2, 5, COLOR_RED);
    }
    if (player.active) draw_bitmap_scaled(frame, (int)player.x, PLAYER_Y, galaga_player_bitmap, 8, 8, PLAYER_W, PLAYER_H, COLOR_CYAN);
    for (unsigned i = 1; i < lives; ++i) draw_bitmap_scaled(frame, 6 + (int)(i - 1U) * 20, LCD_HEIGHT - 13,
        galaga_player_bitmap, 8, 8, 16, 12, COLOR_CYAN);
    fill_rect(frame, 0, 0, LCD_WIDTH, PLAY_TOP, COLOR_BLACK);
    char text[32];
    drawPet2001TextTransparent(frame, 52, 1, "1UP", COLOR_RED, 1);
    snprintf(text, sizeof(text), "%u", (unsigned)score); drawPet2001TextCentredTransparent(frame, 68, 9, text, COLOR_WHITE, 1);
    drawPet2001TextCentredTransparent(frame, 190, 1, "HIGH SCORE", COLOR_RED, 1);
    snprintf(text, sizeof(text), "%u", (unsigned)high_score); drawPet2001TextCentredTransparent(frame, 190, 9, text, COLOR_WHITE, 1);
    if (state == READY) {
        clear(frame, COLOR_BLACK);
        starfield_draw(frame, &stars);
        drawPet2001TextCentredTransparent(frame, 160, 14, "-- GALAGA --", COLOR_CYAN, 3);
        drawPet2001TextCentredTransparent(frame, 160, 49, "SCORE", COLOR_WHITE, 1);
        draw_bitmap_scaled(frame, 82, 63, galaga_boss_bitmap, 8, 8, 16, 14, COLOR_MAGENTA);
        drawPet2001TextTransparent(frame, 148, 66, "100", COLOR_MAGENTA, 1);
        drawPet2001TextTransparent(frame, 214, 66, "200", COLOR_RED, 1);
        draw_bitmap_scaled(frame, 82, 81, galaga_butterfly_bitmap, 8, 8, 16, 14, COLOR_CYAN);
        drawPet2001TextTransparent(frame, 148, 84, "80", COLOR_CYAN, 1);
        drawPet2001TextTransparent(frame, 214, 84, "160", COLOR_LIME, 1);
        draw_bitmap_scaled(frame, 82, 99, galaga_bee_bitmap, 8, 8, 16, 14, COLOR_YELLOW);
        drawPet2001TextTransparent(frame, 148, 102, "50", COLOR_YELLOW, 1);
        drawPet2001TextTransparent(frame, 214, 102, "100", COLOR_ORANGE, 1);
        drawPet2001TextCentredTransparent(frame, 160, 124, "JOYSTICK MOVE  A FIRE", COLOR_CYAN, 1);
        drawPet2001TextCentredTransparent(frame, 160, 145, "BOOT BACK", COLOR_LIGHTGRAY, 1);
    } else if (state == FINISHED) drawPet2001TextCentredTransparent(frame, 160, 78, "GAME OVER", COLOR_RED, 2);
    else if (last_update < banner_until) drawPet2001TextCentredTransparent(frame, 160, 23,
        challenge_stage ? "CHALLENGING STAGE" : "READY", challenge_stage ? COLOR_YELLOW : COLOR_WHITE, 1);
}