Crawler

Source: src/crawler.c · 630 lines · 30 functions.

Screen captures

Views of this part running on the Mini Arcade.

Crawler game screen on the Mini Arcade
Crawler, also presented as Centipede in the gallery, during 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

clampValue() · source line 91
static int clampValue(int value, int minimum, int maximum)
  1. Exit this functionreturn value < minimum ? minimum : value > maximum ? maximum : value;
Read original function source
static int clampValue(int value, int minimum, int maximum)
{
    return value < minimum ? minimum : value > maximum ? maximum : value;
}
absoluteValue() · source line 96
static int absoluteValue(int value)
  1. Exit this functionreturn value < 0 ? -value : value;
Read original function source
static int absoluteValue(int value)
{
    return value < 0 ? -value : value;
}
playerDiamondHalfWidth() · source line 101
static int playerDiamondHalfWidth(int y_offset)

Calls into this documentation:

  1. Executeconst int distance = absoluteValue(y_offset);
  2. Decisiondistance > PLAYER_HALF_HEIGHT
    True
    1. Exit this functionreturn -1;
    False

    Continue after this decision.

  3. Exit this functionreturn PLAYER_HALF_WIDTH * (PLAYER_HALF_HEIGHT - distance) / PLAYER_HALF_HEIGHT;
Read original function source
static int playerDiamondHalfWidth(int y_offset)
{
    const int distance = absoluteValue(y_offset);
    if (distance > PLAYER_HALF_HEIGHT) return -1;
    return PLAYER_HALF_WIDTH * (PLAYER_HALF_HEIGHT - distance) / PLAYER_HALF_HEIGHT;
}
crawlerHitsPlayer() · source line 108
static bool crawlerHitsPlayer(const crawler_segment_t *segment)

Calls into this documentation:

  1. Decisionsegment == NULL
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  2. Executeconst int player_centre_x = player_x + PLAYER_HALF_WIDTH;
  3. Executeconst int player_centre_y = player_y + PLAYER_HALF_HEIGHT;
  4. FOR loopint y = segment->y; y < segment->y + SEGMENT_SIZE; ++y

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

      1. Executeconst int half_width = playerDiamondHalfWidth(y - player_centre_y);
      2. Decisionhalf_width < 0
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      3. FOR loopint x = segment->x; x < segment->x + SEGMENT_SIZE; ++x

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

          1. DecisionabsoluteValue(x - player_centre_x) <= half_width
            True
            1. Exit this functionreturn true;
            False

            Continue after this decision.

  5. Exit this functionreturn false;
Read original function source
static bool crawlerHitsPlayer(const crawler_segment_t *segment)
{
    if (segment == NULL) return false;
    const int player_centre_x = player_x + PLAYER_HALF_WIDTH;
    const int player_centre_y = player_y + PLAYER_HALF_HEIGHT;
    for (int y = segment->y; y < segment->y + SEGMENT_SIZE; ++y) {
        const int half_width = playerDiamondHalfWidth(y - player_centre_y);
        if (half_width < 0) continue;
        for (int x = segment->x; x < segment->x + SEGMENT_SIZE; ++x) {
            if (absoluteValue(x - player_centre_x) <= half_width) return true;
        }
    }
    return false;
}
randomCrawlerLength() · source line 123
static unsigned randomCrawlerLength(void)
  1. Exit this functionreturn CRAWLER_MIN_SEGMENTS + esp_random() % (CRAWLER_MAX_SEGMENTS - CRAWLER_MIN_SEGMENTS + 1U);
Read original function source
static unsigned randomCrawlerLength(void)
{
    return CRAWLER_MIN_SEGMENTS +
        esp_random() % (CRAWLER_MAX_SEGMENTS - CRAWLER_MIN_SEGMENTS + 1U);
}
topSpawnReady() · source line 129
static bool topSpawnReady(uint32_t now_ms)
  1. Exit this functionreturn (int32_t)(now_ms - top_spawn_ready_at) >= 0;
Read original function source
static bool topSpawnReady(uint32_t now_ms)
{
    return (int32_t)(now_ms - top_spawn_ready_at) >= 0;
}
sectionMaskFromHead() · source line 134
static uint32_t sectionMaskFromHead(int head_segment)
  1. Executeuint32_t mask = 0U;
  2. FOR loopint segment = head_segment; segment >= 0; segment = followers[(unsigned)segment]

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

      1. Executemask |= 1U << (unsigned)segment;
  3. Exit this functionreturn mask;
Read original function source
static uint32_t sectionMaskFromHead(int head_segment)
{
    uint32_t mask = 0U;
    for (int segment = head_segment; segment >= 0; segment = followers[(unsigned)segment]) {
        mask |= 1U << (unsigned)segment;
    }
    return mask;
}
configureCrawlerSection() · source line 143
static void configureCrawlerSection(unsigned section, unsigned first_segment, unsigned count,
                                    int head_x, int head_y, int direction,
                                    int vertical_direction)
  1. Executeuint32_t mask = 0U;
  2. FOR loopunsigned offset = 0U; offset < count; ++offset

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

      1. Executeconst unsigned segment = first_segment + offset;
      2. Executesegments[segment].x = head_x - direction * (int)offset * SEGMENT_SIZE;
      3. Executesegments[segment].y = head_y;
      4. Executesegments[segment].alive = true;
      5. Executefollowers[segment] = offset + 1U < count ? (int8_t)(segment + 1U) : -1;
      6. Executemask |= 1U << segment;
  3. Executesections[section].segment_mask = mask;
  4. Executesections[section].head_segment = (int)first_segment;
  5. Executesections[section].direction = direction;
  6. Executesections[section].vertical_direction = vertical_direction;
Read original function source
static void configureCrawlerSection(unsigned section, unsigned first_segment, unsigned count,
                                    int head_x, int head_y, int direction,
                                    int vertical_direction)
{
    uint32_t mask = 0U;
    for (unsigned offset = 0U; offset < count; ++offset) {
        const unsigned segment = first_segment + offset;
        segments[segment].x = head_x - direction * (int)offset * SEGMENT_SIZE;
        segments[segment].y = head_y;
        segments[segment].alive = true;
        followers[segment] = offset + 1U < count ? (int8_t)(segment + 1U) : -1;
        mask |= 1U << segment;
    }
    sections[section].segment_mask = mask;
    sections[section].head_segment = (int)first_segment;
    sections[section].direction = direction;
    sections[section].vertical_direction = vertical_direction;
}
resetMushrooms() · source line 162
static void resetMushrooms(void)
  1. FOR loopunsigned row = 0U; row < MUSHROOM_ROWS; ++row

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

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

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

          1. Executemushrooms[row][column] = (esp_random() % 100U) < MUSHROOM_DENSITY_PERCENT ? MUSHROOM_HEALTH : 0U;
Read original function source
static void resetMushrooms(void)
{
    for (unsigned row = 0U; row < MUSHROOM_ROWS; ++row) {
        for (unsigned column = 0U; column < MUSHROOM_COLUMNS; ++column) {
            mushrooms[row][column] = (esp_random() % 100U) < MUSHROOM_DENSITY_PERCENT ?
                MUSHROOM_HEALTH : 0U;
        }
    }
}
resetCrawler() · source line 172
static void resetCrawler(void)

Calls into this documentation:

  1. Executeconst unsigned count = INITIAL_CRAWLER_SEGMENTS;
  2. FOR loopunsigned index = 0U; index < MAX_CRAWLER_SEGMENTS; ++index

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

      1. Executesegments[index].alive = false;
      2. Executefollowers[index] = -1;
      3. Executesections[index].segment_mask = 0U;
      4. Executesections[index].head_segment = -1;
      5. Executesections[index].direction = 0;
      6. Executesections[index].vertical_direction = 0;
  3. ExecuteconfigureCrawlerSection(0U, 0U, count, LCD_WIDTH - (int)count * SEGMENT_SIZE - 10, PLAYFIELD_TOP + 5, -1, 1);
  4. Executetop_spawn_ready_at = 0U;
Read original function source
static void resetCrawler(void)
{
    const unsigned count = INITIAL_CRAWLER_SEGMENTS;
    for (unsigned index = 0U; index < MAX_CRAWLER_SEGMENTS; ++index) {
        segments[index].alive = false;
        followers[index] = -1;
        sections[index].segment_mask = 0U;
        sections[index].head_segment = -1;
        sections[index].direction = 0;
        sections[index].vertical_direction = 0;
    }
    configureCrawlerSection(0U, 0U, count, LCD_WIDTH - (int)count * SEGMENT_SIZE - 10,
                            PLAYFIELD_TOP + 5, -1, 1);
    top_spawn_ready_at = 0U;
}
resetShots() · source line 188
static void resetShots(void)
  1. FOR loopunsigned index = 0U; index < MAX_SHOTS; ++index

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

    1. Executeshots[index].active = false;
Read original function source
static void resetShots(void)
{
    for (unsigned index = 0U; index < MAX_SHOTS; ++index) shots[index].active = false;
}
remainingSegments() · source line 193
static unsigned remainingSegments(void)
  1. Executeunsigned remaining = 0U;
  2. FOR loopunsigned index = 0U; index < MAX_CRAWLER_SEGMENTS; ++index

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

      1. Decisionsegments[index].alive
        True
        1. Execute++remaining;
        False

        Continue after this decision.

  3. Exit this functionreturn remaining;
Read original function source
static unsigned remainingSegments(void)
{
    unsigned remaining = 0U;
    for (unsigned index = 0U; index < MAX_CRAWLER_SEGMENTS; ++index) {
        if (segments[index].alive) ++remaining;
    }
    return remaining;
}
sectionLeader() · source line 202
static int sectionLeader(unsigned section)
  1. Decisionsection >= MAX_CRAWLER_SEGMENTS || sections[section].segment_mask == 0U || sections[section].head_segment < 0
    True
    1. Exit this functionreturn -1;
    False

    Continue after this decision.

  2. Exit this functionreturn sections[section].head_segment;
