feat: add finite lifetimes and despawn mechanics for rotten and cursed apples

This commit is contained in:
phillip committed 2026-10-03 21:57:13 +02:00
1 parent 9a3a0bd9fd
commit adda5db2f4
8 files changed
+159 -82

No files matched your search

+88 -33
View File
@@ -3,8 +3,8 @@
#include <algorithm>
#include <cstdlib>
#include <nds.h>
#include <consts.h>
#include <nds.h>
void update_snake(GameState &state) {
std::array<int, 3> headpos = state.snake.front();
@@ -79,8 +79,8 @@ bool check_collision_apples_rotten(GameState &state) {
auto it =
std::find_if(state.bad_apples.begin(), state.bad_apples.end(),
[&](const std::array<int, 2> &apple) {
return apple[0] == headpos[0] && apple[1] == headpos[1];
[&](const EvilItem &apple) {
return apple.x == headpos[0] && apple.y == headpos[1];
});
if (it == state.bad_apples.end()) {
@@ -100,8 +100,8 @@ bool check_collision_apples_cursed(GameState &state) {
auto it =
std::find_if(state.cursed_apples.begin(), state.cursed_apples.end(),
[&](const std::array<int, 2> &apple) {
return apple[0] == headpos[0] && apple[1] == headpos[1];
[&](const EvilItem &apple) {
return apple.x == headpos[0] && apple.y == headpos[1];
});
if (it == state.cursed_apples.end()) {
@@ -127,19 +127,64 @@ void spawn_apples(GameState &state) {
}
void spawn_apples_rotten(GameState &state) {
if ((state.ticks % 10 == 0) && state.hardMode && ((rand() % 10) < 10) &&
if ((state.ticks % 10 == 0) && state.hardMode &&
(state.bad_apples.size() < 20)) {
std::array<int, 2> badApple = {rand() % (X_MAX + 1), rand() % (Y_MAX + 1)};
state.bad_apples.push_back(badApple);
int x = rand() % (X_MAX + 1);
int y = rand() % (Y_MAX + 1);
for (int attempt = 0; attempt < 16 && (is_barrier_cell(state, x, y, true) ||
(!state.snake.empty() &&
state.snake.front()[0] == x &&
state.snake.front()[1] == y));
attempt++) {
x = rand() % (X_MAX + 1);
y = rand() % (Y_MAX + 1);
}
state.bad_apples.push_back({static_cast<int8_t>(x), static_cast<int8_t>(y),
BAD_APPLE_LIFETIME_TICKS});
}
}
void spawn_apples_cursed(GameState &state) {
if (state.cursed_apples.empty() && (state.ticks % 20 == 0) &&
((rand() % 5) == 0)) {
std::array<int, 2> cursedApple = {rand() % (X_MAX + 1),
rand() % (Y_MAX + 1)};
state.cursed_apples.push_back(cursedApple);
int x = rand() % (X_MAX + 1);
int y = rand() % (Y_MAX + 1);
for (int attempt = 0; attempt < 16 && (is_barrier_cell(state, x, y, true) ||
(!state.snake.empty() &&
state.snake.front()[0] == x &&
state.snake.front()[1] == y));
attempt++) {
x = rand() % (X_MAX + 1);
y = rand() % (Y_MAX + 1);
}
state.cursed_apples.push_back({static_cast<int8_t>(x),
static_cast<int8_t>(y),
CURSED_APPLE_LIFETIME_TICKS});
}
}
void tick_evil_items(GameState &state) {
for (auto it = state.bad_apples.begin(); it != state.bad_apples.end();) {
if (it->ticksRemaining > 0) {
it->ticksRemaining--;
}
if (it->ticksRemaining == 0) {
it = state.bad_apples.erase(it);
} else {
++it;
}
}
for (auto it = state.cursed_apples.begin();
it != state.cursed_apples.end();) {
if (it->ticksRemaining > 0) {
it->ticksRemaining--;
}
if (it->ticksRemaining == 0) {
it = state.cursed_apples.erase(it);
} else {
++it;
}
}
}
@@ -151,8 +196,8 @@ void tick_status_effects(GameState &state) {
uint8_t calculate_intensity_31(const GameState &state) {
const uint16_t current_apples = state.applesEaten;
const uint16_t apples =
std::min<uint16_t>(current_apples, static_cast<uint16_t>(SPEED_APPLES_CAP));
const uint16_t apples = std::min<uint16_t>(
current_apples, static_cast<uint16_t>(SPEED_APPLES_CAP));
return static_cast<uint8_t>((static_cast<uint32_t>(apples) * 31U) /
static_cast<uint32_t>(SPEED_APPLES_CAP));
}
@@ -163,12 +208,12 @@ uint16_t calculate_timer_freq_mhz(const GameState &state) {
const uint16_t max_mhz =
state.hardMode ? SPEED_MAX_HARD_MHZ : SPEED_MAX_NORMAL_MHZ;
const uint16_t current_apples = state.applesEaten;
const uint16_t apples =
std::min<uint16_t>(current_apples, static_cast<uint16_t>(SPEED_APPLES_CAP));
const uint16_t apples = std::min<uint16_t>(
current_apples, static_cast<uint16_t>(SPEED_APPLES_CAP));
const uint32_t diff = static_cast<uint32_t>(max_mhz - base_mhz);
return static_cast<uint16_t>(
base_mhz + ((static_cast<uint32_t>(apples) * diff) /
static_cast<uint32_t>(SPEED_APPLES_CAP)));
return static_cast<uint16_t>(base_mhz +
((static_cast<uint32_t>(apples) * diff) /
static_cast<uint32_t>(SPEED_APPLES_CAP)));
}
uint16_t calculate_timer_reload(const GameState &state) {
@@ -208,11 +253,18 @@ bool cell_in_list(const std::vector<std::array<int, 2>> &list, int x, int y) {
});
}
bool cell_in_list(const std::vector<EvilItem> &list, int x, int y) {
return std::any_of(list.begin(), list.end(), [&](const EvilItem &item) {
return item.x == x && item.y == y;
});
}
bool cell_blocked_for_barrier(const GameState &state, int x, int y) {
if (is_barrier_cell(state, x, y, true)) {
return true;
}
if (cell_in_list(state.apples, x, y) || cell_in_list(state.bad_apples, x, y) ||
if (cell_in_list(state.apples, x, y) ||
cell_in_list(state.bad_apples, x, y) ||
cell_in_list(state.cursed_apples, x, y)) {
return true;
}
@@ -311,7 +363,8 @@ void tick_barriers_frame(GameState &state) {
}
if (state.barrierSpawnCooldown > 0) {
state.barrierSpawnCooldown = static_cast<uint16_t>(state.barrierSpawnCooldown - 1U);
state.barrierSpawnCooldown =
static_cast<uint16_t>(state.barrierSpawnCooldown - 1U);
}
for (Barrier &barrier : state.barriers) {
@@ -337,9 +390,9 @@ void spawn_barriers(GameState &state) {
return;
}
const int in_use = static_cast<int>(
std::count_if(state.barriers.begin(), state.barriers.end(),
[](const Barrier &b) { return b.phase != BarrierPhase::Inactive; }));
const int in_use = static_cast<int>(std::count_if(
state.barriers.begin(), state.barriers.end(),
[](const Barrier &b) { return b.phase != BarrierPhase::Inactive; }));
const int max_allowed = max_concurrent_barriers(state);
const int available = max_allowed - in_use;
if (available <= 0) {
@@ -359,9 +412,9 @@ void spawn_barriers(GameState &state) {
const int length_range = current_max - BARRIER_MIN_LENGTH + 1;
for (int spawn_idx = 0; spawn_idx < to_spawn; spawn_idx++) {
auto free_slot =
std::find_if(state.barriers.begin(), state.barriers.end(),
[](const Barrier &b) { return b.phase == BarrierPhase::Inactive; });
auto free_slot = std::find_if(
state.barriers.begin(), state.barriers.end(),
[](const Barrier &b) { return b.phase == BarrierPhase::Inactive; });
if (free_slot == state.barriers.end()) {
break;
}
@@ -370,11 +423,13 @@ void spawn_barriers(GameState &state) {
for (int attempt = 0; attempt < 48; attempt++) {
Barrier candidate;
candidate.vertical = (rand() % 2) == 0;
candidate.length = static_cast<uint8_t>(
BARRIER_MIN_LENGTH + (rand() % length_range));
candidate.length =
static_cast<uint8_t>(BARRIER_MIN_LENGTH + (rand() % length_range));
// Keep the whole line on screen (barriers never wrap around edges)
const int max_x = candidate.vertical ? X_MAX : (X_MAX + 1 - candidate.length);
const int max_y = candidate.vertical ? (Y_MAX + 1 - candidate.length) : Y_MAX;
const int max_x =
candidate.vertical ? X_MAX : (X_MAX + 1 - candidate.length);
const int max_y =
candidate.vertical ? (Y_MAX + 1 - candidate.length) : Y_MAX;
candidate.x = static_cast<int8_t>(rand() % (max_x + 1));
candidate.y = static_cast<int8_t>(rand() % (max_y + 1));
@@ -401,9 +456,9 @@ void spawn_barriers(GameState &state) {
}
if (spawned_count > 0) {
state.barrierSpawnCooldown = static_cast<uint16_t>(
calculate_barrier_interval_frames(state) +
(rand() % (BARRIER_INTERVAL_JITTER + 1)));
state.barrierSpawnCooldown =
static_cast<uint16_t>(calculate_barrier_interval_frames(state) +
(rand() % (BARRIER_INTERVAL_JITTER + 1)));
} else {
state.barrierSpawnCooldown = BARRIER_RETRY_FRAMES;
}
+16 -6
View File
@@ -125,13 +125,18 @@ void draw_apples(const GameState &state, const RenderAssets &assets,
void draw_apples_rotten(const GameState &state, const RenderAssets &assets,
uint16_t &oamId) {
for (const std::array<int, 2> &apple : state.bad_apples) {
for (const EvilItem &apple : state.bad_apples) {
if (oamId >= 128) {
return;
}
oamSet(&oamSub, oamId, apple[0] * 8, apple[1] * 8, 0, 0, SpriteSize_8x8,
const bool blink = (apple.ticksRemaining <= EVIL_ITEM_BLINK_TICKS);
const bool is_hidden =
blink && ((apple.ticksRemaining <= EVIL_ITEM_FAST_BLINK_TICKS)
? ((state.ticks % 2) == 0)
: (((state.ticks / 2) % 2) == 0));
oamSet(&oamSub, oamId, apple.x * 8, apple.y * 8, 0, 0, SpriteSize_8x8,
SpriteColorFormat_256Color, assets.gfx_apple_rotten, -1, false,
false, false, false, false);
is_hidden, false, false, false);
oamId++;
}
}
@@ -139,13 +144,18 @@ void draw_apples_rotten(const GameState &state, const RenderAssets &assets,
void draw_apples_cursed(const GameState &state, const RenderAssets &assets,
uint16_t &oamId) {
const bool hflip = ((state.ticks % 6) < 3);
for (const std::array<int, 2> &apple : state.cursed_apples) {
for (const EvilItem &apple : state.cursed_apples) {
if (oamId >= 128) {
return;
}
oamSet(&oamSub, oamId, apple[0] * 8, apple[1] * 8, 0, 0, SpriteSize_8x8,
const bool blink = (apple.ticksRemaining <= EVIL_ITEM_BLINK_TICKS);
const bool is_hidden =
blink && ((apple.ticksRemaining <= EVIL_ITEM_FAST_BLINK_TICKS)
? ((state.ticks % 2) == 0)
: (((state.ticks / 2) % 2) == 0));
oamSet(&oamSub, oamId, apple.x * 8, apple.y * 8, 0, 0, SpriteSize_8x8,
SpriteColorFormat_256Color, assets.gfx_apple_cursed, -1, false,
false, hflip, false, false);
is_hidden, hflip, false, false);
oamId++;
}
}
+31 -37
View File
@@ -37,7 +37,7 @@ bool &hardMode = gameState.hardMode;
std::deque<std::array<int, 3>> &snake = gameState.snake;
std::vector<std::array<int, 2>> &apples = gameState.apples;
std::vector<std::array<int, 2>> &bad_apples = gameState.bad_apples;
std::vector<EvilItem> &bad_apples = gameState.bad_apples;
// 31x23
PrintConsole topScreen;
@@ -49,6 +49,7 @@ AudioContext audioContext;
RenderAssets renderAssets;
BackgroundContext bgContext;
InputContext inputContext;
const void * get_sprite_pointer(uint16_t spriteIndex) {
return &tilemapTiles[spriteIndex*16];
@@ -172,6 +173,11 @@ void setup() {
dest_barrier_warn[i] = src_barrier_warn[i];
}
// Pre-reserve capacities to eliminate dynamic heap allocations inside Timer 0 ISR (do_tick)
gameState.bad_apples.reserve(32);
gameState.cursed_apples.reserve(4);
gameState.apples.reserve(4);
audio_setup(audioContext);
}
@@ -229,6 +235,7 @@ void do_tick() {
if (check_collision_apples_cursed(gameState)) {
audio_play_boom(audioContext);
}
tick_evil_items(gameState);
spawn_apples(gameState);
spawn_apples_rotten(gameState);
spawn_apples_cursed(gameState);
@@ -245,51 +252,41 @@ void do_tick() {
}
void init_game() {
timerStop(0);
applesEaten = 0;
reset_barriers(gameState);
gameState.bad_apples.reserve(32);
gameState.cursed_apples.reserve(4);
gameState.apples.reserve(4);
score = 0;
ticks = 0;
gameOver = false;
snakeDirection = 1;
gameState.curseTicksRemaining = 0;
apples.clear();
bad_apples.clear();
gameState.cursed_apples.clear();
if (hardMode) {
timerStart(0, ClockDivider_1024, calculate_timer_reload(gameState), do_tick);
setBackdropColorSub(RGB15(8, 3, 3));
setBackdropColor(RGB15(6, 2, 2));
score = 0;
snakelength = START_LENGTH_HARD;
snake = { {2,1,1}, {1,1,1} };
apples.clear();
bad_apples.clear();
snakeDirection = 1;
ticks = 0;
gameOver=false;
gameState.curseTicksRemaining = 0;
gameState.cursed_apples.clear();
consoleSelect(&topScreen);
consoleClear();
invalidate_hud(renderAssets);
consoleSelect(&bottomScreen);
consoleClear();
oamClear(&oamSub,0,128);
audio_start_music(audioContext);
}
else
{
timerStart(0, ClockDivider_1024, calculate_timer_reload(gameState), do_tick);
} else {
setBackdropColorSub(RGB15(3, 4, 7));
setBackdropColor(RGB15(2, 3, 6));
score = 0;
snakelength = START_LENGTH;
snake = { {4,1,1}, {3,1,1}, {2,1,1}, {1,1,1} };
snakeDirection = 1;
ticks = 0;
gameOver=false;
gameState.curseTicksRemaining = 0;
gameState.cursed_apples.clear();
consoleSelect(&topScreen);
consoleClear();
invalidate_hud(renderAssets);
consoleSelect(&bottomScreen);
consoleClear();
oamClear(&oamSub,0,128);
audio_start_music(audioContext);
}
consoleSelect(&topScreen);
consoleClear();
invalidate_hud(renderAssets);
consoleSelect(&bottomScreen);
consoleClear();
oamClear(&oamSub, 0, 128);
audio_start_music(audioContext);
timerStart(0, ClockDivider_1024, calculate_timer_reload(gameState), do_tick);
}
int main(void) {
@@ -302,7 +299,6 @@ int main(void) {
setBackdropColorSub(RGB15(2, 2, 6));
draw_menu(gameState, renderAssets, audio_is_music_enabled(audioContext));
InputContext inputContext;
inputContext.state = &gameState;
inputContext.renderAssets = &renderAssets;
inputContext.audio = &audioContext;
@@ -310,12 +306,10 @@ int main(void) {
inputContext.topScreen = &topScreen;
inputContext.init_game = init_game;
while(1) {
if (!handle_input_frame(inputContext)) {
break;
}
}
return 0;