09 — Testing the Untestable

No debugger, no console.log — so I made a cheat code.


The Problem

The 5-tier endgame dungeon was built. Legendary gear dropped from gym leaders. Post-Champion content was in place. 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.

On a GBA, there are no tests. No console.log. No debugger. You boot the ROM, play, and see what happens. The endgame feedback loop 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 opens. No menu option cluttering the UI. No accidental activation. A hidden tool for the developer.

The hook sits in ProcessPlayerFieldInput() — the function that handles all overworld button presses. It checks gMain.heldKeys for L+R (the FieldInput struct does not 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. The player cannot move while the menu is open. It tears down clean on dismiss.

Two Pages, One Menu

The first version carried 10 items — 4 warps and 6 cheats. It fit on a 160px screen with FONT_NORMAL at 16px per row. Adding warps to every town pushed the count to 18 items. 18 × 16 = 288 pixels. The GBA screen is 160 pixels tall.

The game has a scrollable ListMenu, but it is heavy machinery for a debug tool. The fix: two pages with L/R switching. Page 1 holds warps, Page 2 holds cheats. The title bar shows the current page. FONT_SMALL at 13px per row keeps it tight.

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

Page 2 — Cheats (6 options):

The Level-Up Trick

Leveling the party was not as simple as setting level = 70. Pokémon do not store their level. They store experience points. The level comes from the EXP value through 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 — cannot be skipped. Leave it out and the Pokémon carries level 70 EXP with level 5 HP and stats. The game does not recalculate on its own.

What Changed

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

No player will ever see this menu. But it made everything else possible to verify. If you cannot test a thing fast, you cannot build it right.

By the Numbers

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

Back to README