Read original function source
static int sectionLeader(unsigned section)
{
    if (section >= MAX_CRAWLER_SEGMENTS || sections[section].segment_mask == 0U ||
        sections[section].head_segment < 0) return -1;
    return sections[section].head_segment;
}
segmentIsHead() · source line 209
static bool segmentIsHead(unsigned segment)

Calls into this documentation:

  1. FOR loopunsigned section = 0U; section < MAX_CRAWLER_SEGMENTS; ++section

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

      1. DecisionsectionLeader(section) == (int)segment
        True
        1. Exit this functionreturn true;
        False

        Continue after this decision.

  2. Exit this functionreturn false;
Read original function source
static bool segmentIsHead(unsigned segment)
{
    for (unsigned section = 0U; section < MAX_CRAWLER_SEGMENTS; ++section) {
        if (sectionLeader(section) == (int)segment) return true;
    }
    return false;
}
splitCrawlerAt() · source line 217
static void splitCrawlerAt(unsigned segment)

Calls into this documentation:

  1. Executeconst uint32_t hit_bit = 1U << segment;
  2. FOR loopunsigned section = 0U; section < MAX_CRAWLER_SEGMENTS; ++section

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

      1. Decision(sections[section].segment_mask & hit_bit) == 0U
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      2. Executeconst int original_head = sections[section].head_segment;
      3. Executeconst int original_direction = sections[section].direction;
      4. Executeconst int original_vertical_direction = sections[section].vertical_direction;
      5. Executeint predecessor = -1;
      6. Executeint current = original_head;
      7. WHILE loopcurrent >= 0 && current != (int)segment

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

          1. Executepredecessor = current;
          2. Executecurrent = followers[(unsigned)current];
      8. Decisioncurrent < 0
        True
        1. Exit this functionreturn;
        False

        Continue after this decision.

      9. Executeconst int rear_head = followers[segment];
      10. Decisionpredecessor >= 0
        True
        1. Executefollowers[(unsigned)predecessor] = -1;
        False

        Continue after this decision.

      11. Executefollowers[segment] = -1;
      12. Decisionpredecessor >= 0
        True
          1. Executesections[section].head_segment = original_head;
          2. Executesections[section].segment_mask = sectionMaskFromHead(original_head);
        False
        1. Decisionrear_head >= 0
          True
            1. Executesections[section].head_segment = rear_head;
            2. Executesections[section].segment_mask = sectionMaskFromHead(rear_head);
          False
            1. Executesections[section].head_segment = -1;
            2. Executesections[section].segment_mask = 0U;
      13. Decisionpredecessor >= 0 && rear_head >= 0
        True
          1. FOR loopunsigned free_section = 0U; free_section < MAX_CRAWLER_SEGMENTS; ++free_section

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

              1. Decisionsections[free_section].segment_mask == 0U
                True
                  1. Executesections[free_section].head_segment = rear_head;
                  2. Executesections[free_section].segment_mask = sectionMaskFromHead(rear_head);
                  3. Executesections[free_section].direction = -original_direction;
                  4. Executesections[free_section].vertical_direction = original_vertical_direction;
                  5. Exit this functionreturn;
                False

                Continue after this decision.

        False

        Continue after this decision.

      14. Exit this functionreturn;
Read original function source
static void splitCrawlerAt(unsigned segment)
{
    const uint32_t hit_bit = 1U << segment;
    for (unsigned section = 0U; section < MAX_CRAWLER_SEGMENTS; ++section) {
        if ((sections[section].segment_mask & hit_bit) == 0U) continue;
        const int original_head = sections[section].head_segment;
        const int original_direction = sections[section].direction;
        const int original_vertical_direction = sections[section].vertical_direction;
        int predecessor = -1;
        int current = original_head;
        while (current >= 0 && current != (int)segment) {
            predecessor = current;
            current = followers[(unsigned)current];
        }
        if (current < 0) return;
        const int rear_head = followers[segment];
        if (predecessor >= 0) followers[(unsigned)predecessor] = -1;
        followers[segment] = -1;
        if (predecessor >= 0) {
            sections[section].head_segment = original_head;
            sections[section].segment_mask = sectionMaskFromHead(original_head);
        } else if (rear_head >= 0) {
            sections[section].head_segment = rear_head;
            sections[section].segment_mask = sectionMaskFromHead(rear_head);
        } else {
            sections[section].head_segment = -1;
            sections[section].segment_mask = 0U;
        }
        if (predecessor >= 0 && rear_head >= 0) {
            for (unsigned free_section = 0U; free_section < MAX_CRAWLER_SEGMENTS;
                 ++free_section) {
                if (sections[free_section].segment_mask == 0U) {
                    sections[free_section].head_segment = rear_head;
                    sections[free_section].segment_mask = sectionMaskFromHead(rear_head);
                    sections[free_section].direction = -original_direction;
                    sections[free_section].vertical_direction = original_vertical_direction;
                    return;
                }
            }
        }
        return;
    }
}
crawlerHitsMushroom() · source line 261
static bool crawlerHitsMushroom(int x, int y)
  1. FOR loopunsigned row = 0U; row < MUSHROOM_ROWS; ++row

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

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

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

          1. Decisionmushrooms[row][column] == 0U
            True
            1. Next loop iterationcontinue;
            False

            Continue after this decision.

          2. Executeconst int mushroom_x = (int)column * MUSHROOM_X_SPACING + MUSHROOM_X_OFFSET;
          3. Executeconst int mushroom_y = PLAYFIELD_TOP + (int)row * MUSHROOM_Y_SPACING + MUSHROOM_Y_OFFSET;
          4. Decisionx < mushroom_x + MUSHROOM_WIDTH && x + SEGMENT_SIZE > mushroom_x && y < mushroom_y + MUSHROOM_HEIGHT && y + SEGMENT_SIZE > mushroom_y
            True
              1. Exit this functionreturn true;
            False

            Continue after this decision.

  2. Exit this functionreturn false;
Read original function source
static bool crawlerHitsMushroom(int x, int y)
{
    for (unsigned row = 0U; row < MUSHROOM_ROWS; ++row) {
        for (unsigned column = 0U; column < MUSHROOM_COLUMNS; ++column) {
            if (mushrooms[row][column] == 0U) continue;
            const int mushroom_x = (int)column * MUSHROOM_X_SPACING + MUSHROOM_X_OFFSET;
            const int mushroom_y = PLAYFIELD_TOP + (int)row * MUSHROOM_Y_SPACING +
                MUSHROOM_Y_OFFSET;
            if (x < mushroom_x + MUSHROOM_WIDTH && x + SEGMENT_SIZE > mushroom_x &&
                y < mushroom_y + MUSHROOM_HEIGHT && y + SEGMENT_SIZE > mushroom_y) {
                return true;
            }
        }
    }
    return false;
}
damageMushroomAt() · source line 278
static bool damageMushroomAt(int x, int y)
  1. FOR loopunsigned row = 0U; row < MUSHROOM_ROWS; ++row

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

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

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

          1. Decisionmushrooms[row][column] == 0U
            True
            1. Next loop iterationcontinue;
            False

            Continue after this decision.

          2. Executeconst int mushroom_x = (int)column * MUSHROOM_X_SPACING + MUSHROOM_X_OFFSET;
          3. Executeconst int mushroom_y = PLAYFIELD_TOP + (int)row * MUSHROOM_Y_SPACING + MUSHROOM_Y_OFFSET;
          4. Decisionx >= mushroom_x && x < mushroom_x + MUSHROOM_WIDTH && y >= mushroom_y && y < mushroom_y + MUSHROOM_HEIGHT
            True
              1. Execute--mushrooms[row][column];
              2. Exit this functionreturn true;
            False

            Continue after this decision.

  2. Exit this functionreturn false;
Read original function source
static bool damageMushroomAt(int x, int y)
{
    for (unsigned row = 0U; row < MUSHROOM_ROWS; ++row) {
        for (unsigned column = 0U; column < MUSHROOM_COLUMNS; ++column) {
            if (mushrooms[row][column] == 0U) continue;
            const int mushroom_x = (int)column * MUSHROOM_X_SPACING + MUSHROOM_X_OFFSET;
            const int mushroom_y = PLAYFIELD_TOP + (int)row * MUSHROOM_Y_SPACING +
                MUSHROOM_Y_OFFSET;
            if (x >= mushroom_x && x < mushroom_x + MUSHROOM_WIDTH &&
                y >= mushroom_y && y < mushroom_y + MUSHROOM_HEIGHT) {
                --mushrooms[row][column];
                return true;
            }
        }
    }
    return false;
}
addMushroomAt() · source line 296
static void addMushroomAt(int x, int y)
  1. Executeconst int centre_x = x + SEGMENT_SIZE / 2;
  2. Executeconst int centre_y = y + SEGMENT_SIZE / 2;
  3. Executeconst int column = (centre_x - MUSHROOM_X_OFFSET + MUSHROOM_X_SPACING / 2) / MUSHROOM_X_SPACING;
  4. Executeconst int row = (centre_y - PLAYFIELD_TOP - MUSHROOM_Y_OFFSET + MUSHROOM_Y_SPACING / 2) / MUSHROOM_Y_SPACING;
  5. Decisionrow >= 0 && row < (int)MUSHROOM_ROWS && column >= 0 && column < (int)MUSHROOM_COLUMNS
    True
      1. Executemushrooms[(unsigned)row][(unsigned)column] = MUSHROOM_HEALTH;
    False

    Continue after this decision.

