docs: section 12.11, and rows 44 to 47 of the trap audit

The rung-10 loop whole: which building the fly was in, why every candidate list
on that map emptied, the byte dump that named the battle menu's swapped entries,
the ROM-gated before and after, and the two arms of the trap hunt being identical
because a restore clears the ledgers the loop was built out of.

Section 13.1's tables follow: MENU off both overworld rows and off the narrow
counter row, the move list's conditional BACK, the start menu reachable only by
the raw START button, and two new rows in the pad-empty audit - the room whose
one door the ledger rests, and the map with no way out at all, which is the one
empty pad left and is asserted rather than covered.
This commit is contained in:
acamilo 2026-09-22 09:07:00 +00:00
parent 0163c66f6a
commit c06d3206e2
3 changed files with 279 additions and 10 deletions

View file

@ -914,6 +914,95 @@ mechanism -- `BACK` **739 starts on the move list** and `NEXT` **739 on the top-
The decoder, the reward catalog, the adapter version and the compatibility string are untouched. The decoder, the reward catalog, the adapter version and the compatibility string are untouched.
### 12.11 `MENU` opens a screen its own `BACK` closes, and Red's battle menu is two columns (2026-09-22, rung 10, thirty-one minutes after v0.4.3)
Rank 10 (PEWTER CITY, next the BOULDER BADGE), the fly inside a Pewter building, and since the
restart the macro starts were `MENU` **82**, `BACK` **82**, `GO FRONTIER` 8 -- the event log
alternating `MENU start/done, BACK start/done` on **map 0x35**. Reproduced from the live checkpoint
with the real cartridge: the map is the **upper floor of the Pewter museum**, fourteen blocks by
eight, 81 walkable tiles all reachable and four never stood on, one warp at (7, 7) down to the
museum's ground floor (0x34), two signs and three exhibits.
`infra/docs/macros-traps.md` has the probe whole.
- **`MENU` is a trap by section 12.2's own definition, and it was the *only* button left.** Opening
the start menu changes nothing in the world, so the macro completes on the tile it started on --
and the scene it opens deals `CLOSE`, `CONFIRM` and `BACK`, of which `BACK` presses B and closes
it again. Two buttons that undo each other with nothing else changing: section 12.10's rule, one
scene wider than a battle. **`MENU` is on no pad now.** Not narrowed -- there is nothing behind
it to press it for, because no macro in the vocabulary uses the start menu except as a scene to
leave, and "MENU to save the game" is not a macro that exists. It stays a type, a population, a
tag and a script, so the thirty-one channels, the roles and `--print-compatibility` do not move,
and the start menu is still the fly's to open with the **raw** START button, which reaches the
cartridge in macros mode.
- **Why that map's pad was `MENU` and `GO FRONTIER` and nothing else.** Every candidate list on the
museum's upper floor empties: `geography` has no row for the museum, so `next_hop` answers `None`
and `GO OBJECTIVE` has nothing to aim at -- the objective itself is right, map 0x36 with a
*person* on it, which is rung 11's gym leader; the three exhibits and two signs are *reached* by
`GO NPC` and `GO ITEM`, which retires them for the session (12.1); the four unstood tiles are
walked or excluded and `GO FRONTIER` empties for the window; and the one way out is classified a
**passage** and not an exit -- it is a staircase -- so `unexcluded_exits` drops it while the
blocked ledger rests it, and `ways`' tier 3 for a passage is built out of the same list.
- **`MENU` was also what made an empty overworld pad impossible, so that guarantee moves to the way
out.** `ways` gains, for a *room*, the last resort `GO ROUTE` has had outdoors since 13.1: with
nothing else on this map worth walking to (`palette::stranded`), the exits of this kind come back
**ignoring the blocked window**, the one toward the objective preferred. A target the ledger is
resting is still the only place to go, and a door the run has been through is a better answer
than a pad that cannot move. It is a last resort and not a tier: one unstood tile and it goes
away again, because "the nearest door, once per hold" is row 2's own two hours seventeen.
- **A map with no way out at all is now a genuinely empty pad, and that is said rather than
papered over.** No map in Red is that -- an interior has its front door or its staircase, an
outdoor map has its connections -- it is asserted as a named residual in the pad-empty sweep, and
`game.padEmptyMs` is what reports it if one ever appears.
- **A cursor step waits for the list it was built for.** `THROW BALL` was **63 starts and 63
`blocked`** on v0.4.3, mean sixty-nine frames, which is the cursor to ITEM, the A that confirms
it, the twenty settle frames, and a refusal on the next frame. Red keeps one cursor for every
menu in the game, so "where is the cursor" was half a question: the step that should have walked
the bag list read the battle *menu*'s four entries instead. A `Listing` now says which list it is
and a cursor step says which list its target indexes into; a step whose list is not up waits, as
it already waited for a list reporting no cursor at all (section 4), and takes its press order
and its budget from that list on the first frame it accepts input. A confirming press that has
begun still finishes, because the press is what answers the list.
- **And the reason `THROW BALL` never reached the bag: Red's battle menu is two columns, so its
order is FIGHT, ITEM, PKMN, RUN.** The screen reads `FIGHT PKMN` over `ITEM RUN` and the game's
index does not: `wCurrentMenuItem` is the row inside the column the cursor is in, and selection
adds two for the right column. `battle_entry` had `PKMN` 1 and `ITEM` 2 -- the row-major reading
of the picture -- so **every macro that meant to open the bag opened the party list and every
macro that meant to open the party list opened the bag**, for as long as the four constants have
existed. Surveyed on the cartridge: A at `wTopMenuItemX` 15 with `wCurrentMenuItem` 0 opens the
party list (`wTopMenuItemY` 1, `wTopMenuItemX` 0, `wListMenuID` `$02`) and the game writes
`wCurrentMenuItem` 2 on the frame after. `THROW BALL` and `ITEM` and `SWITCH` have never once
completed on the release box; they do now. The fake had the same mistake in its own two-by-two
geometry, which is why no unit test could have caught it, and it is column-major now.
- **`BACK` on the move list only where the moves can be read.** The other v0.4.3 residual: `BACK`
was 263 of 797 macro starts and every one was over an open move list. A move list whose battler
the seam cannot place binds no `MOVE n` at all, so its pad was `BACK` alone -- and closing the
list is exactly undoing the `MOVE 1` on the menu underneath that opened it, which is 12.10's pair
with `MOVE 1` in `NEXT`'s place. With nothing readable the pad is `MOVE 1` alone, and its script
confirms wherever the cursor stands, which is the press that ends a turn.
**What the harness holds.** No scene's set and no scene's pad contains `MENU`, in any state; a room
whose one door the ledger rests still offers it and the pad is that walk; the move list's pad is
`MOVE n` plus `BACK` only with a readable battler and `MOVE 1` alone otherwise; a cursor step waits
rather than reading the list it has already answered; and `battle_entry`'s four numbers are pinned
against the survey. ROM-gated from the live checkpoint: the fly leaves the museum's upper floor on
**frame 182**, and over thirty-three brain minutes across seven maps `MENU` is on no pad, no
overworld pad is empty, and `MENU`/`BACK` never alternate -- where v0.4.3 deals `MENU` on all seven
with 147 starts. ROM-gated from the rung-9 forest checkpoint: `THROW BALL` 15 starts and **0**
blocked and `SWITCH` 17 and **0**, against 6 of 6 and 15 of 17 before.
**Two things measured and not fixed here.** The trap hunt from this checkpoint does not reproduce
this loop at all, and it cannot: the reached and blocked ledgers are session state that a restore
clears (12.1), so a restored fly walks straight out of the museum, and the live loop needed thirty
minutes of ledger to build. What the hunt reproduces instead is **row 41**, the Pokémon Center
nurse's box -- 62,804 of 71,673 frames on one tile of map 0x3a with `YES` 1,278 of 1,295 macro
starts -- and that is the next trap. Inside a battle the largest residual is `MOVE n`: 890 of 1,431
macros report `blocked`, every one with the move list drawn and its cursor placeable but not
accepting input, so no press moves it and the step spends its budget. That is row 30b's unplaceable
cursor inverted and it needs a WRAM reading rather than a pad change.
The decoder, the reward catalog, the adapter version, the roles and the compatibility string are
untouched.
## 13. Shops and Pokémon Centers (the operator, 2026-09-17: "refactor the shop macros. make it a ## 13. Shops and Pokémon Centers (the operator, 2026-09-17: "refactor the shop macros. make it a
## priority to visit the shop at least once per area; make shop macros item purchases. same ## priority to visit the shop at least once per area; make shop macros item purchases. same
## for the Pokécenter. heal should be a macro.") ## for the Pokécenter. heal should be a macro.")
@ -981,18 +1070,18 @@ observe is not a precondition, it is a guess.
| scene / sub-state | pad | change | | scene / sub-state | pad | change |
| --- | --- | --- | | --- | --- | --- |
| Overworld, outdoors | GO OBJECTIVE, GO ROUTE, GO SHOP, GO HEAL, GO ITEM, GO NPC, GO FRONTIER, TALK, MENU | **MENU added** (row 18); errands added | | Overworld, outdoors | GO OBJECTIVE, GO ROUTE, GO SHOP, GO HEAL, GO ITEM, GO NPC, GO FRONTIER, TALK | MENU was added by row 18 and **taken back off by 12.11**: it opens a screen whose own `BACK` closes it again, and nothing in the vocabulary uses the start menu. Errands added |
| Overworld, indoors | GO OBJECTIVE, GO OUT, GO WARP, GO SHOP, GO HEAL, GO ITEM, GO NPC, GO FRONTIER, TALK, MENU | **MENU added** (row 18); `GO NPC` was off section 3's fixed indoor row and on the plan's, and with one dealer it is on both | | Overworld, indoors | GO OBJECTIVE, GO OUT, GO WARP, GO SHOP, GO HEAL, GO ITEM, GO NPC, GO FRONTIER, TALK | **MENU off, by 12.11**, as above; `GO NPC` was off section 3's fixed indoor row and on the plan's, and with one dealer it is on both |
| Overworld, inside a Pokémon Center | the indoor pad plus HEAL | new. A centre is a sub-state of the overworld, not a `Scene`: pokered has no "a Pokémon Center is open" byte, so the only honest observable is the map id, and a new `Scene` would be a new `game.scene` on the wire | | Overworld, inside a Pokémon Center | the indoor pad plus HEAL | new. A centre is a sub-state of the overworld, not a `Scene`: pokered has no "a Pokémon Center is open" byte, so the only honest observable is the map id, and a new `Scene` would be a new `game.scene` on the wire |
| Overworld, inside a mart | the indoor pad | the counter is a `Shop`; the mart's *floor* is an ordinary interior, with the one exception below | | Overworld, inside a mart | the indoor pad | the counter is a `Shop`; the mart's *floor* is an ordinary interior, with the one exception below |
| Overworld, inside a mart or a centre, counter unfaced | GO SHOP or GO HEAL, TALK when facing the counter, MENU | new, and it is the one place a pad is deliberately *narrow*. The errand is paid on entering and never offered again, so a walk that leaves the building spends the one visit the area gets — measured: the fly reached the mart in 1.7 brain minutes and `GO OBJECTIVE` walked it straight back out over the doormat. While the counter is unfaced nothing on the pad leaves (row 34b) | | Overworld, inside a mart or a centre, counter unfaced | GO SHOP or GO HEAL, TALK when facing the counter | new, and it is the one place a pad is deliberately *narrow*. The errand is paid on entering and never offered again, so a walk that leaves the building spends the one visit the area gets — measured: the fly reached the mart in 1.7 brain minutes and `GO OBJECTIVE` walked it straight back out over the doormat. While the counter is unfaced nothing on the pad leaves (row 34b) |
| Overworld, inside a mart or a centre, counter faced | the indoor pad, plus HEAL in a centre | the suppression is released by facing the counter, by talking to it, or by a walk to it failing | | Overworld, inside a mart or a centre, counter faced | the indoor pad, plus HEAL in a centre | the suppression is released by facing the counter, by talking to it, or by a walk to it failing |
| Dialog | NEXT, YES, NO | unchanged. There is no "a choice is open" flag (`macros-wram.md`), and A and B both advance a plain box, so all three are dealt for every box — what it buys is the fly being able to answer *no* | | Dialog | NEXT, YES, NO | unchanged. There is no "a choice is open" flag (`macros-wram.md`), and A and B both advance a plain box, so all three are dealt for every box — what it buys is the fly being able to answer *no* |
| Menu (the start menu) | CLOSE, CONFIRM, BACK | unchanged. The cursor is the fly's to move with the **raw** D-pad, which still reaches the cartridge in macros mode; a SAVE or a POKéDEX button would be a macro per start-menu entry and is not asked for | | Menu (the start menu) | CLOSE, CONFIRM, BACK | unchanged as a *scene*, and since **12.11** nothing on any other pad opens it: the fly reaches it with the **raw** START button, which still reaches the cartridge in macros mode, and moves its cursor with the raw D-pad. A SAVE or a POKéDEX button would be a macro per start-menu entry and is not asked for -- which is precisely why `MENU` had nothing behind it |
| Menu (the bag, an elevator, the party list outside a battle) | CLOSE, CONFIRM, BACK | unchanged | | Menu (the bag, an elevator, the party list outside a battle) | CLOSE, CONFIRM, BACK | unchanged |
| Unknown (the Pokédex, the trainer card, OPTION, a naming screen, a mid-warp frame) | NEXT, **BACK** | **BACK added** (row 9): B is what leaves the first three, and A leaves none of them | | Unknown (the Pokédex, the trainer card, OPTION, a naming screen, a mid-warp frame) | NEXT, **BACK** | **BACK added** (row 9): B is what leaves the first three, and A leaves none of them |
| Battle, own turn, main menu | MOVE 1..4, SWITCH, ITEM, THROW BALL, RUN | four move buttons for `ATTACK` (section 14); THROW BALL added, and gated on the species since 12.9; **RUN gated**, below. No `BACK`: the four entries are the answers to this menu. **`NEXT` removed by 12.10** — an A press here confirms FIGHT and reopens the list the move list's `BACK` just closed, and `MOVE 1` is the backstop instead, bound here whatever the battler reads as | | Battle, own turn, main menu | MOVE 1..4, SWITCH, ITEM, THROW BALL, RUN (whose cursor indices are FIGHT 0, **ITEM 1, PKMN 2**, RUN 3 -- two columns, 12.11) | four move buttons for `ATTACK` (section 14); THROW BALL added, and gated on the species since 12.9; **RUN gated**, below. No `BACK`: the four entries are the answers to this menu. **`NEXT` removed by 12.10** — an A press here confirms FIGHT and reopens the list the move list's `BACK` just closed, and `MOVE 1` is the backstop instead, bound here whatever the battler reads as |
| Battle, own turn, move list | MOVE 1..4, BACK | as above | | Battle, own turn, move list | MOVE 1..4, BACK -- or **MOVE 1 alone** | as above, plus **12.11**: `BACK` is dealt here only while `wBattleMon*` reads, because a list that binds no `MOVE n` has a pad whose one button closes the list `MOVE 1` underneath had just opened. With nothing readable the pad is `MOVE 1` and its script confirms where the cursor stands |
| Battle, own turn, party list | SWITCH, BACK | unchanged | | Battle, own turn, party list | SWITCH, BACK | unchanged |
| Battle, own turn, the bag | ITEM, THROW BALL, **BACK** | the bag reports a *cursor* (`macros-wram.md` 7.1), and since **12.10** it is the own turn, because a cursor accepting input is one. Its pad is the list's own answers; `NEXT` and `CONFIRM` are both off it, being the same blind A press that *uses* whatever the cursor holds | | Battle, own turn, the bag | ITEM, THROW BALL, **BACK** | the bag reports a *cursor* (`macros-wram.md` 7.1), and since **12.10** it is the own turn, because a cursor accepting input is one. Its pad is the list's own answers; `NEXT` and `CONFIRM` are both off it, being the same blind A press that *uses* whatever the cursor holds |
| Battle, forced switch | SWITCH, NEXT | unchanged (row 8). The one arm that keeps `NEXT` with a cursor up, because it cannot be cancelled and has no `BACK` to undo it | | Battle, forced switch | SWITCH, NEXT | unchanged (row 8). The one arm that keeps `NEXT` with a cursor up, because it cannot be cancelled and has no `BACK` to undo it |
@ -1031,7 +1120,9 @@ measured where it cannot.
| cause | closed by | | cause | closed by |
| --- | --- | | --- | --- |
| a scene that binds nothing at all | the table above: every playable scene has at least one unconditional button (`MENU` on the overworld, `NEXT` in a dialog and in a battle, `CLOSE` in a menu, `LEAVE` in a shop and a PC) | | a scene that binds nothing at all | the table above: every playable scene but the overworld has at least one unconditional button (`NEXT` in a dialog and in a battle, `CLOSE` in a menu, `LEAVE` in a shop and a PC). The overworld's was `MENU`, and **12.11** took it off rather than keep a button whose only effect is a screen its own scene closes again; what stands in its place is the row below |
| an overworld map with no way out at all | **not closed, and named**: with `MENU` gone this is a genuinely empty pad. No map in Red is that -- an interior has its front door or its staircase, an outdoor map has its connections -- so it is asserted as a residual in the pad-empty sweep rather than covered, and `game.padEmptyMs` reports it |
| an *indoors* overworld where every ledger excludes everything and the blocked window is resting the one door | **fixed** (12.11): `ways`' last resort is a room's too, not only `GO ROUTE`'s. With nothing else on this map worth walking to, the exits of that kind come back ignoring the blocked window, the one toward the objective preferred. This is the rung-10 museum: its only way out is a *passage*, so tier 3 was built out of the excluded list and emptied with it |
| an overworld where the talked, reached and blocked ledgers exclude every person, object and frontier tile, every route leads somewhere visited, and there is no objective hop | **fixed**: `ways`' last resort. With *nothing else on this map worth walking to* (`palette::stranded`), `GO ROUTE` offers the exit toward the objective and, failing that, all of them — **ignoring the blocked window**, because a target the ledger is resting is still the only place to go. It is a last resort and not a tier: with anything else on the pad it stays off, because "all of them, nearest" once per hold is row 2's own loop | | an overworld where the talked, reached and blocked ledgers exclude every person, object and frontier tile, every route leads somewhere visited, and there is no objective hop | **fixed**: `ways`' last resort. With *nothing else on this map worth walking to* (`palette::stranded`), `GO ROUTE` offers the exit toward the objective and, failing that, all of them — **ignoring the blocked window**, because a target the ledger is resting is still the only place to go. It is a last resort and not a tier: with anything else on the pad it stays off, because "all of them, nearest" once per hold is row 2's own loop |
| the frontier is empty because `path::frontier` answers about the ten-by-nine walkable window, while most of the map is unstood | **fixed**: `frontier_aims` falls back to the nearest tile of the *whole* map the stood ledger has no entry for. A fallback and not the rule, because the windowed answer is the correct one whenever it has anything in it | | the frontier is empty because `path::frontier` answers about the ten-by-nine walkable window, while most of the map is unstood | **fixed**: `frontier_aims` falls back to the nearest tile of the *whole* map the stood ledger has no entry for. A fallback and not the rule, because the windowed answer is the correct one whenever it has anything in it |
| a running macro aborts and the pad is not re-dealt until the next frame | not a cause: `observe` runs after every frame and the decoder is handed the bound channels again on the very next one | | a running macro aborts and the pad is not re-dealt until the next frame | not a cause: `observe` runs after every frame and the decoder is handed the bound channels again on the very next one |

