#6 완벽한 상관관계, 그리고 틀린 답
I needed to know when a dialogue box was open in Pokémon Red. I searched 8KB of the Game Boy's memory, found one byte that matched perfectly — 1.00 when a box was on screen, 0.00 when it was not, across 1,500 frames — and very nearly built on it.
It was wrong. Not slightly wrong: it agrees with reality about two-thirds of the time once you are a few hours into the game. The only reason I know is that I tested it somewhere else before using it.
Why I wanted the byte
The AI that plays on my channel gets most of its reward for standing on tiles it has not stood on before. That is a fine reward and it is why the agent leaves the house at all. It also has a property I had never thought about: anything that does not move you is worth exactly nothing.
Talking to someone. Reading a sign. Being handed an item. All zero. And the chain of events that gates progress past the second gym — meeting Bill, using his cell separator, receiving the S.S. Ticket, being given HM01 — is four-for-four exactly that. The reward function is blind precisely where the wall is.
So: reward the agent for opening a dialogue somewhere new. For that I need to know when a dialogue is open.
Finding the byte
The honest way to find an unknown memory address is to correlate it against something you can observe directly. I could see the dialogue box on screen, so I let the agent play, labelled each frame from the pixels, snapshotted 8KB of work RAM each step, and asked which byte's on/off-ness lined up with the label.
| address | non-zero when a box is up | non-zero otherwise | separation |
|---|---|---|---|
| 0xCFC4 | 1.00 | 0.00 | +1.00 |
| 0xDFF5 | 1.00 | 0.01 | +0.99 |
| 0xCD3D | 0.35 | 0.02 | +0.32 |
1.00 and 0.00. Out of 1,500 frames, 458 of them with a box up. There is no result you could get that looks better than that.
The check that saved me
All 1,500 of those frames came from the same place: the game's starting save, the first few minutes, one town. So before using the address I ran the identical comparison from a save several hours in — different maps, different characters, a badge earned, menus and battles in the mix.
| starting save | mid-game save |
|---|---|
| 100% agreement | 68.7% agreement |
Two-thirds. 271 frames where the byte said "dialogue" and the screen showed none, 105 the other way. Whatever 0xCFC4 holds, it happens to track dialogue in the opening minutes and stops doing so later.
A perfect correlation on an unrepresentative sample is not a weak result. It is a confident wrong one, and it is worse than a noisy result, because nothing about it invites a second look.
The documented answer was worse
Pokémon Red has a complete community disassembly,
pret/pokered, and it names a byte for this:
wTextBoxID. My environment's other forty-odd addresses came from that project, so it
was the obvious place to go.
| address | what it is | agreement with the screen |
|---|---|---|
| 0xD125 | wTextBoxID (documented) | 43.4% |
| 0xCFC4 | found empirically | 66.3% |
| 0xD730 | wd730 | 58.2% |
Worse than a coin flip, because it is almost always non-zero — it records which text box was last used, not whether one is currently on screen. The name answered a question next to the one I was asking. That is a very easy mistake to make from a symbol table.
The detector was measuring the wrong event
With no reliable byte, I used the screen directly. That worked, and immediately produced a result that should have been obvious in advance:
| button | share of presses with a box on screen |
|---|---|
| A | 42% |
| up | 42% |
| B | 31% |
Pressing up opens a dialogue as often as pressing A. Of course it does — a dialogue stays on screen for many steps while the text advances, and during those steps every button you press is a button pressed "while a box is visible." I was measuring the box's presence when the thing I cared about was its arrival.
Counting only the no-box → box transition separates them: A opens one on 9.7% of its presses, down on 3.9%.
And the farming vector was hiding in the same table
The transition numbers came with a surprise at the top:
| button | share of presses that OPEN a box |
|---|---|
| START | 13.2% |
| A | 9.7% |
| up | 5.3% |
START opens the menu, anywhere, from any tile. Left in, this reward would have paid the agent for pressing START on every square in the game — a pure farming loop with no progress in it. This is not hypothetical: the academic paper on this environment documents an agent that exploited a healing reward by starting endless battles to hurt itself and heal again. START is excluded.
Then I got the weight wrong too
With the signal working I had to decide what an interaction is worth. I reasoned that a full playthrough might turn up a couple of hundred interaction points against eight thousand tiles, so paying ten tiles apiece would land around a quarter of the exploration reward. I wrote 1.0 and measured it:
| term | reward |
|---|---|
| interact | 15.0 |
| explore | 6.7 |
Two and a bit times exploration — dominant, which is the thing I had explicitly set out not to do. The estimate was wrong because interaction points do not stay rare: measured under a trained policy they settle at 0.094 of the tile count, stable from six thousand steps to twelve thousand. That number gives the weight directly, 0.27 instead of 1.0, and the split lands where I wanted it: explore 55%, required-events 27%, interact 10%.
What I take from it
Four things I believed at some point during this were wrong, and they failed in different ways: a perfect measurement on a narrow sample, a documented name that answers an adjacent question, a detector aimed at the wrong event, and a magnitude derived from a guess instead of a count.
Only one of them would have announced itself. The address would have silently degraded, the detector would have produced a plausible-looking reward that mostly tracked button-mashing, and the weight would have quietly drowned out the rest of the reward function. Each was caught by the same cheap habit: take the measurement again somewhere it could come out differently.
The feature is now running as an experiment against a prediction I wrote down first — at equal step count, a policy trained with it should reach further along the required-event chain than one without. If it only finds more map regions, it is exploration with extra steps and it comes back out.