Scramble
Source: src/scramble.c · 675 lines · 34 functions.
Screen captures
Views of this part running on the Mini Arcade.

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)- Exit this function
return 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)
- Exit this function
return 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)
- Exit this function
return 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)
- Execute
score = score > UINT32_MAX - points ? UINT32_MAX : score + points; - Execute
const uint32_t nextWave = score / 1000U + 1U; - Decision
nextWave > waveTrue- Execute
wave = nextWave;
FalseContinue 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)
- Decision
x <= 0.0fTrue- Exit this function
return terrain[0];
FalseContinue after this decision.
- Execute
const float index = x / TERRAIN_SPACING; - Execute
unsigned left = (unsigned)index; - Decision
left >= TERRAIN_POINTS - 1UTrue- Exit this function
return terrain[TERRAIN_POINTS - 1U];
FalseContinue after this decision.
- Execute
const float fraction = index - left; - Exit this function
return 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:
- Execute
x = fmodf(x, (TERRAIN_POINTS - 1U) * TERRAIN_SPACING); - Decision
x < 0.0fTrue- Execute
x += (TERRAIN_POINTS - 1U) * TERRAIN_SPACING;
FalseContinue after this decision.
- Exit this function
return 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)
- Execute
const unsigned last = first + 2U; - Execute
const float height = baseTerrain[first + 1U]; - Execute
flatZones[zone] = (FlatZone){first, last, height}; - FOR loop
unsigned point = first; point <= last; ++pointRepeat body while the loop condition permits; then continue below.
- Execute
terrain[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)
- Execute
const unsigned last = first + 2U; - FOR loop
unsigned zone = 0; zone < count; ++zoneRepeat body while the loop condition permits; then continue below.
- Decision
last >= flatZones[zone].first && first <= flatZones[zone].lastTrue- Exit this function
return false;
FalseContinue after this decision.
- Exit this function
return 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:
- FOR loop
unsigned i = 0; i < TERRAIN_POINTS; ++iRepeat body while the loop condition permits; then continue below.
- Execute
baseTerrain[i] = terrain[i] = 130.0f + scramble_terrain_offsets[i];
- FOR loop
unsigned target = 0; target < MAX_TARGETS; ++targetRepeat body while the loop condition permits; then continue below.
- Execute
const unsigned first = scramble_target_shelf_starts[target]; - Execute
flattenTerrainZone(target, first); - Execute
scramble_target_lanes[target] = (first + 1U) * TERRAIN_SPACING;
- FOR loop
unsigned zone = MAX_TARGETS; zone < FLAT_ZONE_COUNT; ++zoneRepeat body while the loop condition permits; then continue below.
- Execute
unsigned first; - DO / WHILE loop
!flatZoneAvailable(first, zone)Run body, then test condition; repeat while true.
- Execute
first = 1U + esp_random() % (TERRAIN_POINTS - 5U);
- Execute
flattenTerrainZone(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:
- Execute
const float patternWidth = (TERRAIN_POINTS - 1U) * TERRAIN_SPACING; - Execute
const int firstPattern = (int)floorf(scroll / patternWidth) - 1; - FOR loop
int pattern = firstPattern; pattern <= firstPattern + 3; ++patternRepeat body while the loop condition permits; then continue below.
- FOR loop
unsigned zone = 0; zone < FLAT_ZONE_COUNT; ++zoneRepeat body while the loop condition permits; then continue below.
- Execute
const FlatZone *flat = &flatZones[zone]; - Execute
const int left = (int)(flat->first * TERRAIN_SPACING + pattern * patternWidth - scroll); - Execute
const int right = (int)(flat->last * TERRAIN_SPACING + pattern * patternWidth - scroll); - Execute
draw_line(frame, left, (int)baseTerrain[flat->first], left, (int)flat->height, COLOR_LIME); - Execute
draw_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)
- FOR loop
unsigned i = 0; i < MAX_EFFECTS; ++iRepeat body while the loop condition permits; then continue below.
- Decision
effects[i].remaining == 0True- Execute
effects[i] = (Effect){x, y, 260U, color}; - Exit this function
return;
FalseContinue 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)
- Decision
rocketsInGroup != 0True- Execute
--rocketsInGroup; - Exit this function
return TARGET_ROCKET;
FalseContinue after this decision.
- Decision
esp_random() % 3U == 1UTrue- Exit this function
return TARGET_FUEL;
FalseContinue after this decision.
- Decision
--turretsUntilRockets == 0True- Execute
rocketsInGroup = 1U + esp_random() % 2U; - Execute
turretsUntilRockets = 5U + esp_random() % 4U;
FalseContinue after this decision.
- Exit this function
return 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:
- 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:
- Execute
memset(shots, 0, sizeof(shots)); - Execute
memset(bombs, 0, sizeof(bombs)); - Execute
memset(bombDropAt, 0, sizeof(bombDropAt)); - Execute
memset(enemies, 0, sizeof(enemies)); - Execute
memset(rockets, 0, sizeof(rockets)); - Execute
memset(effects, 0, sizeof(effects)); - Execute
turretsUntilRockets = 5U + esp_random() % 4U; - Execute
rocketsInGroup = 0; - FOR loop
unsigned i = 0; i < MAX_TARGETS; ++iRepeat body while the loop condition permits; then continue below.
- Execute
placeTarget(&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:
- Execute
makeTerrain(); - Execute
scroll = 0.0f; - Execute
fuel = FUEL_MAX; - Execute
score = 0; - Execute
wave = 1; - Execute
lives = 3; - Execute
player = (Object){52.0f, 75.0f, 0.0f, 0.0f, true}; - Execute
fireDelay = bombDelay = 0; - Execute
enemyDelay = ENEMY_SPAWN_MIN_MS; - Execute
accumulator = 0; - Execute
playerFrame = 0; - Execute
controlsReady = false; - Execute
resetObjects(); - Execute
gameState = STATE_PLAYING; - Execute
lastUpdate = 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)
- Execute
player.active = false; - Execute
gameState = STATE_FINISHED; - Execute
stateStarted = lastUpdate; - Execute
ESP_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:
- Decision
!player.activeTrue- Exit this function
return;
FalseContinue after this decision.
- Execute
startEffect(player.x + PLAYER_W / 2.0f, player.y + PLAYER_H / 2.0f, COLOR_YELLOW); - Execute
audioPlay(AUDIO_SFX_PLAYER_EXPLOSION); - Decision
--lives == 0True- Execute
finishGame(); - Exit this function
return;
FalseContinue after this decision.
- Execute
player.active = false; - Execute
stateStarted = lastUpdate; - Execute
memset(shots, 0, sizeof(shots)); - Execute
memset(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)
- Execute
player = (Object){52.0f, 75.0f, 0.0f, 0.0f, true}; - Execute
controlsReady = 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:
- Decision
fireDelay != 0 || !player.activeTrue- Exit this function
return;
FalseContinue after this decision.
- FOR loop
unsigned i = 0; i < MAX_SHOTS; ++iRepeat body while the loop condition permits; then continue below.
- Decision
!shots[i].activeTrue- Execute
shots[i] = (Object){player.x + PLAYER_W, player.y + 3.0f, 240.0f, 0.0f, true}; - Execute
fireDelay = FIRE_DELAY_MS; - Execute
audioPlay(AUDIO_SFX_PLAYER_SHOT); - Exit this function
return;
FalseContinue 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)
- Decision
bombDelay != 0 || !player.activeTrue- Exit this function
return;
FalseContinue after this decision.
- FOR loop
unsigned i = 0; i < MAX_BOMBS; ++iRepeat body while the loop condition permits; then continue below.
- Decision
!bombs[i].activeTrue- Execute
bombs[i] = (Object){player.x + 7.0f, player.y + PLAYER_H, 70.0f, 25.0f, true}; - Execute
bombDropAt[i] = bombs[i].x + PLAYER_W; - Execute
bombDelay = BOMB_DELAY_MS; - Exit this function
return;
FalseContinue 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)
- FOR loop
unsigned i = 0; i < MAX_ENEMIES; ++iRepeat body while the loop condition permits; then continue below.
- Decision
!enemies[i].activeTrue- Execute
enemies[i] = (Object){320.0f, 25.0f + esp_random() % 80U, -(48.0f + wave * 3.0f), 0.0f, true}; - Exit this function
return;
FalseContinue 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:
- Execute
enemy->active = false; - Execute
startEffect(enemy->x + 8.0f, enemy->y + 6.0f, COLOR_MAGENTA); - Execute
addPoints(100U); - Execute
audioPlay(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:
- Execute
rocket->active = false; - Execute
startEffect(rocket->x + 4.0f, rocket->y + 6.0f, COLOR_ORANGERED); - Execute
addPoints(150U); - Execute
audioPlay(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:
- Execute
target->active = false; - Execute
startEffect(target->x, target->y - 6.0f, COLOR_ORANGE); - Decision
target->type == TARGET_FUELTrue- Execute
fuel = FUEL_MAX; - Execute
addPoints(150U); - Execute
audioPlay(AUDIO_SFX_POWER_UP);
False- Decision
target->type == TARGET_ROCKETTrue- Execute
addPoints(100U); - Execute
audioPlay(AUDIO_SFX_HIT);
False- Execute
addPoints(50U); - Execute
audioPlay(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:
- FOR loop
unsigned i = 0; i < MAX_ROCKETS; ++iRepeat body while the loop condition permits; then continue below.
- Decision
rockets[i].activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
const float screenX = target->x - scroll; - Execute
const float aim = player.x + PLAYER_W / 2.0f - screenX; - Execute
const float accuracy = clampValue(0.24f + (wave - 1U) * 0.09f, 0.24f, 0.78f); - Execute
rockets[i] = (Object){screenX, terrainHeight(target->x) - 12.0f, aim * accuracy, -(82.0f + wave * 5.0f), true}; - Execute
target->active = false; - Execute
startEffect(screenX, terrainHeight(target->x) - 5.0f, COLOR_RED); - Execute
audioPlay(AUDIO_SFX_ENEMY_SHOT); - Exit this function
return;
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:
- Execute
fireDelay = reduceTimer(fireDelay); - Execute
bombDelay = reduceTimer(bombDelay); - FOR loop
unsigned i = 0; i < MAX_EFFECTS; ++iRepeat body while the loop condition permits; then continue below.
- Execute
effects[i].remaining = reduceTimer(effects[i].remaining);
- Decision
!player.activeTrue- Decision
lastUpdate - stateStarted >= RESPAWN_MSTrue- Execute
respawnPlayer();
FalseContinue after this decision.
- Exit this function
return;
FalseContinue after this decision.
- Execute
player.x = clampValue(player.x + velocityX * STEP_MS / 1000.0f, 8.0f, 160.0f); - Execute
player.y = clampValue(player.y + velocityY * STEP_MS / 1000.0f, PLAY_TOP + 3.0f, 116.0f); - Execute
scroll += SCROLL_SPEED * STEP_MS / 1000.0f; - Execute
fuel -= FUEL_DRAIN_PER_SECOND * STEP_MS / 1000.0f; - Decision
fuel <= 0.0fTrue- Execute
fuel = 0.0f; - Execute
hitPlayer(); - Exit this function
return;
FalseContinue after this decision.
- Decision
fireTrue- Execute
fireShot();
FalseContinue after this decision.
- Decision
bombTrue- Execute
dropBomb();
FalseContinue after this decision.
- Execute
playerFrame = (uint8_t)((lastUpdate / 120U) & 1U); - FOR loop
unsigned i = 0; i < MAX_TARGETS; ++iRepeat body while the loop condition permits; then continue below.
- Execute
GroundTarget *target = &targets[i]; - Decision
target->x - scroll >= -16.0fTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
target->x += (TERRAIN_POINTS - 1U) * TERRAIN_SPACING; - Execute
placeTarget(target, target->x);
- FOR loop
unsigned i = 0; i < MAX_TARGETS; ++iRepeat body while the loop condition permits; then continue below.
- Execute
GroundTarget *target = &targets[i]; - Execute
const float screenX = target->x - scroll; - Decision
!target->active || target->type != TARGET_ROCKET || screenX < 0.0f || screenX >= LCD_WIDTHTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
target->launchDelay = reduceTimer(target->launchDelay); - Decision
target->launchDelay == 0True- Execute
launchRocket(target);
FalseContinue after this decision.
- FOR loop
unsigned i = 0; i < MAX_SHOTS; ++iRepeat body while the loop condition permits; then continue below.
- Execute
Object *shot = &shots[i]; - Decision
!shot->activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
shot->x += shot->vx * STEP_MS / 1000.0f; - Decision
shot->x >= LCD_WIDTHTrue- Execute
shot->active = false; - Next loop iteration
continue;
FalseContinue after this decision.
- FOR loop
unsigned e = 0; e < MAX_ENEMIES; ++eRepeat body while the loop condition permits; then continue below.
- Decision
enemies[e].active && overlaps(shot->x, shot->y - 3.0f, 6, 8, enemies[e].x, enemies[e].y, 16, 12)True- Execute
shot->active = false; - Execute
destroyEnemy(&enemies[e]); - Exit enclosing loop or switch
break;
FalseContinue after this decision.
- Decision
!shot->activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- FOR loop
unsigned r = 0; r < MAX_ROCKETS; ++rRepeat body while the loop condition permits; then continue below.
- Decision
rockets[r].active && overlaps(shot->x, shot->y - 3.0f, 6, 8, rockets[r].x, rockets[r].y, 8, 12)True- Execute
shot->active = false; - Execute
destroyRocket(&rockets[r]); - Exit enclosing loop or switch
break;
FalseContinue after this decision.
- Decision
!shot->activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- FOR loop
unsigned t = 0; t < MAX_TARGETS; ++tRepeat body while the loop condition permits; then continue below.
- Execute
GroundTarget *target = &targets[t]; - Decision
!target->activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
const float screenX = target->x - scroll; - Execute
const float targetY = terrainHeight(target->x) - 12.0f; - Execute
const float targetWidth = target->type == TARGET_ROCKET ? 8.0f : 12.0f; - Decision
overlaps(shot->x, shot->y - 3.0f, 6, 8, screenX - targetWidth / 2.0f, targetY, targetWidth, 12.0f)True- Execute
shot->active = false; - Execute
destroyTarget(target); - Exit enclosing loop or switch
break;
FalseContinue after this decision.
- FOR loop
unsigned i = 0; i < MAX_BOMBS; ++iRepeat body while the loop condition permits; then continue below.
- Execute
Object *falling = &bombs[i]; - Decision
!falling->activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Decision
falling->x >= bombDropAt[i]True- Execute
falling->vx = 5.0f; - Execute
falling->vy = 180.0f;
FalseContinue after this decision.
- Execute
falling->x += falling->vx * STEP_MS / 1000.0f; - Execute
falling->y += falling->vy * STEP_MS / 1000.0f; - Execute
const float ground = terrainHeight(falling->x + scroll); - Decision
falling->y >= groundTrue- Execute
falling->active = false; - Execute
startEffect(falling->x, ground - 3.0f, COLOR_ORANGE); - Execute
audioPlay(AUDIO_SFX_EXPLOSION); - FOR loop
unsigned t = 0; t < MAX_TARGETS; ++tRepeat body while the loop condition permits; then continue below.
- Execute
GroundTarget *target = &targets[t]; - Execute
const float screenX = target->x - scroll; - Decision
target->active && fabsf(falling->x - screenX) < 13.0fTrue- Execute
destroyTarget(target);
FalseContinue after this decision.
FalseContinue after this decision.
- Execute
enemyDelay = reduceTimer(enemyDelay); - Decision
enemyDelay == 0True- Execute
spawnEnemy(); - Execute
enemyDelay = ENEMY_SPAWN_MIN_MS + esp_random() % ENEMY_SPAWN_RANGE_MS;
FalseContinue after this decision.
- FOR loop
unsigned i = 0; i < MAX_ENEMIES; ++iRepeat body while the loop condition permits; then continue below.
- Execute
Object *enemy = &enemies[i]; - Decision
!enemy->activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
enemy->x += enemy->vx * STEP_MS / 1000.0f; - Decision
enemy->x < -16.0fTrue- Execute
enemy->active = false;
False- Decision
overlaps(player.x, player.y, PLAYER_W, PLAYER_H, enemy->x, enemy->y, 16, 12)True- Execute
enemy->active = false; - Execute
hitPlayer();
FalseContinue after this decision.
- FOR loop
unsigned i = 0; i < MAX_ROCKETS; ++iRepeat body while the loop condition permits; then continue below.
- Execute
Object *rocket = &rockets[i]; - Decision
!rocket->activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
const float maxSideSpeed = 52.0f + wave * 6.0f; - Execute
const float desiredVx = clampValue((player.x + PLAYER_W / 2.0f - rocket->x) * (0.34f + wave * 0.05f), -maxSideSpeed, maxSideSpeed); - Execute
const float steering = clampValue(0.24f + (wave - 1U) * 0.09f, 0.24f, 0.78f); - Execute
rocket->vx += (desiredVx - rocket->vx) * steering * 0.12f; - Execute
rocket->x += rocket->vx * STEP_MS / 1000.0f; - Execute
rocket->y += rocket->vy * STEP_MS / 1000.0f; - Decision
rocket->y < -12.0f || rocket->x < -12.0f || rocket->x >= LCD_WIDTHTrue- Execute
rocket->active = false;
False- Decision
overlaps(player.x, player.y, PLAYER_W, PLAYER_H, rocket->x, rocket->y, 8, 12)True- Execute
rocket->active = false; - Execute
hitPlayer();
FalseContinue after this decision.
- Execute
const float playerGround = terrainHeight(player.x + scroll); - Decision
player.y + PLAYER_H >= playerGroundTrue- Execute
hitPlayer();
FalseContinue 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:
- Decision
offset >= -350 && offset <= 350True- Exit this function
return 0.0f;
FalseContinue after this decision.
- Execute
const float adjusted = offset > 0 ? (float)offset - 350.0f : (float)offset + 350.0f; - Exit this function
return 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:
- Execute
running = true; - Execute
beginGame(now_ms); - Execute
gameState = STATE_READY; - Execute
ESP_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:
- Execute
cancelHighScoreEntry(); - Execute
running = false; - Execute
ESP_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:
- Decision
running && start && gameState == STATE_READYTrue- Execute
beginGame(now_ms); - Execute
audioPlay(AUDIO_SFX_GAME_START);
FalseContinue 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:
- Decision
!runningTrue- Exit this function
return;
FalseContinue after this decision.
- Execute
const uint32_t elapsed = now_ms - lastUpdate; - Execute
lastUpdate = now_ms; - Decision
gameState == STATE_FINISHED && now_ms - stateStarted >= 1000UTrue- Execute
startHighScoreEntry(HIGH_SCORE_SCRAMBLE, score, now_ms); - Execute
gameState = STATE_SCORES;
FalseContinue after this decision.
- Decision
gameState == STATE_SCORESTrue- Decision
updateHighScoreEntry(x_offset, y_offset, valid, now_ms)True- Execute
gameState = STATE_READY;
FalseContinue after this decision.
- Exit this function
return;
FalseContinue after this decision.
- Decision
gameState != STATE_PLAYINGTrue- Exit this function
return;
FalseContinue after this decision.
- Decision
!validTrue- Execute
controlsReady = false;
False- Decision
x_offset >= -350 && x_offset <= 350 && y_offset >= -350 && y_offset <= 350True- Execute
controlsReady = true;
FalseContinue after this decision.
- Execute
const float velocityX = valid && controlsReady ? axisVelocity(x_offset) : 0.0f; - Execute
const float velocityY = valid && controlsReady ? -axisVelocity(y_offset) : 0.0f; - Execute
accumulator += elapsed > 100U ? 100U : elapsed; - WHILE loop
accumulator >= STEP_MS && gameState == STATE_PLAYINGRepeat body while the loop condition permits; then continue below.
- Execute
accumulator -= STEP_MS; - Execute
simulateStep(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:
- Execute
const float terrainWidth = (TERRAIN_POINTS - 1U) * TERRAIN_SPACING; - Execute
float patternX = fmodf(scroll, terrainWidth); - FOR loop
int x = 0; x < LCD_WIDTH; ++xRepeat body while the loop condition permits; then continue below.
- Execute
const int ground = (int)terrainHeightInPattern(patternX); - Execute
fill_rect(frame, x, ground, 1, LCD_HEIGHT - ground, COLOR_DARKGREEN); - Execute
fill_rect(frame, x, ground, 1, 2, COLOR_LIME); - Execute
patternX += 1.0f; - Decision
patternX >= terrainWidthTrue- Execute
patternX -= terrainWidth;
FalseContinue after this decision.
- Execute
drawFlatTerrainEdges(frame); - FOR loop
unsigned i = 0; i < MAX_TARGETS; ++iRepeat body while the loop condition permits; then continue below.
- Execute
const GroundTarget *target = &targets[i]; - Decision
!target->activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
const int x = (int)(target->x - scroll); - Decision
x < -12 || x >= LCD_WIDTHTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
const int y = (int)terrainHeight(target->x) - 12; - Execute
const uint16_t color = target->type == TARGET_FUEL ? COLOR_CYAN : target->type == TARGET_ROCKET ? COLOR_ORANGERED : COLOR_YELLOW; - Execute
const uint8_t *bitmap = target->type == TARGET_FUEL ? scramble_fuel_bitmap : target->type == TARGET_ROCKET ? scramble_rocket_bitmap : scramble_turret_bitmap; - Execute
const int width = target->type == TARGET_ROCKET ? 8 : 12; - Execute
draw_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:
- Execute
char text[32]; - Execute
fill_rect(frame, 0, 0, LCD_WIDTH, PLAY_TOP, COLOR_BLACK); - Execute
snprintf(text, sizeof(text), "SCORE %u", (unsigned)score); - Execute
draw_text(frame, 3, 3, text, 1, COLOR_WHITE); - Execute
snprintf(text, sizeof(text), "LIVES %u", (unsigned)lives); - Execute
draw_text_centred(frame, 162, 3, text, 1, COLOR_YELLOW); - Execute
draw_text(frame, 245, 3, "FUEL", 1, COLOR_CYAN); - Execute
draw_rect(frame, 276, 3, 40, 8, COLOR_WHITE); - Execute
fill_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:
- Decision
!running || frame == NULLTrue- Exit this function
return;
FalseContinue after this decision.
- Decision
gameState == STATE_SCORESTrue- Execute
drawHighScoreEntry(frame); - Exit this function
return;
FalseContinue after this decision.
- Execute
clear(frame, COLOR_BLACK); - Execute
drawTerrain(frame); - FOR loop
unsigned i = 0; i < MAX_ENEMIES; ++iRepeat body while the loop condition permits; then continue below.
- Decision
enemies[i].activeTrue- Execute
draw_bitmap_scaled(frame, (int)enemies[i].x, (int)enemies[i].y, scramble_enemy_bitmap, 8, 8, 16, 12, COLOR_MAGENTA);
FalseContinue after this decision.
- FOR loop
unsigned i = 0; i < MAX_ROCKETS; ++iRepeat body while the loop condition permits; then continue below.
- Decision
rockets[i].activeTrue- Execute
draw_bitmap_scaled(frame, (int)rockets[i].x - 4, (int)rockets[i].y, scramble_rocket_bitmap, 8, 8, 8, 12, COLOR_ORANGERED);
FalseContinue after this decision.
- FOR loop
unsigned i = 0; i < MAX_SHOTS; ++iRepeat body while the loop condition permits; then continue below.
- Decision
shots[i].activeTrue- Execute
fill_rect(frame, (int)shots[i].x, (int)shots[i].y, 6, 2, COLOR_WHITE);
FalseContinue after this decision.
- FOR loop
unsigned i = 0; i < MAX_BOMBS; ++iRepeat body while the loop condition permits; then continue below.
- Decision
bombs[i].activeTrue- Execute
fill_rect(frame, (int)bombs[i].x, (int)bombs[i].y, 3, 4, COLOR_YELLOW);
FalseContinue after this decision.
- FOR loop
unsigned i = 0; i < MAX_EFFECTS; ++iRepeat body while the loop condition permits; then continue below.
- Decision
effects[i].remaining != 0True- Execute
const unsigned pose = effects[i].remaining > 130U ? 0U : 1U; - Execute
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);
FalseContinue after this decision.
- Decision
player.activeTrue- Execute
draw_bitmap_scaled(frame, (int)player.x, (int)player.y, scramble_player_frames[playerFrame], 8, 8, PLAYER_W, PLAYER_H, COLOR_CYAN);
FalseContinue after this decision.
- Execute
drawHud(frame); - Decision
gameState == STATE_READYTrue- Execute
clear(frame, COLOR_BLACK); - Execute
draw_text_centred(frame, 160, 20, "SCRAMBLE", 3, COLOR_CYAN); - Execute
draw_text_centred(frame, 160, 59, "JOYSTICK FLY", 1, COLOR_WHITE); - Execute
draw_text_centred(frame, 160, 76, "A FIRE FORWARD", 1, COLOR_YELLOW); - Execute
draw_text_centred(frame, 160, 92, "B DROP BOMB", 1, COLOR_ORANGE); - Execute
draw_text_centred(frame, 160, 139, "BOOT BACK", 1, COLOR_LIGHTGRAY);
False- Decision
gameState == STATE_FINISHEDTrue- Execute
draw_text_centred(frame, 160, 78, "GAME OVER", 2, COLOR_RED);
FalseContinue 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);
}
}