# AGENTS.md - Developer & Agent Guide for snakePlus This document provides a comprehensive overview of the **snakePlus** codebase for AI agents and developers. It details the project architecture, target platform, hardware constraints, build workflows, code organization, and development guidelines. --- ## 1. Project Overview - **Project Name:** Snake Plus (`snakePlus`) - **Subtitle:** *Snake with a twist* - **Author:** Phillip Kühne - **Platform:** Nintendo DS (NDS) Homebrew - **Target CPU:** ARM946E-S (`armv5te`, `-mtune=arm946e-s`, `-marm`, `-mthumb-interwork`) - **ROM Output:** [`snakePlus.nds`](snakePlus.nds) & [`snakePlus.elf`](snakePlus.elf) - **Primary Toolchain:** [devkitPro](https://devkitpro.org) / `devkitARM`, `libnds`, `maxmod9`, `grit`, `mmutil`, `bin2s`, `ndstool` - **Build System:** CMake (>= 3.13) with Ninja generator --- ## 2. Directory Structure & File Map ``` snakePlus/ ├── CMakeLists.txt # Primary CMake cross-compilation & asset build rules ├── compile_commands.json # Exported compilation database for clangd / clang-tidy ├── icon.bmp # 32x32 banner icon embedded into the .nds ROM ├── melonDS.ini # Local configuration for melonDS emulator │ ├── include/ # C++ Header declarations │ ├── consts.h # Grid dimensions, scores, tick rates, legacy bitmaps │ ├── game_audio.h # AudioContext struct, MaxMod sound & BGM declarations │ ├── game_bg.h # BackgroundContext struct, animated dual-screen BG declarations │ ├── game_input.h # InputContext struct, button scanning & menu logic │ ├── game_logic.h # Snake mechanics, collision detection, apple spawning │ ├── game_render.h # RenderAssets struct, dual-screen console/OAM draw routines │ └── game_state.h # Core GameState struct, Scene enum, coordinate containers │ ├── 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_bg.cpp # Dual-screen procedural tilemaps, scrolling & VBlank palette cycling │ ├── 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 │ ├── gfx/ # Graphical assets │ ├── tilemap.png # 8bpp tile sheet (snake head/body/tail, apples) │ ├── tilemap.grit # Grit conversion configuration (-gt, -gB8, -mRtf, etc.) │ └── tilemap.xcf # GIMP source image │ ├── sounds/ # Audio assets (built into soundbank by mmutil) │ ├── 0rigin.xm # FastTracker II background music track (MOD_0RIGIN) │ ├── 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 │ ├── tools/ # Tooling & automation scripts │ └── clang_tidy_watch.sh # File watcher script running clang-tidy on file change │ ├── .vscode/ # VS Code configuration │ ├── launch.json # GDB debugger launch for melonDS (port 3333) │ ├── settings.json # clangd and editor settings │ └── tasks.json # CMake build, run, clean, and clang-tidy tasks │ ├── .clang-tidy # Clang-tidy rules and naming conventions ├── .clangd # Clangd LSP configuration and system include paths └── .gitignore # Git ignore rules for build artifacts and caches ``` --- ## 3. Game Mechanics & State Flow ### 3.1 Grid & Screen Setup - The playfield is a **32 × 24 grid** (`X_MAX = 31`, `Y_MAX = 23`). - Each cell corresponds to an **8 × 8 pixel** sprite tile. - Moving outside the boundary wraps around to the opposing edge. - **Dual-Screen Layout:** - **Top Screen:** Console text display (Score, status effects like "CONTROLS INVERTED!", pause state, debug internals HUD). - **Bottom Screen:** Sub-engine 2D graphics (`oamSub`), rendering the snake and apples using 8x8 256-color sprites. - Can be swapped in real-time by pressing the **R** trigger (`lcdSwap()`). ### 3.2 Scenes & State Machine The game operates in one of two scenes ([`Scene`](include/game_state.h#L9-L12)): 1. **`Scene::Menu`**: - Menu options: **Start Game**, **Hard Mode: ON/OFF**, **Music: ON/OFF**. - Navigated via D-Pad Up/Down; toggled/started via **A** or **START**. 2. **`Scene::Playing`**: - Driven by hardware **Timer 0** executing [`do_tick()`](source/main.cpp#L127-L163). - Interrupt handler [`handle_vblank()`](source/main.cpp#L117-L125) handles VBlank frame counting, OAM updates (`oamUpdate(&oamSub)`), and top-screen stat refreshes. ### 3.3 Game Elements & Apples - **Snake Body:** Represented as `std::deque>` where each entry is `{x, y, direction}`. Alternating segments use regular and small sprite tiles. Head and tail sprites flip dynamically according to direction. - **Immediate Head Rotation Feedback:** When the player presses a D-Pad direction while moving, `handle_input_frame()` validates that the input is not a reverse into the neck. If valid, `state.snakeDirection` is immediately updated and `draw_game()` is called on the main thread before the next movement tick. In `draw_snake_head()`, the head sprite tile pointer (`gfx_head_v` vs `gfx_head_h`) and hardware flip flags (`hflip`, `vflip`) are selected directly from `state.snakeDirection` and written to shadow OAM (`oamSub`). On the subsequent 60 FPS VBlank (~16.6 ms), `handle_vblank()` commits shadow OAM to hardware registers via `oamUpdate(&oamSub)`, providing instantaneous visual feedback that the snake has turned to face the new heading before the slower Timer 0 tick (5.0–17.0 Hz) advances grid coordinates via `update_snake()`. - **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. Despawns after `BAD_APPLE_LIFETIME_TICKS` (80 ticks). - **Cursed Apple (Electric Neon Purple & Radioactive Green):** Spawns occasionally. Inverts player steering controls (Up ↔ Down, Left ↔ Right) for `CURSE_DURATION_TICKS` (40 ticks). Rendered using a dedicated 8x8 sprite with a glowing core rune and toxic green horn, using high-contrast dedicated palette entries (`CURSE_PAL_OUTLINE`, `CURSE_PAL_FILL`, `CURSE_PAL_GLOW`, `CURSE_PAL_CORE`, `CURSE_PAL_LEAF`). Its palette dynamically breathes and pulses with radioactive energy every frame at 60 FPS in [`handle_vblank()`](source/main.cpp#L134-L153), and its sprite sways/twitches with mystical instability in [`draw_apples_cursed()`](source/game_render.cpp#L139-L151). Despawns after `CURSED_APPLE_LIFETIME_TICKS` (90 ticks), clearing the way for new cursed apples to spawn if previously avoided. - **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 & Progressive Speed Mechanics - **Normal Mode:** Starts with length 4, base timer frequency 5.0 Hz (`SPEED_BASE_NORMAL_MHZ = 5000`), scaling up to 13.0 Hz (`SPEED_MAX_NORMAL_MHZ = 13000`). - **Hard Mode:** Starts with length 2, base timer frequency 5.0 Hz (`SPEED_BASE_HARD_MHZ = 5000`, matching Normal Mode's starting speed), scaling up to 17.0 Hz (`SPEED_MAX_HARD_MHZ = 17000`), spawns rotten apples, and spawns temporary fatal barriers. - **Temporary Fatal Barriers (Hard Mode):** Straight lines of blocks appear dynamically across the playfield without overlapping the snake or apples. Barriers only begin spawning after a few apples have been eaten (`BARRIER_START_APPLES = 3`). Their maximum possible length begins at 4 blocks (`BARRIER_BASE_MAX_LENGTH`) and gradually scales up to 10 blocks (`BARRIER_MAX_LENGTH`) later on in the game. At later stages of the game, multiple barriers can spawn simultaneously in a single spawn event (up to 2 at once starting at 16 apples, and up to 3 at once starting at 30 apples), synchronizing their warning and activation phases. Before becoming lethal, each barrier announces itself with a flashing warning reticle frame (`BARRIER_WARN_FRAMES = 120` frames / 2.0 s) whose pulse frequency accelerates and warning color intensifies from dim orange to incandescent yellow-white as solidification approaches. Once active, barriers turn into solid hazard-striped energy walls (`BARRIER_ACTIVE_FRAMES = 240` frames / 4.0 s) fatal to the snake's head on contact. As apples are eaten, spawn intervals accelerate from 9.0 s down to 2.0 s (`BARRIER_INTERVAL_MAX_FRAMES` to `BARRIER_INTERVAL_MIN_FRAMES`) and max concurrent barriers scale up from 1 to 4 (`BARRIER_MAX_COUNT`). - **Progressive Speed Scaling:** Each consumed regular apple increases `state.applesEaten`. Game speed scales linearly with consumed apples up to `SPEED_APPLES_CAP = 40`, granting smooth +200 mHz acceleration per apple (or +300 mHz in Hard Mode). Timer 0 reload registers are recomputed on the fly (`calculate_timer_reload()`). - **Progressive Background Intensity Scaling:** Visual effects intensify at the exact same rate (`calculate_intensity_31()`): - Hardware drift scroll speeds accelerate on both top and bottom screens. - Celestial node oscillation & twinkling breathing frequencies increase via fixed-point phase accumulation (`anim_phase_fp`). - Color palettes dynamically interpolate from calm cyber tones towards blazing electric plasma cyan (Normal) or volcanic hyper-crimson / solar white-gold (Hard). - Deep-space backdrop colors illuminate in tandem with the intensity level. - Apple pickup shockwave pulses flare with heightened luminance. --- ## 4. Input & Control Mapping | Context | Button | Action | |---|---|---| | **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; 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 | | **Playing** | START + L | Exit main loop / Quit | | **Paused** | B | Return to Main Menu | | **Paused** | X | Toggle background music on/off | | **Paused** | Y | Toggle Normal/Hard mode and restart | | **Game Over** | START | Restart game | | **Game Over** | B | Return to Main Menu | --- ## 5. Build System & Toolchain ### Prerequisites - `DEVKITPRO` environment variable pointing to the devkitPro root (e.g. `/opt/devkitpro`). - `devkitARM` toolchain with GCC cross-compiler (`arm-none-eabi-gcc`, `arm-none-eabi-g++`). - Tools on PATH or in `$DEVKITPRO/tools/bin`: `ndstool`, `grit`, `mmutil`, `bin2s`. - Build tools: `cmake` (>= 3.13), `ninja`. - Emulator: `melonDS`. ### Build Commands To configure and build from the command line: ```bash # Configure with Ninja generator cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_EXPORT_COMPILE_COMMANDS=ON # Copy compile_commands.json to root for clangd cmake -E copy_if_different build/compile_commands.json compile_commands.json # Build the NDS ROM image cmake --build build ``` ### Running & Emulation ```bash # Run using the custom CMake target (launches melonDS) cmake --build build --target run # Or launch directly with melonDS melonDS snakePlus.nds ``` ### VS Code Integration The repository includes preconfigured tasks in [`.vscode/tasks.json`](.vscode/tasks.json): - **`Run new build`** (Default Build Task, Ctrl+Shift+B): Configures, compiles, and runs in `melonDS`. - **`cmake build`**: Compiles the project. - **`cmake clean`**: Cleans build artifacts. - **`clang-tidy (all)`**: Runs `run-clang-tidy` across all source and header files. - **`clang-tidy (watch changed)`**: Runs the background watcher [`tools/clang_tidy_watch.sh`](tools/clang_tidy_watch.sh). - **`Copy new build to SD`**: Copies ROM to external SD card mount. --- ## 6. Architecture & Implementation Details ### 6.1 Hardware Subsystems ([`source/main.cpp`](source/main.cpp)) - **Video Modes:** Both Main and Sub 2D engines are initialized into `MODE_0_2D`. - **VRAM Allocation:** - `VRAM_A`: Assigned to Main Engine Background (`VRAM_A_MAIN_BG`). - `VRAM_C`: Assigned to Sub Engine Background (`VRAM_C_SUB_BG`). - `VRAM_D`: Assigned to Sub Engine Sprites (`VRAM_D_SUB_SPRITE`). - **Consoles:** Standard text consoles are attached to both screens for debug / stats / menus. - **OAM:** Sub-engine OAM is initialized via `oamInit(&oamSub, SpriteMapping_1D_32, false)`. Sprites are allocated with `oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color)`. - **Timer & IRQ:** Hardware Timer 0 triggers `do_tick` using `TIMER_FREQ_1024()`. Interrupt `IRQ_VBLANK` runs `handle_vblank()`. ### 6.2 Asset Pipeline - **Graphics Pipeline:** - [`gfx/tilemap.png`](gfx/tilemap.png) + [`gfx/tilemap.grit`](gfx/tilemap.grit) are processed by `grit` into `${CMAKE_CURRENT_BINARY_DIR}/tilemap.s` and `tilemap.h`. - Sprites are referenced by index: tile 1 (segment), 2 (small segment), 3 (head horizontal), 4 (head vertical), 5/6 (apple flash frames), 7/8/9/10 (tail variations), 11 (rotten apple). All 12 sprite tiles are preloaded once into VRAM at startup; directional and blinking variants are rendered via pointer selection and hardware flipping flags without mid-frame DMA transfers. - Active OAM sprites are clamped at hardware limit (128) and trailing unused sprites are hidden every frame via `oamClear(&oamSub, oamId, 128 - oamId)`. - **Decoupled Steering & Render Flow:** Steering inputs update `state.snakeDirection` and invoke `draw_game()` immediately on the main thread, updating the snake head's sprite pointer and flipping flags in shadow OAM (`oamSub`). Display updates are synchronized via VBlank (`handle_vblank()`), which flushes `oamSub` to hardware registers with `oamUpdate(&oamSub)` at 60 FPS (~16.6 ms) independently of the slower Timer 0 movement ticks (5.0–17.0 Hz). - Console text HUD rendering in `draw_stats()` is throttled via dirty-flag caching (`HudCache`), eliminating redundant `iprintf` calls on VBlank. - **Audio Pipeline:** - Audio files in [`sounds/`](sounds/) are compiled by `mmutil` into `soundbank.bin` and header `soundbank.h`. - `bin2s` embeds `soundbank.bin` into assembly source `soundbank_bin.S` and header `soundbank_bin.h`. - Audio runs via `libnds`'s `maxmod9` (`mm9`). ### 6.3 Background Pipeline ([`include/game_bg.h`](include/game_bg.h), [`source/game_bg.cpp`](source/game_bg.cpp)) - **Dual-Engine Tiled Layers:** Both Main and Sub 2D engines allocate Background Layer 2 (`BgType_Text4bpp`, `BgSize_T_256x256`, `mapBase = 28`, `tileBase = 1`). - **Main Engine:** BG2 renders a dynamic cyber horizon, corner brackets, and starfield behind the top-screen console HUD and menu options (`bgSetPriority(topScreen.bgId, 0)`, `bgSetPriority(bgMainId, 3)`). - **Sub Engine:** BG2 renders an animated retro cyber-grid with 8x8 cell alignment dots, ambient celestial nodes, and circuit pathways behind the snake and apple sprites (`bgSetPriority(bottomScreen.bgId, 1)`, `bgSetPriority(bgSubId, 3)`, sprites at priority 0). - **Smooth 60 FPS Hardware Scrolling:** Both background layers use fixed-point 8.8 subpixel accumulators with `bgSetScroll()` and `bgUpdate()` during VBlank for silky smooth drift at 0% CPU cost. Drift speed accelerates dynamically as apples are consumed. - **Dynamic VBlank Palette Animation & Progressive Intensity:** - **Normal Mode:** Neon cyan / teal / midnight blue theme with oscillating sine-wave twinkling stars and breathing grid nodes, transitioning smoothly towards blazing electric plasma cyan at higher apple counts. - **Hard Mode:** Fiery amber / crimson cyber grid with faster pulse rate, transitioning towards volcanic hyper-crimson and incandescent solar white-gold. - **Cursed Mode:** Radioactive violet / magenta glitch strobe and high-speed warp drift during inverted controls (further intensified with apple count). - **Apple Pickup Wave:** Eating an apple triggers a glowing shockwave pulse (`bg_trigger_pulse()`) that ripples through the grid with luminance scaled by current intensity. - **Deep-Space Backdrop Glow:** Screen backdrops illuminate in sync with intensity levels. - **Game Over:** Palette dims to a somber dark charcoal / muted red tone with gentle drift deceleration. ### 6.4 Static Analysis & Tooling - **Clang-Tidy:** Configured in [`.clang-tidy`](.clang-tidy). Enforces modern C++ guidelines, bugprone checks, readability conventions, and snake_case / CamelCase identifiers. - **Clangd:** Configured in [`.clangd`](.clangd). Removes `-mthumb-interwork` (which clang warns against) and explicitly includes devkitARM GCC 16.1.0 system headers. --- ## 7. Guidelines for AI Agents Modifying the Codebase 1. **Embedded / Bare-Metal Constraints:** - The ARM9 CPU lacks an OS runtime. Avoid throwing exceptions or relying on RTTI (`-fno-exceptions -fno-rtti` are compiler flags). - Prefer lightweight containers (`std::array`, `std::deque`, `std::vector` with controlled growth). - Be mindful of OAM hardware limitations (Sub engine supports up to 128 sprites; current game allocates sprite slots per apple and snake segment). 2. **Interrupt & Concurrency Safety:** - Variables accessed across both the main loop, timer ticks, and VBlank IRQ (such as `ticks`, `frame`, `snakelength`, `snakeDirection`, `internals`) must maintain appropriate `volatile` qualifiers or thread-safe synchronization. - **IRQ Stack Limit (256 bytes):** In `calico/lib/ds9.ld`, the ARM9 IRQ stack (`sp_irq`) has a total budget of only 256 bytes (`__sp_irq = __sp_usr + 0x100`), growing downwards directly into the user stack area. Therefore: - **Zero dynamic allocations in ISRs:** Never perform dynamic heap allocation (`malloc`, `operator new`, vector reallocation) inside hardware interrupt handlers (`do_tick`, `handle_vblank`). Dynamic vectors like `bad_apples`, `cursed_apples`, and `apples` must pre-reserve capacity upfront on the main thread in `setup()` / `init_game()`. - **Context objects in `.bss`:** Large structs such as `InputContext` must be allocated in `.bss` (global or static) rather than on `main()`'s DTCM stack frame to avoid stack-smash corruption if an interrupt handler pushes registers. - **Safe state re-initialization:** In `init_game()`, call `timerStop(0)` before resetting game state or clearing containers, and only call `timerStart(0, ...)` at the very end when all state members and HUD caches are cleanly initialized. 3. **Hardware Registers & Memory Banking:** - Graphic tiles and palettes must be transferred using DMA (`dmaCopy`) or direct memory copies into allocated VRAM / OAM memory banks. - Do not modify VRAM allocation configurations without ensuring compatibility with console text generators and OAM sub-engines. 4. **Toolchain Flags:** - Keep `-mthumb-interwork`, `-march=armv5te`, `-mtune=arm946e-s`, and specs `-specs=${DEVKITPRO_ROOT}/calico/share/ds9.specs` intact in [`CMakeLists.txt`](CMakeLists.txt). 5. **Code Style:** - Follow the naming and check options specified in [`.clang-tidy`](.clang-tidy) (functions/variables in `snake_case`, classes/structs in `CamelCase`). - Run `cmake --build build` to verify clean compilation without warnings or errors. 6. **Commit Message Attribution:** - Every commit message generated by an AI agent **must** include a `Co-authored-by` trailer, separated from the subject/body by a blank line. Each agent should substitute its own name and contact address: ``` Co-authored-by: ``` - Example commit message format: ``` feat: add cursed apple spawn rate tuning Expose CURSE_SPAWN_CHANCE constant in consts.h so it can be adjusted without touching game logic. Co-authored-by: ```