From 611fe9e108ee76e1498cf56f03c1b62f57639de7 Mon Sep 17 00:00:00 2001 From: acamilo Date: Tue, 22 Sep 2026 14:57:34 +0000 Subject: [PATCH] macros: an errand arrives inside the building, and a building already entered has none Two rules for section 13's errands, both from row 54's `GO HEAL`: 204 starts at a mean net of 0.0 tiles and a mean reach of 0.0, cycling with `GO ROUTE` and `GO FRONTIER` over five tiles for seven and a half brain minutes. An errand's aim at a door carries no press -- 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. A completed errand walk writes the reached ledger, which `goals_toward` does not filter, so the same button was dealt on the next hold and the same nothing happened again. `exit_goals` has excluded a settled goal underfoot since row 13; this is the one walk that did not have the rule. And the errand ledger 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. `map_visited` is the adapter's lifetime answer to the same question and it does survive, so both are asked and either pays the errand. --- .../src/pokemon_red/macros/palette.rs | 28 ++++++++++- .../src/pokemon_red/macros/tests.rs | 50 +++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/palette.rs b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/palette.rs index 4f094e9..1c641ad 100644 --- a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/palette.rs +++ b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/palette.rs @@ -868,7 +868,17 @@ pub fn errand(state: &mut dyn MacroState, kind: Amenity) -> Option { if kind == Amenity::Mart && state.money() < CHEAPEST_PURCHASE { return None; } - geography::amenity_of(area, kind) + let map = geography::amenity_of(area, kind)?; + // **A building this run has already been inside is an errand already discharged.** The session + // ledger above is the errand's own record and it is the one that can be missing: it is written + // from the frame the fly stands on the building's map, so a restore starts with it empty and + // the run walks back to a counter it has already used. [`MacroState::map_visited`] is the + // adapter's lifetime answer to the same question and it does survive, so the two together are + // "has this run been in there", asked twice (`infra/docs/macros-traps.md` row 54). + if state.map_visited(map) { + return None; + } + Some(map) } /// The errand `GO OBJECTIVE` puts *ahead* of the rung's place, when this area has one. @@ -930,7 +940,21 @@ pub fn amenity_goals(state: &mut dyn MacroState, kind: Amenity) -> Vec { return counter_aims(state, counter_sprite(kind)); } match errand(state, kind) { - Some(map) => goals_toward(state, map), + Some(map) => { + let here = state.player().map(|player| Tile::new(player.x, player.y)); + goals_toward(state, map) + .into_iter() + // **An errand arrives inside the building, never on the doormat outside it** + // (section 12.2's rule, row 54). An aim with no press settles where it stands, so + // an aim on the tile the fly is already on is `Done` in `SETTLE_FRAMES` with the + // world exactly as it was -- and a completed errand walk writes the reached ledger, + // so the same button is dealt on the next hold and the same nothing happens again: + // `GO HEAL` 204 starts at a mean net of 0.0 tiles and a mean reach of 0.0. The same + // exclusion [`super::executor::exit_goals`] has made since row 13, for the same + // reason, on the one walk that did not have it. + .filter(|aim| aim.press.is_some() || Some(aim.tile) != here) + .collect() + } None => Vec::new(), } } diff --git a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/tests.rs b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/tests.rs index 9847925..9101e05 100644 --- a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/tests.rs +++ b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/tests.rs @@ -3970,6 +3970,56 @@ fn go_shop_and_go_heal_are_on_the_pad_while_their_errand_stands() { assert!(!on_the_pad(&mut pallet, MacroKind::GoHeal)); } +#[test] +fn an_errand_does_not_settle_on_the_doormat_it_is_standing_on() { + // Row 54 of `infra/docs/macros-traps.md`, and section 12.2's rule: "a macro that completes + // without moving because its precondition is already satisfied where the fly stands is a + // trap". An errand's aim at a door carries no press -- the warp fires when it is stepped on -- + // so an aim on the tile the fly is already on settles for `SETTLE_FRAMES` and reports `done` + // with the world exactly as it was. A completed errand walk writes the reached ledger, which + // `goals_toward` does not filter, so the same button was dealt on the next hold and the same + // nothing happened again: `GO HEAL` 204 starts at a mean net of 0.0 tiles and a mean reach of + // 0.0, in a cycle with `GO ROUTE` and `GO FRONTIER` over five tiles. + let mut world = viridian(); + world.player = Tile::new(6, 1); + assert!( + amenity_goals(&mut world, Amenity::Center).is_empty(), + "the centre's own doormat is not somewhere to walk to" + ); + assert!(!on_the_pad(&mut world, MacroKind::GoHeal), "so the button is not on the pad"); + + // The other errand is a tile away and untouched: this excludes one aim, not the walk. + assert_eq!( + amenity_goals(&mut world, Amenity::Mart).iter().map(|aim| aim.tile).collect::>(), + vec![Tile::new(1, 1)] + ); + assert!(on_the_pad(&mut world, MacroKind::GoShop)); + + // And one tile off the doormat the centre is a walk again. + world.player = Tile::new(6, 2); + assert_eq!( + amenity_goals(&mut world, Amenity::Center).iter().map(|aim| aim.tile).collect::>(), + vec![Tile::new(6, 1)] + ); + assert!(on_the_pad(&mut world, MacroKind::GoHeal)); +} + +#[test] +fn an_errand_is_paid_by_a_building_this_run_has_already_been_inside() { + // The errand ledger is session state and the adapter's map ledger is not, so a restored run + // re-armed every errand in the town and walked back to a counter it had already used + // (`docs/design/macros.md` section 13's own residual). Asking both is asking "has this run + // been in there" twice, and either answer pays the errand. + let mut world = viridian(); + assert_eq!(errand(&mut world, Amenity::Center), Some(maps::VIRIDIAN_POKECENTER)); + world.seen_maps.insert(maps::VIRIDIAN_POKECENTER); + assert_eq!(errand(&mut world, Amenity::Center), None, "already been inside it"); + assert!(!on_the_pad(&mut world, MacroKind::GoHeal)); + // The mart is a different building and a different errand. + assert_eq!(errand(&mut world, Amenity::Mart), Some(maps::VIRIDIAN_MART)); + assert!(on_the_pad(&mut world, MacroKind::GoShop)); +} + #[test] fn go_shop_walks_to_the_marts_door_and_then_to_the_counter() { // Outside: the goal is the door, and it is the warp's own tile.