Files
snakePlus/source/game_logic.cpp
T

423 lines
13 KiB
C++

#include "game_logic.h"
#include <algorithm>
#include <cstdlib>
#include <nds.h>
#include <consts.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;
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 std::array<int, 2> &apple) {
return apple[0] == headpos[0] && apple[1] == headpos[1];
});
if (it == state.bad_apples.end()) {
return false;
}
state.snakelength--;
state.bad_apples.erase(it);
if (state.snakelength < 2) {
state.gameOver = true;
}
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 std::array<int, 2> &apple) {
return apple[0] == headpos[0] && apple[1] == 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 && ((rand() % 10) < 10) &&
(state.bad_apples.size() < 20)) {
std::array<int, 2> badApple = {rand() % (X_MAX + 1), rand() % (Y_MAX + 1)};
state.bad_apples.push_back(badApple);
}
}
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);
}
}
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_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;
return true;
}
return false;
}