bracket_pathfinding/
astar.rs

1use bracket_algorithm_traits::prelude::BaseMap;
2use std::cmp::Ordering;
3use std::collections::{BinaryHeap, HashMap};
4use std::convert::TryInto;
5
6/// Bail out if the A* search exceeds this many steps.
7const MAX_ASTAR_STEPS: usize = 65536;
8
9/// Request an A-Star search. The start and end are specified as index numbers (compatible with your
10/// BaseMap implementation), and it requires access to your map so as to call distance and exit determinations.
11pub fn a_star_search<T>(start: T, end: T, map: &dyn BaseMap) -> NavigationPath
12where
13    T: TryInto<usize>,
14{
15    AStar::new(start.try_into().ok().unwrap(), end.try_into().ok().unwrap()).search(map)
16}
17
18/// Holds the result of an A-Star navigation query.
19/// `destination` is the index of the target tile.
20/// `success` is true if it reached the target, false otherwise.
21/// `steps` is a vector of each step towards the target, *including* the starting position.
22#[derive(Clone, Default)]
23pub struct NavigationPath {
24    pub destination: usize,
25    pub success: bool,
26    pub steps: Vec<usize>,
27}
28
29#[allow(dead_code)]
30#[derive(Copy, Clone, Debug)]
31/// Node is an internal step inside the A-Star path (not exposed/public). Idx is the current cell,
32/// f is the total cost, g the neighbor cost, and h the heuristic cost.
33/// See: https://en.wikipedia.org/wiki/A*_search_algorithm
34struct Node {
35    idx: usize,
36    f: f32,
37    g: f32,
38}
39
40impl PartialEq for Node {
41    fn eq(&self, other: &Self) -> bool {
42        self.f == other.f
43    }
44}
45
46impl Eq for Node {}
47
48impl Ord for Node {
49    fn cmp(&self, b: &Self) -> Ordering {
50        b.f.partial_cmp(&self.f).unwrap()
51    }
52}
53
54impl PartialOrd for Node {
55    fn partial_cmp(&self, b: &Self) -> Option<Ordering> {
56        b.f.partial_cmp(&self.f)
57    }
58}
59
60impl NavigationPath {
61    /// Makes a new (empty) NavigationPath
62    pub fn new() -> NavigationPath {
63        NavigationPath {
64            destination: 0,
65            success: false,
66            steps: Vec::new(),
67        }
68    }
69}
70
71/// Private structure for calculating an A-Star navigation path.
72struct AStar {
73    start: usize,
74    end: usize,
75    open_list: BinaryHeap<Node>,
76    closed_list: HashMap<usize, f32>,
77    parents: HashMap<usize, (usize, f32)>, // (index, cost)
78    step_counter: usize,
79}
80
81impl AStar {
82    /// Creates a new path, with specified starting and ending indices.
83    fn new(start: usize, end: usize) -> AStar {
84        let mut open_list: BinaryHeap<Node> = BinaryHeap::new();
85        open_list.push(Node {
86            idx: start,
87            f: 0.0,
88            g: 0.0,
89        });
90
91        AStar {
92            start,
93            end,
94            open_list,
95            parents: HashMap::new(),
96            closed_list: HashMap::new(),
97            step_counter: 0,
98        }
99    }
100
101    /// Wrapper to the BaseMap's distance function.
102    fn distance_to_end(&self, idx: usize, map: &dyn BaseMap) -> f32 {
103        map.get_pathing_distance(idx, self.end)
104    }
105
106    /// Adds a successor; if we're at the end, marks success.
107    fn add_successor(&mut self, q: Node, idx: usize, cost: f32, map: &dyn BaseMap) {
108        let distance_to_end = self.distance_to_end(idx, map);
109        let s = Node {
110            idx,
111            f: q.g + cost + distance_to_end,
112            g: cost,
113        };
114
115        // If a node with the same position as successor is in the open list with a lower f, skip add
116        let mut should_add = true;
117        if let Some(e) = self.parents.get(&idx) {
118            if e.1 < s.g {
119                should_add = false;
120            }
121        }
122
123        // If a node with the same position as successor is in the closed list, with a lower f, skip add
124        if should_add && self.closed_list.contains_key(&idx) {
125            should_add = false;
126        }
127
128        if should_add {
129            self.open_list.push(s);
130            self.parents.insert(idx, (q.idx, s.g));
131        }
132    }
133
134    /// Helper function to unwrap a path once we've found the end-point.
135    fn found_it(&self) -> NavigationPath {
136        let mut result = NavigationPath::new();
137        result.success = true;
138        result.destination = self.end;
139
140        result.steps.push(self.end);
141        let mut current = self.end;
142        while current != self.start {
143            let parent = self.parents[&current];
144            result.steps.insert(0, parent.0);
145            current = parent.0;
146        }
147
148        result
149    }
150
151    /// Performs an A-Star search
152    fn search(&mut self, map: &dyn BaseMap) -> NavigationPath {
153        let result = NavigationPath::new();
154        while !self.open_list.is_empty() && self.step_counter < MAX_ASTAR_STEPS {
155            self.step_counter += 1;
156
157            // Pop Q off of the list
158            let q = self.open_list.pop().unwrap();
159            if q.idx == self.end {
160                let success = self.found_it();
161                return success;
162            }
163
164            // Generate successors
165            map.get_available_exits(q.idx)
166                .iter()
167                .for_each(|s| self.add_successor(q, s.0, s.1, map));
168
169            if self.closed_list.contains_key(&q.idx) {
170                self.closed_list.remove(&q.idx);
171            }
172            self.closed_list.insert(q.idx, q.f);
173        }
174        result
175    }
176}
177
178#[cfg(test)]
179mod test {
180    use bracket_algorithm_traits::prelude::BaseMap;
181    use smallvec::smallvec;
182
183    use super::a_star_search;
184
185    /// A triangular graph with unidirectional edges.
186    ///       1
187    ///       /\
188    ///  1.0 /  \ 1.0
189    ///     /    \
190    ///  0 /______\ 2
191    ///      3.0
192    struct TriangleMap;
193
194    impl BaseMap for TriangleMap {
195        fn get_available_exits(&self, idx: usize) -> smallvec::SmallVec<[(usize, f32); 10]> {
196            match idx {
197                0 => smallvec![(1, 1.0), (2, 3.0)],
198                1 => smallvec![(2, 1.0)],
199                _ => smallvec![],
200            }
201        }
202
203        fn get_pathing_distance(&self, idx1: usize, idx2: usize) -> f32 {
204            match (idx1, idx2) {
205                (0, 1) | (1, 2) => 1.0,
206                (0, 2) => 3.0,
207                (2, 2) => 0.0,
208                x => panic!("This distance should never be requested: {:?}", x),
209            }
210        }
211    }
212
213    #[test]
214    fn avoid_expensive_shortcut_on_triangle() {
215        let map = TriangleMap;
216        let path = a_star_search(0, 2, &map);
217        println!("{:?}", path.steps);
218        assert_eq!(path.steps, [0, 1, 2]);
219    }
220
221    /// A simple graph with `len` nodes. Same concept as the `TriangleMap`, but with more nodes in
222    /// the indirect path.
223    /// Each node is connected to it's successor but the first node also connects to the last this
224    /// "shortcut" has slightly higher cost than walking all the other nodes
225    struct ExpensiveShortcutMap {
226        len: usize,
227    }
228
229    impl BaseMap for ExpensiveShortcutMap {
230        fn get_available_exits(&self, idx: usize) -> smallvec::SmallVec<[(usize, f32); 10]> {
231            let mut exits = smallvec::SmallVec::new();
232
233            // shortcut to the end with slightly higher cost
234            if idx == 0 {
235                exits.push((self.len - 1, self.len as f32))
236            }
237            // step to next node
238            if idx <= self.len - 1 {
239                exits.push((idx + 1, 1.0));
240            }
241
242            exits
243        }
244
245        fn get_pathing_distance(&self, idx1: usize, idx2: usize) -> f32 {
246            if idx1 == 0 && idx2 == self.len {
247                return self.len as f32;
248            }
249            (idx1.abs_diff(idx2)) as f32
250        }
251    }
252
253    #[test]
254    fn avoid_expensive_shortcut() {
255        let len = 15;
256        let map = ExpensiveShortcutMap { len };
257        let path = a_star_search(0, len - 1, &map);
258        println!("{:?}", path.steps);
259        assert_eq!(path.steps, (0..len).collect::<Vec<_>>());
260    }
261}