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 eddd01f..eca338d 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 @@ -349,10 +349,9 @@ impl MacroPalette for PokemonPalette { // 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. - let approach = standing.and_then(|player| { + let approach = standing.and_then(|_| { let objective = palette::objective_place(&mut state)?; - let hops = - geography::hops(geography::region_at(player.map, player.y), objective.map)?; + let hops = geography::hops(palette::region_here(&mut state)?, objective.map)?; Some((objective.map, hops)) }); *cached = Some(palette); diff --git a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/geography.rs b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/geography.rs index 3f7a44f..5677dd9 100644 --- a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/geography.rs +++ b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/geography.rs @@ -28,6 +28,7 @@ use std::collections::{HashMap, HashSet, VecDeque}; use super::super::maps; use super::cartridge::Edge; +use super::state::MapGrid; /// A column with no connection on it. const NONE: u8 = 0xff; @@ -149,11 +150,12 @@ const LINKS: &[(u8, u8)] = &[ /// the forest's south gate; the north half touches Pewter City and the forest's north gate; the /// belt of trees between them needs CUT. A graph with one node for it answered "Pewter is two /// hops from the south gate, south" — which is a road that does not exist — and sent the fly back -/// out of the gate it had just walked into, once per hold, for four hours. +/// out of the gate it had just walked into, once per hold, for four hours. Row 59 found three more +/// on the road to Cerulean: Route 4, and Mt. Moon's two lower floors ([`SPLIT`]). #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Region { pub map: u8, - /// Which piece, for a map [`SPLIT`] has a row for; 0 everywhere else. + /// Which piece, for a map [`SPLIT`] has a row for: its index in that row. 0 everywhere else. pub part: u8, } @@ -162,108 +164,268 @@ impl Region { pub const fn whole(map: u8) -> Self { Self { map, part: 0 } } + + /// Piece `part` of a map [`SPLIT`] has a row for. + pub const fn piece(map: u8, part: u8) -> Self { + Self { map, part } + } } -/// [`SPLIT`]'s two piece numbers. -const NORTH_PIECE: u8 = 0; -const SOUTH_PIECE: u8 = 1; +/// One piece of a split map. +struct Piece { + /// The map's own warps that stand on this piece's ground: the index into its warp table (as + /// `wWarpEntries` holds it, and as a warp elsewhere names it for its destination, 0-based) and + /// the tile. A warp that lands on one of these lands in this piece; the tiles are what the + /// fly's own piece is told apart by ([`region_on`]). + doors: &'static [(u8, u8, u8)], + /// Everything one step from this piece: a whole map, or a piece of another split map. Their + /// maps together are exactly [`neighbours`]'s answer for the map, and every step is listed + /// back from the other side; [`tests::a_split_maps_pieces_add_up_and_answer_each_other`] pins + /// both. + next: &'static [Region], +} -/// A map whose walkable ground is in two pieces, and which of its neighbours each piece touches. +/// A map whose walkable ground is in pieces the player cannot walk between. struct Split { map: u8, - /// The tile rows each piece's own doorway is on, measured from the cartridge: a tile belongs - /// to the piece whose row it is nearer to. Anchoring on the doorways rather than on a row in - /// the middle means the number comes from the warp table rather than from a claim about where - /// the trees are, and a tile in the impassable belt between them — ground the fly cannot - /// stand on — is the only place the answer could be wrong. - north_door: u8, - south_door: u8, - /// The neighbours reachable from each piece. Together they are exactly [`neighbours`]'s answer - /// for the map, which [`tests::a_split_maps_pieces_divide_its_neighbours_between_them`] pins. - north: &'static [u8], - south: &'static [u8], + pieces: &'static [Piece], } -/// Every map whose ground is in two pieces. One row, and it took four hours of stream to find. +/// Route 2's two halves, in [`SPLIT`]'s order. +#[cfg(test)] +const NORTH_PIECE: u8 = 0; +#[cfg(test)] +const SOUTH_PIECE: u8 = 1; + +/// Route 4's two sides of the mountain. +#[cfg(test)] +const WEST_SIDE: u8 = 0; +const EAST_SIDE: u8 = 1; + +/// Mt. Moon B1F's four chambers, named by what they hold. +const B1F_EXIT: u8 = 0; +const B1F_WEST: u8 = 1; +const B1F_MIDDLE: u8 = 2; +const B1F_SOUTH: u8 = 3; + +/// Mt. Moon B2F's three pieces. +const B2F_MAIN: u8 = 0; +const B2F_NORTH: u8 = 1; +const B2F_SOUTH: u8 = 2; + +/// Every map whose ground is in pieces, with each piece's doors and neighbours. /// -/// `ROUTE_2`, surveyed from the cartridge on 2026-09-17 (`docs/design/macros-wram.md`'s method, -/// the run recorded in `infra/docs/macros-traps.md` row 33). The map is 20 by 72 and its warp -/// table reads: +/// Every row is measured from the disassembly at the pinned commit: the map's blocks, its +/// tileset's blockset and collision list, the tile-pair walls and the ledges, flooded tile by tile +/// (`infra/docs/macros-traps.md` row 59 has the method). A map is listed here only when two of its +/// ways out are on different pieces, and the audit ran over every map on the graph. /// -/// | warp | tile | into | -/// | ---: | --- | --- | -/// | 0 | (12, 9) | `DIGLETTS_CAVE_ROUTE_2` (46) | -/// | 1 | (3, 11) | `VIRIDIAN_FOREST_NORTH_GATE` (47) | -/// | 2 | (15, 19) | `ROUTE_2_TRADE_HOUSE` (48) | -/// | 3 | (16, 35) | `ROUTE_2_GATE` (49) | -/// | 4 | (15, 39) | `ROUTE_2_GATE` (49) | -/// | 5 | (3, 43) | `VIRIDIAN_FOREST_SOUTH_GATE` (50) | -/// -/// with `north: true` and `south: true` in `wCurMapConnections` — Pewter off the top row, Viridian -/// off the bottom one. The two forest gates at rows 11 and 43 are the doorways this splits on. -/// -/// Maps 46, 48 and 49 are deliberately *not* on the graph, which is this module's standing rule -/// for a building no rung place needs a route through: 49's two doors are both warps of `ROUTE_2` -/// itself, so it is a shortcut within one map rather than a way between two, and 46 and 48 are -/// ends of the line. A route the table does not carry is simply not offered; nothing is guessed. -const SPLIT: &[Split] = &[Split { - map: maps::ROUTE_2, - north_door: 11, - south_door: 43, - north: &[maps::PEWTER_CITY, maps::VIRIDIAN_FOREST_NORTH_GATE], - south: &[maps::VIRIDIAN_CITY, maps::VIRIDIAN_FOREST_SOUTH_GATE], -}]; +/// - **`ROUTE_2`** (row 33): the forest's north gate at (3, 11) and Pewter's edge; the south gate +/// at (3, 43) and Viridian's. Maps 46, 48 and 49 stay off the graph, the module's standing rule +/// for a building no rung place needs a route through: 49's two doors are both Route 2's own. +/// - **`ROUTE_4`** (row 59): Mt. Moon stands across it. The west side holds the Pokécenter at +/// (11, 5), the cave mouth at (18, 5) and the road down to Route 3; the east side holds B1F's +/// exit at (24, 5) and the ledges down to Cerulean. From the Pewter side the only way east is +/// through the mountain. +/// - **`MT_MOON_B1F`** (row 59): four chambers, each two ladders and nothing between them. The +/// one road through is 1F (5, 5) to the west chamber, (21, 17) down to B2F, B2F (5, 7) up to the +/// exit chamber, (27, 3) out onto Route 4's east side. The middle and south chambers are ladders +/// to dead ends on B2F. +/// - **`MT_MOON_B2F`** (row 59): the fossil floor, one large piece with the two ladders the road +/// uses, and two small pieces under the dead-end ladders. +const SPLIT: &[Split] = &[ + Split { + map: maps::ROUTE_2, + pieces: &[ + Piece { + doors: &[(1, 3, 11)], + next: &[ + Region::whole(maps::PEWTER_CITY), + Region::whole(maps::VIRIDIAN_FOREST_NORTH_GATE), + ], + }, + Piece { + doors: &[(5, 3, 43)], + next: &[ + Region::whole(maps::VIRIDIAN_CITY), + Region::whole(maps::VIRIDIAN_FOREST_SOUTH_GATE), + ], + }, + ], + }, + Split { + map: maps::ROUTE_4, + pieces: &[ + Piece { + doors: &[(0, 11, 5), (1, 18, 5)], + next: &[Region::whole(maps::ROUTE_3), Region::whole(maps::MT_MOON_1F)], + }, + Piece { + doors: &[(2, 24, 5)], + next: &[ + Region::piece(maps::MT_MOON_B1F, B1F_EXIT), + Region::whole(maps::CERULEAN_CITY), + ], + }, + ], + }, + Split { + map: maps::MT_MOON_B1F, + pieces: &[ + Piece { + doors: &[(6, 23, 3), (7, 27, 3)], + next: &[ + Region::piece(maps::MT_MOON_B2F, B2F_MAIN), + Region::piece(maps::ROUTE_4, EAST_SIDE), + ], + }, + Piece { + doors: &[(0, 5, 5), (4, 21, 17)], + next: &[ + Region::whole(maps::MT_MOON_1F), + Region::piece(maps::MT_MOON_B2F, B2F_MAIN), + ], + }, + Piece { + doors: &[(1, 17, 11), (2, 25, 9)], + next: &[ + Region::whole(maps::MT_MOON_1F), + Region::piece(maps::MT_MOON_B2F, B2F_NORTH), + ], + }, + Piece { + doors: &[(3, 25, 15), (5, 13, 27)], + next: &[ + Region::whole(maps::MT_MOON_1F), + Region::piece(maps::MT_MOON_B2F, B2F_SOUTH), + ], + }, + ], + }, + Split { + map: maps::MT_MOON_B2F, + pieces: &[ + Piece { + doors: &[(1, 21, 17), (3, 5, 7)], + next: &[ + Region::piece(maps::MT_MOON_B1F, B1F_EXIT), + Region::piece(maps::MT_MOON_B1F, B1F_WEST), + ], + }, + Piece { + doors: &[(0, 25, 9)], + next: &[Region::piece(maps::MT_MOON_B1F, B1F_MIDDLE)], + }, + Piece { + doors: &[(2, 15, 27)], + next: &[Region::piece(maps::MT_MOON_B1F, B1F_SOUTH)], + }, + ], + }, +]; fn split_of(map: u8) -> Option<&'static Split> { SPLIT.iter().find(|split| split.map == map) } -/// The piece of `map` a tile on row `y` is in. -/// -/// For every map but [`SPLIT`]'s rows this is [`Region::whole`]. Callers pass the player's own -/// row, which is the only thing that can tell the two halves of `ROUTE_2` apart. -pub fn region_at(map: u8, y: u8) -> Region { - match split_of(map) { - None => Region::whole(map), - Some(split) => { - let north = y.abs_diff(split.north_door); - let south = y.abs_diff(split.south_door); - Region { map, part: if north <= south { NORTH_PIECE } else { SOUTH_PIECE } } - } - } +/// The pieces of a split map with their numbers, which are their [`Region::part`]s. +fn pieces(split: &'static Split) -> impl Iterator { + split.pieces.iter().enumerate().filter_map(|(part, piece)| Some((u8::try_from(part).ok()?, piece))) } -/// The piece of `map` that `from` opens onto, or `None` when no piece of it touches `from`. +/// The piece of `map` the tile `(x, y)` is in, by the doors alone. /// -/// This is the reverse of [`region_at`] and it needs no tile: a door or an edge is listed under -/// exactly one piece, so "which half of Route 2 does the north gate open onto" is a table lookup. -/// `None` is an edge the graph does not have -- the south half of Route 2 is not reachable from -/// Pewter City, whatever the map ids alone would suggest. -fn region_toward(map: u8, from: u8) -> Option { - match split_of(map) { - None => Some(Region::whole(map)), - Some(split) => { - if split.north.contains(&from) { - Some(Region { map, part: NORTH_PIECE }) - } else if split.south.contains(&from) { - Some(Region { map, part: SOUTH_PIECE }) - } else { - None - } +/// For every map but [`SPLIT`]'s rows this is [`Region::whole`]. On a split map it is the piece +/// with the nearest door, counting tiles across and down, the first piece on a tie. That is exact +/// for every tile of Route 2's and Route 4's ground, which is where the grid cannot answer +/// ([`region_on`]: a ledge is a one-way step the grid does not model), and it is only the fallback +/// on Mt. Moon's floors, whose chambers wrap round each other. +pub fn region_at(map: u8, x: u8, y: u8) -> Region { + let Some(split) = split_of(map) else { return Region::whole(map) }; + let distance = |piece: &Piece| { + piece + .doors + .iter() + .map(|(_, dx, dy)| u16::from(x.abs_diff(*dx)) + u16::from(y.abs_diff(*dy))) + .min() + .unwrap_or(u16::MAX) + }; + let part = pieces(split).min_by_key(|(part, piece)| (distance(piece), *part)).map_or(0, |(part, _)| part); + Region { map, part } +} + +/// The piece of `map` the fly standing on `(x, y)` is in. +/// +/// The ground decides: with the decoded map grid (section 15) the piece is the one whose doors a +/// walk from here can reach, when exactly one piece's can. The grid has no ledges -- a ledge is a +/// step one way only, and the grid reads it as a wall -- so on the part of Route 4 below the +/// ledges no door is reachable and [`region_at`] answers from the doors. Row 59 flooded every tile +/// of every piece's ground in the disassembly under this rule and it names the right piece for +/// all of them. +pub fn region_on(map: u8, x: u8, y: u8, grid: Option<&MapGrid>) -> Region { + let Some(split) = split_of(map) else { return Region::whole(map) }; + if let Some(grid) = grid.filter(|grid| grid.map() == map) { + let walk = grid.reachable(x, y); + // A door tile the collision list refuses is still stepped onto from beside it. + let reached = |dx: u8, dy: u8| { + walk.contains(dx, dy) + || [(0i16, 1i16), (0, -1), (1, 0), (-1, 0)].iter().any(|(ox, oy)| { + match (u8::try_from(i16::from(dx) + ox), u8::try_from(i16::from(dy) + oy)) { + (Ok(nx), Ok(ny)) => walk.contains(nx, ny), + _ => false, + } + }) + }; + let mut hit = + pieces(split).filter(|(_, piece)| piece.doors.iter().any(|(_, dx, dy)| reached(*dx, *dy))); + if let (Some((part, _)), None) = (hit.next(), hit.next()) { + return Region { map, part }; } } + region_at(map, x, y) +} + +/// The piece of `map` a warp lands in when it names `map`'s warp `index` as its destination. +/// +/// `None` only for a split map whose table does not list that warp, which is not guessed at. +pub fn arrival_by_warp(map: u8, index: u8) -> Option { + let Some(split) = split_of(map) else { return Some(Region::whole(map)) }; + pieces(split) + .find(|(_, piece)| piece.doors.iter().any(|(door, _, _)| *door == index)) + .map(|(part, _)| Region { map, part }) +} + +/// The piece of `map` that stepping off an edge of `from` lands in. +/// +/// An edge is listed under exactly one piece of a split map: Pewter's south edge opens onto Route +/// 2's north half, Route 3's north edge onto Route 4's west side and Cerulean's west edge onto its +/// east side. `None` is an edge the graph does not have. +pub fn arrival_by_edge(map: u8, from: u8) -> Option { + let Some(split) = split_of(map) else { return Some(Region::whole(map)) }; + pieces(split) + .find(|(_, piece)| piece.next.iter().any(|next| next.map == from)) + .map(|(part, _)| Region { map, part }) } /// Every piece of ground one step from `region`. fn region_neighbours(region: Region) -> Vec { - let of = |map: u8| region_toward(map, region.map); - match split_of(region.map) { - None => neighbours(region.map).into_iter().filter_map(of).collect(), - Some(split) => { - let own = if region.part == NORTH_PIECE { split.north } else { split.south }; - own.iter().copied().filter_map(of).collect() + if let Some(split) = split_of(region.map) { + return split.pieces.get(usize::from(region.part)).map_or_else(Vec::new, |piece| piece.next.to_vec()); + } + // A whole map steps onto every piece of a split neighbour that lists it back: Mt. Moon's + // first floor has a ladder into three of B1F's four chambers. + let mut out = Vec::new(); + for map in neighbours(region.map) { + match split_of(map) { + None => out.push(Region::whole(map)), + Some(split) => out.extend( + pieces(split) + .filter(|(_, piece)| piece.next.contains(®ion)) + .map(|(part, _)| Region { map, part }), + ), } } + out } /// The map on the other side of `map`'s `edge`, or `None` where the table does not know. @@ -321,16 +483,23 @@ pub fn neighbours(map: u8) -> Vec { /// an unreachable one, and for `from == to` -- there is no hop to take when the fly is already /// there, and `GO OBJECTIVE` has its own answer for that case. pub fn next_hop(from: Region, to: u8) -> Option { + next_step(from, to).map(|hop| hop.map) +} + +/// [`next_hop`] with the piece it lands in, which is what an exit has to match on a map with two +/// doors into one split map: three of Mt. Moon's first-floor ladders go down to B1F, and only +/// one of them reaches the way out ([`arrival_by_warp`] names where each one lands). +pub fn next_step(from: Region, to: u8) -> Option { if from.map == to { return None; } let mut seen: HashSet = HashSet::from([from]); // piece -> the first hop out of `from` that reaches it - let mut first: HashMap = HashMap::new(); + let mut first: HashMap = HashMap::new(); let mut queue: VecDeque = VecDeque::new(); for hop in region_neighbours(from) { if seen.insert(hop) { - first.insert(hop, hop.map); + first.insert(hop, hop); queue.push_back(hop); } } @@ -556,7 +725,7 @@ mod tests { assert_eq!(next_hop(at(maps::OAKS_LAB), maps::VIRIDIAN_MART), Some(maps::PALLET_TOWN)); // Upstairs is two hops from the town, through the ground floor. assert_eq!(next_hop(at(maps::PALLET_TOWN), maps::REDS_HOUSE_2F), Some(maps::REDS_HOUSE_1F)); - // Through the cave, because Route 3 and Route 4 are the same two maps either way round. + // Out along Route 3, whose only other end is the road up to Mt. Moon. assert_eq!(next_hop(at(maps::PEWTER_CITY), maps::CERULEAN_GYM), Some(maps::ROUTE_3)); // Nowhere to go, and nowhere known. assert_eq!(next_hop(at(maps::PALLET_TOWN), maps::PALLET_TOWN), None); @@ -564,37 +733,169 @@ mod tests { } #[test] - fn a_split_maps_pieces_divide_its_neighbours_between_them() { - // The invariant that keeps [`SPLIT`] honest: a piece's own list is a real subset of the - // map's neighbours, the two pieces together are all of them, and neither claims the same - // neighbour twice. A typo here is a road that does not exist. + fn a_split_maps_pieces_add_up_and_answer_each_other() { + // The invariants that keep [`SPLIT`] honest. A typo in any of them is a road that does + // not exist, or a door that leads nowhere. for split in SPLIT { - let mut both: Vec = - split.north.iter().chain(split.south.iter()).copied().collect(); - both.sort_unstable(); - let mut once = both.clone(); - once.dedup(); - assert_eq!(both, once, "{:#04x} lists a neighbour under both pieces", split.map); + // The pieces' neighbours together are exactly the map's. + let mut maps_of: Vec = + split.pieces.iter().flat_map(|piece| piece.next.iter().map(|r| r.map)).collect(); + maps_of.sort_unstable(); + maps_of.dedup(); assert_eq!( - both, + maps_of, neighbours(split.map), "{:#04x}'s pieces do not add up to its neighbours", split.map ); - assert_ne!(split.north_door, split.south_door); + let mut doors: Vec = + split.pieces.iter().flat_map(|piece| piece.doors.iter().map(|d| d.0)).collect(); + doors.sort_unstable(); + let count = doors.len(); + doors.dedup(); + assert_eq!(doors.len(), count, "{:#04x} lists one warp under two pieces", split.map); + for (part, piece) in pieces(split) { + let here = Region { map: split.map, part }; + assert!(!piece.doors.is_empty(), "{here:?} has no door to be told apart by"); + // A door's own tile is in its own piece. + for (_, x, y) in piece.doors { + assert_eq!(region_at(split.map, *x, *y), here, "door ({x}, {y})"); + } + // Every step is listed back from the other side, so a route is reversible. + for next in piece.next { + assert!( + region_neighbours(*next).contains(&here), + "{next:?} does not step back onto {here:?}" + ); + if let Some(other) = split_of(next.map) { + assert!(usize::from(next.part) < other.pieces.len(), "{next:?}"); + } + } + } } } + #[test] + fn the_road_from_pewter_to_cerulean_is_through_mt_moon_one_chamber_at_a_time() { + // Row 59. Every step of the road, as the pieces the fly stands in, measured from the + // disassembly: Route 3's top edge, Route 4's west side, the cave mouth at (18, 5), 1F's + // ladder at (5, 5), B1F's west chamber, its ladder at (21, 17), B2F, its ladder at (5, 7), + // B1F's exit chamber, (27, 3), Route 4's east side, Cerulean. + let road = [ + Region::whole(maps::PEWTER_CITY), + Region::whole(maps::ROUTE_3), + Region::piece(maps::ROUTE_4, WEST_SIDE), + Region::whole(maps::MT_MOON_1F), + Region::piece(maps::MT_MOON_B1F, B1F_WEST), + Region::piece(maps::MT_MOON_B2F, B2F_MAIN), + Region::piece(maps::MT_MOON_B1F, B1F_EXIT), + Region::piece(maps::ROUTE_4, EAST_SIDE), + Region::whole(maps::CERULEAN_CITY), + ]; + for pair in road.windows(2) { + assert_eq!(next_step(pair[0], maps::CERULEAN_CITY), Some(pair[1]), "from {:?}", pair[0]); + } + assert_eq!(hops(road[0], maps::CERULEAN_CITY), Some(8)); + // Mt. Moon's rung is the first floor, one hop from the cave mouth's side of Route 4 -- + // which is where the Pewter ring said the fly could never get to. + assert_eq!(next_hop(Region::whole(maps::PEWTER_CITY), maps::MT_MOON_1F), Some(maps::ROUTE_3)); + assert_eq!(next_hop(Region::whole(maps::ROUTE_3), maps::MT_MOON_1F), Some(maps::ROUTE_4)); + assert_eq!( + next_hop(Region::piece(maps::ROUTE_4, WEST_SIDE), maps::MT_MOON_1F), + Some(maps::MT_MOON_1F) + ); + // The dead ends lead back the way they came. + assert_eq!( + next_step(Region::piece(maps::MT_MOON_B1F, B1F_MIDDLE), maps::CERULEAN_CITY), + Some(Region::whole(maps::MT_MOON_1F)) + ); + assert_eq!( + next_step(Region::piece(maps::MT_MOON_B2F, B2F_SOUTH), maps::CERULEAN_CITY), + Some(Region::piece(maps::MT_MOON_B1F, B1F_SOUTH)) + ); + // From Cerulean's side of the mountain the way back to Pewter is the cave, not Route 4's + // south edge, which is on the other side. + assert_eq!( + next_step(Region::piece(maps::ROUTE_4, EAST_SIDE), maps::PEWTER_CITY), + Some(Region::piece(maps::MT_MOON_B1F, B1F_EXIT)) + ); + assert_eq!( + next_step(Region::whole(maps::CERULEAN_CITY), maps::ROUTE_24), + Some(Region::whole(maps::ROUTE_24)) + ); + assert_eq!(next_hop(Region::whole(maps::ROUTE_24), maps::ROUTE_25), Some(maps::ROUTE_25)); + } + + #[test] + fn a_door_or_an_edge_lands_in_the_piece_it_opens_onto() { + // Which warp of the destination a warp names is the cartridge's own answer (`wWarpEntries` + // byte 2, 0-based): 1F's ladders are B1F's warps 0, 2 and 3. + assert_eq!(arrival_by_warp(maps::MT_MOON_B1F, 0), Some(Region::piece(maps::MT_MOON_B1F, B1F_WEST))); + assert_eq!(arrival_by_warp(maps::MT_MOON_B1F, 2), Some(Region::piece(maps::MT_MOON_B1F, B1F_MIDDLE))); + assert_eq!(arrival_by_warp(maps::MT_MOON_B1F, 3), Some(Region::piece(maps::MT_MOON_B1F, B1F_SOUTH))); + // B1F's exit is `LAST_MAP` warp 2: Route 4's (24, 5), the far side of the mountain. + assert_eq!(arrival_by_warp(maps::ROUTE_4, 2), Some(Region::piece(maps::ROUTE_4, EAST_SIDE))); + // 1F's doormat is `LAST_MAP` warp 1: the cave mouth, the Pewter side. + assert_eq!(arrival_by_warp(maps::ROUTE_4, 1), Some(Region::piece(maps::ROUTE_4, WEST_SIDE))); + // The forest gates' doormats onto Route 2 are its warps 1 and 5. + assert_eq!(arrival_by_warp(maps::ROUTE_2, 1), Some(Region::piece(maps::ROUTE_2, NORTH_PIECE))); + assert_eq!(arrival_by_warp(maps::ROUTE_2, 5), Some(Region::piece(maps::ROUTE_2, SOUTH_PIECE))); + // A warp the table does not list is not guessed at; a whole map is always whole. + assert_eq!(arrival_by_warp(maps::ROUTE_2, 0), None); + assert_eq!(arrival_by_warp(maps::PEWTER_GYM, 0), Some(Region::whole(maps::PEWTER_GYM))); + // Edges. + assert_eq!(arrival_by_edge(maps::ROUTE_4, maps::ROUTE_3), Some(Region::piece(maps::ROUTE_4, WEST_SIDE))); + assert_eq!( + arrival_by_edge(maps::ROUTE_4, maps::CERULEAN_CITY), + Some(Region::piece(maps::ROUTE_4, EAST_SIDE)) + ); + assert_eq!(arrival_by_edge(maps::ROUTE_2, maps::PEWTER_CITY), Some(Region::piece(maps::ROUTE_2, NORTH_PIECE))); + assert_eq!(arrival_by_edge(maps::ROUTE_3, maps::ROUTE_4), Some(Region::whole(maps::ROUTE_3))); + } + + #[test] + fn route_4s_sides_are_told_apart_by_the_doors_where_the_ground_cannot() { + // With no grid, the nearest door: the cave mouth's side reaches down to Route 3's road at + // (7..11, 17), and Cerulean's side is everything east of the mountain. + assert_eq!(region_at(maps::ROUTE_4, 9, 17), Region::piece(maps::ROUTE_4, WEST_SIDE)); + assert_eq!(region_at(maps::ROUTE_4, 18, 6), Region::piece(maps::ROUTE_4, WEST_SIDE)); + assert_eq!(region_at(maps::ROUTE_4, 24, 6), Region::piece(maps::ROUTE_4, EAST_SIDE)); + assert_eq!(region_at(maps::ROUTE_4, 89, 10), Region::piece(maps::ROUTE_4, EAST_SIDE)); + assert_eq!(region_at(maps::ROUTE_3, 60, 0), Region::whole(maps::ROUTE_3)); + // B2F's chambers wrap round each other, so there the grid decides. + let mut grid = MapGrid::new(maps::MT_MOON_B2F, 40, 36); + // A corridor from B2F's (5, 7) ladder east along row 7 and down column 33 to (33, 31): + // nearer the (15, 27) ladder than either of its own, and on the main piece. + for x in 4..=33 { + grid.set(x, 7, 0, super::super::state::Walkable::Yes); + } + for y in 7..=31 { + grid.set(33, y, 0, super::super::state::Walkable::Yes); + } + assert_eq!(region_at(maps::MT_MOON_B2F, 33, 31), Region::piece(maps::MT_MOON_B2F, B2F_SOUTH)); + assert_eq!( + region_on(maps::MT_MOON_B2F, 33, 31, Some(&grid)), + Region::piece(maps::MT_MOON_B2F, B2F_MAIN) + ); + // A grid of another map says nothing, and neither does none. + let other = MapGrid::new(maps::ROUTE_4, 90, 18); + assert_eq!( + region_on(maps::MT_MOON_B2F, 33, 31, Some(&other)), + Region::piece(maps::MT_MOON_B2F, B2F_SOUTH) + ); + assert_eq!(region_on(maps::MT_MOON_B2F, 33, 31, None), Region::piece(maps::MT_MOON_B2F, B2F_SOUTH)); + } + #[test] fn route_2s_halves_are_told_apart_by_the_row_the_fly_is_standing_on() { // The two doorways are rows 11 and 43, measured from the cartridge's own warp table. - assert_eq!(region_at(maps::ROUTE_2, 11).part, NORTH_PIECE); - assert_eq!(region_at(maps::ROUTE_2, 43).part, SOUTH_PIECE); - assert_eq!(region_at(maps::ROUTE_2, 0).part, NORTH_PIECE, "Pewter's end"); - assert_eq!(region_at(maps::ROUTE_2, 71).part, SOUTH_PIECE, "Viridian's end"); + assert_eq!(region_at(maps::ROUTE_2, 3, 11).part, NORTH_PIECE); + assert_eq!(region_at(maps::ROUTE_2, 3, 43).part, SOUTH_PIECE); + assert_eq!(region_at(maps::ROUTE_2, 8, 0).part, NORTH_PIECE, "Pewter's end"); + assert_eq!(region_at(maps::ROUTE_2, 8, 71).part, SOUTH_PIECE, "Viridian's end"); // Every other map is one piece, whatever row is asked about. - assert_eq!(region_at(maps::ROUTE_1, 30), Region::whole(maps::ROUTE_1)); - assert_eq!(region_at(maps::VIRIDIAN_FOREST_SOUTH_GATE, 7).part, 0); + assert_eq!(region_at(maps::ROUTE_1, 10, 30), Region::whole(maps::ROUTE_1)); + assert_eq!(region_at(maps::VIRIDIAN_FOREST_SOUTH_GATE, 4, 7).part, 0); } #[test] @@ -614,16 +915,16 @@ mod tests { Some(maps::ROUTE_2) ); // Route 2's north half steps off its own top edge; its south half walks to the gate. - assert_eq!(next_hop(region_at(maps::ROUTE_2, 11), maps::PEWTER_CITY), Some(maps::PEWTER_CITY)); + assert_eq!(next_hop(region_at(maps::ROUTE_2, 3, 11), maps::PEWTER_CITY), Some(maps::PEWTER_CITY)); assert_eq!( - next_hop(region_at(maps::ROUTE_2, 43), maps::PEWTER_CITY), + next_hop(region_at(maps::ROUTE_2, 3, 43), maps::PEWTER_CITY), Some(maps::VIRIDIAN_FOREST_SOUTH_GATE) ); // From Viridian City the first hop is still Route 2, which is the road the fly takes north. assert_eq!(next_hop(Region::whole(maps::VIRIDIAN_CITY), maps::PEWTER_CITY), Some(maps::ROUTE_2)); // And the way back south from the north half is the forest, not Route 2's own bottom edge. assert_eq!( - next_hop(region_at(maps::ROUTE_2, 11), maps::VIRIDIAN_CITY), + next_hop(region_at(maps::ROUTE_2, 3, 11), maps::VIRIDIAN_CITY), Some(maps::VIRIDIAN_FOREST_NORTH_GATE) ); } @@ -681,11 +982,11 @@ mod tests { assert_eq!(here.map, maps::PEWTER_GYM); assert_eq!(steps, 3); // A split map is measured from the piece the fly is standing in, exactly as `next_hop` is. - assert_eq!(hops(region_at(maps::ROUTE_2, 11), maps::PEWTER_CITY), Some(1)); + assert_eq!(hops(region_at(maps::ROUTE_2, 3, 11), maps::PEWTER_CITY), Some(1)); // Five from the south half, because the belt of trees between the halves needs CUT and // the road is the forest: the south gate, the forest, the north gate, Route 2's north // half, Pewter. - assert_eq!(hops(region_at(maps::ROUTE_2, 43), maps::PEWTER_CITY), Some(5)); + assert_eq!(hops(region_at(maps::ROUTE_2, 3, 43), maps::PEWTER_CITY), Some(5)); // And nothing is guessed. assert_eq!(hops(at(maps::PALLET_TOWN), 0xf0), None); } 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 588fc86..291921d 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 @@ -1422,14 +1422,47 @@ fn exit_tiers(state: &mut dyn MacroState, way: Way) -> Vec { /// was in. Empty when there is no objective and when it is on this map. pub fn toward_objective(state: &mut dyn MacroState, candidates: &[Exit]) -> Vec { let Some(objective) = objective_place(state) else { return Vec::new() }; - let Some(player) = state.player() else { return Vec::new() }; - let here = player.map; + let Some(from) = region_here(state) else { return Vec::new() }; + let here = from.map; if here == objective.map { return Vec::new(); } - let hop = geography::next_hop(geography::region_at(here, player.y), objective.map); - let aim = hop.unwrap_or(objective.map); - candidates.iter().copied().filter(|exit| exit.destination(here) == Some(aim)).collect() + match geography::next_step(from, objective.map) { + Some(hop) => { + candidates.iter().copied().filter(|exit| leads_to(state, exit, here, hop)).collect() + } + None => candidates + .iter() + .copied() + .filter(|exit| exit.destination(here) == Some(objective.map)) + .collect(), + } +} + +/// The piece of ground the fly is standing in: its map, and on a map whose ground is in pieces +/// the piece its walk can reach the doors of (`docs/design/macros.md` sections 12.7 and 12.23). +pub fn region_here(state: &mut dyn MacroState) -> Option { + let player = state.player()?; + let grid = state.map_grid(); + Some(geography::region_on(player.map, player.x, player.y, grid.as_deref())) +} + +/// Whether `exit` takes the fly onto `hop`: the map on the other side, and on a map whose ground +/// is in pieces, the piece it lands in. A warp names the destination's warp it arrives at, which +/// is what tells Mt. Moon's three ladders down to B1F apart (section 12.23); an edge lands in the +/// piece that lists the map it is stepped off. A landing the table cannot name is not a match. +fn leads_to(state: &mut dyn MacroState, exit: &Exit, here: u8, hop: geography::Region) -> bool { + if exit.destination(here) != Some(hop.map) { + return false; + } + let landing = match exit.id { + ExitId::Warp(index) => state + .warps() + .get(usize::from(index)) + .and_then(|warp| geography::arrival_by_warp(hop.map, warp.destination_warp)), + ExitId::Edge(_) => geography::arrival_by_edge(hop.map, here), + }; + landing == Some(hop) } /// The people on this map still worth walking to, each with the key the ledgers name it by. @@ -1768,9 +1801,10 @@ pub fn goals_toward(state: &mut dyn MacroState, target: u8) -> Vec { }) .collect() }; - if let Some(hop) = geography::next_hop(geography::region_at(here, player.y), target) { + let hop = region_here(state).and_then(|from| geography::next_step(from, target)); + if let Some(hop) = hop { let toward: Vec = - exits.iter().copied().filter(|exit| exit.destination(here) == Some(hop)).collect(); + exits.iter().copied().filter(|exit| leads_to(state, exit, here, hop)).collect(); if !toward.is_empty() { return of(toward); } diff --git a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/state.rs b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/state.rs index 0b40cab..645813b 100644 --- a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/state.rs +++ b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/state.rs @@ -536,18 +536,22 @@ impl MapGrid { /// reachable ones is fenced in, and no amount of re-planning is going to help it /// (`docs/design/macros.md` section 15, `examples/scene_probe.rs`). pub fn reachable_from(&self, x: u8, y: u8) -> usize { - if self.index(x, y).is_none() { - return 0; - } + self.reachable(x, y).iter().filter(|seen| **seen).count() + } + + /// Whether `(tx, ty)` is among the tiles [`MapGrid::reachable_from`] counts from `(x, y)`. + /// + /// The whole flood at once, row-major like the grid itself, so a caller asking about several + /// tiles pays for one walk. Off the map is never reachable. + pub fn reachable(&self, x: u8, y: u8) -> Reachable { let mut seen = vec![false; self.tiles.len()]; + let Some(start) = self.index(x, y) else { + return Reachable { width: self.width, seen }; + }; + seen[start] = true; let mut queue = std::collections::VecDeque::new(); - if let Some(index) = self.index(x, y) { - seen[index] = true; - } queue.push_back((x, y)); - let mut count = 0; while let Some((tx, ty)) = queue.pop_front() { - count += 1; for facing in [Facing::Up, Facing::Down, Facing::Left, Facing::Right] { if self.walled(tx, ty, facing) { continue; @@ -563,7 +567,30 @@ impl MapGrid { queue.push_back((nx, ny)); } } - count + Reachable { width: self.width, seen } + } +} + +/// The tiles a walk from one tile of a [`MapGrid`] could reach ([`MapGrid::reachable`]). +#[derive(Debug, Clone)] +pub struct Reachable { + width: u8, + seen: Vec, +} + +impl Reachable { + /// Whether the walk reaches `(x, y)`. + pub fn contains(&self, x: u8, y: u8) -> bool { + x < self.width + && self + .seen + .get(usize::from(y) * usize::from(self.width) + usize::from(x)) + .copied() + .unwrap_or(false) + } + + fn iter(&self) -> impl Iterator { + self.seen.iter() } } diff --git a/services/flysim/crates/flysim/examples/scene_probe.rs b/services/flysim/crates/flysim/examples/scene_probe.rs index 79ebdd7..98d4d9a 100644 --- a/services/flysim/crates/flysim/examples/scene_probe.rs +++ b/services/flysim/crates/flysim/examples/scene_probe.rs @@ -189,11 +189,12 @@ fn pad(gb: &mut Emulator, adapter: &PokemonRedReward, label: &str) { println!("- scene `{scene:?}`, player {player:?}, map {}x{}", size.width, size.height); println!("- objective: {:?}", state.objective()); if let Some(objective) = state.objective() { + let from = palette::region_here(state) + .unwrap_or(geography::Region::whole(player.map)); println!( - "- `next_hop({:?}, {:#04x})` = {:?}, neighbours {:?}", - geography::region_at(player.map, player.y), + "- `next_step({from:?}, {:#04x})` = {:?}, neighbours {:?}", objective.map, - geography::next_hop(geography::region_at(player.map, player.y), objective.map), + geography::next_step(from, objective.map), geography::neighbours(player.map) ); }