feat: add temporary hazardous barriers in hard mode
This commit is contained in:
1 parent
08a4b41280
commit
9a3a0bd9fd
8 files changed
+466
-17
No files matched your search
+249
-1
@@ -114,8 +114,14 @@ bool check_collision_apples_cursed(GameState &state) {
|
||||
}
|
||||
|
||||
void spawn_apples(GameState &state) {
|
||||
if (state.apples.size() < 1) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -172,3 +178,245 @@ uint16_t calculate_timer_reload(const GameState &state) {
|
||||
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;
|
||||
}
|
||||
Reference in new issue
Block a user