condor_grid/
flow_field.rs1use std::collections::VecDeque;
11
12use crate::{
13 Grid, Path, Point,
14 search::{GridSearchError, SearchResult},
15};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum FlowDirection {
22 None,
24 Left,
26 Right,
28 Up,
30 Down,
32}
33
34impl FlowDirection {
35 #[must_use]
37 pub const fn delta(self) -> Option<(isize, isize)> {
38 match self {
39 Self::None => None,
40 Self::Left => Some((-1, 0)),
41 Self::Right => Some((1, 0)),
42 Self::Up => Some((0, -1)),
43 Self::Down => Some((0, 1)),
44 }
45 }
46}
47
48#[derive(Debug, Clone, Copy, Default)]
53pub struct FlowFieldBuilder;
54
55impl FlowFieldBuilder {
56 #[must_use]
58 pub const fn new() -> Self {
59 Self
60 }
61
62 pub fn preprocess(
70 &self,
71 grid: &Grid,
72 goal: Point,
73 ) -> Result<PreparedFlowField, FlowFieldBuildError> {
74 if !grid.contains(goal) || !grid.is_walkable(goal) {
75 return Err(FlowFieldBuildError::InvalidGoal { goal });
76 }
77 ensure_uniform_costs(grid)?;
78
79 let cell_count = grid.cell_count();
80 let mut integration = vec![None; cell_count];
81 let mut directions = vec![FlowDirection::None; cell_count];
82 let goal_index = grid.index_of(goal).expect("goal in bounds");
83
84 let mut queue = VecDeque::from([goal_index]);
85 integration[goal_index] = Some(0u32);
86
87 while let Some(index) = queue.pop_front() {
88 let cost = integration[index].expect("enqueued cells have integration");
89 let point = grid.point_from_index(index);
90 for next in grid.neighbors4(point) {
91 let Some(next_index) = grid.index_of(next) else {
92 continue;
93 };
94 if integration[next_index].is_some() {
95 continue;
96 }
97 integration[next_index] = Some(cost + 1);
98 queue.push_back(next_index);
99 }
100 }
101
102 for index in 0..cell_count {
103 let point = grid.point_from_index(index);
104 if !grid.is_walkable(point) {
105 continue;
106 }
107 let Some(here) = integration[index] else {
108 continue;
109 };
110 if point == goal {
111 directions[index] = FlowDirection::None;
112 continue;
113 }
114
115 let mut best_dir = FlowDirection::None;
116 let mut best_cost = here;
117 for neighbor in grid.neighbors4(point) {
118 let Some(n_index) = grid.index_of(neighbor) else {
119 continue;
120 };
121 let Some(n_cost) = integration[n_index] else {
122 continue;
123 };
124 if n_cost >= best_cost {
125 continue;
126 }
127 best_cost = n_cost;
128 best_dir = if neighbor.x + 1 == point.x {
129 FlowDirection::Left
130 } else if neighbor.x == point.x + 1 {
131 FlowDirection::Right
132 } else if neighbor.y + 1 == point.y {
133 FlowDirection::Up
134 } else {
135 FlowDirection::Down
136 };
137 }
138 directions[index] = best_dir;
139 }
140
141 Ok(PreparedFlowField {
142 grid: grid.clone(),
143 goal,
144 integration,
145 directions,
146 })
147 }
148}
149
150#[derive(Debug, Clone)]
155pub struct PreparedFlowField {
156 grid: Grid,
157 goal: Point,
158 integration: Vec<Option<u32>>,
159 directions: Vec<FlowDirection>,
160}
161
162impl PreparedFlowField {
163 #[must_use]
165 pub fn name(&self) -> &'static str {
166 "flow-field"
167 }
168
169 #[must_use]
171 pub fn grid(&self) -> &Grid {
172 &self.grid
173 }
174
175 #[must_use]
177 pub fn goal(&self) -> Point {
178 self.goal
179 }
180
181 #[must_use]
183 pub fn width(&self) -> usize {
184 self.grid.width()
185 }
186
187 #[must_use]
189 pub fn height(&self) -> usize {
190 self.grid.height()
191 }
192
193 #[must_use]
195 pub fn integration_at(&self, point: Point) -> Option<u32> {
196 let index = self.grid.index_of(point)?;
197 self.integration[index]
198 }
199
200 #[must_use]
203 pub fn direction_at(&self, point: Point) -> FlowDirection {
204 self.grid
205 .index_of(point)
206 .map(|index| self.directions[index])
207 .unwrap_or(FlowDirection::None)
208 }
209
210 pub fn sample_path(&self, start: Point) -> SearchResult {
220 self.sample_path_limited(
221 start,
222 self.grid.width().saturating_mul(self.grid.height()).max(1),
223 )
224 }
225
226 pub fn sample_path_limited(&self, start: Point, max_steps: usize) -> SearchResult {
234 if !self.grid.is_walkable(start) {
235 return Err(GridSearchError::InvalidStart { point: start });
236 }
237 if self.integration_at(start).is_none() {
238 return crate::search::not_found(0);
239 }
240 if start == self.goal {
241 return crate::search::found(
242 Path::from_steps(vec![start]).expect("flow paths always contain their start"),
243 1,
244 );
245 }
246
247 let mut steps = vec![start];
248 let mut current = start;
249 let mut visited = 1usize;
250
251 for _ in 0..max_steps {
252 let dir = self.direction_at(current);
253 let Some((dx, dy)) = dir.delta() else {
254 break;
255 };
256 let nx = current.x as isize + dx;
257 let ny = current.y as isize + dy;
258 if nx < 0 || ny < 0 {
259 break;
260 }
261 let next = Point::new(nx as usize, ny as usize);
262 if !self.grid.contains(next) || !self.grid.is_walkable(next) {
263 break;
264 }
265 if steps.len() >= 2 && steps[steps.len() - 2] == next {
267 break;
268 }
269 steps.push(next);
270 visited += 1;
271 current = next;
272 if current == self.goal {
273 return crate::search::found(
274 Path::from_steps(steps).expect("flow paths always contain their start"),
275 visited,
276 );
277 }
278 }
279
280 if current == self.goal {
281 crate::search::found(
282 Path::from_steps(steps).expect("flow paths always contain their start"),
283 visited,
284 )
285 } else {
286 Err(GridSearchError::StepLimitReached {
287 max_steps,
288 reached: current,
289 })
290 }
291 }
292}
293
294#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
296#[non_exhaustive]
297pub enum FlowFieldBuildError {
298 #[error("flow field goal {goal:?} must be a walkable in-bounds cell")]
300 InvalidGoal { goal: Point },
301 #[error("flow field supports only uniform cost; cell {point:?} has cost {cost}")]
303 NonUniformCost { point: Point, cost: usize },
304}
305
306fn ensure_uniform_costs(grid: &Grid) -> Result<(), FlowFieldBuildError> {
307 for y in 0..grid.height() {
308 for x in 0..grid.width() {
309 let p = Point::new(x, y);
310 if let Some(cost) = grid.traversal_cost(p)
311 && cost != 1
312 {
313 return Err(FlowFieldBuildError::NonUniformCost { point: p, cost });
314 }
315 }
316 }
317 Ok(())
318}