Scramble

Source: src/scramble.c · 675 lines · 34 functions.

Screen captures

Views of this part running on the Mini Arcade.

Scramble game screen on the Mini Arcade
Scramble showing the ship and scrolling terrain.

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

overlaps() · source line 115
static bool overlaps(float ax, float ay, float aw, float ah,
                     float bx, float by, float bw, float bh)
  1. Exit this functionreturn ax < bx + bw && ax + aw > bx && ay < by + ah && ay + ah > by;
Read original function source
static bool overlaps(float ax, float ay, float aw, float ah,
                     float bx, float by, float bw, float bh)
{
    return ax < bx + bw && ax + aw > bx && ay < by + ah && ay + ah > by;
}
clampValue() · source line 121
static float clampValue(float value, float low, float high)
  1. Exit this functionreturn value < low ? low : value > high ? high : value;
Read original function source
static float clampValue(float value, float low, float high)
{
    return value < low ? low : value > high ? high : value;
}
reduceTimer() · source line 126
static uint32_t reduceTimer(uint32_t timer)
  1. Exit this functionreturn timer > STEP_MS ? timer - STEP_MS : 0;
Read original function source
static uint32_t reduceTimer(uint32_t timer)
{
    return timer > STEP_MS ? timer - STEP_MS : 0;
}
addPoints() · source line 131
static void addPoints(uint32_t points)
  1. Executescore = score > UINT32_MAX - points ? UINT32_MAX : score + points;
  2. Executeconst uint32_t nextWave = score / 1000U + 1U;
  3. DecisionnextWave > wave
    True
    1. Executewave = nextWave;
    False

    Continue after this decision.

Read original function source
static void addPoints(uint32_t points)
{
    score = score > UINT32_MAX - points ? UINT32_MAX : score + points;
    // Every thousand points advances the internal difficulty used by enemies.
    const uint32_t nextWave = score / 1000U + 1U;
    if (nextWave > wave) wave = nextWave;
}
terrainHeightInPattern() · source line 139
static float terrainHeightInPattern(float x)
  1. Decisionx <= 0.0f
    True
    1. Exit this functionreturn terrain[0];
    False

    Continue after this decision.

  2. Executeconst float index = x / TERRAIN_SPACING;
  3. Executeunsigned left = (unsigned)index;
  4. Decisionleft >= TERRAIN_POINTS - 1U
    True
    1. Exit this functionreturn terrain[TERRAIN_POINTS - 1U];
    False

    Continue after this decision.

  5. Executeconst float fraction = index - left;
  6. Exit this functionreturn terrain[left] + (terrain[left + 1U] - terrain[left]) * fraction;
Read original function source
static float terrainHeightInPattern(float x)
{
    if (x <= 0.0f) return terrain[0];
    const float index = x / TERRAIN_SPACING;
    unsigned left = (unsigned)index;
    if (left >= TERRAIN_POINTS - 1U) return terrain[TERRAIN_POINTS - 1U];
    const float fraction = index - left;
    return terrain[left] + (terrain[left + 1U] - terrain[left]) * fraction;
}
terrainHeight() · source line 149
static float terrainHeight(float x)

Calls into this documentation:

  1. Executex = fmodf(x, (TERRAIN_POINTS - 1U) * TERRAIN_SPACING);
  2. Decisionx < 0.0f
    True
    1. Executex += (TERRAIN_POINTS - 1U) * TERRAIN_SPACING;
    False

    Continue after this decision.

  3. Exit this functionreturn terrainHeightInPattern(x);
Read original function source
static float terrainHeight(float x)
{
    // The scrolling landscape repeats after its editable 320-pixel pattern.
    x = fmodf(x, (TERRAIN_POINTS - 1U) * TERRAIN_SPACING);
    if (x < 0.0f) x += (TERRAIN_POINTS - 1U) * TERRAIN_SPACING;
    return terrainHeightInPattern(x);
}
flattenTerrainZone() · source line 157
static void flattenTerrainZone(unsigned zone, unsigned first)
  1. Executeconst unsigned last = first + 2U;
  2. Executeconst float height = baseTerrain[first + 1U];
  3. ExecuteflatZones[zone] = (FlatZone){first, last, height};
  4. FOR loopunsigned point = first; point <= last; ++point

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

    1. Executeterrain[point] = height;
Read original function source
static void flattenTerrainZone(unsigned zone, unsigned first)
{
    const unsigned last = first + 2U;
    const float height = baseTerrain[first + 1U];
    flatZones[zone] = (FlatZone){first, last, height};
    for (unsigned point = first; point <= last; ++point) terrain[point] = height;
}
flatZoneAvailable() · source line 165
static bool flatZoneAvailable(unsigned first, unsigned count)
  1. Executeconst unsigned last = first + 2U;
  2. FOR loopunsigned zone = 0; zone < count; ++zone

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

      1. Decisionlast >= flatZones[zone].first && first <= flatZones[zone].last
        True
        1. Exit this functionreturn false;
        False

        Continue after this decision.

  3. Exit this functionreturn true;
Read original function source
static bool flatZoneAvailable(unsigned first, unsigned count)
{
    const unsigned last = first + 2U;
    for (unsigned zone = 0; zone < count; ++zone) {
        if (last >= flatZones[zone].first && first <= flatZones[zone].last)
            return false;
    }
    return true;
}
makeTerrain() · source line 175
static void makeTerrain(void)

Calls into this documentation:

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

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

      1. ExecutebaseTerrain[i] = terrain[i] = 130.0f + scramble_terrain_offsets[i];
  2. FOR loopunsigned target = 0; target < MAX_TARGETS; ++target

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

      1. Executeconst unsigned first = scramble_target_shelf_starts[target];
      2. ExecuteflattenTerrainZone(target, first);
      3. Executescramble_target_lanes[target] = (first + 1U) * TERRAIN_SPACING;
  3. FOR loopunsigned zone = MAX_TARGETS; zone < FLAT_ZONE_COUNT; ++zone

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

      1. Executeunsigned first;
      2. DO / WHILE loop!flatZoneAvailable(first, zone)

        Run body, then test condition; repeat while true.

          1. Executefirst = 1U + esp_random() % (TERRAIN_POINTS - 5U);
      3. ExecuteflattenTerrainZone(zone, first);
Read original function source
static void makeTerrain(void)
{
    for (unsigned i = 0; i < TERRAIN_POINTS; ++i) {
        baseTerrain[i] = terrain[i] = 130.0f + scramble_terrain_offsets[i];
    }
    for (unsigned target = 0; target < MAX_TARGETS; ++target) {
        const unsigned first = scramble_target_shelf_starts[target];
        flattenTerrainZone(target, first);
        scramble_target_lanes[target] = (first + 1U) * TERRAIN_SPACING;
    }
    for (unsigned zone = MAX_TARGETS; zone < FLAT_ZONE_COUNT; ++zone) {
        unsigned first;
        do {
            first = 1U + esp_random() % (TERRAIN_POINTS - 5U);
        } while (!flatZoneAvailable(first, zone));
        flattenTerrainZone(zone, first);
    }
}
drawFlatTerrainEdges() · source line 194
static void drawFlatTerrainEdges(uint16_t *frame)

Calls into this documentation:

  1. Executeconst float patternWidth = (TERRAIN_POINTS - 1U) * TERRAIN_SPACING;
  2. Executeconst int firstPattern = (int)floorf(scroll / patternWidth) - 1;
  3. FOR loopint pattern = firstPattern; pattern <= firstPattern + 3; ++pattern

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

      1. FOR loopunsigned zone = 0; zone < FLAT_ZONE_COUNT; ++zone

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

          1. Executeconst FlatZone *flat = &flatZones[zone];
          2. Executeconst int left = (int)(flat->first * TERRAIN_SPACING + pattern * patternWidth - scroll);
          3. Executeconst int right = (int)(flat->last * TERRAIN_SPACING + pattern * patternWidth - scroll);
          4. Executedraw_line(frame, left, (int)baseTerrain[flat->first], left, (int)flat->height, COLOR_LIME);
          5. Executedraw_line(frame, right, (int)baseTerrain[flat->last], right, (int)flat->height, COLOR_LIME);
Read original function source
static void drawFlatTerrainEdges(uint16_t *frame)
{
    const float patternWidth = (TERRAIN_POINTS - 1U) * TERRAIN_SPACING;
    const int firstPattern = (int)floorf(scroll / patternWidth) - 1;
    for (int pattern = firstPattern; pattern <= firstPattern + 3; ++pattern) {
        for (unsigned zone = 0; zone < FLAT_ZONE_COUNT; ++zone) {
            const FlatZone *flat = &flatZones[zone];
            const int left = (int)(flat->first * TERRAIN_SPACING + pattern * patternWidth - scroll);
            const int right = (int)(flat->last * TERRAIN_SPACING + pattern * patternWidth - scroll);
            // Join each two-space flat shelf back to the underlying curve with
            // sharp vertical lime edges.
            draw_line(frame, left, (int)baseTerrain[flat->first], left,
                      (int)flat->height, COLOR_LIME);
            draw_line(frame, right, (int)baseTerrain[flat->last], right,
                      (int)flat->height, COLOR_LIME);
        }
    }
}
startEffect() · source line 213
static void startEffect(float x, float y, uint16_t color)
  1. FOR loopunsigned i = 0; i < MAX_EFFECTS; ++i

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

      1. Decisioneffects[i].remaining == 0
        True
          1. Executeeffects[i] = (Effect){x, y, 260U, color};
          2. Exit this functionreturn;
        False

        Continue after this decision.

Read original function source
static void startEffect(float x, float y, uint16_t color)
{
    for (unsigned i = 0; i < MAX_EFFECTS; ++i) {
        if (effects[i].remaining == 0) {
            effects[i] = (Effect){x, y, 260U, color};
            return;
        }
    }
}
nextTargetType() · source line 223
static TargetType nextTargetType(void)
  1. DecisionrocketsInGroup != 0
    True
      1. Execute--rocketsInGroup;
      2. Exit this functionreturn TARGET_ROCKET;
    False

    Continue after this decision.

  2. Decisionesp_random() % 3U == 1U
    True
    1. Exit this functionreturn TARGET_FUEL;
    False

    Continue after this decision.

  3. Decision--turretsUntilRockets == 0
    True
      1. ExecuterocketsInGroup = 1U + esp_random() % 2U;
      2. ExecuteturretsUntilRockets = 5U + esp_random() % 4U;
    False

    Continue after this decision.

  4. Exit this functionreturn TARGET_TURRET;
