diff --git a/AGENTS.md b/AGENTS.md index 518ebb7..c7d217a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 | diff --git a/CMakeLists.txt b/CMakeLists.txt index 807014a..c174f89 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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() diff --git a/include/game_audio.h b/include/game_audio.h index 537419f..f2bb7e0 100644 --- a/include/game_audio.h +++ b/include/game_audio.h @@ -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); diff --git a/sounds/denied.wav b/sounds/denied.wav new file mode 100644 index 0000000..e3f717c Binary files /dev/null and b/sounds/denied.wav differ diff --git a/source/game_audio.cpp b/source/game_audio.cpp index 5e88118..51ae7e6 100644 --- a/source/game_audio.cpp +++ b/source/game_audio.cpp @@ -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(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; diff --git a/source/game_input.cpp b/source/game_input.cpp index b679ff4..778bbc3 100644 --- a/source/game_input.cpp +++ b/source/game_input.cpp @@ -4,6 +4,8 @@ #include #include +#include "consts.h" + bool handle_input_frame(InputContext &ctx) { scanKeys(); const uint32_t keys = keysDown(); @@ -69,25 +71,76 @@ 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 ((keys != 0U) && ((keys & KEY_DOWN) != 0U)) { - ctx.state->snakeDirection = cursed ? 0 : 2; - draw_game(*ctx.state, *ctx.renderAssets); - } + 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]; - if ((keys != 0U) && ((keys & KEY_LEFT) != 0U)) { - ctx.state->snakeDirection = cursed ? 1 : 3; - draw_game(*ctx.state, *ctx.renderAssets); + 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 (next_x > X_MAX) { + next_x = 0; + } else if (next_x < 0) { + next_x = X_MAX; + } + + 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); + } } } diff --git a/source/main.cpp b/source/main.cpp index a8298dd..5ad0718 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -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(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(TIMER_FREQ_1024(5)), do_tick); setBackdropColorSub(RGB15(5,5,5)); score = 0; snakelength = START_LENGTH;