#include #include #include #include #include #include #include #include #include #include #include #include "tilemap.h" using namespace std; volatile uint8_t frame = 0; volatile bool internals = false; GameState gameState; volatile uint16_t &ticks = gameState.ticks; volatile uint16_t &snakelength = gameState.snakelength; /*** * 0 = up * 1 = right * 2 = down * 3 = left ***/ volatile uint16_t &snakeDirection = gameState.snakeDirection; volatile uint16_t &score = gameState.score; volatile uint16_t &applesEaten = gameState.applesEaten; bool &gameOver = gameState.gameOver; bool &paused = gameState.paused; bool &hardMode = gameState.hardMode; std::deque> &snake = gameState.snake; std::vector> &apples = gameState.apples; std::vector &bad_apples = gameState.bad_apples; // 31x23 PrintConsole topScreen; PrintConsole bottomScreen; int sound; AudioContext audioContext; RenderAssets renderAssets; BackgroundContext bgContext; InputContext inputContext; const void *get_sprite_pointer(uint16_t spriteIndex) { return &tilemapTiles[spriteIndex * 16]; } namespace { // 8x8 256-color tile data for Cursed Apple (vibrant electric purple apple with // radioactive green horn & glowing core) alignas(4) constexpr std::array cursed_apple_tile = { 0x10000000, 0x00001414, 0x10101000, 0x00101014, 0x12111110, 0x10111112, 0x13121110, 0x10111112, 0x13131110, 0x10111112, 0x13121110, 0x10111211, 0x11111000, 0x00101111, 0x10100000, 0x00101000, }; // 8x8 256-color tile data for Rotten Apple (bruised amber decaying apple with // dark sunken rot & toxic green mold) alignas(4) constexpr std::array rotten_apple_tile = { 0x1E000000, 0x0000001E, 0x19191900, 0x0019191E, 0x1B1A1A19, 0x191A1A1B, 0x1D1B1A19, 0x191A1A1C, 0x1D1C1A19, 0x191A1C1D, 0x1C1A1A19, 0x191A1B1C, 0x1A1A1900, 0x00191A1A, 0x19190000, 0x00001919, }; // 8x8 256-color tile data for solid fatal barrier (bordered hazard stripe // block) alignas(4) constexpr std::array barrier_solid_tile = { 0x15151515, 0x15151515, 0x16171715, 0x15171716, 0x16161715, 0x15161717, 0x17161615, 0x15161617, 0x17171615, 0x15171616, 0x16171715, 0x15171716, 0x16161715, 0x15161717, 0x15151515, 0x15151515, }; // 8x8 256-color tile data for barrier warning announcement (pulsing reticle // frame) alignas(4) constexpr std::array barrier_warn_tile = { 0x00001818, 0x18180000, 0x18000018, 0x18000018, 0x18180000, 0x00001818, 0x00181800, 0x00181800, 0x00181800, 0x00181800, 0x18180000, 0x00001818, 0x18000018, 0x18000018, 0x00001818, 0x18180000, }; } // namespace void setup() { // Set up screens videoSetMode(MODE_0_2D); videoSetModeSub(MODE_0_2D); vramSetBankA(VRAM_A_MAIN_BG); vramSetBankC(VRAM_C_SUB_BG); vramSetBankD(VRAM_D_SUB_SPRITE); consoleInit(&topScreen, 3, BgType_Text4bpp, BgSize_T_256x256, X_MAX, 0, true, true); consoleInit(&bottomScreen, 3, BgType_Text4bpp, BgSize_T_256x256, X_MAX, 0, false, true); oamInit(&oamSub, SpriteMapping_1D_32, false); bg_setup(bgContext, topScreen.bgId, bottomScreen.bgId); renderAssets.topScreen = &topScreen; renderAssets.bottomScreen = &bottomScreen; renderAssets.bg_context = &bgContext; renderAssets.gfx_segment = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color); renderAssets.gfx_segment_small = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color); renderAssets.gfx_head_h = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color); renderAssets.gfx_head_v = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color); renderAssets.gfx_apple = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color); renderAssets.gfx_apple_blink = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color); renderAssets.gfx_tail_h[0] = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color); renderAssets.gfx_tail_h[1] = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color); renderAssets.gfx_tail_v[0] = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color); renderAssets.gfx_tail_v[1] = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color); renderAssets.gfx_apple_rotten = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color); renderAssets.gfx_apple_cursed = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color); renderAssets.gfx_barrier = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color); renderAssets.gfx_barrier_warn = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color); dmaCopy(get_sprite_pointer(1), renderAssets.gfx_segment, 64); dmaCopy(get_sprite_pointer(2), renderAssets.gfx_segment_small, 64); dmaCopy(get_sprite_pointer(3), renderAssets.gfx_head_h, 64); dmaCopy(get_sprite_pointer(4), renderAssets.gfx_head_v, 64); dmaCopy(get_sprite_pointer(5), renderAssets.gfx_apple, 64); dmaCopy(get_sprite_pointer(6), renderAssets.gfx_apple_blink, 64); dmaCopy(get_sprite_pointer(7), renderAssets.gfx_tail_h[0], 64); dmaCopy(get_sprite_pointer(8), renderAssets.gfx_tail_h[1], 64); dmaCopy(get_sprite_pointer(9), renderAssets.gfx_tail_v[0], 64); dmaCopy(get_sprite_pointer(10), renderAssets.gfx_tail_v[1], 64); consoleSelect(&topScreen); dmaCopy(tilemapPal, SPRITE_PALETTE_SUB, tilemapPalLen); // Setup vibrant base colors for the Cursed Apple sprite (neon electric purple // with toxic green horn) SPRITE_PALETTE_SUB[CURSE_PAL_OUTLINE] = RGB15(5, 0, 12); // Deep mystical dark purple outline SPRITE_PALETTE_SUB[CURSE_PAL_FILL] = RGB15(22, 2, 28); // Vibrant Electric Purple SPRITE_PALETTE_SUB[CURSE_PAL_GLOW] = RGB15(29, 6, 31); // Radiant Neon Magenta Glow SPRITE_PALETTE_SUB[CURSE_PAL_CORE] = RGB15(31, 26, 31); // Glowing White-Pink Core SPRITE_PALETTE_SUB[CURSE_PAL_LEAF] = RGB15(2, 31, 10); // Toxic Radioactive Neon Green // Setup initial colors for the hard-mode temporary fatal barriers SPRITE_PALETTE_SUB[BARRIER_PAL_EDGE] = RGB15(12, 0, 3); SPRITE_PALETTE_SUB[BARRIER_PAL_FILL] = RGB15(22, 2, 4); SPRITE_PALETTE_SUB[BARRIER_PAL_STRIPE] = RGB15(31, 22, 6); SPRITE_PALETTE_SUB[BARRIER_PAL_WARN] = RGB15(18, 4, 2); // Setup initial colors for the Rotten Apple sprite (bruised amber decaying // skin with dark rot & glowing green mold) SPRITE_PALETTE_SUB[ROTTEN_PAL_OUTLINE] = RGB15(2, 2, 1); // Crisp dark outline SPRITE_PALETTE_SUB[ROTTEN_PAL_SKIN] = RGB15( 25, 17, 4); // Bruised golden-amber decaying skin (high contrast against crimson) SPRITE_PALETTE_SUB[ROTTEN_PAL_DECAY] = RGB15(5, 3, 1); // Sunken dark necrotic rot cavity SPRITE_PALETTE_SUB[ROTTEN_PAL_MOLD] = RGB15(12, 24, 4); // Vivid toxic fungal green mold SPRITE_PALETTE_SUB[ROTTEN_PAL_SPORE] = RGB15(22, 31, 10); // Radiant glowing spore pustule SPRITE_PALETTE_SUB[ROTTEN_PAL_STEM] = RGB15(13, 8, 3); // Withered woody twig stem dmaCopy(cursed_apple_tile.data(), renderAssets.gfx_apple_cursed, 64); dmaCopy(rotten_apple_tile.data(), renderAssets.gfx_apple_rotten, 64); dmaCopy(barrier_solid_tile.data(), renderAssets.gfx_barrier, 64); dmaCopy(barrier_warn_tile.data(), renderAssets.gfx_barrier_warn, 64); // Pre-reserve capacities to eliminate dynamic heap allocations inside Timer 0 // ISR (do_tick) gameState.bad_apples.reserve(32); gameState.cursed_apples.reserve(4); gameState.apples.reserve(4); audio_setup(audioContext); } void handle_vblank() { frame++; } void update_frame() { bg_update(bgContext, gameState, frame); if (gameState.scene == Scene::Playing) { consoleSelect(&topScreen); if (gameState.gameOver) { HudCache &cache = renderAssets.hudCache; if (cache.dirty || (cache.gameOver != gameState.gameOver)) { consoleClear(); draw_game_over_screen(gameState); cache.gameOver = gameState.gameOver; cache.dirty = false; } } else { draw_stats(gameState, renderAssets, internals, audio_is_music_enabled(audioContext), frame); if (!gameState.paused) { tick_barriers_frame(gameState); } } draw_barriers(gameState, renderAssets, frame); } // Dynamic pulsing radioactive glow for Cursed Apples (smooth 60fps breathing) const auto pulse = static_cast( (static_cast(frame) >> 2U) & 7U); // 8-step cycle const uint8_t wave = (pulse < 4U) ? pulse : static_cast(7U - pulse); // 0, 1, 2, 3, 3, 2, 1, 0 SPRITE_PALETTE_SUB[CURSE_PAL_FILL] = static_cast(RGB15(20 + (wave * 3), 2 + wave, 26 + wave)); SPRITE_PALETTE_SUB[CURSE_PAL_GLOW] = static_cast(RGB15(26 + wave, 6 + (wave * 4), 31)); SPRITE_PALETTE_SUB[CURSE_PAL_CORE] = (wave >= 2U) ? static_cast(RGB15(31, 31, 31)) : static_cast(RGB15(31, 22 + (wave * 3), 31)); SPRITE_PALETTE_SUB[CURSE_PAL_LEAF] = static_cast(RGB15(2 + (wave * 2), 31, 10 + (wave * 3))); // Dynamic festering mold & spore respiration for Rotten Apples (smooth 60fps // organic pulse) const auto rot_pulse = static_cast( (static_cast(frame + 3U) >> 2U) & 7U); // 8-step cycle, phase-shifted from cursed apple const uint8_t rot_wave = (rot_pulse < 4U) ? rot_pulse : static_cast(7U - rot_pulse); // 0, 1, 2, 3, 3, 2, 1, 0 SPRITE_PALETTE_SUB[ROTTEN_PAL_DECAY] = static_cast(RGB15(5 + rot_wave, 3 + rot_wave, 1)); SPRITE_PALETTE_SUB[ROTTEN_PAL_MOLD] = static_cast( RGB15(10 + (rot_wave * 2), 22 + (rot_wave * 2), 3 + rot_wave)); SPRITE_PALETTE_SUB[ROTTEN_PAL_SPORE] = (rot_wave >= 2U) ? static_cast(RGB15(26, 31, 14)) : static_cast( RGB15(20 + (rot_wave * 2), 28 + rot_wave, 8 + (rot_wave * 2))); oamUpdate(&oamSub); // flashy_colors(); } void do_tick() { if (gameState.scene == Scene::Playing && !paused) { ticks++; if (!gameOver) { const bool was_game_over = gameOver; /*if (hardMode) { timerStop(0); timerStart(0, ClockDivider_1024, TIMER_FREQ_1024(ticks+5), do_tick); }*/ update_snake(gameState); audio_play_plop(audioContext); check_collision_self(gameState); if (check_collision_barriers(gameState)) { audio_play_boom(audioContext); } if (check_collision_apples(gameState)) { audio_play_pickup(audioContext); bg_trigger_pulse(bgContext); timerStart(0, ClockDivider_1024, calculate_timer_reload(gameState), do_tick); } if (check_collision_apples_rotten(gameState)) { audio_play_boom(audioContext); } if (check_collision_apples_cursed(gameState)) { audio_play_boom(audioContext); } tick_evil_items(gameState); spawn_apples(gameState); spawn_apples_rotten(gameState); spawn_apples_cursed(gameState); spawn_barriers(gameState); tick_status_effects(gameState); if (!was_game_over && gameOver) { audio_stop_music(); audio_play_fail(audioContext); invalidate_hud(renderAssets); } // consoleSelect(&bottomScreen); draw_game(gameState, renderAssets); } } } void init_game() { timerStop(0); applesEaten = 0; reset_barriers(gameState); gameState.bad_apples.reserve(32); gameState.cursed_apples.reserve(4); gameState.apples.reserve(4); score = 0; ticks = 0; gameOver = false; gameState.gameOverReason = GameOverReason::None; snakeDirection = 1; gameState.curseTicksRemaining = 0; apples.clear(); bad_apples.clear(); gameState.cursed_apples.clear(); if (hardMode) { setBackdropColorSub(RGB15(3, 1, 1)); setBackdropColor(RGB15(2, 1, 1)); snakelength = START_LENGTH_HARD; snake = {{2, 1, 1}, {1, 1, 1}}; } else { setBackdropColorSub(RGB15(3, 4, 7)); setBackdropColor(RGB15(2, 3, 6)); snakelength = START_LENGTH; snake = {{4, 1, 1}, {3, 1, 1}, {2, 1, 1}, {1, 1, 1}}; } consoleSelect(&topScreen); consoleClear(); invalidate_hud(renderAssets); consoleSelect(&bottomScreen); consoleClear(); oamClear(&oamSub, 0, 128); audio_start_music(audioContext); timerStart(0, ClockDivider_1024, calculate_timer_reload(gameState), do_tick); } int main(void) { setup(); setBackdropColor(RGB15(2, 2, 6)); setBackdropColorSub(RGB15(2, 2, 6)); draw_menu(gameState, renderAssets, audio_is_music_enabled(audioContext)); inputContext.state = &gameState; inputContext.renderAssets = &renderAssets; inputContext.audio = &audioContext; inputContext.internals = &internals; inputContext.topScreen = &topScreen; inputContext.init_game = init_game; // Set up VBlank handler only after all subsystems, assets, and contexts are initialized irqSet(IRQ_VBLANK, handle_vblank); while (1) { swiWaitForVBlank(); update_frame(); if (!handle_input_frame(inputContext)) { break; } } return 0; }