Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(Improvement) Add additional calculation methods to RoomXY #521

Merged
merged 7 commits into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Direction>`, `Sub<(i8, i8)>`, `Sub<Direction>`,
and `Sub<RoomXY>` traits for `RoomXY`

0.21.0 (2024-05-14)
===================
Expand Down
32 changes: 31 additions & 1 deletion src/local/room_xy.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 }
jciskey marked this conversation as resolved.
Show resolved Hide resolved
}

/// 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, OutOfBoundsError> {
RoomXY::try_from((x, y))
}

/// Create a `RoomXY` from a pair of `u8`, without checking whether it's in
/// the range of valid values.
///
Expand Down Expand Up @@ -229,6 +246,19 @@ impl RoomXY {
}
}

impl PartialOrd for RoomXY {
#[inline]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strongly disagree with these impls existing, considering even in-game objects disagree on being row or column major (which this implicitly enforces).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was copied over from Position, purely because it allows for usage as a key in things like BTreeMap and that seems like a reasonable thing to keep parity with. If we want to tear it out here, we should tear it out there as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, yes. Position is even worse, since on top of picking row/column major, you also have to decide if you're doing a lex order on the (room name, room xy) pair or treating the whole position as a single world xy.

Copy link
Contributor

@khoover khoover Jun 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For position, there's really 6 different ways you could arrange the components into a 4-tuple and use the lex ordering to come up with a sensible Position ordering:

  • (room_name.x, room_xy.x, room_name.y, room_xy.y)
  • (room_name.y, room_xy.y, room_name.x, room_xy.x) <- the one Position implements
  • (room_name.x, room_name.y, room_xy.x, room_xy.y)
  • (room_name.x, room_name.y, room_xy.y, room_xy.x)
  • (room_name.y, room_name.x, room_xy.x, room_xy.y)
  • (room_name.y, room_name.x, room_xy.y, room_xy.x)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can tear this out if we really want to, and you can open up a PR for tearing out the Position implementation, but I'm not comfortable doing the latter in this PR. It feels out of scope for the intended goal of bringing RoomXY into approximate API parity with Position.

fn partial_cmp(&self, other: &RoomXY) -> Option<Ordering> {
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)
Expand Down
236 changes: 236 additions & 0 deletions src/local/room_xy/approximate_offsets.rs
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit of a weird one; wouldn't you want a float in [0,1] for self*x + target*(1-x)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not entirely sure what you're asking here. Can you clarify a bit more?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like this is supposed to be some sort of interpolation method, where you get a point on the line segment between self and target.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes, this is a directional interpolation method. I'm not sure that returning the interpolation multiplier (or even vector) is all that helpful, though, since the user would still need to determine what RoomXY corresponds to that interpolation multiplier.

As with PartialOrd, this was copied from Position for API parity.

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));
}
}
Loading
Loading