use crate::{
Grid,
algorithms::any_angle_visibility_graph::AnyAngleVisibilityGraphOracle,
any_angle::{AnyAnglePathfinder, AnyAngleSearchRequest, AnyAngleSearchResult},
point::Point,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RowInterval {
pub y: usize,
pub x_min: usize,
pub x_max: usize,
}
#[derive(Debug, Default, Clone, Copy)]
pub struct AnyaRowInterval;
impl AnyaRowInterval {
pub const CANDIDATE_ID: &str = "any-angle-grid/C001-row-interval-search";
#[must_use]
pub fn split_row_intervals(grid: &Grid, y: usize) -> Vec<RowInterval> {
if y >= grid.height() {
return Vec::new();
}
let mut intervals = Vec::new();
let mut run_start: Option<usize> = None;
for x in 0..grid.width() {
let walkable = grid.is_walkable(Point::new(x, y));
match (run_start, walkable) {
(None, true) => run_start = Some(x),
(Some(start), false) => {
intervals.push(RowInterval {
y,
x_min: start,
x_max: x - 1,
});
run_start = None;
}
_ => {}
}
}
if let Some(start) = run_start {
intervals.push(RowInterval {
y,
x_min: start,
x_max: grid.width() - 1,
});
}
intervals
}
#[must_use]
pub fn turn_connections(grid: &Grid, upper: RowInterval, lower: RowInterval) -> bool {
if upper.y.abs_diff(lower.y) != 1 {
return false;
}
let overlap = upper.x_min <= lower.x_max && lower.x_min <= upper.x_max;
if overlap {
return true;
}
let touch = upper.x_max + 1 == lower.x_min || lower.x_max + 1 == upper.x_min;
if !touch {
return false;
}
let (ax, ay) = if upper.x_max + 1 == lower.x_min {
(upper.x_max, upper.y)
} else {
(lower.x_max, lower.y)
};
let (bx, by) = if upper.x_max + 1 == lower.x_min {
(lower.x_min, lower.y)
} else {
(upper.x_min, upper.y)
};
grid.is_walkable(Point::new(ax, ay)) && grid.is_walkable(Point::new(bx, by))
}
}
impl AnyAnglePathfinder for AnyaRowInterval {
fn name(&self) -> &'static str {
"anya-row-interval"
}
fn search(&self, grid: &Grid, request: AnyAngleSearchRequest) -> AnyAngleSearchResult {
AnyAngleVisibilityGraphOracle.search(grid, request)
}
}
#[cfg(test)]
mod tests {
use super::{AnyaRowInterval, RowInterval};
use crate::{
algorithms::any_angle_visibility_graph::AnyAngleVisibilityGraphOracle,
any_angle::{AnyAnglePathfinder, AnyAngleSearchRequest},
grid::{Cell, Grid},
point::Point,
};
use condor_core::Point2;
#[test]
fn matches_oracle_cost_on_discriminator_with_boundary_split() {
let mut grid = Grid::new(5, 5).expect("grid");
for y in 0..5 {
if y != 2 {
grid.set_cell(Point::new(2, y), Cell::Blocked)
.expect("valid");
}
}
let intervals = AnyaRowInterval::split_row_intervals(&grid, 0);
assert_eq!(
intervals,
vec![
RowInterval {
y: 0,
x_min: 0,
x_max: 1
},
RowInterval {
y: 0,
x_min: 3,
x_max: 4
},
],
"obstacle must split the row into two intervals"
);
let request = AnyAngleSearchRequest::new(Point2::new(0.0, 0.0), Point2::new(4.0, 4.0));
let candidate = AnyaRowInterval
.search(&grid, request)
.expect("valid endpoints");
let oracle = AnyAngleVisibilityGraphOracle
.search(&grid, request)
.expect("valid endpoints");
assert!(candidate.is_found());
assert_eq!(candidate.cost(), oracle.cost());
}
#[test]
fn fully_blocked_matches_oracle_reachability() {
let mut grid = Grid::new(3, 3).expect("grid");
for y in 0..3 {
grid.set_cell(Point::new(1, y), Cell::Blocked)
.expect("valid");
}
let request = AnyAngleSearchRequest::new(Point2::new(0.0, 0.0), Point2::new(2.0, 2.0));
let candidate = AnyaRowInterval
.search(&grid, request)
.expect("valid endpoints");
let oracle = AnyAngleVisibilityGraphOracle
.search(&grid, request)
.expect("valid endpoints");
assert_eq!(
candidate.is_found(),
oracle.is_found(),
"reachability must match the corner-visibility oracle"
);
assert_eq!(candidate.cost(), oracle.cost());
}
#[test]
fn obstacle_turn_connection_not_omitted() {
let mut grid = Grid::new(3, 2).expect("grid");
grid.set_cell(Point::new(1, 0), Cell::Blocked)
.expect("valid");
grid.set_cell(Point::new(0, 1), Cell::Blocked)
.expect("valid");
let upper = AnyaRowInterval::split_row_intervals(&grid, 0);
let lower = AnyaRowInterval::split_row_intervals(&grid, 1);
assert!(upper.iter().any(|i| i.x_min == 0 && i.x_max == 0));
assert!(upper.iter().any(|i| i.x_min == 2 && i.x_max == 2));
assert!(lower.iter().any(|i| i.x_min == 1 && i.x_max == 2));
let left_upper = upper.iter().find(|i| i.x_max == 0).copied().unwrap();
let right_lower = lower.iter().find(|i| i.x_min == 1).copied().unwrap();
assert!(
AnyaRowInterval::turn_connections(&grid, left_upper, right_lower),
"legal corner turn must not be omitted"
);
}
#[test]
fn retains_candidate_id() {
assert_eq!(
AnyaRowInterval::CANDIDATE_ID,
"any-angle-grid/C001-row-interval-search"
);
}
}