Read original function source
static void addMushroomAt(int x, int y)
{
    const int centre_x = x + SEGMENT_SIZE / 2;
    const int centre_y = y + SEGMENT_SIZE / 2;
    const int column = (centre_x - MUSHROOM_X_OFFSET + MUSHROOM_X_SPACING / 2) /
        MUSHROOM_X_SPACING;
    const int row = (centre_y - PLAYFIELD_TOP - MUSHROOM_Y_OFFSET + MUSHROOM_Y_SPACING / 2) /
        MUSHROOM_Y_SPACING;
    if (row >= 0 && row < (int)MUSHROOM_ROWS && column >= 0 &&
        column < (int)MUSHROOM_COLUMNS) {
        mushrooms[(unsigned)row][(unsigned)column] = MUSHROOM_HEALTH;
    }
}
beginGame() · source line 310
static void beginGame(uint32_t now_ms)

Calls into this documentation:

  1. Executescore = 0U;
  2. Executewave = 1U;
  3. Executelives = 3U;
  4. Executeplayer_x = (LCD_WIDTH - PLAYER_WIDTH) / 2;
  5. Executeplayer_y = LCD_HEIGHT - PLAYER_HEIGHT - 2;
  6. ExecuteresetMushrooms();
  7. ExecuteresetCrawler();
  8. ExecuteresetShots();
  9. Executecrawler_last_step = now_ms;
  10. Executeshot_last_step = now_ms;
  11. Executenext_fire = now_ms;
  12. Executestate = CRAWLER_PLAYING;
  13. ExecuteaudioPlay(AUDIO_SFX_GAME_START);
Read original function source
static void beginGame(uint32_t now_ms)
{
    score = 0U;
    wave = 1U;
    lives = 3U;
    player_x = (LCD_WIDTH - PLAYER_WIDTH) / 2;
    player_y = LCD_HEIGHT - PLAYER_HEIGHT - 2;
    resetMushrooms();
    resetCrawler();
    resetShots();
    crawler_last_step = now_ms;
    shot_last_step = now_ms;
    next_fire = now_ms;
    state = CRAWLER_PLAYING;
    audioPlay(AUDIO_SFX_GAME_START);
}
loseLife() · source line 327
static void loseLife(uint32_t now_ms)

Calls into this documentation:

  1. Decisionlives > 0U
    True
    1. Execute--lives;
    False

    Continue after this decision.

  2. ExecuteresetShots();
  3. ExecuteaudioPlay(AUDIO_SFX_PLAYER_EXPLOSION);
  4. Decisionlives == 0U
    True
      1. Executestate = CRAWLER_FINISHED;
      2. Executestate_started = now_ms;
      3. ExecuteESP_LOGI("crawler", "GAME OVER: SCORE %u WAVE %u", (unsigned)score, (unsigned)wave);
      4. Exit this functionreturn;
    False

    Continue after this decision.

  5. Executestate = CRAWLER_LIFE_LOST;
  6. Executestate_started = now_ms;
Read original function source
static void loseLife(uint32_t now_ms)
{
    if (lives > 0U) --lives;
    resetShots();
    audioPlay(AUDIO_SFX_PLAYER_EXPLOSION);
    if (lives == 0U) {
        state = CRAWLER_FINISHED;
        state_started = now_ms;
        ESP_LOGI("crawler", "GAME OVER: SCORE %u WAVE %u", (unsigned)score, (unsigned)wave);
        return;
    }
    state = CRAWLER_LIFE_LOST;
    state_started = now_ms;
}
fireShot() · source line 342
static void fireShot(void)

Calls into this documentation:

  1. FOR loopunsigned index = 0U; index < MAX_SHOTS; ++index

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

      1. Decision!shots[index].active
        True
          1. Executeshots[index].x = player_x + PLAYER_WIDTH / 2;
          2. Executeshots[index].y = player_y - 2;
          3. Executeshots[index].active = true;
          4. ExecuteaudioPlay(AUDIO_SFX_PLAYER_SHOT);
          5. Exit this functionreturn;
        False

        Continue after this decision.

Read original function source
static void fireShot(void)
{
    for (unsigned index = 0U; index < MAX_SHOTS; ++index) {
        if (!shots[index].active) {
            shots[index].x = player_x + PLAYER_WIDTH / 2;
            shots[index].y = player_y - 2;
            shots[index].active = true;
            audioPlay(AUDIO_SFX_PLAYER_SHOT);
            return;
        }
    }
}
findFreeSegmentRun() · source line 355
static bool findFreeSegmentRun(unsigned count, unsigned *first_segment)
  1. Decisionfirst_segment == NULL || count == 0U || count > MAX_CRAWLER_SEGMENTS
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  2. FOR loopunsigned first = 0U; first + count <= MAX_CRAWLER_SEGMENTS; ++first

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

      1. Executebool available = true;
      2. FOR loopunsigned offset = 0U; offset < count; ++offset

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

          1. Decisionsegments[first + offset].alive
            True
              1. Executeavailable = false;
              2. Exit enclosing loop or switchbreak;
            False

            Continue after this decision.

      3. Decisionavailable
        True
          1. Execute*first_segment = first;
          2. Exit this functionreturn true;
        False

        Continue after this decision.

  3. Exit this functionreturn false;
Read original function source
static bool findFreeSegmentRun(unsigned count, unsigned *first_segment)
{
    if (first_segment == NULL || count == 0U || count > MAX_CRAWLER_SEGMENTS) return false;
    for (unsigned first = 0U; first + count <= MAX_CRAWLER_SEGMENTS; ++first) {
        bool available = true;
        for (unsigned offset = 0U; offset < count; ++offset) {
            if (segments[first + offset].alive) {
                available = false;
                break;
            }
        }
        if (available) {
            *first_segment = first;
            return true;
        }
    }
    return false;
}
spawnTopCrawler() · source line 374
static void spawnTopCrawler(uint32_t now_ms)

Calls into this documentation:

  1. Decision!topSpawnReady(now_ms)
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Executeconst unsigned count = randomCrawlerLength();
  3. Executeunsigned first_segment = 0U;
  4. Executeunsigned free_section = 0U;
  5. Decision!findFreeSegmentRun(count, &first_segment)
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  6. WHILE loopfree_section < MAX_CRAWLER_SEGMENTS && sections[free_section].segment_mask != 0U

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

      1. Execute++free_section;
  7. Decisionfree_section == MAX_CRAWLER_SEGMENTS
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  8. Executeconst int first_x = (int)(esp_random() % (LCD_WIDTH - (int)count * SEGMENT_SIZE));
  9. Executeconst int direction = (esp_random() & 1U) == 0U ? -1 : 1;
  10. Executeconst int head_x = direction < 0 ? first_x : first_x + (int)(count - 1U) * SEGMENT_SIZE;
  11. ExecuteconfigureCrawlerSection(free_section, first_segment, count, head_x, PLAYFIELD_TOP + 5, direction, 1);
  12. Executetop_spawn_ready_at = now_ms + TOP_SPAWN_GUARD_MS;
Read original function source
static void spawnTopCrawler(uint32_t now_ms)
{
    if (!topSpawnReady(now_ms)) return;
    const unsigned count = randomCrawlerLength();
    unsigned first_segment = 0U;
    unsigned free_section = 0U;
    if (!findFreeSegmentRun(count, &first_segment)) return;
    while (free_section < MAX_CRAWLER_SEGMENTS &&
           sections[free_section].segment_mask != 0U) {
        ++free_section;
    }
    if (free_section == MAX_CRAWLER_SEGMENTS) return;
    const int first_x = (int)(esp_random() %
        (LCD_WIDTH - (int)count * SEGMENT_SIZE));
    const int direction = (esp_random() & 1U) == 0U ? -1 : 1;
    const int head_x = direction < 0 ? first_x : first_x + (int)(count - 1U) * SEGMENT_SIZE;
    configureCrawlerSection(free_section, first_segment, count, head_x, PLAYFIELD_TOP + 5,
                            direction, 1);
    top_spawn_ready_at = now_ms + TOP_SPAWN_GUARD_MS;
}
updateShots() · source line 395
static void updateShots(void)

Calls into this documentation:

  1. FOR loopunsigned index = 0U; index < MAX_SHOTS; ++index

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

      1. Decision!shots[index].active
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      2. Executeshots[index].y -= 4;
      3. Decisionshots[index].y < PLAYFIELD_TOP
        True
          1. Executeshots[index].active = false;
          2. Next loop iterationcontinue;
        False

        Continue after this decision.

      4. Executebool hit_segment = false;
      5. FOR loopunsigned segment = 0U; segment < MAX_CRAWLER_SEGMENTS; ++segment

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

          1. Decision!segments[segment].alive
            True
            1. Next loop iterationcontinue;
            False

            Continue after this decision.

          2. Decisionshots[index].x >= segments[segment].x && shots[index].x < segments[segment].x + SEGMENT_SIZE && shots[index].y >= segments[segment].y && shots[index].y < segments[segment].y + SEGMENT_SIZE
            True
              1. ExecutesplitCrawlerAt(segment);
              2. ExecuteaddMushroomAt(segments[segment].x, segments[segment].y);
              3. Executesegments[segment].alive = false;
              4. Executeshots[index].active = false;
              5. Executescore = score > UINT32_MAX - 10U ? UINT32_MAX : score + 10U;
              6. Executehit_segment = true;
              7. ExecuteaudioPlay(AUDIO_SFX_HIT);
              8. Exit enclosing loop or switchbreak;
            False

            Continue after this decision.

      6. Decision!hit_segment && damageMushroomAt(shots[index].x, shots[index].y)
        True
          1. Executeshots[index].active = false;
        False

        Continue after this decision.

