feat: prevent reversing directly into snake neck and play denied sound effect

This commit is contained in:
phillip committed 2026-10-01 22:11:00 +02:00
1 parent 60740a53c2
commit fd87ad3091
7 files changed
+86 -16

No files matched your search

+4 -2
View File
@@ -37,7 +37,7 @@ snakePlus/
├── source/ # C++ Implementation files
│ ├── main.cpp # Hardware setup, VBlank IRQ, timer ticks, game bootstrap
│ ├── game_audio.cpp # MaxMod soundbank loading, BGM loop, SFX dispatch
│ ├── game_input.cpp # Key mapping, inverted control logic, menu navigation
│ ├── game_input.cpp # Key mapping, inverted control logic, reverse-denial, menu navigation
│ ├── game_logic.cpp # Position update, edge wrapping, collision resolution, RNG
│ └── game_render.cpp # OAM sprite rendering, console text HUD & menu screens
│
@@ -51,6 +51,7 @@ snakePlus/
│ ├── 273792__pacomav__plop.wav # Movement plop SFX
│ ├── 332629__treasuresounds__item-pickup.wav # Apple pickup SFX
│ ├── Boom.wav # Rotten / Cursed apple boom SFX
│ ├── denied.wav # Reverse-direction denied thud SFX (synthesized)
│ ├── 167126__crisstanza__unpause.wav # Unpause SFX
│ ├── 167127__crisstanza__pause.wav # Pause SFX
│ └── 73750__timbre__remix-of-benboncan-sad-trombone-more-wah-bright-de-clicked.wav # Fail SFX
@@ -95,6 +96,7 @@ The game operates in one of two scenes ([`Scene`](include/game_state.h#L9-L12)):
- **Normal Apple (Red):** Spawns if count < 1. Awards +10 score, increases length by 1, plays pickup sound. Flashes/blinks every 4 ticks.
- **Rotten Apple (Brown/Green):** Spawns in Hard Mode (periodic chance up to 20 on screen). Decreases length by 1, plays explosion SFX. If length drops below 2, triggers Game Over.
- **Cursed Apple (Purple):** Spawns occasionally. Inverts player steering controls (Up ↔ Down, Left ↔ Right) for `CURSE_DURATION_TICKS` (40 ticks). The sprite is procedurally constructed at startup in [`main.cpp:91-112`](source/main.cpp#L91-L112) by copying the rotten apple tile pixels and mapping them to dedicated palette entries (`CURSE_PAL_SHADOW`, `CURSE_PAL_OUTLINE`, `CURSE_PAL_FILL`, `CURSE_PAL_GLOW`).
- **Reverse-Direction Denial:** Pressing the D-Pad in the exact opposite direction (i.e. into the snake's own neck) is silently denied instead of causing an immediate Game Over. The game computes the would-be next head position (including edge-wrapping) and compares it to the neck segment; if they match, the direction change is rejected and a subtle "denied" thud SFX plays.
### 3.4 Game Modes
- **Normal Mode:** Starts with length 4, timer frequency `TIMER_FREQ_1024(5)`, background color `RGB15(5,5,5)`.
@@ -108,7 +110,7 @@ The game operates in one of two scenes ([`Scene`](include/game_state.h#L9-L12)):
|---|---|---|
| **Menu** | D-Pad Up / Down | Change selected menu item |
| **Menu** | A / START | Select / Toggle option / Start game |
| **Playing** | D-Pad | Steer snake (inverted if cursed) |
| **Playing** | D-Pad | Steer snake (inverted if cursed; reversing into neck is denied with subtle SFX) |
| **Playing** | START | Pause / Unpause |
| **Playing** | R | Swap LCD displays (`lcdSwap()`) |
| **Playing** | SELECT | Toggle debug internals HUD overlay |
+2 -2
View File
@@ -40,12 +40,12 @@ if(ENABLE_CLANG_TIDY)
set(CMAKE_C_CLANG_TIDY
${CLANG_TIDY_EXE}
--header-filter=^(include|source)/
"--header-filter=^(include|source)/"
--extra-arg=--target=arm-none-eabi
)
set(CMAKE_CXX_CLANG_TIDY
${CLANG_TIDY_EXE}
--header-filter=^(include|source)/
"--header-filter=^(include|source)/"
--extra-arg=--target=arm-none-eabi
)
endif()
+2
View File
@@ -12,6 +12,7 @@ struct AudioContext {
mm_sound_effect sfx_unpause;
mm_sound_effect sfx_plop;
mm_sound_effect sfx_fail;
mm_sound_effect sfx_denied;
};
void audio_setup(AudioContext &audio);
@@ -26,6 +27,7 @@ void audio_play_plop(const AudioContext &audio);
void audio_play_pause(const AudioContext &audio);
void audio_play_unpause(const AudioContext &audio);
void audio_play_fail(const AudioContext &audio);
void audio_play_denied(const AudioContext &audio);
bool audio_toggle_music(AudioContext &audio);
bool audio_is_music_enabled(const AudioContext &audio);
BIN
View File
Binary file not shown.
+13
View File
@@ -14,6 +14,7 @@ void audio_setup(AudioContext &audio) {
mmLoadEffect(SFX_167126__CRISSTANZA__UNPAUSE);
mmLoadEffect(SFX_273792__PACOMAV__PLOP);
mmLoadEffect(SFX_73750__TIMBRE__REMIX_OF_BENBONCAN_SAD_TROMBONE_MORE_WAH_BRIGHT_DE_CLICKED);
mmLoadEffect(SFX_DENIED);
audio.sfx_pickup = {
{ SFX_332629__TREASURESOUNDS__ITEM_PICKUP },
@@ -62,6 +63,14 @@ void audio_setup(AudioContext &audio) {
255,
128,
};
audio.sfx_denied = {
{ SFX_DENIED },
static_cast<int>(1.0F * (1 << 10)),
5,
160,
128,
};
}
void audio_start_music(const AudioContext &audio) {
@@ -111,6 +120,10 @@ void audio_play_fail(const AudioContext &audio) {
mmEffectEx(&audio.sfx_fail);
}
void audio_play_denied(const AudioContext &audio) {
mmEffectEx(&audio.sfx_denied);
}
bool audio_toggle_music(AudioContext &audio) {
audio.musicEnabled = !audio.musicEnabled;
+63 -10
View File
@@ -4,6 +4,8 @@
#include <cstdint>
#include <cstdio>
#include "consts.h"
bool handle_input_frame(InputContext &ctx) {
scanKeys();
const uint32_t keys = keysDown();
@@ -69,27 +71,78 @@ bool handle_input_frame(InputContext &ctx) {
if (!ctx.state->paused && !ctx.state->gameOver) {
const bool cursed = ctx.state->curseTicksRemaining > 0;
uint16_t requested_dir = 0;
bool has_dir_input = false;
if ((keys != 0U) && ((keys & KEY_UP) != 0U)) {
ctx.state->snakeDirection = cursed ? 2 : 0;
draw_game(*ctx.state, *ctx.renderAssets);
requested_dir = cursed ? 2 : 0;
has_dir_input = true;
} else if ((keys != 0U) && ((keys & KEY_RIGHT) != 0U)) {
requested_dir = cursed ? 3 : 1;
has_dir_input = true;
} else if ((keys != 0U) && ((keys & KEY_DOWN) != 0U)) {
requested_dir = cursed ? 0 : 2;
has_dir_input = true;
} else if ((keys != 0U) && ((keys & KEY_LEFT) != 0U)) {
requested_dir = cursed ? 1 : 3;
has_dir_input = true;
}
if ((keys != 0U) && ((keys & KEY_RIGHT) != 0U)) {
ctx.state->snakeDirection = cursed ? 3 : 1;
draw_game(*ctx.state, *ctx.renderAssets);
if (has_dir_input) {
bool is_reversing = false;
if (ctx.state->snake.size() >= 2) {
const auto &head = ctx.state->snake.front();
const auto &neck = ctx.state->snake.at(1);
int next_x = head[0];
int next_y = head[1];
switch (requested_dir) {
case 0:
next_y--;
break;
case 1:
next_x++;
break;
case 2:
next_y++;
break;
case 3:
next_x--;
break;
default:
break;
}
if ((keys != 0U) && ((keys & KEY_DOWN) != 0U)) {
ctx.state->snakeDirection = cursed ? 0 : 2;
draw_game(*ctx.state, *ctx.renderAssets);
if (next_x > X_MAX) {
next_x = 0;
} else if (next_x < 0) {
next_x = X_MAX;
}
if ((keys != 0U) && ((keys & KEY_LEFT) != 0U)) {
ctx.state->snakeDirection = cursed ? 1 : 3;
if (next_y > Y_MAX) {
next_y = 0;
} else if (next_y < 0) {
next_y = Y_MAX;
}
if (next_x == neck[0] && next_y == neck[1]) {
is_reversing = true;
}
} else {
if ((ctx.state->snakeDirection + 2) % 4 == requested_dir) {
is_reversing = true;
}
}
if (is_reversing) {
audio_play_denied(*ctx.audio);
} else if (ctx.state->snakeDirection != requested_dir) {
ctx.state->snakeDirection = requested_dir;
draw_game(*ctx.state, *ctx.renderAssets);
}
}
}
if (ctx.state->paused) {
if ((keys != 0U) && ((keys & KEY_X) != 0U)) {
+2 -2
View File
@@ -175,7 +175,7 @@ void do_tick() {
void init_game() {
if (hardMode) {
timerStart(0, ClockDivider_1024, TIMER_FREQ_1024(9), do_tick);
timerStart(0, ClockDivider_1024, static_cast<uint16_t>(TIMER_FREQ_1024(9)), do_tick);
setBackdropColorSub(RGB15(10,5,5));
score = 0;
snakelength = START_LENGTH_HARD;
@@ -197,7 +197,7 @@ void init_game() {
}
else
{
timerStart(0, ClockDivider_1024, TIMER_FREQ_1024(5), do_tick);
timerStart(0, ClockDivider_1024, static_cast<uint16_t>(TIMER_FREQ_1024(5)), do_tick);
setBackdropColorSub(RGB15(5,5,5));
score = 0;
snakelength = START_LENGTH;