From fd87ad309135ecf82497825a5d86e7d4fe898b3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Phillip=20K=C3=BChne?= Date: Thu, 1 Oct 2026 22:11:00 +0200 Subject: [PATCH] feat: prevent reversing directly into snake neck and play denied sound effect --- AGENTS.md | 6 ++-- CMakeLists.txt | 4 +-- include/game_audio.h | 2 ++ sounds/denied.wav | Bin 0 -> 4276 bytes source/game_audio.cpp | 13 +++++++ source/game_input.cpp | 79 +++++++++++++++++++++++++++++++++++------- source/main.cpp | 4 +-- 7 files changed, 89 insertions(+), 19 deletions(-) create mode 100644 sounds/denied.wav 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 0000000000000000000000000000000000000000..e3f717c6e93f0f84678c2d3f437de79b60661e68 GIT binary patch literal 4276 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 literal 0 HcmV?d00001 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;