From bffbaca394b0a646478bc0c5b8196dd6256538e7 Mon Sep 17 00:00:00 2001 From: acamilo Date: Tue, 22 Sep 2026 17:19:25 +0000 Subject: [PATCH] docs: row 54, the mid-step survey and both arms of the hunt macros.md gains section 12.17; macros-wram.md section 9 gains the frame-by-frame trace of a step, the three readings that come out of it and the one byte that tracks a step exactly and is recorded unused; macros-traps.md closes rows 54 and 54b, strikes the two residuals they worked, and prints both checkpoints' before and after whole -- including the arm where the ethos check's "fewer flagged windows, more distinct tiles" does not hold, and why. --- docs/design/macros-wram.md | 41 ++++++++++++++++ docs/design/macros.md | 57 ++++++++++++++++++++--- infra/docs/macros-traps.md | 95 ++++++++++++++++++++++++++++++++------ 3 files changed, 174 insertions(+), 19 deletions(-) diff --git a/docs/design/macros-wram.md b/docs/design/macros-wram.md index 0c721a3..3b55030 100644 --- a/docs/design/macros-wram.md +++ b/docs/design/macros-wram.md @@ -649,6 +649,47 @@ is exactly the shape of mistake the cross-check below exists for. both are the old map — so only the *id* is wrong, and the check that catches it is one byte: does the cached grid still agree with the screen about the tile the fly is standing on. +### The frame mid-step, which the check refused (2026-09-22, row 54) + +The cross-check above refused on **every frame the fly was moving**, and the reason is a fact about +when the cartridge writes the coordinates. `FLY_PROBE_CATCH=step` in +`services/flysim/crates/flysim/examples/scene_probe.rs` holds one direction from a checkpoint and +prints, per frame, the coordinates, the grid's verdict, the tiles the two readings disagree on, and +every plausible candidate for "a step is in progress". Holding UP out of the Pewter museum: + +| frame | `wYCoord` | the grid | the disagreement | +| ---: | ---: | --- | --- | +| 0 | 7 | ok | -- | +| 1 | 7 | ok | -- | +| 2 .. 15 | **7** | screen disagrees | (10, 7) decoded `$20`, screen `$01`; (11, 7) `$20` against `$01` | +| 16 | **6** | ok | -- | +| 19 .. 32 | 6 | screen disagrees | (10, 7) `$20`/`$01`; (11, 6) `$01`/`$50` | +| 33 | 5 | ok | -- | + +Three readings come out of it, and the first is the one everything else follows from: + +1. **The coordinates change at the *end* of a step.** A step is sixteen frames; `wYCoord` reads the + tile it began on for all of them but the last. The background scrolls throughout, so from the + second frame the screen buffer is already centred one tile ahead. +2. **`map_tile_id(x, y)` therefore answers for `(x + dx, y + dy)` mid-step**, where `(dx, dy)` is + the step. Verified on both arms of the trace: at frame 19 the screen's reading for (10, 7) is + the decode of (10, 6) and its reading for (11, 6) is the decode of (11, 5), exactly. +3. **No pinned address says a step is in flight.** The player sprite's Y and X step deltas + (`wSpriteStateData1 + 3` and `+ 5`) keep their last value after the step ends -- `$ff, $00` on + every frame of the trace after the first -- so they cannot tell a step from the one before it. + `wStatusFlags5` stayed `$00`, `wMovementFlags` tracked the warp tile the fly was standing on and + not the step, and `rSCY` lags the coordinates by a frame of its own. The one byte that does + track it exactly -- counting `$07 $07 $06 $06 … $01 $01` down to `$00` on the frame the + coordinates catch up -- is **`$cfc5`**, and `gen_symbols.py` refuses a hand-written address while + the checkout `resolve_wram.py` reads is not on this box. So it is recorded here and **not used**. + +What the reader does instead is measure the anchor: the screen is centred on the fly's own tile or +on one of its four neighbours, and the anchor it is centred on is the one whose **whole** +neighbourhood agrees with the decode. `(0, 0)` is tried first, so a standing frame costs exactly +what it did before. The refusals the check exists for all survive, because a wrong stride, a wrong +quadrant, a half-loaded map and the mid-warp tear each disagree under every one of the five: the +neighbourhood has to agree as a unit rather than tile by tile. + ### The survey, on two maps `services/flysim/crates/flysim/tests/rom_map_grid.rs`, the method of diff --git a/docs/design/macros.md b/docs/design/macros.md index acd7643..81535b7 100644 --- a/docs/design/macros.md +++ b/docs/design/macros.md @@ -1165,12 +1165,57 @@ 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: `wYCoord` is the tile being walked *to* while the background is -still scrolling. 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". Naming it needs a WRAM reading of "a step is in progress" that this crate's -reviewed symbol list does not carry, so it is reported here and by the probes rather than guessed at. +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. ## 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 diff --git a/infra/docs/macros-traps.md b/infra/docs/macros-traps.md index d0e7e12..6539843 100644 --- a/infra/docs/macros-traps.md +++ b/infra/docs/macros-traps.md @@ -2057,23 +2057,92 @@ that covers more ground walks into more grass, and the hunt cannot tell a long f | # | trap | trigger | test | fix, or why it is left | | ---: | --- | --- | --- | --- | -| 54 | `GO FRONTIER`, `GO HEAL` and `GO ROUTE` cycle on five tiles: three walks that each end where they began | measured in the **after** arm only, brain minutes 1.0 to 8.5, `GO HEAL` **204** starts at a mean net of 0.0 tiles and a mean reach of 0.0, `GO ROUTE` 211 at a net of 0.2 | -- | **named, not worked, and it is the next brief.** Two readings fit and the hunt cannot separate them: the errand walking the fly in and out of a building whose door is underfoot (row 2's shape, with `GO HEAL` in `GO ROUTE`'s place), and the windowed walk oscillation of 12.3's row 23 -- which the whole-map grid was built to end and which is back **because the grid is refused on every frame the fly is mid-step** (below). Rows 1, 2b, 23, 24 and 41 were each found this way | +| 54 | `GO FRONTIER`, `GO HEAL` and `GO ROUTE` cycle on five tiles: three walks that each end where they began | rung 10: brain minutes 1.0 to 8.5, `GO HEAL` **204** starts at a mean net of 0.0 tiles and a mean reach of 0.0, `GO ROUTE` 211 at a net of 0.2. Rung 11, the same cycle one town on: **14 distinct tiles in six brain minutes**, 17 of 17 windows flagged, `GO FRONTIER` 122 / `GO HEAL` 129 / `GO ROUTE` 126, **every one `done` at a mean net of 0.0**, printed as `GO ROUTE, GO FRONTIER, GO HEAL` x34 to x42 | `an_errand_does_not_settle_on_the_doormat_it_is_standing_on`, `an_errand_is_paid_by_a_building_this_run_has_already_been_inside`, `a_frame_mid_step_is_read_from_the_tile_the_screen_is_centred_on`, `a_decode_the_screen_disagrees_with_is_refused_mid_step_too`, `the_tile_a_step_is_landing_on_is_ground_the_run_has_covered`, `an_edge_the_table_cannot_name_stops_being_somewhere_new_once_it_is_stood_on`, and both ROM runs below | **fixed, and both readings were right.** Four facts, all measured: the coordinates change at the **end** of a step, so the grid was refused on every moving frame and every walk was planned over the ten-by-nine window; the tile a step is landing on was unrecorded for fifteen frames of every sixteen, so the fly's own next tile was a frontier it arrived at without moving; an errand's aim at a door the fly was standing on settled where it stood, and a completed errand walk writes the reached ledger, so the button came back every hold; and the errand ledger is session state, so a restore re-armed a town the run had already shopped and healed in. `docs/design/macros.md` section 12.17 | +| 54b | an **edge** the geography table has no row for is "somewhere new" for ever | Route 3: the cartridge reports its connections as **north and west** (`wCurMapConnections`; `warps: []`), the table carries west and **east**, so the seven walkable tiles of its north edge answered "leads somewhere this run has not stood on" on every hold, with `GO OBJECTIVE` off the pad beside them because nothing on that map leads to the objective | `an_edge_the_table_cannot_name_stops_being_somewhere_new_once_it_is_stood_on` | **fixed, narrowly.** A warp's destination is a byte the cartridge publishes, so `None` there is the `LAST_MAP` case row 2 already handles; an edge's comes only from `geography::connected`, so `None` there means the table cannot name it and never will. The only record left is the adapter's boundary ledger, and an edge the run has already stood on is not somewhere new. **The table row itself is not guessed at**: which map is north of Route 3 is a survey nobody has run, and it is a residual below | ### Residuals, named rather than worked around -- **The whole-map grid is refused while the fly is moving.** `map_grid` checks its decode against - the screen buffer over the fly's tile and its four neighbours, and mid-step the two are one tile - apart: `wYCoord` is the tile being walked *to* while the background is still scrolling. Measured - on Pewter City from this 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", - which is what row 23's oscillation is made of. The honest fix is a WRAM reading of "a step is in - progress", which this crate's reviewed symbol list does not carry, so it is reported by the - probes and left. -- **The town's errands are session state**, so every restart re-arms them and `GO OBJECTIVE` aims - at the mart and the centre before the rung's place, the gym included. Section 13's own design. +- ~~**The whole-map grid is refused while the fly is moving.**~~ **Worked, 2026-09-22 (row 54).** + The guess was the wrong way round: `FLY_PROBE_CATCH=step` in `examples/scene_probe.rs` found the + coordinates change at the **end** of a sixteen-frame step, so it is the screen that is one tile + ahead of `wYCoord` and not `wYCoord` ahead of the screen. The reader now measures which tile the + screen is centred on. `docs/design/macros-wram.md` section 9 has the frame-by-frame trace and the + candidates; `$cfc5` tracks a step exactly and is recorded there **unused**, because + `gen_symbols.py` refuses a hand-written address and the checkout `resolve_wram.py` reads is not + on this box. +- ~~**The town's errands are session state**~~, so every restart re-armed them. **Worked, + 2026-09-22 (row 54):** the adapter's lifetime `map_visited` is asked beside the session ledger, + and a building this run has already been inside pays the errand whichever one remembers it. - **`MOVE n` still reports `blocked`** with the move list drawn and its cursor placeable but not - accepting input (row 50): 42 of 65 in the after arm. Unchanged since v0.4.3. + accepting input (row 50). It is now **the largest thing in the way**: after row 54 the fly wins + the Boulder Badge and then spends 30,809 frames in one battle on the rung-10 arm and 13,251 on + the rung-11 arm, and the hunt flags every window of both because its tile rule cannot tell a long + battle from a stall. Unchanged since v0.4.3. +- **Which map is north of Route 3.** The cartridge says that edge is connected and the geography + table has no row for it (row 54b). Naming it is a survey -- walk the fly off that edge with real + presses and read `wCurMap` back, the method of `docs/design/macros-wram.md` -- and nothing here + guesses at it. Until then that edge is walked once and then falls out of the first tier. + +## Row 54: the two arms, and the ROM runs (2026-09-22, v0.4.6) + +Two checkpoints, because the loop was found twice: the rung-10 one the previous review left it in, +and the rung-11 one the stream fell into forty minutes after the badge was won. Same seed, same +ground, `main` at `cb9a88c` against this branch. + +**The rung-10 checkpoint, twenty brain minutes.** + +| measure | before (`main`, v0.4.6) | after | +| --- | ---: | ---: | +| rung reached | 10 | **11 (BOULDER BADGE at 10.78 brain minutes)** | +| distinct (map, tile) | **175** | 165 | +| windows flagged | **69 / 73** | 73 / 73 | +| macros started | 1,056 | 1,211 | +| `GO HEAL` starts | **204**, every one `done` at a net of 0.0 | **0** | +| `GO FRONTIER` starts | 242 | 34 | +| `GO ROUTE` starts | 211, mean net 0.2 | 7, mean net 6.1, max 30 | +| the repeated sequence | `GO FRONTIER, GO HEAL, GO ROUTE` | no walk cycle at all | +| frames in `battle` | 20,894 | **58,687** (longest run 30,809) | +| wall clock | 7,515 s | **3,086 s** | + +**The cycle is gone and the fly wins the badge, and the hunt still flags every window.** Both are +true and both are reported. Eighty-two per cent of the after arm is inside battles and the longest +single battle is 30,809 frames, so the tile rule -- fewer than four distinct tiles in two brain +minutes -- flags a fly that is fighting exactly as hard as a fly that is stuck. That is section +15's own measurement in `docs/design/macros.md`, and the second branch running into it. +**The ethos check's "fewer flagged windows, more distinct tiles" does not hold on this arm**, and +the merge is Fable's call. The wall clock is the grid fix seen from outside: `main` re-decodes the +whole map on most frames because the cross-check refuses them, and this branch serves the cache. + +**The rung-11 checkpoint, six brain minutes.** This is the arm the fix is about, on ground with no +gym leader in it. + +| measure | before (`main`, v0.4.6) | after | +| --- | ---: | ---: | +| distinct (map, tile) | **14** | **88** | +| windows flagged | 17 / 17 | 17 / 17 | +| macros started | 378, every one `done` | 314 | +| `GO HEAL` starts | **129**, every one at a net of 0.0 | **0** | +| `GO FRONTIER` starts | 122, net 0.0 | 9 | +| `GO ROUTE` starts | 126, net 0.0 | **4, mean net 4.8, mean reach 19.5** | +| the repeated sequence | `GO ROUTE, GO FRONTIER, GO HEAL` x34 to x42 | `NEXT` and `BACK` in a battle's move list | +| the fly leaves Pewter City | **never** | **at 0.85 brain minutes**, and it ends the run on Route 3 | +| frames in `battle` | 0 | 15,619 (longest run 13,251, from minute 1.09) | + +**Six times the ground, and the same seventeen flagged windows.** No walk completes at a net of +zero tiles any more, and from brain minute 1.09 the fly is inside a single 13,251-frame battle, +which the tile rule flags exactly as hard as the cycle it replaced. What makes that battle last is +row 50. + +**ROM-gated, from both checkpoints** (`services/flysim/crates/flysim/tests/rom_macros_mode.rs`, +skipped cleanly without `FLY_ROM` and the checkpoint): + +- `the_fly_reaches_the_pewter_gym_from_the_rung_ten_checkpoint` -- the gym's own interior on frame + **3,163** on **25 macros**, `BACK` in a box **0**, unknown pads with no box **0**, `GO FRONTIER` + on the museum's two floors **0**, and the new claim: **the longest chain of walks that completed + at a net of zero tiles is 1**, against a bound of three. +- `the_fly_leaves_pewter_from_the_rung_eleven_checkpoint` -- the fly reaches Route 3, the same + net-zero bound holds, and neither errand is offered in a town the run has already shopped and + healed in. ### Gates