macros: plan every walk over the map grid
route and frontier ask for the grid once per plan and fall back to the window when there is none. With it: one plan crosses a whole map, GO WARP / GO OUT / GO ROUTE route to their warp or connection tile rather than to the nearest tile of an edge that reads Unknown, a tile-pair wall is planned around instead of being learned by walking into it, and the frontier is the nearest unstood walkable tile anywhere on the map instead of the nearest one on screen.
This commit is contained in:
parent
68228ef2a1
commit
f7fa39afa7
3 changed files with 426 additions and 25 deletions
|
|
@ -13,16 +13,29 @@
|
|||
//! minimum Manhattan distance to any goal. That stays admissible because a step costs one and
|
||||
//! moves one tile.
|
||||
//!
|
||||
//! **The walkable predicate has a window.** Agent A's [`Walkable::Unknown`] is load-bearing: the
|
||||
//! tile ids a walkability test needs live in the screen buffer, so only the tiles around the
|
||||
//! player can be answered at all. An unknown tile is *expensive* to path through rather than
|
||||
//! forbidden ([`UNKNOWN_STEP`]): a known-walkable way round is always preferred, and the search
|
||||
//! steps into the unknown only when nothing known gets any closer. That is what a map edge six
|
||||
//! tiles away needs — it is off the screen by definition, so a search that refused every unknown
|
||||
//! tile could not plan a single step toward Route 1 from the middle of Pallet Town, which is half
|
||||
//! of why the fly never took it (`infra/docs/macros-bench.md`, 2026-09-16). The guess is cheap and
|
||||
//! bounded: [`super::executor`] re-plans after every tile with a per-step check that the player
|
||||
//! moved, and three failed steps abort as `Blocked`.
|
||||
//! **It plans over the whole map when the map can be decoded** (`docs/design/macros.md` section
|
||||
//! 15, the operator 2026-09-22: "the frontier and warp macros need to be map aware: A* over
|
||||
//! walkable tiles"). [`MacroState::map_grid`] is every tile of the loaded map, walkability and
|
||||
//! directed walls, decoded from the block and collision tables the cartridge has loaded
|
||||
//! ([`crate::pokemon_red::mapgrid`]). With it, one plan crosses a town: `GO WARP` routes to its
|
||||
//! warp tile, `GO OUT` and `GO ROUTE` to their door or connection tile, and the frontier is the
|
||||
//! nearest unstood ground *anywhere on the map* rather than the nearest on screen.
|
||||
//!
|
||||
//! **Without it, the window is still the fallback.** Agent A's [`Walkable::Unknown`] is
|
||||
//! load-bearing on a frame the grid cannot be decoded — no cartridge behind the seam, a battle or
|
||||
//! a text box over the map, a header that is not loaded
|
||||
//! ([`crate::pokemon_red::state::GridRefusal`] says which): the tile ids a walkability test needs
|
||||
//! live in the screen buffer then, so only the tiles around the player can be answered at all. An
|
||||
//! unknown tile is *expensive* to path through rather than forbidden ([`UNKNOWN_STEP`]): a
|
||||
//! known-walkable way round is always preferred, and the search steps into the unknown only when
|
||||
//! nothing known gets any closer. That is what a map edge six tiles away needed — it is off the
|
||||
//! screen by definition, so a search that refused every unknown tile could not plan a single step
|
||||
//! toward Route 1 from the middle of Pallet Town, which was half of why the fly never took it
|
||||
//! (`infra/docs/macros-bench.md`, 2026-09-16).
|
||||
//!
|
||||
//! Either way the walk is re-planned only when the ground says so — a refusal, or the player not
|
||||
//! where the plan expects it — which is [`super::executor`]'s committed route and not this
|
||||
//! module's business.
|
||||
//!
|
||||
//! The search still has its second answer for a goal it cannot reach at all: when no goal is
|
||||
//! reachable, it returns the route to the reachable tile that gets closest to one.
|
||||
|
|
@ -34,7 +47,24 @@ use super::cartridge::{
|
|||
Edge, ExitId, LAST_MAP, MacroState, TalkTarget, Tile, destination_outdoors, outdoors,
|
||||
};
|
||||
use super::geography;
|
||||
use super::state::{Facing, Walkable};
|
||||
use super::state::{Facing, MapGrid, Walkable};
|
||||
|
||||
/// Whether the player could stand on a tile of the current map: the grid's answer, or the
|
||||
/// window's when there is no grid.
|
||||
///
|
||||
/// One place asks the question so that the search, the exit list and the frontier cannot disagree
|
||||
/// about which reading they are on (`docs/design/macros.md` section 15).
|
||||
fn walkable_at(
|
||||
state: &mut dyn MacroState,
|
||||
grid: Option<&MapGrid>,
|
||||
x: u8,
|
||||
y: u8,
|
||||
) -> Walkable {
|
||||
match grid {
|
||||
Some(grid) => grid.walkable(x, y),
|
||||
None => state.walkable(x, y),
|
||||
}
|
||||
}
|
||||
|
||||
/// What one step onto a tile the walkable predicate cannot answer for costs.
|
||||
///
|
||||
|
|
@ -156,6 +186,10 @@ pub fn route_avoiding(
|
|||
}
|
||||
let player = state.player()?;
|
||||
let size = state.map_size()?;
|
||||
// One decode per plan, from the cache the sim loop keeps: with a grid the search is over the
|
||||
// whole map, without one it is over the ten-by-nine window as it always was.
|
||||
let grid = state.map_grid();
|
||||
let grid = grid.as_deref();
|
||||
let start = Tile::new(player.x, player.y);
|
||||
if let Some(goal) = goals.iter().position(|tile| *tile == start) {
|
||||
return Some(Route { goal: Some(goal), steps: Vec::new() });
|
||||
|
|
@ -191,6 +225,12 @@ pub fn route_avoiding(
|
|||
if refused.contains(&(tile, facing)) {
|
||||
continue;
|
||||
}
|
||||
// A step the *tables* refuse: a tile-pair collision, which is passable ground on both
|
||||
// sides and a wall between them (`mapgrid::TILE_PAIRS_LAND`). The walk used to learn
|
||||
// each of these by spending a step on it; with the grid the first plan goes round.
|
||||
if grid.is_some_and(|grid| grid.walled(tile.x, tile.y, facing)) {
|
||||
continue;
|
||||
}
|
||||
// **A tile the cartridge pushes the fly off is not a tile to walk through**, either
|
||||
// (row 37 of `infra/docs/macros-traps.md`). Excluding it as a *goal* was half the fix
|
||||
// and the measurement said so: Viridian City's (19, 9) went from 53,266 text-box
|
||||
|
|
@ -200,10 +240,11 @@ pub fn route_avoiding(
|
|||
if next != start && state.pushed_tile(next.x, next.y) {
|
||||
continue;
|
||||
}
|
||||
let step = match state.walkable(next.x, next.y) {
|
||||
let step = match walkable_at(state, grid, next.x, next.y) {
|
||||
_ if next == start => 1,
|
||||
Walkable::Yes => 1,
|
||||
// Off the screen buffer: plausible ground, priced so that anything known beats it.
|
||||
// Off the screen buffer, or a block the blockset was read short of: plausible
|
||||
// ground, priced so that anything known beats it.
|
||||
Walkable::Unknown => UNKNOWN_STEP,
|
||||
Walkable::No => continue,
|
||||
};
|
||||
|
|
@ -334,6 +375,8 @@ pub fn target_at(state: &mut dyn MacroState, tile: Tile) -> Option<TalkTarget> {
|
|||
pub fn exits(state: &mut dyn MacroState) -> Vec<Exit> {
|
||||
let Some(size) = state.map_size() else { return Vec::new() };
|
||||
let Some(player) = state.player() else { return Vec::new() };
|
||||
let grid = state.map_grid();
|
||||
let grid = grid.as_deref();
|
||||
let here_outdoors = outdoors(player.map);
|
||||
let mut out = Vec::new();
|
||||
for (index, warp) in state.warps().iter().enumerate() {
|
||||
|
|
@ -373,13 +416,16 @@ pub fn exits(state: &mut dyn MacroState) -> Vec<Exit> {
|
|||
let way = if here_outdoors { Way::Route } else { Way::Exit };
|
||||
let into = geography::connected(player.map, edge);
|
||||
for tile in edge_tiles(facing, size.width, size.height) {
|
||||
// Not `== Yes`: the walkable predicate's window is the screen, so the far edge of an
|
||||
// outdoor map reads `Unknown` from anywhere but next to it, and filtering on `Yes`
|
||||
// left a town's connections out of the exit list entirely -- which is half of why
|
||||
// nothing could ever aim at Route 1 from the middle of Pallet Town. An unknown tile is
|
||||
// a goal worth walking towards; the route search's own approach answer handles a goal
|
||||
// it cannot reach yet, and a tile that turns out to be a wall costs one blocked walk.
|
||||
if state.walkable(tile.x, tile.y) != Walkable::No {
|
||||
// Not `== Yes`: without a grid the walkable predicate's window is the screen, so the
|
||||
// far edge of an outdoor map reads `Unknown` from anywhere but next to it, and
|
||||
// filtering on `Yes` left a town's connections out of the exit list entirely -- which
|
||||
// is half of why nothing could ever aim at Route 1 from the middle of Pallet Town. An
|
||||
// unknown tile is a goal worth walking towards; the route search's own approach answer
|
||||
// handles a goal it cannot reach yet, and a tile that turns out to be a wall costs one
|
||||
// blocked walk. With a grid the answer is `Yes` or `No` for every edge tile of the map
|
||||
// and this rejects the walls, which is what lets one plan reach the right end of a
|
||||
// connection instead of the nearest of twenty tiles along it.
|
||||
if walkable_at(state, grid, tile.x, tile.y) != Walkable::No {
|
||||
out.push(Exit { id: ExitId::Edge(edge), tile, press: Some(facing), way, into });
|
||||
}
|
||||
}
|
||||
|
|
@ -396,9 +442,12 @@ pub fn exits(state: &mut dyn MacroState) -> Vec<Exit> {
|
|||
/// Each answer is a tile to stand on paired with the direction the new ground lies in, which is
|
||||
/// the same shape `GO NPC` and `GO ITEM` use -- and the same press, which in the overworld walks
|
||||
/// onto the tile when it is walkable, so the frontier the fly is looking at becomes ground it has
|
||||
/// stood on. Both tiles have to be walkable: an unreachable one is not ground, and the walkable
|
||||
/// predicate's window means the answer is always local to the player, which is what makes the
|
||||
/// re-plan after every tile do the work of a long walk.
|
||||
/// stood on. Both tiles have to be walkable: an unreachable one is not ground.
|
||||
///
|
||||
/// **With a grid this is the whole map** (`docs/design/macros.md` section 15): the nearest unstood
|
||||
/// walkable tile anywhere on it, which is what the operator asked for and what the route search
|
||||
/// then plans one walk to. Without a grid it is what it always was -- the ten-by-nine window, so
|
||||
/// the answer is local to the player and the long walk is done by re-planning.
|
||||
///
|
||||
/// Deduplicated by the tile to stand on, in tile order, so the choice between two equally near
|
||||
/// frontiers does not depend on iteration order.
|
||||
|
|
@ -415,13 +464,15 @@ pub fn exits(state: &mut dyn MacroState) -> Vec<Exit> {
|
|||
pub fn frontier(state: &mut dyn MacroState) -> Vec<(Tile, Facing)> {
|
||||
let Some(size) = state.map_size() else { return Vec::new() };
|
||||
let Some(player) = state.player() else { return Vec::new() };
|
||||
let grid = state.map_grid();
|
||||
let grid = grid.as_deref();
|
||||
let here = Tile::new(player.x, player.y);
|
||||
let held: Vec<Tile> = state.npcs().iter().map(|npc| Tile::new(npc.x, npc.y)).collect();
|
||||
let mut out: Vec<(Tile, Facing)> = Vec::new();
|
||||
for y in 0..size.height {
|
||||
for x in 0..size.width {
|
||||
let tile = Tile::new(x, y);
|
||||
if tile != here && state.walkable(x, y) != Walkable::Yes {
|
||||
if tile != here && walkable_at(state, grid, x, y) != Walkable::Yes {
|
||||
continue;
|
||||
}
|
||||
if tile != here && held.contains(&tile) {
|
||||
|
|
@ -432,7 +483,12 @@ pub fn frontier(state: &mut dyn MacroState) -> Vec<(Tile, Facing)> {
|
|||
if next.x >= size.width || next.y >= size.height || next == here {
|
||||
continue;
|
||||
}
|
||||
if state.walkable(next.x, next.y) != Walkable::Yes {
|
||||
if walkable_at(state, grid, next.x, next.y) != Walkable::Yes {
|
||||
continue;
|
||||
}
|
||||
// A step the tables refuse is not a way onto that ground, so the tile it leads to
|
||||
// is not this tile's frontier -- somebody else's, if anything reaches it.
|
||||
if grid.is_some_and(|grid| grid.walled(tile.x, tile.y, facing)) {
|
||||
continue;
|
||||
}
|
||||
if held.contains(&next) {
|
||||
|
|
|
|||
|
|
@ -3994,4 +3994,5 @@ fn a_scripted_push_back_records_the_tile_it_happened_on() {
|
|||
);
|
||||
}
|
||||
|
||||
mod map_aware;
|
||||
mod shop_purchase;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,344 @@
|
|||
//! Walks planned over the whole map, and the same walks with the window as the only reading.
|
||||
//!
|
||||
//! `docs/design/macros.md` section 15. [`super::super::path`] has two readings of the ground now
|
||||
//! and the interesting tests are the ones that tell them apart: a map bigger than the ten-by-nine
|
||||
//! window, a frontier on the far side of it, and a wall the collision list cannot predict. The
|
||||
//! fake here is deliberately not [`super::World`] — it implements the seam and nothing else, so a
|
||||
//! failure is about the search rather than about a script's frames.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::pokemon_red::macros::cartridge::{Edge, ExitId, MacroState, Tile};
|
||||
use crate::pokemon_red::macros::path::{self, Way};
|
||||
use crate::pokemon_red::macros::state::{
|
||||
BagItem, Battle, Connections, Facing, GameState, MapGrid, MapSize, Mon, Npc, Party, Pc, Player,
|
||||
Scene, Shop, Sign, StartMenu, TextBox, Walkable, Warp,
|
||||
};
|
||||
|
||||
/// A passable tile id and a wall tile id, for a grid built by hand.
|
||||
const FLOOR: u8 = 0x01;
|
||||
const WALL: u8 = 0x60;
|
||||
|
||||
/// The ground, and nothing else: the seam's questions about a map and the fly standing on it.
|
||||
struct Ground {
|
||||
map: u8,
|
||||
size: MapSize,
|
||||
player: Tile,
|
||||
/// Tiles that are not walkable; everything else inside `size` is.
|
||||
walls: BTreeSet<Tile>,
|
||||
/// Steps the cartridge refuses although both tiles are passable, as the grid records them.
|
||||
pair_walls: Vec<(Tile, Facing)>,
|
||||
/// Ground the run has stood on, which is what makes a tile not a frontier.
|
||||
stood: BTreeSet<Tile>,
|
||||
warps: Vec<Warp>,
|
||||
connections: Connections,
|
||||
npcs: Vec<Npc>,
|
||||
/// Whether the whole map is decoded, or only the window can answer.
|
||||
decoded: bool,
|
||||
}
|
||||
|
||||
impl Ground {
|
||||
/// A map `wide` by `high` tiles with the fly at `player` and every tile walkable.
|
||||
fn new(wide: u8, high: u8, player: (u8, u8)) -> Self {
|
||||
Self {
|
||||
map: 0,
|
||||
size: MapSize { width: wide, height: high },
|
||||
player: Tile::new(player.0, player.1),
|
||||
walls: BTreeSet::new(),
|
||||
pair_walls: Vec::new(),
|
||||
stood: BTreeSet::new(),
|
||||
warps: Vec::new(),
|
||||
connections: Connections::default(),
|
||||
npcs: Vec::new(),
|
||||
decoded: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn wall(mut self, x: u8, y: u8) -> Self {
|
||||
self.walls.insert(Tile::new(x, y));
|
||||
self
|
||||
}
|
||||
|
||||
/// A column of wall with one gap in it, which is the shape that tells the two readings apart.
|
||||
fn wall_column(mut self, x: u8, gap: u8) -> Self {
|
||||
for y in 0..self.size.height {
|
||||
if y != gap {
|
||||
self.walls.insert(Tile::new(x, y));
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Everything within `distance` of the fly counts as stood on, which is what a fly that has
|
||||
/// been walking around one corner of a route has.
|
||||
fn stood_around(mut self, distance: u32) -> Self {
|
||||
for y in 0..self.size.height {
|
||||
for x in 0..self.size.width {
|
||||
let tile = Tile::new(x, y);
|
||||
if tile.distance(self.player) <= distance {
|
||||
self.stood.insert(tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// The window reading only: what every walk had before section 15.
|
||||
fn window_only(mut self) -> Self {
|
||||
self.decoded = false;
|
||||
self
|
||||
}
|
||||
|
||||
fn pair_wall(mut self, from: (u8, u8), facing: Facing) -> Self {
|
||||
self.pair_walls.push((Tile::new(from.0, from.1), facing));
|
||||
self
|
||||
}
|
||||
|
||||
fn connected(mut self, connections: Connections) -> Self {
|
||||
self.connections = connections;
|
||||
self
|
||||
}
|
||||
|
||||
/// The ten-by-nine window agent A's predicate can answer for, which moves with the fly.
|
||||
fn in_window(&self, x: u8, y: u8) -> bool {
|
||||
let dx = i32::from(x) - i32::from(self.player.x);
|
||||
let dy = i32::from(y) - i32::from(self.player.y);
|
||||
(-4..=5).contains(&dx) && (-4..=4).contains(&dy)
|
||||
}
|
||||
|
||||
fn walkable_tile(&self, x: u8, y: u8) -> bool {
|
||||
x < self.size.width && y < self.size.height && !self.walls.contains(&Tile::new(x, y))
|
||||
}
|
||||
}
|
||||
|
||||
impl GameState for Ground {
|
||||
fn scene(&mut self) -> Scene {
|
||||
Scene::Overworld
|
||||
}
|
||||
|
||||
fn player(&mut self) -> Option<Player> {
|
||||
Some(Player { map: self.map, x: self.player.x, y: self.player.y, facing: Facing::Down })
|
||||
}
|
||||
|
||||
fn map_size(&mut self) -> Option<MapSize> {
|
||||
Some(self.size)
|
||||
}
|
||||
|
||||
fn party(&mut self) -> Party {
|
||||
Party { mons: Vec::<Mon>::new(), active: None }
|
||||
}
|
||||
|
||||
fn battle(&mut self) -> Option<Battle> {
|
||||
None
|
||||
}
|
||||
|
||||
fn text_box(&mut self) -> TextBox {
|
||||
TextBox { open: false, waiting: false }
|
||||
}
|
||||
|
||||
fn start_menu(&mut self) -> Option<StartMenu> {
|
||||
None
|
||||
}
|
||||
|
||||
fn shop(&mut self) -> Option<Shop> {
|
||||
None
|
||||
}
|
||||
|
||||
fn pc(&mut self) -> Option<Pc> {
|
||||
None
|
||||
}
|
||||
|
||||
fn money(&mut self) -> u32 {
|
||||
0
|
||||
}
|
||||
|
||||
fn bag(&mut self) -> Vec<BagItem> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn npcs(&mut self) -> Vec<Npc> {
|
||||
self.npcs.clone()
|
||||
}
|
||||
|
||||
fn signs(&mut self) -> Vec<Sign> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// Agent A's predicate: the window, and `Unknown` outside it.
|
||||
fn walkable(&mut self, x: u8, y: u8) -> Walkable {
|
||||
if x >= self.size.width || y >= self.size.height {
|
||||
return Walkable::No;
|
||||
}
|
||||
if !self.in_window(x, y) {
|
||||
return Walkable::Unknown;
|
||||
}
|
||||
if self.walkable_tile(x, y) { Walkable::Yes } else { Walkable::No }
|
||||
}
|
||||
|
||||
fn warps(&mut self) -> Vec<Warp> {
|
||||
self.warps.clone()
|
||||
}
|
||||
|
||||
fn connections(&mut self) -> Connections {
|
||||
self.connections
|
||||
}
|
||||
}
|
||||
|
||||
impl MacroState for Ground {
|
||||
fn map_grid(&mut self) -> Option<Arc<MapGrid>> {
|
||||
if !self.decoded {
|
||||
return None;
|
||||
}
|
||||
let mut grid = MapGrid::new(self.map, self.size.width, self.size.height);
|
||||
for y in 0..self.size.height {
|
||||
for x in 0..self.size.width {
|
||||
let walkable = self.walkable_tile(x, y);
|
||||
grid.set(
|
||||
x,
|
||||
y,
|
||||
if walkable { FLOOR } else { WALL },
|
||||
if walkable { Walkable::Yes } else { Walkable::No },
|
||||
);
|
||||
}
|
||||
}
|
||||
for (tile, facing) in &self.pair_walls {
|
||||
grid.wall(tile.x, tile.y, *facing);
|
||||
}
|
||||
Some(Arc::new(grid))
|
||||
}
|
||||
|
||||
fn tile_visited(&mut self, x: u8, y: u8) -> bool {
|
||||
self.stood.contains(&Tile::new(x, y))
|
||||
}
|
||||
}
|
||||
|
||||
/// Walk a route's presses and report where they land and whether every tile was walkable.
|
||||
fn walk(ground: &Ground, from: Tile, steps: &[Facing]) -> (Tile, bool) {
|
||||
let mut at = from;
|
||||
let mut clean = true;
|
||||
for facing in steps {
|
||||
let Some(next) = at.step(*facing) else {
|
||||
clean = false;
|
||||
break;
|
||||
};
|
||||
if !ground.walkable_tile(next.x, next.y) {
|
||||
clean = false;
|
||||
}
|
||||
at = next;
|
||||
}
|
||||
(at, clean)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_plan_crosses_a_map_larger_than_the_window() {
|
||||
// Twenty by eighteen — Pallet Town's size — with a wall down the middle and one gap in it,
|
||||
// which is where a plan has to go and is off the screen from where the fly starts.
|
||||
let start = (2, 2);
|
||||
let goal = Tile::new(18, 16);
|
||||
let mut ground = Ground::new(20, 18, start).wall_column(10, 1);
|
||||
let route = path::route(&mut ground, &[goal]).expect("a route across the map");
|
||||
assert_eq!(route.goal, Some(0), "the goal itself, not an approach to it");
|
||||
let (landed, clean) = walk(&ground, Tile::new(start.0, start.1), &route.steps);
|
||||
assert_eq!(landed, goal);
|
||||
assert!(clean, "every tile of the plan is walkable ground");
|
||||
// The gap is at the top, so the plan is longer than the Manhattan distance and knows it.
|
||||
assert!(route.steps.len() > Tile::new(start.0, start.1).distance(goal) as usize);
|
||||
|
||||
// The same map with only the window to read: the plan sets off through a wall it cannot see.
|
||||
let mut blind = Ground::new(20, 18, start).wall_column(10, 1).window_only();
|
||||
let route = path::route(&mut blind, &[goal]).expect("a route through the unknown");
|
||||
let (_, clean) = walk(&blind, Tile::new(start.0, start.1), &route.steps);
|
||||
assert!(!clean, "the window cannot see the wall, so the plan walks into it");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_frontier_is_the_nearest_unstood_tile_anywhere_on_the_map() {
|
||||
// A fly that has covered the ground around it. Nine tiles is the furthest corner of the
|
||||
// ten-by-nine window (five across and four down), so every tile the window can answer for has
|
||||
// been stood on and the nearest new ground is the first tile outside it.
|
||||
let mut ground = Ground::new(20, 18, (3, 3)).stood_around(9);
|
||||
let frontier = path::frontier(&mut ground);
|
||||
assert!(!frontier.is_empty(), "the rest of the map is still new ground");
|
||||
let (tile, facing) = frontier
|
||||
.iter()
|
||||
.copied()
|
||||
.min_by_key(|(tile, _)| tile.distance(Tile::new(3, 3)))
|
||||
.expect("a nearest frontier");
|
||||
let new_ground = tile.step(facing).expect("the ground it faces");
|
||||
assert!(!ground.tile_visited(new_ground.x, new_ground.y));
|
||||
assert_eq!(tile.distance(Tile::new(3, 3)), 9, "the near side of the unstood ground");
|
||||
// And a route to it, in one plan.
|
||||
let goals: Vec<Tile> = frontier.iter().map(|(tile, _)| *tile).collect();
|
||||
let route = path::route(&mut ground, &goals).expect("a route to the frontier");
|
||||
assert!(route.goal.is_some());
|
||||
|
||||
// The window reading has nothing to offer here at all, which is the pad the operator saw
|
||||
// hanging: every tile it can answer for has been stood on.
|
||||
let mut blind = Ground::new(20, 18, (3, 3)).stood_around(9).window_only();
|
||||
assert!(path::frontier(&mut blind).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_step_the_tables_refuse_is_planned_around_rather_than_walked_into() {
|
||||
// A tile-pair collision: both tiles passable, the step between them refused
|
||||
// (`data/tilesets/pair_collision_tile_ids.asm`). The wall is the only way east on its row, so
|
||||
// a search that did not know about it would plan straight through.
|
||||
let goal = Tile::new(3, 0);
|
||||
let mut ground = Ground::new(4, 3, (1, 0))
|
||||
.pair_wall((1, 0), Facing::Right)
|
||||
.pair_wall((2, 0), Facing::Left);
|
||||
let route = path::route(&mut ground, &[goal]).expect("a route round the pair wall");
|
||||
assert_eq!(route.steps.first(), Some(&Facing::Down), "round it, not through it");
|
||||
let (landed, clean) = walk(&ground, Tile::new(1, 0), &route.steps);
|
||||
assert_eq!(landed, goal);
|
||||
assert!(clean);
|
||||
assert!(!route.steps.is_empty());
|
||||
|
||||
// Without the pair rule the same map is a straight line east.
|
||||
let mut open = Ground::new(4, 3, (1, 0));
|
||||
let route = path::route(&mut open, &[goal]).expect("a route east");
|
||||
assert_eq!(route.steps, vec![Facing::Right, Facing::Right]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_connection_on_the_far_edge_is_a_goal_and_the_walls_along_it_are_not() {
|
||||
// A route's north edge, off the screen from where the fly stands, with two walkable tiles on
|
||||
// it. With the map decoded the exit list is those two tiles and not the whole row.
|
||||
let mut ground = Ground::new(20, 18, (3, 16))
|
||||
.connected(Connections { north: true, south: false, east: false, west: false });
|
||||
for x in 0..20u8 {
|
||||
if x != 7 && x != 8 {
|
||||
ground = ground.wall(x, 0);
|
||||
}
|
||||
}
|
||||
let exits = path::exits(&mut ground);
|
||||
let north: Vec<ExitId> = exits
|
||||
.iter()
|
||||
.filter(|exit| exit.id == ExitId::Edge(Edge::North))
|
||||
.map(|exit| exit.id)
|
||||
.collect();
|
||||
assert_eq!(north.len(), 2, "the two tiles a step north can be taken from");
|
||||
let goals: Vec<Tile> = exits
|
||||
.iter()
|
||||
.filter(|exit| exit.way == Way::Route && exit.id == ExitId::Edge(Edge::North))
|
||||
.map(|exit| exit.tile)
|
||||
.collect();
|
||||
assert!(goals.iter().all(|tile| tile.y == 0 && (tile.x == 7 || tile.x == 8)));
|
||||
let route = path::route(&mut ground, &goals).expect("a route to the connection");
|
||||
assert!(route.goal.is_some());
|
||||
let (landed, clean) = walk(&ground, Tile::new(3, 16), &route.steps);
|
||||
assert!(goals.contains(&landed));
|
||||
assert!(clean, "one plan, across sixteen tiles of map, every tile of it known ground");
|
||||
|
||||
// With only the window, every tile of that edge is `Unknown` and so every one of them is an
|
||||
// exit, walls included: the search's own approach answer is what used to carry the walk.
|
||||
let mut blind = Ground::new(20, 18, (3, 16))
|
||||
.connected(Connections { north: true, south: false, east: false, west: false })
|
||||
.window_only();
|
||||
let count = path::exits(&mut blind)
|
||||
.iter()
|
||||
.filter(|exit| exit.id == ExitId::Edge(Edge::North))
|
||||
.count();
|
||||
assert_eq!(count, 20);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue