diff --git a/CHANGELOG.md b/CHANGELOG.md index 30774372..30da01fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,28 @@ Unreleased every position set to a given `u8` value - Implement `JsCollectionIntoValue` and `JsCollectionFromValue` for `IntershardResourceType` and `u32` to allow `game::resources()` return value to be used as expected +- Add static function `RoomXY::new` to allow creating a new `RoomXY` from `RoomCoordinates` +- Add static function `RoomXY::checked_new` to allow creating a new `RoomXY` from a (u8, u8) + pair, while checking the validity of the coordinates provided +- Add function `RoomXY::towards` which returns a `RoomXY` between two `RoomXY` positions, + rounding towards the start position if necessary +- Add function `RoomXY::between` which returns a `RoomXY` between two `RoomXY` positions, + rounding towards the target position if necessary +- Add function `RoomXY::midpoint_between` which returns the `RoomXY` midpoint between + two `RoomXY` positions, rounding towards the target position if necessary +- Add function `RoomXY::offset` which modifies a `RoomXY` in-place by a (i8, i8) offset +- Add function `RoomXY::get_direction_to` which returns a `Direction` that is closest to + a given `RoomXY` position +- Add function `RoomXY::get_range_to` which returns the Chebyshev Distance to a given + `RoomXY` position +- Add function `RoomXY::in_range_to` which returns whether the Chebyshev Distance to a given + `RoomXY` position is less-than-or-equal-to a given distance +- Add function `RoomXY::is_near_to` which returns whether a given `RoomXY` position is adjacent +- Add function `RoomXY::is_equal_to` which returns whether a given `RoomXY` position is + the same position +- Implement the `PartialOrd` and `Ord` traits for `RoomXY` +- Implement the `Add<(i8, i8)>`, `Add`, `Sub<(i8, i8)>`, `Sub`, + and `Sub` traits for `RoomXY` 0.21.0 (2024-05-14) =================== diff --git a/src/local/room_xy.rs b/src/local/room_xy.rs index 368c9156..c11bb441 100644 --- a/src/local/room_xy.rs +++ b/src/local/room_xy.rs @@ -1,10 +1,14 @@ -use std::fmt; +use std::{cmp::Ordering, fmt}; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use super::room_coordinate::{OutOfBoundsError, RoomCoordinate}; use crate::constants::{Direction, ROOM_SIZE}; +mod approximate_offsets; +mod extra_math; +mod game_math; + pub(crate) const ROOM_AREA: usize = (ROOM_SIZE as usize) * (ROOM_SIZE as usize); /// Converts a [`RoomXY`] coordinate pair to a linear index appropriate for use @@ -67,6 +71,19 @@ pub struct RoomXY { } impl RoomXY { + /// Create a new `RoomXY` from a pair of `RoomCoordinate`. + #[inline] + pub fn new(x: RoomCoordinate, y: RoomCoordinate) -> Self { + RoomXY { x, y } + } + + /// Create a new `RoomXY` from a pair of `u8`, checking that they're in + /// the range of valid values. + #[inline] + pub fn checked_new(x: u8, y: u8) -> Result { + RoomXY::try_from((x, y)) + } + /// Create a `RoomXY` from a pair of `u8`, without checking whether it's in /// the range of valid values. /// @@ -229,6 +246,19 @@ impl RoomXY { } } +impl PartialOrd for RoomXY { + #[inline] + fn partial_cmp(&self, other: &RoomXY) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for RoomXY { + fn cmp(&self, other: &Self) -> Ordering { + (self.y, self.x).cmp(&(other.y, other.x)) + } +} + impl fmt::Display for RoomXY { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "({}, {})", self.x, self.y) diff --git a/src/local/room_xy/approximate_offsets.rs b/src/local/room_xy/approximate_offsets.rs new file mode 100644 index 00000000..b141b6e6 --- /dev/null +++ b/src/local/room_xy/approximate_offsets.rs @@ -0,0 +1,236 @@ +//! Methods related to approximating in-room positions +//! between other in-room positions. + +use super::RoomXY; + +impl RoomXY { + /// Calculates an approximate midpoint between this point and the target. + /// + /// In case of a tie, rounds towards this point. + /// + /// If `distance_towards_target` is bigger than the distance to the target, + /// the target is returned. + /// + /// # Example + /// + /// ``` + /// # use screeps::RoomXY; + /// + /// // Exact distances + /// let start = RoomXY::checked_new(10, 10).unwrap(); + /// let target = RoomXY::checked_new(10, 15).unwrap(); + /// assert_eq!( + /// start.towards(target, 1), + /// RoomXY::checked_new(10, 11).unwrap() + /// ); + /// assert_eq!( + /// start.towards(target, 4), + /// RoomXY::checked_new(10, 14).unwrap() + /// ); + /// assert_eq!( + /// start.towards(target, 10), + /// RoomXY::checked_new(10, 15).unwrap() + /// ); + /// + /// // Approximate/rounded distances + /// let start = RoomXY::checked_new(10, 10).unwrap(); + /// let target_1 = RoomXY::checked_new(15, 20).unwrap(); + /// let target_2 = RoomXY::checked_new(0, 5).unwrap(); + /// assert_eq!( + /// start.towards(target_1, 1), + /// RoomXY::checked_new(10, 11).unwrap() + /// ); + /// assert_eq!( + /// start.towards(target_1, 9), + /// RoomXY::checked_new(14, 19).unwrap() + /// ); + /// assert_eq!( + /// start.towards(target_2, 1), + /// RoomXY::checked_new(9, 10).unwrap() + /// ); + /// ``` + pub fn towards(self, target: RoomXY, distance_towards_target: i8) -> RoomXY { + let (offset_x, offset_y) = target - self; + let total_distance = offset_x.abs().max(offset_y.abs()); + if distance_towards_target > total_distance { + return target; + } + + let new_offset_x = (offset_x * distance_towards_target) / total_distance; + let new_offset_y = (offset_y * distance_towards_target) / total_distance; + + self + (new_offset_x, new_offset_y) + } + + /// Calculates an approximate midpoint between this point and the target. + /// + /// In case of a tie, rounds towards the target. + /// + /// If `distance_from_target` is bigger than the distance to the target, + /// this position is returned. + /// + /// Note: This is essentially the same as [`RoomXY::towards`], just rounding + /// towards the target instead of the starting position. + /// + /// # Example + /// + /// ``` + /// # use screeps::RoomXY; + /// + /// // Exact distances + /// let start = RoomXY::checked_new(10, 15).unwrap(); + /// let target = RoomXY::checked_new(10, 10).unwrap(); + /// assert_eq!( + /// start.between(target, 1), + /// RoomXY::checked_new(10, 11).unwrap() + /// ); + /// assert_eq!( + /// start.between(target, 4), + /// RoomXY::checked_new(10, 14).unwrap() + /// ); + /// assert_eq!( + /// start.between(target, 10), + /// RoomXY::checked_new(10, 15).unwrap() + /// ); + /// + /// // Approximate/rounded distances + /// let start_1 = RoomXY::checked_new(15, 20).unwrap(); + /// let start_2 = RoomXY::checked_new(0, 5).unwrap(); + /// let target = RoomXY::checked_new(10, 10).unwrap(); + /// assert_eq!( + /// start_1.between(target, 1), + /// RoomXY::checked_new(10, 11).unwrap() + /// ); + /// assert_eq!( + /// start_1.between(target, 9), + /// RoomXY::checked_new(14, 19).unwrap() + /// ); + /// assert_eq!( + /// start_2.between(target, 1), + /// RoomXY::checked_new(9, 10).unwrap() + /// ); + /// ``` + pub fn between(self, target: RoomXY, distance_from_target: i8) -> RoomXY { + target.towards(self, distance_from_target) + } + + /// Calculates an approximate midpoint between this point and the target. + /// + /// In case of a tie, rounds towards the target. + /// + /// # Example + /// + /// ``` + /// # use screeps::RoomXY; + /// + /// // Exact distances + /// let start = RoomXY::checked_new(10, 10).unwrap(); + /// + /// let target_1 = RoomXY::checked_new(10, 16).unwrap(); + /// assert_eq!( + /// start.midpoint_between(target_1), + /// RoomXY::checked_new(10, 13).unwrap() + /// ); + /// + /// let target_2 = RoomXY::checked_new(20, 10).unwrap(); + /// assert_eq!( + /// start.midpoint_between(target_2), + /// RoomXY::checked_new(15, 10).unwrap() + /// ); + /// + /// let target_3 = RoomXY::checked_new(12, 12).unwrap(); + /// assert_eq!( + /// start.midpoint_between(target_3), + /// RoomXY::checked_new(11, 11).unwrap() + /// ); + /// + /// let target_4 = RoomXY::checked_new(4, 4).unwrap(); + /// assert_eq!( + /// start.midpoint_between(target_4), + /// RoomXY::checked_new(7, 7).unwrap() + /// ); + /// + /// // Approximate/rounded distances + /// let start = RoomXY::checked_new(10, 10).unwrap(); + /// + /// let target_1 = RoomXY::checked_new(10, 15).unwrap(); + /// assert_eq!( + /// start.midpoint_between(target_1), + /// RoomXY::checked_new(10, 13).unwrap() + /// ); + /// + /// let target_2 = RoomXY::checked_new(19, 10).unwrap(); + /// assert_eq!( + /// start.midpoint_between(target_2), + /// RoomXY::checked_new(15, 10).unwrap() + /// ); + /// + /// let target_3 = RoomXY::checked_new(11, 11).unwrap(); + /// assert_eq!( + /// start.midpoint_between(target_3), + /// RoomXY::checked_new(11, 11).unwrap() + /// ); + /// + /// let target_4 = RoomXY::checked_new(15, 15).unwrap(); + /// assert_eq!( + /// start.midpoint_between(target_4), + /// RoomXY::checked_new(13, 13).unwrap() + /// ); + /// ``` + pub fn midpoint_between(self, target: RoomXY) -> RoomXY { + let (offset_x, offset_y) = self - target; + + let new_offset_x = offset_x / 2; + let new_offset_y = offset_y / 2; + + target + (new_offset_x, new_offset_y) + } +} + +#[cfg(test)] +mod test { + use super::RoomXY; + + fn pos(x: u8, y: u8) -> RoomXY { + RoomXY::checked_new(x, y).unwrap() + } + + #[test] + fn towards_accurate() { + let start = pos(10, 10); + assert_eq!(start.towards(pos(10, 15), 1), pos(10, 11)); + assert_eq!(start.towards(pos(10, 15), 4), pos(10, 14)); + assert_eq!(start.towards(pos(10, 15), 10), pos(10, 15)); + assert_eq!(start.towards(pos(15, 15), 1), pos(11, 11)); + assert_eq!(start.towards(pos(15, 15), 3), pos(13, 13)); + assert_eq!(start.towards(pos(15, 20), 2), pos(11, 12)); + assert_eq!(start.towards(pos(0, 5), 2), pos(8, 9)); + } + #[test] + fn towards_approximate() { + let start = pos(10, 10); + assert_eq!(start.towards(pos(15, 20), 1), pos(10, 11)); + assert_eq!(start.towards(pos(15, 20), 9), pos(14, 19)); + assert_eq!(start.towards(pos(0, 5), 1), pos(9, 10)); + } + #[test] + fn midpoint_accurate() { + let start = pos(10, 10); + assert_eq!(start.midpoint_between(pos(10, 16)), pos(10, 13)); + assert_eq!(start.midpoint_between(pos(20, 10)), pos(15, 10)); + assert_eq!(start.midpoint_between(pos(12, 12)), pos(11, 11)); + assert_eq!(start.midpoint_between(pos(4, 4)), pos(7, 7)); + } + #[test] + fn midpoint_approximate() { + let start = pos(10, 10); + assert_eq!(start.midpoint_between(pos(10, 15)), pos(10, 13)); + assert_eq!(start.midpoint_between(pos(19, 10)), pos(15, 10)); + assert_eq!(start.midpoint_between(pos(11, 11)), pos(11, 11)); + assert_eq!(start.midpoint_between(pos(15, 15)), pos(13, 13)); + assert_eq!(start.midpoint_between(pos(15, 25)), pos(13, 18)); + assert_eq!(start.midpoint_between(pos(9, 10)), pos(9, 10)); + assert_eq!(start.midpoint_between(pos(7, 10)), pos(8, 10)); + assert_eq!(start.midpoint_between(pos(1, 3)), pos(5, 6)); + } +} diff --git a/src/local/room_xy/extra_math.rs b/src/local/room_xy/extra_math.rs new file mode 100644 index 00000000..49b75b39 --- /dev/null +++ b/src/local/room_xy/extra_math.rs @@ -0,0 +1,167 @@ +//! Math utilities on `RoomXY` which don't exist in the Screeps API +//! proper. + +use std::ops::{Add, Sub}; + +use super::RoomXY; +use crate::constants::Direction; + +impl RoomXY { + /// Returns a new position offset from this position by the specified x + /// coords and y coords. + /// + /// Unlike [`Position::offset`], this function operates on room coordinates, + /// and will panic if the new position overflows the room. + /// + /// To return a new position rather than modifying in place, use `pos + (x, + /// y)`. See the implementation of `Add<(i8, i8)>` for + /// [`RoomXY`] further down on this page. + /// + /// # Panics + /// + /// Will panic if the new position overflows the room. + /// + /// # Example + /// + /// ``` + /// # use screeps::RoomXY; + /// + /// let mut pos = RoomXY::checked_new(21, 21).unwrap(); + /// pos.offset(5, 5); + /// assert_eq!(pos, RoomXY::checked_new(26, 26).unwrap()); + /// + /// let mut pos = RoomXY::checked_new(21, 21).unwrap(); + /// pos.offset(-5, 5); + /// assert_eq!(pos, RoomXY::checked_new(16, 26).unwrap()); + /// ``` + #[inline] + #[track_caller] + pub fn offset(&mut self, x: i8, y: i8) { + *self = *self + (x, y); + } +} + +impl Add<(i8, i8)> for RoomXY { + type Output = RoomXY; + + /// Adds an `(x, y)` pair to this position's coordinates. + /// + /// # Panics + /// + /// Will panic if the new position is outside standard room bounds. + /// + /// # Example + /// + /// ``` + /// # use screeps::RoomXY; + /// + /// let pos1 = RoomXY::checked_new(42, 42).unwrap(); + /// let pos2 = pos1 + (7, 7); + /// assert_eq!(pos2, RoomXY::checked_new(49, 49).unwrap()); + /// ``` + #[inline] + #[track_caller] + fn add(self, (x, y): (i8, i8)) -> Self { + self.checked_add((x, y)).unwrap() + } +} + +impl Add for RoomXY { + type Output = RoomXY; + + /// Adds a `Direction` to this position's coordinates. + /// + /// # Panics + /// + /// Will panic if the new position is outside standard room bounds. + /// + /// # Example + /// + /// ``` + /// # use screeps::{RoomXY, Direction}; + /// + /// let pos1 = RoomXY::checked_new(49, 40).unwrap(); + /// let pos2 = pos1 + Direction::Top; + /// assert_eq!(pos2, RoomXY::checked_new(49, 39).unwrap()); + /// ``` + #[inline] + #[track_caller] + fn add(self, direction: Direction) -> Self { + self.checked_add_direction(direction).unwrap() + } +} + +impl Sub<(i8, i8)> for RoomXY { + type Output = RoomXY; + + /// Subtracts an `(x, y)` pair from this position's coordinates. + /// + /// # Panics + /// + /// Will panic if the new position is outside standard room bounds. + /// + /// # Example + /// + /// ``` + /// # use screeps::RoomXY; + /// + /// let pos1 = RoomXY::checked_new(49, 40).unwrap(); + /// let pos2 = pos1 - (49, 0); + /// assert_eq!(pos2, RoomXY::checked_new(0, 40).unwrap()); + /// ``` + #[inline] + #[track_caller] + fn sub(self, (x, y): (i8, i8)) -> Self { + self.checked_add((-x, -y)).unwrap() + } +} + +impl Sub for RoomXY { + type Output = RoomXY; + + /// Subtracts a `Direction` from this position's coordinates. + /// + /// # Panics + /// + /// Will panic if the new position is outside standard room bounds. + /// + /// # Example + /// + /// ``` + /// # use screeps::{RoomXY, Direction}; + /// + /// let pos1 = RoomXY::checked_new(49, 40).unwrap(); + /// let pos2 = pos1 - Direction::Top; + /// assert_eq!(pos2, RoomXY::checked_new(49, 41).unwrap()); + /// ``` + #[inline] + fn sub(self, direction: Direction) -> Self { + self.checked_add_direction(-direction).unwrap() + } +} + +impl Sub for RoomXY { + type Output = (i8, i8); + + /// Subtracts the other position from this one, extracting the + /// difference as the output. + /// + /// # Example + /// + /// ``` + /// # use screeps::RoomXY; + /// + /// let pos1 = RoomXY::checked_new(40, 40).unwrap(); + /// let pos2 = RoomXY::checked_new(0, 20).unwrap(); + /// assert_eq!(pos1 - pos2, (40, 20)); + /// + /// let pos3 = RoomXY::checked_new(45, 45).unwrap(); + /// assert_eq!(pos1 - pos3, (-5, -5)); + /// ``` + #[inline] + fn sub(self, other: RoomXY) -> (i8, i8) { + let dx = self.x.0.wrapping_sub(other.x.0) as i8; + let dy = self.y.0.wrapping_sub(other.y.0) as i8; + (dx, dy) + } +} diff --git a/src/local/room_xy/game_math.rs b/src/local/room_xy/game_math.rs new file mode 100644 index 00000000..46abcf1d --- /dev/null +++ b/src/local/room_xy/game_math.rs @@ -0,0 +1,151 @@ +//! Utilities for doing math on [`RoomXY`]s which are present in the +//! JavaScript API. + +use crate::constants::Direction; + +use super::RoomXY; + +impl RoomXY { + /// Gets linear direction to the specified position. + /// + /// Note that this chooses between `Top`/`Bottom`/`Left`/`Right` and + /// `TopLeft`/`TopRight`/`BottomLeft`/`BottomRight` by the magnitude in both + /// directions. For instance, [`Direction::Top`] can be returned even + /// if the target has a slightly different `x` coordinate. + pub fn get_direction_to(self, target: RoomXY) -> Option { + // Logic copied from https://github.com/screeps/engine/blob/020ba168a1fde9a8072f9f1c329d5c0be8b440d7/src/utils.js#L73-L107 + let (dx, dy) = target - self; + if dx.abs() > dy.abs() * 2 { + if dx > 0 { + Some(Direction::Right) + } else { + Some(Direction::Left) + } + } else if dy.abs() > dx.abs() * 2 { + if dy > 0 { + Some(Direction::Bottom) + } else { + Some(Direction::Top) + } + } else if dx > 0 && dy > 0 { + Some(Direction::BottomRight) + } else if dx > 0 && dy < 0 { + Some(Direction::TopRight) + } else if dx < 0 && dy > 0 { + Some(Direction::BottomLeft) + } else if dx < 0 && dy < 0 { + Some(Direction::TopLeft) + } else { + None + } + } + + /// Gets linear range to the specified position. + /// + /// Linear range (also called Chebyshev Distance) is an alternate + /// calculation of distance, calculated as the greater of the distance along + /// the x axis or the y axis. Most calculations in Screeps use this distance + /// metric. For more information see [Chebeshev Distance](https://en.wikipedia.org/wiki/Chebyshev_distance). + /// + /// # Examples + /// ```rust + /// # use screeps::RoomXY; + /// let pos_1 = RoomXY::checked_new(5, 10).unwrap(); + /// let pos_2 = RoomXY::checked_new(8, 15).unwrap(); + /// // The differences are 3 along the X axis and 5 along the Y axis + /// // so the linear distance is 5. + /// assert_eq!(pos_1.get_range_to(pos_2), 5); + /// ``` + #[doc(alias = "distance")] + #[inline] + pub fn get_range_to(self, target: RoomXY) -> u32 { + let (dx, dy) = self - target; + dx.abs().max(dy.abs()) as u32 + } + + /// Checks whether this position is in the given range of another position. + /// + /// Linear range (also called Chebyshev Distance) is an alternate + /// calculation of distance, calculated as the greater of the distance along + /// the x axis or the y axis. Most calculations in Screeps use this distance + /// metric. For more information see [Chebeshev Distance](https://en.wikipedia.org/wiki/Chebyshev_distance). + /// + /// # Examples + /// ```rust + /// # use screeps::RoomXY; + /// let pos_1 = RoomXY::checked_new(5, 10).unwrap(); + /// let pos_2 = RoomXY::checked_new(8, 10).unwrap(); + /// + /// // The differences are 3 along the X axis and 0 along the Y axis + /// // so the linear distance is 3. + /// assert_eq!(pos_1.in_range_to(pos_2, 5), true); + /// + /// let pos_3 = RoomXY::checked_new(8, 15).unwrap(); + /// + /// // The differences are 3 along the X axis and 5 along the Y axis + /// // so the linear distance is 5. + /// // `in_range_to` returns true if the linear distance is equal to the range + /// assert_eq!(pos_1.in_range_to(pos_3, 5), true); + /// + /// let pos_4 = RoomXY::checked_new(20, 20).unwrap(); + /// // The differences are 15 along the X axis and 10 along the Y axis + /// // so the linear distance is 15. + /// assert_eq!(pos_1.in_range_to(pos_4, 5), false); + /// assert_eq!(pos_1.in_range_to(pos_4, 10), false); + /// assert_eq!(pos_1.in_range_to(pos_4, 15), true); + /// ``` + #[doc(alias = "distance")] + #[inline] + pub fn in_range_to(self, target: RoomXY, range: u32) -> bool { + self.get_range_to(target) <= range + } + + /// Checks whether this position is the same as the specified position. + /// + /// # Examples + /// ```rust + /// # use screeps::RoomXY; + /// let pos_1 = RoomXY::checked_new(5, 10).unwrap(); + /// let pos_2 = RoomXY::checked_new(5, 10).unwrap(); + /// let pos_3 = RoomXY::checked_new(4, 9).unwrap(); + /// + /// assert_eq!(pos_1.is_equal_to(pos_2), true); + /// assert_eq!(pos_1.is_equal_to(pos_3), false); + /// ``` + #[inline] + pub fn is_equal_to(self, target: RoomXY) -> bool { + self == target + } + + /// True if the range from this position to the target is at most 1. + /// + /// # Examples + /// ```rust + /// # use screeps::RoomXY; + /// let pos_1 = RoomXY::checked_new(5, 10).unwrap(); + /// let pos_2 = RoomXY::checked_new(6, 10).unwrap(); + /// let pos_3 = RoomXY::checked_new(4, 9).unwrap(); + /// let pos_4 = RoomXY::checked_new(20, 20).unwrap(); + /// + /// assert_eq!(pos_1.is_near_to(pos_2), true); + /// assert_eq!(pos_1.is_near_to(pos_3), true); + /// assert_eq!(pos_1.is_near_to(pos_4), false); + /// ``` + #[inline] + pub fn is_near_to(self, target: RoomXY) -> bool { + (u8::from(self.x) as i32 - u8::from(target.x) as i32).abs() <= 1 + && (u8::from(self.y) as i32 - u8::from(target.y) as i32).abs() <= 1 + } +} + +#[cfg(test)] +mod test { + use crate::{Direction, RoomXY}; + + #[test] + fn test_direction_to() { + let a = RoomXY::checked_new(1, 1).unwrap(); + let b = RoomXY::checked_new(2, 2).unwrap(); + assert_eq!(a.get_direction_to(b), Some(Direction::BottomRight)); + } +}