macros.md gains 12.18 and macros-wram.md a section 10 for the accessor and the survey that found it: press at every battle frame with a rollback pulse, and ask every byte of WRAM and HRAM which of them separates a honoured press from a refused one. The half of row 50 that was wrong is where the fix is. The move list was not drawn: MoveSelectionMenu's cursor bytes are never cleared and SelectMenuItem decrements wCurrentMenuItem back into the one-based range on its way out, so a turn's whole text and animation read as an open list with a placeable cursor. The battle bag is the same trap on wListMenuID and is named rather than fixed, because the figure that tells its list from the frame after it closes has not been surveyed yet.
1589 lines
127 KiB
Markdown
1589 lines
127 KiB
Markdown
# 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`)
|
|
|
|
```rust
|
|
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_event`s 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
|
|
|
|
```rust
|
|
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 menu is up while its box is on screen, not while its cursor bytes say so (2026-09-22, row 50)
|
|
|
|
The largest thing left inside a battle after 12.17: `MOVE n` reported `blocked` **890 times in
|
|
1,431 macros** from the rung-9 forest checkpoint, `MOVE 4` **222 of 224**, and 82% of a fixed run's
|
|
frames were battle time with one battle running 30,809 of them. Row 50 called it "the move list
|
|
drawn and its cursor placeable but not accepting input". Half of that turned out to be wrong, and
|
|
finding out which half is the whole fix.
|
|
|
|
- **The cursor bytes outlive the list, so the list was not drawn at all.** `MoveSelectionMenu`
|
|
writes `wTopMenuItemY` 12 and `wTopMenuItemX` 5 and **nothing in the game clears them**. That is
|
|
the same fact 12.12 rested the YES/NO box on — "the cursor bytes survive the box closing" — and
|
|
the reason the *top-level* battle menu never had it is that `wTextBoxID` = `$0b` sits beside its
|
|
geometry and is written by somebody else. `SelectMenuItem` then decrements `wCurrentMenuItem` back
|
|
into a 0-based move slot on its way out, which lands inside the one-based range the accessor reads
|
|
as valid, so a turn spent on move 2, 3 or 4 leaves a *placeable* cursor behind it. Every frame of
|
|
the text, the animation, the damage and the enemy's reply read as the fly's own turn on an open
|
|
move list. The pad dealt `MOVE 1..4` and `BACK` on all of them, the roll landed on one, and the
|
|
cursor step pressed at a list nobody was reading until its budget ran out. That is section 12.2's
|
|
trap wearing 12.6's clothes: a macro whose precondition is satisfied where the fly stands.
|
|
- **So a menu is up while its box is on screen.** The reading is the figure `MoveSelectionMenu`
|
|
draws — a box at (4, 12) fourteen wide, with a horizontal run over its top-left corner and the
|
|
`┘` junction over (10, 12) — read whole, exactly as `text_box`'s `waiting` and `yes_no_prompt`
|
|
are. `docs/design/macros-wram.md` section 10 has the accessor.
|
|
- **It was surveyed by pressing, not by nominating a flag.** `examples/scene_probe.rs`,
|
|
`FLY_PROBE_CATCH=accept`: on every battle frame the emulator exports its state, one directional
|
|
pulse is issued, `wCurrentMenuItem` is read and the state goes straight back, so every frame has a
|
|
ground truth and the run is not perturbed by the measurement. By the cursor bytes alone a press
|
|
was honoured on **264 frames of 3,102**; by the cursor bytes and the box on **231 of 231**. Beside
|
|
it every byte of WRAM and HRAM was asked whether it separates the two classes, so a reading was
|
|
found rather than guessed — nothing separates once the box is in it, which is what "exact" means
|
|
here.
|
|
- **A frame whose list is not on screen is between turns**, whose pad is the one `NEXT` that
|
|
advances text (12.10). No pad gains or loses a button anywhere else: the top-level menu, the party
|
|
list, the bag and the forced switch keep exactly the rows 13.1 gives them, and which move the fly
|
|
uses is still the fly's.
|
|
- **The bag is the same trap and it is named rather than fixed.** `wListMenuID` = `ITEMLISTMENU`
|
|
outlives the bag as surely as the cursor bytes outlive the move list: over the frames the seam
|
|
calls an open battle bag, the same survey refused **449** presses against 36 honoured. The bag's
|
|
list is drawn in the top half of the screen and the survey has not yet found the figure that tells
|
|
it from the frame after it closes, so `ITEM` and `THROW BALL` still pay for it and that is
|
|
reported. A reading this crate cannot verify does not go in (`docs/design/ladder.md`).
|
|
|
|
The decoder, the reward catalog, the adapter version, the roles and the compatibility string are
|
|
untouched.
|
|
|
|
## 13. Shops and Pokémon Centers (the operator, 2026-09-17: "refactor the shop macros. make it a
|
|
## 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 (**the box on screen**, 12.18) | 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 | since **12.18** this row is most of a battle, and correctly so: a frame whose move list is remembered rather than drawn lands here. `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.
|