devlog.

Standing page · Gen 1 cartridge

Cartridge finds.

Things the cartridge actually does, collected as they are found — what it is, where it lives, and the mechanism underneath it. A fact about a ROM that nobody re-ran is a rumour with a hex number attached, so each entry ends with how it was checked.

Cartridge Pokémon Blue, USA/Europe SHA-1 d7037c83…d4c48ce2 1,048,576 bytes 5 entries Updated 27 Aug 2026

01 — NINTEN and SONY are the real defaults

Before either naming screen runs, the player is already called NINTEN and the rival is already called SONY. Not merely two strings sitting in the ROM — two strings the game unconditionally copies into the live name fields on every single new game.

1
Occurrences of each string
In the whole 1,048,576-byte image. Once each is what a real data table looks like.
$45AA
NINTEN, ROM bank 1
7 bytes, Gen 1 text encoding, $50-terminated.
$45B1
SONY, ROM bank 1
5 bytes. Directly after NINTEN, with nothing between them.
bit 1
What decides if they survive
wStatusFlags6 $D732 — cleared on every ordinary new game.

The mechanism

The copy happens before the decision to run the naming screens

PrepareOakSpeech zero-fills the block from wPlayerName through wBoxDataEnd and then copies two 11-byte fields in — the labels at those two ROM addresses, DebugNewGamePlayerName and DebugNewGameRivalName. Only afterwards does OakSpeech test bit 1 of wStatusFlags6 and decide whether to run ChoosePlayerName and ChooseRivalName at all.

On a retail cartridge that bit is explicitly cleared by StartNewGame every time, so the naming screens always run and always overwrite the defaults a moment later. The defaults are real, they are written on every new game, and they last a fraction of a second.

This is not an emulator quirk and not a Japanese-only leftover. It is the retail USA/Europe cartridge's own code path — one that exists, is reachable, and is simply never taken in ordinary play.

$45AA $45B1 8D N 88 I 8D N 93 T 84 E 8D N 50 · 92 S 8E O 8D N 98 Y 50 · FA ? 5F ? DA ? EA ? 91 ? CF ? wPlayerName $D158 ← 11 bytes copied reads NINTEN, its terminator, then four bytes of the string that follows it wRivalName $D34A ← 11 bytes copied reads SONY, its terminator, then six bytes of whatever code sits after it
The eighteen bytes at ROM bank 1 $45AA, read out of the cartridge image. Both source strings are shorter than the 11-byte field they are copied into, so each copy runs past its own terminator: the player's name field ends up holding NINTEN, its terminator, and the first four letters of SONY. Harmless — the game reads a name only as far as its $50 — but it is a second, independent confirmation that the two sit contiguously in exactly the order a straight-line copy would overrun. Drawn from the real byte values, not a sketch.

How it was verified

Read, then run — and the control run is the half that matters

A one-off trace drives a genuine cold boot — no save file, deliberately, because the thing under test only exists before any save has a chosen name in it — through the title screen and into a new game, twice. The second run is the control, and it is what turns "the strings are there" into "the strings are the defaults":

Run wPlayerName $D158 wRivalName $D34A
Debug bit forced on every frame NINTEN from frame 368, unchanged to frame 6600 SONY from frame 368, unchanged to frame 6600
Same script, bit not forced — a plain new game NINTEN at frame 368, overwritten with BLUE at 1784 SONY at frame 368, overwritten with RED at 2270

BLUE and RED are the first canned choices the two naming screens offer on this cartridge — the button-mash landing on them is the naming screen behaving exactly as a naming screen should, which is the entire point of running the control. The bit is forced on every frame rather than once, because StartNewGame's own reset of it lands at an unknown frame during the mash, and a one-shot force could be silently undone.

Both encoded strings occur exactly once in the 1,048,576-byte image, at file offsets 0x45AA and 0x45B1, on the cartridge whose SHA-1 is d7037c83e1ae5b39bde3c30787637ba1d4c48ce2.
Lives at
ROM bank 1 $45AA (NINTEN) and $45B1 (SONY) → WRAM $D158 (wPlayerName) and $D34A (wRivalName), 11 bytes each
Gate
wStatusFlags6 $D732, bit 1 — set means skip both naming screens
Kind
Cartridge mechanic — an outside claim, checked against our own cartridge and confirmed
Scope
Pokémon Blue, USA/Europe only. Not checked against the Japanese release — no such cartridge is available here, so no claim is made about it either way

02 — The learnset table is not indexed by dex number

It is indexed by a species' internal index — the number the engine uses on the bus. Read the first 151 slots, which is the obvious thing to do, and you lose all three starters without a single error being raised.

The trap

Both wrong answers parse cleanly and look plausible

EvosMovesPointerTable has 190 slots. Internal index 1 is RHYDON, not BULBASAUR. Thirty-nine of the 190 belong to no species at all — the table that says which is PokedexOrder, and a zero there is what everybody calls MissingNo.

So there are two ways to get this wrong and they are not the same mistake. Reading all 190 slots is fine. Reading the first 151 is not — and it is the one that looks right.

How the table is read Learn entries Evolutions By level By stone By trade
The first 151 slots5805640124
Filtered by PokedexOrder7287252164
All 190 slots, unfiltered7287252164

The third row names the mistake precisely. Every one of the 39 MissingNo slots has its own distinct pointer, and every one of those records is empty — so walking all 190 gives exactly the right totals. The error is not reading too many slots. It is reading the wrong 151.

What the wrong 151 loses

Twenty-seven real species, starting with all three starters