Read original function source
static void updateShots(void)
{
    for (unsigned index = 0U; index < MAX_SHOTS; ++index) {
        if (!shots[index].active) continue;
        shots[index].y -= 4;
        if (shots[index].y < PLAYFIELD_TOP) {
            shots[index].active = false;
            continue;
        }
        bool hit_segment = false;
        for (unsigned segment = 0U; segment < MAX_CRAWLER_SEGMENTS; ++segment) {
            if (!segments[segment].alive) continue;
            if (shots[index].x >= segments[segment].x &&
                shots[index].x < segments[segment].x + SEGMENT_SIZE &&
                shots[index].y >= segments[segment].y &&
                shots[index].y < segments[segment].y + SEGMENT_SIZE) {
                splitCrawlerAt(segment);
                addMushroomAt(segments[segment].x, segments[segment].y);
                segments[segment].alive = false;
                shots[index].active = false;
                score = score > UINT32_MAX - 10U ? UINT32_MAX : score + 10U;
                hit_segment = true;
                audioPlay(AUDIO_SFX_HIT);
                break;
            }
        }
        if (!hit_segment && damageMushroomAt(shots[index].x, shots[index].y)) {
            shots[index].active = false;
        }
    }
}
moveCrawler() · source line 427
static void moveCrawler(uint32_t now_ms)

Calls into this documentation:

  1. Executebool top_spawn_requested = false;
  2. FOR loopunsigned section = 0U; section < MAX_CRAWLER_SEGMENTS; ++section

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

      1. Executeconst int leader = sectionLeader(section);
      2. Decisionleader < 0
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      3. Executeconst int previous_head_x = segments[(unsigned)leader].x;
      4. Executeconst int previous_head_y = segments[(unsigned)leader].y;
      5. Executeconst int next_x = previous_head_x + sections[section].direction * SEGMENT_SIZE;
      6. Executeconst bool blocked = next_x < 0 || next_x + SEGMENT_SIZE > LCD_WIDTH || crawlerHitsMushroom(next_x, previous_head_y);
      7. Decisionblocked
        True
          1. Executesections[section].direction = -sections[section].direction;
          2. Executeint vertical_step = sections[section].vertical_direction * MUSHROOM_Y_SPACING;
          3. Executeconst int next_y = previous_head_y + vertical_step;
          4. Decisionnext_y < PLAYFIELD_TOP || next_y + SEGMENT_SIZE > LCD_HEIGHT
            True
              1. DecisiontopSpawnReady(now_ms)
                True
                  1. Executetop_spawn_requested = true;
                False

                Continue after this decision.

              2. Executesections[section].vertical_direction = -sections[section].vertical_direction;
              3. Executevertical_step = sections[section].vertical_direction * MUSHROOM_Y_SPACING;
            False

            Continue after this decision.

          5. Executesegments[(unsigned)leader].y += vertical_step;
        False
          1. Executesegments[(unsigned)leader].x = next_x;
      8. Executeint follower = followers[(unsigned)leader];
      9. Executeint previous_x = previous_head_x;
      10. Executeint previous_y = previous_head_y;
      11. WHILE loopfollower >= 0

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

          1. Executeconst int follower_x = segments[(unsigned)follower].x;
          2. Executeconst int follower_y = segments[(unsigned)follower].y;
          3. Executesegments[(unsigned)follower].x = previous_x;
          4. Executesegments[(unsigned)follower].y = previous_y;
          5. Executeprevious_x = follower_x;
          6. Executeprevious_y = follower_y;
          7. Executefollower = followers[(unsigned)follower];
  3. Decisiontop_spawn_requested
    True
    1. ExecutespawnTopCrawler(now_ms);
    False

    Continue after this decision.

  4. FOR loopunsigned index = 0U; index < MAX_CRAWLER_SEGMENTS; ++index

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

      1. Decision!segments[index].alive
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      2. DecisioncrawlerHitsPlayer(&segments[index])
        True
          1. ExecuteloseLife(now_ms);
          2. Exit this functionreturn;
        False

        Continue after this decision.

Read original function source
static void moveCrawler(uint32_t now_ms)
{
    bool top_spawn_requested = false;
    for (unsigned section = 0U; section < MAX_CRAWLER_SEGMENTS; ++section) {
        const int leader = sectionLeader(section);
        if (leader < 0) continue;
        const int previous_head_x = segments[(unsigned)leader].x;
        const int previous_head_y = segments[(unsigned)leader].y;
        const int next_x = previous_head_x + sections[section].direction * SEGMENT_SIZE;
        const bool blocked = next_x < 0 || next_x + SEGMENT_SIZE > LCD_WIDTH ||
            crawlerHitsMushroom(next_x, previous_head_y);
        if (blocked) {
            sections[section].direction = -sections[section].direction;
            int vertical_step = sections[section].vertical_direction * MUSHROOM_Y_SPACING;
            const int next_y = previous_head_y + vertical_step;
            if (next_y < PLAYFIELD_TOP || next_y + SEGMENT_SIZE > LCD_HEIGHT) {
                if (topSpawnReady(now_ms)) {
                    top_spawn_requested = true;
                }
                sections[section].vertical_direction = -sections[section].vertical_direction;
                vertical_step = sections[section].vertical_direction * MUSHROOM_Y_SPACING;
            }
            segments[(unsigned)leader].y += vertical_step;
        } else {
            segments[(unsigned)leader].x = next_x;
        }
        int follower = followers[(unsigned)leader];
        int previous_x = previous_head_x;
        int previous_y = previous_head_y;
        while (follower >= 0) {
            const int follower_x = segments[(unsigned)follower].x;
            const int follower_y = segments[(unsigned)follower].y;
            segments[(unsigned)follower].x = previous_x;
            segments[(unsigned)follower].y = previous_y;
            previous_x = follower_x;
            previous_y = follower_y;
            follower = followers[(unsigned)follower];
        }
    }
    if (top_spawn_requested) spawnTopCrawler(now_ms);
    for (unsigned index = 0U; index < MAX_CRAWLER_SEGMENTS; ++index) {
        if (!segments[index].alive) continue;
        if (crawlerHitsPlayer(&segments[index])) {
            loseLife(now_ms);
            return;
        }
    }
}
startCrawler() · source line 476
void startCrawler(uint32_t now_ms)

Calls into this documentation:

  1. Executerunning = true;
  2. Executestate = CRAWLER_READY;
  3. Executescore = 0U;
  4. Executewave = 1U;
  5. Executelives = 3U;
  6. Executeplayer_x = (LCD_WIDTH - PLAYER_WIDTH) / 2;
  7. Executeplayer_y = LCD_HEIGHT - PLAYER_HEIGHT - 2;
  8. ExecuteresetMushrooms();
  9. ExecuteresetCrawler();
  10. ExecuteresetShots();
  11. Executelast_update = now_ms;
  12. Executecrawler_last_step = now_ms;
  13. Executeshot_last_step = now_ms;
  14. Executenext_fire = now_ms;
  15. Executecontrols_ready = false;
  16. ExecuteESP_LOGI("crawler", "START");
Read original function source
void startCrawler(uint32_t now_ms)
{
    running = true;
    state = CRAWLER_READY;
    score = 0U;
    wave = 1U;
    lives = 3U;
    player_x = (LCD_WIDTH - PLAYER_WIDTH) / 2;
    player_y = LCD_HEIGHT - PLAYER_HEIGHT - 2;
    resetMushrooms();
    resetCrawler();
    resetShots();
    last_update = now_ms;
    crawler_last_step = now_ms;
    shot_last_step = now_ms;
    next_fire = now_ms;
    controls_ready = false;
    ESP_LOGI("crawler", "START");
}
stopCrawler() · source line 496
void stopCrawler(void)

Calls into this documentation:

  1. ExecutecancelHighScoreEntry();
  2. Executerunning = false;
  3. ExecuteESP_LOGI("crawler", "STOP");
Read original function source
void stopCrawler(void)
{
    cancelHighScoreEntry();
    running = false;
    ESP_LOGI("crawler", "STOP");
}
crawlerAction() · source line 503
void crawlerAction(bool action, bool fire_held, uint32_t now_ms)

Calls into this documentation:

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

    Continue after this decision.

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

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  3. Decisionstate != CRAWLER_PLAYING || !fire_held || now_ms < next_fire
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  4. ExecutefireShot();
  5. Executenext_fire = now_ms + FIRE_INTERVAL_MS;
Read original function source
void crawlerAction(bool action, bool fire_held, uint32_t now_ms)
{
    if (!running) return;
    if (state == CRAWLER_READY) {
        if (action) beginGame(now_ms);
        return;
    }
    if (state != CRAWLER_PLAYING || !fire_held || now_ms < next_fire) return;
    fireShot();
    next_fire = now_ms + FIRE_INTERVAL_MS;
}
updateCrawler() · source line 515
void updateCrawler(int x_offset, int y_offset, bool joystick_valid, bool left, bool right,
                   uint32_t now_ms)