Read original function source
static TargetType nextTargetType(void)
{
    if (rocketsInGroup != 0) {
        --rocketsInGroup;
        return TARGET_ROCKET;
    }
    if (esp_random() % 3U == 1U) return TARGET_FUEL;
    if (--turretsUntilRockets == 0) {
        // One or two rockets follow every five to eight ordinary turrets.
        rocketsInGroup = 1U + esp_random() % 2U;
        turretsUntilRockets = 5U + esp_random() % 4U;
    }
    return TARGET_TURRET;
}
placeTarget() · source line 238
static void placeTarget(GroundTarget *target, float x)

Calls into this documentation:

  1. Execute*target = (GroundTarget){x, terrainHeight(x), nextTargetType(), 900U + esp_random() % 1501U, true};
Read original function source
static void placeTarget(GroundTarget *target, float x)
{
    *target = (GroundTarget){x, terrainHeight(x), nextTargetType(),
                             900U + esp_random() % 1501U, true};
}
resetObjects() · source line 244
static void resetObjects(void)

Calls into this documentation:

  1. Executememset(shots, 0, sizeof(shots));
  2. Executememset(bombs, 0, sizeof(bombs));
  3. Executememset(bombDropAt, 0, sizeof(bombDropAt));
  4. Executememset(enemies, 0, sizeof(enemies));
  5. Executememset(rockets, 0, sizeof(rockets));
  6. Executememset(effects, 0, sizeof(effects));
  7. ExecuteturretsUntilRockets = 5U + esp_random() % 4U;
  8. ExecuterocketsInGroup = 0;
  9. FOR loopunsigned i = 0; i < MAX_TARGETS; ++i

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

      1. ExecuteplaceTarget(&targets[i], scramble_target_lanes[i]);
Read original function source
static void resetObjects(void)
{
    memset(shots, 0, sizeof(shots));
    memset(bombs, 0, sizeof(bombs));
    memset(bombDropAt, 0, sizeof(bombDropAt));
    memset(enemies, 0, sizeof(enemies));
    memset(rockets, 0, sizeof(rockets));
    memset(effects, 0, sizeof(effects));
    turretsUntilRockets = 5U + esp_random() % 4U;
    rocketsInGroup = 0;
    for (unsigned i = 0; i < MAX_TARGETS; ++i) {
        placeTarget(&targets[i], scramble_target_lanes[i]);
    }
}
beginGame() · source line 259
static void beginGame(uint32_t now_ms)

Calls into this documentation:

  1. ExecutemakeTerrain();
  2. Executescroll = 0.0f;
  3. Executefuel = FUEL_MAX;
  4. Executescore = 0;
  5. Executewave = 1;
  6. Executelives = 3;
  7. Executeplayer = (Object){52.0f, 75.0f, 0.0f, 0.0f, true};
  8. ExecutefireDelay = bombDelay = 0;
  9. ExecuteenemyDelay = ENEMY_SPAWN_MIN_MS;
  10. Executeaccumulator = 0;
  11. ExecuteplayerFrame = 0;
  12. ExecutecontrolsReady = false;
  13. ExecuteresetObjects();
  14. ExecutegameState = STATE_PLAYING;
  15. ExecutelastUpdate = now_ms;
Read original function source
static void beginGame(uint32_t now_ms)
{
    makeTerrain();
    scroll = 0.0f;
    fuel = FUEL_MAX;
    score = 0;
    wave = 1;
    lives = 3;
    player = (Object){52.0f, 75.0f, 0.0f, 0.0f, true};
    fireDelay = bombDelay = 0;
    enemyDelay = ENEMY_SPAWN_MIN_MS;
    accumulator = 0;
    playerFrame = 0;
    controlsReady = false;
    resetObjects();
    gameState = STATE_PLAYING;
    lastUpdate = now_ms;
}
finishGame() · source line 278
static void finishGame(void)
  1. Executeplayer.active = false;
  2. ExecutegameState = STATE_FINISHED;
  3. ExecutestateStarted = lastUpdate;
  4. ExecuteESP_LOGI("scramble", "GAME OVER: %u", (unsigned)score);
Read original function source
static void finishGame(void)
{
    player.active = false;
    gameState = STATE_FINISHED;
    stateStarted = lastUpdate;
    ESP_LOGI("scramble", "GAME OVER: %u", (unsigned)score);
}
hitPlayer() · source line 286
static void hitPlayer(void)

Calls into this documentation:

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

    Continue after this decision.

  2. ExecutestartEffect(player.x + PLAYER_W / 2.0f, player.y + PLAYER_H / 2.0f, COLOR_YELLOW);
  3. ExecuteaudioPlay(AUDIO_SFX_PLAYER_EXPLOSION);
  4. Decision--lives == 0
    True
      1. ExecutefinishGame();
      2. Exit this functionreturn;
    False

    Continue after this decision.

  5. Executeplayer.active = false;
  6. ExecutestateStarted = lastUpdate;
  7. Executememset(shots, 0, sizeof(shots));
  8. Executememset(bombs, 0, sizeof(bombs));
Read original function source
static void hitPlayer(void)
{
    if (!player.active) return;
    startEffect(player.x + PLAYER_W / 2.0f, player.y + PLAYER_H / 2.0f, COLOR_YELLOW);
    audioPlay(AUDIO_SFX_PLAYER_EXPLOSION);
    if (--lives == 0) {
        finishGame();
        return;
    }
    player.active = false;
    stateStarted = lastUpdate;
    memset(shots, 0, sizeof(shots));
    memset(bombs, 0, sizeof(bombs));
}
respawnPlayer() · source line 301
static void respawnPlayer(void)
  1. Executeplayer = (Object){52.0f, 75.0f, 0.0f, 0.0f, true};
  2. ExecutecontrolsReady = false;
Read original function source
static void respawnPlayer(void)
{
    player = (Object){52.0f, 75.0f, 0.0f, 0.0f, true};
    controlsReady = false;
}
fireShot() · source line 307
static void fireShot(void)

Calls into this documentation:

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

    Continue after this decision.

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

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

      1. Decision!shots[i].active
        True
          1. Executeshots[i] = (Object){player.x + PLAYER_W, player.y + 3.0f, 240.0f, 0.0f, true};
          2. ExecutefireDelay = FIRE_DELAY_MS;
          3. ExecuteaudioPlay(AUDIO_SFX_PLAYER_SHOT);
          4. Exit this functionreturn;
        False

        Continue after this decision.

Read original function source
static void fireShot(void)
{
    if (fireDelay != 0 || !player.active) return;
    for (unsigned i = 0; i < MAX_SHOTS; ++i) {
        if (!shots[i].active) {
            shots[i] = (Object){player.x + PLAYER_W, player.y + 3.0f, 240.0f, 0.0f, true};
            fireDelay = FIRE_DELAY_MS;
            audioPlay(AUDIO_SFX_PLAYER_SHOT);
            return;
        }
    }
}
dropBomb() · source line 320
static void dropBomb(void)
  1. DecisionbombDelay != 0 || !player.active
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

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

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

      1. Decision!bombs[i].active
        True
          1. Executebombs[i] = (Object){player.x + 7.0f, player.y + PLAYER_H, 70.0f, 25.0f, true};
          2. ExecutebombDropAt[i] = bombs[i].x + PLAYER_W;
          3. ExecutebombDelay = BOMB_DELAY_MS;
          4. Exit this functionreturn;
        False

        Continue after this decision.

Read original function source
static void dropBomb(void)
{
    if (bombDelay != 0 || !player.active) return;
    for (unsigned i = 0; i < MAX_BOMBS; ++i) {
        if (!bombs[i].active) {
            // Travel one ship length forward, then fall almost straight down.
            bombs[i] = (Object){player.x + 7.0f, player.y + PLAYER_H, 70.0f, 25.0f, true};
            bombDropAt[i] = bombs[i].x + PLAYER_W;
            bombDelay = BOMB_DELAY_MS;
            return;
        }
    }
}
spawnEnemy() · source line 334
static void spawnEnemy(void)
  1. FOR loopunsigned i = 0; i < MAX_ENEMIES; ++i

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

      1. Decision!enemies[i].active
        True
          1. Executeenemies[i] = (Object){320.0f, 25.0f + esp_random() % 80U, -(48.0f + wave * 3.0f), 0.0f, true};
          2. Exit this functionreturn;
        False

        Continue after this decision.

Read original function source
static void spawnEnemy(void)
{
    for (unsigned i = 0; i < MAX_ENEMIES; ++i) {
        if (!enemies[i].active) {
            enemies[i] = (Object){320.0f, 25.0f + esp_random() % 80U,
                                  -(48.0f + wave * 3.0f), 0.0f, true};
            return;
        }
    }
}
destroyEnemy() · source line 345
static void destroyEnemy(Object *enemy)

Calls into this documentation:

  1. Executeenemy->active = false;
  2. ExecutestartEffect(enemy->x + 8.0f, enemy->y + 6.0f, COLOR_MAGENTA);
  3. ExecuteaddPoints(100U);
  4. ExecuteaudioPlay(AUDIO_SFX_HIT);
Read original function source
static void destroyEnemy(Object *enemy)
{
    enemy->active = false;
    startEffect(enemy->x + 8.0f, enemy->y + 6.0f, COLOR_MAGENTA);
    addPoints(100U);
    audioPlay(AUDIO_SFX_HIT);
}
destroyRocket() · source line 353
static void destroyRocket(Object *rocket)

Calls into this documentation:

  1. Executerocket->active = false;
  2. ExecutestartEffect(rocket->x + 4.0f, rocket->y + 6.0f, COLOR_ORANGERED);
  3. ExecuteaddPoints(150U);
  4. ExecuteaudioPlay(AUDIO_SFX_HIT);
Read original function source
static void destroyRocket(Object *rocket)
{
    rocket->active = false;
    startEffect(rocket->x + 4.0f, rocket->y + 6.0f, COLOR_ORANGERED);
    addPoints(150U);
    audioPlay(AUDIO_SFX_HIT);
}
destroyTarget() · source line 361
static void destroyTarget(GroundTarget *target)

Calls into this documentation:

  1. Executetarget->active = false;
  2. ExecutestartEffect(target->x, target->y - 6.0f, COLOR_ORANGE);
  3. Decisiontarget->type == TARGET_FUEL
    True
      1. Executefuel = FUEL_MAX;
      2. ExecuteaddPoints(150U);
      3. ExecuteaudioPlay(AUDIO_SFX_POWER_UP);
    False
    1. Decisiontarget->type == TARGET_ROCKET
      True
        1. ExecuteaddPoints(100U);
        2. ExecuteaudioPlay(AUDIO_SFX_HIT);
      False
        1. ExecuteaddPoints(50U);
        2. ExecuteaudioPlay(AUDIO_SFX_HIT);
