Skip to main content

rustsim_pathfinding/
walkability.rs

1//! Walkability query utilities for pathfinding.
2//!
3//! Convenience functions that combine spatial neighbor queries with
4//! walkability filtering. Mirrors Agents.jl `Pathfinding.nearby_walkable`
5//! and `Pathfinding.random_walkable`.
6//!
7//! These functions operate on raw grid data (walkability arrays and
8//! dimensions) rather than depending on specific space types, keeping
9//! the pathfinding crate independent.
10
11use rand::Rng;
12
13/// Returns all walkable grid cells within Chebyshev radius `r` of `pos`.
14///
15/// Excludes `pos` itself. Supports periodic boundaries.
16///
17/// # Example
18///
19/// ```
20/// use rustsim_pathfinding::walkability::nearby_walkable;
21///
22/// let walkmap = vec![true; 25]; // 5x5, all walkable
23/// let neighbors = nearby_walkable((2, 2), 1, &walkmap, 5, 5, false);
24/// assert_eq!(neighbors.len(), 8); // 8 neighbors of center cell
25/// ```
26pub fn nearby_walkable(
27    pos: (usize, usize),
28    radius: usize,
29    walkmap: &[bool],
30    width: usize,
31    height: usize,
32    periodic: bool,
33) -> Vec<(usize, usize)> {
34    let r = radius as i32;
35    let (cx, cy) = (pos.0 as i32, pos.1 as i32);
36    let mut result = Vec::new();
37
38    for dy in -r..=r {
39        for dx in -r..=r {
40            if dx == 0 && dy == 0 {
41                continue;
42            }
43            let nx = cx + dx;
44            let ny = cy + dy;
45
46            let cell = if periodic {
47                let px = ((nx % width as i32) + width as i32) % width as i32;
48                let py = ((ny % height as i32) + height as i32) % height as i32;
49                Some((px as usize, py as usize))
50            } else if nx >= 0 && ny >= 0 && (nx as usize) < width && (ny as usize) < height {
51                Some((nx as usize, ny as usize))
52            } else {
53                None
54            };
55
56            if let Some((x, y)) = cell {
57                if walkmap[y * width + x] {
58                    result.push((x, y));
59                }
60            }
61        }
62    }
63
64    result
65}
66
67/// Returns a random walkable grid cell, or `None` if none exist.
68///
69/// Uniformly samples from all walkable cells.
70///
71/// # Example
72///
73/// ```
74/// use rustsim_pathfinding::walkability::random_walkable;
75/// use rand::rngs::StdRng;
76/// use rand::SeedableRng;
77///
78/// let walkmap = vec![true; 25];
79/// let mut rng = StdRng::seed_from_u64(42);
80/// let pos = random_walkable(&walkmap, 5, 5, &mut rng);
81/// assert!(pos.is_some());
82/// ```
83pub fn random_walkable<R: Rng>(
84    walkmap: &[bool],
85    width: usize,
86    _height: usize,
87    rng: &mut R,
88) -> Option<(usize, usize)> {
89    let walkable_cells: Vec<usize> = walkmap
90        .iter()
91        .enumerate()
92        .filter(|(_, &w)| w)
93        .map(|(i, _)| i)
94        .collect();
95
96    if walkable_cells.is_empty() {
97        return None;
98    }
99
100    let idx = walkable_cells[rng.gen_range(0..walkable_cells.len())];
101    let x = idx % width;
102    let y = idx / width;
103    Some((x, y))
104}
105
106/// Returns a random walkable grid cell within Chebyshev radius `r` of `pos`.
107///
108/// Returns `None` if no walkable cells exist in the neighborhood.
109///
110/// # Example
111///
112/// ```
113/// use rustsim_pathfinding::walkability::random_walkable_nearby;
114/// use rand::rngs::StdRng;
115/// use rand::SeedableRng;
116///
117/// let walkmap = vec![true; 25];
118/// let mut rng = StdRng::seed_from_u64(42);
119/// let pos = random_walkable_nearby((2, 2), 1, &walkmap, 5, 5, false, &mut rng);
120/// assert!(pos.is_some());
121/// ```
122pub fn random_walkable_nearby<R: Rng>(
123    pos: (usize, usize),
124    radius: usize,
125    walkmap: &[bool],
126    width: usize,
127    height: usize,
128    periodic: bool,
129    rng: &mut R,
130) -> Option<(usize, usize)> {
131    let candidates = nearby_walkable(pos, radius, walkmap, width, height, periodic);
132    if candidates.is_empty() {
133        return None;
134    }
135    Some(candidates[rng.gen_range(0..candidates.len())])
136}
137
138/// Returns a random walkable continuous position within the space.
139///
140/// Picks a random walkable grid cell, then returns a random position
141/// within that cell's continuous extent.
142///
143/// Returns `None` if no walkable cells exist.
144pub fn random_walkable_continuous<R: Rng>(
145    walkmap: &[bool],
146    grid_w: usize,
147    grid_h: usize,
148    extent_x: f64,
149    extent_y: f64,
150    rng: &mut R,
151) -> Option<(f64, f64)> {
152    let cell = random_walkable(walkmap, grid_w, grid_h, rng)?;
153    let cell_w = extent_x / grid_w as f64;
154    let cell_h = extent_y / grid_h as f64;
155    let x = cell.0 as f64 * cell_w + rng.gen_range(0.0..cell_w);
156    let y = cell.1 as f64 * cell_h + rng.gen_range(0.0..cell_h);
157    Some((x, y))
158}
159
160/// Returns a random walkable continuous position within Euclidean radius
161/// `r` of `pos`.
162///
163/// Finds walkable grid cells near `pos`, picks one randomly, then returns
164/// a random continuous position within that cell that is within `r` of `pos`.
165///
166/// Returns `None` if no walkable cells exist nearby.
167#[allow(clippy::too_many_arguments)]
168pub fn random_walkable_continuous_nearby<R: Rng>(
169    pos: (f64, f64),
170    radius: f64,
171    walkmap: &[bool],
172    grid_w: usize,
173    grid_h: usize,
174    extent_x: f64,
175    extent_y: f64,
176    periodic: bool,
177    rng: &mut R,
178) -> Option<(f64, f64)> {
179    let cell_w = extent_x / grid_w as f64;
180    let cell_h = extent_y / grid_h as f64;
181
182    // Convert continuous position to grid cell
183    let cx = ((pos.0 / extent_x * grid_w as f64).floor() as usize).min(grid_w - 1);
184    let cy = ((pos.1 / extent_y * grid_h as f64).floor() as usize).min(grid_h - 1);
185
186    // Grid radius that covers the continuous radius
187    let grid_r = ((radius / cell_w.min(cell_h)).ceil() as usize).max(1);
188
189    let candidates = nearby_walkable((cx, cy), grid_r, walkmap, grid_w, grid_h, periodic);
190
191    if candidates.is_empty() {
192        return None;
193    }
194
195    // Try up to 100 times to find a position within Euclidean radius
196    for _ in 0..100 {
197        let cell = candidates[rng.gen_range(0..candidates.len())];
198        let x = cell.0 as f64 * cell_w + rng.gen_range(0.0..cell_w);
199        let y = cell.1 as f64 * cell_h + rng.gen_range(0.0..cell_h);
200
201        let mut dx = (x - pos.0).abs();
202        let mut dy = (y - pos.1).abs();
203        if periodic {
204            if dx > extent_x * 0.5 {
205                dx = extent_x - dx;
206            }
207            if dy > extent_y * 0.5 {
208                dy = extent_y - dy;
209            }
210        }
211        let dist = (dx * dx + dy * dy).sqrt();
212        if dist <= radius {
213            return Some((x, y));
214        }
215    }
216
217    None
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223    use rand::rngs::StdRng;
224    use rand::SeedableRng;
225
226    #[test]
227    fn nearby_walkable_all_open() {
228        let walkmap = vec![true; 25]; // 5x5
229        let result = nearby_walkable((2, 2), 1, &walkmap, 5, 5, false);
230        assert_eq!(result.len(), 8); // 8 neighbors
231    }
232
233    #[test]
234    fn nearby_walkable_with_walls() {
235        let mut walkmap = vec![true; 25];
236        walkmap[5 + 2] = false; // (2,1) blocked
237        walkmap[3 * 5 + 2] = false; // (2,3) blocked
238        let result = nearby_walkable((2, 2), 1, &walkmap, 5, 5, false);
239        assert_eq!(result.len(), 6);
240    }
241
242    #[test]
243    fn nearby_walkable_corner_periodic() {
244        let walkmap = vec![true; 25]; // 5x5
245        let result = nearby_walkable((0, 0), 1, &walkmap, 5, 5, true);
246        assert_eq!(result.len(), 8); // wraps around
247    }
248
249    #[test]
250    fn nearby_walkable_corner_bounded() {
251        let walkmap = vec![true; 25]; // 5x5
252        let result = nearby_walkable((0, 0), 1, &walkmap, 5, 5, false);
253        assert_eq!(result.len(), 3); // only (1,0), (0,1), (1,1)
254    }
255
256    #[test]
257    fn random_walkable_basic() {
258        let walkmap = vec![true; 25];
259        let mut rng = StdRng::seed_from_u64(42);
260        let pos = random_walkable(&walkmap, 5, 5, &mut rng);
261        assert!(pos.is_some());
262        let (x, y) = pos.unwrap();
263        assert!(x < 5 && y < 5);
264    }
265
266    #[test]
267    fn random_walkable_none() {
268        let walkmap = vec![false; 25];
269        let mut rng = StdRng::seed_from_u64(42);
270        let pos = random_walkable(&walkmap, 5, 5, &mut rng);
271        assert!(pos.is_none());
272    }
273
274    #[test]
275    fn random_walkable_continuous_basic() {
276        let walkmap = vec![true; 100];
277        let mut rng = StdRng::seed_from_u64(42);
278        let pos = random_walkable_continuous(&walkmap, 10, 10, 100.0, 100.0, &mut rng);
279        assert!(pos.is_some());
280        let (x, y) = pos.unwrap();
281        assert!((0.0..100.0).contains(&x));
282        assert!((0.0..100.0).contains(&y));
283    }
284}