diff --git a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/executor.rs b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/executor.rs index e100568..1d320a2 100644 --- a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/executor.rs +++ b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/executor.rs @@ -36,7 +36,7 @@ use super::palette::{ precondition, shop_screen, stock_index, throw_slot, untalked_objects, untalked_people, ways, }; -use super::path::{self, Route, Way}; +use super::path::{self, Exit, Route, Way}; use super::state::{Facing, Scene, ShopScreen}; /// Hard cap on one macro, in game frames. `docs/design/macros.md` section 4: "hard cap 600 frames @@ -1686,7 +1686,17 @@ fn script( }; let goals = exit_goals(state, way); unreachable.extend(goal_keys(&goals)); - let (walk, target) = walk_to(state, goals)?; + // A last resort that preferred the way toward the objective, and the route search + // cannot reach it: the rest of the last resort, nearest reachable first (row 57). + let walked = match walk_to(state, goals) { + Some(walked) => Some(walked), + None => { + let rest = super::palette::last_resort_wide(state, way); + let wide = goals_of(state, rest); + if wide.is_empty() { None } else { walk_to(state, wide) } + } + }; + let (walk, target) = walked?; aimed = target; vec![Step::Walk(walk)] } @@ -2075,7 +2085,13 @@ fn one_target(goals: &[Goal]) -> Option { /// is in the moment it comes down a staircase and lands on the warp tile. A doormat underfoot is /// different: its press is the point, so it stays. fn exit_goals(state: &mut dyn MacroState, way: Way) -> Vec { - let mut goals: Vec = ways(state, way) + let exits = ways(state, way); + goals_of(state, exits) +} + +/// [`exit_goals`] over a list of exits the caller already has. +fn goals_of(state: &mut dyn MacroState, exits: Vec) -> Vec { + let mut goals: Vec = exits .into_iter() .map(|exit| Goal { tile: exit.tile, 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 c7a3fae..ea88b10 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 @@ -1259,6 +1259,49 @@ fn last_resort(state: &mut dyn MacroState, way: Way) -> Vec { if toward.is_empty() { every } else { toward } } +/// Every way out of this kind, when [`ways`] answered with its last resort and that answer was +/// narrowed to the ways toward the objective; empty otherwise. +/// +/// Row 57 (`infra/docs/macros-traps.md`). The narrowing is a preference -- "a map the fly has +/// already seen is still the way to the next rung" -- and the dealer cannot search, so it can +/// prefer a door the fly is walled off from over one it can walk to. Live in Pewter City the last +/// resort was the gym's door on the far side of a fence while the road east, resting in the +/// blocked window, was three tiles away. This is the rest of the last resort, for `start` to try +/// when its route search cannot reach the preferred ones; the choice of *which* reachable way is +/// the route search's, nearest first, exactly as it is for every walk. +pub fn last_resort_wide(state: &mut dyn MacroState, way: Way) -> Vec { + // Only where `ways` fell through to the last resort: a tier that has anything in it is + // already the answer, and so is a room's or a floor's own unexcluded list. + if !exit_tiers(state, way).is_empty() { + return Vec::new(); + } + match way { + Way::Exit => { + if !unexcluded_exits(state, way).is_empty() { + return Vec::new(); + } + } + Way::Passage => { + if path::exits(state).iter().any(|exit| exit.way == Way::Exit) + || !unexcluded_exits(state, way).is_empty() + { + return Vec::new(); + } + } + Way::Route => {} + } + if !stranded(state) { + return Vec::new(); + } + let every: Vec = + path::exits(state).into_iter().filter(|exit| exit.way == way).collect(); + if toward_objective(state, &every).is_empty() { + // Not narrowed: the last resort was every one of them already. + return Vec::new(); + } + every +} + /// The exits of the current map of one kind that no ledger excludes. fn unexcluded_exits(state: &mut dyn MacroState, way: Way) -> Vec { if !objective_targets(state).is_empty() { 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 a006d78..5fe1580 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 @@ -4955,6 +4955,32 @@ fn a_way_out_refused_from_here_is_not_dealt_again_from_here() { assert!(names(&mut world).contains(&"GO ROUTE"), "{:?}", names(&mut world)); } +#[test] +fn a_last_resort_that_cannot_reach_the_objectives_door_takes_a_way_out_it_can_reach() { + // Row 57's pocket had a way out: the road east, three tiles from the fly and resting in the + // blocked window. The last resort ignores that window but *narrows* to the ways toward the + // objective, and the only one was the gym's door on the far side of the fence -- so the + // route search refused, and the reachable road was never tried. The narrowing is a + // preference; when the route search cannot honour it, the rest of the last resort is the + // walk's to take, nearest reachable first. + let mut world = fenced_in_pewter(); + world.connections = Connections { north: false, south: true, east: false, west: false }; + world.seen_maps.insert(maps::ROUTE_2); + let south = TargetKey::Exit(ExitId::Edge(Edge::South)); + world.targets.record_blocked(world.map, south); + world.targets.record_blocked(world.map, TargetKey::Exit(ExitId::Warp(0))); + assert!(on_the_pad(&mut world, MacroKind::GoRoute), "the last resort deals it"); + + let started_at = world.player; + let outcome = run(&mut world, MacroKind::GoRoute); + assert!(outcome.is_ok(), "the road it can reach is walked, not refused: {outcome:?}"); + assert!( + world.player.y > started_at.y, + "south, toward the way out on this side of the fence: {:?}", + world.player + ); +} + #[test] fn an_escorted_walk_walls_the_tile_it_reached_not_the_one_it_set_out_from() { // Row 57's other half. Pewter City's youngster takes the joypad on four tiles by the road diff --git a/services/flysim/crates/flysim/tests/rom_macros_mode.rs b/services/flysim/crates/flysim/tests/rom_macros_mode.rs index 372e411..2fb329e 100644 --- a/services/flysim/crates/flysim/tests/rom_macros_mode.rs +++ b/services/flysim/crates/flysim/tests/rom_macros_mode.rs @@ -3046,7 +3046,9 @@ impl Palette57 { /// The session ledgers are what dealt that pad and a restore starts them empty, so part two seeds /// the pocket: the tiles every walk has stood on, the map's frontier mark, every sign and person /// talked to, the road east resting, and three pushed tiles sealing the strip by the road from the -/// town. That is a reconstruction of state the checkpoint cannot carry, said as one. +/// town. That is a reconstruction of state the checkpoint cannot carry, said as one. In it the last +/// resort's preferred way out, the gym's door, is beyond the fence, while the road east is three +/// tiles away and resting in its window: `start` now takes the way it can reach. /// /// ```sh /// FLY_ROM=/path/to/pokemon-red.gb \ @@ -3132,6 +3134,7 @@ fn the_pewter_east_pad_is_never_one_dead_button_from_the_rung_ten_checkpoint() { let (mut running, mut since) = (false, Palette57::HOLD_FRAMES); let mut refusals = 0u32; + let mut in_the_pocket = 0u32; let mut run_of_refusals = 0u32; let mut longest = 0u32; let mut last_refused: Option<(&'static str, (u8, u8, u8))> = None; @@ -3144,6 +3147,9 @@ fn the_pewter_east_pad_is_never_one_dead_button_from_the_rung_ten_checkpoint() { first_pad.get_or_insert(pad.clone()); if let Some(flybrain_gb::Started::Refused { name: Some(name), reason }) = tried { refusals += 1; + if at.0 == pewter && at.1 >= 34 { + in_the_pocket += 1; + } let key = (name, at); run_of_refusals = if last_refused == Some(key) { run_of_refusals + 1 } else { 1 }; last_refused = Some(key); @@ -3159,18 +3165,20 @@ fn the_pewter_east_pad_is_never_one_dead_button_from_the_rung_ten_checkpoint() { } } eprintln!( - "part two: first pad {first_pad:?}, {refusals} refusals, longest run on one tile {longest}, \ - left the pocket at {left:?}" + "part two: first pad {first_pad:?}, {refusals} refusals ({in_the_pocket} in the pocket), \ + longest run on one tile {longest}, left the pocket at {left:?}" ); // The live pad: `GO ROUTE` refused every hold, 740 times in ten brain minutes. assert!( longest <= 1, "a button was refused {longest} holds running on one tile: the pad kept dealing it" ); - assert!(refusals <= 6, "{refusals} refusals in twelve brain minutes"); + assert!(in_the_pocket <= 2, "{in_the_pocket} refusals in the pocket"); let Some(left) = left else { panic!("the fly never left the pocket in twelve brain minutes") }; - eprintln!( - "left the pocket on frame {left} ({:.2} brain minutes)", - f64::from(left) * MS_PER_FRAME / 60_000.0 - ); + let minutes = f64::from(left) * MS_PER_FRAME / 60_000.0; + eprintln!("left the pocket on frame {left} ({minutes:.2} brain minutes)"); + // The road east is three tiles away and resting in its window; the last resort preferred the + // gym's door beyond the fence. On the base the fly leaves only when the road's window lapses, + // ten brain minutes in. + assert!(minutes < 1.0, "the fly waited {minutes:.2} brain minutes for a window to lapse"); }