flybrain/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/geography.rs

1127 lines
55 KiB
Rust

//! Kanto as a graph: which map is on the other side of an edge, and the way from here to there.
//!
//! `docs/design/macros.md` section 9.2. Two questions the cartridge's own WRAM cannot answer, and
//! one that it can only answer about the map that is loaded:
//!
//! - **where does this map edge go?** `wCurMapConnections` is four bits — north, south, west,
//! east — and the connected map's *id* lives in the connection headers beside it, which this
//! crate has no reviewed symbol for. So a step off an edge used to carry no destination at all
//! ([`super::path::Exit::into`] was `None`), which made it invisible to every question about
//! where the fly has already been.
//! - **which way is Viridian from here?** The warp table names the destination of each of *this*
//! map's doors and nothing further, so "the mart is two maps north" is not a thing one map's
//! data can say.
//!
//! Both are answered here, from a static adjacency table rather than from memory: the routes and
//! towns of `constants/map_constants.asm` with the connection tables of `data/maps/headers`, plus
//! the doors the ladder's own places need. That is enough for a breadth-first route over maps,
//! which is all `GO OBJECTIVE` asks for — it still finds its own way *across* the map it is
//! standing on with the collision data it can see.
//!
//! **An unknown entry is not a guess.** A map with no row in [`CONNECTIONS`] and no link in
//! [`LINKS`] has no known neighbours, which reads as "destination unknown": an exit into it counts
//! as *unvisited* (so `GO ROUTE` still offers it) and [`next_hop`] answers `None` (so
//! `GO OBJECTIVE` falls through to the next plan entry). Nothing invents a route it cannot justify,
//! and the table can grow one row at a time without any other behaviour moving.
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;
/// Compass columns of [`CONNECTIONS`], in [`Edge`]'s own order.
const NORTH: usize = 0;
const SOUTH: usize = 1;
const WEST: usize = 2;
const EAST: usize = 3;
/// Every outdoor map's connections, as north, south, west, east.
///
/// `data/maps/headers/*.asm`: each header's `connection` lines, which is the same data
/// `wCurMapConnections`' four bits are loaded from. Kanto's overworld is one grid, so the table is
/// symmetric by construction and [`neighbours`] does not rely on that — it reads both directions.
///
/// Every row is the header's own, checked line by line against the disassembly at the pinned
/// commit (row 59). Four pairs had the right neighbour in the wrong column, and a wrong column is
/// a wrong map on the other side of an edge: `ROUTE_3` / `ROUTE_4` (Route 4 is north of Route 3,
/// not east, and Route 3's top edge is the road to Mt. Moon's Pokécenter), `ROUTE_14` /
/// `ROUTE_15` and `ROUTE_24` / `ROUTE_25` (west and east, not south and north), and `ROUTE_22` /
/// `ROUTE_23`, which the table had left out. A connection nobody can walk across is still the
/// header's, and [`NO_CROSSING`] says which.
const CONNECTIONS: &[(u8, [u8; 4])] = &[
(maps::PALLET_TOWN, [maps::ROUTE_1, maps::ROUTE_21, NONE, NONE]),
(maps::VIRIDIAN_CITY, [maps::ROUTE_2, maps::ROUTE_1, maps::ROUTE_22, NONE]),
(maps::PEWTER_CITY, [NONE, maps::ROUTE_2, NONE, maps::ROUTE_3]),
(maps::CERULEAN_CITY, [maps::ROUTE_24, maps::ROUTE_5, maps::ROUTE_4, maps::ROUTE_9]),
(maps::LAVENDER_TOWN, [maps::ROUTE_10, maps::ROUTE_12, maps::ROUTE_8, NONE]),
(maps::VERMILION_CITY, [maps::ROUTE_6, NONE, NONE, maps::ROUTE_11]),
(maps::CELADON_CITY, [NONE, NONE, maps::ROUTE_16, maps::ROUTE_7]),
(maps::FUCHSIA_CITY, [NONE, maps::ROUTE_19, maps::ROUTE_18, maps::ROUTE_15]),
(maps::CINNABAR_ISLAND, [maps::ROUTE_21, NONE, NONE, maps::ROUTE_20]),
(maps::INDIGO_PLATEAU, [NONE, maps::ROUTE_23, NONE, NONE]),
(maps::SAFFRON_CITY, [maps::ROUTE_5, maps::ROUTE_6, maps::ROUTE_7, maps::ROUTE_8]),
(maps::ROUTE_1, [maps::VIRIDIAN_CITY, maps::PALLET_TOWN, NONE, NONE]),
(maps::ROUTE_2, [maps::PEWTER_CITY, maps::VIRIDIAN_CITY, NONE, NONE]),
(maps::ROUTE_3, [maps::ROUTE_4, NONE, maps::PEWTER_CITY, NONE]),
(maps::ROUTE_4, [NONE, maps::ROUTE_3, NONE, maps::CERULEAN_CITY]),
(maps::ROUTE_5, [maps::CERULEAN_CITY, maps::SAFFRON_CITY, NONE, NONE]),
(maps::ROUTE_6, [maps::SAFFRON_CITY, maps::VERMILION_CITY, NONE, NONE]),
(maps::ROUTE_7, [NONE, NONE, maps::CELADON_CITY, maps::SAFFRON_CITY]),
(maps::ROUTE_8, [NONE, NONE, maps::SAFFRON_CITY, maps::LAVENDER_TOWN]),
(maps::ROUTE_9, [NONE, NONE, maps::CERULEAN_CITY, maps::ROUTE_10]),
(maps::ROUTE_10, [NONE, maps::LAVENDER_TOWN, maps::ROUTE_9, NONE]),
(maps::ROUTE_11, [NONE, NONE, maps::VERMILION_CITY, maps::ROUTE_12]),
(maps::ROUTE_12, [maps::LAVENDER_TOWN, maps::ROUTE_13, maps::ROUTE_11, NONE]),
(maps::ROUTE_13, [maps::ROUTE_12, NONE, maps::ROUTE_14, NONE]),
(maps::ROUTE_14, [NONE, NONE, maps::ROUTE_15, maps::ROUTE_13]),
(maps::ROUTE_15, [NONE, NONE, maps::FUCHSIA_CITY, maps::ROUTE_14]),
(maps::ROUTE_16, [NONE, maps::ROUTE_17, NONE, maps::CELADON_CITY]),
(maps::ROUTE_17, [maps::ROUTE_16, maps::ROUTE_18, NONE, NONE]),
(maps::ROUTE_18, [maps::ROUTE_17, NONE, NONE, maps::FUCHSIA_CITY]),
(maps::ROUTE_19, [maps::FUCHSIA_CITY, NONE, maps::ROUTE_20, NONE]),
(maps::ROUTE_20, [NONE, NONE, maps::CINNABAR_ISLAND, maps::ROUTE_19]),
(maps::ROUTE_21, [maps::PALLET_TOWN, maps::CINNABAR_ISLAND, NONE, NONE]),
(maps::ROUTE_22, [maps::ROUTE_23, NONE, NONE, maps::VIRIDIAN_CITY]),
(maps::ROUTE_23, [maps::INDIGO_PLATEAU, maps::ROUTE_22, NONE, NONE]),
(maps::ROUTE_24, [NONE, maps::CERULEAN_CITY, NONE, maps::ROUTE_25]),
(maps::ROUTE_25, [NONE, NONE, maps::ROUTE_24, NONE]),
];
/// Connections in the headers that no step on foot crosses, from both sides.
///
/// Row 59, measured from the disassembly: for every connection, the tiles of this map's edge that
/// are walkable *and* land on a walkable tile of the other map's strip (the header's offset, the
/// other map's blocks and collision list). These eight have none. Pallet Town's south edge has
/// two walkable tiles and Route 21 is water under both; Cinnabar's east edge and Route 20's two
/// ends are sea; Route 22's north edge is the League's fence, and the road is its gate, a building
/// the graph has no row for. The map on the other side is still named ([`connected`]), and a
/// surfer's road is for a later row; on foot none of them is a way out
/// ([`super::path::exits`] offers no exit on them) or a road ([`neighbours`] leaves them out).
/// Without this the road from Pallet Town to Cerulean was by sea, and a fly that whited out in
/// Mt. Moon walked into Pallet's shore once every two seconds.
const NO_CROSSING: &[(u8, Edge)] = &[
(maps::PALLET_TOWN, Edge::South),
(maps::ROUTE_21, Edge::North),
(maps::CINNABAR_ISLAND, Edge::East),
(maps::ROUTE_20, Edge::West),
(maps::ROUTE_20, Edge::East),
(maps::ROUTE_19, Edge::West),
(maps::ROUTE_22, Edge::North),
(maps::ROUTE_23, Edge::South),
];
/// Whether a step off `map`'s `edge` can land on the other map on foot ([`NO_CROSSING`]).
pub fn crossable(map: u8, edge: Edge) -> bool {
!NO_CROSSING.contains(&(map, edge))
}
/// Doors and floor changes, as undirected pairs of maps.
///
/// Only the ones a rung place needs a route through, because that is all [`next_hop`] is for: an
/// unlisted building is simply not on the graph, which makes it a place `GO OBJECTIVE` cannot aim
/// at from another map and changes nothing else. Every pair is two warp tables that name each
/// other (a `LAST_MAP` door resolved to the one outdoor map whose warps lead in).
const LINKS: &[(u8, u8)] = &[
(maps::REDS_HOUSE_1F, maps::PALLET_TOWN),
(maps::REDS_HOUSE_2F, maps::REDS_HOUSE_1F),
(maps::BLUES_HOUSE, maps::PALLET_TOWN),
(maps::OAKS_LAB, maps::PALLET_TOWN),
(maps::VIRIDIAN_MART, maps::VIRIDIAN_CITY),
(maps::VIRIDIAN_POKECENTER, maps::VIRIDIAN_CITY),
(maps::VIRIDIAN_GYM, maps::VIRIDIAN_CITY),
// Route 2's two forest gates. Both rows say `ROUTE_2`, and which *piece* of Route 2 each one
// opens onto is [`SPLIT`]'s business: the map is one id with two disconnected halves, and
// without that the graph thought Pewter was two hops south of the south gate
// (`infra/docs/macros-traps.md` row 33).
(maps::VIRIDIAN_FOREST_SOUTH_GATE, maps::ROUTE_2),
(maps::VIRIDIAN_FOREST_SOUTH_GATE, maps::VIRIDIAN_FOREST),
(maps::VIRIDIAN_FOREST_NORTH_GATE, maps::ROUTE_2),
(maps::VIRIDIAN_FOREST_NORTH_GATE, maps::VIRIDIAN_FOREST),
(maps::PEWTER_GYM, maps::PEWTER_CITY),
// The museum, both floors. It is on the graph for the same reason the gym is: the rung-10
// loop of 2026-09-22 spent its hours on these two maps with `GO OBJECTIVE` off the pad
// entirely, because a map with no row here has no neighbours, so `next_hop` answers nothing
// and `area_of` answers nothing -- no road to the gym from indoors, and no errand either.
// Both rows are the cartridge's own warp table, surveyed from the rung-10 checkpoint: Pewter
// City names `$34` at (14, 7) and (19, 5), and `$34` names `$35` at (7, 7).
(maps::PEWTER_MUSEUM_1F, maps::PEWTER_CITY),
(maps::PEWTER_MUSEUM_2F, maps::PEWTER_MUSEUM_1F),
(maps::PEWTER_MART, maps::PEWTER_CITY),
(maps::PEWTER_POKECENTER, maps::PEWTER_CITY),
// Mt. Moon has two mouths, and both are on Route 4 (`data/maps/objects/Route4.asm`): (18, 5)
// into the first floor, and (24, 5) into B1F, whose (27, 3) is the way back out on the far
// side of the mountain. Route 3 has no warps at all. Which chamber of B1F and B2F each ladder
// opens onto is [`SPLIT`]'s business.
(maps::MT_MOON_1F, maps::ROUTE_4),
(maps::MT_MOON_1F, maps::MT_MOON_B1F),
(maps::MT_MOON_B1F, maps::MT_MOON_B2F),
(maps::MT_MOON_B1F, maps::ROUTE_4),
(maps::CERULEAN_GYM, maps::CERULEAN_CITY),
(maps::CERULEAN_MART, maps::CERULEAN_CITY),
(maps::CERULEAN_POKECENTER, maps::CERULEAN_CITY),
(maps::ROCK_TUNNEL_1F, maps::ROUTE_10),
(maps::INDIGO_PLATEAU_LOBBY, maps::INDIGO_PLATEAU),
];
/// One piece of walkable ground: a map, and which of its pieces when the map has more than one.
///
/// Almost every map in Kanto is one piece and `part` is 0 for all of them. The exception is the
/// thing row 33 of `infra/docs/macros-traps.md` is about: **`ROUTE_2` is one map id whose ground
/// is in two halves the player cannot walk between.** The south half touches Viridian City and
/// 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. 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: its index in that row. 0 everywhere else.
pub part: u8,
}
impl Region {
/// A map whose ground is all one piece, which is every map but [`SPLIT`]'s.
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 }
}
}
/// 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 pieces the player cannot walk between.
struct Split {
map: u8,
pieces: &'static [Piece],
}
/// 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.
///
/// 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.
///
/// - **`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 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)> {
split.pieces.iter().enumerate().filter_map(|(part, piece)| Some((u8::try_from(part).ok()?, piece)))
}
/// The piece of `map` the tile `(x, y)` is in, by the doors alone.
///
/// 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<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`.
fn region_neighbours(region: Region) -> Vec<Region> {
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(&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.
pub fn connected(map: u8, edge: Edge) -> Option<u8> {
let column = match edge {
Edge::North => NORTH,
Edge::South => SOUTH,
Edge::West => WEST,
Edge::East => EAST,
};
let row = CONNECTIONS.iter().find(|(id, _)| *id == map)?;
(row.1[column] != NONE).then_some(row.1[column])
}
/// The outdoor map a building's own front door opens onto, when the table knows.
///
/// pokered writes a front door's destination as `LAST_MAP` (`$ff`, "back where you came from"), so
/// the warp table cannot name it and this is the only place that can. Used for two things: what
/// "has the map on the other side of this door been visited" means for a `GO OUT`, and the first
/// hop of a route that starts indoors.
pub fn outdoor_of(interior: u8) -> Option<u8> {
neighbours(interior).into_iter().find(|map| super::cartridge::outdoors(*map))
}
/// Every map one step on foot from `map`, doors and edges together, deduplicated and in id order.
///
/// A connection with no crossing ([`NO_CROSSING`]) is not a step on foot and is left out.
pub fn neighbours(map: u8) -> Vec<u8> {
const EDGES: [Edge; 4] = [Edge::North, Edge::South, Edge::West, Edge::East];
let mut out: Vec<u8> = Vec::new();
if let Some(row) = CONNECTIONS.iter().find(|(id, _)| *id == map) {
out.extend(
row.1
.iter()
.zip(EDGES)
.filter(|(id, edge)| **id != NONE && crossable(map, *edge))
.map(|(id, _)| *id),
);
}
for (a, b) in LINKS {
if *a == map {
out.push(*b);
}
if *b == map {
out.push(*a);
}
}
// Symmetric closure: a row that names a neighbour is a connection whichever side lists it.
for (id, row) in CONNECTIONS {
if row.iter().zip(EDGES).any(|(other, edge)| *other == map && crossable(*id, edge)) {
out.push(*id);
}
}
out.sort_unstable();
out.dedup();
out
}
/// The neighbour of `from` on a shortest route to the map `to`, or `None` when none is known.
///
/// Breadth-first over [`neighbours`], so the answer is a *map* rather than a path: the caller aims
/// at whichever of the current map's exits leads to that one hop and asks again when it arrives,
/// which is the same re-plan-every-tile shape the walk itself has. `None` for an unknown map, for
/// 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<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 {
return None;
}
let mut seen: HashSet<Region> = HashSet::from([from]);
// piece -> the first hop out of `from` that reaches it
let mut first: HashMap<Region, Region> = HashMap::new();
let mut queue: VecDeque<Region> = VecDeque::new();
for hop in region_neighbours(from) {
if seen.insert(hop) {
first.insert(hop, hop);
queue.push_back(hop);
}
}
while let Some(region) = queue.pop_front() {
let hop = *first.get(&region)?;
if region.map == to {
return Some(hop);
}
for next in region_neighbours(region) {
if seen.insert(next) {
first.insert(next, hop);
queue.push_back(next);
}
}
}
None
}
/// How many hops the shortest known route from `from` to the map `to` takes, or `None` when none
/// is known.
///
/// [`next_hop`]'s own breadth-first walk, counting instead of naming: `Some(0)` when the fly is
/// already on `to`, `Some(1)` for a door out of this map into it, and `None` for a map the table
/// cannot route to -- which is the same "nothing is guessed" [`next_hop`] answers with.
///
/// What it is *for* is the ratchet (`docs/design/ladder.md`, the 2026-09-17 progress rule): the
/// stall window is reset by exploration, and a fly crossing a town it has already covered to
/// reach the rung's own door earns no new ground while it does it. "Nearer the objective than
/// this run has ever been" is the other thing that is plainly progress, and it is this number
/// falling. Nothing about the *choice* reads it: no macro is ranked by it and no button is bound
/// on it.
pub fn hops(from: Region, to: u8) -> Option<u32> {
if from.map == to {
return Some(0);
}
let mut seen: HashSet<Region> = HashSet::from([from]);
let mut queue: VecDeque<(Region, u32)> = VecDeque::new();
queue.push_back((from, 0));
while let Some((region, depth)) = queue.pop_front() {
if region.map == to {
return Some(depth);
}
for next in region_neighbours(region) {
if seen.insert(next) {
queue.push_back((next, depth + 1));
}
}
}
None
}
/// A building an area has at most one of, and which this run may not have been into yet.
///
/// `docs/design/macros.md` section 13: the two errands. A kind rather than two parallel tables
/// because everything about them is the same shape -- one building per area, one session ledger
/// entry per area, one walk to its door -- and the only thing that differs is which precondition
/// the button carries.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Amenity {
/// A Poké Mart.
Mart,
/// A Pokémon Center.
Center,
}
impl Amenity {
/// Both kinds, in the order section 13 lists them.
pub const ALL: [Self; 2] = [Self::Mart, Self::Center];
/// The word the log line and the tests use.
pub const fn label(self) -> &'static str {
match self {
Self::Mart => "mart",
Self::Center => "center",
}
}
}
/// Which mart and which Pokémon Center each area has, by the area's own map id.
///
/// The same standing rule as [`LINKS`] and [`SPLIT`]: **a row this table does not carry is not a
/// guess.** An area with no row has no errand, `GO SHOP` and `GO HEAL` are off its pad, and
/// `GO OBJECTIVE`'s errand list is empty there -- which is what the routes, Pallet Town (which
/// has neither) and every town this run cannot reach yet all are. The table grows a row at a time
/// as the ladder does, and nothing else moves when it does.
///
/// Every id in it is also a [`LINKS`] row, because a building with no way in is a place
/// `GO OBJECTIVE` can never route to ([`tests::every_amenity_is_on_the_map_graph`]).
const AMENITIES: &[(u8, Amenity, u8)] = &[
(maps::VIRIDIAN_CITY, Amenity::Mart, maps::VIRIDIAN_MART),
(maps::VIRIDIAN_CITY, Amenity::Center, maps::VIRIDIAN_POKECENTER),
(maps::PEWTER_CITY, Amenity::Mart, maps::PEWTER_MART),
(maps::PEWTER_CITY, Amenity::Center, maps::PEWTER_POKECENTER),
(maps::CERULEAN_CITY, Amenity::Mart, maps::CERULEAN_MART),
(maps::CERULEAN_CITY, Amenity::Center, maps::CERULEAN_POKECENTER),
];
/// The area a map belongs to: the town or route the errands are counted once per.
///
/// Outdoors a map *is* its own area. Indoors the area is the outdoor map the building's front door
/// opens onto ([`outdoor_of`]), so standing in a Viridian house, in the mart or in the gym are all
/// "in Viridian" -- which is what makes one visit per area per run a question the fly can answer
/// from wherever it is standing. `None` for a building the graph has no row for, which is most
/// houses: such a map has no area, so it offers no errand, and the errand comes back the moment
/// the fly is outside again.
pub fn area_of(map: u8) -> Option<u8> {
if super::cartridge::outdoors(map) {
return Some(map);
}
outdoor_of(map)
}
/// The map of `area`'s mart or Pokémon Center, when the table has a row for it.
pub fn amenity_of(area: u8, kind: Amenity) -> Option<u8> {
AMENITIES
.iter()
.find(|(id, of, _)| *id == area && *of == kind)
.map(|(_, _, map)| *map)
}
/// Which kind of amenity `map` *is*, when it is one.
///
/// The reverse lookup, and what tells the fly it is standing in a mart rather than in a house: the
/// shop scene's own buttons and the centre's `HEAL` are dealt on this answer rather than on a
/// tileset read, because a map id is a byte the adapter already has and a tileset is not.
pub fn amenity_at(map: u8) -> Option<Amenity> {
AMENITIES.iter().find(|(_, _, id)| *id == map).map(|(_, kind, _)| *kind)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_edge_knows_which_map_is_on_the_other_side() {
assert_eq!(connected(maps::PALLET_TOWN, Edge::North), Some(maps::ROUTE_1));
assert_eq!(connected(maps::PALLET_TOWN, Edge::South), Some(maps::ROUTE_21));
assert_eq!(connected(maps::PALLET_TOWN, Edge::East), None, "the town has no east exit");
assert_eq!(connected(maps::ROUTE_1, Edge::North), Some(maps::VIRIDIAN_CITY));
// An interior has no connections at all, and an unknown map answers nothing rather than
// guessing -- which is what keeps an exit into it a candidate.
assert_eq!(connected(maps::OAKS_LAB, Edge::North), None);
assert_eq!(connected(0xf0, Edge::North), None);
}
#[test]
fn the_overworld_grid_agrees_with_itself() {
// Every connection named in one row is named back in the other's, which is what makes a
// breadth-first route reversible and a typo in the table visible.
for (map, row) in CONNECTIONS {
for (column, other) in row.iter().enumerate() {
if *other == NONE {
continue;
}
let back = match column {
NORTH => SOUTH,
SOUTH => NORTH,
WEST => EAST,
_ => WEST,
};
let there = CONNECTIONS
.iter()
.find(|(id, _)| id == other)
.unwrap_or_else(|| panic!("{other:#04x} is connected to but has no row"));
assert_eq!(
there.1[back], *map,
"{map:#04x} and {other:#04x} disagree about which way they touch"
);
}
}
}
#[test]
fn the_rows_the_disassembly_corrected_say_what_its_headers_say() {
// Row 59, `data/maps/headers/*.asm` at the pinned commit. Route 4 is north of Route 3:
// Route 3's top edge is the road to Mt. Moon's Pokécenter, and Route 3 has no east exit.
assert_eq!(connected(maps::ROUTE_3, Edge::North), Some(maps::ROUTE_4));
assert_eq!(connected(maps::ROUTE_3, Edge::East), None);
assert_eq!(connected(maps::ROUTE_4, Edge::South), Some(maps::ROUTE_3));
assert_eq!(connected(maps::ROUTE_4, Edge::West), None);
assert_eq!(connected(maps::ROUTE_4, Edge::East), Some(maps::CERULEAN_CITY));
// Nugget Bridge's far end is Route 24's east edge, not its north one.
assert_eq!(connected(maps::ROUTE_24, Edge::East), Some(maps::ROUTE_25));
assert_eq!(connected(maps::ROUTE_24, Edge::North), None);
assert_eq!(connected(maps::ROUTE_25, Edge::West), Some(maps::ROUTE_24));
assert_eq!(connected(maps::ROUTE_25, Edge::South), None);
assert_eq!(connected(maps::ROUTE_14, Edge::West), Some(maps::ROUTE_15));
assert_eq!(connected(maps::ROUTE_15, Edge::East), Some(maps::ROUTE_14));
// Mt. Moon's two mouths are both on Route 4, and Route 3 has no warps: a cave door that
// is not there is a road the fly walks up and down for ever (row 59's Pewter ring).
assert!(!neighbours(maps::ROUTE_3).contains(&maps::MT_MOON_1F));
assert_eq!(outdoor_of(maps::MT_MOON_1F), Some(maps::ROUTE_4));
assert_eq!(outdoor_of(maps::MT_MOON_B1F), Some(maps::ROUTE_4));
assert_eq!(
neighbours(maps::ROUTE_3),
vec![maps::PEWTER_CITY, maps::ROUTE_4],
"Route 3 is a road between two maps and nothing else"
);
}
#[test]
fn a_connection_nobody_can_walk_across_is_named_and_is_not_a_road() {
// Row 59: Pallet Town's shore. The header connects it to Route 21, which is water.
assert_eq!(connected(maps::PALLET_TOWN, Edge::South), Some(maps::ROUTE_21));
assert!(!crossable(maps::PALLET_TOWN, Edge::South));
assert!(!neighbours(maps::PALLET_TOWN).contains(&maps::ROUTE_21));
assert!(!neighbours(maps::ROUTE_21).contains(&maps::PALLET_TOWN));
// So the road from Pallet Town to Cerulean is the long one on land: north, through the
// forest, Pewter and Mt. Moon, and not by sea through Cinnabar and Fuchsia.
assert_eq!(next_hop(Region::whole(maps::PALLET_TOWN), maps::CERULEAN_CITY), Some(maps::ROUTE_1));
assert_eq!(hops(Region::whole(maps::PALLET_TOWN), maps::CERULEAN_CITY), Some(16));
// Every entry is one of the header's own connections, and both sides are listed.
for (map, edge) in NO_CROSSING {
let other = connected(*map, *edge).expect("a no-crossing entry is a header connection");
let back = match edge {
Edge::North => Edge::South,
Edge::South => Edge::North,
Edge::West => Edge::East,
Edge::East => Edge::West,
};
assert!(!crossable(other, back), "{other:#04x} lists {map:#04x} as crossable");
}
// Indigo Plateau is on the graph and, until a row gives the League gate, not on foot
// from the south.
assert_eq!(connected(maps::ROUTE_22, Edge::North), Some(maps::ROUTE_23));
assert_eq!(next_hop(Region::whole(maps::VIRIDIAN_CITY), maps::INDIGO_PLATEAU), None);
}
#[test]
fn a_front_door_resolves_to_the_town_outside_it() {
assert_eq!(outdoor_of(maps::OAKS_LAB), Some(maps::PALLET_TOWN));
assert_eq!(outdoor_of(maps::REDS_HOUSE_1F), Some(maps::PALLET_TOWN));
assert_eq!(outdoor_of(maps::VIRIDIAN_MART), Some(maps::VIRIDIAN_CITY));
// A bedroom's only neighbour is the floor below, which is not outdoors.
assert_eq!(outdoor_of(maps::REDS_HOUSE_2F), None);
assert_eq!(outdoor_of(0xf0), None);
}
#[test]
fn the_first_hop_of_a_route_is_the_map_to_aim_at() {
let at = |map: u8| Region::whole(map);
// The rung-6 errand, which is the one the town loop never took: the mart is two maps north
// of Pallet Town, and the first hop is the connection the fly was walking past.
assert_eq!(next_hop(at(maps::PALLET_TOWN), maps::VIRIDIAN_MART), Some(maps::ROUTE_1));
assert_eq!(next_hop(at(maps::ROUTE_1), maps::VIRIDIAN_MART), Some(maps::VIRIDIAN_CITY));
assert_eq!(
next_hop(at(maps::VIRIDIAN_CITY), maps::VIRIDIAN_MART),
Some(maps::VIRIDIAN_MART)
);
// Out of a building first: the lab's only way anywhere is its own front door.
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));
// 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);
assert_eq!(next_hop(at(maps::PALLET_TOWN), 0xf0), None);
}
#[test]
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 {
// The pieces' neighbours together are exactly the map's.
let mut maps_of: Vec<u8> =
split.pieces.iter().flat_map(|piece| piece.next.iter().map(|r| r.map)).collect();
maps_of.sort_unstable();
maps_of.dedup();
assert_eq!(
maps_of,
neighbours(split.map),
"{:#04x}'s pieces do not add up to its neighbours",
split.map
);
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]
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, 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, 10, 30), Region::whole(maps::ROUTE_1));
assert_eq!(region_at(maps::VIRIDIAN_FOREST_SOUTH_GATE, 4, 7).part, 0);
}
#[test]
fn the_way_to_pewter_from_the_forests_south_gate_is_north_through_the_forest() {
// Row 33 of `infra/docs/macros-traps.md`, as a number. Standing in the gate house, the
// graph used to answer `ROUTE_2` -- the door the fly had just come in through -- because
// Route 2's north edge touches Pewter and the table had one node for the whole map.
let gate = Region::whole(maps::VIRIDIAN_FOREST_SOUTH_GATE);
assert_eq!(next_hop(gate, maps::PEWTER_CITY), Some(maps::VIRIDIAN_FOREST));
// And on through: the forest, the north gate, Route 2's north half, Pewter.
assert_eq!(
next_hop(Region::whole(maps::VIRIDIAN_FOREST), maps::PEWTER_CITY),
Some(maps::VIRIDIAN_FOREST_NORTH_GATE)
);
assert_eq!(
next_hop(Region::whole(maps::VIRIDIAN_FOREST_NORTH_GATE), maps::PEWTER_CITY),
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, 3, 11), maps::PEWTER_CITY), Some(maps::PEWTER_CITY));
assert_eq!(
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, 3, 11), maps::VIRIDIAN_CITY),
Some(maps::VIRIDIAN_FOREST_NORTH_GATE)
);
}
#[test]
fn the_museums_two_floors_know_the_road_to_the_gym() {
// The rung-10 loop of 2026-09-22: five and a half hours on `PEWTER_CITY` with the
// objective two doors away, and inside the museum `GO OBJECTIVE` was off the pad because
// the map had no row here at all. Both floors, because the fly spent the run on both.
let at = |map: u8| Region::whole(map);
assert_eq!(
next_hop(at(maps::PEWTER_MUSEUM_1F), maps::PEWTER_GYM),
Some(maps::PEWTER_CITY),
"the way to the gym from the museum's ground floor is out of its front door"
);
assert_eq!(
next_hop(at(maps::PEWTER_MUSEUM_2F), maps::PEWTER_GYM),
Some(maps::PEWTER_MUSEUM_1F),
"and from the upper floor it is the staircase"
);
assert_eq!(next_hop(at(maps::PEWTER_CITY), maps::PEWTER_GYM), Some(maps::PEWTER_GYM));
// The other direction, which is what `GO OBJECTIVE` asks when the errand is the museum's
// own town: the museum is one hop from Pewter City and two from its upper floor.
assert_eq!(
next_hop(at(maps::PEWTER_CITY), maps::PEWTER_MUSEUM_2F),
Some(maps::PEWTER_MUSEUM_1F)
);
// And the area, which is what puts the town's errands on the pad indoors. The upper
// floor has no area, exactly as `REDS_HOUSE_2F` has none: a floor with no front door of
// its own is not "in" anywhere, so it offers no errand -- and the road out is still the
// staircase above, which is what the fly needs there.
assert_eq!(area_of(maps::PEWTER_MUSEUM_1F), Some(maps::PEWTER_CITY));
assert_eq!(area_of(maps::PEWTER_MUSEUM_2F), None);
// The museum is neither a mart nor a centre, so it is nobody's errand.
assert_eq!(amenity_at(maps::PEWTER_MUSEUM_1F), None);
assert_eq!(amenity_at(maps::PEWTER_MUSEUM_2F), None);
}
#[test]
fn the_hop_count_is_the_road_measured_rather_than_named() {
let at = |map: u8| Region::whole(map);
assert_eq!(hops(at(maps::PEWTER_CITY), maps::PEWTER_CITY), Some(0), "already there");
assert_eq!(hops(at(maps::PEWTER_CITY), maps::PEWTER_GYM), Some(1), "one door");
assert_eq!(hops(at(maps::PEWTER_MUSEUM_1F), maps::PEWTER_GYM), Some(2));
assert_eq!(hops(at(maps::PEWTER_MUSEUM_2F), maps::PEWTER_GYM), Some(3));
// The count agrees with the hop by hop answer, which is the thing it has to: walking the
// road one `next_hop` at a time takes exactly this many steps.
let mut here = at(maps::PEWTER_MUSEUM_2F);
let mut steps = 0;
while let Some(hop) = next_hop(here, maps::PEWTER_GYM) {
here = Region::whole(hop);
steps += 1;
assert!(steps < 10, "the road to the gym does not wander");
}
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, 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, 3, 43), maps::PEWTER_CITY), Some(5));
// And nothing is guessed.
assert_eq!(hops(at(maps::PALLET_TOWN), 0xf0), None);
}
#[test]
fn a_maps_area_is_its_town_indoors_and_out() {
// Outdoors a map is its own area, including a route -- which has no amenity row, so it
// offers no errand, which is the point.
assert_eq!(area_of(maps::VIRIDIAN_CITY), Some(maps::VIRIDIAN_CITY));
assert_eq!(area_of(maps::ROUTE_1), Some(maps::ROUTE_1));
// Indoors it is the town outside the front door, so the mart, the centre, the gym and an
// ordinary house on the graph all answer the same town.
assert_eq!(area_of(maps::VIRIDIAN_MART), Some(maps::VIRIDIAN_CITY));
assert_eq!(area_of(maps::VIRIDIAN_POKECENTER), Some(maps::VIRIDIAN_CITY));
assert_eq!(area_of(maps::VIRIDIAN_GYM), Some(maps::VIRIDIAN_CITY));
assert_eq!(area_of(maps::OAKS_LAB), Some(maps::PALLET_TOWN));
// A floor with no front door of its own, and a map nobody can name, have no area.
assert_eq!(area_of(maps::REDS_HOUSE_2F), None);
assert_eq!(area_of(0xf0), None);
}
#[test]
fn an_area_knows_its_mart_and_its_centre_and_nothing_it_does_not() {
assert_eq!(amenity_of(maps::VIRIDIAN_CITY, Amenity::Mart), Some(maps::VIRIDIAN_MART));
assert_eq!(
amenity_of(maps::VIRIDIAN_CITY, Amenity::Center),
Some(maps::VIRIDIAN_POKECENTER)
);
assert_eq!(amenity_of(maps::PEWTER_CITY, Amenity::Mart), Some(maps::PEWTER_MART));
assert_eq!(amenity_of(maps::CERULEAN_CITY, Amenity::Mart), Some(maps::CERULEAN_MART));
// Pallet Town has neither, and a route has neither: no row, no errand, nothing guessed.
assert_eq!(amenity_of(maps::PALLET_TOWN, Amenity::Mart), None);
assert_eq!(amenity_of(maps::PALLET_TOWN, Amenity::Center), None);
assert_eq!(amenity_of(maps::ROUTE_1, Amenity::Center), None);
// And the reverse lookup, which is how the fly knows which building it is standing in.
assert_eq!(amenity_at(maps::VIRIDIAN_MART), Some(Amenity::Mart));
assert_eq!(amenity_at(maps::VIRIDIAN_POKECENTER), Some(Amenity::Center));
assert_eq!(amenity_at(maps::VIRIDIAN_GYM), None);
assert_eq!(amenity_at(maps::VIRIDIAN_CITY), None);
}
#[test]
fn every_amenity_is_on_the_map_graph() {
// A building with no `LINKS` row is one `GO OBJECTIVE` and `GO SHOP` could never route to,
// and the errand would sit on the pad for ever with nothing to aim at.
for (area, kind, map) in AMENITIES {
assert!(
neighbours(*map).contains(area),
"{} {:#04x} is not linked to its area {area:#04x}",
kind.label(),
map
);
assert_eq!(area_of(*map), Some(*area), "{:#04x}", map);
assert_eq!(
next_hop(Region::whole(*area), *map),
Some(*map),
"the first hop to {}'s own {} is the building itself",
area,
kind.label()
);
}
}
#[test]
fn every_rung_place_the_catalog_carries_is_on_the_graph() {
// A place with no neighbours is a place `GO OBJECTIVE` can never be routed to, which is
// worth knowing at build time rather than on the stream.
for place in crate::pokemon_red::RUNG_PLACES.iter().flatten() {
assert!(
!neighbours(place.map).is_empty(),
"rung place {:#04x} is not on the map graph",
place.map
);
}
}
}