Read original function source
static void destroyTarget(GroundTarget *target)
{
    target->active = false;
    startEffect(target->x, target->y - 6.0f, COLOR_ORANGE);
    if (target->type == TARGET_FUEL) {
        fuel = FUEL_MAX;
        addPoints(150U);
        audioPlay(AUDIO_SFX_POWER_UP);
    } else if (target->type == TARGET_ROCKET) {
        addPoints(100U);
        audioPlay(AUDIO_SFX_HIT);
    } else {
        addPoints(50U);
        audioPlay(AUDIO_SFX_HIT);
    }
}
launchRocket() · source line 378
static void launchRocket(GroundTarget *target)

Calls into this documentation:

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

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

      1. Decisionrockets[i].active
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      2. Executeconst float screenX = target->x - scroll;
      3. Executeconst float aim = player.x + PLAYER_W / 2.0f - screenX;
      4. Executeconst float accuracy = clampValue(0.24f + (wave - 1U) * 0.09f, 0.24f, 0.78f);
      5. Executerockets[i] = (Object){screenX, terrainHeight(target->x) - 12.0f, aim * accuracy, -(82.0f + wave * 5.0f), true};
      6. Executetarget->active = false;
      7. ExecutestartEffect(screenX, terrainHeight(target->x) - 5.0f, COLOR_RED);
      8. ExecuteaudioPlay(AUDIO_SFX_ENEMY_SHOT);
      9. Exit this functionreturn;
Read original function source
static void launchRocket(GroundTarget *target)
{
    for (unsigned i = 0; i < MAX_ROCKETS; ++i) {
        if (rockets[i].active) continue;
        const float screenX = target->x - scroll;
        const float aim = player.x + PLAYER_W / 2.0f - screenX;
        const float accuracy = clampValue(0.24f + (wave - 1U) * 0.09f, 0.24f, 0.78f);
        rockets[i] = (Object){screenX, terrainHeight(target->x) - 12.0f,
                              aim * accuracy, -(82.0f + wave * 5.0f), true};
        target->active = false;
        startEffect(screenX, terrainHeight(target->x) - 5.0f, COLOR_RED);
        audioPlay(AUDIO_SFX_ENEMY_SHOT);
        return;
    }
}
simulateStep() · source line 394
static void simulateStep(float velocityX, float velocityY, bool fire, bool bomb)

Calls into this documentation:

  1. ExecutefireDelay = reduceTimer(fireDelay);
  2. ExecutebombDelay = reduceTimer(bombDelay);
  3. FOR loopunsigned i = 0; i < MAX_EFFECTS; ++i

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

    1. Executeeffects[i].remaining = reduceTimer(effects[i].remaining);
  4. Decision!player.active
    True
      1. DecisionlastUpdate - stateStarted >= RESPAWN_MS
        True
        1. ExecuterespawnPlayer();
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  5. Executeplayer.x = clampValue(player.x + velocityX * STEP_MS / 1000.0f, 8.0f, 160.0f);
  6. Executeplayer.y = clampValue(player.y + velocityY * STEP_MS / 1000.0f, PLAY_TOP + 3.0f, 116.0f);
  7. Executescroll += SCROLL_SPEED * STEP_MS / 1000.0f;
  8. Executefuel -= FUEL_DRAIN_PER_SECOND * STEP_MS / 1000.0f;
  9. Decisionfuel <= 0.0f
    True
      1. Executefuel = 0.0f;
      2. ExecutehitPlayer();
      3. Exit this functionreturn;
    False

    Continue after this decision.

  10. Decisionfire
    True
    1. ExecutefireShot();
    False

    Continue after this decision.

  11. Decisionbomb
    True
    1. ExecutedropBomb();
    False

    Continue after this decision.

  12. ExecuteplayerFrame = (uint8_t)((lastUpdate / 120U) & 1U);
  13. FOR loopunsigned i = 0; i < MAX_TARGETS; ++i

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

      1. ExecuteGroundTarget *target = &targets[i];
      2. Decisiontarget->x - scroll >= -16.0f
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      3. Executetarget->x += (TERRAIN_POINTS - 1U) * TERRAIN_SPACING;
      4. ExecuteplaceTarget(target, target->x);
  14. FOR loopunsigned i = 0; i < MAX_TARGETS; ++i

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

      1. ExecuteGroundTarget *target = &targets[i];
      2. Executeconst float screenX = target->x - scroll;
      3. Decision!target->active || target->type != TARGET_ROCKET || screenX < 0.0f || screenX >= LCD_WIDTH
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      4. Executetarget->launchDelay = reduceTimer(target->launchDelay);
      5. Decisiontarget->launchDelay == 0
        True
        1. ExecutelaunchRocket(target);
        False

        Continue after this decision.

  15. FOR loopunsigned i = 0; i < MAX_SHOTS; ++i

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

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

        Continue after this decision.

      3. Executeshot->x += shot->vx * STEP_MS / 1000.0f;
      4. Decisionshot->x >= LCD_WIDTH
        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. Decisionenemies[e].active && overlaps(shot->x, shot->y - 3.0f, 6, 8, enemies[e].x, enemies[e].y, 16, 12)
            True
              1. Executeshot->active = false;
              2. ExecutedestroyEnemy(&enemies[e]);
              3. Exit enclosing loop or switchbreak;
            False

            Continue after this decision.

      6. Decision!shot->active
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      7. FOR loopunsigned r = 0; r < MAX_ROCKETS; ++r

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

          1. Decisionrockets[r].active && overlaps(shot->x, shot->y - 3.0f, 6, 8, rockets[r].x, rockets[r].y, 8, 12)
            True
              1. Executeshot->active = false;
              2. ExecutedestroyRocket(&rockets[r]);
              3. Exit enclosing loop or switchbreak;
            False

            Continue after this decision.

      8. Decision!shot->active
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      9. FOR loopunsigned t = 0; t < MAX_TARGETS; ++t

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

          1. ExecuteGroundTarget *target = &targets[t];
          2. Decision!target->active
            True
            1. Next loop iterationcontinue;
            False

            Continue after this decision.

          3. Executeconst float screenX = target->x - scroll;
          4. Executeconst float targetY = terrainHeight(target->x) - 12.0f;
          5. Executeconst float targetWidth = target->type == TARGET_ROCKET ? 8.0f : 12.0f;
          6. Decisionoverlaps(shot->x, shot->y - 3.0f, 6, 8, screenX - targetWidth / 2.0f, targetY, targetWidth, 12.0f)
            True
              1. Executeshot->active = false;
              2. ExecutedestroyTarget(target);
              3. Exit enclosing loop or switchbreak;
            False

            Continue after this decision.

  16. FOR loopunsigned i = 0; i < MAX_BOMBS; ++i

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

      1. ExecuteObject *falling = &bombs[i];
      2. Decision!falling->active
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      3. Decisionfalling->x >= bombDropAt[i]
        True
          1. Executefalling->vx = 5.0f;
          2. Executefalling->vy = 180.0f;
        False

        Continue after this decision.

      4. Executefalling->x += falling->vx * STEP_MS / 1000.0f;
      5. Executefalling->y += falling->vy * STEP_MS / 1000.0f;
      6. Executeconst float ground = terrainHeight(falling->x + scroll);
      7. Decisionfalling->y >= ground
        True
          1. Executefalling->active = false;
          2. ExecutestartEffect(falling->x, ground - 3.0f, COLOR_ORANGE);
          3. ExecuteaudioPlay(AUDIO_SFX_EXPLOSION);
          4. FOR loopunsigned t = 0; t < MAX_TARGETS; ++t

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

              1. ExecuteGroundTarget *target = &targets[t];
              2. Executeconst float screenX = target->x - scroll;
              3. Decisiontarget->active && fabsf(falling->x - screenX) < 13.0f
                True
                1. ExecutedestroyTarget(target);
                False

                Continue after this decision.

        False

        Continue after this decision.

  17. ExecuteenemyDelay = reduceTimer(enemyDelay);
  18. DecisionenemyDelay == 0
    True
      1. ExecutespawnEnemy();
      2. ExecuteenemyDelay = ENEMY_SPAWN_MIN_MS + esp_random() % ENEMY_SPAWN_RANGE_MS;
    False

    Continue after this decision.

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

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

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

        Continue after this decision.

      3. Executeenemy->x += enemy->vx * STEP_MS / 1000.0f;
      4. Decisionenemy->x < -16.0f
        True
        1. Executeenemy->active = false;
        False
        1. Decisionoverlaps(player.x, player.y, PLAYER_W, PLAYER_H, enemy->x, enemy->y, 16, 12)
          True
            1. Executeenemy->active = false;
            2. ExecutehitPlayer();
          False

          Continue after this decision.

  20. FOR loopunsigned i = 0; i < MAX_ROCKETS; ++i

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

      1. ExecuteObject *rocket = &rockets[i];
      2. Decision!rocket->active
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      3. Executeconst float maxSideSpeed = 52.0f + wave * 6.0f;
      4. Executeconst float desiredVx = clampValue((player.x + PLAYER_W / 2.0f - rocket->x) * (0.34f + wave * 0.05f), -maxSideSpeed, maxSideSpeed);
      5. Executeconst float steering = clampValue(0.24f + (wave - 1U) * 0.09f, 0.24f, 0.78f);
      6. Executerocket->vx += (desiredVx - rocket->vx) * steering * 0.12f;
      7. Executerocket->x += rocket->vx * STEP_MS / 1000.0f;
      8. Executerocket->y += rocket->vy * STEP_MS / 1000.0f;
      9. Decisionrocket->y < -12.0f || rocket->x < -12.0f || rocket->x >= LCD_WIDTH
        True
          1. Executerocket->active = false;
        False
        1. Decisionoverlaps(player.x, player.y, PLAYER_W, PLAYER_H, rocket->x, rocket->y, 8, 12)
          True
            1. Executerocket->active = false;
            2. ExecutehitPlayer();
          False

          Continue after this decision.

  21. Executeconst float playerGround = terrainHeight(player.x + scroll);
  22. Decisionplayer.y + PLAYER_H >= playerGround
    True
    1. ExecutehitPlayer();
    False

    Continue after this decision.