View file

@ -1589,3 +1589,181 @@ state's `runs/` directory.
- `infra/tests/lint.sh`: all checks passed, de-PII guard included. - `infra/tests/lint.sh`: all checks passed, de-PII guard included.
- `--print-compatibility`: **648 bytes, sha256 `0d9bfde7...707fa`** -- byte-identical to v0.4.1 and - `--print-compatibility`: **648 bytes, sha256 `0d9bfde7...707fa`** -- byte-identical to v0.4.1 and
v0.4.2. Decoder, reward catalog, adapter version and roles untouched. v0.4.2. Decoder, reward catalog, adapter version and roles untouched.
## 2026-09-22, rows 44 to 47: the screen the pad opens and closes again, and a menu read row-major
Thirty-one minutes after v0.4.3 deployed, the release watchdog flagged the next rung. Rank 10
(PEWTER CITY, next the BOULDER BADGE), the fly inside a Pewter building, and since the restart the
macro starts were `MENU` **82**, `BACK` **82** and `GO FRONTIER` 8, with the event log alternating
```
MENU start, MENU done, BACK start, BACK done
```
on map `0x35`. It is section 12.10's pair again -- two buttons that undo each other with nothing
else changing -- with the difference that the two are on **different scenes**, so no per-pad rule
could see it: `MENU` is on the overworld and `BACK` is on the start menu that `MENU` opens.
`docs/design/macros.md` section 12.11 is the contract this closed against.
### Which building, and why its pad was two buttons
Reproduced from the release container's own checkpoint with the real cartridge,
`examples/scene_probe.rs`:
```
- rank 10 (PEWTER CITY), badges 0, unique tiles 2132
- player: Player { map: 53, x: 3, y: 3, facing: Left }
- objective: Objective { map: 54, target: Some(Person) }
- map size: MapSize { width: 14, height: 8 }
- warps: [Warp { x: 7, y: 7, destination_warp: 4, destination_map: 52 }]
- signs: [Sign { x: 11, y: 2 }, Sign { x: 2, y: 5 }]
- people and objects: slot 1 picture 0x04 at (2, 7), slot 2 picture 0x25 at (0, 5),
slot 3 picture 0x20 at (7, 5)
- the pad: ["GO WARP", "GO ITEM", "GO NPC", "GO FRONTIER", "MENU"]
- the map grid: 14x8 walkable 81 reachable 81 unstood 4 unknown 0
- `next_hop(Region { map: 53, part: 0 }, 0x36)` = None, neighbours []
- `ways(Exit)` = [], `ways(Passage)` = [Warp(0)], `ways(Route)` = []
- `objective_goals` = [], `objective_targets` = []
```
Map `0x35` is **the upper floor of the Pewter museum** -- `0x34` is its ground floor, `0x36` the
gym, `0x38` the mart, `0x3a` the centre -- fourteen blocks by eight, one staircase down at (7, 7),
two exhibit signs and three people. Every candidate list on it empties, which is why the live pad
was `MENU` and an occasional `GO FRONTIER`:
| button | why it was not there |
| --- | --- |
| `GO OBJECTIVE` | the objective is right -- map `0x36` with a **person** on it, which is rung 11's gym leader -- but `geography` has no row for the museum, so `next_hop` from map 53 answers `None` with no neighbours, and `objective_goals` is empty |
| `GO OUT` | the museum's upper floor has **no exit-class warp at all**: its one way out is a staircase, which is a `Passage` |
| `GO WARP` | on the pad at a fresh restore, and off it live: `ways(Passage)`' tier 3 is `unexcluded_exits`, which the blocked window empties for ten brain minutes after a refused walk |
| `GO ITEM`, `GO NPC` | the two signs and three exhibits are **reached**, and a reached target is retired for the session (12.1) |
| `TALK` | the fly at (3, 3) facing Left has nothing in front of it |
| `GO SHOP`, `GO HEAL` | `geography::area_of` has no row for the museum, so the area's errands cannot be aimed at from inside it |
| `GO FRONTIER` | on the pad while any of the four unstood tiles is outside the blocked window, which is the 8 starts in thirty minutes |
### The survey that named the second trap
`THROW BALL` was **63 starts and 63 `blocked`** in v0.4.3's own after-arm, mean sixty-nine frames,
and the first reading of that -- the bag had not finished drawing when the step read the cursor --
is true and is not the whole of it. Instrumenting *where* a macro reports `blocked` from the rung-9
checkpoint answered `battle/party` and `battle/between-turns`, never `battle/bag`, and a frame dump
of the menu bytes across one `THROW BALL` says why:
```
DUMP 21 topy=14 topx=9 cur=1 max=1 watch=0x11 sub=main (the cursor walked DOWN)
DUMP 35 topy=14 topx=15 cur=1 max=1 watch=0x21 sub=main (then RIGHT)
DUMP 52 topy=14 topx=15 cur=0 max=1 watch=0x21 sub=main (then UP: the step's target)
DUMP 55 topy=14 topx=15 cur=2 max=1 watch=0x21 sub=main (A: the game adds 2 for the column)
DUMP 60 topy=1 topx=0 cur=0 max=0 watch=0x03 sub=party list=0x02
```
The A press at the right column's first row opens the **party list**. Red's battle menu is two
*columns* -- the screen reads `FIGHT PKMN` over `ITEM RUN` -- and `wCurrentMenuItem` is the row
inside the column the cursor is in, with two added for the right column on selection. So the
game's order is **FIGHT, ITEM, PKMN, RUN**, and `macros::cartridge::battle_entry` had `PKMN` 1 and
`ITEM` 2: the row-major reading of the picture. Every macro that meant to open the bag opened the
party list and every macro that meant to open the party list opened the bag, for as long as the
four constants have existed. The fake's own two-by-two moved row-major too, which is why no unit
test could have caught it.
### What changed, all of it inside the macros
- **`MENU` is on no pad.** Not narrowed: nothing in the vocabulary uses the start menu except as a
scene to leave, so there is nothing behind the button. It stays a type, a population, a tag and a
script -- thirty-one channels, the roles and `--print-compatibility` do not move -- and the start
menu is still the fly's to open with the raw START button.
- **`ways` gains a room's last resort**, the one `GO ROUTE` has had outdoors since 13.1: with
nothing else on this map worth walking to (`palette::stranded`), the exits of that kind come back
ignoring the blocked window, the one toward the objective preferred.
- **A cursor step waits for the list it was built for.** A `Listing` says which list it is and a
step says which list its target indexes into; a step whose list is not up waits rather than
pressing at the list it has already answered, and takes its order and budget from that list on
the first frame it accepts input.
- **`battle_entry`'s `ITEM` and `PKMN` swap**, and the fake's geometry becomes column-major.
- **`BACK` on the move list only where the battler reads**, else `MOVE 1` alone.
| # | trap | trigger | test | fix, or why it is left |
| ---: | --- | --- | --- | --- |
| 44 | `MENU` opens the start menu and that scene's `BACK` closes it again: a pair split across two scenes, and on a map where every other list has emptied it is the whole pad | thirty brain minutes on map `0x35`, `MENU` 82 starts and `BACK` 82; on v0.4.3 `MENU` is dealt on every overworld map | `menu_is_on_no_scenes_pad`, `the_overworld_plan_never_truncates_the_frontier_away`, `the_fly_leaves_the_pewter_building_from_the_rung_ten_checkpoint` (ROM-gated: it **fails on v0.4.3** with `MENU` on the pad of maps 2, 52, 53, 54, 55, 57, 58 and 147 starts) | **fixed**: `MENU` is off every scene's set. A macro whose precondition holds wherever the fly stands and whose effect a neighbouring scene undoes is section 12.2's trap, and the start menu has nothing in it for the fly |
| 45 | the overworld's never-empty guarantee *was* `MENU`, so taking it off could strand a room -- and the museum's one way out is a passage the blocked window rests | the same thirty minutes: `ways(Passage)` empty, `ways(Exit)` empty because there is no exit-class warp at all | `a_room_whose_only_way_out_the_ledger_rests_still_offers_it`, `an_overworld_pad_is_never_one_button_that_undoes_itself`, `no_playable_scene_and_no_sub_state_deals_an_empty_pad` | **fixed**: `ways`' last resort covers a room as well as a route. A map with **no way out at all** is a genuinely empty pad, is asserted as such, and is reported by `game.padEmptyMs` -- no map in Red is that shape |
| 46 | Red's battle menu is two columns, so `battle_entry`'s row-major `PKMN` 1 / `ITEM` 2 sent `ITEM` and `THROW BALL` to the party list and `SWITCH` to the bag | every `THROW BALL`, `ITEM` and `SWITCH` ever started: 63 starts and 63 `blocked` in v0.4.3's after-arm, `SWITCH` 15 of them | `the_battle_menus_two_columns_put_item_under_fight_and_pkmn_beside_it`, `throw_ball_waits_for_the_bag_rather_than_reading_the_menu_it_came_from`, `the_battles_turns_advance_from_the_rung_nine_forest_checkpoint` (ROM-gated, now asserting `THROW BALL` never blocks) | **fixed**: the order is FIGHT, ITEM, PKMN, RUN, surveyed byte by byte. ROM-gated from the rung-9 checkpoint: `THROW BALL` 15 starts and 0 blocked, `SWITCH` 17 and 0, against 6 of 6 and 15 before |
| 47 | a move list whose battler the seam cannot read binds no `MOVE n`, so its pad is `BACK` alone -- which closes the list `MOVE 1` underneath had just opened | `BACK` was 263 of 797 macro starts in v0.4.3's after-arm, every one over an open move list | `the_move_list_deals_back_only_where_the_moves_can_be_read` | **fixed**: `BACK` is dealt on the move list only while `wBattleMon*` reads; otherwise `MOVE 1` alone, whose script confirms where the cursor stands, which is the press that ends a turn |
### The ROM-gated runs, before and after
From the rung-10 checkpoint, thirty-three brain minutes of the game-blind rotation, v0.4.3
(`928d66b`) against this branch:
| measure | before (v0.4.3) | after |
| --- | ---: | ---: |
| maps whose overworld pad dealt `MENU` | **7** (2, 52, 53, 54, 55, 57, 58) | **0** |
| `MENU` starts | **147** | **0** |
| overworld pads that were empty | 0 | 0 |
| frame the fly left map `0x35` | (it left, then came back) | **182** |
From the rung-9 forest checkpoint, sixty-seven brain minutes, the same two arms:
| measure | before (v0.4.3) | after |
| --- | ---: | ---: |
| `THROW BALL` starts / blocked | 6 / **6** | 15 / **0** |
| `SWITCH` starts / blocked | 17 / **15** | 17 / **0** |
| `RUN` blocked | 6 | 0 |
| battles entered / ended | 8 / 8 | 4 / 3 (one still running at the budget) |
| worst battle, in macros | 275 | 450 |
The worst battle grows because a fly whose `SWITCH` and `ITEM` reach their own lists spends turns
switching and healing instead of only attacking. The claim the assertion carries is unchanged and
still holds: a battle **ends**, on a bounded number of macros.
### The trap hunt, before and after -- and why it says nothing about this trap
Twenty brain minutes, seed 20260917, 4 sweep threads, the same connectome and the same cartridge,
from the release container's own rung-10 checkpoint, driven by the brain.
| measure | before (v0.4.3) | after |
| --- | ---: | ---: |
| distinct (map, tile) | 175 | 175 |
| windows flagged | 73/73 | 73/73 |
| macros started | 1413 | 1413 |
| `MENU` starts | **0** | **0** |
| `THROW BALL` blocked | 0 (never started) | 0 (never started) |
| frames in `dialog` | 63,668 | 63,668 |
**The two arms are identical, and that is the honest result rather than a null one.** The reached
and blocked ledgers are session state that a restore clears (12.1), so a restored fly is not in the
state the loop needed: it walks out of the museum's upper floor in one `GO WARP` and never presses
`MENU` at all, so removing a channel the decoder never picked changes nothing downstream. The trap
hunt cannot reproduce a ledger-built loop from a checkpoint, and this is the first review where
that has mattered; the proof for rows 44 and 45 is the pad rules and the ROM-gated run above.
What the hunt *does* reproduce, from this checkpoint, is **row 41** -- the Pokémon Center nurse's
box -- at full scale: 62,804 of 71,673 frames are one text box on **one tile** of map `0x3a` at
(3, 3), with `YES` **1,278** of 1,295 macro starts and the run ending there. That is the next trap
and it now has a checkpoint of its own.
### Residuals, named rather than worked around
- **Row 41 is reproduced and is the next brief.** 1,295 of 1,413 macro starts in both arms are
`YES` at the nurse's counter, on one tile, for the last eighteen brain minutes of the run.
- **`MOVE n` reports `blocked` 890 times in 1,431 macros** in the forest ROM run, every one of them
with the move list drawn and its cursor *placeable* but not accepting input: no press moves the
cursor and the step spends its budget (`reason=budget target=2 max=3 kind=BattleMoves here=1`,
535 of them). That is row 30b's unplaceable cursor inverted, it is unchanged from v0.4.3 (747
blocked in 1,260), and the honest fix is a WRAM reading rather than a pad change.
- **The museum is not in `geography`**, which is why `GO OBJECTIVE`, `GO SHOP` and `GO HEAL` are
all off the pad inside it. Adding the row would give the fly the road to the gym from indoors;
this branch did not, because the map graph is a data change with its own survey (12.7) and the
way out now stands on its own.
- **A map with no way out at all deals an empty pad.** No map in Red is that shape; it is asserted
and reported rather than covered.
### Gates
- `cargo test --workspace` with `FLY_ROM` set: green except
`flysim::integration::the_service_streams_takes_sugar_checkpoints_and_resumes_after_being_killed`,
which fails identically on v0.4.3 on this box (a debug build of the service does not finish
booting inside the test's window here). Pre-existing and unrelated to the macro layer.
- `cargo clippy --all-targets`: clean.
- `infra/tests/lint.sh`: all checks passed, de-PII guard included.
- `--print-compatibility`: **648 bytes, sha256 `0d9bfde7...707fa`** -- byte-identical to v0.4.1,
v0.4.2 and v0.4.3. Decoder, reward catalog, adapter version and roles untouched.

View file

@ -4267,9 +4267,9 @@ fn throw_ball_opens_the_bag_and_moves_the_cursor_to_the_ball_by_reading_it() {
/// Section 12.11: a room whose one way out every ledger is resting still offers it. /// Section 12.11: a room whose one way out every ledger is resting still offers it.
/// ///
/// **What was live** (2026-09-22, rung 10, `ct150-rank10` checkpoint): the fly on **map 0x35, the /// **What was live** (2026-09-22, rung 10, the release container's own checkpoint): the fly on
/// Pewter museum's upper floor** -- fourteen blocks by eight, one warp at (7, 7) down to the floor /// **map 0x35, the Pewter museum's upper floor** -- fourteen blocks by eight, one warp at (7, 7)
/// below (0x34), two signs and three exhibits. The reproduction is in /// down to the floor below (0x34), two signs and three exhibits. The reproduction is in
/// `infra/docs/macros-traps.md`; the shape of it is that every candidate list on that map empties: /// `infra/docs/macros-traps.md`; the shape of it is that every candidate list on that map empties:
/// ///
/// - `geography` has no row for the museum, so `next_hop` from it answers `None` and /// - `geography` has no row for the museum, so `next_hop` from it answers `None` and