Calls into this documentation:

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

    Continue after this decision.

  2. Executeconst uint32_t elapsed = now_ms - last_update;
  3. Executelast_update = now_ms;
  4. Decisionstate == CRAWLER_FINISHED
    True
      1. Decisionnow_ms - state_started >= 1000U
        True
          1. ExecutestartHighScoreEntry(HIGH_SCORE_CRAWLER, score, now_ms);
          2. Executestate = CRAWLER_SCORES;
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

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

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  6. Decisionstate == CRAWLER_LIFE_LOST
    True
      1. Decisionnow_ms - state_started >= LIFE_LOST_MS
        True
          1. Executeplayer_x = (LCD_WIDTH - PLAYER_WIDTH) / 2;
          2. Executeplayer_y = LCD_HEIGHT - PLAYER_HEIGHT - 2;
          3. ExecuteresetCrawler();
          4. Executecrawler_last_step = now_ms;
          5. Executestate = CRAWLER_PLAYING;
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  7. Decisionstate != CRAWLER_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. Executeconst int direction = left == right ? 0 : left ? -1 : 1;
  10. Executeconst int joystick_move = joystick_valid && controls_ready && !left && !right ? x_offset / 600 : 0;
  11. Executeconst int joystick_vertical_move = joystick_valid && controls_ready ? y_offset / 600 : 0;
  12. Executeconst int elapsed_limited = (int)(elapsed > 30U ? 30U : elapsed);
  13. Executeconst int button_move = direction * elapsed_limited / 2;
  14. Executeplayer_x = clampValue(player_x + (left || right ? button_move : joystick_move), 0, LCD_WIDTH - PLAYER_WIDTH);
  15. Executeplayer_y = clampValue(player_y - joystick_vertical_move, PLAYER_ZONE_TOP, LCD_HEIGHT - PLAYER_HEIGHT - 2);
  16. WHILE loopnow_ms - shot_last_step >= SHOT_STEP_MS

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

      1. Executeshot_last_step += SHOT_STEP_MS;
      2. ExecuteupdateShots();
  17. WHILE loopnow_ms - crawler_last_step >= CRAWLER_STEP_MS && state == CRAWLER_PLAYING

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

      1. Executecrawler_last_step += CRAWLER_STEP_MS;
      2. ExecutemoveCrawler(now_ms);
  18. Decisionstate == CRAWLER_PLAYING && remainingSegments() == 0U
    True
      1. ExecuteaudioPlay(AUDIO_SFX_WAVE_CLEAR);
      2. Decisionwave < UINT32_MAX
        True
        1. Execute++wave;
        False

        Continue after this decision.

      3. ExecuteresetCrawler();
    False

    Continue after this decision.

Read original function source
void updateCrawler(int x_offset, int y_offset, bool joystick_valid, bool left, bool right,
                   uint32_t now_ms)
{
    if (!running) return;
    const uint32_t elapsed = now_ms - last_update;
    last_update = now_ms;
    if (state == CRAWLER_FINISHED) {
        if (now_ms - state_started >= 1000U) {
            startHighScoreEntry(HIGH_SCORE_CRAWLER, score, now_ms);
            state = CRAWLER_SCORES;
        }
        return;
    }
    if (state == CRAWLER_SCORES) {
        if (updateHighScoreEntry(x_offset, y_offset, joystick_valid, now_ms)) state = CRAWLER_READY;
        return;
    }
    if (state == CRAWLER_LIFE_LOST) {
        if (now_ms - state_started >= LIFE_LOST_MS) {
            player_x = (LCD_WIDTH - PLAYER_WIDTH) / 2;
            player_y = LCD_HEIGHT - PLAYER_HEIGHT - 2;
            resetCrawler();
            crawler_last_step = now_ms;
            state = CRAWLER_PLAYING;
        }
        return;
    }
    if (state != CRAWLER_PLAYING) return;
    if (!joystick_valid) controls_ready = false;
    else if (x_offset >= -350 && x_offset <= 350) controls_ready = true;
    const int direction = left == right ? 0 : left ? -1 : 1;
    const int joystick_move = joystick_valid && controls_ready && !left && !right
        ? x_offset / 600 : 0;
    const int joystick_vertical_move = joystick_valid && controls_ready ? y_offset / 600 : 0;
    const int elapsed_limited = (int)(elapsed > 30U ? 30U : elapsed);
    const int button_move = direction * elapsed_limited / 2;
    player_x = clampValue(player_x + (left || right ? button_move : joystick_move), 0,
                          LCD_WIDTH - PLAYER_WIDTH);
    player_y = clampValue(player_y - joystick_vertical_move, PLAYER_ZONE_TOP,
                          LCD_HEIGHT - PLAYER_HEIGHT - 2);
    while (now_ms - shot_last_step >= SHOT_STEP_MS) {
        shot_last_step += SHOT_STEP_MS;
        updateShots();
    }
    while (now_ms - crawler_last_step >= CRAWLER_STEP_MS && state == CRAWLER_PLAYING) {
        crawler_last_step += CRAWLER_STEP_MS;
        moveCrawler(now_ms);
    }
    if (state == CRAWLER_PLAYING && remainingSegments() == 0U) {
        audioPlay(AUDIO_SFX_WAVE_CLEAR);
        if (wave < UINT32_MAX) ++wave;
        resetCrawler();
    }
}
drawCrawler() · source line 570
void drawCrawler(uint16_t *frame)

Calls into this documentation:

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

    Continue after this decision.

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

    Continue after this decision.

  3. Executeclear(frame, COLOR_BLACK);
  4. Executechar text[28];
  5. Decisionstate == CRAWLER_READY
    True
      1. Executedraw_text_centred(frame, LCD_WIDTH / 2, 10, "CRAWLER", 2, COLOR_LIME);
      2. Executedraw_text_centred(frame, LCD_WIDTH / 2, 42, "CLEAR THE CRAWLER", 1, COLOR_CYAN);
      3. Executedraw_text_centred(frame, LCD_WIDTH / 2, 63, "10 POINTS EACH", 1, COLOR_YELLOW);
      4. Executedraw_text_centred(frame, LCD_WIDTH / 2, 109, "D/B OR STICK MOVE", 1, COLOR_LIGHTGRAY);
      5. Executedraw_text_centred(frame, LCD_WIDTH / 2, 124, "A/K FIRE", 1, COLOR_LIGHTGRAY);
      6. Executedraw_text_centred(frame, LCD_WIDTH / 2, 151, "A/K START", 2, COLOR_YELLOW);
      7. Exit this functionreturn;
    False

    Continue after this decision.

  6. Executesnprintf(text, sizeof(text), "SCORE %u", (unsigned)score);
  7. Executedraw_text(frame, 4, 3, text, 1, COLOR_WHITE);
  8. Executesnprintf(text, sizeof(text), "LIVES %u", (unsigned)lives);
  9. Executedraw_text_centred(frame, LCD_WIDTH / 2, 3, text, 1, COLOR_CYAN);
  10. Executesnprintf(text, sizeof(text), "WAVE %u", (unsigned)wave);
  11. Executedraw_text(frame, 253, 3, text, 1, COLOR_YELLOW);
  12. Executedraw_line(frame, 0, 13, LCD_WIDTH - 1, 13, COLOR_DARKGRAY);
  13. FOR loopunsigned row = 0U; row < MUSHROOM_ROWS; ++row

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

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

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

          1. Decisionmushrooms[row][column] == 0U
            True
            1. Next loop iterationcontinue;
            False

            Continue after this decision.

          2. Executeconst int x = (int)column * MUSHROOM_X_SPACING + MUSHROOM_X_OFFSET;
          3. Executeconst int y = PLAYFIELD_TOP + (int)row * MUSHROOM_Y_SPACING + MUSHROOM_Y_OFFSET;
          4. Executeconst uint16_t cap_colour = mushrooms[row][column] == 4U ? COLOR_MAGENTA : mushrooms[row][column] == 3U ? COLOR_CYAN : mushrooms[row][column] == 2U ? COLOR_YELLOW : COLOR_WHITE;
          5. Executedraw_ui_bitmap_8x8(frame, x, y, mushroom_bitmap, 1, cap_colour);
  14. FOR loopunsigned index = 0U; index < MAX_CRAWLER_SEGMENTS; ++index

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

      1. Decision!segments[index].alive
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      2. Executeconst uint16_t colour = segmentIsHead(index) ? COLOR_RED : COLOR_LIME;
      3. Executefill_circle(frame, segments[index].x + SEGMENT_SIZE / 2, segments[index].y + SEGMENT_SIZE / 2, SEGMENT_SIZE / 2, colour);
  15. FOR loopunsigned index = 0U; index < MAX_SHOTS; ++index

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

      1. Decisionshots[index].active
        True
        1. Executedraw_line(frame, shots[index].x, shots[index].y, shots[index].x, shots[index].y + 4, COLOR_WHITE);
        False

        Continue after this decision.

  16. Executeconst int player_centre_x = player_x + PLAYER_HALF_WIDTH;
  17. Executeconst int player_centre_y = player_y + PLAYER_HALF_HEIGHT;
  18. FOR loopint y_offset = -PLAYER_HALF_HEIGHT; y_offset <= PLAYER_HALF_HEIGHT; ++y_offset

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

      1. Executeconst int half_width = playerDiamondHalfWidth(y_offset);
      2. Executefill_rect(frame, player_centre_x - half_width, player_centre_y + y_offset, half_width * 2 + 1, 1, COLOR_CYAN);
  19. Decisionstate == CRAWLER_LIFE_LOST
    True
      1. Executedraw_text_centred(frame, LCD_WIDTH / 2, 83, "LIFE LOST", 2, COLOR_RED);
    False
    1. Decisionstate == CRAWLER_FINISHED
      True
        1. Executedraw_text_centred(frame, LCD_WIDTH / 2, 83, "GAME OVER", 2, COLOR_RED);
      False

      Continue after this decision.

