flybrain/docs/design/macros.md
acamilo 549df6c323 docs: row 55, and the two claims about a mart the survey narrowed
`macros.md` section 12.18 is the review; `macros-wram.md` section 7.1 corrects
the address table, where `wListMenuID` was said to be cleared by every text
display and an item's position in the stock was said to be its cursor index;
`macros-traps.md` carries row 55, its two arms and its ROM run, and two new
residuals -- `wListScrollOffset` is not a pinned address, and a mart that has
never drawn a buy list reads `Unknown` rather than `Shop`.
2026-09-22 22:29:07 +00:00

127 KiB

Macro palette: scene-appropriate actions for the fly

Queued by the operator 2026-09-16 ("give the system something more than randomly mashing buttons"); contract written by Fable the same day. docs/stream-mvp-plan.md has the assessment. This file is the binding contract for the agents building it; where it differs from prose elsewhere, this wins.

1. Doctrine

  • The readout stays fixed. The population decoder, its channels (UP DOWN LEFT RIGHT as one exclusive group; A B as pulses; START SELECT on boot) and its numbers do not change. Palette mode is a scene-dependent meaning of those same channels, applied after the decoder, in the game layer: UP means "slot 0", DOWN "slot 1", LEFT "slot 2", RIGHT "slot 3", A "slot 4", B "slot 5". A scene defines up to six macros and binds them to slots; an unbound slot is "no action".
  • The fly chooses the action. Nothing chooses for it: no default macro, no fallback action on timeout, no scripted objective. If the fly's channels are silent, the game waits.
  • A macro is a button script, executed by the sim on the emulator with the same button register the fly's raw buttons use. There is still no button endpoint on the control API and no button path from the bridge or the page.
  • Disclosed. The honesty panel sentence becomes "the fly chooses the action"; the mode (RAW or PALETTE), the scene, the palette and the chosen macro are on screen at all times.
  • Raw mode remains and is the default until measured; the mode is a config/env knob and a [macros] block in flysim.toml, never a chat command.

2. Scenes (Pokémon Red, pret/pokered at the pinned commit in docs/design/ladder.md)

pub enum Scene {
    Title,                                  // no palette: boot variant of the readout applies
    Overworld,                              // player can walk
    Dialog,                                 // a text box is open and waiting
    Menu,                                   // start menu / submenu open, outside battle
    Battle { own_turn: bool, forced_switch: bool },
    Shop,                                   // mart buy/sell menu
    Pc,                                     // PC / storage menu
    Unknown,                                // detection failed: treated like Dialog (advance only)
}

Detection is from WRAM only, sampled once per game frame after the frame, by pokemon_red::scene::detect(&mut dyn MemoryReader) -> Scene. Sources and offsets are listed in docs/design/macros-wram.md with the pokered symbol names and the verification method for each.

3. Palettes

Small on purpose (3 to 6 slots). Slot binding is fixed per scene so the meaning of a channel is stable inside a scene. Every macro has a name of at most 14 characters for the screen.

scene slot 0 (UP) slot 1 (DOWN) slot 2 (LEFT) slot 3 (RIGHT) slot 4 (A) slot 5 (B)
Overworld GO EXIT nearest unvisited warp or map-edge exit GO NPC nearest visible person and face it GO ITEM nearest interactable object or sign, and face it WANDER 8 random steps (raw flail, bounded) TALK press A MENU open start menu
Dialog NEXT advance text NEXT NEXT NEXT YES A NO B
Menu CLOSE back out to overworld CLOSE CLOSE CLOSE CONFIRM A BACK B
Battle own turn ATTACK best damaging move with PP SWITCH healthiest other Pokémon ITEM potion if own HP < 50% and one is held, else no action RUN (wild only; trainer: no action) ATTACK BACK
Battle forced switch SWITCH healthiest SWITCH SWITCH SWITCH SWITCH no action
Shop BUY POTION if money allows BUY BALL if money allows LEAVE LEAVE CONFIRM LEAVE
Pc LEAVE LEAVE LEAVE LEAVE CONFIRM LEAVE

"Best damaging move" is the highest base power move with PP, type effectiveness applied from the ROM's type chart; ties by index. "Healthiest" is highest HP fraction, not fainted, not the active one. Visit sets for GO EXIT are the adapter's existing per-map exploration ledger.

Amended 2026-09-16, after the smoke run (infra/docs/macros-bench.md). Palette mode left the house eleven times faster than raw and then stalled in Oak's lab, one rung short of the starter, and the two causes were both in this row:

  • Slot 2 was LOOK, and LOOK was TALK. Both pressed A at the tile ahead; they differed only in a precondition, so the palette spent a channel on a duplicate and nothing in it ever walked the fly to a thing. The starter Pokéballs are object_events with SPRITE_POKE_BALL and item balls, signs and bookshelves are the same kind of target — none of them people, so GO NPC is not for them. Slot 2 is now GO ITEM ("nearest object"), the same walk-and-turn as GO NPC over the other half of the map's object data: the object sprites (picture >= FIRST_STILL_SPRITE) and the bg_event sign tiles. TALK's gloss becomes "talk / look", because that is what the one A press was doing all along. GO NPC is now "nearest person" in fact as well as in its gloss: the two macros partition the sprite list rather than both reading all of it.
  • "Unvisited" was never true. GO EXIT asked MacroState::exit_visited, whose default is false, so every exit read as unvisited and the nearest one always won — which, for a fly standing outside its own front door, is that front door. It is now wired to the adapter's boundary ledger through GameAdapter::exit_visited, a read accessor that changes no version string: an exit counts as visited once the ledger holds either half of its boundary key, i.e. once this run has stood on it or beside it. "Found", not "used" — the :on half alone is unobservable for a town door, which fires on the step onto it. The fallback to every exit when the unvisited set is empty is unchanged.

4. Execution

pub struct MacroId(pub u8);             // slot within the current scene's palette
pub struct Palette { pub scene: Scene, pub slots: [Option<MacroSpec>; 6] }
pub struct MacroSpec { pub name: &'static str, pub kind: MacroKind }

pub trait MacroExecutor {
    /// Begin a macro. Returns Err if the slot is unbound or the precondition fails; nothing
    /// is pressed in that case.
    fn start(&mut self, palette: &Palette, slot: MacroId, memory: &mut dyn MemoryReader)
        -> Result<(), MacroRefused>;
    /// Called once per game frame while running: returns the button mask to hold this frame.
    /// `None` means the macro has finished (or aborted) and the fly is consulted again.
    fn step(&mut self, memory: &mut dyn MemoryReader) -> Option<u8>;
    fn running(&self) -> Option<&'static str>;
}
  • A macro owns the buttons from start until step returns None. Hard cap 600 frames (10 s); a macro that exceeds it aborts with MacroAbort::Timeout, logged and shown.
  • A scene change mid-macro (a wild battle starting during GO EXIT) aborts the macro at the next frame; the fly is consulted on the new scene's palette.
  • Between macros the sim consults the decoder's current channels exactly as raw mode does; the active exclusive channel or the most recent pulse selects the slot. A decision is taken at most once per holdMs; while no channel is active nothing happens.
  • Overworld movement uses A* over the current map's walkable tiles (collision from the tileset's collision table and the map blocks, warps and connections from the tables the adapter already reads) with one step per tile and a per-step check that the player moved; three failed steps abort with MacroAbort::Blocked.
  • Battle and menu macros navigate by reading cursor state from WRAM, never by counting presses.

5. Learning and rewards

Unchanged reward catalog and adapter version. Every macro start and finish is a FeedEvent (kind: macro, label = name, outcome = done/blocked/timeout/refused) so the ticker shows it and the event log records it. Reward attribution is what the design is for: the KC->MBON rule already reinforces the recent spike history on payout; the interval between a macro decision and its payout is now one macro long.

6. Feed and control

  • docs/feed-protocol.md header gains game.scene: string, game.mode: "raw" | "palette", game.palette: {slot: number, name: string}[], game.macro: {name, sinceMs} | null. Additive, protocol version unchanged.
  • docs/control-api.md: /status mirrors the same fields. No new endpoints.
  • Stage (the operator, 2026-09-16): the palette is always visible directly under the Game Boy screen, not inside a tab. Layout (the operator): "slide the fly over and put the macro palette right next to it": the strip under the screen becomes the 3D fly on one side, shrunk to make room, and the palette beside it filling the rest of the strip's width; the two share one baseline and one frame so they read as a single instrument. Six cells in the D-pad/A/B order, each with the macro's name and a two or three word gloss of what it does in this scene ("GO EXIT · nearest door"), the button it hangs off as a small glyph, empty cells shown dim as "—". When the fly chooses one the cell lights and stays lit while the macro runs, then shows its outcome for a beat (done / blocked / timeout). Scene changes re-deal the cells with the same halfway-arcade motion the rest of the rail uses. A MODE chip (RAW / PALETTE) sits by the version chip. The existing button afterglow stays, since the macro's presses are real presses. Ticker shows macro events. Terse copy, PNG mockups for The operator before it is wired.

7. Measurement (the gate for turning it on in release)

From the same archived checkpoint, 6 brain hours each, unthrottled, on the dev box: raw mode vs palette mode, reporting rungs reached, time to each rung, macro outcome counts, and reward per brain hour. Ship palette mode as default only if it reaches rung 6 or higher in fewer brain hours in 3 of 3 runs. The decoder preset is not touched in either arm.

8. Work split

  • Agent A, scenes and WRAM: docs/design/macros-wram.md, pokemon_red/scene.rs (Scene, detect), and the state accessors macros need: party (species, HP, max HP, moves, PP, status), enemy species and HP, battle menu and cursor state, text box open flag, start menu state, shop state, money, bag contents, NPC sprite positions and facing, collision for the current map (walkable predicate over tile coordinates), warps and connections. Tests: synthetic WRAM traces and ROM-gated checks from the archived snapshots under the repo's fixture policy (never the ROM itself).
  • Agent B, executor: pokemon_red/macros/ with Palette::for_scene, A* pathing, the macro scripts and MacroExecutor. Depends on A's accessors by the signatures in macros-wram.md; until A lands, B codes against a trait GameState declared in macros/state.rs and A implements it.
  • Agent C, integration (after A and B): sim loop mode, feed and status fields, stage chip and palette cells, event log, measurement harness (examples/palette_bench.rs), docs.

Not for release until section 7 passes and the operator has seen the screen.

