use std::ops::{Add, Sub};
use spacewalk::height::height_gate;
use spacewalk::{Adjacency, Coord, Dir8, FullGrid, Grid, Hex, Metric, Movement, Sq, Step};
mod common;
#[test]
fn costs_too_large_for_the_board_are_refused_at_the_door() {
let g = FullGrid::square(10, 1, Adjacency::Four);
let panic = std::panic::catch_unwind(|| Movement::scan(&g, |_: Step<Sq>| Some(600_000_000)));
let msg = *panic.unwrap_err().downcast::<String>().unwrap();
assert!(
msg.contains("600000000"),
"it names the offending cost: {msg}"
);
assert!(msg.contains("overflow"), "and says why it matters: {msg}");
}
#[test]
fn a_total_that_would_overflow_saturates_instead_of_hanging() {
let g = FullGrid::square(10, 1, Adjacency::Four);
let m = Movement::new(|_: Step<Sq>| Some(600_000_000), 0);
let a = g.at(Sq::new(0, 0));
let b = g.at(Sq::new(9, 0));
let p = g
.path(a, b, &m)
.expect("it must terminate, and it must find the corridor");
assert_eq!(p.len(), 9);
assert_eq!(
p.cost(),
u32::MAX,
"the total pegs at the ceiling rather than wrapping to a lie"
);
}
#[test]
fn a_colossal_min_step_cannot_overflow_the_heuristic() {
let g = FullGrid::square(8, 8, Adjacency::Four);
let m = Movement::new(|_: Step<Sq>| Some(10), u32::MAX);
let a = g.at(Sq::new(0, 0));
let b = g.at(Sq::new(7, 7));
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| g.path(a, b, &m)));
if let Ok(found) = outcome {
assert!(
found.is_some(),
"it saturates, terminates, and still finds the path"
);
}
}
#[test]
fn reach_and_path_toward_survive_the_same_costs() {
let g = FullGrid::square(12, 1, Adjacency::Four);
let m = Movement::new(|_: Step<Sq>| Some(900_000_000), 0);
let a = g.at(Sq::new(0, 0));
let z = g.at(Sq::new(11, 0));
assert!(!g.reachable(a, u32::MAX, &m).is_empty());
assert!(g.path_toward(a, z, u32::MAX, &m).is_some());
}
#[test]
fn distances_are_exact_at_the_extremes_of_i32() {
let far = Sq::new(i32::MAX, 0).chebyshev(Sq::new(i32::MIN, 0));
assert_eq!(far, u32::MAX, "the true distance, clamped — not 1");
let both = Sq::new(i32::MIN, i32::MIN).manhattan(Sq::new(0, 0));
assert_eq!(both, u32::MAX, "clamped, not 0");
let hex = Hex::new(i32::MIN, 0).distance(Hex::new(0, 0));
assert!(hex > 1_000_000_000, "a real distance, not 0: {hex}");
}
#[test]
fn a_grid_of_extreme_coordinates_can_be_built_and_measured() {
let g = FullGrid::new(
[Sq::new(i32::MAX, 0), Sq::new(i32::MIN, 0), Sq::new(0, 0)],
&Dir8::ORTHO,
Metric::MANHATTAN,
);
let hi = g.at(Sq::new(i32::MAX, 0));
let lo = g.at(Sq::new(i32::MIN, 0));
assert!(g.distance(hi, lo) > 1, "they are not neighbours");
assert_eq!(
g.neighbors(hi).count(),
0,
"and no wrap-around edge was forged"
);
}
fn strung(xs: impl IntoIterator<Item = i32>) -> FullGrid<Sq> {
FullGrid::new(
xs.into_iter().map(|x| Sq::new(x, 0)),
&Dir8::ORTHO,
Metric::MANHATTAN,
)
}
#[test]
fn a_line_keeps_its_own_endpoints_however_far_out_they_sit() {
const Q: i32 = (1 << 30) - 1;
let g = strung([i32::MIN, 0, Q, i32::MAX]);
let (lo, hi) = (g.at(Sq::new(i32::MIN, 0)), g.at(Sq::new(i32::MAX, 0)));
let line = g.line(lo, hi);
assert_eq!(line.first(), Some(&lo), "a line starts where you are");
assert_eq!(line.last(), Some(&hi), "and ends where you look");
let g = strung([i32::MAX - 2, i32::MAX - 1, i32::MAX]);
let (lo, hi) = (g.at(Sq::new(i32::MAX - 2, 0)), g.at(Sq::new(i32::MAX, 0)));
let line = g.line(lo, hi);
assert_eq!(line.first(), Some(&lo));
assert_eq!(line.last(), Some(&hi));
}
#[test]
fn sight_is_symmetric_even_past_the_lattice_limit() {
const Q: i32 = (1 << 30) - 1;
let g = strung([i32::MIN, 0, Q, i32::MAX]);
let lo = g.at(Sq::new(i32::MIN, 0));
let hi = g.at(Sq::new(i32::MAX, 0));
let tower = g.at(Sq::new(Q, 0));
let wall = |i| i == tower;
assert_eq!(g.los(lo, hi, wall), g.los(hi, lo, wall), "one-sided sight");
assert!(!g.los(lo, hi, wall), "and the tower does stop the view");
for blocker in g.indices() {
let wall = |i| i == blocker;
for a in g.indices() {
for b in g.indices() {
assert_eq!(
g.los(a, b, wall),
g.los(b, a, wall),
"blocker {:?}: {:?} <-> {:?}",
g.coord(blocker),
g.coord(a),
g.coord(b)
);
}
}
}
}
#[test]
fn a_sight_line_at_the_extremes_of_height_does_not_wrap() {
const LATTICE_LIMIT: i32 = (1 << 30) - 1;
let g = FullGrid::new(
[
Sq::new(i32::MIN, 0),
Sq::new(0, 0),
Sq::new(LATTICE_LIMIT, 0),
Sq::new(i32::MAX, 0),
],
&Dir8::ORTHO,
Metric::MANHATTAN,
);
let lo = g.at(Sq::new(i32::MIN, 0));
let hi = g.at(Sq::new(i32::MAX, 0));
let tower = g.at(Sq::new(LATTICE_LIMIT, 0));
assert_eq!(g.distance(lo, hi), u32::MAX, "the widest span there is");
assert_eq!(g.distance(lo, tower), 3_221_225_471);
let ground = move |i| if i == tower { i32::MAX } else { i32::MIN };
let sight = height_gate(&g, ground, move |_| i32::MIN);
assert!(
!g.los_by(lo, hi, &sight),
"a tower four billion units high is not see-through — an i64 would have said it was"
);
assert!(
!g.los_by(hi, lo, &sight),
"and not from the other side either"
);
assert!(g.los_by(lo, tower, &sight));
assert!(g.los_by(hi, tower, &sight));
}
#[test]
fn a_height_field_cannot_hang_a_field_of_view() {
let g = FullGrid::square(64, 64, Adjacency::Eight);
let extreme = |i| {
if g.coord(i).x % 2 == 0 {
i32::MAX
} else {
i32::MIN
}
};
let sight = height_gate(&g, extreme, extreme);
let eye = g.at(Sq::new(32, 32));
let seen = g.visible_from_by(eye, spacewalk::MAX_SIGHT, &sight);
assert!(
seen.contains(Sq::new(32, 32)),
"you are always in your own view"
);
}
#[test]
fn a_wide_direction_alphabet_keeps_reverse_edges_correct() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct WideDir(u16);
const fn dirs() -> [WideDir; 257] {
let mut out = [WideDir(0); 257];
let mut i = 0;
while i < out.len() {
out[i] = WideDir(i as u16);
i += 1;
}
out
}
static DIRS: [WideDir; 257] = dirs();
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct Wide(i32);
impl Add for Wide {
type Output = Self;
fn add(self, other: Self) -> Self {
Self(self.0 + other.0)
}
}
impl Sub for Wide {
type Output = Self;
fn sub(self, other: Self) -> Self {
Self(self.0 - other.0)
}
}
impl Coord for Wide {
type Dir = WideDir;
const DIRS: &'static [WideDir] = &DIRS;
fn step(self, d: WideDir) -> Self {
if d.0 == 256 { Self(self.0 + 1) } else { self }
}
}
let g = FullGrid::new(
[Wide(0), Wide(1)],
Wide::DIRS,
Metric::scanning(|a: Wide, b: Wide| (b.0 - a.0).unsigned_abs()),
);
let one = g.at(Wide(1));
assert!(
g.in_neighbors(one)
.any(|(dir, from)| dir == WideDir(256) && from == g.at(Wide(0)))
);
}
const W: i32 = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct Ring(i32);
impl Add for Ring {
type Output = Self;
fn add(self, o: Self) -> Self {
Ring((self.0 + o.0).rem_euclid(W))
}
}
impl Sub for Ring {
type Output = Self;
fn sub(self, o: Self) -> Self {
Ring((self.0 - o.0).rem_euclid(W))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum Spin {
Round,
}
impl Coord for Ring {
type Dir = Spin;
const DIRS: &'static [Spin] = &[Spin::Round];
fn step(self, _: Spin) -> Self {
Ring((self.0 + 1).rem_euclid(W))
}
}
#[test]
fn a_ray_on_a_wrapping_world_terminates() {
let g = FullGrid::new(
(0..W).map(Ring),
Ring::DIRS,
Metric::scanning(|a: Ring, b: Ring| (b - a).0.unsigned_abs()),
);
let start = g.at(Ring(0));
let walked: Vec<_> = g.ray(start, Spin::Round).collect();
assert_eq!(walked.len(), g.len(), "bounded by the board, and no longer");
}
#[test]
fn a_clamping_step_does_not_become_a_self_loop() {
common::coord_1d!(Clamp, Spin = Spin::Round, |x| Clamp((x.0 + 1).min(4)));
let g = FullGrid::new(
(0..=4).map(Clamp),
Clamp::DIRS,
Metric::scanning(|a: Clamp, b: Clamp| (b.0 - a.0).unsigned_abs()),
);
let last = g.at(Clamp(4));
assert_eq!(
g.step(last, Spin::Round),
None,
"the self-step is not an edge"
);
assert_eq!(g.ray(last, Spin::Round).count(), 0);
}
#[test]
fn a_negative_board_is_refused_rather_than_silently_empty() {
assert!(std::panic::catch_unwind(|| FullGrid::square(-5, 3, Adjacency::Four)).is_err());
}
#[test]
fn an_impossibly_large_board_is_refused_rather_than_attempted() {
let boom = std::panic::catch_unwind(|| FullGrid::square(46_341, 46_341, Adjacency::Eight));
let msg = *boom.unwrap_err().downcast::<String>().unwrap();
assert!(msg.contains("at most"), "it says what the limit is: {msg}");
assert!(std::panic::catch_unwind(|| FullGrid::hexagon(50_000)).is_err());
assert!(std::panic::catch_unwind(|| FullGrid::hexagon(-1)).is_err());
assert!(std::panic::catch_unwind(|| FullGrid::disc(i32::MAX, Adjacency::Four)).is_err());
assert!(std::panic::catch_unwind(|| FullGrid::disc(-1, Adjacency::Four)).is_err());
}
#[test]
fn an_empty_grid_is_harmless() {
let g = FullGrid::square(0, 0, Adjacency::Four);
assert_eq!(g.len(), 0);
assert!(g.is_empty());
assert_eq!(g.indices().count(), 0);
assert_eq!(g.index_of(Sq::new(0, 0)), None);
let other = FullGrid::square(1, 1, Adjacency::Four);
let stale = other.at(Sq::new(0, 0));
assert!(std::panic::catch_unwind(|| g.coord(stale)).is_err());
}
#[test]
fn a_foreign_index_says_so() {
let g = FullGrid::square(3, 3, Adjacency::Four);
let big = FullGrid::square(40, 40, Adjacency::Four);
let far = big.at(Sq::new(39, 24)); assert_eq!(far.get(), 999);
let boom = std::panic::catch_unwind(|| g.coord(far));
let msg = *boom.unwrap_err().downcast::<String>().unwrap();
assert!(msg.contains("999"), "names the index: {msg}");
if cfg!(debug_assertions) {
assert!(
msg.contains("different grid"),
"and that it is foreign: {msg}"
);
} else {
assert!(msg.contains("9 cells"), "and the board it is not on: {msg}");
}
}
#[test]
fn a_metric_that_disagrees_with_the_directions_is_refused() {
let boom = std::panic::catch_unwind(|| {
FullGrid::new(
(0..5).flat_map(|y| (0..5).map(move |x| Sq::new(x, y))),
&Dir8::ALL, Metric::MANHATTAN, )
});
let msg = *boom.unwrap_err().downcast::<String>().unwrap();
assert!(
msg.contains("covers 2"),
"it says what the step actually spans: {msg}"
);
assert!(msg.contains("disagrees"), "and names the problem: {msg}");
let _ = FullGrid::square(5, 5, Adjacency::Four);
let _ = FullGrid::square(5, 5, Adjacency::Eight);
}
#[test]
fn a_board_with_genuine_multi_cell_steps_may_opt_out_with_a_zero_metric() {
common::coord_1d!(Leap, Jump, |x| Leap(x.0 + 3));
let g = FullGrid::new((0..9).map(Leap), Leap::DIRS, Metric::scanning(|_, _| 0));
let m = Movement::scan(&g, |_| Some(10));
let start = g.at(Leap(0));
let far = g.at(Leap(6));
assert_eq!(g.path(start, far, &m).unwrap().len(), 2, "two portal hops");
}
#[test]
fn a_path_that_goes_nowhere_is_length_zero_not_eighteen_quintillion() {
let g = FullGrid::square(3, 3, Adjacency::Four);
let here = g.at(Sq::new(1, 1));
let p = g.path(here, here, &Movement::uniform(&g, 10)).unwrap();
assert_eq!(p.len(), 0);
assert!(p.is_empty());
assert_eq!(p.destination(), here);
assert_eq!(p.start(), here, "it never left");
assert_eq!(p.cost(), 0, "standing still is free");
}