Files
scrabble-with-numbers/board-shared/src/position.rs
2023-03-23 17:18:59 +01:00

187 lines
4.9 KiB
Rust

/// A position in a 2d grid.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Position2d {
pub x: usize,
pub y: usize,
}
/// An alignment in a 2d grid.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Alignment {
Horizontal,
Vertical,
}
/// A direction in a 2d grid.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
Up,
Down,
Left,
Right,
}
impl Alignment {
/// Returns the direction to the start of an alignment.
pub fn start(self) -> Direction {
match self {
Alignment::Horizontal => Direction::Left,
Alignment::Vertical => Direction::Up,
}
}
/// Returns the direction to the end of an alignment.
pub fn end(self) -> Direction {
match self {
Alignment::Horizontal => Direction::Right,
Alignment::Vertical => Direction::Down,
}
}
/// Test if two positions are aligned in this direction.
pub fn is_aligned(self, a: Position2d, b: Position2d) -> bool {
match self {
Alignment::Horizontal => a.y == b.y,
Alignment::Vertical => a.x == b.x,
}
}
}
impl Position2d {
pub fn new(x: usize, y: usize) -> Self {
Self { x, y }
}
/// Returns the position relative to this position in the given direction.
/// If the position is out of bounds, returns None.
pub fn relative(self, dir: Direction, sized: &impl Grid2d) -> Option<Position2d> {
let (x, y) = match dir {
Direction::Up => (self.x, self.y.checked_sub(1)?),
Direction::Down => (self.x, self.y.checked_add(1)?),
Direction::Left => (self.x.checked_sub(1)?, self.y),
Direction::Right => (self.x.checked_add(1)?, self.y),
};
if x < sized.width() && y < sized.height() {
Some(Position2d::new(x, y))
} else {
None
}
}
/// Returns an iterator over all positions relative to this position in the given direction.
///
/// If the positions are not aligned in any direction, the iterator will be empty.
/// If the end position is before the start position, the iterator will be empty.
pub fn iterate_to(self, end: Position2d, offset: (usize, usize)) -> RelativeIterator {
if (self.x != end.x && self.y != end.y) || self.x > end.x || self.y > end.y {
return RelativeIterator {
pos: self,
end: self,
offset: (0, 0),
};
}
RelativeIterator {
pos: self,
end,
offset,
}
}
pub fn manhattan_distance(self, other: Position2d) -> usize {
self.x.abs_diff(other.x) + self.y.abs_diff(other.y)
}
pub fn is_contiguous(self, other: Position2d) -> bool {
self.manhattan_distance(other) == 1
}
}
pub struct RelativeIterator {
pos: Position2d,
end: Position2d,
offset: (usize, usize),
}
impl Iterator for RelativeIterator {
type Item = Position2d;
fn next(&mut self) -> Option<Self::Item> {
if self.pos == self.end {
None
} else {
let pos = self.pos;
self.pos = Position2d::new(self.pos.x + self.offset.0, self.pos.y + self.offset.1);
Some(pos)
}
}
}
impl From<(usize, usize)> for Position2d {
fn from((x, y): (usize, usize)) -> Self {
Self { x, y }
}
}
/// Trait for elements that have a size.
pub trait Grid2d {
/// Returns the grid width.
fn width(&self) -> usize;
/// Returns the grid height.
fn height(&self) -> usize;
}
#[cfg(test)]
mod tests {
use super::*;
struct Rectangle {
width: usize,
height: usize,
}
impl Rectangle {
fn new(width: usize, height: usize) -> Self {
Self { width, height }
}
}
impl Grid2d for Rectangle {
fn width(&self) -> usize {
self.width
}
fn height(&self) -> usize {
self.height
}
}
#[test]
fn test_relative() {
let rect = Rectangle::new(3, 3);
let pos = Position2d::new(0, 0);
assert_eq!(pos.relative(Direction::Up, &rect), None);
assert_eq!(
pos.relative(Direction::Down, &rect),
Some(Position2d::new(0, 1))
);
assert_eq!(pos.relative(Direction::Left, &rect), None);
assert_eq!(
pos.relative(Direction::Right, &rect),
Some(Position2d::new(1, 0))
);
let pos = Position2d::new(2, 2);
assert_eq!(
pos.relative(Direction::Up, &rect),
Some(Position2d::new(2, 1))
);
assert_eq!(pos.relative(Direction::Down, &rect), None);
assert_eq!(
pos.relative(Direction::Left, &rect),
Some(Position2d::new(1, 2))
);
assert_eq!(pos.relative(Direction::Right, &rect), None);
}
}