screeps/local/position/
world_utils.rs

1use crate::local::{position::WorldPositionOutOfBoundsError, VALID_WORLD_POSITIONS};
2
3use super::{Position, HALF_WORLD_SIZE};
4
5impl Position {
6    /// Returns this position's horizontal "world coordinate".
7    ///
8    /// The value is equal to `50 * room_x + x`, where `room_x` is defined as
9    /// `room_x = -xx - 1` for `Wxx` rooms and as `room_x = xx` for `Exx` rooms.
10    #[inline]
11    pub fn world_x(self) -> i32 {
12        self.room_x() * 50 + (u8::from(self.x()) as i32)
13    }
14
15    /// Returns this position's vertical "world coordinate".
16    ///
17    /// The value is equal to `50 * room_y + y`, where `room_y` is defined as
18    /// `room_y = -yy - 1` for `Nyy` rooms and as `room_y = yy` for `Syy` rooms.
19    #[inline]
20    pub fn world_y(self) -> i32 {
21        self.room_y() * 50 + (u8::from(self.y()) as i32)
22    }
23
24    /// Returns this position's "world coordinates".
25    ///
26    /// The first value is equal to `50 * room_x + x`, where `room_x` is defined
27    /// as `room_x = -xx - 1` for `Wxx` rooms and as `room_x = xx` for `Exx`
28    /// rooms.
29    ///
30    /// The second value is equal to `50 * room_y + y`, where `room_y` is
31    /// defined as `room_y = -yy - 1` for `Nyy` rooms and as `room_y = yy`
32    /// for `Syy` rooms.
33    ///
34    /// See also [`Position::world_x`] and
35    /// [`Position::world_y`].
36    #[inline]
37    pub fn world_coords(self) -> (i32, i32) {
38        (self.world_x(), self.world_y())
39    }
40
41    /// Creates a room position from world coords.
42    ///
43    /// # Panics
44    ///
45    /// Panics if either x or y is out of the range `-128 * 50 .. +128 * 50`.
46    ///
47    /// For a checked variant of this function, see
48    /// [`Position::checked_from_world_coords`].
49    ///
50    /// See [`Position::world_coords`].
51    #[inline]
52    #[track_caller]
53    pub fn from_world_coords(x: i32, y: i32) -> Self {
54        Self::checked_from_world_coords(x, y).unwrap()
55    }
56
57    /// Creates a room position from world coords if they are within the range
58    /// `-128 * 50 .. +128 * 50`. Otherwise returns `None`.
59    ///
60    /// For a panicing variant of this function, see
61    /// [`Position::from_world_coords`].
62    ///
63    /// See [`Position::world_coords`].
64    #[inline]
65    pub fn checked_from_world_coords(
66        x: i32,
67        y: i32,
68    ) -> Result<Self, WorldPositionOutOfBoundsError> {
69        if VALID_WORLD_POSITIONS.contains(&x) && VALID_WORLD_POSITIONS.contains(&y) {
70            // We do the `HALF_WORLD_SIZE` transition here first so that the division and
71            // modulo operations work correctly.
72            let pos_x = (x + HALF_WORLD_SIZE * 50) as u32;
73            let pos_y = (y + HALF_WORLD_SIZE * 50) as u32;
74            let room_x = pos_x / 50;
75            let room_y = pos_y / 50;
76            let x = (pos_x % 50) as u8;
77            let y = (pos_y % 50) as u8;
78
79            Ok(Self::from_coords_and_world_coords_adjusted(
80                x, y, room_x, room_y,
81            ))
82        } else {
83            Err(WorldPositionOutOfBoundsError(x, y))
84        }
85    }
86}
87
88#[cfg(test)]
89mod test {
90    use super::Position;
91    use crate::{
92        local::{position::WorldPositionOutOfBoundsError, RoomCoordinate},
93        ROOM_SIZE,
94    };
95    use core::ops::Range;
96
97    const TEST_ROOM_NAMES: &[&str] = &[
98        "E1N1", "E20N0", "W0N0", "E0N0", "W0S0", "E0S0", "W0N0", "E0N0", "W0S0", "E0S0", "W50S20",
99        "W127S127", "W127N127", "E127S127", "E127N127",
100    ];
101
102    fn gen_test_coords() -> [RoomCoordinate; 4] {
103        unsafe {
104            [
105                RoomCoordinate::unchecked_new(0),
106                RoomCoordinate::unchecked_new(21),
107                RoomCoordinate::unchecked_new(44),
108                RoomCoordinate::unchecked_new(49),
109            ]
110        }
111    }
112
113    #[test]
114    fn world_coords_round_trip() {
115        for room_name in TEST_ROOM_NAMES {
116            for x in gen_test_coords().iter().cloned() {
117                for y in gen_test_coords().iter().cloned() {
118                    let original_pos = Position::new(x, y, room_name.parse().unwrap());
119                    let (wx, wy) = original_pos.world_coords();
120                    let new = Position::from_world_coords(wx, wy);
121                    assert_eq!(original_pos, new);
122                }
123            }
124        }
125    }
126
127    #[test]
128    fn checked_world_coords() {
129        // this tests:
130        // - the 16 rooms around the center of the world
131        // - the 16 rooms around each corner of the max world size (12 of them are out
132        //   of bounds)
133
134        const ROOM_RANGE: Range<i32> = -((ROOM_SIZE as i32) * 2)..((ROOM_SIZE as i32) * 2);
135        for x in ROOM_RANGE {
136            for y in ROOM_RANGE {
137                let room_x = x.div_euclid(50);
138                let room_y = y.div_euclid(50);
139                let pos_x = x.rem_euclid(50) as u8;
140                let pos_y = y.rem_euclid(50) as u8;
141
142                let new_pos = Position::checked_from_world_coords(x, y).unwrap();
143                assert_eq!(room_x, new_pos.room_x());
144                assert_eq!(room_y, new_pos.room_y());
145                assert_eq!(pos_x, new_pos.x().u8());
146                assert_eq!(pos_y, new_pos.y().u8());
147            }
148        }
149
150        const CORNERS: [(i32, i32); 4] =
151            [(-6400, 6399), (6399, 6399), (-6400, -6400), (6399, 6400)];
152        for (corner_x, corner_y) in CORNERS {
153            for x in ROOM_RANGE {
154                for y in ROOM_RANGE {
155                    let x = corner_x + x;
156                    let y = corner_y + y;
157
158                    if !(-6400..=6399).contains(&x) || !(-6400..=6399).contains(&y) {
159                        assert_eq!(
160                            Err(WorldPositionOutOfBoundsError(x, y)),
161                            Position::checked_from_world_coords(x, y)
162                        );
163                    } else {
164                        let room_x = x.div_euclid(50);
165                        let room_y = y.div_euclid(50);
166                        let pos_x = x.rem_euclid(50) as u8;
167                        let pos_y = y.rem_euclid(50) as u8;
168
169                        let new_pos = Position::checked_from_world_coords(x, y).unwrap();
170                        assert_eq!(room_x, new_pos.room_x());
171                        assert_eq!(room_y, new_pos.room_y());
172                        assert_eq!(pos_x, new_pos.x().u8());
173                        assert_eq!(pos_y, new_pos.y().u8());
174                    }
175                }
176            }
177        }
178    }
179
180    #[test]
181    #[should_panic(expected = "WorldPositionOutOfBoundsError(6400, 6400)")]
182    fn oob_world_coords_panic() {
183        // note: world coords are -6400..6400 (not including the end)
184        let _val = Position::from_world_coords(6400, 6400);
185    }
186
187    #[test]
188    #[should_panic(expected = "WorldPositionOutOfBoundsError(6400, 6399)")]
189    fn oob_coords_add() {
190        let pos = Position::from_world_coords(6395, 6399);
191        let _new_pos = pos + (5, 0);
192    }
193
194    // don't run this test if debug assertions are enabled, it won't complete
195    #[cfg(not(debug_assertions))]
196    #[test]
197    fn exhaustive_checked_world_coords() {
198        use crate::local::VALID_WORLD_POSITIONS;
199        // Test that the entire input space returns `Some` or `None` as expected.
200        // If this test completes in release mode, it means that the compiler is able to
201        // prove enough to optimize it away. If the test stops being instant,
202        // something went wrong with the implementation or the compiler (probably the
203        // implementation).
204        for x in i32::MIN..=i32::MAX {
205            for y in i32::MIN..=i32::MAX {
206                if VALID_WORLD_POSITIONS.contains(&x) && VALID_WORLD_POSITIONS.contains(&y) {
207                    assert!(matches!(Position::checked_from_world_coords(x, y), Ok(_)));
208                } else {
209                    assert_eq!(
210                        Err(WorldPositionOutOfBoundsError(x, y)),
211                        Position::checked_from_world_coords(x, y)
212                    );
213                }
214            }
215        }
216    }
217}