09 — Testing the Untestable

Building a debug menu for a platform that doesn’t believe in debugging


The Problem

I had a 5-tier endgame dungeon, legendary gear drops from gym leaders, and post-Champion content — but the feedback loop was slow. Playing from Pallet Town to the credits takes hours. Testing the Rift Zone meant beating the game first. Every time.

In professional software development, you write tests. On a GBA, there are no tests. There’s no console.log. There’s no debugger. You boot the ROM, play, and see what happens. The feedback loop for endgame content was brutal.

I needed a shortcut.

The Cheat Code

The solution was a classic: a secret button combo on the overworld. Hold L+R, press SELECT, and a debug menu appears. No menu option to clutter the UI. No accidental activation. Just a hidden tool for the developer.

The implementation hooks into ProcessPlayerFieldInput() — the function that handles all overworld button presses. It checks gMain.heldKeys for L+R (since the FieldInput struct doesn’t expose L/R as discrete fields) and triggers on SELECT:

if (input->pressedSelectButton
    && (gMain.heldKeys & L_BUTTON)
    && (gMain.heldKeys & R_BUTTON))
{
    LockPlayerFieldControls();
    OpenDebugWarpMenu();
    return TRUE;
}

Five lines of code. The player can’t move while the menu is open, and it tears down cleanly when dismissed.

Two Pages, One Menu

The first version had 10 items — 4 warps and 6 cheats. It barely fit on a 160px screen using FONT_NORMAL at 16px per row. When I asked for warps to every town, that jumped to 18 items. 18 × 16 = 288 pixels. The GBA screen is 160 pixels tall.

I considered a scrollable ListMenu (the game has one), but it was heavy machinery for a debug tool. Instead: two pages with L/R switching. Page 1 is warps, Page 2 is cheats. The title bar shows which page you’re on. FONT_SMALL at 13px per row keeps everything tight.

Page 1 — Warps (12 destinations): All 11 Kanto towns plus the Rift Zone. Each warp drops you near the Pokémon Center. The Rift Zone warp auto-grants all 8 badges and sets the game-clear flag — without those, the Rift Warden won’t let you in.

Page 2 — Cheats (6 options):

The Level-Up Trick

Leveling the party wasn’t as simple as “set level = 70.” Pokémon don’t store their level directly — they store experience points, and the level is derived from the EXP value using species-specific growth rate tables:

u32 exp = gExperienceTables[gSpeciesInfo[species].growthRate][70];
SetMonData(&gPlayerParty[i], MON_DATA_EXP, &exp);
CalculateMonStats(&gPlayerParty[i]);

That last call — CalculateMonStats — is critical. Without it, the Pokémon has level 70 EXP but level 5 HP and stats. The game doesn’t lazily recalculate.

What Changed

The debug menu collapsed my testing loop from “play for 2 hours to reach endgame” to “press three buttons.” I could warp to Cinnabar, talk to the Rift Warden, enter the Rift Zone, fight a boss, check shard rewards, warp back to Viridian, test the merchant stock — all in under a minute.

This is the kind of tool that barely counts as a “feature.” No player will ever see it. But it made everything else possible to verify. Professional development instincts applied to a GBA: if you can’t test it quickly, you can’t iterate on it.

By the Numbers

Metric Value
Commits ~12
Copilot requests ~32
Tool executions ~965
Sub-agents 42

Back to README