Read original function source
static void simulateStep(float velocityX, float velocityY, bool fire, bool bomb)
{
    fireDelay = reduceTimer(fireDelay);
    bombDelay = reduceTimer(bombDelay);
    for (unsigned i = 0; i < MAX_EFFECTS; ++i)
        effects[i].remaining = reduceTimer(effects[i].remaining);
    if (!player.active) {
        if (lastUpdate - stateStarted >= RESPAWN_MS) respawnPlayer();
        return;
    }

    player.x = clampValue(player.x + velocityX * STEP_MS / 1000.0f, 8.0f, 160.0f);
    player.y = clampValue(player.y + velocityY * STEP_MS / 1000.0f, PLAY_TOP + 3.0f, 116.0f);
    scroll += SCROLL_SPEED * STEP_MS / 1000.0f;
    fuel -= FUEL_DRAIN_PER_SECOND * STEP_MS / 1000.0f;
    if (fuel <= 0.0f) {
        fuel = 0.0f;
        hitPlayer();
        return;
    }
    if (fire) fireShot();
    if (bomb) dropBomb();
    playerFrame = (uint8_t)((lastUpdate / 120U) & 1U);

    // Reuse each ground slot only after it has moved off-screen. This keeps the
    // scrolling level populated without allocating or tying state to drawing.
    for (unsigned i = 0; i < MAX_TARGETS; ++i) {
        GroundTarget *target = &targets[i];
        if (target->x - scroll >= -16.0f) continue;
        target->x += (TERRAIN_POINTS - 1U) * TERRAIN_SPACING;
        placeTarget(target, target->x);
    }

    for (unsigned i = 0; i < MAX_TARGETS; ++i) {
        GroundTarget *target = &targets[i];
        const float screenX = target->x - scroll;
        if (!target->active || target->type != TARGET_ROCKET || screenX < 0.0f || screenX >= LCD_WIDTH)
            continue;
        target->launchDelay = reduceTimer(target->launchDelay);
        if (target->launchDelay == 0) launchRocket(target);
    }

    for (unsigned i = 0; i < MAX_SHOTS; ++i) {
        Object *shot = &shots[i];
        if (!shot->active) continue;
        shot->x += shot->vx * STEP_MS / 1000.0f;
        if (shot->x >= LCD_WIDTH) { shot->active = false; continue; }
        for (unsigned e = 0; e < MAX_ENEMIES; ++e) {
            // Keep the tracer slim but use a forgiving vertical hit band for
            // the enlarged 16x12 enemy artwork.
            if (enemies[e].active && overlaps(shot->x, shot->y - 3.0f, 6, 8,
                                               enemies[e].x, enemies[e].y, 16, 12)) {
                shot->active = false;
                destroyEnemy(&enemies[e]);
                break;
            }
        }
        if (!shot->active) continue;
        for (unsigned r = 0; r < MAX_ROCKETS; ++r) {
            if (rockets[r].active && overlaps(shot->x, shot->y - 3.0f, 6, 8,
                                              rockets[r].x, rockets[r].y, 8, 12)) {
                shot->active = false;
                destroyRocket(&rockets[r]);
                break;
            }
        }
        if (!shot->active) continue;
        for (unsigned t = 0; t < MAX_TARGETS; ++t) {
            GroundTarget *target = &targets[t];
            if (!target->active) continue;
            const float screenX = target->x - scroll;
            const float targetY = terrainHeight(target->x) - 12.0f;
            const float targetWidth = target->type == TARGET_ROCKET ? 8.0f : 12.0f;
            if (overlaps(shot->x, shot->y - 3.0f, 6, 8,
                         screenX - targetWidth / 2.0f, targetY, targetWidth, 12.0f)) {
                shot->active = false;
                destroyTarget(target);
                break;
            }
        }
    }

    for (unsigned i = 0; i < MAX_BOMBS; ++i) {
        Object *falling = &bombs[i];
        if (!falling->active) continue;
        if (falling->x >= bombDropAt[i]) {
            falling->vx = 5.0f;
            falling->vy = 180.0f;
        }
        falling->x += falling->vx * STEP_MS / 1000.0f;
        falling->y += falling->vy * STEP_MS / 1000.0f;
        const float ground = terrainHeight(falling->x + scroll);
        if (falling->y >= ground) {
            falling->active = false;
            startEffect(falling->x, ground - 3.0f, COLOR_ORANGE);
            audioPlay(AUDIO_SFX_EXPLOSION);
            for (unsigned t = 0; t < MAX_TARGETS; ++t) {
                GroundTarget *target = &targets[t];
                const float screenX = target->x - scroll;
                if (target->active && fabsf(falling->x - screenX) < 13.0f) destroyTarget(target);
            }
        }
    }

    enemyDelay = reduceTimer(enemyDelay);
    if (enemyDelay == 0) {
        spawnEnemy();
        enemyDelay = ENEMY_SPAWN_MIN_MS + esp_random() % ENEMY_SPAWN_RANGE_MS;
    }
    for (unsigned i = 0; i < MAX_ENEMIES; ++i) {
        Object *enemy = &enemies[i];
        if (!enemy->active) continue;
        enemy->x += enemy->vx * STEP_MS / 1000.0f;
        if (enemy->x < -16.0f) enemy->active = false;
        else if (overlaps(player.x, player.y, PLAYER_W, PLAYER_H, enemy->x, enemy->y, 16, 12)) {
            enemy->active = false;
            hitPlayer();
        }
    }
    for (unsigned i = 0; i < MAX_ROCKETS; ++i) {
        Object *rocket = &rockets[i];
        if (!rocket->active) continue;
        const float maxSideSpeed = 52.0f + wave * 6.0f;
        const float desiredVx = clampValue((player.x + PLAYER_W / 2.0f - rocket->x) *
                                           (0.34f + wave * 0.05f),
                                           -maxSideSpeed, maxSideSpeed);
        const float steering = clampValue(0.24f + (wave - 1U) * 0.09f, 0.24f, 0.78f);
        rocket->vx += (desiredVx - rocket->vx) * steering * 0.12f;
        rocket->x += rocket->vx * STEP_MS / 1000.0f;
        rocket->y += rocket->vy * STEP_MS / 1000.0f;
        if (rocket->y < -12.0f || rocket->x < -12.0f || rocket->x >= LCD_WIDTH) {
            rocket->active = false;
        } else if (overlaps(player.x, player.y, PLAYER_W, PLAYER_H,
                            rocket->x, rocket->y, 8, 12)) {
            rocket->active = false;
            hitPlayer();
        }
    }
    const float playerGround = terrainHeight(player.x + scroll);
    if (player.y + PLAYER_H >= playerGround) hitPlayer();
}
axisVelocity() · source line 536
static float axisVelocity(int offset)

Calls into this documentation:

  1. Decisionoffset >= -350 && offset <= 350
    True
    1. Exit this functionreturn 0.0f;
    False

    Continue after this decision.

  2. Executeconst float adjusted = offset > 0 ? (float)offset - 350.0f : (float)offset + 350.0f;
  3. Exit this functionreturn clampValue(adjusted / 1500.0f, -1.0f, 1.0f) * PLAYER_SPEED;
Read original function source
static float axisVelocity(int offset)
{
    if (offset >= -350 && offset <= 350) return 0.0f;
    const float adjusted = offset > 0 ? (float)offset - 350.0f : (float)offset + 350.0f;
    return clampValue(adjusted / 1500.0f, -1.0f, 1.0f) * PLAYER_SPEED;
}
startScramble() · source line 543
void startScramble(uint32_t now_ms)

Calls into this documentation:

  1. Executerunning = true;
  2. ExecutebeginGame(now_ms);
  3. ExecutegameState = STATE_READY;
  4. ExecuteESP_LOGI("scramble", "START");
Read original function source
void startScramble(uint32_t now_ms)
{
    running = true;
    beginGame(now_ms);
    gameState = STATE_READY;
    ESP_LOGI("scramble", "START");
}
stopScramble() · source line 551
void stopScramble(void)

Calls into this documentation:

  1. ExecutecancelHighScoreEntry();
  2. Executerunning = false;
  3. ExecuteESP_LOGI("scramble", "STOP");
Read original function source
void stopScramble(void)
{
    cancelHighScoreEntry();
    running = false;
    ESP_LOGI("scramble", "STOP");
}
scrambleAction() · source line 558
void scrambleAction(bool start, uint32_t now_ms)

Calls into this documentation:

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

    Continue after this decision.