Exactly 27 real species sit at an internal index above 151, and the first 151 slots hold 27 MissingNo in their place. Among the 27: BULBASAUR (internal 153), CHARMANDER (176) and SQUIRTLE (177), with their whole evolution lines, plus ODDISH, BELLSPROUT, RATTATA, GEODUDE, PONYTA, AERODACTYL and PORYGON.

A tool built on the naive read reports that Charmander learns nothing at all, ever, and never says anything is wrong.

The discriminating check

Count the stone evolutions, not the trade ones

Gen 1 has 16 stone evolutions. The naive read produces 12, so that count catches the mistake immediately.

Trade evolutions are not a check. There are 4, and the wrong read also produces 4 — all four of KADABRA, MACHOKE, GRAVELER and HAUNTER happen to sit below internal index 152. A validator built on the trade count alone would have passed a table missing every starter in the game.

Lives at
ROM only — no WRAM address. EvosMovesPointerTable at bank $0E $705C; PokedexOrder at file offset $41024, on this cartridge
Found by
Property, not by a remembered address: the one offset in the image whose 190 consecutive little-endian words all land in $4000-$7FFF and whose records parse as valid evolution and learnset bytes throughout
How it was verified
Read out of the cartridge image: 190 slots located by that property (a unique hit across all 63 banks), PokedexOrder read back as 39 zero entries and 151 real species, 27 of them above internal index 151
Kind
Cartridge mechanic, and the trap it sets for every reader of it

03 — Two species share one printable name

NIDORAN♂ and NIDORAN♀ are two species with two internal indices, two base-stat records and two different learnsets. Their names differ by one gender glyph.

Any name lookup that normalises away characters outside the alphabet collapses both to NIDORAN and returns whichever the cartridge lists first. There is nothing wrong with that rule; there is something wrong with using a name to identify a species at all. The party stores an internal index, and so does every table in the cartridge.

It stayed invisible until a second bug was fixed. A scale-out script filed each plan under a sanitised stem, so one of the two NIDORAN plans was written twice and the other silently never ran — 140 lines planned, 139 files on disk, and a total that looked complete. Giving them distinct filenames exposed the real fault underneath: the female's plan armed a male, which learned HORN ATTACK at level 8 where the plan said SCRATCH.

It was settled by reading the species byte the game had actually generated (wPartyMon1Species = 3, the male) beside the plan that had asked for 15. The screen said NIDORAN in both runs and could not have told anybody apart.

The rule it earns: anything identifying a Pokémon, a move or a map by its printed name should carry the index beside it. The name is for the reader.
Lives at
wPartyMon1Species $D16B
How it was verified
Both lines drill to 6 of 6 events once each is identified by index rather than by name — the male learning HORN ATTACK at level 8 and the female SCRATCH, as the tables say they should
Kind
A cartridge fact, and the trap it sets for every name lookup

04 — A trainer sees through walls

Gen 1's trainer detection is a coordinate comparison, not a raycast. It never asks what is between the trainer and the player.

Cerulean Gym's swimmer stands at (8,7) facing left, with (7,7) and (6,7) both solid — the only walkable ground beside him is to his right. He engages anyway, from (5,7): three tiles away, with two walls in between.

It showed up on the first scripted attempt at that gym, when the player crossed open floor three tiles from the swimmer and the battle simply started, with no line of sight the terrain could plausibly have supplied. Reading the engagement check rather than assuming a sight cone settled it: it establishes that the player is somewhere on the row or column the trainer faces, within range, and tests nothing about the tiles the line crosses.

A trainer that "sees" through solid tiles is not a bug in the usual sense. The cartridge was never testing occlusion in the first place.

Why it generalises: a sweep that blocks a trainer's cone and asks whether the map still connects is the right model for may the player stand here. It is not a model of occlusion, and this entry is the reason none of ours ever assumed a wall between a trainer and a route bought anything.
Lives at
wXCoord $D362 · wYCoord $D361 — what the engagement check compares against the trainer's own facing and position
How it was verified
Observed live on the cartridge, then confirmed by reading the check itself rather than inferring a cone from the behaviour
Kind
Cartridge mechanic

05 — Crossing two levels loses the move between them

Gen 1 announces one level per battle and checks the learnset for the level it ended on. Skip a boundary and the move belonging to it is never offered, and there is no second chance.

A level 6 BULBASAUR was pointed at a single level 5 CHANSEY — 182 experience, the highest base-experience Pokémon in the cartridge — to make it learn LEECH SEED at level 7. It reported LEARNER grew to level 8! and offered no learn prompt of any kind. The first version of that drill read this as "the prompt does not fire" and went looking for a bug in itself.

Nothing needed fixing. The arithmetic did: start the BULBASAUR at level 12 and feed it two CHANSEY, so that exactly one boundary falls inside the run — 973 experience for level 12, +182 → 1155 (still level 12), +182 → 1337, which is level 13 and VINE WHIP.

The general rule it earns: when a harness reports that something never happens, check that the harness put the game in a state where it could.
Lives at
wPartyMon1Level $D18C, and the experience triple below it
How it was verified
Measured on the cartridge with the full prompt trace recorded — the failing single-CHANSEY run and the two-CHANSEY run that produces the prompt, side by side
Kind
Cartridge mechanic

Where these come from. Every entry is drawn from the discovery log of AtlasGB's Gen 1 memory atlas, which records each finding with its addresses, the symptom that produced it, the wrong turns, and the run that settled it.

The cartridge. Pokémon Blue, USA/Europe, SHA-1 d7037c83e1ae5b39bde3c30787637ba1d4c48ce2, 1,048,576 bytes. Where a finding is version-specific or region-specific, the entry says so rather than generalising quietly.

This page grows. Entries are appended as they are found, and nothing goes on it that has only been read about.

← Back to devlog