From 775d9a8929fc37ea4f74a1f23e18dbc4d7a7d658 Mon Sep 17 00:00:00 2001 From: Ken Hoover Date: Thu, 14 Nov 2024 23:04:23 -0800 Subject: [PATCH] Lots of breaking changes to RoomXY math. --- src/local/room_coordinate.rs | 32 ++- src/local/room_xy.rs | 313 +++++++++++++++++------ src/local/room_xy/approximate_offsets.rs | 15 +- src/local/room_xy/extra_math.rs | 33 +-- src/local/room_xy/game_math.rs | 8 +- 5 files changed, 281 insertions(+), 120 deletions(-) diff --git a/src/local/room_coordinate.rs b/src/local/room_coordinate.rs index 8bdc7118..f2c9cb0b 100644 --- a/src/local/room_coordinate.rs +++ b/src/local/room_coordinate.rs @@ -340,6 +340,18 @@ impl Sub for RoomCoordinate { } } +impl std::cmp::PartialOrd for RoomCoordinate { + fn partial_cmp(&self, other: &u8) -> Option { + Some(self.0.cmp(other)) + } +} + +impl std::cmp::PartialEq for RoomCoordinate { + fn eq(&self, other: &u8) -> bool { + self.0 == *other + } +} + const ROOM_SIZE_I8: i8 = { // If this fails, we need to rework the arithmetic code debug_assert!(2 * ROOM_SIZE <= i8::MAX as u8); @@ -439,7 +451,7 @@ impl RoomOffset { pub fn saturating_add(self, rhs: Self) -> Self { self.assume_bounds_constraint(); rhs.assume_bounds_constraint(); - Self::new((self.0 + rhs.0).clamp(-ROOM_SIZE_I8 + 1, ROOM_SIZE_I8 - 1)).unwrap_throw() + Self::saturating_new(self.0 + rhs.0) } /// Add two offsets together, wrapping around at the ends of the valid @@ -528,6 +540,12 @@ impl RoomOffset { } } +impl fmt::Display for RoomOffset { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + impl From for i8 { fn from(offset: RoomOffset) -> i8 { offset.0 @@ -566,6 +584,18 @@ impl Neg for RoomOffset { } } +impl std::cmp::PartialOrd for RoomOffset { + fn partial_cmp(&self, other: &i8) -> Option { + Some(self.0.cmp(other)) + } +} + +impl std::cmp::PartialEq for RoomOffset { + fn eq(&self, other: &i8) -> bool { + self.0 == *other + } +} + #[cfg(feature = "nightly")] impl std::iter::Step for RoomCoordinate { fn steps_between(start: &Self, end: &Self) -> Option { diff --git a/src/local/room_xy.rs b/src/local/room_xy.rs index bf523d20..e34d72d4 100644 --- a/src/local/room_xy.rs +++ b/src/local/room_xy.rs @@ -1,12 +1,16 @@ use std::{ cmp::Ordering, fmt, - ops::{Index, IndexMut}, + ops::{Index, IndexMut, Sub}, }; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +use wasm_bindgen::UnwrapThrowExt; -use super::room_coordinate::{OutOfBoundsError, RoomCoordinate}; +use super::{ + room_coordinate::{OutOfBoundsError, RoomCoordinate, RoomOffset}, + OffsetOutOfBoundsError, +}; use crate::constants::{Direction, ROOM_AREA, ROOM_USIZE}; mod approximate_offsets; @@ -65,27 +69,37 @@ pub fn terrain_index_to_xy(idx: usize) -> RoomXY { } } -/// An X/Y pair representing a given coordinate relative to any room. +/// A generic x-y pair of values. #[derive(Debug, Default, Hash, Clone, Copy, PartialEq, Eq)] -pub struct RoomXY { - pub x: RoomCoordinate, - pub y: RoomCoordinate, +pub struct XY { + pub x: T, + pub y: T, } -impl RoomXY { - /// Create a new `RoomXY` from a pair of `RoomCoordinate`. +/// An X/Y pair representing a given coordinate relative to any room. +pub type RoomXY = XY; + +/// An X/Y pair representing a given offset relative to any room. +pub type RoomOffsetXY = XY; + +impl XY { + /// Create a new `XY` from a pair of `T`s. #[inline] - pub fn new(x: RoomCoordinate, y: RoomCoordinate) -> Self { - RoomXY { x, y } + pub fn new(x: T, y: T) -> Self { + Self { x, y } } - /// Create a new `RoomXY` from a pair of `u8`, checking that they're in - /// the range of valid values. + /// Try to create a new `XY` from a pair of convertable values. #[inline] - pub fn checked_new(x: u8, y: u8) -> Result { - RoomXY::try_from((x, y)) + pub fn checked_new(x: U, y: U) -> Result>::Error> + where + Self: TryFrom<(U, U)>, + { + Self::try_from((x, y)) } +} +impl RoomXY { /// Create a `RoomXY` from a pair of `u8`, without checking whether it's in /// the range of valid values. /// @@ -129,6 +143,12 @@ impl RoomXY { Some(RoomXY { x, y }) } + pub fn checked_add_offset(self, rhs: RoomOffsetXY) -> Option { + let x = self.x.checked_add_offset(rhs.x)?; + let y = self.y.checked_add_offset(rhs.y)?; + Some(Self { x, y }) + } + /// Get the coordinate adjusted by a certain value, saturating at the edges /// of the room if the result would be outside the valid room area. /// @@ -154,48 +174,48 @@ impl RoomXY { RoomXY { x, y } } - /// Get the neighbor of a given `RoomXY` in the given direction, returning - /// `None` if the result is outside the valid room area. - /// - /// Example usage: - /// - /// ``` - /// use screeps::{constants::Direction::*, local::RoomXY}; - /// - /// let zero = unsafe { RoomXY::unchecked_new(0, 0) }; - /// let one = unsafe { RoomXY::unchecked_new(1, 1) }; - /// let forty_nine = unsafe { RoomXY::unchecked_new(49, 49) }; - /// - /// assert_eq!(zero.checked_add_direction(BottomRight), Some(one)); - /// assert_eq!(zero.checked_add_direction(TopLeft), None); - /// assert_eq!(one.checked_add_direction(TopLeft), Some(zero)); - /// assert_eq!(forty_nine.checked_add_direction(BottomRight), None); - /// ``` - pub fn checked_add_direction(self, rhs: Direction) -> Option { - let (dx, dy) = rhs.into(); - self.checked_add((dx as i8, dy as i8)) + pub fn saturating_add_offset(self, rhs: RoomOffsetXY) -> Self { + let x = self.x.saturating_add_offset(rhs.x); + let y = self.y.saturating_add_offset(rhs.y); + Self { x, y } } - /// Get the neighbor of a given `RoomXY` in the given direction, saturating - /// at the edges if the result is outside the valid room area. - /// - /// Example usage: - /// - /// ``` - /// use screeps::{constants::Direction::*, local::RoomXY}; - /// - /// let zero = unsafe { RoomXY::unchecked_new(0, 0) }; - /// let one = unsafe { RoomXY::unchecked_new(1, 1) }; - /// let forty_nine = unsafe { RoomXY::unchecked_new(49, 49) }; - /// - /// assert_eq!(zero.saturating_add_direction(BottomRight), one); - /// assert_eq!(zero.saturating_add_direction(TopLeft), zero); - /// assert_eq!(one.saturating_add_direction(TopLeft), zero); - /// assert_eq!(forty_nine.saturating_add_direction(BottomRight), forty_nine); - /// ``` - pub fn saturating_add_direction(self, rhs: Direction) -> RoomXY { - let (dx, dy) = rhs.into(); - self.saturating_add((dx as i8, dy as i8)) + pub fn overflowing_add(self, rhs: (i8, i8)) -> (Self, (bool, bool)) { + let (x, x_overflow) = self.x.overflowing_add(rhs.0); + let (y, y_overflow) = self.y.overflowing_add(rhs.1); + (Self { x, y }, (x_overflow, y_overflow)) + } + + pub fn overflowing_add_offset(self, rhs: RoomOffsetXY) -> (Self, XY) { + let (x, x_overflow) = self.x.overflowing_add_offset(rhs.x); + let (y, y_overflow) = self.y.overflowing_add_offset(rhs.y); + ( + Self { x, y }, + XY { + x: x_overflow, + y: y_overflow, + }, + ) + } + + pub fn wrapping_add(self, rhs: (i8, i8)) -> Self { + self.overflowing_add(rhs).0 + } + + pub fn wrapping_add_offset(self, rhs: RoomOffsetXY) -> Self { + self.overflowing_add_offset(rhs).0 + } + + pub unsafe fn unchecked_add(self, rhs: (i8, i8)) -> Self { + let x = self.x.unchecked_add(rhs.0); + let y = self.y.unchecked_add(rhs.1); + Self { x, y } + } + + pub unsafe fn unchecked_add_offset(self, rhs: RoomOffsetXY) -> Self { + let x = self.x.unchecked_add_offset(rhs.x); + let y = self.y.unchecked_add_offset(rhs.y); + Self { x, y } } /// Get all the valid neighbors of a given `RoomXY`. @@ -237,7 +257,7 @@ impl RoomXY { /// ``` pub fn neighbors(self) -> Vec { Direction::iter() - .filter_map(|dir| self.checked_add_direction(*dir)) + .filter_map(|&dir| self.checked_add_offset(dir.into())) .collect() } } @@ -261,50 +281,147 @@ impl fmt::Display for RoomXY { } } -impl From for (u8, u8) { - fn from(xy: RoomXY) -> (u8, u8) { - (xy.x.u8(), xy.y.u8()) +impl RoomOffsetXY { + #[inline] + pub unsafe fn unchecked_new(x: i8, y: i8) -> Self { + Self { + x: RoomOffset::unchecked_new(x), + y: RoomOffset::unchecked_new(y), + } + } + + pub fn manhattan_distance(self) -> u8 { + self.x.abs() + self.y.abs() + } + + pub fn chebyshev_distance(self) -> u8 { + self.x.abs().max(self.y.abs()) + } +} + +impl std::ops::Neg for RoomOffsetXY { + type Output = Self; + + fn neg(self) -> Self::Output { + Self { + x: -self.x, + y: -self.y, + } + } +} + +impl fmt::Display for RoomOffsetXY { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "({}, {})", self.x, self.y) + } +} + +impl From> for (U, U) +where + U: From, +{ + fn from(XY { x, y }: XY) -> Self { + (x.into(), y.into()) + } +} + +impl From<(U, U)> for XY +where + T: From, +{ + fn from((x, y): (U, U)) -> Self { + Self { + x: x.into(), + y: y.into(), + } + } +} + +impl Sub> for XY +where + A: Sub, +{ + type Output = XY; + + /// Implements subtraction between [`XY`] values when the contained types can be subtracted from each other as scalars. + /// + /// # Example + /// + /// ``` + /// # use screeps::{RoomXY, XY, RoomOffsetXY}; + /// + /// assert_eq!(XY {x: 5, y: 4} - XY {x: 1, y: 1}, XY {x: 4, y: 3}); + /// let pos1 = RoomXY::checked_new(40, 40).unwrap(); + /// let pos2 = RoomXY::checked_new(0, 20).unwrap(); + /// assert_eq!(pos1 - pos2, RoomOffsetXY::checked_new(40, 20).unwrap()); + /// + /// let pos3 = RoomXY::checked_new(45, 45).unwrap(); + /// assert_eq!(pos1 - pos3, RoomOffsetXY::checked_new(-5, -5).unwrap()); + /// ``` + fn sub(self, rhs: XY) -> XY { + XY { + x: self.x - rhs.x, + y: self.y - rhs.y, + } } } impl TryFrom<(u8, u8)> for RoomXY { type Error = OutOfBoundsError; - fn try_from(xy: (u8, u8)) -> Result { - Ok(RoomXY { - x: RoomCoordinate::try_from(xy.0)?, - y: RoomCoordinate::try_from(xy.1)?, + fn try_from((x, y): (u8, u8)) -> Result { + Ok(Self { + x: x.try_into()?, + y: y.try_into()?, }) } } -impl From<(RoomCoordinate, RoomCoordinate)> for RoomXY { - fn from(xy: (RoomCoordinate, RoomCoordinate)) -> RoomXY { - RoomXY { x: xy.0, y: xy.1 } +impl TryFrom<(i8, i8)> for RoomOffsetXY { + type Error = OffsetOutOfBoundsError; + + fn try_from((x, y): (i8, i8)) -> Result { + Ok(Self { + x: x.try_into()?, + y: y.try_into()?, + }) } } -impl From for (RoomCoordinate, RoomCoordinate) { - fn from(xy: RoomXY) -> (RoomCoordinate, RoomCoordinate) { - (xy.x, xy.y) +impl From for RoomOffsetXY { + fn from(value: Direction) -> Self { + use Direction::*; + let y = match value { + Top | TopLeft | TopRight => RoomOffset::new(-1), + Right | Left => RoomOffset::new(0), + Bottom | BottomLeft | BottomRight => RoomOffset::new(1), + } + .unwrap_throw(); + let x = match value { + Left | TopLeft | BottomLeft => RoomOffset::new(-1), + Top | Bottom => RoomOffset::new(0), + Right | TopRight | BottomRight => RoomOffset::new(1), + } + .unwrap_throw(); + Self { x, y } } } #[derive(Serialize, Deserialize)] -struct ReadableXY { - x: RoomCoordinate, - y: RoomCoordinate, +struct ReadableXY { + x: T, + y: T, } -impl From for RoomXY { - fn from(ReadableXY { x, y }: ReadableXY) -> RoomXY { - RoomXY { x, y } +impl From> for XY { + fn from(ReadableXY { x, y }: ReadableXY) -> XY { + Self { x, y } } } -impl From for ReadableXY { - fn from(RoomXY { x, y }: RoomXY) -> ReadableXY { - ReadableXY { x, y } +impl From> for ReadableXY { + fn from(XY { x, y }: XY) -> Self { + Self { x, y } } } @@ -314,7 +431,7 @@ impl Serialize for RoomXY { S: Serializer, { if serializer.is_human_readable() { - ReadableXY::from(*self).serialize(serializer) + ReadableXY::::from(*self).serialize(serializer) } else { let xy: (u8, u8) = (*self).into(); let packed: u16 = ((xy.0 as u16) << 8) | (xy.1 as u16); @@ -329,7 +446,7 @@ impl<'de> Deserialize<'de> for RoomXY { D: Deserializer<'de>, { if deserializer.is_human_readable() { - ReadableXY::deserialize(deserializer).map(Into::into) + ReadableXY::::deserialize(deserializer).map(Into::into) } else { let packed = u16::deserialize(deserializer)?; let xy = (((packed >> 8) & 0xFF) as u8, (packed & 0xFF) as u8); @@ -343,6 +460,44 @@ impl<'de> Deserialize<'de> for RoomXY { } } +impl Serialize for RoomOffsetXY { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + if serializer.is_human_readable() { + ReadableXY::::from(*self).serialize(serializer) + } else { + let xy: (i8, i8) = (*self).into(); + let packed: u16 = ((xy.0 as u8 as u16) << 8) | (xy.1 as u8 as u16); + packed.serialize(serializer) + } + } +} + +impl<'de> Deserialize<'de> for RoomOffsetXY { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + if deserializer.is_human_readable() { + ReadableXY::::deserialize(deserializer).map(Into::into) + } else { + let packed = u16::deserialize(deserializer)?; + let xy = ( + ((packed >> 8) & 0xFF) as u8 as i8, + (packed & 0xFF) as u8 as i8, + ); + RoomOffsetXY::try_from(xy).map_err(|err| { + de::Error::invalid_value( + de::Unexpected::Signed(err.0 as i64), + &format!("an integer with absolute value less-than {ROOM_USIZE}").as_str(), + ) + }) + } + } +} + /// A wrapper struct indicating that the inner array should be indexed X major, /// i.e. ``` /// use screeps::{ diff --git a/src/local/room_xy/approximate_offsets.rs b/src/local/room_xy/approximate_offsets.rs index b141b6e6..9fb98b59 100644 --- a/src/local/room_xy/approximate_offsets.rs +++ b/src/local/room_xy/approximate_offsets.rs @@ -1,7 +1,7 @@ //! Methods related to approximating in-room positions //! between other in-room positions. -use super::RoomXY; +use super::{RoomOffsetXY, RoomXY}; impl RoomXY { /// Calculates an approximate midpoint between this point and the target. @@ -50,14 +50,17 @@ impl RoomXY { /// ); /// ``` 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()); + let RoomOffsetXY { + x: offset_x, + y: offset_y, + } = target - self; + let total_distance = offset_x.abs().max(offset_y.abs()) as i8; 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; + let new_offset_x = (i8::from(offset_x) * distance_towards_target) / total_distance; + let new_offset_y = (i8::from(offset_y) * distance_towards_target) / total_distance; self + (new_offset_x, new_offset_y) } @@ -178,7 +181,7 @@ impl RoomXY { /// ); /// ``` pub fn midpoint_between(self, target: RoomXY) -> RoomXY { - let (offset_x, offset_y) = self - target; + let (offset_x, offset_y) = <(i8, i8)>::from(self - target); let new_offset_x = offset_x / 2; let new_offset_y = offset_y / 2; diff --git a/src/local/room_xy/extra_math.rs b/src/local/room_xy/extra_math.rs index d26c73a3..286dd4d6 100644 --- a/src/local/room_xy/extra_math.rs +++ b/src/local/room_xy/extra_math.rs @@ -3,7 +3,7 @@ use std::ops::{Add, Sub}; -use super::RoomXY; +use super::{RoomOffsetXY, RoomXY, XY}; use crate::constants::Direction; impl RoomXY { @@ -89,7 +89,7 @@ impl Add for RoomXY { #[inline] #[track_caller] fn add(self, direction: Direction) -> Self { - self.checked_add_direction(direction).unwrap() + self.checked_add_offset(direction.into()).unwrap() } } @@ -138,32 +138,7 @@ impl Sub for RoomXY { /// ``` #[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.u8() as i8 - other.x.u8() as i8; - let dy = self.y.u8() as i8 - other.y.u8() as i8; - (dx, dy) + self.checked_add_offset(-RoomOffsetXY::from(direction)) + .unwrap() } } diff --git a/src/local/room_xy/game_math.rs b/src/local/room_xy/game_math.rs index df19d2a4..ff692961 100644 --- a/src/local/room_xy/game_math.rs +++ b/src/local/room_xy/game_math.rs @@ -1,9 +1,8 @@ //! Utilities for doing math on [`RoomXY`]s which are present in the //! JavaScript API. - use crate::constants::Direction; -use super::RoomXY; +use super::{RoomOffsetXY, RoomXY}; impl RoomXY { /// Gets linear direction to the specified position. @@ -14,7 +13,7 @@ impl RoomXY { /// 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; + let RoomOffsetXY { x: dx, y: dy } = target - self; if dx.abs() > dy.abs() * 2 { if dx > 0 { Some(Direction::Right) @@ -59,8 +58,7 @@ impl RoomXY { #[doc(alias = "distance")] #[inline] pub fn get_range_to(self, target: RoomXY) -> u8 { - let (dx, dy) = self - target; - dx.unsigned_abs().max(dy.unsigned_abs()) + (self - target).chebyshev_distance() } /// Checks whether this position is in the given range of another position.