Read original function source
void scrambleAction(bool start, uint32_t now_ms)
{
    if (running && start && gameState == STATE_READY) {
        beginGame(now_ms);
        audioPlay(AUDIO_SFX_GAME_START);
    }
}
updateScramble() · source line 566
void updateScramble(int x_offset, int y_offset, bool valid, bool fire, bool bomb,
                    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 - lastUpdate;
  3. ExecutelastUpdate = now_ms;
  4. DecisiongameState == STATE_FINISHED && now_ms - stateStarted >= 1000U
    True
      1. ExecutestartHighScoreEntry(HIGH_SCORE_SCRAMBLE, score, now_ms);
      2. ExecutegameState = STATE_SCORES;
    False

    Continue after this decision.

  5. DecisiongameState == STATE_SCORES
    True
      1. DecisionupdateHighScoreEntry(x_offset, y_offset, valid, now_ms)
        True
        1. ExecutegameState = STATE_READY;
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  6. DecisiongameState != STATE_PLAYING
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  7. Decision!valid
    True
    1. ExecutecontrolsReady = false;
    False
    1. Decisionx_offset >= -350 && x_offset <= 350 && y_offset >= -350 && y_offset <= 350
      True
      1. ExecutecontrolsReady = true;
      False

      Continue after this decision.

  8. Executeconst float velocityX = valid && controlsReady ? axisVelocity(x_offset) : 0.0f;
  9. Executeconst float velocityY = valid && controlsReady ? -axisVelocity(y_offset) : 0.0f;
  10. Executeaccumulator += elapsed > 100U ? 100U : elapsed;
  11. WHILE loopaccumulator >= STEP_MS && gameState == STATE_PLAYING

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

      1. Executeaccumulator -= STEP_MS;
      2. ExecutesimulateStep(velocityX, velocityY, fire, bomb);
Read original function source
void updateScramble(int x_offset, int y_offset, bool valid, bool fire, bool bomb,
                    uint32_t now_ms)
{
    if (!running) return;
    const uint32_t elapsed = now_ms - lastUpdate;
    lastUpdate = now_ms;
    if (gameState == STATE_FINISHED && now_ms - stateStarted >= 1000U) {
        startHighScoreEntry(HIGH_SCORE_SCRAMBLE, score, now_ms);
        gameState = STATE_SCORES;
    }
    if (gameState == STATE_SCORES) {
        if (updateHighScoreEntry(x_offset, y_offset, valid, now_ms)) gameState = STATE_READY;
        return;
    }
    if (gameState != STATE_PLAYING) return;
    if (!valid) controlsReady = false;
    else if (x_offset >= -350 && x_offset <= 350 && y_offset >= -350 && y_offset <= 350)
        controlsReady = true;
    const float velocityX = valid && controlsReady ? axisVelocity(x_offset) : 0.0f;
    const float velocityY = valid && controlsReady ? -axisVelocity(y_offset) : 0.0f;
    accumulator += elapsed > 100U ? 100U : elapsed;
    while (accumulator >= STEP_MS && gameState == STATE_PLAYING) {
        accumulator -= STEP_MS;
        simulateStep(velocityX, velocityY, fire, bomb);
    }
}
drawTerrain() · source line 593
static void drawTerrain(uint16_t *frame)

Calls into this documentation:

  1. Executeconst float terrainWidth = (TERRAIN_POINTS - 1U) * TERRAIN_SPACING;
  2. Executefloat patternX = fmodf(scroll, terrainWidth);
  3. FOR loopint x = 0; x < LCD_WIDTH; ++x

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

      1. Executeconst int ground = (int)terrainHeightInPattern(patternX);
      2. Executefill_rect(frame, x, ground, 1, LCD_HEIGHT - ground, COLOR_DARKGREEN);
      3. Executefill_rect(frame, x, ground, 1, 2, COLOR_LIME);
      4. ExecutepatternX += 1.0f;
      5. DecisionpatternX >= terrainWidth
        True
        1. ExecutepatternX -= terrainWidth;
        False

        Continue after this decision.

  4. ExecutedrawFlatTerrainEdges(frame);
  5. FOR loopunsigned i = 0; i < MAX_TARGETS; ++i

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

      1. Executeconst GroundTarget *target = &targets[i];
      2. Decision!target->active
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      3. Executeconst int x = (int)(target->x - scroll);
      4. Decisionx < -12 || x >= LCD_WIDTH
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      5. Executeconst int y = (int)terrainHeight(target->x) - 12;
      6. Executeconst uint16_t color = target->type == TARGET_FUEL ? COLOR_CYAN : target->type == TARGET_ROCKET ? COLOR_ORANGERED : COLOR_YELLOW;
      7. Executeconst uint8_t *bitmap = target->type == TARGET_FUEL ? scramble_fuel_bitmap : target->type == TARGET_ROCKET ? scramble_rocket_bitmap : scramble_turret_bitmap;
      8. Executeconst int width = target->type == TARGET_ROCKET ? 8 : 12;
      9. Executedraw_bitmap_scaled(frame, x - width / 2, y, bitmap, 8, 8, width, 12, color);
Read original function source
static void drawTerrain(uint16_t *frame)
{
    const float terrainWidth = (TERRAIN_POINTS - 1U) * TERRAIN_SPACING;
    float patternX = fmodf(scroll, terrainWidth);
    for (int x = 0; x < LCD_WIDTH; ++x) {
        const int ground = (int)terrainHeightInPattern(patternX);
        fill_rect(frame, x, ground, 1, LCD_HEIGHT - ground, COLOR_DARKGREEN);
        fill_rect(frame, x, ground, 1, 2, COLOR_LIME);
        patternX += 1.0f;
        if (patternX >= terrainWidth) patternX -= terrainWidth;
    }
    drawFlatTerrainEdges(frame);
    for (unsigned i = 0; i < MAX_TARGETS; ++i) {
        const GroundTarget *target = &targets[i];
        if (!target->active) continue;
        const int x = (int)(target->x - scroll);
        if (x < -12 || x >= LCD_WIDTH) continue;
        const int y = (int)terrainHeight(target->x) - 12;
        const uint16_t color = target->type == TARGET_FUEL ? COLOR_CYAN :
                               target->type == TARGET_ROCKET ? COLOR_ORANGERED : COLOR_YELLOW;
        const uint8_t *bitmap = target->type == TARGET_FUEL ? scramble_fuel_bitmap :
                                target->type == TARGET_ROCKET ? scramble_rocket_bitmap : scramble_turret_bitmap;
        const int width = target->type == TARGET_ROCKET ? 8 : 12;
        draw_bitmap_scaled(frame, x - width / 2, y, bitmap, 8, 8, width, 12, color);
    }
}
drawHud() · source line 620
static void drawHud(uint16_t *frame)

Calls into this documentation:

  1. Executechar text[32];
  2. Executefill_rect(frame, 0, 0, LCD_WIDTH, PLAY_TOP, COLOR_BLACK);
  3. Executesnprintf(text, sizeof(text), "SCORE %u", (unsigned)score);
  4. Executedraw_text(frame, 3, 3, text, 1, COLOR_WHITE);
  5. Executesnprintf(text, sizeof(text), "LIVES %u", (unsigned)lives);
  6. Executedraw_text_centred(frame, 162, 3, text, 1, COLOR_YELLOW);
  7. Executedraw_text(frame, 245, 3, "FUEL", 1, COLOR_CYAN);
  8. Executedraw_rect(frame, 276, 3, 40, 8, COLOR_WHITE);
  9. Executefill_rect(frame, 278, 5, (int)(fuel * 36.0f / FUEL_MAX), 4, COLOR_LIME);
Read original function source
static void drawHud(uint16_t *frame)
{
    char text[32];
    fill_rect(frame, 0, 0, LCD_WIDTH, PLAY_TOP, COLOR_BLACK);
    snprintf(text, sizeof(text), "SCORE %u", (unsigned)score);
    draw_text(frame, 3, 3, text, 1, COLOR_WHITE);
    snprintf(text, sizeof(text), "LIVES %u", (unsigned)lives);
    draw_text_centred(frame, 162, 3, text, 1, COLOR_YELLOW);
    draw_text(frame, 245, 3, "FUEL", 1, COLOR_CYAN);
    draw_rect(frame, 276, 3, 40, 8, COLOR_WHITE);
    fill_rect(frame, 278, 5, (int)(fuel * 36.0f / FUEL_MAX), 4, COLOR_LIME);
}
drawScramble() · source line 633
void drawScramble(uint16_t *frame)

Calls into this documentation:

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

    Continue after this decision.

  2. DecisiongameState == STATE_SCORES
    True
      1. ExecutedrawHighScoreEntry(frame);
      2. Exit this functionreturn;
    False

    Continue after this decision.

  3. Executeclear(frame, COLOR_BLACK);
  4. ExecutedrawTerrain(frame);
  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. Executedraw_bitmap_scaled(frame, (int)enemies[i].x, (int)enemies[i].y, scramble_enemy_bitmap, 8, 8, 16, 12, COLOR_MAGENTA);
        False

        Continue after this decision.

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

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

      1. Decisionrockets[i].active
        True
        1. Executedraw_bitmap_scaled(frame, (int)rockets[i].x - 4, (int)rockets[i].y, scramble_rocket_bitmap, 8, 8, 8, 12, COLOR_ORANGERED);
        False

        Continue after this decision.

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

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

    1. Decisionshots[i].active
      True
      1. Executefill_rect(frame, (int)shots[i].x, (int)shots[i].y, 6, 2, COLOR_WHITE);
      False

      Continue after this decision.

  8. FOR loopunsigned i = 0; i < MAX_BOMBS; ++i

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

    1. Decisionbombs[i].active
      True
      1. Executefill_rect(frame, (int)bombs[i].x, (int)bombs[i].y, 3, 4, COLOR_YELLOW);
      False

      Continue after this decision.

  9. FOR loopunsigned i = 0; i < MAX_EFFECTS; ++i

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

      1. Decisioneffects[i].remaining != 0
        True
          1. Executeconst unsigned pose = effects[i].remaining > 130U ? 0U : 1U;
          2. Executedraw_bitmap_scaled(frame, (int)effects[i].x - 8, (int)effects[i].y - 6, scramble_explosion_frames[pose], 8, 8, 16, 12, effects[i].color);
        False

        Continue after this decision.

  10. Decisionplayer.active
    True
    1. Executedraw_bitmap_scaled(frame, (int)player.x, (int)player.y, scramble_player_frames[playerFrame], 8, 8, PLAYER_W, PLAYER_H, COLOR_CYAN);
    False

    Continue after this decision.

  11. ExecutedrawHud(frame);
  12. DecisiongameState == STATE_READY
    True
      1. Executeclear(frame, COLOR_BLACK);
      2. Executedraw_text_centred(frame, 160, 20, "SCRAMBLE", 3, COLOR_CYAN);
      3. Executedraw_text_centred(frame, 160, 59, "JOYSTICK FLY", 1, COLOR_WHITE);
      4. Executedraw_text_centred(frame, 160, 76, "A FIRE FORWARD", 1, COLOR_YELLOW);
      5. Executedraw_text_centred(frame, 160, 92, "B DROP BOMB", 1, COLOR_ORANGE);
      6. Executedraw_text_centred(frame, 160, 139, "BOOT BACK", 1, COLOR_LIGHTGRAY);
    False
    1. DecisiongameState == STATE_FINISHED
      True
        1. Executedraw_text_centred(frame, 160, 78, "GAME OVER", 2, COLOR_RED);
      False

      Continue after this decision.

Read original function source
void drawScramble(uint16_t *frame)
{
    if (!running || frame == NULL) return;
    if (gameState == STATE_SCORES) { drawHighScoreEntry(frame); return; }
    clear(frame, COLOR_BLACK);
    drawTerrain(frame);
    for (unsigned i = 0; i < MAX_ENEMIES; ++i) {
        if (enemies[i].active)
            draw_bitmap_scaled(frame, (int)enemies[i].x, (int)enemies[i].y,
                               scramble_enemy_bitmap, 8, 8, 16, 12, COLOR_MAGENTA);
    }
    for (unsigned i = 0; i < MAX_ROCKETS; ++i) {
        if (rockets[i].active)
            draw_bitmap_scaled(frame, (int)rockets[i].x - 4, (int)rockets[i].y,
                               scramble_rocket_bitmap, 8, 8, 8, 12, COLOR_ORANGERED);
    }
    for (unsigned i = 0; i < MAX_SHOTS; ++i)
        if (shots[i].active) fill_rect(frame, (int)shots[i].x, (int)shots[i].y, 6, 2, COLOR_WHITE);
    for (unsigned i = 0; i < MAX_BOMBS; ++i)
        if (bombs[i].active) fill_rect(frame, (int)bombs[i].x, (int)bombs[i].y, 3, 4, COLOR_YELLOW);
    for (unsigned i = 0; i < MAX_EFFECTS; ++i) {
        if (effects[i].remaining != 0) {
            const unsigned pose = effects[i].remaining > 130U ? 0U : 1U;
            draw_bitmap_scaled(frame, (int)effects[i].x - 8, (int)effects[i].y - 6,
                               scramble_explosion_frames[pose], 8, 8, 16, 12, effects[i].color);
        }
    }
    if (player.active)
        draw_bitmap_scaled(frame, (int)player.x, (int)player.y,
                           scramble_player_frames[playerFrame], 8, 8, PLAYER_W, PLAYER_H, COLOR_CYAN);
    drawHud(frame);
    if (gameState == STATE_READY) {
        clear(frame, COLOR_BLACK);
        draw_text_centred(frame, 160, 20, "SCRAMBLE", 3, COLOR_CYAN);
        draw_text_centred(frame, 160, 59, "JOYSTICK FLY", 1, COLOR_WHITE);
        draw_text_centred(frame, 160, 76, "A FIRE FORWARD", 1, COLOR_YELLOW);
        draw_text_centred(frame, 160, 92, "B DROP BOMB", 1, COLOR_ORANGE);
        draw_text_centred(frame, 160, 139, "BOOT BACK", 1, COLOR_LIGHTGRAY);
    } else if (gameState == STATE_FINISHED) {
        draw_text_centred(frame, 160, 78, "GAME OVER", 2, COLOR_RED);
    }
}
Complete original file, including constants and data
/* Coded By JD(2026) */
#error "Reference listing only — not provided as build-ready source."
#include "scramble.h"
#include "audio.h"
#include "game_graphics.h"
#include "PET2001_font.h"
#include "gold.h"
#include "high_scores.h"
#include "esp_random.h"
#include "esp_log.h"
#include <math.h>
#include <stdio.h>
#include <string.h>

#define STEP_MS 10U
#define PLAY_TOP 14
#define PLAY_BOTTOM 154
#define PLAYER_W 16
#define PLAYER_H 12
#define MAX_SHOTS 3
#define MAX_BOMBS 2
#define MAX_ENEMIES 5
#define MAX_ROCKETS 2
#define MAX_TARGETS 6
#define EMPTY_FLAT_ZONES 3
#define FLAT_ZONE_COUNT (MAX_TARGETS + EMPTY_FLAT_ZONES)
#define MAX_EFFECTS 6
#define TERRAIN_POINTS 41
#define TERRAIN_SPACING 8
#define FIRE_DELAY_MS 150U
#define BOMB_DELAY_MS 300U
#define PLAYER_SPEED 105.0f
#define SCROLL_SPEED 30.0f
#define FUEL_DRAIN_PER_SECOND 2.0f
#define FUEL_MAX 100.0f
#define RESPAWN_MS 1200U
#define ENEMY_SPAWN_MIN_MS 650U
#define ENEMY_SPAWN_RANGE_MS 700U

// SCRAMBLE ARTWORK: replace these byte patterns to change the game's pictures.
// Each row is one byte, from top to bottom. Bit 7 is the leftmost pixel. A set
// bit is drawn in the colour stated at its draw call; a clear bit is transparent.
// The player and enemy frames are native 8x8 images, enlarged to 16x12 below.
// If you change a graphic's native size, also change the matching draw call's
// source width and height. Keep the two animation frames for the player.

// Player ship: two flying poses. It faces right; frame 2 is the engine flicker.
static const uint8_t scramble_player_frames[2][8] = {
    {0x08, 0x1C, 0x3F, 0xFF, 0x3F, 0x1C, 0x08, 0x00},
    {0x08, 0x1C, 0x3F, 0xFF, 0x3F, 0x1C, 0x14, 0x08},
};

// Flying enemy: one 8x8 frame, enlarged to 16x12.
static const uint8_t scramble_enemy_bitmap[8] = {
    0x18, 0x3C, 0x7E, 0xDB, 0xFF, 0x24, 0x42, 0x00,
};

// Ground turret: one 8x8 frame, enlarged to 12x12.
static const uint8_t scramble_turret_bitmap[8] = {
    0x00, 0x18, 0x18, 0x7E, 0xFF, 0x7E, 0x3C, 0x00,
};

// Fuel tank: one 8x8 frame, enlarged to 12x12. Destroying it refills fuel.
static const uint8_t scramble_fuel_bitmap[8] = {
    0x3C, 0x7E, 0xFF, 0xDB, 0xDB, 0xFF, 0x7E, 0x3C,
};

// Ground rocket and launched rocket: one vertical 8x8 frame, drawn at 8x12.
// The same artwork is used before and after launch so it is easy to replace.
static const uint8_t scramble_rocket_bitmap[8] = {
    0x18, 0x3C, 0x3C, 0x7E, 0x7E, 0x3C, 0x24, 0x24,
};

// Explosion: the two 8x8 frames alternate while a destroyed object is visible.
static const uint8_t scramble_explosion_frames[2][8] = {
    {0x18, 0x5A, 0x3C, 0xFF, 0x3C, 0x5A, 0x18, 0x00},
    {0x81, 0x24, 0x5A, 0x3C, 0x5A, 0x24, 0x81, 0x00},
};

// TERRAIN PROFILE: one height offset every eight horizontal pixels. Replace
// these values to reshape the landscape. Broad matching values form thick,
// high peaks. Flat shelves are cut into this underlying curve at game start;
// keep offsets roughly between -26 and +18 so the terrain stays on-screen.
static const int8_t scramble_terrain_offsets[TERRAIN_POINTS] = {
     0,  -2,  -6, -12, -18, -22, -24, -24, -24, -24,
   -20, -12,  -2,  14,  14,  14,  10,   2,  -8, -18,
   -24, -24, -24, -18,  -8,   4,  18,  18,  18,  12,
     2, -10, -20, -26, -26, -26, -18,  -6,  16,  16,
    16,
};

// The six ground-target shelves use these curve indices. Each shelf is two
// terrain spaces (16 pixels) wide, with its target centred on the flat top.
static const uint8_t scramble_target_shelf_starts[MAX_TARGETS] = {2, 8, 15, 22, 29, 36};

typedef enum { STATE_READY, STATE_PLAYING, STATE_FINISHED, STATE_SCORES } GameState;
typedef enum { TARGET_TURRET, TARGET_FUEL, TARGET_ROCKET } TargetType;

typedef struct { float x, y, vx, vy; bool active; } Object;
typedef struct { float x, y; TargetType type; uint32_t launchDelay; bool active; } GroundTarget;
typedef struct { float x, y; uint32_t remaining; uint16_t color; } Effect;
typedef struct { uint8_t first, last; float height; } FlatZone;

static GameState gameState;
static Object player, shots[MAX_SHOTS], bombs[MAX_BOMBS], enemies[MAX_ENEMIES], rockets[MAX_ROCKETS];
static float bombDropAt[MAX_BOMBS];
static GroundTarget targets[MAX_TARGETS];
static Effect effects[MAX_EFFECTS];
static float terrain[TERRAIN_POINTS], baseTerrain[TERRAIN_POINTS];
static FlatZone flatZones[FLAT_ZONE_COUNT];
static float scramble_target_lanes[MAX_TARGETS];
static float scroll, fuel;
static uint32_t score, wave, lastUpdate, accumulator, fireDelay, bombDelay;
static uint32_t enemyDelay, stateStarted;
static uint8_t lives, playerFrame, turretsUntilRockets, rocketsInGroup;
static bool running, controlsReady;

static bool overlaps(float ax, float ay, float aw, float ah,
                     float bx, float by, float bw, float bh)
{
    return ax < bx + bw && ax + aw > bx && ay < by + ah && ay + ah > by;
}

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

static uint32_t reduceTimer(uint32_t timer)
{
    return timer > STEP_MS ? timer - STEP_MS : 0;
}

static void addPoints(uint32_t points)
{
    score = score > UINT32_MAX - points ? UINT32_MAX : score + points;
    // Every thousand points advances the internal difficulty used by enemies.
    const uint32_t nextWave = score / 1000U + 1U;
    if (nextWave > wave) wave = nextWave;
}

static float terrainHeightInPattern(float x)
{
    if (x <= 0.0f) return terrain[0];
    const float index = x / TERRAIN_SPACING;
    unsigned left = (unsigned)index;
    if (left >= TERRAIN_POINTS - 1U) return terrain[TERRAIN_POINTS - 1U];
    const float fraction = index - left;
    return terrain[left] + (terrain[left + 1U] - terrain[left]) * fraction;
}

static float terrainHeight(float x)
{
    // The scrolling landscape repeats after its editable 320-pixel pattern.
    x = fmodf(x, (TERRAIN_POINTS - 1U) * TERRAIN_SPACING);
    if (x < 0.0f) x += (TERRAIN_POINTS - 1U) * TERRAIN_SPACING;
    return terrainHeightInPattern(x);
}

static void flattenTerrainZone(unsigned zone, unsigned first)
{
    const unsigned last = first + 2U;
    const float height = baseTerrain[first + 1U];
    flatZones[zone] = (FlatZone){first, last, height};
    for (unsigned point = first; point <= last; ++point) terrain[point] = height;
}

static bool flatZoneAvailable(unsigned first, unsigned count)
{
    const unsigned last = first + 2U;
    for (unsigned zone = 0; zone < count; ++zone) {
        if (last >= flatZones[zone].first && first <= flatZones[zone].last)
            return false;
    }
    return true;
}

static void makeTerrain(void)
{
    for (unsigned i = 0; i < TERRAIN_POINTS; ++i) {
        baseTerrain[i] = terrain[i] = 130.0f + scramble_terrain_offsets[i];
    }
    for (unsigned target = 0; target < MAX_TARGETS; ++target) {
        const unsigned first = scramble_target_shelf_starts[target];
        flattenTerrainZone(target, first);
        scramble_target_lanes[target] = (first + 1U) * TERRAIN_SPACING;
    }
    for (unsigned zone = MAX_TARGETS; zone < FLAT_ZONE_COUNT; ++zone) {
        unsigned first;
        do {
            first = 1U + esp_random() % (TERRAIN_POINTS - 5U);
        } while (!flatZoneAvailable(first, zone));
        flattenTerrainZone(zone, first);
    }
}

static void drawFlatTerrainEdges(uint16_t *frame)
{
    const float patternWidth = (TERRAIN_POINTS - 1U) * TERRAIN_SPACING;
    const int firstPattern = (int)floorf(scroll / patternWidth) - 1;
    for (int pattern = firstPattern; pattern <= firstPattern + 3; ++pattern) {
        for (unsigned zone = 0; zone < FLAT_ZONE_COUNT; ++zone) {
            const FlatZone *flat = &flatZones[zone];
            const int left = (int)(flat->first * TERRAIN_SPACING + pattern * patternWidth - scroll);
            const int right = (int)(flat->last * TERRAIN_SPACING + pattern * patternWidth - scroll);
            // Join each two-space flat shelf back to the underlying curve with
            // sharp vertical lime edges.
            draw_line(frame, left, (int)baseTerrain[flat->first], left,
                      (int)flat->height, COLOR_LIME);
            draw_line(frame, right, (int)baseTerrain[flat->last], right,
                      (int)flat->height, COLOR_LIME);
        }
    }
}

static void startEffect(float x, float y, uint16_t color)
{
    for (unsigned i = 0; i < MAX_EFFECTS; ++i) {
        if (effects[i].remaining == 0) {
            effects[i] = (Effect){x, y, 260U, color};
            return;
        }
    }
}

static TargetType nextTargetType(void)
{
    if (rocketsInGroup != 0) {
        --rocketsInGroup;
        return TARGET_ROCKET;
    }
    if (esp_random() % 3U == 1U) return TARGET_FUEL;
    if (--turretsUntilRockets == 0) {
        // One or two rockets follow every five to eight ordinary turrets.
        rocketsInGroup = 1U + esp_random() % 2U;
        turretsUntilRockets = 5U + esp_random() % 4U;
    }
    return TARGET_TURRET;
}

static void placeTarget(GroundTarget *target, float x)
{
    *target = (GroundTarget){x, terrainHeight(x), nextTargetType(),
                             900U + esp_random() % 1501U, true};
}

static void resetObjects(void)
{
    memset(shots, 0, sizeof(shots));
    memset(bombs, 0, sizeof(bombs));
    memset(bombDropAt, 0, sizeof(bombDropAt));
    memset(enemies, 0, sizeof(enemies));
    memset(rockets, 0, sizeof(rockets));
    memset(effects, 0, sizeof(effects));
    turretsUntilRockets = 5U + esp_random() % 4U;
    rocketsInGroup = 0;
    for (unsigned i = 0; i < MAX_TARGETS; ++i) {
        placeTarget(&targets[i], scramble_target_lanes[i]);
    }
}

static void beginGame(uint32_t now_ms)
{
    makeTerrain();
    scroll = 0.0f;
    fuel = FUEL_MAX;
    score = 0;
    wave = 1;
    lives = 3;
    player = (Object){52.0f, 75.0f, 0.0f, 0.0f, true};
    fireDelay = bombDelay = 0;
    enemyDelay = ENEMY_SPAWN_MIN_MS;
    accumulator = 0;
    playerFrame = 0;
    controlsReady = false;
    resetObjects();
    gameState = STATE_PLAYING;
    lastUpdate = now_ms;
}

static void finishGame(void)
{
    player.active = false;
    gameState = STATE_FINISHED;
    stateStarted = lastUpdate;
    ESP_LOGI("scramble", "GAME OVER: %u", (unsigned)score);
}

static void hitPlayer(void)
{
    if (!player.active) return;
    startEffect(player.x + PLAYER_W / 2.0f, player.y + PLAYER_H / 2.0f, COLOR_YELLOW);
    audioPlay(AUDIO_SFX_PLAYER_EXPLOSION);
    if (--lives == 0) {
        finishGame();
        return;
    }
    player.active = false;
    stateStarted = lastUpdate;
    memset(shots, 0, sizeof(shots));
    memset(bombs, 0, sizeof(bombs));
}

static void respawnPlayer(void)
{
    player = (Object){52.0f, 75.0f, 0.0f, 0.0f, true};
    controlsReady = false;
}

static void fireShot(void)
{
    if (fireDelay != 0 || !player.active) return;
    for (unsigned i = 0; i < MAX_SHOTS; ++i) {
        if (!shots[i].active) {
            shots[i] = (Object){player.x + PLAYER_W, player.y + 3.0f, 240.0f, 0.0f, true};
            fireDelay = FIRE_DELAY_MS;
            audioPlay(AUDIO_SFX_PLAYER_SHOT);
            return;
        }
    }
}

static void dropBomb(void)
{
    if (bombDelay != 0 || !player.active) return;
    for (unsigned i = 0; i < MAX_BOMBS; ++i) {
        if (!bombs[i].active) {
            // Travel one ship length forward, then fall almost straight down.
            bombs[i] = (Object){player.x + 7.0f, player.y + PLAYER_H, 70.0f, 25.0f, true};
            bombDropAt[i] = bombs[i].x + PLAYER_W;
            bombDelay = BOMB_DELAY_MS;
            return;
        }
    }
}

static void spawnEnemy(void)
{
    for (unsigned i = 0; i < MAX_ENEMIES; ++i) {
        if (!enemies[i].active) {
            enemies[i] = (Object){320.0f, 25.0f + esp_random() % 80U,
                                  -(48.0f + wave * 3.0f), 0.0f, true};
            return;
        }
    }
}

static void destroyEnemy(Object *enemy)
{
    enemy->active = false;
    startEffect(enemy->x + 8.0f, enemy->y + 6.0f, COLOR_MAGENTA);
    addPoints(100U);
    audioPlay(AUDIO_SFX_HIT);
}

static void destroyRocket(Object *rocket)
{
    rocket->active = false;
    startEffect(rocket->x + 4.0f, rocket->y + 6.0f, COLOR_ORANGERED);
    addPoints(150U);
    audioPlay(AUDIO_SFX_HIT);
}

static void destroyTarget(GroundTarget *target)
{
    target->active = false;
    startEffect(target->x, target->y - 6.0f, COLOR_ORANGE);
    if (target->type == TARGET_FUEL) {
        fuel = FUEL_MAX;
        addPoints(150U);
        audioPlay(AUDIO_SFX_POWER_UP);
    } else if (target->type == TARGET_ROCKET) {
        addPoints(100U);
        audioPlay(AUDIO_SFX_HIT);
    } else {
        addPoints(50U);
        audioPlay(AUDIO_SFX_HIT);
    }
}

static void launchRocket(GroundTarget *target)
{
    for (unsigned i = 0; i < MAX_ROCKETS; ++i) {
        if (rockets[i].active) continue;
        const float screenX = target->x - scroll;
        const float aim = player.x + PLAYER_W / 2.0f - screenX;
        const float accuracy = clampValue(0.24f + (wave - 1U) * 0.09f, 0.24f, 0.78f);
        rockets[i] = (Object){screenX, terrainHeight(target->x) - 12.0f,
                              aim * accuracy, -(82.0f + wave * 5.0f), true};
        target->active = false;
        startEffect(screenX, terrainHeight(target->x) - 5.0f, COLOR_RED);
        audioPlay(AUDIO_SFX_ENEMY_SHOT);
        return;
    }
}

static void simulateStep(float velocityX, float velocityY, bool fire, bool bomb)
{
    fireDelay = reduceTimer(fireDelay);
    bombDelay = reduceTimer(bombDelay);
    for (unsigned i = 0; i < MAX_EFFECTS; ++i)
        effects[i].remaining = reduceTimer(effects[i].remaining);
    if (!player.active) {
        if (lastUpdate - stateStarted >= RESPAWN_MS) respawnPlayer();
        return;
    }

    player.x = clampValue(player.x + velocityX * STEP_MS / 1000.0f, 8.0f, 160.0f);
    player.y = clampValue(player.y + velocityY * STEP_MS / 1000.0f, PLAY_TOP + 3.0f, 116.0f);
    scroll += SCROLL_SPEED * STEP_MS / 1000.0f;
    fuel -= FUEL_DRAIN_PER_SECOND * STEP_MS / 1000.0f;
    if (fuel <= 0.0f) {
        fuel = 0.0f;
        hitPlayer();
        return;
    }
    if (fire) fireShot();
    if (bomb) dropBomb();
    playerFrame = (uint8_t)((lastUpdate / 120U) & 1U);

    // Reuse each ground slot only after it has moved off-screen. This keeps the
    // scrolling level populated without allocating or tying state to drawing.
    for (unsigned i = 0; i < MAX_TARGETS; ++i) {
        GroundTarget *target = &targets[i];
        if (target->x - scroll >= -16.0f) continue;
        target->x += (TERRAIN_POINTS - 1U) * TERRAIN_SPACING;
        placeTarget(target, target->x);
    }

    for (unsigned i = 0; i < MAX_TARGETS; ++i) {
        GroundTarget *target = &targets[i];
        const float screenX = target->x - scroll;
        if (!target->active || target->type != TARGET_ROCKET || screenX < 0.0f || screenX >= LCD_WIDTH)
            continue;
        target->launchDelay = reduceTimer(target->launchDelay);
        if (target->launchDelay == 0) launchRocket(target);
    }

    for (unsigned i = 0; i < MAX_SHOTS; ++i) {
        Object *shot = &shots[i];
        if (!shot->active) continue;
        shot->x += shot->vx * STEP_MS / 1000.0f;
        if (shot->x >= LCD_WIDTH) { shot->active = false; continue; }
        for (unsigned e = 0; e < MAX_ENEMIES; ++e) {
            // Keep the tracer slim but use a forgiving vertical hit band for
            // the enlarged 16x12 enemy artwork.
            if (enemies[e].active && overlaps(shot->x, shot->y - 3.0f, 6, 8,
                                               enemies[e].x, enemies[e].y, 16, 12)) {
                shot->active = false;
                destroyEnemy(&enemies[e]);
                break;
            }
        }
        if (!shot->active) continue;
        for (unsigned r = 0; r < MAX_ROCKETS; ++r) {
            if (rockets[r].active && overlaps(shot->x, shot->y - 3.0f, 6, 8,
                                              rockets[r].x, rockets[r].y, 8, 12)) {
                shot->active = false;
                destroyRocket(&rockets[r]);
                break;
            }
        }
        if (!shot->active) continue;
        for (unsigned t = 0; t < MAX_TARGETS; ++t) {
            GroundTarget *target = &targets[t];
            if (!target->active) continue;
            const float screenX = target->x - scroll;
            const float targetY = terrainHeight(target->x) - 12.0f;
            const float targetWidth = target->type == TARGET_ROCKET ? 8.0f : 12.0f;
            if (overlaps(shot->x, shot->y - 3.0f, 6, 8,
                         screenX - targetWidth / 2.0f, targetY, targetWidth, 12.0f)) {
                shot->active = false;
                destroyTarget(target);
                break;
            }
        }
    }

    for (unsigned i = 0; i < MAX_BOMBS; ++i) {
        Object *falling = &bombs[i];
        if (!falling->active) continue;
        if (falling->x >= bombDropAt[i]) {
            falling->vx = 5.0f;
            falling->vy = 180.0f;
        }
        falling->x += falling->vx * STEP_MS / 1000.0f;
        falling->y += falling->vy * STEP_MS / 1000.0f;
        const float ground = terrainHeight(falling->x + scroll);
        if (falling->y >= ground) {
            falling->active = false;
            startEffect(falling->x, ground - 3.0f, COLOR_ORANGE);
            audioPlay(AUDIO_SFX_EXPLOSION);
            for (unsigned t = 0; t < MAX_TARGETS; ++t) {
                GroundTarget *target = &targets[t];
                const float screenX = target->x - scroll;
                if (target->active && fabsf(falling->x - screenX) < 13.0f) destroyTarget(target);
            }
        }
    }

    enemyDelay = reduceTimer(enemyDelay);
    if (enemyDelay == 0) {
        spawnEnemy();
        enemyDelay = ENEMY_SPAWN_MIN_MS + esp_random() % ENEMY_SPAWN_RANGE_MS;
    }
    for (unsigned i = 0; i < MAX_ENEMIES; ++i) {
        Object *enemy = &enemies[i];
        if (!enemy->active) continue;
        enemy->x += enemy->vx * STEP_MS / 1000.0f;
        if (enemy->x < -16.0f) enemy->active = false;
        else if (overlaps(player.x, player.y, PLAYER_W, PLAYER_H, enemy->x, enemy->y, 16, 12)) {
            enemy->active = false;
            hitPlayer();
        }
    }
    for (unsigned i = 0; i < MAX_ROCKETS; ++i) {
        Object *rocket = &rockets[i];
        if (!rocket->active) continue;
        const float maxSideSpeed = 52.0f + wave * 6.0f;
        const float desiredVx = clampValue((player.x + PLAYER_W / 2.0f - rocket->x) *
                                           (0.34f + wave * 0.05f),
                                           -maxSideSpeed, maxSideSpeed);
        const float steering = clampValue(0.24f + (wave - 1U) * 0.09f, 0.24f, 0.78f);
        rocket->vx += (desiredVx - rocket->vx) * steering * 0.12f;
        rocket->x += rocket->vx * STEP_MS / 1000.0f;
        rocket->y += rocket->vy * STEP_MS / 1000.0f;
        if (rocket->y < -12.0f || rocket->x < -12.0f || rocket->x >= LCD_WIDTH) {
            rocket->active = false;
        } else if (overlaps(player.x, player.y, PLAYER_W, PLAYER_H,
                            rocket->x, rocket->y, 8, 12)) {
            rocket->active = false;
            hitPlayer();
        }
    }
    const float playerGround = terrainHeight(player.x + scroll);
    if (player.y + PLAYER_H >= playerGround) hitPlayer();
}

static float axisVelocity(int offset)
{
    if (offset >= -350 && offset <= 350) return 0.0f;
    const float adjusted = offset > 0 ? (float)offset - 350.0f : (float)offset + 350.0f;
    return clampValue(adjusted / 1500.0f, -1.0f, 1.0f) * PLAYER_SPEED;
}

void startScramble(uint32_t now_ms)
{
    running = true;
    beginGame(now_ms);
    gameState = STATE_READY;
    ESP_LOGI("scramble", "START");
}

void stopScramble(void)
{
    cancelHighScoreEntry();
    running = false;
    ESP_LOGI("scramble", "STOP");
}

void scrambleAction(bool start, uint32_t now_ms)
{
    if (running && start && gameState == STATE_READY) {
        beginGame(now_ms);
        audioPlay(AUDIO_SFX_GAME_START);
    }
}

void updateScramble(int x_offset, int y_offset, bool valid, bool fire, bool bomb,
                    uint32_t now_ms)
{
    if (!running) return;
    const uint32_t elapsed = now_ms - lastUpdate;
    lastUpdate = now_ms;
    if (gameState == STATE_FINISHED && now_ms - stateStarted >= 1000U) {
        startHighScoreEntry(HIGH_SCORE_SCRAMBLE, score, now_ms);
        gameState = STATE_SCORES;
    }
    if (gameState == STATE_SCORES) {
        if (updateHighScoreEntry(x_offset, y_offset, valid, now_ms)) gameState = STATE_READY;
        return;
    }
    if (gameState != STATE_PLAYING) return;
    if (!valid) controlsReady = false;
    else if (x_offset >= -350 && x_offset <= 350 && y_offset >= -350 && y_offset <= 350)
        controlsReady = true;
    const float velocityX = valid && controlsReady ? axisVelocity(x_offset) : 0.0f;
    const float velocityY = valid && controlsReady ? -axisVelocity(y_offset) : 0.0f;
    accumulator += elapsed > 100U ? 100U : elapsed;
    while (accumulator >= STEP_MS && gameState == STATE_PLAYING) {
        accumulator -= STEP_MS;
        simulateStep(velocityX, velocityY, fire, bomb);
    }
}

static void drawTerrain(uint16_t *frame)
{
    const float terrainWidth = (TERRAIN_POINTS - 1U) * TERRAIN_SPACING;
    float patternX = fmodf(scroll, terrainWidth);
    for (int x = 0; x < LCD_WIDTH; ++x) {
        const int ground = (int)terrainHeightInPattern(patternX);
        fill_rect(frame, x, ground, 1, LCD_HEIGHT - ground, COLOR_DARKGREEN);
        fill_rect(frame, x, ground, 1, 2, COLOR_LIME);
        patternX += 1.0f;
        if (patternX >= terrainWidth) patternX -= terrainWidth;
    }
    drawFlatTerrainEdges(frame);
    for (unsigned i = 0; i < MAX_TARGETS; ++i) {
        const GroundTarget *target = &targets[i];
        if (!target->active) continue;
        const int x = (int)(target->x - scroll);
        if (x < -12 || x >= LCD_WIDTH) continue;
        const int y = (int)terrainHeight(target->x) - 12;
        const uint16_t color = target->type == TARGET_FUEL ? COLOR_CYAN :
                               target->type == TARGET_ROCKET ? COLOR_ORANGERED : COLOR_YELLOW;
        const uint8_t *bitmap = target->type == TARGET_FUEL ? scramble_fuel_bitmap :
                                target->type == TARGET_ROCKET ? scramble_rocket_bitmap : scramble_turret_bitmap;
        const int width = target->type == TARGET_ROCKET ? 8 : 12;
        draw_bitmap_scaled(frame, x - width / 2, y, bitmap, 8, 8, width, 12, color);
    }
}

static void drawHud(uint16_t *frame)
{
    char text[32];
    fill_rect(frame, 0, 0, LCD_WIDTH, PLAY_TOP, COLOR_BLACK);
    snprintf(text, sizeof(text), "SCORE %u", (unsigned)score);
    drawPet2001TextTransparent(frame, 3, 3, text, COLOR_WHITE, 1);
    snprintf(text, sizeof(text), "LIVES %u", (unsigned)lives);
    drawPet2001TextCentredTransparent(frame, 162, 3, text, COLOR_YELLOW, 1);
    drawPet2001TextTransparent(frame, 242, 3, "FUEL", COLOR_CYAN, 1);
    draw_rect(frame, 276, 3, 40, 8, COLOR_WHITE);
    fill_rect(frame, 278, 5, (int)(fuel * 36.0f / FUEL_MAX), 4, COLOR_LIME);
}

void drawScramble(uint16_t *frame)
{
    if (!running || frame == NULL) return;
    if (gameState == STATE_SCORES) { drawHighScoreEntry(frame); return; }
    clear(frame, COLOR_BLACK);
    drawTerrain(frame);
    for (unsigned i = 0; i < MAX_ENEMIES; ++i) {
        if (enemies[i].active)
            draw_bitmap_scaled(frame, (int)enemies[i].x, (int)enemies[i].y,
                               scramble_enemy_bitmap, 8, 8, 16, 12, COLOR_MAGENTA);
    }
    for (unsigned i = 0; i < MAX_ROCKETS; ++i) {
        if (rockets[i].active)
            draw_bitmap_scaled(frame, (int)rockets[i].x - 4, (int)rockets[i].y,
                               scramble_rocket_bitmap, 8, 8, 8, 12, COLOR_ORANGERED);
    }
    for (unsigned i = 0; i < MAX_SHOTS; ++i)
        if (shots[i].active) fill_rect(frame, (int)shots[i].x, (int)shots[i].y, 6, 2, COLOR_WHITE);
    for (unsigned i = 0; i < MAX_BOMBS; ++i)
        if (bombs[i].active) fill_rect(frame, (int)bombs[i].x, (int)bombs[i].y, 3, 4, COLOR_YELLOW);
    for (unsigned i = 0; i < MAX_EFFECTS; ++i) {
        if (effects[i].remaining != 0) {
            const unsigned pose = effects[i].remaining > 130U ? 0U : 1U;
            draw_bitmap_scaled(frame, (int)effects[i].x - 8, (int)effects[i].y - 6,
                               scramble_explosion_frames[pose], 8, 8, 16, 12, effects[i].color);
        }
    }
    if (player.active)
        draw_bitmap_scaled(frame, (int)player.x, (int)player.y,
                           scramble_player_frames[playerFrame], 8, 8, PLAYER_W, PLAYER_H, COLOR_CYAN);
    drawHud(frame);
    if (gameState == STATE_READY) {
        clear(frame, COLOR_BLACK);
        drawPet2001TextCentredTransparent(frame, 160, 20, "SCRAMBLE", COLOR_CYAN, 3);
        drawPet2001TextCentredTransparent(frame, 160, 59, "JOYSTICK FLY", COLOR_WHITE, 1);
        drawPet2001TextCentredTransparent(frame, 160, 76, "A FIRE FORWARD", COLOR_YELLOW, 1);
        drawPet2001TextCentredTransparent(frame, 160, 92, "B DROP BOMB", COLOR_ORANGE, 1);
        drawPet2001TextCentredTransparent(frame, 160, 139, "BOOT BACK", COLOR_LIGHTGRAY, 1);
    } else if (gameState == STATE_FINISHED) {
        drawPet2001TextCentredTransparent(frame, 160, 78, "GAME OVER", COLOR_RED, 2);
    }
}