20 KiB
20 KiB
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.elf - Primary Toolchain: devkitPro /
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):
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.
Scene::Playing:- Driven by hardware Timer 0 executing
do_tick(). - Interrupt handler
handle_vblank()handles VBlank frame counting, OAM updates (oamUpdate(&oamSub)), and top-screen stat refreshes.
- Driven by hardware Timer 0 executing
3.3 Game Elements & Apples
- Snake Body: Represented as
std::deque<std::array<int, 3>>where each entry is{x, y, direction}. Alternating segments use regular and small sprite tiles. Head and tail sprites flip dynamically according to direction. - 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); flashes during its lastEVIL_ITEM_BLINK_TICKS(20 ticks, accelerating to rapid strobe during the final 8 ticks) to signal expiration. - 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 inhandle_vblank(), and its sprite sways/twitches with mystical instability indraw_apples_cursed(). Despawns afterCURSED_APPLE_LIFETIME_TICKS(90 ticks) with the same warning blink, 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 = 120frames / 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 = 240frames / 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_FRAMEStoBARRIER_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 toSPEED_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
DEVKITPROenvironment variable pointing to the devkitPro root (e.g./opt/devkitpro).devkitARMtoolchain 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:
# 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
# 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:
Run new build(Default Build Task, Ctrl+Shift+B): Configures, compiles, and runs inmelonDS.cmake build: Compiles the project.cmake clean: Cleans build artifacts.clang-tidy (all): Runsrun-clang-tidyacross all source and header files.clang-tidy (watch changed): Runs the background watchertools/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)
- 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 withoamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color). - Timer & IRQ: Hardware Timer 0 triggers
do_tickusingTIMER_FREQ_1024(). InterruptIRQ_VBLANKrunshandle_vblank().
6.2 Asset Pipeline
- Graphics Pipeline:
gfx/tilemap.png+gfx/tilemap.gritare processed bygritinto${CMAKE_CURRENT_BINARY_DIR}/tilemap.sandtilemap.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). - Console text HUD rendering in
draw_stats()is throttled via dirty-flag caching (HudCache), eliminating redundantiprintfcalls on VBlank.
- Audio Pipeline:
- Audio files in
sounds/are compiled bymmutilintosoundbank.binand headersoundbank.h. bin2sembedssoundbank.bininto assembly sourcesoundbank_bin.Sand headersoundbank_bin.h.- Audio runs via
libnds'smaxmod9(mm9).
- Audio files in
6.3 Background Pipeline (include/game_bg.h, 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).
- Main Engine: BG2 renders a dynamic cyber horizon, corner brackets, and starfield behind the top-screen console HUD and menu options (
- Smooth 60 FPS Hardware Scrolling: Both background layers use fixed-point 8.8 subpixel accumulators with
bgSetScroll()andbgUpdate()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. Enforces modern C++ guidelines, bugprone checks, readability conventions, and snake_case / CamelCase identifiers. - Clangd: Configured in
.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
- Embedded / Bare-Metal Constraints:
- The ARM9 CPU lacks an OS runtime. Avoid throwing exceptions or relying on RTTI (
-fno-exceptions -fno-rttiare compiler flags). - Prefer lightweight containers (
std::array,std::deque,std::vectorwith 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).
- The ARM9 CPU lacks an OS runtime. Avoid throwing exceptions or relying on RTTI (
- Interrupt & Concurrency Safety:
- Variables accessed across both the main loop, timer ticks, and VBlank IRQ (such as
ticks,frame,snakelength,snakeDirection,internals) must maintain appropriatevolatilequalifiers 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 likebad_apples,cursed_apples, andapplesmust pre-reserve capacity upfront on the main thread insetup()/init_game(). - Context objects in
.bss: Large structs such asInputContextmust be allocated in.bss(global or static) rather than onmain()'s DTCM stack frame to avoid stack-smash corruption if an interrupt handler pushes registers. - Safe state re-initialization: In
init_game(), calltimerStop(0)before resetting game state or clearing containers, and only calltimerStart(0, ...)at the very end when all state members and HUD caches are cleanly initialized.
- Zero dynamic allocations in ISRs: Never perform dynamic heap allocation (
- Variables accessed across both the main loop, timer ticks, and VBlank IRQ (such as
- 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.
- Graphic tiles and palettes must be transferred using DMA (
- Toolchain Flags:
- Keep
-mthumb-interwork,-march=armv5te,-mtune=arm946e-s, and specs-specs=${DEVKITPRO_ROOT}/calico/share/ds9.specsintact inCMakeLists.txt.
- Keep
- Code Style:
- Follow the naming and check options specified in
.clang-tidy(functions/variables insnake_case, classes/structs inCamelCase). - Run
cmake --build buildto verify clean compilation without warnings or errors.
- Follow the naming and check options specified in
- Commit Message Attribution:
- Every commit message generated by an AI agent must include a
Co-authored-bytrailer, separated from the subject/body by a blank line. Each agent should substitute its own name and contact address:Co-authored-by: <Agent Name> <agent-email-or-url> - 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: <Agent Name> <agent-email-or-url>
- Every commit message generated by an AI agent must include a