diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8def986 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,209 @@ +# 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 Kuehne +- **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_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_input.cpp # Key mapping, inverted control logic, 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 +│ ├── 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. +- **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`). + +### 3.4 Game Modes +- **Normal Mode:** Starts with length 4, timer frequency `TIMER_FREQ_1024(5)`, background color `RGB15(5,5,5)`. +- **Hard Mode:** Starts with length 2, timer frequency `TIMER_FREQ_1024(9)` (faster), background color `RGB15(10,5,5)`, spawns rotten apples. + +--- + +## 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) | +| **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), 5/6 (apple flash frames), 7/8/9/10 (tail variations), 11 (rotten apple). +- **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 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 15.2.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. +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5b1d2e2 --- /dev/null +++ b/README.md @@ -0,0 +1,135 @@ +# Snake Plus (Nintendo DS) + +> **Snake with a twist** — A homebrew Snake game for the Nintendo DS developed in modern C++ with `libnds` and `devkitPro`. + +--- + +## Features + +- **Classic Gameplay with a Twist:** + - Classic grid-based snake action wrapped on a 32×24 playfield with edge-wrapping. + - **Normal Apples:** Boost length and score (+10), with flashing animation and pickup sounds. + - **Rotten Apples:** Shrink snake length by 1; shrinking below length 2 results in Game Over! + - **Cursed Apples:** Invert steering controls (Up ↔ Down, Left ↔ Right) for 40 ticks, requiring quick reflexes and adapted steering. +- **Dual-Screen Layout:** + - **Top Screen:** Real-time scoreboard, status alerts (*"CONTROLS INVERTED!"*), pause overlay, and optional debug stats. + - **Bottom Screen:** Smooth 2D hardware sprite rendering using DS Sub-engine OAM. + - **LCD Swap:** Switch top and bottom screens on the fly with the **R** shoulder button. +- **Audio & Sound Effects:** + - Background tracker music ([`0rigin.xm`](sounds/0rigin.xm)) powered by MaxMod 9. + - Sound effects for movement, pickups, rotten/cursed explosions, pause, and game over. + - On-the-fly music toggle from the menu or pause screen. +- **Game Modes:** + - **Normal Mode:** Relaxed tick rate (`TIMER_FREQ_1024(5)`), starting length 4. + - **Hard Mode:** High-speed tick rate (`TIMER_FREQ_1024(9)`), starting length 2, and periodic rotten apple hazards. +- **Main Menu & Pause Screen:** + - Integrated start menu to toggle Hard Mode and Music. + - In-game pause menu with instant restarts and mode toggles. + +--- + +## Controls + +| Screen / Context | Button | Action | +|---|---|---| +| **Main Menu** | **D-Pad Up / Down** | Navigate menu options | +| **Main Menu** | **A** or **START** | Select option / Start game | +| **In-Game** | **D-Pad** | Steer snake (inverted while cursed) | +| **In-Game** | **START** | Pause / Resume | +| **In-Game** | **R Trigger** | Swap top and bottom screens (`lcdSwap`) | +| **In-Game** | **SELECT** | Toggle debug HUD (coordinates, ticks, raw keys) | +| **In-Game** | **START + L** | Exit game | +| **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 | + +--- + +## Building from Source + +### Prerequisites + +Ensure you have the following installed: +1. **[devkitPro](https://devkitpro.org)** with the `nds-dev` payload: + - `devkitARM` toolchain + - `libnds`, `calico`, `maxmod9` + - Utilities: `ndstool`, `grit`, `mmutil`, `bin2s` +2. **CMake** (>= 3.13) +3. **Ninja** build system +4. **[melonDS](https://melonds.kuribo64.net/)** (optional, for emulation and testing) + +Ensure the `DEVKITPRO` environment variable is set: +```bash +export DEVKITPRO=/opt/devkitpro +``` + +### Build Instructions + +```bash +# Clone the repository +git clone snakePlus +cd snakePlus + +# Configure build with Ninja +cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + +# Compile the .nds ROM and .elf binary +cmake --build build +``` + +This generates: +- [`snakePlus.nds`](snakePlus.nds) — Ready to run on an emulator or flashcard. +- [`snakePlus.elf`](snakePlus.elf) — ELF binary for GDB debugging. + +--- + +## Running & Emulation + +### Using melonDS via CMake target + +```bash +cmake --build build --target run +``` + +### Direct launch + +```bash +melonDS snakePlus.nds +``` + +### Installing to Flashcard / SD Card + +A convenience target is included in CMake to copy the `.nds` ROM to an SD card path: + +```bash +cmake --build build --target install_sd +``` +*(Configure `OUTPUT_DIRECTORY` in [`CMakeLists.txt`](CMakeLists.txt) as desired).* + +--- + +## Development & Code Quality + +- **VS Code Tasks:** Built-in tasks in [`.vscode/tasks.json`](.vscode/tasks.json) support one-key build, run in melonDS, and debug with GDB. +- **Clang-Tidy:** Run `run-clang-tidy` or launch the watch script: + ```bash + bash tools/clang_tidy_watch.sh + ``` +- **Language Standards:** C11 / C++14 targeting ARM9 (`armv5te`). + +--- + +## Architecture & Developer Documentation + +For an in-depth architectural breakdown, hardware register mapping, OAM details, and guidelines for AI agents or contributors, refer to [`AGENTS.md`](AGENTS.md). + +--- + +## Credits & Author + +- **Game Design & Code:** Phillip Kuehne +- **Music:** `0rigin.xm` +- **Sound Effects:** Freesound.org contributors (`treasuresounds`, `pacomav`, `crisstanza`, `timbre`, `benboncan`) +- **Platform Libraries:** `libnds`, `maxmod` by devkitPro team diff --git a/source/game_logic.cpp b/source/game_logic.cpp index 47a02cb..cbb4e1a 100644 --- a/source/game_logic.cpp +++ b/source/game_logic.cpp @@ -6,129 +6,137 @@ #include void update_snake(GameState &state) { - std::array headpos = state.snake.front(); - std::array newHead = headpos; + std::array headpos = state.snake.front(); + std::array newHead = headpos; - if (state.snakeDirection == 0) { - newHead = { headpos[0], headpos[1] - 1, state.snakeDirection }; - } else if (state.snakeDirection == 1) { - newHead = { headpos[0] + 1, headpos[1], state.snakeDirection }; - } else if (state.snakeDirection == 2) { - newHead = { headpos[0], headpos[1] + 1, state.snakeDirection }; - } else if (state.snakeDirection == 3) { - newHead = { headpos[0] - 1, headpos[1], state.snakeDirection }; - } + if (state.snakeDirection == 0) { + newHead = {headpos[0], headpos[1] - 1, state.snakeDirection}; + } else if (state.snakeDirection == 1) { + newHead = {headpos[0] + 1, headpos[1], state.snakeDirection}; + } else if (state.snakeDirection == 2) { + newHead = {headpos[0], headpos[1] + 1, state.snakeDirection}; + } else if (state.snakeDirection == 3) { + newHead = {headpos[0] - 1, headpos[1], state.snakeDirection}; + } - if (newHead[0] > X_MAX) { - newHead[0] = 0; - } - if (newHead[1] > Y_MAX) { - newHead[1] = 0; - } - if (newHead[0] < 0) { - newHead[0] = X_MAX; - } - if (newHead[1] < 0) { - newHead[1] = Y_MAX; - } + if (newHead[0] > X_MAX) { + newHead[0] = 0; + } + if (newHead[1] > Y_MAX) { + newHead[1] = 0; + } + if (newHead[0] < 0) { + newHead[0] = X_MAX; + } + if (newHead[1] < 0) { + newHead[1] = Y_MAX; + } - state.snake.push_front(newHead); - state.snake.resize(state.snakelength); + state.snake.push_front(newHead); + state.snake.resize(state.snakelength); } bool check_collision_self(GameState &state) { - if (state.snake.size() < 2 || state.gameOver) { - return false; - } - - const std::array headpos = state.snake.front(); - - for (auto it = state.snake.begin() + 1; it != state.snake.end(); ++it) { - if ((*it)[0] == headpos[0] && (*it)[1] == headpos[1]) { - state.gameOver = true; - return true; - } - } - + if (state.snake.size() < 2 || state.gameOver) { return false; + } + + const std::array headpos = state.snake.front(); + + for (auto it = state.snake.begin() + 1; it != state.snake.end(); ++it) { + if ((*it)[0] == headpos[0] && (*it)[1] == headpos[1]) { + state.gameOver = true; + return true; + } + } + + return false; } bool check_collision_apples(GameState &state) { - const std::array headpos = state.snake.front(); + const std::array headpos = state.snake.front(); - auto it = std::find_if(state.apples.begin(), state.apples.end(), [&](const std::array &apple) { - return apple[0] == headpos[0] && apple[1] == headpos[1]; - }); + auto it = + std::find_if(state.apples.begin(), state.apples.end(), + [&](const std::array &apple) { + return apple[0] == headpos[0] && apple[1] == headpos[1]; + }); - if (it == state.apples.end()) { - return false; - } + if (it == state.apples.end()) { + return false; + } - state.score += APPLE_SCORE; - state.snakelength++; - state.apples.erase(it); - return true; + state.score += APPLE_SCORE; + state.snakelength++; + state.apples.erase(it); + return true; } bool check_collision_apples_rotten(GameState &state) { - const std::array headpos = state.snake.front(); + const std::array headpos = state.snake.front(); - auto it = std::find_if(state.bad_apples.begin(), state.bad_apples.end(), [&](const std::array &apple) { - return apple[0] == headpos[0] && apple[1] == headpos[1]; - }); + auto it = + std::find_if(state.bad_apples.begin(), state.bad_apples.end(), + [&](const std::array &apple) { + return apple[0] == headpos[0] && apple[1] == headpos[1]; + }); - if (it == state.bad_apples.end()) { - return false; - } + if (it == state.bad_apples.end()) { + return false; + } - state.snakelength--; - state.bad_apples.erase(it); - if (state.snakelength < 2) { - state.gameOver = true; - } - return true; + state.snakelength--; + state.bad_apples.erase(it); + if (state.snakelength < 2) { + state.gameOver = true; + } + return true; } bool check_collision_apples_cursed(GameState &state) { - const std::array headpos = state.snake.front(); + const std::array headpos = state.snake.front(); - auto it = std::find_if(state.cursed_apples.begin(), state.cursed_apples.end(), - [&](const std::array &apple) { - return apple[0] == headpos[0] && apple[1] == headpos[1]; - }); + auto it = + std::find_if(state.cursed_apples.begin(), state.cursed_apples.end(), + [&](const std::array &apple) { + return apple[0] == headpos[0] && apple[1] == headpos[1]; + }); - if (it == state.cursed_apples.end()) { - return false; - } + if (it == state.cursed_apples.end()) { + return false; + } - state.cursed_apples.erase(it); - state.curseTicksRemaining = CURSE_DURATION_TICKS; - return true; + state.cursed_apples.erase(it); + state.curseTicksRemaining = CURSE_DURATION_TICKS; + return true; } void spawn_apples(GameState &state) { - if (state.apples.size() < 1) { - std::array apple = { rand() % (X_MAX + 1), rand() % (Y_MAX + 1) }; - state.apples.push_back(apple); - } + if (state.apples.size() < 1) { + std::array apple = {rand() % (X_MAX + 1), rand() % (Y_MAX + 1)}; + state.apples.push_back(apple); + } } void spawn_apples_rotten(GameState &state) { - if ((state.ticks % 10 == 0) && state.hardMode && ((rand() % 10) < 10) && (state.bad_apples.size() < 20)) { - std::array badApple = { rand() % (X_MAX + 1), rand() % (Y_MAX + 1) }; - state.bad_apples.push_back(badApple); - } + if ((state.ticks % 10 == 0) && state.hardMode && ((rand() % 10) < 10) && + (state.bad_apples.size() < 20)) { + std::array badApple = {rand() % (X_MAX + 1), rand() % (Y_MAX + 1)}; + state.bad_apples.push_back(badApple); + } } void spawn_apples_cursed(GameState &state) { - if (state.cursed_apples.empty() && (state.ticks % 20 == 0) && ((rand() % 5) == 0)) { - std::array cursedApple = { rand() % (X_MAX + 1), rand() % (Y_MAX + 1) }; - state.cursed_apples.push_back(cursedApple); - } + if (state.cursed_apples.empty() && (state.ticks % 20 == 0) && + ((rand() % 5) == 0)) { + std::array cursedApple = {rand() % (X_MAX + 1), + rand() % (Y_MAX + 1)}; + state.cursed_apples.push_back(cursedApple); + } } void tick_status_effects(GameState &state) { - if (state.curseTicksRemaining > 0) { - state.curseTicksRemaining--; - } + if (state.curseTicksRemaining > 0) { + state.curseTicksRemaining--; + } }