9. Plan mode: the map plans, the fly drives (the operator, 2026-09-16 ~16:30 UTC: "we want the fly

to act not roll dice" … "yes. go frontier. approved.")

A third mode beside raw and palette: FLY_MACRO_MODE=plan. The doctrine sentence for it is "the map plans, the fly drives".

  • The plan. Each scene has a deterministic policy that orders its macros by game knowledge into a plan; the palette shows that order, top row first. Overworld: GO OBJECTIVE (path toward the next unreached ladder rung's place, using the map, warp, connection and object data the adapter already reads; if the rung has no known place, fall through), then GO EXIT (unvisited), GO ITEM (untalked object), GO NPC (untalked person), then GO FRONTIER. Dialog: NEXT. Menu: CLOSE. Battle: ATTACK, SWITCH when the active Pokémon is under a quarter HP and a healthier one exists, RUN for a wild battle when the party is weak. Shop and PC: LEAVE. No macro is ever started by the policy itself.
  • The drive. The fly's readout chooses when and how hard, never which: any active direction channel executes the plan's next step; A repeats the step just finished; B skips to the next fallback in the order; silence waits. One decision per holdMs as before; a running macro owns the buttons; scene change re-plans.
  • GO FRONTIER replaces WANDER in every mode: walk to the nearest tile bordering ground this run has never stood on, using the exploration ledger, and face it. No random steps remain anywhere in the palette.
  • Screen. Same strip: rows in plan order, the running row lit, the MODE chip says PLAN. Honesty panel line for this mode: "the map plans, the fly drives".
  • Gate. Same bench as section 7, three arms: raw, palette, plan.

9.1 Building exits, floor changes and routes (the operator, 2026-09-16: "make a distinction between building exit and something like stairs")

GO EXIT conflated three different moves. Every warp on a map is classified by where it goes, from the destination map id the adapter already reads out of wWarpEntries:

  • an exit — its destination is an outdoor map, i.e. leaving a building;
  • a passage — its destination is another interior map: stairs, a ladder, a door between rooms;
  • a route — on an outdoor map, a step off a connected map edge, or a door into a building whose interior this run has not visited.

"Outdoor" is constants/map_constants.asm's own ordering rather than a tileset read: the towns, Indigo Plateau, Saffron and every route are map ids $00..$24 (ROUTE_25), and REDS_HOUSE_1F at $25 begins the interiors. A warp whose destination is LAST_MAP ($ff) — which is how pokered writes a building's own front door — counts as outdoors, because that is what it is on every interior map in the game. Verified with the survey method on the cartridge (tests/rom_macros.rs): Red's ground floor has one passage (the staircase, destination $26) and two exits (the doormats, destination $ff), and Pallet Town has no exits and no passages, only routes.

So GO EXIT becomes three macros:

macro gloss target
GO OUT leave building nearest unvisited exit
GO WARP floor change nearest unvisited passage
GO ROUTE next area nearest unvisited connection, or door into an unvisited interior

The overworld palette keeps six slots and depends on which side of a door the fly is standing:

slot indoors outdoors
0 (UP) GO OUT GO ROUTE
1 (DOWN) GO WARP GO NPC
2 (LEFT) GO ITEM GO ITEM
3 (RIGHT) GO FRONTIER GO FRONTIER
4 (A) TALK TALK
5 (B) MENU MENU

Indoors GO NPC is not on a channel at all; it stays in plan mode's fallbacks, because a room with a person in it almost always has that person between the fly and the door, and TALK already reaches whatever is ahead.

The plan's overworld order becomes GO OBJECTIVE, then GO OUT or GO ROUTE, GO ITEM, GO NPC, GO WARP, GO FRONTIER — with GO WARP promoted above GO OUT when a passage on this map leads to the objective's own map, which is what "the objective is on another floor of the same building" is observable as.

Amended 2026-09-16, after the first cartridge run of plan mode (services/flysim/crates/flysim/tests/rom_plan.rs): the way onward — GO OUT or GO ROUTE — ranks below GO ITEM, GO NPC and GO WARP when the fly is standing on the objective's own map. Every place the rung catalog carries is a map and nothing finer, so arriving on the objective's map makes GO OBJECTIVE fall through, and with the order above unchanged the next entry is GO OUT — which walks straight back out of the building the rung is in. The cartridge showed it: plan mode reached Oak's lab in four hundred frames and then bounced between the lab and Pallet Town, once per hold, for as long as it was driven. Leaving the map the ladder is asking for is the one move the map knows is wrong, so it goes last but for the frontier.

Amended 2026-09-16 (the operator, the contract answer to the rung-4 stall): TALK belongs in the overworld order, and it ranks first whenever the fly is facing an untalked object or person. Otherwise it is absent from the plan: a tile ahead with nothing on it is not a reason to press A. So the plan's two-step for a starter Pokéball, a sign or a villager is GO ITEM (or GO NPC) and then TALK, and A keeps its drive meaning of "repeat the step just finished".

Without it plan mode could not take a starter at all, which is what the first plan smoke hour measured: rung 4 in 0.005 brain hours and then fifty-nine minutes on it, because the one press that accepts a Pokéball can only come from the plan and the plan did not carry it (infra/docs/macros-bench.md).

Building it took three attempts on the cartridge, and the two halves of "facing an untalked thing" turned out to live in different places:

  • Facing something is the plan's own test: the tile ahead is one of the map's object sprites, one of its people or one of its bg_event signs.
  • Untalked is GO ITEM's and GO NPC's choice of target — "nearest untalked" is the nearest thing with a tile beside it this run has never stood on — so the walk that puts the fly in front of something is what makes it untalked, and a thing the run has already stood beside is not walked to again. It cannot be answered from in front of the thing: nothing in WRAM records that a shelf was read, the fly's own tile enters the exploration ledger three stable samples after it arrives (before the next decision is due), and a rule about the thing's other sides never closes for an object on a table, which has only one. Both readings were measured and both failed — the first offered the press once in 549 walks, the second offered it 615 times at one shelf — and neither took a starter.
  • What bounds the press is the drive, which is where a repeat belongs, and it needed two rules that are now part of what "scene change re-plans" means. The plan is re-derived every frame either way; what the cursor does is: it is kept per scene, so the dialog a press opens does not put the overworld back at the press that opened it; and it returns to rank 0 whenever the plan's head changes, because a different macro at rank 0 is the map saying there is a better thing to do than the one the fly was working down towards. TALK is exactly that kind of entry: it becomes rank 0 the moment GO ITEM finishes facing a ball.

With both rules the stub readout of services/flysim/crates/flysim/tests/rom_plan.rs — one direction burst per holdMs, no A, no B, no brain — takes a starter from Oak's lab in 3.5 brain minutes on 96 macros.

9.2 Visited, untalked, and the way to the next town (the operator, 2026-09-16 ~22:10 UTC: "fly is looping")

Kept by section 12: this is knowledge inside macros, not ranking. The stream showed a fly on rung 5 in Pallet Town cycling through the town's houses — GO ROUTE, GO WARP, TALK, GO OUT, GO ROUTE — and never taking the Route 1 connection north. Four things were wrong at once and all four are in this chain.

  • "Visited" is a question about the map on the other side. A connection or a door counts as visited only once the run has stood on the map it leads to (GameAdapter::map_visited), never because the fly once stood on the boundary tile. The old test was the adapter's boundary ledger, which pays for standing on or beside an exit, so one walk along the top row of Pallet Town marked the way to Route 1 used up while a front door the fly had been through twice still read fresh — and with every route "visited" the fallback to all of them let nearest pick a house door, once per hold, for ever. exit_visited stays on the adapter as the boundary question; it is no longer what "somewhere new" means.
  • A map edge knows where it goes. wCurMapConnections is four bits and this crate has no reviewed symbol for the connection headers beside it, so a step off an edge used to carry no destination at all — invisible to "unvisited interior" and unusable as "the way to Viridian". macros/geography.rs is a static adjacency table (the routes and towns of constants/map_constants.asm with the connection tables of data/maps/headers, plus the doors the ladder's places need), and next_hop is breadth-first over it. A map with no row has no known neighbours: an exit into it counts as unvisited and GO OBJECTIVE falls through. Nothing is guessed.
  • Ranking. Exits into a map this run has not stood on come first, and among those a connection before a door: the next area is a bigger unknown than a room off this one. When everything of a kind is exhausted, the ones that take the first hop toward the objective; failing that, all of them, nearest.
  • GO OBJECTIVE crosses maps. When the rung's place is on another map, the goal is whichever of this map's exits takes the first hop of the breadth-first route, asked again on arrival. The search itself steps into Walkable::Unknown at eight tiles a step — a map edge is off the screen buffer by definition, so a search that refused every unknown tile could not plan one step toward Route 1 from the middle of town. A known way round always wins, and a wrong guess costs three failed steps.
  • "Untalked" is a ledger, not a guess about the ground. A talked ledger keyed by map and object index — sprite slot, or a sign's text id — is written when a TALK finishes facing the thing. GO NPC and GO ITEM are offered only for targets it does not hold, with no fallback to the talked ones, so a map whose people have all been talked to leaves the button off the pad entirely; TALK is on the pad only while the tile ahead holds something untalked. The two readings tried before it — the ground beside the thing, and the thing's other sides — each failed in a different direction, and the "never stood on a tile beside it" one never closes at all for a villager standing in the open, which is why the fly talked to the same people all hour. The ledger lives in the executor layer and never reaches the checkpoint: a restored run offers every person once more, which is the honest answer for a ledger that did not survive.

Rung places (pokemon_red/mod.rs). A place is a map and, where the game makes one derivable, an edge of that map; no rung carries a tile, because a tile would have to come from map data in a ROM bank this crate cannot reach. Two are conditional, because the game moves them: rungs 4 and 5 are Pallet Town's north connection until EVENT_FOLLOWED_OAK_INTO_LAB — Oak stops the fly on the path out of town and walks it in — and the lab afterwards; rung 6 is Viridian's mart until EVENT_GOT_OAKS_PARCEL and the lab after it, because the rung is the parcel delivered and the errand starts two maps north. The old catalog answered "Oak's lab" at rank 5, which told a fly standing beside the lab to go back into it. Badge rungs name the gym where the interior ordering derives its id from a verified anchor (Pewter, Cerulean, Viridian) and the town otherwise. Still unknown, and left None: rung 19, the Thunder Badge, because Vermilion Gym's id is not derived.

10. Biased slots: the scene weighs the fly's vote (the operator, 2026-09-16 ~18:45 UTC: "make each

macro slot its own button and have a bias per scene. battles are very structured.

exploration, less so." … "ok good. plan it build it push it.")

Replaces the drive rules of section 9 (direction executes / A repeats / B skips). Mode name stays plan; sections 9's policy ordering, GO OBJECTIVE, GO FRONTIER and the exit split are unchanged.

  • Each slot is its own channel. The six slots keep the fixed D-pad/A/B binding of section 1, so a slot's channel is the fly's population for that button. The decoder is not modified; the game layer reads its per-channel normalized scores.
  • The scene's prior. The section 9 policy ranks the scene's bound macros; the rank becomes a prior over slots: rank 0 gets 1.0, rank 1 gets 0.5, rank 2 gets 0.25, and so on; unbound slots 0.
  • The blend. Once per holdMs, for every bound slot, score = w(scene) * prior[slot] + (1 - w(scene)) * fly[slot], where fly[slot] is the channel's normalized score in [0, 1]; the slot with the highest score starts, ties to the lower rank. The fly must be awake: if every fly[slot] is below the readout's own activity floor, nothing starts (silence still waits; the scene never acts alone).
  • Weights w(scene) (the operator, defaults; env-tunable FLY_MACRO_BIAS_<SCENE>): battle 0.9, battle-switch 0.95, dialog 0.95, menu 0.9, shop 0.9, pc 0.9, overworld 0.3, unknown 0.5. Overworld is mostly the fly; battles are mostly the plan.
  • Screen. Each cell gains a thin weight bar (the scene's prior for that slot, scaled by w) under the gloss; the cell that fired lights as before. Honesty line for this mode: "the scene weighs the fly's vote". MODE chip still PLAN.
  • Feed. game.palette[i] gains prior: number (0..1, already weighted by w) — additive.
  • Gate. Same bench, arms raw / palette / plan(biased); report macros chosen by rank.

Amended 2026-09-16 (the operator, after the first plan(biased) hour): fly[slot] is the spread across the bound slots, not the distance above each channel's own baseline.

fly[slot] = (score[slot] - min) / (max - min)     over the bound slots at that decision

so the fly's strongest bound channel votes 1.0 and its weakest 0.0. Nothing starts when the strongest bound channel is at or under the readout's activity floor, and nothing starts when every bound slot reads exactly alike — a fly with no preference to express is not a fly choosing the head. The one exception is a plan of a single entry, which has no spread at all: an awake fly votes 1.0 for it, because otherwise a one-entry plan could never run and a dialog is a one-entry plan.

The first reading of "the channel's normalized score in [0, 1]" was each channel against its own baseline, clamp(score - 1, 0, 1), and the measured hour is what ruled it out: the readout's score is a ratio around 1 and a settled network's directions sit inside a 9% spread (docs/readout.md), so every vote landed in 0..0.1, every decision went to rank 0, and the fly chose 0 of 4,399 macros — the blend was the plan alone (infra/docs/macros-bench.md). Against the spread, a fly that leans at all leans by 1.0, which is what makes w = 0.3 "mostly the fly" and w = 0.95 "mostly the plan" mean what the weights were chosen to mean. The weights, the prior ladder, the tie rule, the screen and the feed field are unchanged.

11. Macro channels of their own (the operator, 2026-09-16 ~21:15 UTC: "not double mappings on

existing buttons" … "the real keys and the macros. all pressed by neurons like buttons.")

Section 10's blend stays, but the six macro slots stop borrowing the button channels. The eight Game Boy buttons keep their eight descending-neuron populations (command_0..7, round-robin over the 1,305 descending neurons). The six macro slots get six populations of their own, macro_0..5, built from the 96 mushroom body output neurons (MBONs) split round-robin into six groups of 16. This is the honest choice: the MBONs are exactly the neurons whose input synapses the reward rule changes, so a learned preference for a macro in a scene is the mushroom body doing what it does in the real fly. The roles are a dataset artifact (circuit-roles.json, tools/build_flywire.py), regenerated with checksums updated in tools/artifact-checksums.txt; the neuron ids, edges and kernel are untouched, so the compatibility string must not move (rates by role are restored by name; new roles start at zero).

  • Decoder: a second exclusive group macros with channels m0..m5 over macro_0..5, same hold, hysteresis, fatigue and blocked rules as the direction group; no pulses. In plan mode the macro group's normalized scores are the fly's votes of section 10; the button group is ignored for decisions but still drives the on-screen button afterglow so the audience sees the motor side alive. In raw and palette modes the macro group is unused.
  • Screen: each palette cell's glyph becomes its channel, M1..M6, and the SENSES rate bars gain a MACROS row beside DRIVE and A/B.
  • Feed: game.palette[i].channel: string (additive). Honesty line: "the mushroom body picks the macro; the descending neurons press the buttons".

Amended minutes later (the operator: "better yet map each macro type to a new motor neuron"). Not six slot channels that change meaning per scene: every macro TYPE gets a population of its own, stable across scenes, so a channel always means the same action and learning can attach to it. Types (22): GO OBJECTIVE, GO OUT, GO WARP, GO ROUTE, GO ITEM, GO NPC, GO FRONTIER, TALK, MENU, NEXT, YES, NO, CLOSE, CONFIRM, BACK, ATTACK, SWITCH, ITEM, RUN, BUY POTION, BUY BALL, LEAVE. Populations macro_<type> are drawn round-robin from the pool of the 96 MBONs plus the 110 brain motor neurons (206 neurons, 9 or 10 per type; a single neuron would be too noisy to read, a population is what a button is too). The eight buttons keep command_0..7. Decoder: the macros exclusive group has 22 channels; at a decision only the channels of the scene's bound macros compete (the others are masked, the same mechanism as a blocked channel), and section 10's blend uses their normalized scores. Screen: a cell's glyph is the type's short channel tag (e.g. MB·GO, MB·ATK), and the SENSES tab's MACROS row shows the bound channels' rates.

12. Simplification: macros are buttons (the operator, 2026-09-16 ~21:40 UTC: "it seems like we're

defeating what we set out to do … perhaps a simplification." … "good. build it.")

Supersedes sections 9, 9.1 (the drive rules), 10 and the blend of section 11. Kept from them: scene detection, the executor and every macro script, GO OBJECTIVE / GO OUT / GO WARP / GO ROUTE / GO ITEM / GO NPC / GO FRONTIER, the exit/stairs/route split, the visited and talked ledgers, the rung places, and the per-type populations of section 11.

  • Macros are buttons. Each macro type is a button on the pad, pressed by its own population (macro_<type>, section 11), decided by the decoder's macros exclusive group with the same hold, hysteresis, fatigue and blocked rules as the direction group. Whichever bound channel wins, starts. No ranking, no prior, no scene weight, no cursor.
  • The scene decides which buttons exist. Overworld: GO OBJECTIVE, GO OUT or GO ROUTE, GO WARP (indoors), GO ITEM, GO NPC, GO FRONTIER, TALK (only when facing something untalked), MENU. Dialog: NEXT (YES / NO when a choice is open). Menu: CLOSE, CONFIRM, BACK. Battle own turn: ATTACK, SWITCH (ITEM when a potion is held and HP is low; RUN in a wild battle). Battle text between turns: NEXT (it waits for a press; live deadlock fixed in v0.2.4). Forced switch: SWITCH. Shop: BUY POTION, BUY BALL, LEAVE. PC: LEAVE. Unbound channels are masked from the decision. A precondition failure means the button is not on the pad.
  • Knowledge lives inside macros, never in the choice. The only thing that can make the choice deliberate is the reward rule acting on the MBON populations.
  • Modes: raw (buttons only) and macros (buttons plus the scene's macro buttons; a raw button press and a macro cannot overlap: while a macro runs it owns the pad). plan and palette are removed; env values palette/plan map to macros with a warning for one release.
  • Screen: cells in a fixed order by type, present cells lit dim, absent cells hidden, the running cell lit; glyph = the channel tag; no weight bar. MODE chip: RAW / MACROS. SENSES gets the MACROS rate row. Honesty line: "the scene sets which buttons exist; the fly presses; the mushroom body learns which".
  • Feed: game.macroMode: "raw" | "macros", game.palette[i] keeps slot, name, gloss, channel; prior removed.
  • Gate: bench arms raw vs macros, 6 brain hours from the live checkpoint.

12.1 Target ledgers (the operator, 2026-09-16 ~23:30 UTC, 45 minutes live in Viridian City)

The event log cycled GO ITEM start, GO ITEM blocked — the nearest object was unreachable from where the fly stood — and GO NPC start, GO NPC done, walking to the same person again and again, while GO OBJECTIVE toward Route 2 sat on the pad unchosen. Two ledgers, both inside the macros where section 12 says knowledge lives, both read by the target choice and neither by the choice of macro:

  • Blocked. When GO ITEM, GO NPC, GO OBJECTIVE, GO OUT, GO WARP, GO ROUTE or GO FRONTIER aborts Blocked or Timeout, the target it set out for — the exit, the thing or the tile, keyed with the map — is excluded from that macro's target choice for 10 brain minutes (FLY_MACRO_BLOCKED_MINUTES). So the macro picks the next candidate, and with no candidate left the button is not on the pad. A window rather than a permanent entry because "unreachable" is a fact about where the fly was standing: a ledge or a person in a doorway stops being in the way once the fly has moved, and a target excluded for ever would lose the map its item.
  • Reached. When GO ITEM or GO NPC completes — arrived and facing, which is all either one promises — the target is retired for the rest of the session. Only a completed TALK marked a person talked before this, and the fly is free never to press A, which is why the same villager was worth walking to for forty-five minutes. A later TALK writes the talked ledger, which excludes it anyway. GO FRONTIER needs no entry here: a frontier tile stood on stops being a frontier through the exploration ledger, which is now a test rather than a claim.

The abort's target is the one the walk set out for, recorded at start beside TALK's facing entry, because by the time a walk gives up the candidate list has moved on. Both ledgers are session state in the executor layer and neither reaches the checkpoint, exactly like talked: a restored run offers every target once more. A rollback's cancellation writes neither — that is the loop's doing, not the map's. The decoder, the reward catalog, the adapter version and the compatibility string are untouched (648 bytes, 0d9bfde7…707fa).

Screen, same review: the lit cell hid the macro's name — --ink-0 on a 30% accent wash, which measures 5.4:1 and reads as pale green-grey type on muddy amber. The accent moves to the row's glyph chip (page accent behind the darkest ink, 9.5:1) and the row keeps a 16% wash the name stands clear of (7.4:1); the three outcome states take the same arrangement at 10%. Theme tokens throughout, so the three themes keep their own palettes. apps/stage/tests/e2e/macros.spec.ts holds the floor at 4.5:1 for the lit row and each outcome, and the baselines are re-shot.

12.2 Traps: a macro that completes without moving (2026-09-17, rung 8, two hours seventeen)

infra/docs/macros-traps.md is the reproduction, the audit of every macro and scene binding, and the before/after of the loop detector this added (services/flysim/crates/flysim/examples/trap_hunt.rs). The rule the whole of it comes from, and the one to hold new macros to:

A macro that completes without moving because its precondition is already satisfied where the fly stands is a trap. It must either not be on the pad, or count as reached.

The live loop was GO ROUTE into a house, GO OUT straight back out, GO NPC at the girl by the door and GO FRONTIER one tile, every three brain seconds, on five tiles, with GO OBJECTIVE off the pad. What changed, all of it inside the macros:

  • The frame cap is not a fact about the target. A Timeout no longer excludes what the walk was aimed at when the walk ended nearer a goal than it began: it costs one of TIMEOUT_STRIKES (three, section 4's own number at the macro's scale) and the next hold resumes the walk. A Timeout that ended no nearer excludes the target at once. Without this, every attempt at a connection more than ten seconds' walk away excluded it for ten brain minutes — and took GO OBJECTIVE off the pad with it, because both aim at the same TargetKey::Exit.
  • GO ROUTE has no last-resort fallback. With nothing fresh and nothing on the way to the objective it leaves the pad, instead of letting "nearest" pick the door underfoot. GO OUT and GO WARP keep the fallback of section 9.2's third tier, because a room still has to be leavable.
  • A front door nobody can name is not somewhere new. A LAST_MAP warp whose town geography::outdoor_of cannot name counts as visited: the only way onto an interior map is through its own front door, so the map outside it is one the run has stood on.
  • A thing the fly is already facing is TALK's, not GO NPC's or GO ITEM's. "Nearest untalked" excludes the tile ahead, which is the state TALK's own precondition is.
  • A frontier tile whose new ground cannot be stood on is excluded. The arrival press faced it, which stays Done, and the tile enters the blocked ledger, because the exploration ledger only ever records ground somebody stood on.
  • A no route refusal records what it could not reach. The preconditions are deliberately the cheap question and the route search is the real one, so a bound macro could refuse once per hold for ever while its candidate list never changed.
  • Every playable scene deals a pad that can press something. Dialog: NEXT, YES, NO — there is no WRAM observable for "a choice is open" (docs/design/macros-wram.md), and A and B both advance a plain box, so the approximation costs nothing and the fly can answer no. Unknown: NEXT, BACK, because the Pokédex, the trainer card and OPTION land there and B is what leaves them. Menu: CLOSE, CONFIRM, BACK. Shop: BUY POTION, BUY BALL, LEAVE. Battle own turn: ATTACK, SWITCH, ITEM, RUN, then NEXT; forced switch: SWITCH, NEXT — the last entry of each is what keeps a turn with nothing to attack, switch, heal or flee with from dealing an empty pad, which is the battle-text deadlock of v0.2.4 in a different scene.
  • Six rows, eight buttons: the tail survives. The overworld order is truncated in the middle, so GO FRONTIER — the fallback that always has somewhere to go — is never the entry that is cut.

Still open, and named rather than worked around: the name-entry screen reads Unknown and needs START, which the pad has no button for; MENU is on no pad at all, because the overworld row is full at six slots and widening it is a feed and stage change; the mart cannot buy until MacroState::shop_stock reads the counter's stock. The ratchet is not a backstop for any of this — at three attempts on one rung it stops firing by design, and one new tile resets its stall window — which is why the loop detector runs on the dev box before a release.

12.3 Walks that finish: a budget from the plan, a route it keeps, a resume (2026-09-17 08:10 UTC, eight hours on rung 8)

Section 12.2 left the frame cap as a fact about the macro and fixed what a Timeout means. This amends what it measures. Live from 02:04 to 07:00 UTC after v0.3.2 the fly sat on rank 8 in Viridian City and the last 40 KB of the event log held 116 GO ITEM start with 117 GO ITEM timeout, 36 GO ROUTE starts and timeouts, 12 GO OBJECTIVE starts and timeouts, and nothing else: every walk spent its whole 600-frame cap and not one macro completed in five hours. infra/docs/macros-traps.md has the reproduction and the numbers; the mechanism was two tiles wide.

  • The frame cap is not a fact about the ground. A walk's cap is now its own plan's: 24 frames per planned tile plus FRAME_CAP as the floor, capped at 60 s of brain time (3,583 frames). Viridian City is about twenty tiles by eighteen and 600 frames is twenty tiles of walking at worst, so the walk from its south end to the Route 2 connection could not finish inside one macro however well it went. Every other script keeps section 4's flat ten seconds.
  • A walk that is making progress does not time out. The budget only ends a walk that has got no nearer a goal for two tiles' worth of frames; the ceiling is what always ends one. "Ever got closer" (12.2) is the question a finished walk is judged by; "closer lately" is the question a running one is.
  • A walk the cap cut short resumes. The remaining route is kept per target — map and TargetKey, so GO OBJECTIVE and GO ROUTE resume each other's journey to one exit — and the next start continues it instead of planning the same first tiles again. Only from the tile the walk was suspended at: between two holds the fly may press a raw button, take a warp or be rolled back, and a route is a list of directions from one tile. Session state in the executor layer beside the talked, blocked and reached ledgers; it does not reach the checkpoint, and a rollback drops it.
  • The planner commits to its route. The walkable predicate's window is the ten-by-nine screen buffer and it moves with the player, so an A* re-planned after every tile is an A* over a different map after every tile: a tile priced as plausible ground at eight becomes a wall as the window reaches it, the cheapest route flips to the far side of the obstacle, and the step back makes the tile unknown again. The route is planned once and followed while the player is still on it; it is re-planned only when the ground says something new — a step was refused, the plan ran out, or the player is not where the plan left it. This is 12.2's row 23, which three strikes bounded rather than fixed.
  • A refused step is a wall in the direction it refuses. Ledges (HandleLedges matches a facing, the tile stood on and the tile in front against LedgeTiles, and hops two tiles) and tile-pair collisions (TilePairCollisionsLand/...Water refuse a step between two tiles that are each passable) are invisible to the tileset collision list the predicate reads. The ledge tile is absent from every passable list, so the search already treats a ledge as a wall in both directions and declines the hop; a tile-pair refusal is measured instead — a step that spends its whole window with the player where it started becomes a directed wall for the rest of that walk and the re-plan goes round it. Per walk, carried across a resume, never longer: a refusal is a fact about the frame it happened in, and a person in a doorway is the same shape.
  • A goal the search cannot reach is excluded, not walked toward. The search covers the whole map and prices an off-screen tile as passable, so "no goal reachable" means walled off from where the fly stands rather than too far to see. It refuses at start, presses nothing and writes every goal key to the blocked ledger. The closest-approach route was what the two-tile oscillation walked: a step toward the nearest reachable tile, a re-plan that answered with the tile just left, and the pair of them trading the fly back and forth for the whole cap — while the frame cap read as "closing", because it had been one tile nearer once.

The decoder, the reward catalog, the adapter version and the compatibility string are untouched.

12.4 The gate, and the rung under the rank (2026-09-17, audit row 28)

Section 12.3's walks worked and walked the fly into a wall. infra/docs/macros-traps.md has the reproduction: at Viridian City's (18, 10) the fly stepped north into a still sprite at (18, 9), the cartridge drew a box reading "This is private property!", took the joypad and walked it back one tile — and every macro that hit it ended Done, because a scene change is "the world moved on under it" and records nothing. So it was walked into once per hold, for eight hours.

What shut the road was two rungs under the rank. The save has EVENT_GOT_OAKS_PARCEL set and EVENT_OAK_GOT_PARCEL clear: the parcel is carried and undelivered, so rungs 6 (the delivery) and 7 (the Pokédex) are unearned — while the rank reads 8, because the rank is the maximum over satisfied rungs and Viridian City had been stood on. The ladder's rungs are not a chain: rung 8 is a map the fly can walk to, and the two under it are a script two maps south.

  • The objective is the lowest rung this run has not earned, not rank + 1. Nothing in the macros learns any plot: the catalog already held rung 6's place (Oak's lab, once the parcel is in the bag) and the arithmetic was picking the wrong rung out of it. The rank itself is unchanged — it is what the stream shows and what the ratchet measures.
  • A conversation counts as talked to only when the dialog closed without the game moving the fly. TALK is one A press and it finishes while the box is still open, so the ledger entry is pending until the box closes: with the fly on the tile it pressed from and nothing driving it, it is written; if the cartridge takes the joypad or the fly ends up somewhere else, it is not; and a NO drops it, because what the fly said no to is still on offer. The gate was marked talked on the first press, which took it off TALK's pad and out of GO ITEM's list — the one conversation that might have changed something, had once.
  • A macro the cartridge answers by moving the fly excludes its target for the window. A scene change with MacroState::scripted — a non-zero wSimulatedJoypadStatesIndex or wStatusFlags5's scripted-movement bit — is the cartridge refusing the step, which is a fact about the target, so it goes to the blocked ledger like any other refusal. An ordinary scene change (a wild battle starting mid-walk) still records nothing. scripted is false during an ordinary text box, which is what makes it the test for "the conversation ended by moving the fly" rather than for "a conversation happened".
  • YES and NO both stay on the pad in every text box, unchanged from 12.2's row 15: there is no WRAM observable for "a choice is open", and the fly being able to answer no is what makes the rule above meaningful.
  • Reached is a window, not a retirement. 12.1 retired a GO NPC/GO ITEM target for the session on arrival, which also retired every person the fly walked to and wandered away from without pressing A, with nothing able to bring it back. Arriving is not talking: the entry now expires with the same window the blocked ledger uses, so the loop stays bounded at one walk per target per window and the conversation stays available.

The decoder, the reward catalog, the adapter version and the compatibility string are untouched.

12.5 An objective can name a target, not just a map (2026-09-17, audit row 29)

Row 29 went live: GO OBJECTIVE done, GO FRONTIER done, GO OUT done, NEXT done every three brain seconds at Oak's lab door, from about 09:00 UTC. Section 12.4 pointed the objective at the right rung and the rung's place was still only a map, so GO OBJECTIVE arrived on the lab's own doormat and called that arriving, and GO OUT walked straight back out.

  • A place can name what earns the rung. MapPlace::target is a PlaceKind — Person or Object — and the catalog uses it for every rung the early game earns by talking: rung 5 the starter's Pokéball (object), rungs 6 and 7 the parcel and the Pokédex (person, both out of Oak's one script), and the badges (person, the gym leader). Where a rung's place is only a map, nothing changes.
  • Not a sprite slot. A slot is an index into the map's object_event list, which is in a ROM bank this crate cannot reach, and docs/design/ladder.md's rule is that an unverified number does not go in. A kind needs no number: the macro layer already reads the loaded map's sprite list and already splits it at FIRST_STILL_SPRITE.
  • The arrival is GO NPC's. GO OBJECTIVE aims at the four tiles around the nearest untalked, unexcluded target of that kind, with the press that turns to face it, and leaves out the tile ahead exactly as untalked does — so a fly already facing the thing has nothing left for a walk to do, GO OBJECTIVE leaves the pad, and TALK, whose precondition is precisely that state, is what the next hold is offered.
  • The ways out are not on the pad while the objective's own thing is here. GO OUT, GO ROUTE and GO WARP have no candidates while the ladder's target is on this map, untalked and unexcluded. Knowledge inside the macro, the way GO EXIT knows which exits the run has taken: nothing ranks anything, the button simply has nothing to aim at. The earlier attempt at this suppressed the way out with nothing leading to the target and stalled the fly in the room; the escape hatch is the blocked ledger — a target no walk can reach is excluded for the window, the objective's list empties, and the door is a candidate again.
  • The reached ledger does not apply to the objective's target. That ledger is GO NPC's politeness; the ladder is not being polite. The rung is the conversation, so the thing stays worth walking back to until it has been had.
  • The header names the rung the fly is going for. milestone.next is GameAdapter::next_rung rather than rank + 1 (docs/feed-protocol.md, dated).

Measured from the live checkpoint with the rotating stub: Oak's lab in 1.4 brain minutes, EVENT_OAK_GOT_PARCEL at 9.0 brain minutes on 504 macros, and EVENT_GOT_POKEDEX in the same script — the first time any harness has earned a rung that is a conversation. The decoder, the reward catalog, the adapter version and the compatibility string are untouched.

12.6 The battle's own sub-states (2026-09-17, live in Viridian Forest)

Rank 9, Viridian Forest, an hour and forty-one minutes on the rung: a wild Kakuna, Bulbasaur at 8/28, the FIGHT move list open with TACKLE showing 0 of 40 PP — and the event log was NEXT start, NEXT done, every 268 brain milliseconds. Battle::own_turn was the top-level menu alone, so a frame with the move list open read as between turns, whose pad is one NEXT: an A press on whatever the cursor happens to be sitting on. The cursor sat on TACKLE, TACKLE had no PP, the game said so, the box closed, the list came back.

  • A battle menu that is accepting input is the fly's turn. The top-level menu, the move list, and the party list outside a forced switch. The party list is the exception that proves it: opened by ChooseNextMon after a faint it cannot be cancelled and is a forced switch, which has its own pad; opened by choosing PKMN it is an ordinary part of the turn.
  • A list whose cursor cannot be placed is not accepting input. MoveSelectionMenu's coordinates appear on a battle's opening frames, before the engine has copied the active Pokémon into wBattleMon* and while wCurrentMenuItem is still 0 rather than the one-based slot the menu keeps — measured on the cartridge as Moves { cursor: None, count: 2 } with no own Pokémon. That frame is between turns, which is what it was before.
  • The pad is the sub-state's. Main menu: ATTACK, SWITCH, ITEM, RUN, then NEXT (the row-7 backstop for a turn where all four drop). Move list: ATTACK, BACK. Party list: SWITCH, BACK. Forced switch: SWITCH, NEXT, unchanged. NEXT is on no pad while a list is accepting input, because there are exactly two answers to a list — choose from it, or back out of it — and an A press on whatever the cursor holds is neither.
  • ATTACK over an open list moves the cursor to the best damaging move with PP and confirms it; best_move has always skipped a move with no PP, and what was missing is the button being on the pad at all and the script going straight to the move rather than through FIGHT. If every move is out of PP it confirms the cursor's own slot, because Struggle is the cartridge's answer there and the way to it is to choose a move anyway — refusing would take ATTACK off the pad on the one turn the fly has nothing else to press, which is the shape the loop had.
  • The battle bag is the one sub-state with no observable, and it is named rather than guessed. BattleMenu has no item variant: the bag inside a battle reports no cursor through agent A's seam (listing() answers None for it), so an open battle bag reads as BattleMenu::None and the between-turns NEXT advances it — which is survivable, because ITEM's own precondition needs a potion and a hurt Pokémon, so the bag is rarely opened at all. Giving it a pad needs a verified reading first, exactly as MacroState::shop_stock does.

The decoder, the reward catalog, the adapter version and the compatibility string are untouched.

12.7 A doormat is ground, and Route 2 is two maps (2026-09-17 17:32 UTC, four hours on rung 9)

Rank 9 (Viridian Forest, next Pewter City), four hours and eighteen minutes on the rung, the fly on map 50 — VIRIDIAN_FOREST_SOUTH_GATE, the ten-by-eight gate house between Route 2 and the forest. The pad was GO FRONTIER and nothing else, and the last 40 KB of the event log was 175 GO FRONTIER start/done pairs, each done about 420 brain milliseconds after its start — one tile of walking — and a new one every 800 ms. GO OBJECTIVE, GO ROUTE and GO OUT were absent. infra/docs/macros-traps.md has the reproduction and the numbers; two things were wrong and both are here.

  • A doormat is walkable, standable ground, and the reward ledger can never record it. MacroState::tile_visited was the adapter's exploration ledger alone, and that ledger belongs to a payout: sample inserts a coordinate only on a frame its gate accepts, and the gate rejects wMovementFlags & 0xc7 — bits 0, 1 and 2 are BIT_STANDING_ON_DOOR, BIT_EXITING_DOOR and BIT_STANDING_ON_WARP. So every warp tile in the game was a permanent frontier. In the gate house the two southern doormats are the whole south wall, four tiles from everything, and GO FRONTIER walked one tile at them for ever. The gate stays exactly as it is — it is the reward rule's own and the catalog is not this section's business — and the macro layer keeps its own answer to its own question: a session ledger in the executor layer beside talked, blocked and reached, written once per frame from the tile the fly is standing on, never checkpointed, and OR'd with the adapter's. It can only ever make the frontier smaller. A frame the cartridge is driving records nothing, because the coordinates and the loaded map header come from different frames then.
  • A tile a sprite is standing on is not ground. The collision table knows nothing about people or objects, so a villager standing in the open reads walkable underneath; the fly can never stand there while the villager does, so the tile can never enter either ledger. Row 5 bounded that at one walk per exclusion window; leaving the tile out of path::frontier is what lets the frontier exhaust, which is what "the button leaves the pad" needs. The gate house had a guard on one.
  • ROUTE_2 is one map id whose ground is in two halves the player cannot walk between. Its north half touches Pewter City and the forest's north gate; its south half touches Viridian City and the forest's south gate; the belt of trees between them needs CUT. geography had one node for it, so from inside the gate house the breadth-first answer to "which way is Pewter" was ROUTE_2 — two hops, south, through the door the fly had just walked in by. A road that does not exist. geography::Region is a map and a piece of it, next_hop is asked from the piece the fly is standing in, and the one split row in the table carries the two pieces' own neighbour lists and the two doorway rows they are told apart by — 11 and 43, surveyed from the cartridge's own warp table. Maps 46, 48 and 49 stay off the graph, which is the table's standing rule for a building no rung place needs a route through: 49's two doors are both warps of ROUTE_2 itself.
  • An exit into the objective's own map is toward the objective, even with no route in the graph. ways' second tier asked next_hop and gave up when the graph answered nothing, so a way out whose every destination was visited had no candidate at any tier and the button left the pad. The direct answer is the one objective_goals already had and ways did not.

What the fly does with all this is unchanged: the gate house now deals a pad with a way north on it, and which button is pressed is the readout's. The decoder, the reward catalog, the adapter version and the compatibility string are untouched.

12.8 FIGHT is always pressable (2026-09-17, audit row 34)

Section 12.6 put ATTACK on the pad over an open move list and made it confirm the cursor's own slot when nothing has PP, "because Struggle is the cartridge's answer there and the way to it is to choose a move anyway". The blind spot was one step earlier: the move list is only ever reached through FIGHT, and ATTACK's precondition over the top-level menu was best_move(..).is_some() — a move with PP. So the one turn where the fly has nothing left to attack with took the button off the pad, which is the state measured in Viridian Forest at 99.1 brain minutes: a wild battle, a party of one, TACKLE, GROWL and LEECH SEED all at 0 PP, a RUN the cartridge refused, and a pad of ITEM over a bag it could only close and NEXT on whatever the cursor held — which opens the party list, whose only bound button with one Pokémon is BACK. Six hundred thousand frames of it.

  • The precondition is "is there a move list to open", not "is there a move with PP". FIGHT always opens; what is behind it is the cartridge's business. can_fight is the Pokémon that is out having any move at all, and the one case that still answers no is a Pokémon with no move in any slot, which nothing in the game reaches.
  • What FIGHT does with no PP is measured, not assumed. Surveyed on the cartridge from the state above (examples/scene_probe.rs, FLY_PROBE_CATCH=nopp): backing out to the top-level menu and confirming FIGHT leaves the move list closed on every frame — CheckPlayerHasUsableMoves prints "has no moves left!", sets Struggle, and the turn moves on. So the macro's script is "confirm FIGHT and stop": a second cursor step would press A at text.
  • A move list that is open with nothing having PP keeps section 12.6's answer. The same survey found the list can be open in that state, and confirming a 0-PP slot from it earns "There's no PP left for this move!" and the list again. BACK is on the pad beside ATTACK and leads to the menu where FIGHT now works, so the cost is one wasted press rather than a loop — and taking ATTACK off the list's pad would leave BACK alone on it, which is the shape this section is about.
  • The fly's own turn never deals one button. That is the invariant, and it is what the party list with a one-Pokémon party breaks if it is reached at all: its one entry is the Pokémon that is already out, so there is nothing to choose and BACK is the only press. Nothing is added there — a second button would be a macro that completes without moving, which section 12.2 calls a trap — and what changed is where BACK leads: a menu with ATTACK on it.

The decoder, the reward catalog, the adapter version and the compatibility string are untouched.

12.9 BACK belongs to a list, and a ball is not thrown at what the party already has (2026-09-22, rung 9, sixty-nine hours)

Rank 9 (VIRIDIAN FOREST, next PEWTER CITY), sixty-nine hours on the rung, the ratchet's three attempts spent, the fly on Route 2 and in the forest and mostly in wild battles. Since the restart the macro starts were BACK 135, THROW BALL 28, MOVE 2 10, RUN 5 and GO ROUTE 5, and the event log repeated RUN blocked, BACK start, BACK done. infra/docs/macros-traps.md has the reproduction and the numbers; two macros, and section 12.2's one rule between them.

  • BACK is a button only where there is a list to leave. The top-level battle menu never had it and does not now: FIGHT, PKMN, ITEM and RUN are the four answers to it and B is not a fifth. What dealt it was the between-turns row, which section 13.1 gave NEXT, BACK for the sake of the battle bag -- the bag reads as nobody's turn (12.6), so it lands there -- and which is also every frame of battle text, every animation and every turn resolving. On those frames there is nothing open, so the B press changes nothing the NEXT beside it does not, the macro completes in a handful of frames on the tile it started on, and half the pad is a button that cannot move the game on. That is exactly a macro that completes without moving because its precondition is already satisfied where the fly stands (12.2). The row is now the sub-state's, like the own turn's: BACK with the bag open, NEXT alone otherwise, and the move list and the party list keep it as before.
  • RUN blocked was the script, not the cartridge. Not "can't escape": RUN is one cursor navigation, and a cursor macro whose list is not accepting input waits rather than pressing blind (section 4), for CURSOR_WAIT = 180 frames before it reports Blocked. So a RUN that won a hold as the menu closed spent three brain seconds pressing nothing and then gave up -- row 19 of the audit, bounded and left, and the reason it was visible at all is the BACK above filling the frames on either side of it. RUN's own precondition is unchanged (13.1: only a wild battle the fly is losing).
  • A ball is not thrown at a species the party already holds. THROW BALL's precondition was three facts -- a wild battle, a ball in the bag, room in the party -- and it now has a fourth: the Pokémon on the other side is not one the party already has. Viridian Forest holds five species and the fly had caught its own, so every throw spent a ball on a Caterpie or a Weedle it was carrying, and a catch opens the nickname screen, which reads Unknown, needs the START the pad has no button for (row 14), and is the one screen in the game neither NEXT nor BACK leaves. The party is the caught set here and it is the honest one: it is the cartridge's own lifetime record, it survives the restart the session ledgers do not, and it is in the same numbering the enemy is read in -- the internal species index. wPokedexOwned is not asked, because that bitset is by Pokédex number and the table converting an internal index into one lives in a ROM bank this crate cannot read: docs/design/ladder.md's rule is that an unverified number does not go in. It costs nothing measurable -- the button is already off the pad while the party is full and nothing in the vocabulary deposits into a box (row 17), so every species this run has caught is in the party this reads. An enemy the seam cannot place leaves the button where it was: a precondition this crate cannot observe is not a precondition (13.1).
  • The ratchet is not the backstop, and on this rung it cannot be. budget_spent precedes both triggers and three attempts on rank 9 were spent sixty-nine hours ago, so no recovery can fire again until the rank improves -- row 22, contract, and the reason the trap hunt runs on the dev box before a release. What has to carry the run is the road: rung 9's place stays VIRIDIAN_FOREST, the objective is the lowest unearned rung (12.4), which is rung 10's PEWTER_CITY, and geography::next_hop from the forest answers VIRIDIAN_FOREST_NORTH_GATE (47), then Route 2's north piece, then Pewter (2) -- already asserted hop by hop in macros::geography's own tests since 12.7. GO OBJECTIVE is on the overworld pad there, which the ROM-gated run from this checkpoint holds.

The decoder, the reward catalog, the adapter version and the compatibility string are untouched.

12.10 No two buttons on a battle pad undo each other (2026-09-22, rung 9, seventy-one hours)

Thirty-five minutes after v0.4.2 deployed, the watchdog flagged the same rung again. Rank 9 (VIRIDIAN FOREST, next PEWTER CITY), seventy-one hours on it, and since the restart the macro starts were NEXT 1264, BACK 1241, THROW BALL 5 and GO WARP 3 -- the event log alternating NEXT start/done, BACK start/done every hold, on map 51.

Reproduced from the live checkpoint with the real cartridge and the real brain (infra/docs/macros-traps.md has the numbers): twenty brain minutes, 71,673 frames of them all in one battle, 73 of 73 windows flagged, one distinct tile, and the two counts that name the mechanism -- BACK 739 starts on the move list and NEXT 739 on the top-level menu.

  • The pair was split across two sub-states of one turn, which is why 12.9 did not catch it. NEXT's script is one press of A. On a frame of battle text that A advances the text, which is what the button is for. On the top-level battle menu the same A press confirms whatever the cursor is sitting on, and the cursor sits on FIGHT, so NEXT opened the move list -- whose BACK closed it again. Each button was legitimate where it stood: 12.9's rule is about BACK and a list, and there was a list to leave. What is a trap is the pair: two buttons that undo each other with nothing else changing, so the roll lands on one of them nearly every hold and the turn never resolves. Section 12.2's rule, stated at the pad instead of at one macro: no pair of buttons on any battle pad may undo each other with nothing else changing.
  • NEXT is the A that advances text, so it belongs only on a frame with no cursor accepting input. It is off every own-turn pad: the top-level menu, the move list, the party list and the bag. The between-turns row keeps it alone, and the forced switch keeps it as row 8's backstop -- the one arm with a cursor and no BACK at all, because a forced switch cannot be cancelled.
  • MOVE 1 is the backstop the top-level menu keeps. NEXT was there for the turn where every other button drops (row 7), and it was the wrong button for the job twice over: it does not end a turn, and what it does instead is reopen the list BACK had just closed. MOVE 1's precondition over that menu is 12.8's -- "is there a move list to open", and FIGHT always opens, because CheckPlayerHasUsableMoves sets Struggle without opening it -- so it is bound there whatever the seam makes of wBattleMon*, and its script over that menu is "confirm FIGHT and stop", which reads no move at all. A battler the seam cannot read is not a reason to take the turn's one ending button away.
  • The bag is the fly's turn, and its pad is the bag's own answers. Battle::own_turn answered false for it, which was the last frame in the game with a cursor accepting input and no own turn -- so it landed on the between-turns row, and that row's NEXT on an open bag is the A press that uses whatever the cursor holds. The invariant is now whole: a battle frame with a cursor accepting input is the fly's turn, the forced switch excepted because it has a pad of its own. The bag's row is ITEM (use the thing) / THROW BALL / BACK; CONFIRM is gone from it, because it was the same blind A press under another name, and both scripts already navigated this list by reading its cursor (7.1 of macros-wram.md).
  • wListMenuID is not stale, which is why the bag reading stands. It is zeroed by DisplayTextIDInit at the start of every text display (macros-wram.md), so a battle's text frames cannot inherit an ITEMLISTMENU from a bag the fly closed. Checked because a stale byte there would have put the bag's pad on every frame of battle text.
  • What the harness now holds, rather than the pads alone. The ROM-gated run from this checkpoint asserts that no battle pad deals both buttons, that NEXT is on no frame with a cursor accepting input, that the longest NEXT/BACK alternation is under four, and that every battle it enters ends -- 25 of 25, worst 275 macros -- where v0.4.2 spent a whole hunt inside one that never did. The battle rules are asked of the scene the pad was dealt for and not of wIsInBattle: the $ff frame a lost battle passes through reads Unknown, whose pad is NEXT, BACK by contract (row 9), and it is not under a battle's rules.

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.

12.12 The nurse's box is a ring, and YES and NEXT are one press (2026-09-22, rung 10, row 41)

Rank 10 (PEWTER CITY), the fly in the Pewter Pokémon Center, and since the v0.4.4 restart the macro starts were YES 2,142, TALK 107, GO FRONTIER 26, BACK 24, with the event log's tail YES start/done for ever. This is row 41, first measured in the rung-9 trap hunt (YES 1,278 of 1,295 starts on one tile of map 0x3a) and named by 12.11 as the next trap. Reproduced from the live checkpoint with the real cartridge and surveyed press by press (examples/scene_probe.rs, FLY_PROBE_CATCH=nurse); infra/docs/macros-traps.md has the survey whole.

  • The conversation is a ring of forty-six A presses, and the box is a choice on one of them. Welcome, "We heal your POKéMON back to perfect health!", the YES/NO box, "OK. We'll need your POKéMON.", the machine, "Your POKéMON are fighting fit!", "We hope to see you again!", the box closes for a single frame, and the next A press at a nurse two tiles away over her counter opens the whole thing again. The party read 70/70 and healthy on every frame of it, so not one of those presses changed anything. That is section 12.2's rule at conversation scale: a macro whose precondition is already satisfied where the fly stands is a trap.
  • The brief allowed three readings and the survey settles it as none of them. The box open at the checkpoint is not the prompt, it is the closing line, and YES there is an A press on plain text -- forty-five of the forty-six frames are like that. HEAL is not in the loop at all: its precondition already reads the live party, party_needs_rest answers false, and the button was off the pad the whole time. And HEAL's own wait is a read of the party, not a loop waiting for a timer, so it cannot spin on a party that is already full. What was in the loop was the dialog pad, dealt unconditionally, and TALK to get back into it.
  • On a box that is a choice, NEXT is YES under another name. An A press at a two-option menu confirms the option the cursor is on, and the cursor opens on YES, so NEXT and YES are one press with two channels -- 12.10's rule about a pair of buttons, in a dialog rather than a battle. NEXT is off any pad dealt for a readable YES/NO prompt; the pad is the box's own two answers.
  • At the nurse's prompt the answer that changes something is the only one bound. Hurt or statused, YES; full and healthy, NO. Section 13 has read that byte for HEAL since the errands existed; this is the same byte read for the box HEAL opens. Knowledge inside the macro as a precondition, and nothing ranks the two: one of them simply is not there.
  • TALK is not offered at a nurse the party has no use for. The nurse is an object with a purpose rather than a person to chat with, and she is the one person in Red whose conversation has a precondition the cartridge publishes. This is the door into the ring, and closing it is what makes the rest of the section a backstop rather than the fix. Nobody else is narrowed: an ordinary villager is TALK's whatever the party reads.
  • The nurse enters the talked ledger on a completed heal or a declined prompt. A completed HEAL has had the conversation with its own presses, so TALK has nothing left to open, and the reached window would otherwise expire in ten brain minutes and offer the ring again. A declined prompt is 12.4's rule deliberately inverted, for one person: "the thing it said no to is still on offer" is true of a villager with something to say and false of a service the party does not need -- and the pad only ever offers NO at her prompt when the party is already full.
  • TALK's precondition and TALK's ledger entry were two different questions, which is why she was never retired. The precondition reaches over a counter, because IsSpriteOrSignInFrontOfPlayer does; the entry was read one tile ahead. So TALK was bound at the counter and recorded nothing at all, 107 times. A precondition and the ledger that answers it have to be the same reading, and they are now.
  • The general rule: an answer that brings the same prompt straight back is excluded for the blocked window. Same map, same tile, same readable prompt, within one hold of the answer finishing -- nothing moved and nothing was settled, so the press did nothing the next press will not undo. It is 12.1's ledger doing for an answer what it does for a walk, with the same ten brain minutes, keyed TargetKey::Answer { at, yes }. The exclusion narrows a pad and never empties one: with both answers excluded both come back, because a box nothing can answer is a screen nothing can leave.
  • What the reading rests on, and what it does not claim. docs/design/macros-wram.md said outright that there is no "a choice is open" flag, and there is not -- so yes_no_prompt is the construction text_box's waiting already makes: wFontLoaded plus the figure the game draws, a border at (11, 6)-(19, 11) with the shared cursor parked at row 8, column 12, one item below the first, watching A and B. Both halves are needed: the cursor bytes are not cleared when the box closes, so all forty-six of the nurse's frames carry that geometry while the box itself is drawn on exactly one. Red places a two-option menu where the script asking for it says, so a prompt drawn somewhere else reads false and its dialog keeps the pad it has always had. That is a named limit, not a gap being papered over.

What the harness holds. Unit: HEAL is off a full party's pad, including the rung-10 party's own numbers; TALK is off a rested nurse's pad and on a hurt one's; the nurse's prompt deals one answer and a plain box still deals three; a readable prompt that is not hers deals both answers and no NEXT; an answer whose prompt reopens is excluded and an answer that settled the box is not; a completed heal and a declined prompt both write the nurse into the talked ledger. ROM-gated from the live checkpoint: the fly leaves map 0x3a, YES starts stay under five, NEXT is on no pad while a prompt is readable, and TALK is on no pad at a rested nurse.

The decoder, the reward catalog, the adapter version, the roles and the compatibility string are untouched.

12.13 UNKNOWN is two states, and one of them has nothing to press (2026-09-22, rung 10, five and a half hours)

Live on v0.4.5, rank 10, the fly inside the Pewter museum with rung 11's BOULDER BADGE two doors away: since the 11:30 restart the macro starts were GO FRONTIER 1,235, BACK 678, GO OBJECTIVE 267, GO OUT 242, YES 169, and the previous review had already named the shape -- BACK pressed 189 times "in a text box" on map 0x02, surrounded by GO OBJECTIVE and GO FRONTIER. infra/docs/macros-traps.md has the reproduction; three things were wrong and all three are here, and the first of them is not a text box at all.

  • BACK was dealt by Scene::Unknown, on frames with nothing drawn. BACK is on no overworld pad and on no dialog pad, so every one of those presses came from Unknown — and Unknown holds two states under one name. One is a screen this crate cannot name: the Pokédex, the trainer card, OPTION, where A and B are what leave it and 13.1 put them there on purpose. The other is a frame of the overworld where the buttons are not reaching the player — a warp in flight, a scripted push-back, the museum guide walking the fly through the door — which scene::detect calls Unknown because its overworld branch needs controllable. On the second, NEXT and BACK are an A and a B pressed into somebody else's script: they change nothing, they complete where the fly stands, and that is 12.2's trap with no box to advance. So the pad is dealt on whether a box is drawn (wFontLoaded, the reading text_box already makes), and a scripted overworld frame is an empty pad the fly waits out. Nothing else can wait it out: the cartridge gives the buttons back by itself, which is the difference between this and every other empty pad 13.1 enumerates.

12.14 A frontier no walk can reach is a fact about the map (2026-09-22, the same run)

The museum's ground floor is 98 walkable tiles, 62 of them reachable from the door and 39 never stood on — and almost all of those 39 are behind the admission desk. GO FRONTIER aims at ground the run has not stood on, the route search cannot reach any of it, so the macro refuses no route, presses nothing and writes every goal to the blocked ledger (12.1). That ledger is a ten brain minute window: it lapsed, all of it was a candidate again, and the refusal happened again, once per hold, for hours.

A window is the right shape for a target somebody is standing in front of and the wrong shape for ground the map has fenced off. So the refusal is remembered per map instead, with no window, beside the pushed-tile ledger of row 37 — and it is cleared by the one event that can change the answer: the fly standing somewhere on that map it has not stood on before, because a door opened, a script carried it through, or somebody moved out of a doorway. Re-entering the map clears nothing; that was the loop the window made. Session state like every other ledger, never checkpointed.

12.15 The ratchet's stall window cannot see a fly walking a road it has already covered

Two "Stuck" rollbacks fired on this rung inside half an hour, attempts 0 → 2, each one putting the fly back where it had started. Both were the ratchet working exactly to contract: its stall window is restarted by exploration — progress.unique_locations, ground the run has never stood on (docs/design/ladder.md, and row 22) — and 120 brain seconds of safe overworld samples without one new tile is a stall by definition. Entering a map for the first time already counts, because a new map is a map's worth of tiles nobody has stood on; re-entering one does not, and two museum floors and a covered town are exactly that.

What is plainly progress and is not ground: being nearer the objective than this run has ever been, counted in hops over the same map graph GO OBJECTIVE walks (geography::hops). The macro layer answers it, the sim loop passes it to the ratchet beside the coverage figure, and the ratchet treats it exactly as it treats a rise in coverage — it restarts the window and does nothing else: no budget spent, no snapshot taken, no trigger skipped. It can fire at most once per step of the road, and nothing in the macro layer reads it back: no macro is ranked by it and no button is bound on it.

The checkpointed ratchet state does not move. The signal is a level on one sample, not a counter, so there is nothing to serialise and nothing to drift across a restore.

12.16 What is still in the way of the badge, measured rather than fixed

With 12.13, 12.14 and the museum's two rows on the map graph, the ROM-gated run from the live checkpoint reaches the gym's own interior on 15 macros — against never, in five and a half live hours. Two things it then does are worth naming, because neither is a bug and both cost the rung:

  • the town's errands come first, and they are session state. Section 13 puts an unvisited mart or Pokémon Center ahead of the rung's place for every map in that area, and the gym is in Pewter's area like everything else. The ledger does not survive a restart, so the 11:30 restart re-armed both errands and GO OBJECTIVE aimed at them before the leader. They are paid once and the run goes on; the cost is minutes, not hours.
  • GO FRONTIER is still most of the run — 1,247 starts in 55 brain minutes, 649 of them in Pewter City itself. There the frontier is genuinely reachable, one tile at a time, because the whole-map grid is refused on a walking fly (below) and the windowed frontier is the nearest unstood tile on screen. It is covering ground rather than standing still, which is why it is a residual and not a trap.

The trap hunt does not improve. Distinct tiles 193 -> 175 and flagged windows 59 -> 69, with BACK in a text box 295 -> 0 and battle frames 6,948 -> 20,894. What the old cycle is replaced by is a new one on the same five tiles -- GO FRONTIER, GO HEAL, GO ROUTE, x42, for seven and a half brain minutes -- and then four brain minutes inside one battle, which the hunt's tile rule flags as hard as it flags a stall. infra/docs/macros-traps.md has both arms whole and row 54 is the next brief. The ethos check's "the trap hunt improves" does not hold for this branch; the ROM-gated run does, and both are reported rather than one of them.

The whole-map grid is refused while the fly is moving. pokemon_red::state::map_grid checks its decode against the screen buffer over the fly's own tile and its four neighbours, and on a frame mid-step the two are a tile apart. Measured on Pewter City from the rung-10 checkpoint: standing still it decodes on 118 of 120 frames, and the frame the survey caught disagreed on three tiles by exactly one row in the direction of travel. A walk planned on such a frame is planned over the ten-by-nine window of section 15's "before".

Worked in 12.17, and the guess above was the wrong way round: the survey found the coordinates change at the end of the step, so it is the screen that is a tile ahead of wYCoord rather than wYCoord ahead of the screen.

12.17 The coordinates change at the end of a step, and an errand arrives inside (2026-09-22, row 54)

Section 12.16's two residuals turned out to be one fact and one old rule that had been left off one walk. Both were measured from the same rung-10 checkpoint, with FLY_PROBE_CATCH=step in examples/scene_probe.rs; the bytes are in docs/design/macros-wram.md section 9.

  • wXCoord and wYCoord change at the end of a step, not at its start. Holding UP out of the Pewter museum, wYCoord read 7 for frames 0 to 15 of a sixteen-frame step and 6 from frame 16, while from frame 2 the screen buffer already held the view centred on (10, 6). The grid's cross-check compared the decode of (10, 7) with the screen's reading of (10, 6) -- $20 against $01 -- and refused, on fourteen frames of every sixteen. Pewter City decoded on 118 of 120 standing frames and on none of the moving ones, so every walk the fly actually took was re-planned over the ten-by-nine window: section 15's "before", and row 23's oscillation with it.
  • So the decode is read from the tile the screen is centred on. Nothing in the pinned symbol table says "a step is in progress" and a new address cannot be pinned without the disassembly gen_symbols.py reads, so the anchor is measured rather than named: the screen is centred on the fly's tile or on one of its four neighbours, and the one it is centred on is the one whose whole neighbourhood agrees with the decode. The check keeps the property it exists for -- a wrong stride, a wrong quadrant, a half-loaded map or the mid-warp tear agrees with none of the five, because the whole neighbourhood has to agree under one anchor rather than each tile finding an anchor of its own.
  • The tile a step is landing on is ground the run has covered. The other half of the same fact: for fifteen frames of every sixteen the stood ledger recorded the tile the fly had already left, so the ground under it stayed unstood, path::frontier kept offering it, and GO FRONTIER was dealt aiming one tile away -- a walk that reports done the instant the step it did not make lands. A step that has begun always finishes, and the screen has already centred on it.
  • An errand arrives inside the building, facing the counter, never on the doormat outside it. GO SHOP and GO HEAL aim at a door, and a door's aim carries no press because the warp fires when it is stepped on -- so an aim on the tile the fly is already standing on settles for SETTLE_FRAMES and reports done with the world exactly as it was. Section 12.2's trap in its own words, and exit_goals has excluded a settled goal underfoot since row 13: this was the one walk that did not have the rule. A completed errand walk also writes the reached ledger, which goals_toward does not filter, so the same button came back every hold: GO HEAL 204 starts at a mean net of 0.0 tiles and a mean reach of 0.0.
  • An errand is paid by a building this run has already been inside. areaVisited is session state, so a restore re-armed every errand in the town and walked the fly back to a counter it had already used -- section 13's own residual. MacroState::map_visited is the adapter's lifetime answer to the same question and it does survive a restore, so both are asked and either pays.

Nothing here changes which button the fly presses. The decoder, the reward catalog, the adapter version and the compatibility string are untouched.

12.18 A mart's counter is four screens, and one of them is the clerk talking (2026-09-22, row 55)

Minutes after v0.4.7 went live the watchdog flagged the narrowest loop yet: map 0x38, scene shop, sequence: [BUY ANTIDOTE], period 1, repeats 747, distinctMacros 1 over ten brain minutes, with BUY ANTIDOTE start then BUY ANTIDOTE blocked every 0.8 s and nothing else starting at all. Surveyed from the live checkpoint with a new probe mode (FLY_PROBE_CATCH=shop in examples/scene_probe.rs); the bytes are in docs/design/macros-wram.md section 7.1.

  • wListMenuID says the counter is open, not which of its screens is up. The mart prints its own text from inside DisplayPokemartDialogue_, which never calls the routine that clears that byte, so it holds PRICEDITEMLISTMENU for the whole visit. The frame the stream sat on was the clerk's "Here you are! Thank you!" box waiting for a press, and the seam called it the buy list; the cursor bytes on it belong to a two-option box (max 1). So which screen is up is now read from the figure the game draws -- the construction the dialogue box's waiting test and the YES/NO prompt already use. The clerk talking is ShopScreen::Talking, it reports no listing at all, and no purchase starts on it: a scene whose menu is not open offers what actually opens it, which here is CONFIRM and LEAVE, and both are already on the shop's pad.
  • A mart's buy list scrolls, so an item's position in the stock is its cursor index only for the first three entries. Walked one pulse at a time on the live list, the cursor went 0, 1, 2 and then stopped moving while the window scrolled under it. The offset that names the scrolled position is not a pinned address, so the fourth item of a counter and after have no index this seam can aim at. Pewter's counter carries seven items and its ANTIDOTE is the fourth: BUY ANTIDOTE aimed a cursor step at index 3 in a list reporting a max of 1 and returned Blocked on its own first frame, having pressed nothing -- three of three attempts, zero frames. A macro that cannot run is not on the pad, so the four purchases are bound on the rows the cursor can reach, and Viridian's four-item counter is why this went unseen: its ANTIDOTE is index 1.
  • A purchase has no target, so a blocked purchase records nothing. Section 12.1's ledger is keyed by what a walk set out for and a press sets out for nothing, so the button was dealt again on the very next hold, for ever -- row 6's shape in a scene with no walk in it. The fix is the precondition rather than a new ledger: the question the ledger would have answered is a fact about the counter, and the pad can ask it before the fly presses.

The loop was also the fly's own choice landing on the one button it could afford: the wallet read 104, so of Pewter's stock only the Antidote was under it, and BUY ANTIDOTE was the only purchase bound. Nothing about the choice changes here. What changes is that the button is not offered, and the two presses that leave a counter are.

Nothing here changes which button the fly presses. The decoder, the reward catalog, the adapter version and the compatibility string are untouched.

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

for the Pokécenter. heal should be a macro.")

Knowledge inside macros, never in the choice; the pad still lists buttons and the fly picks.

  • Errands. GO OBJECTIVE gains an errand list ahead of the rung's place: on a map whose area (the town the map belongs to, per geography) has a mart or a Pokémon Center this run has not yet entered, the objective target is that building's door first, then the rung's place. One visit per area per run (session ledger areaVisited(kind, area)), so the errand is paid once and never loops. Money below the cheapest purchase skips the mart errand.
  • GO SHOP / GO HEAL are the errands as buttons of their own too (populations macro_go_shop, macro_go_heal, from the same MBON/motor pool by the same rule): on the pad when the area's mart / center is unvisited (GO SHOP also needs money), walk to its door and in; inside, walk to the counter / nurse and face them.
  • Shop scene. Buttons are purchases: BUY POTION, BUY BALL, BUY ANTIDOTE, BUY REPEL, each bound only when the mart's stock (read from WRAM, shop_stock accessor, verified by the survey method) lists it and money allows at least one; each macro buys ONE unit through the real menus by reading the cursor; LEAVE closes the menu and walks out. A purchase marks nothing visited; the errand was paid on entering.
  • Center scene. HEAL is a macro: walk to the counter, face the nurse, talk, answer YES, wait for the heal animation to end (read the party HP back to full), close the box. On the pad only when at least one party member is not at full HP or has a status — verified against the live party on the cartridge (12.12), which is also what takes TALK at the nurse off the pad and what picks the one answer her YES/NO box is dealt. LEAVE walks out.
  • Ladder. Unchanged; no reward for shopping or healing (rewards are the adapter's, untouched).
  • Screen. Two new channel tags; the cells and the MACROS rate row take them as they come.
  • Proof. ROM-gated from the Viridian checkpoint: GO SHOP enters the Viridian mart and BUY POTION buys one; GO HEAL enters the center and HEAL restores the party; the errand is not offered again in Viridian; trap hunt before/after.

14. The full pad and four move buttons (the operator, 2026-09-17: "scoot fly over. make it 2

columns" … "four, one per move slot" … "2 cols.")

  • Screen. The strip under the game shows EVERY macro type at once as a two-column grid (rows as needed, 14 with the types below), the fly shrunk and scooted further left to make room. A cell is lit when the button is on the pad, dim when it is not, bright while its macro runs, with the outcome beat as before. Fixed order by type so a cell never moves.
  • Battle. ATTACK is replaced by four buttons MOVE 1 .. MOVE 4, one per move slot, each with its own population (macro_move_1..4), bound only when that slot holds a move with PP; the fly picks the move. No "best move" knowledge remains. SWITCH, ITEM, RUN (section 13.1 precondition) unchanged. Over an open move list the four buttons select their slot and confirm; with every move at 0 PP, MOVE 1 confirms the cursor so Struggle happens.
  • Types (27): GO OBJECTIVE, GO OUT, GO WARP, GO ROUTE, GO ITEM, GO NPC, GO FRONTIER, GO SHOP, GO HEAL, TALK, MENU, NEXT, YES, NO, CLOSE, CONFIRM, BACK, MOVE 1, MOVE 2, MOVE 3, MOVE 4, SWITCH, ITEM, RUN, BUY POTION, BUY BALL, HEAL, LEAVE (BUY ANTIDOTE / BUY REPEL fold into the shop scene's BUY buttons only if the grid has room; otherwise they stay as purchases reachable through BUY POTION's script? No: keep them as types; the grid takes rows as needed).

Section 14 layout, decided 2026-09-17 (the operator: "your rec is good"). The full two-column grid under the game does not fit at the 24 px text floor (31 types need about 410 px; the strip has 172), so: the strip under the game shows every button ON THE PAD NOW, up to 14 as two columns of seven at the floor, tag and short name, the running one bright; a new rail tab MACROS shows the whole keyboard, three columns of eleven, bound cells lit and unbound dim, the running one bright. The game stays at 5x; the afterglow row stays. The population re-deal that came with the nine new types is accepted.

13.1 The full complement, scene by scene (the operator, 2026-09-17: "check to make sure each

scene has the full complement of options. we run away a lot." … "sometimes the macro buttons

disappear and everything just hangs there.")

Every scene and sub-state audited against section 12's table and against the game's real options there. "Read from WRAM" is the standing rule for a new button: a precondition this crate cannot observe is not a precondition, it is a guess.

scene / sub-state pad change
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 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 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 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. Since 12.12 TALK is not on it at a nurse the party has no use for: her conversation is a service whose need the cartridge publishes, and a ring of text that ends where it began is section 12.2's trap
Dialog, a plain text box NEXT, YES, NO A and B both advance a plain box, so all three are dealt for one — what it buys is the fly being able to answer no. Forty-five of the nurse's forty-six frames are this row (12.12)
Dialog, a readable YES/NO box YES, NO — or one of them at a Pokémon Center's nurse new, 12.12. NEXT is off it: an A press at a two-option menu confirms the option the cursor is on, which is what YES is, so the two are one press under two names (12.10). At the nurse's own prompt the bound answer is the one that changes something — YES with a hurt or statused party, NO with a full one. An answer whose prompt comes straight back is excluded for the blocked window, and the exclusion never empties the pad
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
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 (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 -- 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, 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, between turns NEXT BACK was added here for the bag and taken back out by 12.9: on a frame of battle text there is no list to leave, and a BACK that changes nothing is the trap of section 12.2. Since 12.10 the bag is not on this row at all, so NEXT here is only ever the A that advances text
Shop BUY POTION, BUY BALL, BUY ANTIDOTE, BUY REPEL, CONFIRM, LEAVE two purchases to four; CONFIRM added
PC CONFIRM, LEAVE CONFIRM added: a list the fly opened is one it can answer rather than only close. Depositing and withdrawing are still not in the vocabulary (row 17)
Title nothing unchanged, by contract: the readout's boot variant applies

RUN is bound only in a wild battle the fly is losing. Two readings, both from the cartridge's own numbers: the Pokémon that is out is under a third of its HP with no healthier reserve to send in (the same reserve SWITCH chooses by, so the two buttons cannot disagree about whether one exists), or every move it has is out of PP with nothing to switch to, where Struggle is a countdown. Anything else and the button is off the pad. Knowledge inside the macro, as a precondition: nothing ranks RUN below MOVE 1, it simply is not there while the fight is worth having — and a fled battle pays nothing and teaches nothing.

LEAVE in a shop closes the menu and does not walk out, and that is a deliberate limit rather than the section's words. A scene-spanning walk would have to be planned while the screen buffer holds the mart's menu, so every tile of it reads Walkable::Unknown and the route is a guess over ground the search cannot see. GO OUT is on the mart's floor pad one press later and is the same walk with the map on screen. Stated here so nobody has to rediscover it.

Sections 3 and 12 had two dealers. Palette::for_scene was section 3's fixed six-channel table and plan::plan_for was section 9's ordering, and macros mode asked for the second because it was the one that knew GO OBJECTIVE's rung. They could disagree about which buttons a scene had, and did: GO OBJECTIVE was on the plan's overworld and on no fixed row for two releases. Section 14 removes the last reason they differ — with a slot per type there is nothing to order and nothing to truncate — so palette::scene_set is the table above and plan_for is for_scene.

Every way the pad can be empty

An empty pad in a playable scene is the one state the doctrine cannot recover from on its own: nothing presses for the fly, so a scene with no button waits, and waiting is correct behaviour that looks exactly like a hang. So it is enumerated, closed where a macro can close it, and measured where it cannot.

cause closed by
a dialog whose one answer the reopen ledger excludes not a cause (12.12): the exclusion narrows a pad and never empties one, so a box with both answers excluded is dealt both again. A box nothing can answer is a screen nothing can leave
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
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 restore, before the first observe not a cause: the loop calls observe once at the end of boot and once after a ratchet recovery, so the first frame is decided on a real palette
the title screen, and raw mode not an empty pad by contract: the readout's boot variant applies and no palette is dealt
a scene the detector cannot name Unknown deals NEXT and BACK; a screen neither press leaves (the naming screen, row 14) is a genuine stall and still needs START, which is a contract change
an Unknown frame that is the overworld with the cartridge driving -- a warp in flight, a scripted push-back, a guide walking the fly through a door empty on purpose (12.13). There is no box to advance and no screen to leave, so an A or a B press is a press into somebody else's script: it changes nothing and completes where the fly stands. This is the one empty pad that ends itself -- the cartridge gives the buttons back within a few frames -- and game.padEmptyMs reports it like any other
the fly's own turn where the seam cannot read the battler, now that NEXT is off that row (12.10) fixed: MOVE 1 is bound over the top-level menu whatever wBattleMon* reads as, because FIGHT is one of that menu's four entries and always opens

And because "closed where a macro can close it" is not "closed": game.padEmptyMs publishes how long a playable scene has had nothing on the pad, in brain milliseconds, 0 otherwise; a running macro counts as not empty, because it owns the pad, which is the honest reading of what the audience is looking at. fly-watchdog exports it as fly_pad_empty_seconds. Both are report only: nothing in the loop reads it back and nothing presses a button because of it.

14.1 One slot per type, and what the four move buttons removed (2026-09-17)

Section 14's screen asks for every macro type at once, and the battle's own turn wants nine buttons. Six slots was section 1's D-pad and A/B, and it stayed a cap on how many buttons a scene could deal long after section 12 stopped a macro being a meaning laid over a button. A slot is now a type index: thirty-one slots, the screen draws every cell in the fixed order and lights the ones this scene binds, the wire carries the bound ones with their type index as slot, and nothing truncates anything. The decoder never cared — it competes among the bound channels.

That retires two fixes: row 12 (GO FRONTIER rescued from the truncation) and row 18 (MENU on no pad at all) are both about a cap that no longer exists.

  • Types (31), in channel order: GO OBJECTIVE, GO OUT, GO WARP, GO ROUTE, GO ITEM, GO NPC, GO FRONTIER, GO SHOP, GO HEAL, TALK, MENU, NEXT, YES, NO, CLOSE, CONFIRM, BACK, MOVE 1, MOVE 2, MOVE 3, MOVE 4, SWITCH, ITEM, THROW BALL, RUN, BUY POTION, BUY BALL, BUY ANTIDOTE, BUY REPEL, HEAL, LEAVE. GO STAIRS is GO WARP (tag MB·WARP): stairs, ladders and the doors between rooms are all the same move and the old name only described one of them.
  • MOVE n is bound when slot n holds a move with PP. MOVE 1 is also bound when nothing anywhere has PP, because Struggle is what the cartridge does then and the only way to it is to choose a move anyway (rows 30a and 34); its script confirms wherever the cursor stands rather than moving it onto some other spent move. Over the main menu a move button presses FIGHT and then its slot; over an open list it goes straight to the slot. The best_move knowledge, the ROM move table and the type chart are removed, not narrowed: which move is used is the fly's choice and the mushroom body's to learn, and that was the point of the four buttons.
  • THROW BALL ("throw pokeball should be a macro") is on the own-turn pad in a wild battle when the bag holds a ball of any kind and the party has room. The script opens ITEM and moves the cursor to the ball's own bag index by reading the list; the throw, the shakes, the result text and a caught Pokémon's nickname prompt are the between-turns NEXT's and the dialog's NO. No catch rate and no HP: when to throw is the fly's. A full party takes the button off the pad even where a PC box would have taken the catch, because this crate has no reviewed symbol for the box count — the ordinary narrowing, said out loud.
  • Populations. Thirty-one macro_<type> roles, cut from the same 96 MBONs plus 110 brain motor neurons by the same round-robin: seven or six per type. 206 neurons dealt thirty-one ways is not twenty-two ways plus nine, so every population was re-dealt, exactly as it was when section 11's amendment went from six slot channels to twenty-two type channels. The neuron ids, the edges and the kernel are untouched and rates are restored by role name, so --print-compatibility is byte-identical (648 bytes) and the live checkpoint carries over: the new and renamed roles start at zero, and what a run loses is the mushroom body's learned preference for particular macros. circuit-roles.json regenerated with tools/build_flywire.py --macro-roles, checksums updated.
  • Tags. MB·MV1..MB·MV4, MB·BALL for THROW BALL, MB·WARP, MB·SHOP, MB·HEAL, MB·NURSE for the conversation at the counter, MB·ANTI, MB·REPEL, and BUY BALL becomes MB·PBALL: the ball the fly throws takes MB·BALL, and two channels cannot share a tag on a screen that identifies a cell by it.

14.2 The grid was reverted, then the layout was decided

Built, measured, and taken out again. The two-column grid of all thirty-one types needed 11 px type — the widest name and tag are BUY ANTIDOTE and MB·NURSE, and at 24 px they want two columns of 346 px in the strip's 364 and 272 px of height in its 208 — and this page's floor is 24 px at the 0.31 phone downscale. A floor with an exemption in it is not a floor, so the strip is section 12's again: six rows, the scene's bound macros in type order, 24 px, and apps/stage/src/lib/geometry.ts is byte-identical to what it was.

What section 14 keeps is the part that is not about pixels: thirty-one types, a slot that is a type index on the wire, and a pad that binds as many as the scene has. The strip is now narrower than the pad, and the cost is real and asserted rather than hidden:

  • a centre binds nine macros and HEAL is type 29, so the strip cannot show it. The audience sees the fly heal in the ticker and not the button it pressed (apps/stage/tests/e2e/macros.spec.ts, "the mart deals purchases, and the centre deals HEAL").
  • the battle's own turn binds nine too, so two of MOVE 1..MOVE 4, THROW BALL, RUN or NEXT are off the strip whenever all nine hold.

And then it was decided (the operator, 2026-09-17: "your rec is good"), which is the paragraph at the end of section 14 above: the strip under the game shows every button on the pad now, up to fourteen as two columns of seven at the floor, tag and short name, the running one bright; and a new rail tab, MACROS, shows the whole keyboard as three columns of eleven with the bound cells lit and the unbound dim. The game stays at 5x and the afterglow row stays. That is what is built; this section stays because the arithmetic behind the decision is the measurement above, and because the six-row strip is what the branch shipped for an afternoon in between.

14.3 What the grid cost while it existed, for the record

Two consequences of drawing all thirty-one cells at 1920x1080, both measured in the built page rather than argued:

  • The 24 px type floor was broken inside the grid, and only there — which is why it is gone. The widest name and tag are BUY ANTIDOTE and MB·NURSE; at 24 px they want two columns of 346 px in the grid's 364 and 272 px of height in its 208. 11 px is the largest that fits. apps/stage/tests/e2e/text-size.spec.ts now measures the grid against MACRO_CELL_FONT_SIZE and holds 24 px everywhere else — a documented exemption for one region with the arithmetic behind it. On-screen copy is the operator's, and an exemption is not this branch's to grant, so it was reverted.
  • The fly was not scooted over. Section 14 asks for it and the grid cannot move left of x = 480 without putting text into the bottom-left no-content zone (0,900 → 480,1080), where Twitch overlays chat: taking 80 px off the fly would widen a column 179 → 219 and the type 11 → 14 px at the price of nine of the sixteen rows' glyph chips sitting under the overlay. What the grid was short of was height, and the button row's 36 px band is where that came from (the button row now spans the fly's 416 px column rather than the strip's 792). Two constants reverse it if the type matters more than the overlay.

LAYOUT.macroPalette was {x: 480, y: 816, w: 364, h: 212} while the grid existed — one box moved, the strip's top edge from 852 to 816 — and it is back to {x: 480, y: 852, w: 364, h: 176}. Nothing in the layout differs from main.

15. Map-aware walks: one plan over the whole map (the operator, 2026-09-22: "the frontier and

warp macros need to be map aware: A* over walkable tiles.")

Section 4 has said "A* over the current map's walkable tiles" since the first draft, and it was never that. The walkable predicate answers for the ten tiles by nine of the screen buffer and Walkable::Unknown for everything else (docs/design/macros-wram.md), so every walk in the game planned through guesses at eight times the price of a known tile, re-planned at every window edge, and GO FRONTIER aimed at whatever unstood ground was on screen -- never the far side of a town, which nothing could see.

What changes is where the tile ids come from. The rule does not change at all: a tile is walkable when the current tileset's collision list holds its id, which is CheckTilePassable.

  • The whole map is decoded from the tables the cartridge has already loaded: the block ids out of wOverworldMap, the block-to-tiles blockset out of the tileset header, the collision list as before, and the TilePairCollisionsLand pairs as directed walls. MapGrid is every tile of the loaded map with those walls, and docs/design/macros-wram.md section 9 is the byte-level evidence, the new addresses and the verification.
  • One read of a ROM bank was needed, so MemoryReader gained one method. The blockset does not live in bank 0, and the only way to reach another bank through the CPU bus would be to write the mapper's bank register. MemoryReader::read_rom(bank, address) reads the cartridge image the process already holds instead: the same bytes, addressed the way the disassembly addresses them, and no write into a running game. The joypad register is still the only write (section 12).
  • path::route and path::frontier plan over the grid, and the plan is the same A*: one step per tile, the multi-goal heuristic, the committed route of section 12.3, re-planned only on a refusal or a displacement. GO WARP, GO OUT and GO ROUTE route to their warp or connection tile across the whole map; GO OBJECTIVE takes the exit that is the first hop; the frontier is the nearest unstood walkable tile anywhere on the map, by the stood ledger of section 12.7.
  • The window stays as the fallback, and it says when. A frame with no grid falls back to the ten-by-nine reading exactly as before, and GridRefusal names which of the five reasons it is: no map header, no player, no collision list, no blockset (which is what a reader with no cartridge behind it answers), the map not on screen, or the decode disagreeing with the screen. Nothing is guessed and nothing silently degrades.
  • The grid is checked against the screen before it is trusted, and again whenever it is served. The decode is compared with the window predicate over the tile the fly is standing on and its four neighbours, and a frame where the window can answer for none of them is refused — the block data shares its bytes with the picture buffer, so a battle is exactly when it belongs to somebody else. Serving a cached grid re-checks the fly's own tile, because a warp writes the map id before the header and the blocks: for a frame or two on a doormat, wCurMap is the map the fly is arriving on and the blocks are still the map it is leaving.
  • Cached per map, decoded once on arrival, dropped when the map or its size changes. Session state beside the talked, blocked, reached, stood and errand ledgers; never checkpointed, so a restored run decodes the map again on its first overworld frame.
  • The probes report it. examples/scene_probe.rs prints the grid's size, its walkable count, the count reachable from where the fly stands and the count never stood on, draws the ground with the reading it used, and names the refusal when there is none; examples/trap_hunt.rs carries the same line into every trace line and into the summary table. Three numbers read a stalled walk at a glance: a fly with forty walkable tiles and four reachable ones is fenced in, and no amount of re-planning will help it.

Nothing about the choice moves. The scene deals the same buttons, the readout presses them, and what changed is what a chosen walk knows about the ground — which is where section 12 puts knowledge. The decoder, the reward catalog, the adapter version and the compatibility string are untouched: 648 bytes, 0d9bfde7…707fa, byte-identical across the change.

Two things the cartridge settled rather than the design, both measured and both written up with their bytes in docs/design/macros-wram.md section 9: the collision id of a map tile is the lower left of its four screen tiles and not the upper left, and a warp writes the map id before the map, which is what the per-serve check above is for.

15.1 The proof

  • Unit tests, no cartridge. The decoder against a made-up tileset: a block's four quadrants, a collision list over a map wider than the window, a tile-pair collision as a wall in both directions and not in another tileset, a block id past the end of the blockset staying Unknown, and the reachable count over a fenced region. The reader against synthetic WRAM with a synthetic blockset in a synthetic bank: the whole map decoded, a screen that disagrees refused, a reader with no cartridge refused, a battle frame refused, and the cache holding one map. The search over a grid: one plan across a map larger than the window where the window's own plan walks into a wall it cannot see, a frontier beyond the window where the window's frontier is empty, a tile-pair wall planned around, and a connection whose walls are not goals.
  • ROM-gated, from the release container's own checkpoints (tests/rom_map_grid.rs, FLY_ROM plus a checkpoint, skipped cleanly without either). On Pallet Town — 20x18, 221 walkable tiles, 207 of them reachable, none unknown — and on Viridian Forest — 34x48, 719 walkable, all reachable, none unknown: the decode agrees with the window predicate on all ninety tiles the window can answer for, and it agrees with a survey of real presses on every one of the first 120 tiles the walk can stand on (58 refused presses on the town, 74 in the forest, every one of them a wall, a directed wall or a tile a sprite was standing on; 10 presses in the forest that the cartridge answered with a battle, which say nothing about the ground either way). GO FRONTIER in the forest planned to ground outside the window and walked there in 215 frames (615 of its frontier tiles are outside the window); every way out of the forest is one plan away, 27 to 149 steps, with no guessed tile in any of them.
  • The trap hunt, twenty brain minutes from the rung-9 checkpoint, against main at v0.4.2: 286 distinct (map, tile) become 489, the median flagged window holds 55 tiles instead of 16, GO FRONTIER runs 64 walks worth up to twelve net tiles instead of four worth one, and one walk spends its cap where six did. It costs battle: flagged windows go 61 to 70 and windows under four tiles 5 to 14, every one of them a window spent inside a battle, because a fly that covers more ground walks into more grass. infra/docs/macros-traps.md has both runs whole and says so at length; this is the one measurement in this file where more ground and fewer flags do not both hold.