use std::collections::VecDeque;
use crate::{
Grid, Path, Point,
search::{GridSearchError, SearchResult},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FlowDirection {
None,
Left,
Right,
Up,
Down,
}
impl FlowDirection {
#[must_use]
pub const fn delta(self) -> Option<(isize, isize)> {
match self {
Self::None => None,
Self::Left => Some((-1, 0)),
Self::Right => Some((1, 0)),
Self::Up => Some((0, -1)),
Self::Down => Some((0, 1)),
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct FlowFieldBuilder;
impl FlowFieldBuilder {
#[must_use]
pub const fn new() -> Self {
Self
}
pub fn preprocess(
&self,
grid: &Grid,
goal: Point,
) -> Result<PreparedFlowField, FlowFieldBuildError> {
if !grid.contains(goal) || !grid.is_walkable(goal) {
return Err(FlowFieldBuildError::InvalidGoal { goal });
}
ensure_uniform_costs(grid)?;
let cell_count = grid.cell_count();
let mut integration = vec![None; cell_count];
let mut directions = vec![FlowDirection::None; cell_count];
let goal_index = grid.index_of(goal).expect("goal in bounds");
let mut queue = VecDeque::from([goal_index]);
integration[goal_index] = Some(0u32);
while let Some(index) = queue.pop_front() {
let cost = integration[index].expect("enqueued cells have integration");
let point = grid.point_from_index(index);
for next in grid.neighbors4(point) {
let Some(next_index) = grid.index_of(next) else {
continue;
};
if integration[next_index].is_some() {
continue;
}
integration[next_index] = Some(cost + 1);
queue.push_back(next_index);
}
}
for index in 0..cell_count {
let point = grid.point_from_index(index);
if !grid.is_walkable(point) {
continue;
}
let Some(here) = integration[index] else {
continue;
};
if point == goal {
directions[index] = FlowDirection::None;
continue;
}
let mut best_dir = FlowDirection::None;
let mut best_cost = here;
for neighbor in grid.neighbors4(point) {
let Some(n_index) = grid.index_of(neighbor) else {
continue;
};
let Some(n_cost) = integration[n_index] else {
continue;
};
if n_cost >= best_cost {
continue;
}
best_cost = n_cost;
best_dir = if neighbor.x + 1 == point.x {
FlowDirection::Left
} else if neighbor.x == point.x + 1 {
FlowDirection::Right
} else if neighbor.y + 1 == point.y {
FlowDirection::Up
} else {
FlowDirection::Down
};
}
directions[index] = best_dir;
}
Ok(PreparedFlowField {
grid: grid.clone(),
goal,
integration,
directions,
})
}
}
#[derive(Debug, Clone)]
pub struct PreparedFlowField {
grid: Grid,
goal: Point,
integration: Vec<Option<u32>>,
directions: Vec<FlowDirection>,
}
impl PreparedFlowField {
#[must_use]
pub fn name(&self) -> &'static str {
"flow-field"
}
#[must_use]
pub fn grid(&self) -> &Grid {
&self.grid
}
#[must_use]
pub fn goal(&self) -> Point {
self.goal
}
#[must_use]
pub fn width(&self) -> usize {
self.grid.width()
}
#[must_use]
pub fn height(&self) -> usize {
self.grid.height()
}
#[must_use]
pub fn integration_at(&self, point: Point) -> Option<u32> {
let index = self.grid.index_of(point)?;
self.integration[index]
}
#[must_use]
pub fn direction_at(&self, point: Point) -> FlowDirection {
self.grid
.index_of(point)
.map(|index| self.directions[index])
.unwrap_or(FlowDirection::None)
}
pub fn sample_path(&self, start: Point) -> SearchResult {
self.sample_path_limited(
start,
self.grid.width().saturating_mul(self.grid.height()).max(1),
)
}
pub fn sample_path_limited(&self, start: Point, max_steps: usize) -> SearchResult {
if !self.grid.is_walkable(start) {
return Err(GridSearchError::InvalidStart { point: start });
}
if self.integration_at(start).is_none() {
return crate::search::not_found(0);
}
if start == self.goal {
return crate::search::found(
Path::from_steps(vec![start]).expect("flow paths always contain their start"),
1,
);
}
let mut steps = vec![start];
let mut current = start;
let mut visited = 1usize;
for _ in 0..max_steps {
let dir = self.direction_at(current);
let Some((dx, dy)) = dir.delta() else {
break;
};
let nx = current.x as isize + dx;
let ny = current.y as isize + dy;
if nx < 0 || ny < 0 {
break;
}
let next = Point::new(nx as usize, ny as usize);
if !self.grid.contains(next) || !self.grid.is_walkable(next) {
break;
}
if steps.len() >= 2 && steps[steps.len() - 2] == next {
break;
}
steps.push(next);
visited += 1;
current = next;
if current == self.goal {
return crate::search::found(
Path::from_steps(steps).expect("flow paths always contain their start"),
visited,
);
}
}
if current == self.goal {
crate::search::found(
Path::from_steps(steps).expect("flow paths always contain their start"),
visited,
)
} else {
Err(GridSearchError::StepLimitReached {
max_steps,
reached: current,
})
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum FlowFieldBuildError {
#[error("flow field goal {goal:?} must be a walkable in-bounds cell")]
InvalidGoal { goal: Point },
#[error("flow field supports only uniform cost; cell {point:?} has cost {cost}")]
NonUniformCost { point: Point, cost: usize },
}
fn ensure_uniform_costs(grid: &Grid) -> Result<(), FlowFieldBuildError> {
for y in 0..grid.height() {
for x in 0..grid.width() {
let p = Point::new(x, y);
if let Some(cost) = grid.traversal_cost(p)
&& cost != 1
{
return Err(FlowFieldBuildError::NonUniformCost { point: p, cost });
}
}
}
Ok(())
}