Files
snakePlus/AGENTS.md
T
phillip d30c0d0330 fix: resolve CodeChecker findings and add CodeChecker ignore rules
- Refactor cause in draw_game_over_screen to an immediately invoked lambda, eliminating dead store warning
- Rename sound effect files to standard-compliant names without double underscores to prevent reserved macro warnings in soundbank.h
- Update source/game_audio.cpp to use clean SFX_* macro identifiers
- Add codechecker_skip.txt to ignore external devkitPro toolchain headers
- Ignore .codechecker/ output artifacts directory in .gitignore
- Update AGENTS.md file tree and sound asset references
2026-10-04 01:51:10 +02:00

25 KiB
Raw Blame History

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)
│   ├── Boom.wav                # Rotten / Cursed apple boom SFX (SFX_BOOM)
│   ├── denied.wav              # Reverse-direction denied thud SFX (SFX_DENIED)
│   ├── pause.wav               # Pause SFX (SFX_PAUSE)
│   ├── pickup.wav              # Apple pickup SFX (SFX_PICKUP)
│   ├── plop.wav                # Movement plop SFX (SFX_PLOP)
│   ├── sad_trombone.wav        # Fail SFX (SFX_SAD_TROMBONE)
│   └── unpause.wav             # Unpause SFX (SFX_UNPAUSE)
│
├── tools/                      # Tooling & automation scripts
│   └── clang_tidy_watch.sh     # File watcher script running clang-tidy on file change
│
├── .agents/                    # Workspace agent customizations
│   └── skills/
│       └── melonds-gdb-debugger/ # GDB debugging runbook & diagnostic script
│
├── .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
└── codechecker_skip.txt        # Skipfile for CodeChecker to omit external devkitPro headers

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):

  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().
    • Interrupt handler handle_vblank() handles VBlank frame counting (frame++), while the main thread synchronizes via swiWaitForVBlank() to execute update_frame() (OAM updates via oamUpdate(&oamSub), palette animations, background scrolling, barrier ticking, and top-screen stat refreshes) and handle_input_frame() at 60 FPS without exceeding the 256-byte IRQ stack limit.

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.
  • 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), update_frame() 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 (Bruised Amber & Toxic Green Mold): 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. Rendered using a dedicated handcrafted 8x8 sprite with bruised golden-amber decaying skin, sunken dark necrotic rot cavities, a withered dead twig stem, and festering green mold colonies with radiant toxic spore pustules. Uses high-contrast dedicated palette entries (ROTTEN_PAL_OUTLINE, ROTTEN_PAL_SKIN, ROTTEN_PAL_DECAY, ROTTEN_PAL_MOLD, ROTTEN_PAL_SPORE, ROTTEN_PAL_STEM) specifically tuned for high visibility and contrast against the Hard Mode crimson background. Its mold and spore pustules organically breathe and fester with toxic bioluminescence every frame at 60 FPS in update_frame() (source/main.cpp), and sprites alternate horizontal orientation based on grid coordinates (hflip) in draw_apples_rotten() for natural visual variety across the playfield. 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 update_frame() (source/main.cpp), and its sprite sways/twitches with mystical instability in draw_apples_cursed(). 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 A / START Restart game
Game Over B Return to Main Menu
Game Over R Swap LCD displays (lcdSwap())

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:

# 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 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.
  • 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 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.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). Special entities (Rotten Apple, Cursed Apple, Barrier solid, and Barrier warning reticle) use dedicated handcrafted 8x8 256-color tile data copied directly into VRAM at startup. All sprites are preloaded once into VRAM; directional, orientation, 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). The main thread synchronizes via swiWaitForVBlank() and flushes oamSub to hardware registers with oamUpdate(&oamSub) at 60 FPS (~16.6 ms) inside update_frame(), 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().
    • Console text HUD rendering in draw_stats() and draw_game_over_screen() is throttled via dirty-flag caching (HudCache), eliminating redundant iprintf calls on VBlank.
  • Audio Pipeline:
    • Audio files in 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, 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 update_frame() for silky smooth drift at 0% CPU cost. Drift speed accelerates dynamically as apples are consumed.
  • Dynamic 60 FPS 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: Playfield sprites (snake, obstacles, apples) remain fully rendered in their exact final state so the player can see where the collision happened. The background smoothly decelerates and dims into a somber dark charcoal / muted red tone over ~0.5s (game_over_fade), while the top screen console presents a dedicated Game Over summary detailing the final score, apples eaten, length, difficulty, and cause of defeat.

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

  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:
      • Minimal VBlank ISR: handle_vblank() must strictly do lightweight bookkeeping (frame++). All heavy lifting (bg_update, draw_stats, iprintf, oamUpdate, barrier frame ticking, dynamic palette math) must run on the main thread in update_frame() synchronized with swiWaitForVBlank(), utilizing the 16 KB DTCM user stack instead of overflowing the 256-byte IRQ stack.
      • 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 and RenderAssets should be allocated in .bss (global or static) rather than on main()'s DTCM stack frame to conserve stack headroom and prevent stack-smash corruption.
      • Console Re-entrancy: consoleSelect() and iprintf() are non-reentrant. Libnds relies on a global currentConsole pointer and internal formatting state. Never call consoleSelect or iprintf from an interrupt handler while the main thread also prints to the console. Centralize all console rendering on the main thread inside update_frame() or draw_menu().
      • HUD Dirty-Flag Caching (HudCache): Running ANSI terminal formatting and iprintf on every frame burns unnecessary cycles and causes text flicker. Use dirty flags (HudCache) to redraw console elements only when values change (score, apples, paused status, game over state).
      • Input Scanning Frequency & Bus Contention: scanKeys() must be called once per frame synchronized with swiWaitForVBlank(). Calling scanKeys() in an unthrottled loop (spinning at 66 MHz) saturates ARM7 IPC queues (touchRead, keypadGetState), resets key-debounce edge detectors within microseconds, and leads to dropped inputs.
      • Boot Subsystem Sequencing: Always complete hardware configuration, console initialization, and data structure setup (setup()) before enabling the VBlank interrupt (irqSet(IRQ_VBLANK, handle_vblank)). Enabling the ISR beforehand causes VBlank interrupts to fire while consoleLoadFont is streaming font tiles to VRAM, triggering Data Aborts at startup.
      • 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. Debugging with GDB & melonDS:
    • melonDS exposes a GDB stub on localhost:3333.
    • For step-by-step instructions, exception vector addresses (__arm_excpt_dabt, __excpt_entry), stack-smash diagnosis, and automated diagnostic scripts, refer to the melonds-gdb-debugger skill.
  4. 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.
  5. Toolchain Flags:
    • Keep -mthumb-interwork, -march=armv5te, -mtune=arm946e-s, and specs -specs=${DEVKITPRO_ROOT}/calico/share/ds9.specs intact in CMakeLists.txt.
  6. Code Style:
    • Follow the naming and check options specified in .clang-tidy (functions/variables in snake_case, classes/structs in CamelCase).
    • Run cmake --build build to verify clean compilation without warnings or errors.
  7. 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: <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>