Read original function source
void drawCrawler(uint16_t *frame)
{
    if (!running || frame == NULL) return;
    if (state == CRAWLER_SCORES) {
        drawHighScoreEntry(frame);
        return;
    }
    clear(frame, COLOR_BLACK);
    char text[28];
    if (state == CRAWLER_READY) {
        draw_text_centred(frame, LCD_WIDTH / 2, 10, "CRAWLER", 2, COLOR_LIME);
        draw_text_centred(frame, LCD_WIDTH / 2, 42, "CLEAR THE CRAWLER", 1, COLOR_CYAN);
        draw_text_centred(frame, LCD_WIDTH / 2, 63, "10 POINTS EACH", 1, COLOR_YELLOW);
        draw_text_centred(frame, LCD_WIDTH / 2, 109, "D/B OR STICK MOVE", 1, COLOR_LIGHTGRAY);
        draw_text_centred(frame, LCD_WIDTH / 2, 124, "A/K FIRE", 1, COLOR_LIGHTGRAY);
        draw_text_centred(frame, LCD_WIDTH / 2, 151, "A/K START", 2, COLOR_YELLOW);
        return;
    }
    snprintf(text, sizeof(text), "SCORE %u", (unsigned)score);
    draw_text(frame, 4, 3, text, 1, COLOR_WHITE);
    snprintf(text, sizeof(text), "LIVES %u", (unsigned)lives);
    draw_text_centred(frame, LCD_WIDTH / 2, 3, text, 1, COLOR_CYAN);
    snprintf(text, sizeof(text), "WAVE %u", (unsigned)wave);
    draw_text(frame, 253, 3, text, 1, COLOR_YELLOW);
    draw_line(frame, 0, 13, LCD_WIDTH - 1, 13, COLOR_DARKGRAY);
    for (unsigned row = 0U; row < MUSHROOM_ROWS; ++row) {
        for (unsigned column = 0U; column < MUSHROOM_COLUMNS; ++column) {
            if (mushrooms[row][column] == 0U) continue;
            const int x = (int)column * MUSHROOM_X_SPACING + MUSHROOM_X_OFFSET;
            const int y = PLAYFIELD_TOP + (int)row * MUSHROOM_Y_SPACING +
                MUSHROOM_Y_OFFSET;
            const uint16_t cap_colour = mushrooms[row][column] == 4U ? COLOR_MAGENTA :
                mushrooms[row][column] == 3U ? COLOR_CYAN :
                mushrooms[row][column] == 2U ? COLOR_YELLOW : COLOR_WHITE;
            draw_ui_bitmap_8x8(frame, x, y, mushroom_bitmap, 1, cap_colour);
        }
    }
    for (unsigned index = 0U; index < MAX_CRAWLER_SEGMENTS; ++index) {
        if (!segments[index].alive) continue;
        const uint16_t colour = segmentIsHead(index) ? COLOR_RED : COLOR_LIME;
        fill_circle(frame, segments[index].x + SEGMENT_SIZE / 2,
                    segments[index].y + SEGMENT_SIZE / 2, SEGMENT_SIZE / 2, colour);
    }
    for (unsigned index = 0U; index < MAX_SHOTS; ++index) {
        if (shots[index].active) draw_line(frame, shots[index].x, shots[index].y,
                                            shots[index].x, shots[index].y + 4, COLOR_WHITE);
    }
    const int player_centre_x = player_x + PLAYER_HALF_WIDTH;
    const int player_centre_y = player_y + PLAYER_HALF_HEIGHT;
    for (int y_offset = -PLAYER_HALF_HEIGHT; y_offset <= PLAYER_HALF_HEIGHT; ++y_offset) {
        const int half_width = playerDiamondHalfWidth(y_offset);
        fill_rect(frame, player_centre_x - half_width, player_centre_y + y_offset,
                  half_width * 2 + 1, 1, COLOR_CYAN);
    }
    if (state == CRAWLER_LIFE_LOST) {
        draw_text_centred(frame, LCD_WIDTH / 2, 83, "LIFE LOST", 2, COLOR_RED);
    } else if (state == CRAWLER_FINISHED) {
        draw_text_centred(frame, LCD_WIDTH / 2, 83, "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 "crawler.h"

#include "audio.h"
#include "game_graphics.h"
#include "PET2001_font.h"
#include "high_scores.h"

#include "esp_log.h"
#include "esp_random.h"

#include <limits.h>
#include <stdio.h>

#define PLAYFIELD_TOP 15
#define PLAYER_ZONE_TOP PLAYFIELD_TOP
#define PLAYER_WIDTH 15
#define PLAYER_HEIGHT 9
#define PLAYER_HALF_WIDTH (PLAYER_WIDTH / 2)
#define PLAYER_HALF_HEIGHT (PLAYER_HEIGHT / 2)
#define SEGMENT_SIZE 7
#define INITIAL_CRAWLER_SEGMENTS 12U
#define CRAWLER_MIN_SEGMENTS 3U
#define CRAWLER_MAX_SEGMENTS 8U
#define MAX_CRAWLER_SEGMENTS 32U
#define MAX_SHOTS 10U
#define MUSHROOM_COLUMNS 20U
#define MUSHROOM_ROWS 15U
#define MUSHROOM_X_SPACING 16
#define MUSHROOM_Y_SPACING 8
#define MUSHROOM_X_OFFSET 5
#define MUSHROOM_Y_OFFSET 1
#define MUSHROOM_WIDTH 8
#define MUSHROOM_HEIGHT 8
#define MUSHROOM_DENSITY_PERCENT 8U
#define MUSHROOM_HEALTH 4U
#define CRAWLER_STEP_MS 75U
#define SHOT_STEP_MS 18U
#define FIRE_INTERVAL_MS 110U
#define TOP_SPAWN_GUARD_MS 300U
#define LIFE_LOST_MS 900U

typedef enum {
    CRAWLER_READY,
    CRAWLER_PLAYING,
    CRAWLER_LIFE_LOST,
    CRAWLER_FINISHED,
    CRAWLER_SCORES,
} crawler_state_t;

typedef struct {
    int x;
    int y;
    bool alive;
} crawler_segment_t;

typedef struct {
    int x;
    int y;
    bool active;
} crawler_shot_t;

typedef struct {
    uint32_t segment_mask;
    int head_segment;
    int direction;
    int vertical_direction;
} crawler_section_t;

static crawler_segment_t segments[MAX_CRAWLER_SEGMENTS];
static int8_t followers[MAX_CRAWLER_SEGMENTS];
static crawler_shot_t shots[MAX_SHOTS];
static crawler_section_t sections[MAX_CRAWLER_SEGMENTS];
static uint8_t mushrooms[MUSHROOM_ROWS][MUSHROOM_COLUMNS];
static const uint8_t mushroom_bitmap[8] = {
    0x00, 0x1C, 0x3E, 0x7F, 0x7F, 0x7F, 0x1C, 0x1C,
};
static crawler_state_t state;
static uint32_t score;
static uint32_t wave;
static uint32_t last_update;
static uint32_t crawler_last_step;
static uint32_t shot_last_step;
static uint32_t next_fire;
static uint32_t state_started;
static uint8_t lives;
static int player_x;
static int player_y;
static uint32_t top_spawn_ready_at;
static bool controls_ready;
static bool running;

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

static int absoluteValue(int value)
{
    return value < 0 ? -value : value;
}

static int playerDiamondHalfWidth(int y_offset)
{
    const int distance = absoluteValue(y_offset);
    if (distance > PLAYER_HALF_HEIGHT) return -1;
    return PLAYER_HALF_WIDTH * (PLAYER_HALF_HEIGHT - distance) / PLAYER_HALF_HEIGHT;
}

static bool crawlerHitsPlayer(const crawler_segment_t *segment)
{
    if (segment == NULL) return false;
    const int player_centre_x = player_x + PLAYER_HALF_WIDTH;
    const int player_centre_y = player_y + PLAYER_HALF_HEIGHT;
    for (int y = segment->y; y < segment->y + SEGMENT_SIZE; ++y) {
        const int half_width = playerDiamondHalfWidth(y - player_centre_y);
        if (half_width < 0) continue;
        for (int x = segment->x; x < segment->x + SEGMENT_SIZE; ++x) {
            if (absoluteValue(x - player_centre_x) <= half_width) return true;
        }
    }
    return false;
}

static unsigned randomCrawlerLength(void)
{
    return CRAWLER_MIN_SEGMENTS +
        esp_random() % (CRAWLER_MAX_SEGMENTS - CRAWLER_MIN_SEGMENTS + 1U);
}

static bool topSpawnReady(uint32_t now_ms)
{
    return (int32_t)(now_ms - top_spawn_ready_at) >= 0;
}

static uint32_t sectionMaskFromHead(int head_segment)
{
    uint32_t mask = 0U;
    for (int segment = head_segment; segment >= 0; segment = followers[(unsigned)segment]) {
        mask |= 1U << (unsigned)segment;
    }
    return mask;
}

static void configureCrawlerSection(unsigned section, unsigned first_segment, unsigned count,
                                    int head_x, int head_y, int direction,
                                    int vertical_direction)
{
    uint32_t mask = 0U;
    for (unsigned offset = 0U; offset < count; ++offset) {
        const unsigned segment = first_segment + offset;
        segments[segment].x = head_x - direction * (int)offset * SEGMENT_SIZE;
        segments[segment].y = head_y;
        segments[segment].alive = true;
        followers[segment] = offset + 1U < count ? (int8_t)(segment + 1U) : -1;
        mask |= 1U << segment;
    }
    sections[section].segment_mask = mask;
    sections[section].head_segment = (int)first_segment;
    sections[section].direction = direction;
    sections[section].vertical_direction = vertical_direction;
}

static void resetMushrooms(void)
{
    for (unsigned row = 0U; row < MUSHROOM_ROWS; ++row) {
        for (unsigned column = 0U; column < MUSHROOM_COLUMNS; ++column) {
            mushrooms[row][column] = (esp_random() % 100U) < MUSHROOM_DENSITY_PERCENT ?
                MUSHROOM_HEALTH : 0U;
        }
    }
}

static void resetCrawler(void)
{
    const unsigned count = INITIAL_CRAWLER_SEGMENTS;
    for (unsigned index = 0U; index < MAX_CRAWLER_SEGMENTS; ++index) {
        segments[index].alive = false;
        followers[index] = -1;
        sections[index].segment_mask = 0U;
        sections[index].head_segment = -1;
        sections[index].direction = 0;
        sections[index].vertical_direction = 0;
    }
    configureCrawlerSection(0U, 0U, count, LCD_WIDTH - (int)count * SEGMENT_SIZE - 10,
                            PLAYFIELD_TOP + 5, -1, 1);
    top_spawn_ready_at = 0U;
}

static void resetShots(void)
{
    for (unsigned index = 0U; index < MAX_SHOTS; ++index) shots[index].active = false;
}

static unsigned remainingSegments(void)
{
    unsigned remaining = 0U;
    for (unsigned index = 0U; index < MAX_CRAWLER_SEGMENTS; ++index) {
        if (segments[index].alive) ++remaining;
    }
    return remaining;
}

static int sectionLeader(unsigned section)
{
    if (section >= MAX_CRAWLER_SEGMENTS || sections[section].segment_mask == 0U ||
        sections[section].head_segment < 0) return -1;
    return sections[section].head_segment;
}

static bool segmentIsHead(unsigned segment)
{
    for (unsigned section = 0U; section < MAX_CRAWLER_SEGMENTS; ++section) {
        if (sectionLeader(section) == (int)segment) return true;
    }
    return false;
}

static void splitCrawlerAt(unsigned segment)
{
    const uint32_t hit_bit = 1U << segment;
    for (unsigned section = 0U; section < MAX_CRAWLER_SEGMENTS; ++section) {
        if ((sections[section].segment_mask & hit_bit) == 0U) continue;
        const int original_head = sections[section].head_segment;
        const int original_direction = sections[section].direction;
        const int original_vertical_direction = sections[section].vertical_direction;
        int predecessor = -1;
        int current = original_head;
        while (current >= 0 && current != (int)segment) {
            predecessor = current;
            current = followers[(unsigned)current];
        }
        if (current < 0) return;
        const int rear_head = followers[segment];
        if (predecessor >= 0) followers[(unsigned)predecessor] = -1;
        followers[segment] = -1;
        if (predecessor >= 0) {
            sections[section].head_segment = original_head;
            sections[section].segment_mask = sectionMaskFromHead(original_head);
        } else if (rear_head >= 0) {
            sections[section].head_segment = rear_head;
            sections[section].segment_mask = sectionMaskFromHead(rear_head);
        } else {
            sections[section].head_segment = -1;
            sections[section].segment_mask = 0U;
        }
        if (predecessor >= 0 && rear_head >= 0) {
            for (unsigned free_section = 0U; free_section < MAX_CRAWLER_SEGMENTS;
                 ++free_section) {
                if (sections[free_section].segment_mask == 0U) {
                    sections[free_section].head_segment = rear_head;
                    sections[free_section].segment_mask = sectionMaskFromHead(rear_head);
                    sections[free_section].direction = -original_direction;
                    sections[free_section].vertical_direction = original_vertical_direction;
                    return;
                }
            }
        }
        return;
    }
}

static bool crawlerHitsMushroom(int x, int y)
{
    for (unsigned row = 0U; row < MUSHROOM_ROWS; ++row) {
        for (unsigned column = 0U; column < MUSHROOM_COLUMNS; ++column) {
            if (mushrooms[row][column] == 0U) continue;
            const int mushroom_x = (int)column * MUSHROOM_X_SPACING + MUSHROOM_X_OFFSET;
            const int mushroom_y = PLAYFIELD_TOP + (int)row * MUSHROOM_Y_SPACING +
                MUSHROOM_Y_OFFSET;
            if (x < mushroom_x + MUSHROOM_WIDTH && x + SEGMENT_SIZE > mushroom_x &&
                y < mushroom_y + MUSHROOM_HEIGHT && y + SEGMENT_SIZE > mushroom_y) {
                return true;
            }
        }
    }
    return false;
}

static bool damageMushroomAt(int x, int y)
{
    for (unsigned row = 0U; row < MUSHROOM_ROWS; ++row) {
        for (unsigned column = 0U; column < MUSHROOM_COLUMNS; ++column) {
            if (mushrooms[row][column] == 0U) continue;
            const int mushroom_x = (int)column * MUSHROOM_X_SPACING + MUSHROOM_X_OFFSET;
            const int mushroom_y = PLAYFIELD_TOP + (int)row * MUSHROOM_Y_SPACING +
                MUSHROOM_Y_OFFSET;
            if (x >= mushroom_x && x < mushroom_x + MUSHROOM_WIDTH &&
                y >= mushroom_y && y < mushroom_y + MUSHROOM_HEIGHT) {
                --mushrooms[row][column];
                return true;
            }
        }
    }
    return false;
}

static void addMushroomAt(int x, int y)
{
    const int centre_x = x + SEGMENT_SIZE / 2;
    const int centre_y = y + SEGMENT_SIZE / 2;
    const int column = (centre_x - MUSHROOM_X_OFFSET + MUSHROOM_X_SPACING / 2) /
        MUSHROOM_X_SPACING;
    const int row = (centre_y - PLAYFIELD_TOP - MUSHROOM_Y_OFFSET + MUSHROOM_Y_SPACING / 2) /
        MUSHROOM_Y_SPACING;
    if (row >= 0 && row < (int)MUSHROOM_ROWS && column >= 0 &&
        column < (int)MUSHROOM_COLUMNS) {
        mushrooms[(unsigned)row][(unsigned)column] = MUSHROOM_HEALTH;
    }
}

static void beginGame(uint32_t now_ms)
{
    score = 0U;
    wave = 1U;
    lives = 3U;
    player_x = (LCD_WIDTH - PLAYER_WIDTH) / 2;
    player_y = LCD_HEIGHT - PLAYER_HEIGHT - 2;
    resetMushrooms();
    resetCrawler();
    resetShots();
    crawler_last_step = now_ms;
    shot_last_step = now_ms;
    next_fire = now_ms;
    state = CRAWLER_PLAYING;
    audioPlay(AUDIO_SFX_GAME_START);
}

static void loseLife(uint32_t now_ms)
{
    if (lives > 0U) --lives;
    resetShots();
    audioPlay(AUDIO_SFX_PLAYER_EXPLOSION);
    if (lives == 0U) {
        state = CRAWLER_FINISHED;
        state_started = now_ms;
        ESP_LOGI("crawler", "GAME OVER: SCORE %u WAVE %u", (unsigned)score, (unsigned)wave);
        return;
    }
    state = CRAWLER_LIFE_LOST;
    state_started = now_ms;
}

static void fireShot(void)
{
    for (unsigned index = 0U; index < MAX_SHOTS; ++index) {
        if (!shots[index].active) {
            shots[index].x = player_x + PLAYER_WIDTH / 2;
            shots[index].y = player_y - 2;
            shots[index].active = true;
            audioPlay(AUDIO_SFX_PLAYER_SHOT);
            return;
        }
    }
}

static bool findFreeSegmentRun(unsigned count, unsigned *first_segment)
{
    if (first_segment == NULL || count == 0U || count > MAX_CRAWLER_SEGMENTS) return false;
    for (unsigned first = 0U; first + count <= MAX_CRAWLER_SEGMENTS; ++first) {
        bool available = true;
        for (unsigned offset = 0U; offset < count; ++offset) {
            if (segments[first + offset].alive) {
                available = false;
                break;
            }
        }
        if (available) {
            *first_segment = first;
            return true;
        }
    }
    return false;
}

static void spawnTopCrawler(uint32_t now_ms)
{
    if (!topSpawnReady(now_ms)) return;
    const unsigned count = randomCrawlerLength();
    unsigned first_segment = 0U;
    unsigned free_section = 0U;
    if (!findFreeSegmentRun(count, &first_segment)) return;
    while (free_section < MAX_CRAWLER_SEGMENTS &&
           sections[free_section].segment_mask != 0U) {
        ++free_section;
    }
    if (free_section == MAX_CRAWLER_SEGMENTS) return;
    const int first_x = (int)(esp_random() %
        (LCD_WIDTH - (int)count * SEGMENT_SIZE));
    const int direction = (esp_random() & 1U) == 0U ? -1 : 1;
    const int head_x = direction < 0 ? first_x : first_x + (int)(count - 1U) * SEGMENT_SIZE;
    configureCrawlerSection(free_section, first_segment, count, head_x, PLAYFIELD_TOP + 5,
                            direction, 1);
    top_spawn_ready_at = now_ms + TOP_SPAWN_GUARD_MS;
}

static void updateShots(void)
{
    for (unsigned index = 0U; index < MAX_SHOTS; ++index) {
        if (!shots[index].active) continue;
        shots[index].y -= 4;
        if (shots[index].y < PLAYFIELD_TOP) {
            shots[index].active = false;
            continue;
        }
        bool hit_segment = false;
        for (unsigned segment = 0U; segment < MAX_CRAWLER_SEGMENTS; ++segment) {
            if (!segments[segment].alive) continue;
            if (shots[index].x >= segments[segment].x &&
                shots[index].x < segments[segment].x + SEGMENT_SIZE &&
                shots[index].y >= segments[segment].y &&
                shots[index].y < segments[segment].y + SEGMENT_SIZE) {
                splitCrawlerAt(segment);
                addMushroomAt(segments[segment].x, segments[segment].y);
                segments[segment].alive = false;
                shots[index].active = false;
                score = score > UINT32_MAX - 10U ? UINT32_MAX : score + 10U;
                hit_segment = true;
                audioPlay(AUDIO_SFX_HIT);
                break;
            }
        }
        if (!hit_segment && damageMushroomAt(shots[index].x, shots[index].y)) {
            shots[index].active = false;
        }
    }
}

static void moveCrawler(uint32_t now_ms)
{
    bool top_spawn_requested = false;
    for (unsigned section = 0U; section < MAX_CRAWLER_SEGMENTS; ++section) {
        const int leader = sectionLeader(section);
        if (leader < 0) continue;
        const int previous_head_x = segments[(unsigned)leader].x;
        const int previous_head_y = segments[(unsigned)leader].y;
        const int next_x = previous_head_x + sections[section].direction * SEGMENT_SIZE;
        const bool blocked = next_x < 0 || next_x + SEGMENT_SIZE > LCD_WIDTH ||
            crawlerHitsMushroom(next_x, previous_head_y);
        if (blocked) {
            sections[section].direction = -sections[section].direction;
            int vertical_step = sections[section].vertical_direction * MUSHROOM_Y_SPACING;
            const int next_y = previous_head_y + vertical_step;
            if (next_y < PLAYFIELD_TOP || next_y + SEGMENT_SIZE > LCD_HEIGHT) {
                if (topSpawnReady(now_ms)) {
                    top_spawn_requested = true;
                }
                sections[section].vertical_direction = -sections[section].vertical_direction;
                vertical_step = sections[section].vertical_direction * MUSHROOM_Y_SPACING;
            }
            segments[(unsigned)leader].y += vertical_step;
        } else {
            segments[(unsigned)leader].x = next_x;
        }
        int follower = followers[(unsigned)leader];
        int previous_x = previous_head_x;
        int previous_y = previous_head_y;
        while (follower >= 0) {
            const int follower_x = segments[(unsigned)follower].x;
            const int follower_y = segments[(unsigned)follower].y;
            segments[(unsigned)follower].x = previous_x;
            segments[(unsigned)follower].y = previous_y;
            previous_x = follower_x;
            previous_y = follower_y;
            follower = followers[(unsigned)follower];
        }
    }
    if (top_spawn_requested) spawnTopCrawler(now_ms);
    for (unsigned index = 0U; index < MAX_CRAWLER_SEGMENTS; ++index) {
        if (!segments[index].alive) continue;
        if (crawlerHitsPlayer(&segments[index])) {
            loseLife(now_ms);
            return;
        }
    }
}

void startCrawler(uint32_t now_ms)
{
    running = true;
    state = CRAWLER_READY;
    score = 0U;
    wave = 1U;
    lives = 3U;
    player_x = (LCD_WIDTH - PLAYER_WIDTH) / 2;
    player_y = LCD_HEIGHT - PLAYER_HEIGHT - 2;
    resetMushrooms();
    resetCrawler();
    resetShots();
    last_update = now_ms;
    crawler_last_step = now_ms;
    shot_last_step = now_ms;
    next_fire = now_ms;
    controls_ready = false;
    ESP_LOGI("crawler", "START");
}

void stopCrawler(void)
{
    cancelHighScoreEntry();
    running = false;
    ESP_LOGI("crawler", "STOP");
}

void crawlerAction(bool action, bool fire_held, uint32_t now_ms)
{
    if (!running) return;
    if (state == CRAWLER_READY) {
        if (action) beginGame(now_ms);
        return;
    }
    if (state != CRAWLER_PLAYING || !fire_held || now_ms < next_fire) return;
    fireShot();
    next_fire = now_ms + FIRE_INTERVAL_MS;
}

void updateCrawler(int x_offset, int y_offset, bool joystick_valid, bool left, bool right,
                   uint32_t now_ms)
{
    if (!running) return;
    const uint32_t elapsed = now_ms - last_update;
    last_update = now_ms;
    if (state == CRAWLER_FINISHED) {
        if (now_ms - state_started >= 1000U) {
            startHighScoreEntry(HIGH_SCORE_CRAWLER, score, now_ms);
            state = CRAWLER_SCORES;
        }
        return;
    }
    if (state == CRAWLER_SCORES) {
        if (updateHighScoreEntry(x_offset, y_offset, joystick_valid, now_ms)) state = CRAWLER_READY;
        return;
    }
    if (state == CRAWLER_LIFE_LOST) {
        if (now_ms - state_started >= LIFE_LOST_MS) {
            player_x = (LCD_WIDTH - PLAYER_WIDTH) / 2;
            player_y = LCD_HEIGHT - PLAYER_HEIGHT - 2;
            resetCrawler();
            crawler_last_step = now_ms;
            state = CRAWLER_PLAYING;
        }
        return;
    }
    if (state != CRAWLER_PLAYING) return;
    if (!joystick_valid) controls_ready = false;
    else if (x_offset >= -350 && x_offset <= 350) controls_ready = true;
    const int direction = left == right ? 0 : left ? -1 : 1;
    const int joystick_move = joystick_valid && controls_ready && !left && !right
        ? x_offset / 600 : 0;
    const int joystick_vertical_move = joystick_valid && controls_ready ? y_offset / 600 : 0;
    const int elapsed_limited = (int)(elapsed > 30U ? 30U : elapsed);
    const int button_move = direction * elapsed_limited / 2;
    player_x = clampValue(player_x + (left || right ? button_move : joystick_move), 0,
                          LCD_WIDTH - PLAYER_WIDTH);
    player_y = clampValue(player_y - joystick_vertical_move, PLAYER_ZONE_TOP,
                          LCD_HEIGHT - PLAYER_HEIGHT - 2);
    while (now_ms - shot_last_step >= SHOT_STEP_MS) {
        shot_last_step += SHOT_STEP_MS;
        updateShots();
    }
    while (now_ms - crawler_last_step >= CRAWLER_STEP_MS && state == CRAWLER_PLAYING) {
        crawler_last_step += CRAWLER_STEP_MS;
        moveCrawler(now_ms);
    }
    if (state == CRAWLER_PLAYING && remainingSegments() == 0U) {
        audioPlay(AUDIO_SFX_WAVE_CLEAR);
        if (wave < UINT32_MAX) ++wave;
        resetCrawler();
    }
}

void drawCrawler(uint16_t *frame)
{
    if (!running || frame == NULL) return;
    if (state == CRAWLER_SCORES) {
        drawHighScoreEntry(frame);
        return;
    }
    clear(frame, COLOR_BLACK);
    char text[28];
    if (state == CRAWLER_READY) {
        drawPet2001TextCentredTransparent(frame, LCD_WIDTH / 2, 10, "CRAWLER", COLOR_LIME, 2);
        drawPet2001TextCentredTransparent(frame, LCD_WIDTH / 2, 42, "CLEAR THE CRAWLER", COLOR_CYAN, 1);
        drawPet2001TextCentredTransparent(frame, LCD_WIDTH / 2, 63, "10 POINTS EACH", COLOR_YELLOW, 1);
        drawPet2001TextCentredTransparent(frame, LCD_WIDTH / 2, 109, "D/B OR STICK MOVE", COLOR_LIGHTGRAY, 1);
        drawPet2001TextCentredTransparent(frame, LCD_WIDTH / 2, 124, "A/K FIRE", COLOR_LIGHTGRAY, 1);
        drawPet2001TextCentredTransparent(frame, LCD_WIDTH / 2, 151, "A/K START", COLOR_YELLOW, 2);
        return;
    }
    snprintf(text, sizeof(text), "SCORE %u", (unsigned)score);
    drawPet2001TextTransparent(frame, 4, 3, text, COLOR_WHITE, 1);
    snprintf(text, sizeof(text), "LIVES %u", (unsigned)lives);
    drawPet2001TextCentredTransparent(frame, LCD_WIDTH / 2, 3, text, COLOR_CYAN, 1);
    snprintf(text, sizeof(text), "WAVE %u", (unsigned)wave);
    drawPet2001TextTransparent(frame, 253, 3, text, COLOR_YELLOW, 1);
    draw_line(frame, 0, 13, LCD_WIDTH - 1, 13, COLOR_DARKGRAY);
    for (unsigned row = 0U; row < MUSHROOM_ROWS; ++row) {
        for (unsigned column = 0U; column < MUSHROOM_COLUMNS; ++column) {
            if (mushrooms[row][column] == 0U) continue;
            const int x = (int)column * MUSHROOM_X_SPACING + MUSHROOM_X_OFFSET;
            const int y = PLAYFIELD_TOP + (int)row * MUSHROOM_Y_SPACING +
                MUSHROOM_Y_OFFSET;
            const uint16_t cap_colour = mushrooms[row][column] == 4U ? COLOR_MAGENTA :
                mushrooms[row][column] == 3U ? COLOR_CYAN :
                mushrooms[row][column] == 2U ? COLOR_YELLOW : COLOR_WHITE;
            draw_ui_bitmap_8x8(frame, x, y, mushroom_bitmap, 1, cap_colour);
        }
    }
    for (unsigned index = 0U; index < MAX_CRAWLER_SEGMENTS; ++index) {
        if (!segments[index].alive) continue;
        const uint16_t colour = segmentIsHead(index) ? COLOR_RED : COLOR_LIME;
        fill_circle(frame, segments[index].x + SEGMENT_SIZE / 2,
                    segments[index].y + SEGMENT_SIZE / 2, SEGMENT_SIZE / 2, colour);
    }
    for (unsigned index = 0U; index < MAX_SHOTS; ++index) {
        if (shots[index].active) draw_line(frame, shots[index].x, shots[index].y,
                                            shots[index].x, shots[index].y + 4, COLOR_WHITE);
    }
    const int player_centre_x = player_x + PLAYER_HALF_WIDTH;
    const int player_centre_y = player_y + PLAYER_HALF_HEIGHT;
    for (int y_offset = -PLAYER_HALF_HEIGHT; y_offset <= PLAYER_HALF_HEIGHT; ++y_offset) {
        const int half_width = playerDiamondHalfWidth(y_offset);
        fill_rect(frame, player_centre_x - half_width, player_centre_y + y_offset,
                  half_width * 2 + 1, 1, COLOR_CYAN);
    }
    if (state == CRAWLER_LIFE_LOST) {
        drawPet2001TextCentredTransparent(frame, LCD_WIDTH / 2, 83, "LIFE LOST", COLOR_RED, 2);
    } else if (state == CRAWLER_FINISHED) {
        drawPet2001TextCentredTransparent(frame, LCD_WIDTH / 2, 83, "GAME OVER", COLOR_RED, 2);
    }
}