- Make handle_vblank strictly increment frame counter (frame++) - Move heavy frame logic (background updates, OAM updates, palette animations, and console stats) to update_frame() on the main thread synchronized with swiWaitForVBlank() - Eliminate 256-byte ARM9 IRQ stack (sp_irq) overflow caused by bg_update and iprintf that was corrupting DTCM stack and causing data aborts - Preserve all playfield entities on game over with top-screen defeat summary and smooth background deceleration Co-authored-by: Antigravity <antigravity@google.com>
481 lines
15 KiB
C++
481 lines
15 KiB
C++
#include "game_logic.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstdlib>
|
|
|
|
#include <consts.h>
|
|
#include <nds.h>
|
|
|
|
void update_snake(GameState &state) {
|
|
std::array<int, 3> headpos = state.snake.front();
|
|
std::array<int, 3> newHead = headpos;
|
|
|
|
if (state.snakeDirection == 0) {
|
|
newHead = {headpos[0], headpos[1] - 1, state.snakeDirection};
|
|
} else if (state.snakeDirection == 1) {
|
|
newHead = {headpos[0] + 1, headpos[1], state.snakeDirection};
|
|
} else if (state.snakeDirection == 2) {
|
|
newHead = {headpos[0], headpos[1] + 1, state.snakeDirection};
|
|
} else if (state.snakeDirection == 3) {
|
|
newHead = {headpos[0] - 1, headpos[1], state.snakeDirection};
|
|
}
|
|
|
|
if (newHead[0] > X_MAX) {
|
|
newHead[0] = 0;
|
|
}
|
|
if (newHead[1] > Y_MAX) {
|
|
newHead[1] = 0;
|
|
}
|
|
if (newHead[0] < 0) {
|
|
newHead[0] = X_MAX;
|
|
}
|
|
if (newHead[1] < 0) {
|
|
newHead[1] = Y_MAX;
|
|
}
|
|
|
|
state.snake.push_front(newHead);
|
|
state.snake.resize(state.snakelength);
|
|
}
|
|
|
|
bool check_collision_self(GameState &state) {
|
|
if (state.snake.size() < 2 || state.gameOver) {
|
|
return false;
|
|
}
|
|
|
|
const std::array<int, 3> headpos = state.snake.front();
|
|
|
|
for (auto it = state.snake.begin() + 1; it != state.snake.end(); ++it) {
|
|
if ((*it)[0] == headpos[0] && (*it)[1] == headpos[1]) {
|
|
state.gameOver = true;
|
|
state.gameOverReason = GameOverReason::SelfCollision;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool check_collision_apples(GameState &state) {
|
|
const std::array<int, 3> headpos = state.snake.front();
|
|
|
|
auto it =
|
|
std::find_if(state.apples.begin(), state.apples.end(),
|
|
[&](const std::array<int, 2> &apple) {
|
|
return apple[0] == headpos[0] && apple[1] == headpos[1];
|
|
});
|
|
|
|
if (it == state.apples.end()) {
|
|
return false;
|
|
}
|
|
|
|
state.score += APPLE_SCORE;
|
|
state.snakelength++;
|
|
state.applesEaten++;
|
|
state.apples.erase(it);
|
|
return true;
|
|
}
|
|
|
|
bool check_collision_apples_rotten(GameState &state) {
|
|
const std::array<int, 3> headpos = state.snake.front();
|
|
|
|
auto it =
|
|
std::find_if(state.bad_apples.begin(), state.bad_apples.end(),
|
|
[&](const EvilItem &apple) {
|
|
return apple.x == headpos[0] && apple.y == headpos[1];
|
|
});
|
|
|
|
if (it == state.bad_apples.end()) {
|
|
return false;
|
|
}
|
|
|
|
state.snakelength--;
|
|
state.bad_apples.erase(it);
|
|
if (state.snakelength < 2) {
|
|
state.gameOver = true;
|
|
state.gameOverReason = GameOverReason::RottenAppleDepleted;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool check_collision_apples_cursed(GameState &state) {
|
|
const std::array<int, 3> headpos = state.snake.front();
|
|
|
|
auto it =
|
|
std::find_if(state.cursed_apples.begin(), state.cursed_apples.end(),
|
|
[&](const EvilItem &apple) {
|
|
return apple.x == headpos[0] && apple.y == headpos[1];
|
|
});
|
|
|
|
if (it == state.cursed_apples.end()) {
|
|
return false;
|
|
}
|
|
|
|
state.cursed_apples.erase(it);
|
|
state.curseTicksRemaining = CURSE_DURATION_TICKS;
|
|
return true;
|
|
}
|
|
|
|
void spawn_apples(GameState &state) {
|
|
if (state.apples.empty()) {
|
|
std::array<int, 2> apple = {rand() % (X_MAX + 1), rand() % (Y_MAX + 1)};
|
|
// Don't place the apple inside a (pending) barrier, it would be unreachable
|
|
for (int attempt = 0;
|
|
attempt < 16 && is_barrier_cell(state, apple[0], apple[1], true);
|
|
attempt++) {
|
|
apple = {rand() % (X_MAX + 1), rand() % (Y_MAX + 1)};
|
|
}
|
|
state.apples.push_back(apple);
|
|
}
|
|
}
|
|
|
|
void spawn_apples_rotten(GameState &state) {
|
|
if ((state.ticks % 10 == 0) && state.hardMode &&
|
|
(state.bad_apples.size() < 20)) {
|
|
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)) {
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
void tick_status_effects(GameState &state) {
|
|
if (state.curseTicksRemaining > 0) {
|
|
state.curseTicksRemaining--;
|
|
}
|
|
}
|
|
|
|
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));
|
|
return static_cast<uint8_t>((static_cast<uint32_t>(apples) * 31U) /
|
|
static_cast<uint32_t>(SPEED_APPLES_CAP));
|
|
}
|
|
|
|
uint16_t calculate_timer_freq_mhz(const GameState &state) {
|
|
const uint16_t base_mhz =
|
|
state.hardMode ? SPEED_BASE_HARD_MHZ : SPEED_BASE_NORMAL_MHZ;
|
|
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 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)));
|
|
}
|
|
|
|
uint16_t calculate_timer_reload(const GameState &state) {
|
|
const uint32_t freq_mhz = calculate_timer_freq_mhz(state);
|
|
// Timer clock with ClockDivider_1024 is BUS_CLOCK >> 10 (32728 Hz)
|
|
const uint32_t timer_clock_mhz = (BUS_CLOCK >> 10U) * 1000U;
|
|
const uint32_t period_ticks = timer_clock_mhz / freq_mhz;
|
|
return static_cast<uint16_t>(-static_cast<int32_t>(period_ticks));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Hard-mode temporary barriers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
namespace {
|
|
|
|
bool barrier_contains(const Barrier &barrier, int x, int y) {
|
|
if (barrier.vertical) {
|
|
return x == barrier.x && y >= barrier.y && y < barrier.y + barrier.length;
|
|
}
|
|
return y == barrier.y && x >= barrier.x && x < barrier.x + barrier.length;
|
|
}
|
|
|
|
// Manhattan distance on the wrapping playfield
|
|
int wrapped_distance(int x1, int y1, int x2, int y2) {
|
|
int dx = std::abs(x1 - x2);
|
|
int dy = std::abs(y1 - y2);
|
|
dx = std::min(dx, (X_MAX + 1) - dx);
|
|
dy = std::min(dy, (Y_MAX + 1) - dy);
|
|
return dx + dy;
|
|
}
|
|
|
|
bool cell_in_list(const std::vector<std::array<int, 2>> &list, int x, int y) {
|
|
return std::any_of(list.begin(), list.end(),
|
|
[&](const std::array<int, 2> &item) {
|
|
return item[0] == x && item[1] == 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) ||
|
|
cell_in_list(state.cursed_apples, x, y)) {
|
|
return true;
|
|
}
|
|
if (!state.snake.empty()) {
|
|
const auto &head = state.snake.front();
|
|
if (wrapped_distance(head[0], head[1], x, y) < BARRIER_HEAD_SAFE_DISTANCE) {
|
|
return true;
|
|
}
|
|
}
|
|
return std::any_of(state.snake.begin(), state.snake.end(),
|
|
[&](const std::array<int, 3> &seg) {
|
|
return seg[0] == x && seg[1] == y;
|
|
});
|
|
}
|
|
|
|
int max_concurrent_barriers(const GameState &state) {
|
|
if (state.applesEaten < BARRIER_START_APPLES) {
|
|
return 0;
|
|
}
|
|
const int apples = std::min<int>(state.applesEaten, SPEED_APPLES_CAP);
|
|
const int apple_span = SPEED_APPLES_CAP - BARRIER_START_APPLES;
|
|
if (apple_span <= 0) {
|
|
return BARRIER_MAX_COUNT;
|
|
}
|
|
const int progress = apples - BARRIER_START_APPLES;
|
|
return 1 + ((progress * (BARRIER_MAX_COUNT - 1)) / apple_span);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void reset_barriers(GameState &state) {
|
|
// Called from the main thread; keep the VBlank/Timer IRQs out while resetting
|
|
const int old_ime = enterCriticalSection();
|
|
for (Barrier &barrier : state.barriers) {
|
|
barrier = Barrier{};
|
|
}
|
|
state.barrierSpawnCooldown = BARRIER_FIRST_SPAWN_FRAMES;
|
|
leaveCriticalSection(old_ime);
|
|
}
|
|
|
|
bool is_barrier_cell(const GameState &state, int x, int y,
|
|
bool include_warning) {
|
|
return std::any_of(
|
|
state.barriers.begin(), state.barriers.end(), [&](const Barrier &b) {
|
|
const bool relevant =
|
|
(b.phase == BarrierPhase::Active) ||
|
|
(include_warning && b.phase == BarrierPhase::Warning);
|
|
return relevant && barrier_contains(b, x, y);
|
|
});
|
|
}
|
|
|
|
uint8_t calculate_barrier_max_length(const GameState &state) {
|
|
if (state.applesEaten <= BARRIER_START_APPLES) {
|
|
return static_cast<uint8_t>(BARRIER_BASE_MAX_LENGTH);
|
|
}
|
|
const int apples = std::min<int>(state.applesEaten, SPEED_APPLES_CAP);
|
|
const int apple_span = SPEED_APPLES_CAP - BARRIER_START_APPLES;
|
|
if (apple_span <= 0) {
|
|
return static_cast<uint8_t>(BARRIER_MAX_LENGTH);
|
|
}
|
|
const int length_span = BARRIER_MAX_LENGTH - BARRIER_BASE_MAX_LENGTH;
|
|
const int progress = apples - BARRIER_START_APPLES;
|
|
return static_cast<uint8_t>(BARRIER_BASE_MAX_LENGTH +
|
|
((progress * length_span) / apple_span));
|
|
}
|
|
|
|
uint8_t calculate_barrier_max_batch(const GameState &state) {
|
|
if (state.applesEaten >= BARRIER_MULTI_SPAWN_APPLES_3) {
|
|
return 3;
|
|
}
|
|
if (state.applesEaten >= BARRIER_MULTI_SPAWN_APPLES_2) {
|
|
return 2;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
uint16_t calculate_barrier_interval_frames(const GameState &state) {
|
|
if (state.applesEaten <= BARRIER_START_APPLES) {
|
|
return BARRIER_INTERVAL_MAX_FRAMES;
|
|
}
|
|
const int apples = std::min<int>(state.applesEaten, SPEED_APPLES_CAP);
|
|
const int apple_span = SPEED_APPLES_CAP - BARRIER_START_APPLES;
|
|
if (apple_span <= 0) {
|
|
return BARRIER_INTERVAL_MIN_FRAMES;
|
|
}
|
|
const int span = BARRIER_INTERVAL_MAX_FRAMES - BARRIER_INTERVAL_MIN_FRAMES;
|
|
const int progress = apples - BARRIER_START_APPLES;
|
|
return static_cast<uint16_t>(BARRIER_INTERVAL_MAX_FRAMES -
|
|
((progress * span) / apple_span));
|
|
}
|
|
|
|
void tick_barriers_frame(GameState &state) {
|
|
if (state.applesEaten < BARRIER_START_APPLES) {
|
|
state.barrierSpawnCooldown = BARRIER_FIRST_SPAWN_FRAMES;
|
|
return;
|
|
}
|
|
|
|
if (state.barrierSpawnCooldown > 0) {
|
|
state.barrierSpawnCooldown =
|
|
static_cast<uint16_t>(state.barrierSpawnCooldown - 1U);
|
|
}
|
|
|
|
for (Barrier &barrier : state.barriers) {
|
|
if (barrier.phase == BarrierPhase::Inactive) {
|
|
continue;
|
|
}
|
|
if (barrier.framesLeft > 0) {
|
|
barrier.framesLeft--;
|
|
continue;
|
|
}
|
|
if (barrier.phase == BarrierPhase::Warning) {
|
|
barrier.phase = BarrierPhase::Active;
|
|
barrier.framesLeft = BARRIER_ACTIVE_FRAMES;
|
|
} else {
|
|
barrier.phase = BarrierPhase::Inactive;
|
|
}
|
|
}
|
|
}
|
|
|
|
void spawn_barriers(GameState &state) {
|
|
if (!state.hardMode || state.applesEaten < BARRIER_START_APPLES ||
|
|
state.barrierSpawnCooldown > 0) {
|
|
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 max_allowed = max_concurrent_barriers(state);
|
|
const int available = max_allowed - in_use;
|
|
if (available <= 0) {
|
|
state.barrierSpawnCooldown = BARRIER_RETRY_FRAMES;
|
|
return;
|
|
}
|
|
|
|
const int max_batch = calculate_barrier_max_batch(state);
|
|
int to_spawn = 1;
|
|
if (max_batch > 1 && available > 1) {
|
|
const int max_possible = std::min(max_batch, available);
|
|
to_spawn = 1 + (rand() % max_possible);
|
|
}
|
|
|
|
int spawned_count = 0;
|
|
const int current_max = calculate_barrier_max_length(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; });
|
|
if (free_slot == state.barriers.end()) {
|
|
break;
|
|
}
|
|
|
|
bool spawned = false;
|
|
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));
|
|
// 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;
|
|
candidate.x = static_cast<int8_t>(rand() % (max_x + 1));
|
|
candidate.y = static_cast<int8_t>(rand() % (max_y + 1));
|
|
|
|
bool valid = true;
|
|
for (int i = 0; i < candidate.length && valid; i++) {
|
|
const int cx = candidate.x + (candidate.vertical ? 0 : i);
|
|
const int cy = candidate.y + (candidate.vertical ? i : 0);
|
|
valid = !cell_blocked_for_barrier(state, cx, cy);
|
|
}
|
|
|
|
if (valid) {
|
|
candidate.phase = BarrierPhase::Warning;
|
|
candidate.framesLeft = BARRIER_WARN_FRAMES;
|
|
*free_slot = candidate;
|
|
spawned = true;
|
|
spawned_count++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!spawned) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (spawned_count > 0) {
|
|
state.barrierSpawnCooldown =
|
|
static_cast<uint16_t>(calculate_barrier_interval_frames(state) +
|
|
(rand() % (BARRIER_INTERVAL_JITTER + 1)));
|
|
} else {
|
|
state.barrierSpawnCooldown = BARRIER_RETRY_FRAMES;
|
|
}
|
|
}
|
|
|
|
bool check_collision_barriers(GameState &state) {
|
|
if (state.gameOver || state.snake.empty()) {
|
|
return false;
|
|
}
|
|
const auto &head = state.snake.front();
|
|
if (is_barrier_cell(state, head[0], head[1], false)) {
|
|
state.gameOver = true;
|
|
state.gameOverReason = GameOverReason::BarrierCollision;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|