geography: a split map is pieces with their own doors and neighbours; Route 4 and Mt. Moon's lower floors, the hop matched on where an exit lands

This commit is contained in:
acamilo 2026-09-23 14:42:59 +00:00
parent 4108fec5ce
commit 97a97a501d
5 changed files with 489 additions and 127 deletions

View file

@ -349,10 +349,9 @@ impl MacroPalette for PokemonPalette {
// How far the objective is, over the same map graph `GO OBJECTIVE` walks (section // 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 // 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. // 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 objective = palette::objective_place(&mut state)?;
let hops = let hops = geography::hops(palette::region_here(&mut state)?, objective.map)?;
geography::hops(geography::region_at(player.map, player.y), objective.map)?;
Some((objective.map, hops)) Some((objective.map, hops))
}); });
*cached = Some(palette); *cached = Some(palette);

View file

@ -28,6 +28,7 @@ use std::collections::{HashMap, HashSet, VecDeque};
use super::super::maps; use super::super::maps;
use super::cartridge::Edge; use super::cartridge::Edge;
use super::state::MapGrid;
/// A column with no connection on it. /// A column with no connection on it.
const NONE: u8 = 0xff; 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 /// 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 /// 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 /// 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)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Region { pub struct Region {
pub map: u8, 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, pub part: u8,
} }
@ -162,108 +164,268 @@ impl Region {
pub const fn whole(map: u8) -> Self { pub const fn whole(map: u8) -> Self {
Self { map, part: 0 } 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. /// One piece of a split map.
const NORTH_PIECE: u8 = 0; struct Piece {
const SOUTH_PIECE: u8 = 1; /// 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 { struct Split {
map: u8, map: u8,
/// The tile rows each piece's own doorway is on, measured from the cartridge: a tile belongs pieces: &'static [Piece],
/// 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],
} }
/// 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, /// Every row is measured from the disassembly at the pinned commit: the map's blocks, its
/// the run recorded in `infra/docs/macros-traps.md` row 33). The map is 20 by 72 and its warp /// tileset's blockset and collision list, the tile-pair walls and the ledges, flooded tile by tile
/// table reads: /// (`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 | /// - **`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
/// | 0 | (12, 9) | `DIGLETTS_CAVE_ROUTE_2` (46) | /// for a building no rung place needs a route through: 49's two doors are both Route 2's own.
/// | 1 | (3, 11) | `VIRIDIAN_FOREST_NORTH_GATE` (47) | /// - **`ROUTE_4`** (row 59): Mt. Moon stands across it. The west side holds the Pokécenter at
/// | 2 | (15, 19) | `ROUTE_2_TRADE_HOUSE` (48) | /// (11, 5), the cave mouth at (18, 5) and the road down to Route 3; the east side holds B1F's
/// | 3 | (16, 35) | `ROUTE_2_GATE` (49) | /// exit at (24, 5) and the ledges down to Cerulean. From the Pewter side the only way east is
/// | 4 | (15, 39) | `ROUTE_2_GATE` (49) | /// through the mountain.
/// | 5 | (3, 43) | `VIRIDIAN_FOREST_SOUTH_GATE` (50) | /// - **`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
/// with `north: true` and `south: true` in `wCurMapConnections` — Pewter off the top row, Viridian /// exit chamber, (27, 3) out onto Route 4's east side. The middle and south chambers are ladders
/// off the bottom one. The two forest gates at rows 11 and 43 are the doorways this splits on. /// to dead ends on B2F.
/// /// - **`MT_MOON_B2F`** (row 59): the fossil floor, one large piece with the two ladders the road
/// Maps 46, 48 and 49 are deliberately *not* on the graph, which is this module's standing rule /// uses, and two small pieces under the dead-end ladders.
/// for a building no rung place needs a route through: 49's two doors are both warps of `ROUTE_2` const SPLIT: &[Split] = &[
/// itself, so it is a shortcut within one map rather than a way between two, and 46 and 48 are Split {
/// ends of the line. A route the table does not carry is simply not offered; nothing is guessed. map: maps::ROUTE_2,
const SPLIT: &[Split] = &[Split { pieces: &[
map: maps::ROUTE_2, Piece {
north_door: 11, doors: &[(1, 3, 11)],
south_door: 43, next: &[
north: &[maps::PEWTER_CITY, maps::VIRIDIAN_FOREST_NORTH_GATE], Region::whole(maps::PEWTER_CITY),
south: &[maps::VIRIDIAN_CITY, maps::VIRIDIAN_FOREST_SOUTH_GATE], 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> { fn split_of(map: u8) -> Option<&'static Split> {
SPLIT.iter().find(|split| split.map == map) SPLIT.iter().find(|split| split.map == map)
} }
/// The piece of `map` a tile on row `y` is in. /// The pieces of a split map with their numbers, which are their [`Region::part`]s.
/// fn pieces(split: &'static Split) -> impl Iterator<Item = (u8, &'static Piece)> {
/// For every map but [`SPLIT`]'s rows this is [`Region::whole`]. Callers pass the player's own split.pieces.iter().enumerate().filter_map(|(part, piece)| Some((u8::try_from(part).ok()?, piece)))
/// 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 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 /// For every map but [`SPLIT`]'s rows this is [`Region::whole`]. On a split map it is the piece
/// exactly one piece, so "which half of Route 2 does the north gate open onto" is a table lookup. /// with the nearest door, counting tiles across and down, the first piece on a tie. That is exact
/// `None` is an edge the graph does not have -- the south half of Route 2 is not reachable from /// for every tile of Route 2's and Route 4's ground, which is where the grid cannot answer
/// Pewter City, whatever the map ids alone would suggest. /// ([`region_on`]: a ledge is a one-way step the grid does not model), and it is only the fallback
fn region_toward(map: u8, from: u8) -> Option<Region> { /// on Mt. Moon's floors, whose chambers wrap round each other.
match split_of(map) { pub fn region_at(map: u8, x: u8, y: u8) -> Region {
None => Some(Region::whole(map)), let Some(split) = split_of(map) else { return Region::whole(map) };
Some(split) => { let distance = |piece: &Piece| {
if split.north.contains(&from) { piece
Some(Region { map, part: NORTH_PIECE }) .doors
} else if split.south.contains(&from) { .iter()
Some(Region { map, part: SOUTH_PIECE }) .map(|(_, dx, dy)| u16::from(x.abs_diff(*dx)) + u16::from(y.abs_diff(*dy)))
} else { .min()
None .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<Region> {
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<Region> {
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`. /// Every piece of ground one step from `region`.
fn region_neighbours(region: Region) -> Vec<Region> { fn region_neighbours(region: Region) -> Vec<Region> {
let of = |map: u8| region_toward(map, region.map); if let Some(split) = split_of(region.map) {
match split_of(region.map) { return split.pieces.get(usize::from(region.part)).map_or_else(Vec::new, |piece| piece.next.to_vec());
None => neighbours(region.map).into_iter().filter_map(of).collect(), }
Some(split) => { // A whole map steps onto every piece of a split neighbour that lists it back: Mt. Moon's
let own = if region.part == NORTH_PIECE { split.north } else { split.south }; // first floor has a ladder into three of B1F's four chambers.
own.iter().copied().filter_map(of).collect() 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(&region))
.map(|(part, _)| Region { map, part }),
),
} }
} }
out
} }
/// The map on the other side of `map`'s `edge`, or `None` where the table does not know. /// 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<u8> {
/// an unreachable one, and for `from == to` -- there is no hop to take when the fly is already /// 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. /// there, and `GO OBJECTIVE` has its own answer for that case.
pub fn next_hop(from: Region, to: u8) -> Option<u8> { pub fn next_hop(from: Region, to: u8) -> Option<u8> {
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<Region> {
if from.map == to { if from.map == to {
return None; return None;
} }
let mut seen: HashSet<Region> = HashSet::from([from]); let mut seen: HashSet<Region> = HashSet::from([from]);
// piece -> the first hop out of `from` that reaches it // piece -> the first hop out of `from` that reaches it
let mut first: HashMap<Region, u8> = HashMap::new(); let mut first: HashMap<Region, Region> = HashMap::new();
let mut queue: VecDeque<Region> = VecDeque::new(); let mut queue: VecDeque<Region> = VecDeque::new();
for hop in region_neighbours(from) { for hop in region_neighbours(from) {
if seen.insert(hop) { if seen.insert(hop) {
first.insert(hop, hop.map); first.insert(hop, hop);
queue.push_back(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)); 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. // 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)); 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)); assert_eq!(next_hop(at(maps::PEWTER_CITY), maps::CERULEAN_GYM), Some(maps::ROUTE_3));
// Nowhere to go, and nowhere known. // Nowhere to go, and nowhere known.
assert_eq!(next_hop(at(maps::PALLET_TOWN), maps::PALLET_TOWN), None); assert_eq!(next_hop(at(maps::PALLET_TOWN), maps::PALLET_TOWN), None);
@ -564,37 +733,169 @@ mod tests {
} }
#[test] #[test]
fn a_split_maps_pieces_divide_its_neighbours_between_them() { fn a_split_maps_pieces_add_up_and_answer_each_other() {
// The invariant that keeps [`SPLIT`] honest: a piece's own list is a real subset of the // The invariants that keep [`SPLIT`] honest. A typo in any of them is a road that does
// map's neighbours, the two pieces together are all of them, and neither claims the same // not exist, or a door that leads nowhere.
// neighbour twice. A typo here is a road that does not exist.
for split in SPLIT { for split in SPLIT {
let mut both: Vec<u8> = // The pieces' neighbours together are exactly the map's.
split.north.iter().chain(split.south.iter()).copied().collect(); let mut maps_of: Vec<u8> =
both.sort_unstable(); split.pieces.iter().flat_map(|piece| piece.next.iter().map(|r| r.map)).collect();
let mut once = both.clone(); maps_of.sort_unstable();
once.dedup(); maps_of.dedup();
assert_eq!(both, once, "{:#04x} lists a neighbour under both pieces", split.map);
assert_eq!( assert_eq!(
both, maps_of,
neighbours(split.map), neighbours(split.map),
"{:#04x}'s pieces do not add up to its neighbours", "{:#04x}'s pieces do not add up to its neighbours",
split.map split.map
); );
assert_ne!(split.north_door, split.south_door); let mut doors: Vec<u8> =
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] #[test]
fn route_2s_halves_are_told_apart_by_the_row_the_fly_is_standing_on() { 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. // 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, 3, 11).part, NORTH_PIECE);
assert_eq!(region_at(maps::ROUTE_2, 43).part, SOUTH_PIECE); assert_eq!(region_at(maps::ROUTE_2, 3, 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, 8, 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, 8, 71).part, SOUTH_PIECE, "Viridian's end");
// Every other map is one piece, whatever row is asked about. // 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::ROUTE_1, 10, 30), Region::whole(maps::ROUTE_1));
assert_eq!(region_at(maps::VIRIDIAN_FOREST_SOUTH_GATE, 7).part, 0); assert_eq!(region_at(maps::VIRIDIAN_FOREST_SOUTH_GATE, 4, 7).part, 0);
} }
#[test] #[test]
@ -614,16 +915,16 @@ mod tests {
Some(maps::ROUTE_2) Some(maps::ROUTE_2)
); );
// Route 2's north half steps off its own top edge; its south half walks to the gate. // 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!( 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) Some(maps::VIRIDIAN_FOREST_SOUTH_GATE)
); );
// From Viridian City the first hop is still Route 2, which is the road the fly takes north. // 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)); 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. // And the way back south from the north half is the forest, not Route 2's own bottom edge.
assert_eq!( 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) Some(maps::VIRIDIAN_FOREST_NORTH_GATE)
); );
} }
@ -681,11 +982,11 @@ mod tests {
assert_eq!(here.map, maps::PEWTER_GYM); assert_eq!(here.map, maps::PEWTER_GYM);
assert_eq!(steps, 3); assert_eq!(steps, 3);
// A split map is measured from the piece the fly is standing in, exactly as `next_hop` is. // 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 // 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 // the road is the forest: the south gate, the forest, the north gate, Route 2's north
// half, Pewter. // 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. // And nothing is guessed.
assert_eq!(hops(at(maps::PALLET_TOWN), 0xf0), None); assert_eq!(hops(at(maps::PALLET_TOWN), 0xf0), None);
} }

View file

@ -1422,14 +1422,47 @@ fn exit_tiers(state: &mut dyn MacroState, way: Way) -> Vec<Exit> {
/// was in. Empty when there is no objective and when it is on this map. /// 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<Exit> { pub fn toward_objective(state: &mut dyn MacroState, candidates: &[Exit]) -> Vec<Exit> {
let Some(objective) = objective_place(state) else { return Vec::new() }; let Some(objective) = objective_place(state) else { return Vec::new() };
let Some(player) = state.player() else { return Vec::new() }; let Some(from) = region_here(state) else { return Vec::new() };
let here = player.map; let here = from.map;
if here == objective.map { if here == objective.map {
return Vec::new(); return Vec::new();
} }
let hop = geography::next_hop(geography::region_at(here, player.y), objective.map); match geography::next_step(from, objective.map) {
let aim = hop.unwrap_or(objective.map); Some(hop) => {
candidates.iter().copied().filter(|exit| exit.destination(here) == Some(aim)).collect() 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<geography::Region> {
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. /// 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<Aim> {
}) })
.collect() .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<Exit> = let toward: Vec<Exit> =
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() { if !toward.is_empty() {
return of(toward); return of(toward);
} }

View file

@ -536,18 +536,22 @@ impl MapGrid {
/// reachable ones is fenced in, and no amount of re-planning is going to help it /// 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`). /// (`docs/design/macros.md` section 15, `examples/scene_probe.rs`).
pub fn reachable_from(&self, x: u8, y: u8) -> usize { pub fn reachable_from(&self, x: u8, y: u8) -> usize {
if self.index(x, y).is_none() { self.reachable(x, y).iter().filter(|seen| **seen).count()
return 0; }
}
/// 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 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(); let mut queue = std::collections::VecDeque::new();
if let Some(index) = self.index(x, y) {
seen[index] = true;
}
queue.push_back((x, y)); queue.push_back((x, y));
let mut count = 0;
while let Some((tx, ty)) = queue.pop_front() { while let Some((tx, ty)) = queue.pop_front() {
count += 1;
for facing in [Facing::Up, Facing::Down, Facing::Left, Facing::Right] { for facing in [Facing::Up, Facing::Down, Facing::Left, Facing::Right] {
if self.walled(tx, ty, facing) { if self.walled(tx, ty, facing) {
continue; continue;
@ -563,7 +567,30 @@ impl MapGrid {
queue.push_back((nx, ny)); 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<bool>,
}
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<Item = &bool> {
self.seen.iter()
} }
} }

View file

@ -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!("- scene `{scene:?}`, player {player:?}, map {}x{}", size.width, size.height);
println!("- objective: {:?}", state.objective()); println!("- objective: {:?}", state.objective());
if let Some(objective) = state.objective() { if let Some(objective) = state.objective() {
let from = palette::region_here(state)
.unwrap_or(geography::Region::whole(player.map));
println!( println!(
"- `next_hop({:?}, {:#04x})` = {:?}, neighbours {:?}", "- `next_step({from:?}, {:#04x})` = {:?}, neighbours {:?}",
geography::region_at(player.map, player.y),
objective.map, objective.map,
geography::next_hop(geography::region_at(player.map, player.y), objective.map), geography::next_step(from, objective.map),
geography::neighbours(player.map) geography::neighbours(player.map)
); );
} }