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:
parent
4108fec5ce
commit
97a97a501d
5 changed files with 489 additions and 127 deletions
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
||||||
const SPLIT: &[Split] = &[Split {
|
|
||||||
map: maps::ROUTE_2,
|
map: maps::ROUTE_2,
|
||||||
north_door: 11,
|
pieces: &[
|
||||||
south_door: 43,
|
Piece {
|
||||||
north: &[maps::PEWTER_CITY, maps::VIRIDIAN_FOREST_NORTH_GATE],
|
doors: &[(1, 3, 11)],
|
||||||
south: &[maps::VIRIDIAN_CITY, maps::VIRIDIAN_FOREST_SOUTH_GATE],
|
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> {
|
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(®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.
|
/// 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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue