diff --git a/docs/design/macros.md b/docs/design/macros.md index d4aac9a..ef0d405 100644 --- a/docs/design/macros.md +++ b/docs/design/macros.md @@ -806,6 +806,59 @@ list, whose only bound button with one Pokémon is `BACK`. Six hundred thousand 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. + ## 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.") @@ -883,12 +936,12 @@ observe is not a precondition, it is a guess. | Menu (the start menu) | CLOSE, CONFIRM, BACK | unchanged. The cursor is the fly's to move with the **raw** D-pad, which still reaches the cartridge in macros mode; a SAVE or a POKéDEX button would be a macro per start-menu entry and is not asked for | | Menu (the 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, NEXT | four move buttons for `ATTACK` (section 14); THROW BALL added; **RUN gated**, below | +| Battle, own turn, main menu | MOVE 1..4, SWITCH, ITEM, THROW BALL, RUN, NEXT | 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 | | Battle, own turn, move list | MOVE 1..4, BACK | as above | | Battle, own turn, party list | SWITCH, BACK | unchanged | | Battle, the bag | NEXT, **BACK** | **BACK added**, and the list now reports a *cursor* (`macros-wram.md` 7.1) — before this `ITEM` could open the bag and had nothing to read | | Battle, forced switch | SWITCH, NEXT | unchanged (row 8) | -| Battle, between turns | NEXT, **BACK** | **BACK added** | +| Battle, between turns | NEXT | `BACK` was added here for the bag (the row above reads as nobody’s turn) and **taken back out by section 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 | | 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 | diff --git a/infra/docs/macros-traps.md b/infra/docs/macros-traps.md index ca58158..9e5918e 100644 --- a/infra/docs/macros-traps.md +++ b/infra/docs/macros-traps.md @@ -1230,3 +1230,132 @@ somewhere else, and the script fires on any frame the fly stands there. so the only thing said about it here is that it exists and is exported. Whether a playable scene ever deals nothing on the release box is the watchdog's gauge to answer, over a run longer than twenty brain minutes. + +## 2026-09-22, rows 38 and 39: `BACK` between turns, and a ball at what the party already has + +Rank 9 (VIRIDIAN FOREST, next PEWTER CITY), **sixty-nine hours** on the rung, ratchet attempts 3 of +3 spent, the fly on Route 2 and in the forest and mostly in wild battles. Since the last 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 +``` + +`docs/design/macros.md` section 12.9 is the contract this closed against. Both traps are section +12.2's one rule — *a macro that completes without moving because its precondition is already +satisfied where the fly stands* — and the reproduction is the hunt from the release container's own +checkpoint, twenty brain minutes, the real brain as the readout. + +### The mechanism + +Three facts, and the first one is the whole of it. + +1. **`BACK` was half of the pad the fly spends a wild battle looking at, and it could not change + anything.** The top-level battle menu never dealt it: `scene_set`'s own-turn arm is + `MOVE 1..4, SWITCH, ITEM, THROW BALL, RUN, NEXT` and B is not a fifth answer to a four-entry + menu. What dealt it was the **between-turns** row, `Scene::Battle { own_turn: false, .. }`, 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 on that arm — and which is *also* every frame of battle text, every + animation and every turn resolving. On those frames nothing is open, so `BACK`'s single B press + changes nothing the `NEXT` beside it does not: measured at **mean 16 frames, 1.0 tiles, net 0**, + 163 starts between turns plus 12 more on a move list whose cursor the seam cannot place, out of + 959 macros in twenty brain minutes. Half a pad of two, once per hold, while the turn did not + move. +2. **`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`. Measured: ten + `RUN` starts, ten `Blocked`, **mean 184 frames**, every one of them started on the top-level + menu. So a `RUN` that won a hold as the menu closed spent three brain seconds pressing nothing + and gave up. That is **row 19**, bounded and left, and the only reason it was legible in the + event log is the `BACK` above filling the frames on either side of it. `RUN`'s precondition is + unchanged (13.1: only a wild battle the fly is losing). +3. **`THROW BALL` had no reading of what it was throwing at.** Its three facts were a wild battle, + a ball in the bag and room in the party, and none of them is "this is not one I already have". + Viridian Forest holds five species; a throw at one the party carries spends a ball for nothing, + and a catch opens the nickname screen, which reads `Unknown`, needs the START the pad has no + button for, and is the one screen neither `NEXT` nor `BACK` leaves (**row 14**). So the trap is + paid for twice. + +| # | trap | trigger | test | fix, or why it is left | +| ---: | --- | --- | --- | --- | +| 38 | `BACK` on a battle pad with no list open completes without changing anything, once per hold | every frame of battle text, every animation, every turn resolving — which is most of a wild battle | `back_is_on_a_battle_pad_only_where_a_list_is_open`, `a_battle_frame_that_is_not_the_players_turn_binds_next_to_advance_its_text`, `the_battles_turns_advance_from_the_rung_nine_forest_checkpoint` (ROM-gated) | **fixed**: the between-turns row is the sub-state's, like the own turn's — `NEXT, BACK` with the bag open, `NEXT` alone otherwise. The move list and the party list keep `BACK`, because backing out of a list is one of exactly two answers to one, and the top-level menu never had it | +| 39 | `THROW BALL` throws at a species the party already holds | any wild battle after the first catch of that species | `throw_ball_refuses_a_species_the_party_already_holds`, and the ROM-gated run above asserts it never happens on the cartridge | **fixed**: a fourth fact in the precondition. The **party** is the caught set — the cartridge's own lifetime record, in the same internal species numbering the enemy is read in. `wPokedexOwned` is *not* asked: 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, so it would be an unverified number (`docs/design/ladder.md`). It costs nothing measurable, because the button is already off the pad while the party is full and nothing in the vocabulary deposits into a box (row 17). An enemy the seam cannot place leaves the button where it was | +| 40 | the ratchet cannot help on a rung sixty-nine hours old | `budget_spent()` precedes both triggers and three attempts on rank 9 were spent | `ratchet.rs`'s own tests ("three attempts per rank") | **left, contract**, and this is row 22 measured again: no recovery can fire until the rank *improves*. What carries the run is the road, and the road is checked rather than assumed — the objective is the lowest **unearned** rung (12.4), which is rung 10's `PEWTER_CITY`; `geography::next_hop` from the forest answers `VIRIDIAN_FOREST_NORTH_GATE` (47), then Route 2's north piece, then Pewter (2), asserted hop by hop since 12.7; and the ROM-gated run reads `GO OBJECTIVE` on the overworld pad of maps 13, 50 and 51. **The run does not reach map 47** in 67 brain minutes of the game-blind rotation, so that is reported and not asserted | +| 41 | the nurse's box answered `YES` four hundred and seventy-four times | measured in the **before** arm only: from 11.5 brain minutes the fly stood on one tile of a Pokémon Center and the windows read `YES` x21 to the end of the run | — | **named, not worked**: 23,548 of the before arm's 71,673 frames were a text box on map `0x29` at (3, 3), 474 of them answered `YES`, and the run ended there. The after arm never enters it — `dialog` frames go **23,548 → 0** — so it is neither reproduced nor fixed by this branch, and it is *the next thing to measure*. Rows 1, 2b, 23 and 24 were each found this way: a loop behind the loop in front of it | + +### The trap hunt, before and after + +Twenty brain minutes, seed 20260917, 4 sweep threads, same connectome and same cartridge, from the +release container's rung-9 checkpoint. **Driven by the brain rather than by `FLY_TRAP_STUB`**, which +is legitimate here and was not for sections 13 and 14: this branch adds no macro type, so +`tools/build_flywire.py` re-deals nothing, the thirty-one `macro_` populations are the same +function in both arms, and `--print-compatibility` is byte-identical at 648 bytes. The two arms +differ in the macro code and in nothing else. + +```sh +FLY_ROM=".../pokemon-red.gb" FLY_MACRO_BRAIN=data/fafb-v783 \ + FLY_TRAP_CHECKPOINT=.local/checkpoints/ \ + FLY_TRAP_MINUTES=20 cargo run --release -p flysim --example trap_hunt +``` + +| measure | before (v0.4.1) | after (`fix/loop-20260922T0254`) | +| --- | ---: | ---: | +| distinct (map, tile) over 20 brain minutes | 216 | **286** | +| windows flagged | 70 of 73 | **61 of 73** | +| windows under 4 distinct tiles | 21 | **5** | +| macros started | 959 | 830 | +| done / blocked / timeout | 873 / 80 / 5 | 726 / 97 / 6 | +| frames in `battle` | 27,525 | 48,756 | +| frames in `dialog` | 23,548 | **0** | +| `MOVE n` starts | 36 | **68** | +| `BACK` starts with **no list open** | **175** | **0** | +| `THROW BALL` starts | 47 | 46 | + +**Both of `docs/loop-review.md`'s two criteria improve**: a third more ground (216 → 286 tiles) and +nine fewer flagged windows, with the windows that hold fewer than four tiles down from 21 to 5. The +pad tables the hunt gained for this work are the direct reading: + +| battle sub-state | pad before | pad after | +| --- | --- | --- | +| main menu | `move_1..4 next run throw_ball` | unchanged | +| move list | `back move_1..4` | unchanged | +| move list, no cursor | `back next` | **`next`** | +| party list | `back` | unchanged | +| between turns | `back next` | **`next`** | + +Four things about it that are not improvements, recorded rather than buried. + +- **`BACK` is still 359 starts of 830**, all of them over an open list: 306 on the move list and 46 + on the party list, which with a party of one is `BACK` alone by design (row 34 — "what changed is + where `BACK` leads: a menu with `ATTACK` on it"). The trap was the *listless* `BACK`, and that is + the row that went to zero. +- **A `NEXT, BACK` two-cycle closes the run**, x75 over the last four brain minutes inside one + battle: the move list opens before its cursor can be placed, `NEXT` advances, the cursor appears, + `BACK` closes the list, and round again. It is bounded — the battle is real, `MOVE n` wins holds + in it, and the turn does move — and it replaces a `BACK` x13 cycle in the same windows of the + before arm. Worth watching; the honest fix is a move list whose cursor the seam can place on its + opening frames, which is a WRAM reading and not a pad change. +- **A window spent in a battle is flagged by the tile rule whatever happens in it.** A battle does + not move the fly, so `tiles = 1` is what a long fight looks like from outside, and the after arm + spends 48,756 of 71,673 frames in one. That is the known false-positive shape of the *tile* rule, + the mirror of the sequence rule's legitimately-repeating explorer, and it is why the pad and + sub-state tables above are what this row is proved on. +- **The `THROW BALL` precondition is inert in this run and is still right.** The save carries a + party of **one** — the starter — so no forest species is ever in it and the new fact never fires; + the 46 starts are all `Blocked` at mean 67 frames, which is row 19's cursor wait again. What the + ROM-gated test holds is the invariant (`threw_at_a_held_species` is never set), not a drop in the + count. The live 28 starts were the same shape. + +### Gates + +- `cargo test --workspace` with `FLY_ROM` set: green except + `flysim::integration::the_service_streams_takes_sugar_checkpoints_and_resumes_after_being_killed`, + which fails **identically on v0.4.1** on this box with the same assertion (`/status` reports + `total: 1` with the service still on `BOOT` 54 s after start, where the test wants the 38-rung + ladder). A debug build of the service does not finish booting inside the test's window here. + Pre-existing and unrelated to the macro layer; measured on both sides rather than assumed. +- `cargo clippy --all-targets`: clean. +- `infra/tests/lint.sh`: all checks passed, de-PII guard included. +- `--print-compatibility`: byte-identical to v0.4.1, 648 bytes, decoder / reward catalog / adapter + version / roles untouched.