Asteroids
Source: src/asteroids.c · 438 lines · 19 functions.
Open the existing readable game overview →
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
randomUnit() · source line 47
static float randomUnit(void)
- Exit this function
return (float)(esp_random() & 0xffffU) / 65536.0f;
Read original function source
static float randomUnit(void)
{
return (float)(esp_random() & 0xffffU) / 65536.0f;
}wrap() · source line 52
static float wrap(float value, float extent)
- WHILE loop
value < 0.0fRepeat body while the loop condition permits; then continue below.
- Execute
value += extent;
- WHILE loop
value >= extentRepeat body while the loop condition permits; then continue below.
- Execute
value -= extent;
- Exit this function
return value;
Read original function source
static float wrap(float value, float extent)
{
while (value < 0.0f) value += extent;
while (value >= extent) value -= extent;
return value;
}distanceSquared() · source line 59
static float distanceSquared(const Body *a, const Body *b)
- Execute
float dx = fabsf(a->x - b->x), dy = fabsf(a->y - b->y); - Decision
dx > LCD_WIDTH / 2.0fTrue- Execute
dx = LCD_WIDTH - dx;
FalseContinue after this decision.
- Decision
dy > LCD_HEIGHT / 2.0fTrue- Execute
dy = LCD_HEIGHT - dy;
FalseContinue after this decision.
- Exit this function
return dx * dx + dy * dy;
Read original function source
static float distanceSquared(const Body *a, const Body *b)
{
float dx = fabsf(a->x - b->x), dy = fabsf(a->y - b->y);
if (dx > LCD_WIDTH / 2.0f) dx = LCD_WIDTH - dx;
if (dy > LCD_HEIGHT / 2.0f) dy = LCD_HEIGHT - dy;
return dx * dx + dy * dy;
}rockRadius() · source line 67
static float rockRadius(uint8_t size)
- Exit this function
return size == 3 ? 16.0f : size == 2 ? 10.0f : 5.0f;
Read original function source
static float rockRadius(uint8_t size)
{
return size == 3 ? 16.0f : size == 2 ? 10.0f : 5.0f;
}moveBody() · source line 72
static void moveBody(Body *body, float seconds)
Calls into this documentation:
- Execute
body->x = wrap(body->x + body->vx * seconds, LCD_WIDTH); - Execute
body->y = wrap(body->y + body->vy * seconds, LCD_HEIGHT);
Read original function source
static void moveBody(Body *body, float seconds)
{
body->x = wrap(body->x + body->vx * seconds, LCD_WIDTH);
body->y = wrap(body->y + body->vy * seconds, LCD_HEIGHT);
}addRock() · source line 78
static bool addRock(float x, float y, uint8_t size, float direction)
Calls into this documentation:
- FOR loop
unsigned i = 0; i < MAX_ROCKS; ++iRepeat body while the loop condition permits; then continue below.
- Decision
rocks[i].activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
const float speed = 16.0f + (3 - size) * 12.0f + (wave < 12 ? wave : 12) * 2.0f + randomUnit() * 10.0f; - Execute
rocks[i] = (Rock){ .body = {x, y, cosf(direction) * speed, sinf(direction) * speed}, .angle = randomUnit() * 2.0f * PI, .spin = (randomUnit() - 0.5f) * 1.5f, .size = size, .active = true, }; - Exit this function
return true;
- Exit this function
return false;
Read original function source
static bool addRock(float x, float y, uint8_t size, float direction)
{
for (unsigned i = 0; i < MAX_ROCKS; ++i) {
if (rocks[i].active) continue;
const float speed = 16.0f + (3 - size) * 12.0f +
(wave < 12 ? wave : 12) * 2.0f + randomUnit() * 10.0f;
rocks[i] = (Rock){
.body = {x, y, cosf(direction) * speed, sinf(direction) * speed},
.angle = randomUnit() * 2.0f * PI,
.spin = (randomUnit() - 0.5f) * 1.5f,
.size = size,
.active = true,
};
return true;
}
return false;
}resetShip() · source line 96
static void resetShip(void)
Calls into this documentation:
- Execute
ship = (Body){LCD_WIDTH / 2.0f, LCD_HEIGHT / 2.0f, 0, 0}; - Execute
shipAngle = -PI / 2.0f; - Execute
protection = PROTECTION_MS; - Execute
thrusting = false; - Execute
audioSetEngineThrust(false); - Execute
controlsReady = false;
Read original function source
static void resetShip(void)
{
ship = (Body){LCD_WIDTH / 2.0f, LCD_HEIGHT / 2.0f, 0, 0};
shipAngle = -PI / 2.0f;
protection = PROTECTION_MS;
thrusting = false;
audioSetEngineThrust(false);
controlsReady = false;
}beginWave() · source line 106
static void beginWave(void)
Calls into this documentation:
- Execute
memset(rocks, 0, sizeof(rocks)); - Execute
memset(bullets, 0, sizeof(bullets)); - Execute
const unsigned count = wave < 6 ? wave + 2 : 8; - FOR loop
unsigned i = 0; i < count; ++iRepeat body while the loop condition permits; then continue below.
- Execute
const float x = wrap(ship.x + LCD_WIDTH / 2.0f, LCD_WIDTH); - Execute
const float y = randomUnit() * LCD_HEIGHT; - Execute
addRock(x, y, 3, randomUnit() * 2.0f * PI);
- Execute
ESP_LOGI("asteroids", "WAVE %u", (unsigned)wave);
Read original function source
static void beginWave(void)
{
memset(rocks, 0, sizeof(rocks));
memset(bullets, 0, sizeof(bullets));
const unsigned count = wave < 6 ? wave + 2 : 8;
for (unsigned i = 0; i < count; ++i) {
// Spawn on a vertical edge, on the side opposite the ship.
const float x = wrap(ship.x + LCD_WIDTH / 2.0f, LCD_WIDTH);
const float y = randomUnit() * LCD_HEIGHT;
addRock(x, y, 3, randomUnit() * 2.0f * PI);
}
ESP_LOGI("asteroids", "WAVE %u", (unsigned)wave);
}beginGame() · source line 120
static void beginGame(uint32_t now_ms)
Calls into this documentation:
- Execute
score = 0; - Execute
wave = 1; - Execute
lives = 3; - Execute
fireDelay = accumulator = 0; - Execute
lastUpdate = now_ms; - Execute
gameState = STATE_PLAYING; - Execute
resetShip(); - Execute
beginWave(); - Execute
audioPlay(AUDIO_SFX_GAME_START);
Read original function source
static void beginGame(uint32_t now_ms)
{
score = 0;
wave = 1;
lives = 3;
fireDelay = accumulator = 0;
lastUpdate = now_ms;
gameState = STATE_PLAYING;
resetShip();
beginWave();
audioPlay(AUDIO_SFX_GAME_START);
}hitRock() · source line 133
static void hitRock(unsigned index)
Calls into this documentation:
- Execute
const Rock rock = rocks[index]; - Execute
rocks[index].active = false; - Execute
const uint32_t points = rock.size == 3 ? 20U : rock.size == 2 ? 50U : 100U; - Execute
score = score > UINT32_MAX - points ? UINT32_MAX : score + points; - Decision
rock.size > 1True- Execute
const float direction = atan2f(rock.body.vy, rock.body.vx); - Execute
addRock(rock.body.x, rock.body.y, rock.size - 1, direction - 0.65f); - Execute
addRock(rock.body.x, rock.body.y, rock.size - 1, direction + 0.65f);
FalseContinue after this decision.
- Execute
audioPlay(AUDIO_SFX_HIT);
Read original function source
static void hitRock(unsigned index)
{
const Rock rock = rocks[index];
rocks[index].active = false;
const uint32_t points = rock.size == 3 ? 20U : rock.size == 2 ? 50U : 100U;
score = score > UINT32_MAX - points ? UINT32_MAX : score + points;
if (rock.size > 1) {
const float direction = atan2f(rock.body.vy, rock.body.vx);
addRock(rock.body.x, rock.body.y, rock.size - 1, direction - 0.65f);
addRock(rock.body.x, rock.body.y, rock.size - 1, direction + 0.65f);
}
audioPlay(AUDIO_SFX_HIT);
}fireBullet() · source line 147
static void fireBullet(void)
Calls into this documentation:
- Decision
fireDelay != 0True- Exit this function
return;
FalseContinue after this decision.
- FOR loop
unsigned i = 0; i < MAX_BULLETS; ++iRepeat body while the loop condition permits; then continue below.
- Decision
bullets[i].remaining != 0True- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
const float dx = cosf(shipAngle), dy = sinf(shipAngle); - Execute
bullets[i] = (Bullet){ .body = {wrap(ship.x + dx * 9.0f, LCD_WIDTH), wrap(ship.y + dy * 9.0f, LCD_HEIGHT), ship.vx + dx * 180.0f, ship.vy + dy * 180.0f}, .remaining = 900U, }; - Execute
fireDelay = FIRE_DELAY_MS; - Execute
audioPlay(AUDIO_SFX_PLAYER_SHOT); - Exit this function
return;
Read original function source
static void fireBullet(void)
{
if (fireDelay != 0) return;
for (unsigned i = 0; i < MAX_BULLETS; ++i) {
if (bullets[i].remaining != 0) continue;
const float dx = cosf(shipAngle), dy = sinf(shipAngle);
bullets[i] = (Bullet){
.body = {wrap(ship.x + dx * 9.0f, LCD_WIDTH),
wrap(ship.y + dy * 9.0f, LCD_HEIGHT),
ship.vx + dx * 180.0f, ship.vy + dy * 180.0f},
.remaining = 900U,
};
fireDelay = FIRE_DELAY_MS;
audioPlay(AUDIO_SFX_PLAYER_SHOT);
return;
}
}autoControls() · source line 165
static void autoControls(int *rotation, bool *thrust, bool *fire)
Calls into this documentation:
- Execute
const Rock *target = NULL; - Execute
float nearestDistanceSquared = 0.0f; - FOR loop
unsigned i = 0; i < MAX_ROCKS; ++iRepeat body while the loop condition permits; then continue below.
- Decision
!rocks[i].activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
const float distanceSquaredToRock = distanceSquared(&ship, &rocks[i].body); - Decision
target == NULL || distanceSquaredToRock < nearestDistanceSquaredTrue- Execute
target = &rocks[i]; - Execute
nearestDistanceSquared = distanceSquaredToRock;
FalseContinue after this decision.
- Decision
target == NULLTrue- Exit this function
return;
FalseContinue after this decision.
- Execute
float dx = target->body.x - ship.x; - Execute
float dy = target->body.y - ship.y; - Decision
dx > LCD_WIDTH / 2.0fTrue- Execute
dx -= LCD_WIDTH;
FalseContinue after this decision.
- Decision
dx < -LCD_WIDTH / 2.0fTrue- Execute
dx += LCD_WIDTH;
FalseContinue after this decision.
- Decision
dy > LCD_HEIGHT / 2.0fTrue- Execute
dy -= LCD_HEIGHT;
FalseContinue after this decision.
- Decision
dy < -LCD_HEIGHT / 2.0fTrue- Execute
dy += LCD_HEIGHT;
FalseContinue after this decision.
- Execute
const float dangerDistance = rockRadius(target->size) + 34.0f; - Execute
const bool danger = nearestDistanceSquared < dangerDistance * dangerDistance; - Execute
const float forwardX = cosf(shipAngle); - Execute
const float forwardY = sinf(shipAngle); - Execute
const float cross = forwardX * dy - forwardY * dx; - Execute
const float dot = forwardX * dx + forwardY * dy; - Execute
const float alignmentLimitSquared = nearestDistanceSquared * 0.0676f; - Decision
dangerTrue- Execute
*rotation = cross > 0.0f ? -1 : cross < 0.0f ? 1 : 1; - Execute
*fire = false; - Execute
*thrust = true; - Exit this function
return;
FalseContinue after this decision.
- Decision
cross * cross <= alignmentLimitSquared && dot > 0.0fTrue- Execute
*rotation = 0;
False- Decision
cross > 0.0fTrue- Execute
*rotation = 1;
False- Decision
cross < 0.0fTrue- Execute
*rotation = -1;
False- Execute
*rotation = 1;
- Execute
*fire = dot > 0.0f && cross * cross <= alignmentLimitSquared; - Execute
*thrust = nearestDistanceSquared > 75.0f * 75.0f && *rotation == 0;
Read original function source
static void autoControls(int *rotation, bool *thrust, bool *fire)
{
const Rock *target = NULL;
float nearestDistanceSquared = 0.0f;
for (unsigned i = 0; i < MAX_ROCKS; ++i) {
if (!rocks[i].active) continue;
const float distanceSquaredToRock = distanceSquared(&ship, &rocks[i].body);
if (target == NULL || distanceSquaredToRock < nearestDistanceSquared) {
target = &rocks[i];
nearestDistanceSquared = distanceSquaredToRock;
}
}
if (target == NULL) return;
float dx = target->body.x - ship.x;
float dy = target->body.y - ship.y;
if (dx > LCD_WIDTH / 2.0f) dx -= LCD_WIDTH;
if (dx < -LCD_WIDTH / 2.0f) dx += LCD_WIDTH;
if (dy > LCD_HEIGHT / 2.0f) dy -= LCD_HEIGHT;
if (dy < -LCD_HEIGHT / 2.0f) dy += LCD_HEIGHT;
const float dangerDistance = rockRadius(target->size) + 34.0f;
const bool danger = nearestDistanceSquared < dangerDistance * dangerDistance;
const float forwardX = cosf(shipAngle);
const float forwardY = sinf(shipAngle);
const float cross = forwardX * dy - forwardY * dx;
const float dot = forwardX * dx + forwardY * dy;
const float alignmentLimitSquared = nearestDistanceSquared * 0.0676f;
// The cross product picks the shorter turn without converting position to an angle.
if (danger) {
*rotation = cross > 0.0f ? -1 : cross < 0.0f ? 1 : 1;
*fire = false;
*thrust = true;
return;
}
if (cross * cross <= alignmentLimitSquared && dot > 0.0f) *rotation = 0;
else if (cross > 0.0f) *rotation = 1;
else if (cross < 0.0f) *rotation = -1;
else *rotation = 1;
*fire = dot > 0.0f && cross * cross <= alignmentLimitSquared;
*thrust = nearestDistanceSquared > 75.0f * 75.0f && *rotation == 0;
}simulateStep() · source line 209
static void simulateStep(int rotation, bool thrust, bool fire)
Calls into this documentation:
- Execute
const float seconds = STEP_MS / 1000.0f; - Decision
protection > 0True- Execute
protection = protection > STEP_MS ? protection - STEP_MS : 0;
FalseContinue after this decision.
- Decision
fireDelay > 0True- Execute
fireDelay = fireDelay > STEP_MS ? fireDelay - STEP_MS : 0;
FalseContinue after this decision.
- Execute
shipAngle = wrap(shipAngle + rotation * 3.4f * seconds, 2.0f * PI); - Decision
thrusting != thrustTrue- Execute
thrusting = thrust; - Execute
audioSetEngineThrust(thrusting);
FalseContinue after this decision.
- Decision
thrustTrue- Execute
ship.vx += cosf(shipAngle) * 95.0f * seconds; - Execute
ship.vy += sinf(shipAngle) * 95.0f * seconds; - Execute
const float speed = sqrtf(ship.vx * ship.vx + ship.vy * ship.vy); - Decision
speed > 100.0fTrue- Execute
ship.vx *= 100.0f / speed; - Execute
ship.vy *= 100.0f / speed;
FalseContinue after this decision.
FalseContinue after this decision.
- Execute
moveBody(&ship, seconds); - Decision
fireTrue- Execute
fireBullet();
FalseContinue after this decision.
- FOR loop
unsigned i = 0; i < MAX_ROCKS; ++iRepeat body while the loop condition permits; then continue below.
- Decision
!rocks[i].activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
moveBody(&rocks[i].body, seconds); - Execute
rocks[i].angle = wrap(rocks[i].angle + rocks[i].spin * seconds, 2.0f * PI);
- FOR loop
unsigned i = 0; i < MAX_BULLETS; ++iRepeat body while the loop condition permits; then continue below.
- Execute
Bullet *bullet = &bullets[i]; - Decision
bullet->remaining == 0True- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
moveBody(&bullet->body, seconds); - Execute
bullet->remaining = bullet->remaining > STEP_MS ? bullet->remaining - STEP_MS : 0; - FOR loop
unsigned j = 0; j < MAX_ROCKS; ++jRepeat body while the loop condition permits; then continue below.
- Decision
!rocks[j].activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
const float radius = rockRadius(rocks[j].size) + 1.0f; - Decision
distanceSquared(&bullet->body, &rocks[j].body) <= radius * radiusTrue- Execute
bullet->remaining = 0; - Execute
hitRock(j); - Exit enclosing loop or switch
break;
FalseContinue after this decision.
- Decision
protection == 0True- FOR loop
unsigned i = 0; i < MAX_ROCKS; ++iRepeat body while the loop condition permits; then continue below.
- Decision
!rocks[i].activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
const float radius = rockRadius(rocks[i].size) + SHIP_RADIUS; - Decision
distanceSquared(&ship, &rocks[i].body) > radius * radiusTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
--lives; - Execute
ESP_LOGI("asteroids", "LIVES %u; SCORE %u", lives, (unsigned)score); - Execute
audioPlay(AUDIO_SFX_PLAYER_EXPLOSION); - Decision
lives == 0True- Execute
gameState = STATE_FINISHED; - Execute
gameOverStarted = lastUpdate; - Execute
thrusting = false; - Execute
audioSetEngineThrust(false); - Execute
const uint32_t gold = score / 1000U; - Decision
gold != 0True- Decision
(autoMode ? addAIGold(gold) : addUserGoldWon(gold))True- Execute
ESP_LOGI("asteroids", "GOLD WON: %u", (unsigned)gold);
False- Execute
ESP_LOGE("asteroids", "Failed to save %u Gold", (unsigned)gold);
FalseContinue after this decision.
False- Execute
resetShip(); - Execute
memset(bullets, 0, sizeof(bullets));
- Exit this function
return;
FalseContinue after this decision.
- FOR loop
unsigned i = 0; i < MAX_ROCKS; ++iRepeat body while the loop condition permits; then continue below.
- Decision
rocks[i].activeTrue- Exit this function
return;
FalseContinue after this decision.
- Execute
audioPlay(AUDIO_SFX_WAVE_CLEAR); - Decision
wave < UINT32_MAXTrue- Execute
++wave;
FalseContinue after this decision.
- Execute
beginWave();
Read original function source
static void simulateStep(int rotation, bool thrust, bool fire)
{
const float seconds = STEP_MS / 1000.0f;
if (protection > 0) protection = protection > STEP_MS ? protection - STEP_MS : 0;
if (fireDelay > 0) fireDelay = fireDelay > STEP_MS ? fireDelay - STEP_MS : 0;
shipAngle = wrap(shipAngle + rotation * 3.4f * seconds, 2.0f * PI);
if (thrusting != thrust) {
thrusting = thrust;
audioSetEngineThrust(thrusting);
}
if (thrust) {
ship.vx += cosf(shipAngle) * 95.0f * seconds;
ship.vy += sinf(shipAngle) * 95.0f * seconds;
const float speed = sqrtf(ship.vx * ship.vx + ship.vy * ship.vy);
if (speed > 100.0f) { ship.vx *= 100.0f / speed; ship.vy *= 100.0f / speed; }
}
moveBody(&ship, seconds);
if (fire) fireBullet();
for (unsigned i = 0; i < MAX_ROCKS; ++i) {
if (!rocks[i].active) continue;
moveBody(&rocks[i].body, seconds);
rocks[i].angle = wrap(rocks[i].angle + rocks[i].spin * seconds, 2.0f * PI);
}
for (unsigned i = 0; i < MAX_BULLETS; ++i) {
Bullet *bullet = &bullets[i];
if (bullet->remaining == 0) continue;
moveBody(&bullet->body, seconds);
bullet->remaining = bullet->remaining > STEP_MS ? bullet->remaining - STEP_MS : 0;
for (unsigned j = 0; j < MAX_ROCKS; ++j) {
if (!rocks[j].active) continue;
const float radius = rockRadius(rocks[j].size) + 1.0f;
if (distanceSquared(&bullet->body, &rocks[j].body) <= radius * radius) {
bullet->remaining = 0;
hitRock(j);
break;
}
}
}
if (protection == 0) {
for (unsigned i = 0; i < MAX_ROCKS; ++i) {
if (!rocks[i].active) continue;
const float radius = rockRadius(rocks[i].size) + SHIP_RADIUS;
if (distanceSquared(&ship, &rocks[i].body) > radius * radius) continue;
--lives;
ESP_LOGI("asteroids", "LIVES %u; SCORE %u", lives, (unsigned)score);
audioPlay(AUDIO_SFX_PLAYER_EXPLOSION);
if (lives == 0) {
gameState = STATE_FINISHED;
gameOverStarted = lastUpdate;
thrusting = false;
audioSetEngineThrust(false);
// Award whole thousands once, when the game ends.
const uint32_t gold = score / 1000U;
if (gold != 0) {
if ((autoMode ? addAIGold(gold) : addUserGoldWon(gold))) {
ESP_LOGI("asteroids", "GOLD WON: %u", (unsigned)gold);
} else {
ESP_LOGE("asteroids", "Failed to save %u Gold", (unsigned)gold);
}
}
} else {
resetShip();
memset(bullets, 0, sizeof(bullets));
}
return;
}
}
for (unsigned i = 0; i < MAX_ROCKS; ++i) {
if (rocks[i].active) return;
}
audioPlay(AUDIO_SFX_WAVE_CLEAR);
if (wave < UINT32_MAX) ++wave;
beginWave();
}startAsteroids() · source line 284
void startAsteroids(uint32_t now_ms)
Calls into this documentation:
- Execute
running = true; - Execute
gameState = STATE_READY; - Execute
lastUpdate = now_ms; - Execute
accumulator = score = 0; - Execute
thrusting = false; - Execute
audioSetEngineThrust(false); - Execute
autoMode = false; - Execute
modeSelectionReset(&modeInput); - Execute
ESP_LOGI("asteroids", "START");
Read original function source
void startAsteroids(uint32_t now_ms)
{
running = true;
gameState = STATE_READY;
lastUpdate = now_ms;
accumulator = score = 0;
thrusting = false;
audioSetEngineThrust(false);
autoMode = false;
modeSelectionReset(&modeInput);
ESP_LOGI("asteroids", "START");
}stopAsteroids() · source line 297
void stopAsteroids(void)
Calls into this documentation:
- Execute
cancelHighScoreEntry(); - Execute
audioSetEngineThrust(false); - Execute
running = false; - Execute
ESP_LOGI("asteroids", "STOP");
Read original function source
void stopAsteroids(void)
{
cancelHighScoreEntry();
audioSetEngineThrust(false);
running = false;
ESP_LOGI("asteroids", "STOP");
}asteroidsAction() · source line 305
void asteroidsAction(bool start, bool stop_auto, uint32_t now_ms)
Calls into this documentation:
- Decision
running && start && gameState == STATE_READYTrue- Execute
ESP_LOGI("asteroids", "START: %s", autoMode ? "AUTO" : "PLAY"); - Execute
beginGame(now_ms);
False- Decision
running && stop_auto && autoMode && gameState == STATE_PLAYINGTrue- Execute
autoMode = false; - Execute
modeSelectionReset(&modeInput); - Execute
thrusting = false; - Execute
audioSetEngineThrust(false); - Execute
gameState = STATE_READY;
FalseContinue after this decision.
Read original function source
void asteroidsAction(bool start, bool stop_auto, uint32_t now_ms)
{
if (running && start && gameState == STATE_READY) {
ESP_LOGI("asteroids", "START: %s", autoMode ? "AUTO" : "PLAY");
beginGame(now_ms);
}
else if (running && stop_auto && autoMode && gameState == STATE_PLAYING) {
autoMode = false;
modeSelectionReset(&modeInput);
thrusting = false;
audioSetEngineThrust(false);
gameState = STATE_READY;
}
}updateAsteroids() · source line 320
void updateAsteroids(int x_offset, int y_offset, bool valid, bool left, bool right,
bool fire, 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 - gameOverStarted >= 1000UTrue- Decision
autoModeTrue- Execute
beginGame(now_ms);
False- Execute
startHighScoreEntry(HIGH_SCORE_ASTEROIDS, 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_READYTrue- Decision
modeSelectionNext(&modeInput, x_offset, y_offset, valid, left, right, now_ms)True- Execute
autoMode = !autoMode;
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
int rotation = left || right ? (int)right - (int)left : valid && controlsReady ? (x_offset > 600 ? 1 : x_offset < -600 ? -1 : 0) : 0; - Execute
bool thrust = valid && controlsReady && y_offset > 600; - Execute
bool shouldFire = fire; - Decision
autoModeTrue- Execute
autoControls(&rotation, &thrust, &shouldFire);
FalseContinue after this decision.
- 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(autoMode ? rotation : (left || right || controlsReady) ? rotation : 0, autoMode ? thrust : controlsReady && thrust, shouldFire);
Read original function source
void updateAsteroids(int x_offset, int y_offset, bool valid, bool left, bool right,
bool fire, uint32_t now_ms)
{
if (!running) return;
const uint32_t elapsed = now_ms - lastUpdate;
lastUpdate = now_ms;
if (gameState == STATE_FINISHED && now_ms - gameOverStarted >= 1000U) {
if (autoMode) {
beginGame(now_ms);
} else {
startHighScoreEntry(HIGH_SCORE_ASTEROIDS, 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_READY) {
if (modeSelectionNext(&modeInput, x_offset, y_offset, valid, left, right, now_ms)) {
autoMode = !autoMode;
}
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;
int rotation = left || right ? (int)right - (int)left
: valid && controlsReady ? (x_offset > 600 ? 1 : x_offset < -600 ? -1 : 0) : 0;
bool thrust = valid && controlsReady && y_offset > 600;
bool shouldFire = fire;
if (autoMode) autoControls(&rotation, &thrust, &shouldFire);
// Bound catch-up after a stall; collision checks still run every 10 ms.
accumulator += elapsed > 100U ? 100U : elapsed;
while (accumulator >= STEP_MS && gameState == STATE_PLAYING) {
accumulator -= STEP_MS;
simulateStep(autoMode ? rotation : (left || right || controlsReady) ? rotation : 0,
autoMode ? thrust : controlsReady && thrust, shouldFire);
}
}drawWrappedLine() · source line 362
static void drawWrappedLine(uint16_t *frame, float x0, float y0, float x1, float y1)
Calls into this documentation:
- FOR loop
int dy = -LCD_HEIGHT; dy <= LCD_HEIGHT; dy += LCD_HEIGHTRepeat body while the loop condition permits; then continue below.
- FOR loop
int dx = -LCD_WIDTH; dx <= LCD_WIDTH; dx += LCD_WIDTHRepeat body while the loop condition permits; then continue below.
- Execute
const int ax = (int)lroundf(x0) + dx, ay = (int)lroundf(y0) + dy; - Execute
const int bx = (int)lroundf(x1) + dx, by = (int)lroundf(y1) + dy; - Decision
(ax < 0 && bx < 0) || (ax >= LCD_WIDTH && bx >= LCD_WIDTH) || (ay < 0 && by < 0) || (ay >= LCD_HEIGHT && by >= LCD_HEIGHT)True- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
draw_line(frame, ax, ay, bx, by, 0xffff);
Read original function source
static void drawWrappedLine(uint16_t *frame, float x0, float y0, float x1, float y1)
{
for (int dy = -LCD_HEIGHT; dy <= LCD_HEIGHT; dy += LCD_HEIGHT) {
for (int dx = -LCD_WIDTH; dx <= LCD_WIDTH; dx += LCD_WIDTH) {
const int ax = (int)lroundf(x0) + dx, ay = (int)lroundf(y0) + dy;
const int bx = (int)lroundf(x1) + dx, by = (int)lroundf(y1) + dy;
if ((ax < 0 && bx < 0) || (ax >= LCD_WIDTH && bx >= LCD_WIDTH) ||
(ay < 0 && by < 0) || (ay >= LCD_HEIGHT && by >= LCD_HEIGHT)) continue;
draw_line(frame, ax, ay, bx, by, 0xffff);
}
}
}drawAsteroids() · source line 375
void drawAsteroids(uint16_t *frame)
Calls into this documentation:
- Decision
!runningTrue- 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, 0); - Execute
char line[64]; - Decision
gameState == STATE_READYTrue- Execute
draw_text_centred(frame, 160, 16, "ASTEROIDS", 3, 0xffff); - Execute
draw_text_centred(frame, 83, 51, "PLAY", 2, autoMode ? COLOR_LIGHTGRAY : COLOR_YELLOW); - Execute
draw_text_centred(frame, 237, 51, "AUTO", 2, autoMode ? COLOR_YELLOW : COLOR_LIGHTGRAY); - Execute
draw_text_centred(frame, 160, 88, "D/B/STICK CHANGE A/K START", 1, 0xffff); - Execute
draw_text_centred(frame, 160, 106, "D/B/STICK LEFT/RIGHT ROTATE", 1, 0xffff); - Execute
draw_text_centred(frame, 160, 124, "STICK UP THRUST HOLD A FIRE", 1, 0xffff); - Execute
draw_text_centred(frame, 160, 153, "BOOT BACK", 1, 0x7bef); - Exit this function
return;
FalseContinue after this decision.
- FOR loop
unsigned i = 0; i < MAX_ROCKS; ++iRepeat body while the loop condition permits; then continue below.
- Execute
const Rock *rock = &rocks[i]; - Decision
!rock->activeTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
static const float shape[8] = {1.0f, 0.78f, 0.95f, 0.72f, 1.0f, 0.83f, 0.92f, 0.75f}; - FOR loop
unsigned j = 0; j < 8; ++jRepeat body while the loop condition permits; then continue below.
- Execute
const unsigned next = (j + 1) % 8; - Execute
const float a = rock->angle + j * PI / 4.0f, b = rock->angle + next * PI / 4.0f; - Execute
const float radius = rockRadius(rock->size); - Execute
drawWrappedLine(frame, rock->body.x + cosf(a) * radius * shape[j], rock->body.y + sinf(a) * radius * shape[j], rock->body.x + cosf(b) * radius * shape[next], rock->body.y + sinf(b) * radius * shape[next]);
- FOR loop
unsigned i = 0; i < MAX_BULLETS; ++iRepeat body while the loop condition permits; then continue below.
- Decision
bullets[i].remaining == 0True- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
const Body *b = &bullets[i].body; - Execute
drawWrappedLine(frame, b->x, b->y, b->x - b->vx * 0.012f, b->y - b->vy * 0.012f);
- Decision
gameState == STATE_PLAYING && (protection == 0 || (protection / 125U) % 2 == 0)True- Execute
const float dx = cosf(shipAngle), dy = sinf(shipAngle); - Execute
const float noseX = ship.x + dx * 8, noseY = ship.y + dy * 8; - Execute
const float leftX = ship.x - dx * 5 - dy * 5, leftY = ship.y - dy * 5 + dx * 5; - Execute
const float rightX = ship.x - dx * 5 + dy * 5, rightY = ship.y - dy * 5 - dx * 5; - Execute
drawWrappedLine(frame, noseX, noseY, leftX, leftY); - Execute
drawWrappedLine(frame, leftX, leftY, rightX, rightY); - Execute
drawWrappedLine(frame, rightX, rightY, noseX, noseY); - Decision
thrustingTrue- Execute
drawWrappedLine(frame, leftX, leftY, ship.x - dx * 12, ship.y - dy * 12); - Execute
drawWrappedLine(frame, rightX, rightY, ship.x - dx * 12, ship.y - dy * 12);
FalseContinue after this decision.
FalseContinue after this decision.
- Execute
fill_rect(frame, 0, 0, LCD_WIDTH, 12, 0); - Execute
snprintf(line, sizeof(line), "SCORE %u", (unsigned)score); - Execute
draw_text(frame, 3, 2, line, 1, 0xffff); - Execute
snprintf(line, sizeof(line), "WAVE %u", (unsigned)wave); - Execute
draw_text(frame, 166, 2, line, 1, 0xffff); - Execute
snprintf(line, sizeof(line), "LIVES %u", lives); - Execute
draw_text(frame, 272, 2, line, 1, 0xffff); - Decision
autoModeTrue- Execute
draw_text_centred(frame, 160, 14, "AUTO", 1, COLOR_YELLOW);
FalseContinue after this decision.
- 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 drawAsteroids(uint16_t *frame)
{
if (!running) return;
if (gameState == STATE_SCORES) {
drawHighScoreEntry(frame);
return;
}
clear(frame, 0);
char line[64];
if (gameState == STATE_READY) {
draw_text_centred(frame, 160, 16, "ASTEROIDS", 3, 0xffff);
draw_text_centred(frame, 83, 51, "PLAY", 2, autoMode ? COLOR_LIGHTGRAY : COLOR_YELLOW);
draw_text_centred(frame, 237, 51, "AUTO", 2, autoMode ? COLOR_YELLOW : COLOR_LIGHTGRAY);
draw_text_centred(frame, 160, 88, "D/B/STICK CHANGE A/K START", 1, 0xffff);
draw_text_centred(frame, 160, 106, "D/B/STICK LEFT/RIGHT ROTATE", 1, 0xffff);
draw_text_centred(frame, 160, 124, "STICK UP THRUST HOLD A FIRE", 1, 0xffff);
draw_text_centred(frame, 160, 153, "BOOT BACK", 1, 0x7bef);
return;
}
for (unsigned i = 0; i < MAX_ROCKS; ++i) {
const Rock *rock = &rocks[i];
if (!rock->active) continue;
static const float shape[8] = {1.0f, 0.78f, 0.95f, 0.72f, 1.0f, 0.83f, 0.92f, 0.75f};
for (unsigned j = 0; j < 8; ++j) {
const unsigned next = (j + 1) % 8;
const float a = rock->angle + j * PI / 4.0f, b = rock->angle + next * PI / 4.0f;
const float radius = rockRadius(rock->size);
drawWrappedLine(frame, rock->body.x + cosf(a) * radius * shape[j],
rock->body.y + sinf(a) * radius * shape[j],
rock->body.x + cosf(b) * radius * shape[next],
rock->body.y + sinf(b) * radius * shape[next]);
}
}
for (unsigned i = 0; i < MAX_BULLETS; ++i) {
if (bullets[i].remaining == 0) continue;
const Body *b = &bullets[i].body;
drawWrappedLine(frame, b->x, b->y, b->x - b->vx * 0.012f, b->y - b->vy * 0.012f);
}
if (gameState == STATE_PLAYING && (protection == 0 || (protection / 125U) % 2 == 0)) {
const float dx = cosf(shipAngle), dy = sinf(shipAngle);
const float noseX = ship.x + dx * 8, noseY = ship.y + dy * 8;
const float leftX = ship.x - dx * 5 - dy * 5, leftY = ship.y - dy * 5 + dx * 5;
const float rightX = ship.x - dx * 5 + dy * 5, rightY = ship.y - dy * 5 - dx * 5;
drawWrappedLine(frame, noseX, noseY, leftX, leftY);
drawWrappedLine(frame, leftX, leftY, rightX, rightY);
drawWrappedLine(frame, rightX, rightY, noseX, noseY);
if (thrusting) {
drawWrappedLine(frame, leftX, leftY, ship.x - dx * 12, ship.y - dy * 12);
drawWrappedLine(frame, rightX, rightY, ship.x - dx * 12, ship.y - dy * 12);
}
}
fill_rect(frame, 0, 0, LCD_WIDTH, 12, 0);
snprintf(line, sizeof(line), "SCORE %u", (unsigned)score);
draw_text(frame, 3, 2, line, 1, 0xffff);
snprintf(line, sizeof(line), "WAVE %u", (unsigned)wave);
draw_text(frame, 166, 2, line, 1, 0xffff);
snprintf(line, sizeof(line), "LIVES %u", lives);
draw_text(frame, 272, 2, line, 1, 0xffff);
if (autoMode) draw_text_centred(frame, 160, 14, "AUTO", 1, COLOR_YELLOW);
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 "asteroids.h"
#include "audio.h"
#include "game_graphics.h"
#include "PET2001_font.h"
#include "gold.h"
#include "high_scores.h"
#include "mode_selection.h"
#include "esp_random.h"
#include "esp_log.h"
#include <math.h>
#include <stdio.h>
#include <string.h>
#define PI 3.14159265f
#define MAX_ROCKS 32
#define MAX_BULLETS 8
#define STEP_MS 10U
#define FIRE_DELAY_MS 180U
#define PROTECTION_MS 2500U
#define SHIP_RADIUS 5.0f
typedef enum { STATE_READY, STATE_PLAYING, STATE_FINISHED, STATE_SCORES } GameState;
typedef struct { float x, y, vx, vy; } Body;
typedef struct {
Body body;
float angle, spin;
uint8_t size;
bool active;
} Rock;
typedef struct {
Body body;
uint32_t remaining;
} Bullet;
// Eight large rocks can split into at most 32 small rocks.
// All objects reuse fixed storage and the caller's display buffer.
static Rock rocks[MAX_ROCKS];
static Bullet bullets[MAX_BULLETS];
static Body ship;
static GameState gameState;
static float shipAngle;
static uint32_t score, wave, lastUpdate, accumulator, protection, fireDelay;
static uint32_t gameOverStarted;
static uint8_t lives;
static bool running, thrusting, controlsReady, autoMode;
static mode_selection_input_t modeInput;
static float randomUnit(void)
{
return (float)(esp_random() & 0xffffU) / 65536.0f;
}
static float wrap(float value, float extent)
{
while (value < 0.0f) value += extent;
while (value >= extent) value -= extent;
return value;
}
static float distanceSquared(const Body *a, const Body *b)
{
float dx = fabsf(a->x - b->x), dy = fabsf(a->y - b->y);
if (dx > LCD_WIDTH / 2.0f) dx = LCD_WIDTH - dx;
if (dy > LCD_HEIGHT / 2.0f) dy = LCD_HEIGHT - dy;
return dx * dx + dy * dy;
}
static float rockRadius(uint8_t size)
{
return size == 3 ? 16.0f : size == 2 ? 10.0f : 5.0f;
}
static void moveBody(Body *body, float seconds)
{
body->x = wrap(body->x + body->vx * seconds, LCD_WIDTH);
body->y = wrap(body->y + body->vy * seconds, LCD_HEIGHT);
}
static bool addRock(float x, float y, uint8_t size, float direction)
{
for (unsigned i = 0; i < MAX_ROCKS; ++i) {
if (rocks[i].active) continue;
const float speed = 16.0f + (3 - size) * 12.0f +
(wave < 12 ? wave : 12) * 2.0f + randomUnit() * 10.0f;
rocks[i] = (Rock){
.body = {x, y, cosf(direction) * speed, sinf(direction) * speed},
.angle = randomUnit() * 2.0f * PI,
.spin = (randomUnit() - 0.5f) * 1.5f,
.size = size,
.active = true,
};
return true;
}
return false;
}
static void resetShip(void)
{
ship = (Body){LCD_WIDTH / 2.0f, LCD_HEIGHT / 2.0f, 0, 0};
shipAngle = -PI / 2.0f;
protection = PROTECTION_MS;
thrusting = false;
audioSetEngineThrust(false);
controlsReady = false;
}
static void beginWave(void)
{
memset(rocks, 0, sizeof(rocks));
memset(bullets, 0, sizeof(bullets));
const unsigned count = wave < 6 ? wave + 2 : 8;
for (unsigned i = 0; i < count; ++i) {
// Spawn on a vertical edge, on the side opposite the ship.
const float x = wrap(ship.x + LCD_WIDTH / 2.0f, LCD_WIDTH);
const float y = randomUnit() * LCD_HEIGHT;
addRock(x, y, 3, randomUnit() * 2.0f * PI);
}
ESP_LOGI("asteroids", "WAVE %u", (unsigned)wave);
}
static void beginGame(uint32_t now_ms)
{
score = 0;
wave = 1;
lives = 3;
fireDelay = accumulator = 0;
lastUpdate = now_ms;
gameState = STATE_PLAYING;
resetShip();
beginWave();
audioPlay(AUDIO_SFX_GAME_START);
}
static void hitRock(unsigned index)
{
const Rock rock = rocks[index];
rocks[index].active = false;
const uint32_t points = rock.size == 3 ? 20U : rock.size == 2 ? 50U : 100U;
score = score > UINT32_MAX - points ? UINT32_MAX : score + points;
if (rock.size > 1) {
const float direction = atan2f(rock.body.vy, rock.body.vx);
addRock(rock.body.x, rock.body.y, rock.size - 1, direction - 0.65f);
addRock(rock.body.x, rock.body.y, rock.size - 1, direction + 0.65f);
}
audioPlay(AUDIO_SFX_HIT);
}
static void fireBullet(void)
{
if (fireDelay != 0) return;
for (unsigned i = 0; i < MAX_BULLETS; ++i) {
if (bullets[i].remaining != 0) continue;
const float dx = cosf(shipAngle), dy = sinf(shipAngle);
bullets[i] = (Bullet){
.body = {wrap(ship.x + dx * 9.0f, LCD_WIDTH),
wrap(ship.y + dy * 9.0f, LCD_HEIGHT),
ship.vx + dx * 180.0f, ship.vy + dy * 180.0f},
.remaining = 900U,
};
fireDelay = FIRE_DELAY_MS;
audioPlay(AUDIO_SFX_PLAYER_SHOT);
return;
}
}
static void autoControls(int *rotation, bool *thrust, bool *fire)
{
const Rock *target = NULL;
float nearestDistanceSquared = 0.0f;
for (unsigned i = 0; i < MAX_ROCKS; ++i) {
if (!rocks[i].active) continue;
const float distanceSquaredToRock = distanceSquared(&ship, &rocks[i].body);
if (target == NULL || distanceSquaredToRock < nearestDistanceSquared) {
target = &rocks[i];
nearestDistanceSquared = distanceSquaredToRock;
}
}
if (target == NULL) return;
float dx = target->body.x - ship.x;
float dy = target->body.y - ship.y;
if (dx > LCD_WIDTH / 2.0f) dx -= LCD_WIDTH;
if (dx < -LCD_WIDTH / 2.0f) dx += LCD_WIDTH;
if (dy > LCD_HEIGHT / 2.0f) dy -= LCD_HEIGHT;
if (dy < -LCD_HEIGHT / 2.0f) dy += LCD_HEIGHT;
const float dangerDistance = rockRadius(target->size) + 34.0f;
const bool danger = nearestDistanceSquared < dangerDistance * dangerDistance;
const float forwardX = cosf(shipAngle);
const float forwardY = sinf(shipAngle);
const float cross = forwardX * dy - forwardY * dx;
const float dot = forwardX * dx + forwardY * dy;
const float alignmentLimitSquared = nearestDistanceSquared * 0.0676f;
// The cross product picks the shorter turn without converting position to an angle.
if (danger) {
*rotation = cross > 0.0f ? -1 : cross < 0.0f ? 1 : 1;
*fire = false;
*thrust = true;
return;
}
if (cross * cross <= alignmentLimitSquared && dot > 0.0f) *rotation = 0;
else if (cross > 0.0f) *rotation = 1;
else if (cross < 0.0f) *rotation = -1;
else *rotation = 1;
*fire = dot > 0.0f && cross * cross <= alignmentLimitSquared;
*thrust = nearestDistanceSquared > 75.0f * 75.0f && *rotation == 0;
}
static void simulateStep(int rotation, bool thrust, bool fire)
{
const float seconds = STEP_MS / 1000.0f;
if (protection > 0) protection = protection > STEP_MS ? protection - STEP_MS : 0;
if (fireDelay > 0) fireDelay = fireDelay > STEP_MS ? fireDelay - STEP_MS : 0;
shipAngle = wrap(shipAngle + rotation * 3.4f * seconds, 2.0f * PI);
if (thrusting != thrust) {
thrusting = thrust;
audioSetEngineThrust(thrusting);
}
if (thrust) {
ship.vx += cosf(shipAngle) * 95.0f * seconds;
ship.vy += sinf(shipAngle) * 95.0f * seconds;
const float speed = sqrtf(ship.vx * ship.vx + ship.vy * ship.vy);
if (speed > 100.0f) { ship.vx *= 100.0f / speed; ship.vy *= 100.0f / speed; }
}
moveBody(&ship, seconds);
if (fire) fireBullet();
for (unsigned i = 0; i < MAX_ROCKS; ++i) {
if (!rocks[i].active) continue;
moveBody(&rocks[i].body, seconds);
rocks[i].angle = wrap(rocks[i].angle + rocks[i].spin * seconds, 2.0f * PI);
}
for (unsigned i = 0; i < MAX_BULLETS; ++i) {
Bullet *bullet = &bullets[i];
if (bullet->remaining == 0) continue;
moveBody(&bullet->body, seconds);
bullet->remaining = bullet->remaining > STEP_MS ? bullet->remaining - STEP_MS : 0;
for (unsigned j = 0; j < MAX_ROCKS; ++j) {
if (!rocks[j].active) continue;
const float radius = rockRadius(rocks[j].size) + 1.0f;
if (distanceSquared(&bullet->body, &rocks[j].body) <= radius * radius) {
bullet->remaining = 0;
hitRock(j);
break;
}
}
}
if (protection == 0) {
for (unsigned i = 0; i < MAX_ROCKS; ++i) {
if (!rocks[i].active) continue;
const float radius = rockRadius(rocks[i].size) + SHIP_RADIUS;
if (distanceSquared(&ship, &rocks[i].body) > radius * radius) continue;
--lives;
ESP_LOGI("asteroids", "LIVES %u; SCORE %u", lives, (unsigned)score);
audioPlay(AUDIO_SFX_PLAYER_EXPLOSION);
if (lives == 0) {
gameState = STATE_FINISHED;
gameOverStarted = lastUpdate;
thrusting = false;
audioSetEngineThrust(false);
// Award whole thousands once, when the game ends.
const uint32_t gold = score / 1000U;
if (gold != 0) {
if ((autoMode ? addAIGold(gold) : addUserGoldWon(gold))) {
ESP_LOGI("asteroids", "GOLD WON: %u", (unsigned)gold);
} else {
ESP_LOGE("asteroids", "Failed to save %u Gold", (unsigned)gold);
}
}
} else {
resetShip();
memset(bullets, 0, sizeof(bullets));
}
return;
}
}
for (unsigned i = 0; i < MAX_ROCKS; ++i) {
if (rocks[i].active) return;
}
audioPlay(AUDIO_SFX_WAVE_CLEAR);
if (wave < UINT32_MAX) ++wave;
beginWave();
}
void startAsteroids(uint32_t now_ms)
{
running = true;
gameState = STATE_READY;
lastUpdate = now_ms;
accumulator = score = 0;
thrusting = false;
audioSetEngineThrust(false);
autoMode = false;
modeSelectionReset(&modeInput);
ESP_LOGI("asteroids", "START");
}
void stopAsteroids(void)
{
cancelHighScoreEntry();
audioSetEngineThrust(false);
running = false;
ESP_LOGI("asteroids", "STOP");
}
void asteroidsAction(bool start, bool stop_auto, uint32_t now_ms)
{
if (running && start && gameState == STATE_READY) {
ESP_LOGI("asteroids", "START: %s", autoMode ? "AUTO" : "PLAY");
beginGame(now_ms);
}
else if (running && stop_auto && autoMode && gameState == STATE_PLAYING) {
autoMode = false;
modeSelectionReset(&modeInput);
thrusting = false;
audioSetEngineThrust(false);
gameState = STATE_READY;
}
}
void updateAsteroids(int x_offset, int y_offset, bool valid, bool left, bool right,
bool fire, uint32_t now_ms)
{
if (!running) return;
const uint32_t elapsed = now_ms - lastUpdate;
lastUpdate = now_ms;
if (gameState == STATE_FINISHED && now_ms - gameOverStarted >= 1000U) {
if (autoMode) {
beginGame(now_ms);
} else {
startHighScoreEntry(HIGH_SCORE_ASTEROIDS, 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_READY) {
if (modeSelectionNext(&modeInput, x_offset, y_offset, valid, left, right, now_ms)) {
autoMode = !autoMode;
}
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;
int rotation = left || right ? (int)right - (int)left
: valid && controlsReady ? (x_offset > 600 ? 1 : x_offset < -600 ? -1 : 0) : 0;
bool thrust = valid && controlsReady && y_offset > 600;
bool shouldFire = fire;
if (autoMode) autoControls(&rotation, &thrust, &shouldFire);
// Bound catch-up after a stall; collision checks still run every 10 ms.
accumulator += elapsed > 100U ? 100U : elapsed;
while (accumulator >= STEP_MS && gameState == STATE_PLAYING) {
accumulator -= STEP_MS;
simulateStep(autoMode ? rotation : (left || right || controlsReady) ? rotation : 0,
autoMode ? thrust : controlsReady && thrust, shouldFire);
}
}
static void drawWrappedLine(uint16_t *frame, float x0, float y0, float x1, float y1)
{
for (int dy = -LCD_HEIGHT; dy <= LCD_HEIGHT; dy += LCD_HEIGHT) {
for (int dx = -LCD_WIDTH; dx <= LCD_WIDTH; dx += LCD_WIDTH) {
const int ax = (int)lroundf(x0) + dx, ay = (int)lroundf(y0) + dy;
const int bx = (int)lroundf(x1) + dx, by = (int)lroundf(y1) + dy;
if ((ax < 0 && bx < 0) || (ax >= LCD_WIDTH && bx >= LCD_WIDTH) ||
(ay < 0 && by < 0) || (ay >= LCD_HEIGHT && by >= LCD_HEIGHT)) continue;
draw_line(frame, ax, ay, bx, by, 0xffff);
}
}
}
void drawAsteroids(uint16_t *frame)
{
if (!running) return;
if (gameState == STATE_SCORES) {
drawHighScoreEntry(frame);
return;
}
clear(frame, 0);
char line[64];
if (gameState == STATE_READY) {
drawPet2001TextCentredTransparent(frame, 160, 16, "ASTEROIDS", 0xffff, 3);
drawPet2001TextCentredTransparent(frame, 83, 51, "PLAY", autoMode ? COLOR_LIGHTGRAY : COLOR_YELLOW, 2);
drawPet2001TextCentredTransparent(frame, 237, 51, "AUTO", autoMode ? COLOR_YELLOW : COLOR_LIGHTGRAY, 2);
drawPet2001TextCentredTransparent(frame, 160, 88, "D/B/STICK CHANGE A/K START", 0xffff, 1);
drawPet2001TextCentredTransparent(frame, 160, 106, "D/B/STICK LEFT/RIGHT ROTATE", 0xffff, 1);
drawPet2001TextCentredTransparent(frame, 160, 124, "STICK UP THRUST HOLD A FIRE", 0xffff, 1);
drawPet2001TextCentredTransparent(frame, 160, 153, "BOOT BACK", 0x7bef, 1);
return;
}
for (unsigned i = 0; i < MAX_ROCKS; ++i) {
const Rock *rock = &rocks[i];
if (!rock->active) continue;
static const float shape[8] = {1.0f, 0.78f, 0.95f, 0.72f, 1.0f, 0.83f, 0.92f, 0.75f};
for (unsigned j = 0; j < 8; ++j) {
const unsigned next = (j + 1) % 8;
const float a = rock->angle + j * PI / 4.0f, b = rock->angle + next * PI / 4.0f;
const float radius = rockRadius(rock->size);
drawWrappedLine(frame, rock->body.x + cosf(a) * radius * shape[j],
rock->body.y + sinf(a) * radius * shape[j],
rock->body.x + cosf(b) * radius * shape[next],
rock->body.y + sinf(b) * radius * shape[next]);
}
}
for (unsigned i = 0; i < MAX_BULLETS; ++i) {
if (bullets[i].remaining == 0) continue;
const Body *b = &bullets[i].body;
drawWrappedLine(frame, b->x, b->y, b->x - b->vx * 0.012f, b->y - b->vy * 0.012f);
}
if (gameState == STATE_PLAYING && (protection == 0 || (protection / 125U) % 2 == 0)) {
const float dx = cosf(shipAngle), dy = sinf(shipAngle);
const float noseX = ship.x + dx * 8, noseY = ship.y + dy * 8;
const float leftX = ship.x - dx * 5 - dy * 5, leftY = ship.y - dy * 5 + dx * 5;
const float rightX = ship.x - dx * 5 + dy * 5, rightY = ship.y - dy * 5 - dx * 5;
drawWrappedLine(frame, noseX, noseY, leftX, leftY);
drawWrappedLine(frame, leftX, leftY, rightX, rightY);
drawWrappedLine(frame, rightX, rightY, noseX, noseY);
if (thrusting) {
drawWrappedLine(frame, leftX, leftY, ship.x - dx * 12, ship.y - dy * 12);
drawWrappedLine(frame, rightX, rightY, ship.x - dx * 12, ship.y - dy * 12);
}
}
fill_rect(frame, 0, 0, LCD_WIDTH, 12, 0);
snprintf(line, sizeof(line), "SCORE %u", (unsigned)score);
drawPet2001TextTransparent(frame, 3, 2, line, 0xffff, 1);
snprintf(line, sizeof(line), "WAVE %u", (unsigned)wave);
drawPet2001TextTransparent(frame, 166, 2, line, 0xffff, 1);
snprintf(line, sizeof(line), "LIVES %u", lives);
drawPet2001TextTransparent(frame, 264, 2, line, 0xffff, 1);
if (autoMode) drawPet2001TextCentredTransparent(frame, 160, 14, "AUTO", COLOR_YELLOW, 1);
if (gameState == STATE_FINISHED) {
drawPet2001TextCentredTransparent(frame, 160, 78, "GAME OVER", COLOR_RED, 2);
}
}