screeps/
pathfinder.rs

1//! Manually accessing the [`PathFinder`] API
2//!
3//! This contains functionality from the [`PathFinder`] object in Screeps, which
4//! is itself a binding to a C++ Jump-Point Search pathfinding algorithm
5//! optimized for Screeps.
6//!
7//! This is both more fine-grained and less automatic than other pathing
8//! methods, such as [`Room::find_path`]. [`PathFinder`] knows about terrain
9//! by default, but you must configure any other obstacles you want it to
10//! consider.
11//!
12//! [Screeps documentation](https://docs.screeps.com/api/#PathFinder)
13//!
14//! [`Room::find_path`]: crate::objects::Room::find_path
15
16use js_sys::{Array, JsString, Object};
17use serde::Serialize;
18use wasm_bindgen::prelude::*;
19
20use crate::{
21    local::{Position, RoomName},
22    objects::{CostMatrix, RoomPosition},
23};
24
25#[wasm_bindgen]
26extern "C" {
27    /// Interfaces for calling the default Screeps [`PathFinder`].
28    #[wasm_bindgen]
29    pub type PathFinder;
30
31    /// Search for a path from an origin to a goal or array of goals.
32    ///
33    /// The goal, or each entry in the goal array if using an array, must be an
34    /// object with a position and optionally a `range` key, if a target
35    /// distance other than 0 is needed.
36    ///
37    /// [Screeps documentation](https://docs.screeps.com/api/#PathFinder.search)
38    #[wasm_bindgen(static_method_of = PathFinder, js_name = search)]
39    fn search_internal(origin: &RoomPosition, goal: &JsValue, options: &JsValue) -> SearchResults;
40}
41
42#[wasm_bindgen]
43extern "C" {
44    /// Object that represents a set of options for a call to
45    /// [`PathFinder::search`].
46    #[wasm_bindgen]
47    pub type JsSearchOptions;
48
49    /// Room callback, which should return a [`CostMatrix`], or
50    /// [`JsValue::FALSE`] to avoid pathing through a room.
51    #[wasm_bindgen(method, setter = roomCallback)]
52    pub fn room_callback(
53        this: &JsSearchOptions,
54        callback: &Closure<dyn FnMut(JsString) -> JsValue>,
55    );
56
57    /// Set the cost of moving on plains tiles during this pathfinder search.
58    /// Defaults to 1.
59    #[wasm_bindgen(method, setter = plainCost)]
60    pub fn plain_cost(this: &JsSearchOptions, cost: u8);
61
62    /// Set the cost of moving on swamp tiles during this pathfinder search.
63    /// Defaults to 5.
64    #[wasm_bindgen(method, setter = swampCost)]
65    pub fn swamp_cost(this: &JsSearchOptions, cost: u8);
66
67    /// Set whether to flee to a certain distance away from the target instead
68    /// of attempting to find a path to it. Defaults to false.
69    #[wasm_bindgen(method, setter = flee)]
70    pub fn flee(this: &JsSearchOptions, val: bool);
71
72    /// Set the maximum number of operations to allow the pathfinder to complete
73    /// before returning an incomplete path. Defaults to 2,000.
74    #[wasm_bindgen(method, setter = maxOps)]
75    pub fn max_ops(this: &JsSearchOptions, ops: u32);
76
77    /// Set the maximum number of rooms allowed to be pathed through. Defaults
78    /// to 16, maximum of 64.
79    #[wasm_bindgen(method, setter = maxRooms)]
80    pub fn max_rooms(this: &JsSearchOptions, rooms: u8);
81
82    /// Set the maximum total path cost allowed. No limit by default.
83    #[wasm_bindgen(method, setter = maxCost)]
84    pub fn max_cost(this: &JsSearchOptions, cost: f64);
85
86    /// Heuristic weight to use for the A* algorithm to be guided toward the
87    /// goal. Defaults to 1.2.
88    #[wasm_bindgen(method, setter = heuristicWeight)]
89    pub fn heuristic_weight(this: &JsSearchOptions, weight: f64);
90}
91
92impl JsSearchOptions {
93    pub fn new() -> JsSearchOptions {
94        Object::new().unchecked_into()
95    }
96}
97
98impl Default for JsSearchOptions {
99    fn default() -> Self {
100        Self::new()
101    }
102}
103
104#[wasm_bindgen]
105extern "C" {
106    /// An object representing the results of a [`PathFinder::search`].
107    #[wasm_bindgen]
108    pub type SearchResults;
109
110    /// Get the path that was found, an [`Array`] of [`RoomPosition`]. May be
111    /// incomplete.
112    #[wasm_bindgen(method, getter, js_name = path)]
113    fn path_internal(this: &SearchResults) -> Array;
114
115    /// The number of operations the pathfinding operation performed.
116    #[wasm_bindgen(method, getter)]
117    pub fn ops(this: &SearchResults) -> u32;
118
119    /// Total cost of all tiles used in the path
120    #[wasm_bindgen(method, getter)]
121    pub fn cost(this: &SearchResults) -> u32;
122
123    /// Whether this search successfully found a complete path.
124    #[wasm_bindgen(method, getter)]
125    pub fn incomplete(this: &SearchResults) -> bool;
126}
127
128impl SearchResults {
129    pub fn path(&self) -> Vec<Position> {
130        self.path_internal()
131            .iter()
132            .map(|p| p.unchecked_into())
133            .map(|p: RoomPosition| p.into())
134            .collect()
135    }
136
137    pub fn opaque_path(&self) -> Array {
138        self.path_internal()
139    }
140}
141
142pub trait RoomCostResult: Into<JsValue> {}
143
144#[derive(Default)]
145pub enum MultiRoomCostResult {
146    CostMatrix(CostMatrix),
147    Impassable,
148    #[default]
149    Default,
150}
151
152impl RoomCostResult for MultiRoomCostResult {}
153
154impl From<MultiRoomCostResult> for JsValue {
155    fn from(v: MultiRoomCostResult) -> JsValue {
156        match v {
157            MultiRoomCostResult::CostMatrix(m) => m.into(),
158            MultiRoomCostResult::Impassable => JsValue::from_bool(false),
159            MultiRoomCostResult::Default => JsValue::undefined(),
160        }
161    }
162}
163
164#[derive(Default)]
165pub enum SingleRoomCostResult {
166    CostMatrix(CostMatrix),
167    #[default]
168    Default,
169}
170
171impl RoomCostResult for SingleRoomCostResult {}
172
173impl From<SingleRoomCostResult> for JsValue {
174    fn from(v: SingleRoomCostResult) -> JsValue {
175        match v {
176            SingleRoomCostResult::CostMatrix(m) => m.into(),
177            SingleRoomCostResult::Default => JsValue::undefined(),
178        }
179    }
180}
181
182pub struct SearchOptions<F>
183where
184    F: FnMut(RoomName) -> MultiRoomCostResult,
185{
186    callback: F,
187    inner: InnerSearchOptions,
188}
189
190#[derive(Default, Serialize)]
191#[serde(rename_all = "camelCase")]
192struct InnerSearchOptions {
193    plain_cost: Option<u8>,
194    swamp_cost: Option<u8>,
195    flee: Option<bool>,
196    max_ops: Option<u32>,
197    max_rooms: Option<u8>,
198    max_cost: Option<f64>,
199    heuristic_weight: Option<f64>,
200}
201
202impl<F> SearchOptions<F>
203where
204    F: FnMut(RoomName) -> MultiRoomCostResult,
205{
206    pub(crate) fn as_js_options<R>(&mut self, callback: impl Fn(&JsSearchOptions) -> R) -> R {
207        // Serialize the inner options into a JsValue, then cast.
208        let js_options: JsSearchOptions = serde_wasm_bindgen::to_value(&self.inner)
209            .expect("Unable to serialize search options.")
210            .unchecked_into();
211
212        let boxed_callback: Box<dyn FnMut(JsString) -> JsValue> = Box::new(move |room| {
213            let room = room
214                .try_into()
215                .expect("expected room name in room callback");
216            (self.callback)(room).into()
217        });
218
219        // SAFETY
220        //
221        // self.callback is valid during the whole lifetime of the as_js_options call,
222        // and this Box is dropped before the call finishes without the contents
223        // being held on to by JS.
224        let boxed_callback_lifetime_erased: Box<dyn 'static + FnMut(JsString) -> JsValue> =
225            unsafe { std::mem::transmute(boxed_callback) };
226
227        let closure = Closure::wrap(boxed_callback_lifetime_erased);
228
229        js_options.room_callback(&closure);
230
231        callback(&js_options)
232    }
233}
234
235impl Default for SearchOptions<fn(RoomName) -> MultiRoomCostResult> {
236    fn default() -> Self {
237        fn cost_matrix(_: RoomName) -> MultiRoomCostResult {
238            MultiRoomCostResult::Default
239        }
240
241        SearchOptions {
242            callback: cost_matrix,
243            inner: Default::default(),
244        }
245    }
246}
247
248impl<F> SearchOptions<F>
249where
250    F: FnMut(RoomName) -> MultiRoomCostResult,
251{
252    #[inline]
253    pub fn new(callback: F) -> Self {
254        SearchOptions {
255            callback,
256            inner: Default::default(),
257        }
258    }
259
260    pub fn room_callback<F2>(self, callback: F2) -> SearchOptions<F2>
261    where
262        F2: FnMut(RoomName) -> MultiRoomCostResult,
263    {
264        SearchOptions {
265            callback,
266            inner: self.inner,
267        }
268    }
269
270    /// Sets plain cost - default `1`.
271    #[inline]
272    pub fn plain_cost(mut self, cost: u8) -> Self {
273        self.inner.plain_cost = Some(cost);
274        self
275    }
276
277    /// Sets swamp cost - default `5`.
278    #[inline]
279    pub fn swamp_cost(mut self, cost: u8) -> Self {
280        self.inner.swamp_cost = Some(cost);
281        self
282    }
283
284    /// Sets whether this is a flee search - default `false`.
285    #[inline]
286    pub fn flee(mut self, flee: bool) -> Self {
287        self.inner.flee = Some(flee);
288        self
289    }
290
291    /// Sets maximum ops - default `2000`.
292    #[inline]
293    pub fn max_ops(mut self, ops: u32) -> Self {
294        self.inner.max_ops = Some(ops);
295        self
296    }
297
298    /// Sets maximum rooms - default `16`, max `16`.
299    #[inline]
300    pub fn max_rooms(mut self, rooms: u8) -> Self {
301        self.inner.max_rooms = Some(rooms);
302        self
303    }
304
305    /// Sets maximum path cost - default `f64::Infinity`.
306    #[inline]
307    pub fn max_cost(mut self, cost: f64) -> Self {
308        self.inner.max_cost = Some(cost);
309        self
310    }
311
312    /// Sets heuristic weight - default `1.2`.
313    #[inline]
314    pub fn heuristic_weight(mut self, weight: f64) -> Self {
315        self.inner.heuristic_weight = Some(weight);
316        self
317    }
318}
319
320#[wasm_bindgen]
321pub struct SearchGoal {
322    pos: Position,
323    range: u32,
324}
325
326impl SearchGoal {
327    pub fn new(pos: Position, range: u32) -> Self {
328        SearchGoal { pos, range }
329    }
330}
331
332#[wasm_bindgen]
333impl SearchGoal {
334    #[wasm_bindgen(getter)]
335    pub fn pos(&self) -> RoomPosition {
336        self.pos.into()
337    }
338
339    #[wasm_bindgen(getter)]
340    pub fn range(&self) -> u32 {
341        self.range
342    }
343}
344
345pub fn search<F>(
346    from: Position,
347    to: Position,
348    range: u32,
349    options: Option<SearchOptions<F>>,
350) -> SearchResults
351where
352    F: FnMut(RoomName) -> MultiRoomCostResult,
353{
354    let goal = SearchGoal { pos: to, range };
355
356    let goal = JsValue::from(goal);
357
358    search_real(from, &goal, options)
359}
360
361pub fn search_many<F>(
362    from: Position,
363    to: impl Iterator<Item = SearchGoal>,
364    options: Option<SearchOptions<F>>,
365) -> SearchResults
366where
367    F: FnMut(RoomName) -> MultiRoomCostResult,
368{
369    let goals: Array = to.map(JsValue::from).collect();
370
371    search_real(from, goals.as_ref(), options)
372}
373
374fn search_real<F>(
375    from: Position,
376    goal: &JsValue,
377    options: Option<SearchOptions<F>>,
378) -> SearchResults
379where
380    F: FnMut(RoomName) -> MultiRoomCostResult,
381{
382    let from = from.into();
383
384    if let Some(mut options) = options {
385        options.as_js_options(|js_options| PathFinder::search_internal(&from, goal, js_options))
386    } else {
387        PathFinder::search_internal(&from, goal, &JsValue::UNDEFINED)
388    }
389}