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.
This commit is contained in:
acamilo 2026-09-22 14:57:34 +00:00
parent 895fb52cda
commit 611fe9e108
2 changed files with 76 additions and 2 deletions

View file

@ -868,7 +868,17 @@ pub fn errand(state: &mut dyn MacroState, kind: Amenity) -> Option<u8> {
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<Aim> {
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(),
}
}

View file

@ -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<_>>(),
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<_>>(),
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.