493 lines
12 KiB
Rust
493 lines
12 KiB
Rust
#![allow(clippy::unit_arg)]
|
|
#![allow(clippy::identity_conversion)]
|
|
|
|
use std::cmp::max;
|
|
use std::cmp::Ordering;
|
|
use std::ops;
|
|
use std::rc::Rc;
|
|
pub mod collision;
|
|
pub mod command;
|
|
pub mod direction;
|
|
pub mod entity_map;
|
|
pub use collision::Collision;
|
|
pub use direction::Direction;
|
|
pub use direction::Direction::*;
|
|
use proptest_derive::Arbitrary;
|
|
use termion::cursor;
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Arbitrary)]
|
|
pub struct Dimensions {
|
|
#[proptest(strategy = "std::ops::Range::<u16>::from(0..100)")]
|
|
pub w: u16,
|
|
|
|
#[proptest(strategy = "std::ops::Range::<u16>::from(0..100)")]
|
|
pub h: u16,
|
|
}
|
|
|
|
pub const ZERO_DIMENSIONS: Dimensions = Dimensions { w: 0, h: 0 };
|
|
pub const UNIT_DIMENSIONS: Dimensions = Dimensions { w: 1, h: 1 };
|
|
|
|
impl Default for Dimensions {
|
|
fn default() -> Self {
|
|
Dimensions { w: 80, h: 20 }
|
|
}
|
|
}
|
|
|
|
impl ops::Sub<Dimensions> for Dimensions {
|
|
type Output = Dimensions;
|
|
fn sub(self, dims: Dimensions) -> Dimensions {
|
|
Dimensions {
|
|
w: self.w - dims.w,
|
|
h: self.h - dims.h,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Arbitrary)]
|
|
pub struct BoundingBox {
|
|
pub dimensions: Dimensions,
|
|
pub position: Position,
|
|
}
|
|
|
|
impl BoundingBox {
|
|
pub fn at_origin(dimensions: Dimensions) -> BoundingBox {
|
|
BoundingBox {
|
|
dimensions,
|
|
position: ORIGIN,
|
|
}
|
|
}
|
|
|
|
pub fn from_corners(
|
|
top_left: Position,
|
|
lower_right: Position,
|
|
) -> BoundingBox {
|
|
BoundingBox {
|
|
position: top_left,
|
|
dimensions: Dimensions {
|
|
w: (lower_right.x - top_left.x) as u16,
|
|
h: (lower_right.y - top_left.y) as u16,
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn lr_corner(self) -> Position {
|
|
self.position
|
|
+ (Position {
|
|
x: self.dimensions.w as i16,
|
|
y: self.dimensions.h as i16,
|
|
})
|
|
}
|
|
|
|
/// Returns a bounding box representing the *inside* of this box if it was
|
|
/// drawn on the screen.
|
|
pub fn inner(self) -> BoundingBox {
|
|
self + UNIT_POSITION - UNIT_DIMENSIONS - UNIT_DIMENSIONS
|
|
}
|
|
|
|
/// Moves the top right corner of the bounding box by the offset specified
|
|
/// by the given position, keeping the lower right corner in place
|
|
pub fn move_tr_corner(self, offset: Position) -> BoundingBox {
|
|
self + offset
|
|
- Dimensions {
|
|
w: offset.x as u16,
|
|
h: offset.y as u16,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ops::Add<Position> for BoundingBox {
|
|
type Output = BoundingBox;
|
|
fn add(self, pos: Position) -> BoundingBox {
|
|
BoundingBox {
|
|
position: self.position + pos,
|
|
..self
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ops::Sub<Dimensions> for BoundingBox {
|
|
type Output = BoundingBox;
|
|
fn sub(self, dims: Dimensions) -> BoundingBox {
|
|
BoundingBox {
|
|
dimensions: self.dimensions - dims,
|
|
..self
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Arbitrary, Hash, Ord)]
|
|
pub struct Position {
|
|
/// x (horizontal) position
|
|
#[proptest(strategy = "std::ops::Range::<i16>::from(0..100)")]
|
|
pub x: i16,
|
|
|
|
#[proptest(strategy = "std::ops::Range::<i16>::from(0..100)")]
|
|
/// y (vertical) position
|
|
pub y: i16,
|
|
}
|
|
|
|
pub fn pos(x: i16, y: i16) -> Position {
|
|
Position { x, y }
|
|
}
|
|
|
|
pub const ORIGIN: Position = Position { x: 0, y: 0 };
|
|
pub const UNIT_POSITION: Position = Position { x: 1, y: 1 };
|
|
|
|
impl Position {
|
|
/// Returns true if this position exists within the bounds of the given box,
|
|
/// inclusive
|
|
pub fn within(self, b: BoundingBox) -> bool {
|
|
(self > b.position - UNIT_POSITION) && self < (b.lr_corner())
|
|
}
|
|
|
|
/// Returns a sequence of ASCII escape characters for moving the cursor to
|
|
/// this Position
|
|
pub fn cursor_goto(self) -> cursor::Goto {
|
|
// + 1 because Goto is 1-based, but position is 0-based
|
|
cursor::Goto(self.x as u16 + 1, self.y as u16 + 1)
|
|
}
|
|
|
|
/// Converts this position to the number of `Tiles` away from the origin it
|
|
/// represents. Usually done after subtracting two positions. Gives distance
|
|
/// as the crow flies
|
|
pub fn as_tiles(self) -> Tiles {
|
|
Tiles(max(self.x.abs(), self.y.abs()).into())
|
|
}
|
|
}
|
|
|
|
impl PartialOrd for Position {
|
|
fn partial_cmp(&self, other: &Position) -> Option<Ordering> {
|
|
if self.x == other.x && self.y == other.y {
|
|
Some(Ordering::Equal)
|
|
} else if self.x > other.x && self.y > other.y {
|
|
Some(Ordering::Greater)
|
|
} else if self.x < other.x && self.y < other.y {
|
|
Some(Ordering::Less)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Implements (bounded) addition of a Dimension to a position.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// let pos = Position { x: 1, y: 10 }
|
|
///
|
|
/// let left_pos = pos + Direction::Left
|
|
/// assert_eq!(left, Position { x: 0, y: 10 })
|
|
///
|
|
/// let right_pos = pos + Direction::Right
|
|
/// assert_eq!(right_pos, Position { x: 0, y: 10 })
|
|
/// ```
|
|
#[allow(clippy::suspicious_arithmetic_impl)]
|
|
impl ops::Add<Direction> for Position {
|
|
type Output = Position;
|
|
fn add(self, dir: Direction) -> Position {
|
|
match dir {
|
|
Left => {
|
|
if self.x > std::i16::MIN {
|
|
Position {
|
|
x: self.x - 1,
|
|
..self
|
|
}
|
|
} else {
|
|
self
|
|
}
|
|
}
|
|
Right => {
|
|
if self.x < std::i16::MAX {
|
|
Position {
|
|
x: self.x + 1,
|
|
..self
|
|
}
|
|
} else {
|
|
self
|
|
}
|
|
}
|
|
Up => {
|
|
if self.y > std::i16::MIN {
|
|
Position {
|
|
y: self.y - 1,
|
|
..self
|
|
}
|
|
} else {
|
|
self
|
|
}
|
|
}
|
|
Down => {
|
|
if self.y < std::i16::MAX {
|
|
Position {
|
|
y: self.y + 1,
|
|
..self
|
|
}
|
|
} else {
|
|
self
|
|
}
|
|
}
|
|
UpLeft => self + Up + Left,
|
|
UpRight => self + Up + Right,
|
|
DownLeft => self + Down + Left,
|
|
DownRight => self + Down + Right,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ops::Add<Position> for Position {
|
|
type Output = Position;
|
|
fn add(self, pos: Position) -> Position {
|
|
Position {
|
|
x: self.x + pos.x,
|
|
y: self.y + pos.y,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ops::Sub<Position> for Position {
|
|
type Output = Position;
|
|
fn sub(self, pos: Position) -> Position {
|
|
Position {
|
|
x: self.x - pos.x,
|
|
y: self.y - pos.y,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Positioned for Position {
|
|
fn position(&self) -> Position {
|
|
*self
|
|
}
|
|
}
|
|
|
|
pub trait Positioned {
|
|
fn x(&self) -> i16 {
|
|
self.position().x
|
|
}
|
|
|
|
fn y(&self) -> i16 {
|
|
self.position().y
|
|
}
|
|
|
|
fn position(&self) -> Position {
|
|
Position {
|
|
x: self.x(),
|
|
y: self.y(),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub trait PositionedMut: Positioned {
|
|
fn set_position(&mut self, pos: Position);
|
|
}
|
|
|
|
// impl<A, I> Positioned for A where A : Deref<Target = I>, I: Positioned {
|
|
// fn position(&self) -> Position {
|
|
// self.position()
|
|
// }
|
|
// }
|
|
|
|
impl<T: Positioned> Positioned for Box<T> {
|
|
fn position(&self) -> Position {
|
|
(**self).position()
|
|
}
|
|
}
|
|
|
|
impl<'a, T: Positioned> Positioned for &'a T {
|
|
fn position(&self) -> Position {
|
|
(**self).position()
|
|
}
|
|
}
|
|
|
|
impl<'a, T: Positioned> Positioned for &'a mut T {
|
|
fn position(&self) -> Position {
|
|
(**self).position()
|
|
}
|
|
}
|
|
|
|
impl<'a, T: Positioned> Positioned for Rc<T> {
|
|
fn position(&self) -> Position {
|
|
(**self).position()
|
|
}
|
|
}
|
|
|
|
impl<'a, T: PositionedMut> PositionedMut for &'a mut T {
|
|
fn set_position(&mut self, pos: Position) {
|
|
(**self).set_position(pos)
|
|
}
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! positioned {
|
|
($name:ident) => {
|
|
positioned!($name, position);
|
|
};
|
|
($name:ident, $attr:ident) => {
|
|
impl $crate::types::Positioned for $name {
|
|
fn position(&self) -> $crate::types::Position {
|
|
self.$attr
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! positioned_mut {
|
|
($name:ident) => {
|
|
positioned_mut!($name, position);
|
|
};
|
|
($name:ident, $attr:ident) => {
|
|
impl crate::types::PositionedMut for $name {
|
|
fn set_position(&mut self, pos: $crate::types::Position) {
|
|
self.$attr = pos;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
/// A number of ticks
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Arbitrary)]
|
|
pub struct Ticks(pub u16);
|
|
|
|
/// A number of tiles
|
|
///
|
|
/// Expressed in terms of a float to allow moving partial tiles in a number of
|
|
/// ticks
|
|
#[derive(Clone, Copy, Debug, PartialEq, Arbitrary)]
|
|
pub struct Tiles(pub f32);
|
|
|
|
/// The speed of an entity, expressed in ticks per tile
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Arbitrary, Deserialize)]
|
|
#[serde(transparent)]
|
|
pub struct Speed(pub u32);
|
|
|
|
impl Speed {
|
|
/// Returns the number of tiles that would be moved in the given number of
|
|
/// ticks at this speed
|
|
pub fn ticks_to_tiles(self, ticks: Ticks) -> Tiles {
|
|
Tiles(f32::from(ticks.0) / self.0 as f32)
|
|
}
|
|
|
|
/// Returns the number of ticks required to move the given number of tiles
|
|
/// at this speed
|
|
pub fn tiles_to_ticks(self, tiles: Tiles) -> Ticks {
|
|
Ticks(tiles.0 as u16 * self.0 as u16)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Arbitrary)]
|
|
pub struct Neighbors<A> {
|
|
pub top_left: A,
|
|
pub top: A,
|
|
pub top_right: A,
|
|
pub left: A,
|
|
pub right: A,
|
|
pub bottom_left: A,
|
|
pub bottom: A,
|
|
pub bottom_right: A,
|
|
}
|
|
|
|
impl Neighbors<Position> {
|
|
fn of_position(pos: Position) -> Self {
|
|
Neighbors {
|
|
top_left: pos + Direction::UpLeft,
|
|
top: pos + Direction::Up,
|
|
top_right: pos + Direction::UpRight,
|
|
left: pos + Direction::Left,
|
|
right: pos + Direction::Right,
|
|
bottom_left: pos + Direction::DownLeft,
|
|
bottom: pos + Direction::Down,
|
|
bottom_right: pos + Direction::DownRight,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<A> Neighbors<A> {
|
|
/// it's a functor, yo
|
|
pub fn map<B, F: Fn(&A) -> B>(&self, f: F) -> Neighbors<B> {
|
|
Neighbors {
|
|
top_left: f(&self.top_left),
|
|
top: f(&self.top),
|
|
top_right: f(&self.top_right),
|
|
left: f(&self.left),
|
|
right: f(&self.right),
|
|
bottom_left: f(&self.bottom_left),
|
|
bottom: f(&self.bottom),
|
|
bottom_right: f(&self.bottom_right),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<A> Neighbors<Vec<A>> {
|
|
pub fn mapmap<B, F: Fn(&A) -> B>(&self, f: &F) -> Neighbors<Vec<B>> {
|
|
self.map(|xs| xs.iter().map(f).collect())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
#![allow(clippy::unnecessary_operation)]
|
|
use super::*;
|
|
use proptest::prelude::*;
|
|
|
|
proptest! {
|
|
#[test]
|
|
fn position_partialord_lt_transitive(
|
|
a: Position,
|
|
b: Position,
|
|
c: Position
|
|
) {
|
|
if a < b && b < c {
|
|
assert!(a < c)
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn position_partialord_eq_transitive(
|
|
a: Position,
|
|
b: Position,
|
|
c: Position
|
|
) {
|
|
if a == b && b == c {
|
|
assert!(a == c)
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn position_partialord_gt_transitive(
|
|
a: Position,
|
|
b: Position,
|
|
c: Position,
|
|
) {
|
|
if a > b && b > c {
|
|
assert!(a > c)
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn position_partialord_antisymmetric(a: Position, b: Position) {
|
|
if a < b {
|
|
assert!(!(a > b))
|
|
} else if a > b {
|
|
assert!(!(a < b))
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_position_plus_dimension_as_tiles_monoid_action(
|
|
pos: Position,
|
|
dir: Direction,
|
|
) {
|
|
prop_assume!(pos.y > 0 && pos.x > 0);
|
|
assert_eq!(((pos + dir) - pos).as_tiles(), Tiles(1.0));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_position_as_tiles() {
|
|
assert_eq!(pos(0, 0).as_tiles(), Tiles(0.0));
|
|
assert_eq!(pos(1, 1).as_tiles(), Tiles(1.0));
|
|
assert_eq!(pos(1, 2).as_tiles(), Tiles(2.0));
|
|
}
|
|
}
|