From 895fb52cda3222dd65c82d4b507fd4dfbd0a4bbf Mon Sep 17 00:00:00 2001 From: acamilo Date: Tue, 22 Sep 2026 14:57:34 +0000 Subject: [PATCH] macros: the tile a step is landing on is ground the run has covered The other half of the same measurement. `wXCoord` and `wYCoord` are the tile the step began on until the frame it ends, so the stood ledger recorded ground the fly had already left and the tile under it stayed unstood: `path::frontier` kept offering it, `GO FRONTIER` was dealt aiming one tile away, and `Arrival::Step` reported `done` the instant the step it did not make landed. A macro that completes without changing anything, which is section 12.2's trap. A step that has begun always finishes -- the cartridge owns the animation and no press stops it -- and the screen has already centred on the tile, so it is ground this run has covered. It clears the map's frontier mark on the same rule the coordinates do: only ground never stood on before. --- .../src/pokemon_red/macros/cartridge.rs | 14 ++++++ .../src/pokemon_red/macros/driver.rs | 47 +++++++++++++++++-- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/cartridge.rs b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/cartridge.rs index de90495..83d6776 100644 --- a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/cartridge.rs +++ b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/cartridge.rs @@ -438,6 +438,20 @@ pub trait MacroState: GameState { false } + /// The tile the fly is stepping onto, or `None` while it is standing still. + /// + /// **Row 54 of `infra/docs/macros-traps.md`, measured on the cartridge.** `wXCoord` and + /// `wYCoord` change at the *end* of a step, so for fifteen frames of every sixteen the fly's + /// coordinates are the tile it has already left. The ground under it is unrecorded for all of + /// them, `path::frontier` keeps offering it, and `GO FRONTIER` is dealt aiming one tile away + /// -- a walk that reports `done` the instant the step it did not make lands. + /// + /// The default is `None`, i.e. never mid-step, which is the narrowing every other default in + /// this trait is: the stood ledger keeps the coordinates alone, which is what it did before. + fn stepping_onto(&mut self) -> Option { + None + } + /// Whether this run has already talked to `target` on the map that is loaded. /// /// `docs/design/macros.md` section 12's talked ledger, and the observable that ends the diff --git a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/driver.rs b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/driver.rs index bde4466..169c559 100644 --- a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/driver.rs +++ b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/driver.rs @@ -200,7 +200,7 @@ impl MacroPalette for PokemonPalette { } fn observe(&mut self, memory: &mut dyn MemoryReader, ledger: &dyn RunLedger) -> Observed { - let (scene, bindings, standing, approach) = { + let (scene, bindings, standing, stepping, approach) = { let Self { machine, mode, @@ -237,6 +237,9 @@ impl MacroPalette for PokemonPalette { // -- the coordinates and the loaded map header are from different frames, and a tile // recorded from that pair is a tile of nowhere. let standing = (!state.scripted()).then(|| state.player()).flatten(); + // And the tile the step in flight is landing on (row 54). Read from the same frame and + // behind the same "the fly is its own master" gate as the ground itself. + let stepping = standing.and_then(|_| state.stepping_onto()); // How far the objective is, over the same map graph `GO OBJECTIVE` walks (section // 12.15). Read from the same frame and the same state everything else is, and only // where the fly is its own master, for the same reason the ground is. @@ -247,7 +250,7 @@ impl MacroPalette for PokemonPalette { Some((objective.map, hops)) }); *cached = Some(palette); - (scene, bindings, standing, approach) + (scene, bindings, standing, stepping, approach) }; // Section 12.7: the macro layer's own answer to "has the run stood here", because the // adapter's reward ledger cannot record a doormat. @@ -258,6 +261,16 @@ impl MacroPalette for PokemonPalette { if self.stood.record(player.map, Tile::new(player.x, player.y)) { self.frontiers.clear(player.map); } + // The tile a step in flight is landing on is ground this run has covered: the + // cartridge owns the animation and no press stops it, and the screen has already + // centred on it. Without this the fly's own next tile is a frontier for the fifteen + // frames it takes to get there, which `GO FRONTIER` arrives at without moving + // (`infra/docs/macros-traps.md` row 54). + if let Some(onto) = stepping + && self.stood.record(player.map, onto) + { + self.frontiers.clear(player.map); + } // Section 13's `areaVisited(kind, area)`: the errand is paid on *entering*, so the // ledger is written from the same frame that records the ground. Standing on the // building's own map is the whole test -- the fly is inside it -- and it is written @@ -454,7 +467,7 @@ mod tests { use super::*; use crate::adapter::{MapEdge, MapExit}; use crate::macros::NoLedger; - use crate::pokemon_red::fake_wram::{REDS_HOUSE_1F, Wram}; + use crate::pokemon_red::fake_wram::{self, REDS_HOUSE_1F, WALL_TILE, Wram}; use crate::pokemon_red::macros::geography::Amenity; use crate::pokemon_red::maps; use crate::pokemon_red::macros::cartridge::{Edge, ExitId, MacroState}; @@ -468,6 +481,34 @@ mod tests { } } + #[test] + fn the_tile_a_step_is_landing_on_is_ground_the_run_has_covered() { + // Row 54 of `infra/docs/macros-traps.md`. `wXCoord` and `wYCoord` are the tile the step + // began on until the frame it ends, so without this the ground under the fly is unrecorded + // for fifteen frames of every sixteen: `path::frontier` keeps offering the tile the fly is + // already halfway onto, `GO FRONTIER` is dealt aiming at it, and `Arrival::Step` reports + // `done` the instant the step it did not make lands -- a macro that completes without + // changing anything, which is section 12.2's trap. + let (mut wram, blocks, blockset) = Wram::town(); + let mut palette = PokemonPalette::new(7); + palette.observe(&mut wram, &NoLedger); + assert_eq!(palette.stood(), 1, "standing still, the tile under the fly and nothing else"); + + // Mid-step west: the coordinates still read (3, 4), the screen is already centred on + // (2, 4). + wram.mid_step(-1, 0, &blocks, &blockset); + palette.observe(&mut wram, &NoLedger); + assert_eq!(palette.stood(), 2, "and the tile the step is landing on"); + + // The step lands. The ledger had it already, so nothing new is recorded and the frontier + // mark is not cleared a second time. + wram.map(fake_wram::PALLET_TOWN, 10, 9, 2, 4) + .fill_screen(WALL_TILE) + .screen_from_blocks(&blocks, &blockset); + palette.observe(&mut wram, &NoLedger); + assert_eq!(palette.stood(), 2, "the tile it landed on was already ground it had covered"); + } + #[test] fn a_fresh_cartridge_reads_as_the_title_and_binds_nothing() { // All-zero WRAM: the game-timer bit is clear, which is the title screen.