diff options
author | Griffin Smith <root@gws.fyi> | 2019-07-06T02·45-0400 |
---|---|---|
committer | Griffin Smith <root@gws.fyi> | 2019-07-06T02·45-0400 |
commit | de081d7b1d0b791b2e61f9cde7369ea11647e0ae (patch) | |
tree | 687236ac6ca2d094053fa43112d967554531de86 /src |
an @-sign in a box
Diffstat (limited to 'src')
-rw-r--r-- | src/cli.yml | 14 | ||||
-rw-r--r-- | src/display/draw_box.rs | 205 | ||||
-rw-r--r-- | src/display/mod.rs | 9 | ||||
-rw-r--r-- | src/display/utils.rs | 9 | ||||
-rw-r--r-- | src/entities/character.rs | 15 | ||||
-rw-r--r-- | src/entities/mod.rs | 1 | ||||
-rw-r--r-- | src/game.rs | 118 | ||||
-rw-r--r-- | src/main.rs | 73 | ||||
-rw-r--r-- | src/settings.rs | 61 | ||||
-rw-r--r-- | src/types/command.rs | 23 | ||||
-rw-r--r-- | src/types/direction.rs | 9 | ||||
-rw-r--r-- | src/types/mod.rs | 296 |
12 files changed, 833 insertions, 0 deletions
diff --git a/src/cli.yml b/src/cli.yml new file mode 100644 index 000000000000..7c374e102048 --- /dev/null +++ b/src/cli.yml @@ -0,0 +1,14 @@ +name: xanthous +version: "0.0" +author: Griffin Smith <root@gws.fyi> +about: hey, it's a terminal game +args: + - config: + short: c + long: config + value_name: FILE + help: Sets a custom config file + takes_value: true +subcommands: + - debug: + about: Writes debug information to the terminal and exits diff --git a/src/display/draw_box.rs b/src/display/draw_box.rs new file mode 100644 index 000000000000..986f09a49f7c --- /dev/null +++ b/src/display/draw_box.rs @@ -0,0 +1,205 @@ +use crate::display::utils::clone_times; +use crate::display::utils::times; +use crate::types::Dimensions; +use itertools::Itertools; +use proptest::prelude::Arbitrary; +use proptest::strategy; +use proptest_derive::Arbitrary; + +// Box Drawing +// 0 1 2 3 4 5 6 7 8 9 A B C D E F +// U+250x ─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏ +// U+251x ┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟ +// U+252x ┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯ +// U+253x ┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿ +// U+254x ╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏ +// U+255x ═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟ +// U+256x ╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯ +// U+257x ╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿ + +static BOX: char = '☐'; + +static BOX_CHARS: [[char; 16]; 8] = [ + [ + // 0 1 2 3 4 5 6 7 8 9 + '─', '━', '│', '┃', '┄', '┅', '┆', '┇', '┈', '┉', + // 10 + '┊', '┋', '┌', '┍', '┎', '┏', + ], + [ + // 0 1 2 3 4 5 6 7 8 9 + '┐', '┑', '┒', '┓', '└', '┕', '┖', '┗', '┘', '┙', + '┚', '┛', '├', '┝', '┞', '┟', + ], + [ + // 0 1 2 3 4 5 6 7 8 9 + '┠', '┡', '┢', '┣', '┤', '┥', '┦', '┧', '┨', '┩', + '┪', '┫', '┬', '┭', '┮', '┯', + ], + [ + // 0 1 2 3 4 5 6 7 8 9 + '┰', '┱', '┲', '┳', '┴', '┵', '┶', '┷', '┸', '┹', + '┺', '┻', '┼', '┽', '┾', '┿', + ], + [ + // 0 1 2 3 4 5 6 7 8 9 + '╀', '╁', '╂', '╃', '╄', '╅', '╆', '╇', '╈', '╉', + '╊', '╋', '╌', '╍', '╎', '╏', + ], + [ + // 0 1 2 3 4 5 6 7 8 9 + '═', '║', '╒', '╓', '╔', '╕', '╖', '╗', '╘', '╙', + '╚', '╛', '╜', '╝', '╞', '╟', + ], + [ + // 0 1 2 3 4 5 6 7 8 9 + '╠', '╡', '╢', '╣', '╤', '╥', '╦', '╧', '╨', '╩', + '╪', '╫', '╬', '╭', '╮', '╯', + ], + [ + // 0 1 2 3 4 5 6 7 8 9 + '╰', '╱', '╲', '╳', '╴', '╵', '╶', '╷', '╸', '╹', + '╺', '╻', '╼', '╽', '╾', '╿', + ], +]; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BoxStyle { + Thin, + Thick, + Dotted, + ThickDotted, + Dashed, + ThickDashed, + Double, +} + +impl Arbitrary for BoxStyle { + type Parameters = (); + type Strategy = strategy::Just<Self>; + fn arbitrary_with(_: Self::Parameters) -> Self::Strategy { + // TODO + strategy::Just(BoxStyle::Thin) + } +} + +trait Stylable { + fn style(self, style: BoxStyle) -> char; +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Arbitrary)] +enum Corner { + TopRight, + TopLeft, + BottomRight, + BottomLeft, +} + +impl Stylable for Corner { + fn style(self, style: BoxStyle) -> char { + use BoxStyle::*; + use Corner::*; + + match (self, style) { + (TopRight, Thin) => BOX_CHARS[1][0], + (TopLeft, Thin) => BOX_CHARS[0][12], + (BottomRight, Thin) => BOX_CHARS[1][8], + (BottomLeft, Thin) => BOX_CHARS[1][4], + _ => unimplemented!(), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Arbitrary)] +enum Line { + H, + V, +} + +impl Stylable for Line { + fn style(self, style: BoxStyle) -> char { + use BoxStyle::*; + use Line::*; + match (self, style) { + (H, Thin) => BOX_CHARS[0][0], + (V, Thin) => BOX_CHARS[0][2], + _ => unimplemented!(), + } + } +} + +#[must_use] +pub fn make_box(style: BoxStyle, dims: Dimensions) -> String { + if dims.h == 0 || dims.w == 0 { + "".to_string() + } else if dims.h == 1 && dims.w == 1 { + BOX.to_string() + } else if dims.h == 1 { + times(Line::H.style(style), dims.w) + } else if dims.w == 1 { + (0..dims.h).map(|_| Line::V.style(style)).join("\n\r") + } else { + let h_line: String = times(Line::H.style(style), dims.w - 2); + let v_line = Line::V.style(style); + let v_walls: String = clone_times( + format!( + "{}{}{}\n\r", + v_line, + times::<_, String>(' ', dims.w - 2), + v_line + ), + dims.h - 2, + ); + + format!( + "{}{}{}\n\r{}{}{}{}", + Corner::TopLeft.style(style), + h_line, + Corner::TopRight.style(style), + v_walls, + Corner::BottomLeft.style(style), + h_line, + Corner::BottomRight.style(style), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use proptest::prelude::*; + + #[test] + fn make_thin_box() { + let res = make_box(BoxStyle::Thin, Dimensions { w: 10, h: 10 }); + assert_eq!( + res, + "┌────────┐ +\r│ │ +\r│ │ +\r│ │ +\r│ │ +\r│ │ +\r│ │ +\r│ │ +\r│ │ +\r└────────┘" + ); + } + + proptest! { + #[test] + fn box_has_height_lines(dims: Dimensions, style: BoxStyle) { + let res = make_box(style, dims); + prop_assume!((dims.w > 0 && dims.h > 0)); + assert_eq!(res.split("\n\r").count(), dims.h as usize); + } + + #[test] + fn box_lines_have_width_length(dims: Dimensions, style: BoxStyle) { + let res = make_box(style, dims); + prop_assume!(dims.w == 0 && dims.h == 0 || (dims.w > 0 && dims.h > 0)); + assert!(res.split("\n\r").all(|l| l.chars().count() == dims.w as usize)); + } + } +} diff --git a/src/display/mod.rs b/src/display/mod.rs new file mode 100644 index 000000000000..5dba48b44d9a --- /dev/null +++ b/src/display/mod.rs @@ -0,0 +1,9 @@ +pub mod draw_box; +pub mod utils; +pub use draw_box::{make_box, BoxStyle}; +use std::io::{self, Write}; +use termion::{clear, cursor, style}; + +pub fn clear<T: Write>(out: &mut T) -> io::Result<()> { + write!(out, "{}{}{}", clear::All, style::Reset, cursor::Goto(1, 1)) +} diff --git a/src/display/utils.rs b/src/display/utils.rs new file mode 100644 index 000000000000..acd4416cb884 --- /dev/null +++ b/src/display/utils.rs @@ -0,0 +1,9 @@ +use std::iter::FromIterator; + +pub fn times<A: Copy, B: FromIterator<A>>(elem: A, n: u16) -> B { + (0..n).map(|_| elem).collect() +} + +pub fn clone_times<A: Clone, B: FromIterator<A>>(elem: A, n: u16) -> B { + (0..n).map(|_| elem.clone()).collect() +} diff --git a/src/entities/character.rs b/src/entities/character.rs new file mode 100644 index 000000000000..e40b7b988e8d --- /dev/null +++ b/src/entities/character.rs @@ -0,0 +1,15 @@ +use crate::types::{Position, Speed}; + +const DEFAULT_SPEED: Speed = Speed(100); + +pub struct Character { + position: Position, +} + +impl Character { + pub fn speed(&self) -> Speed { + Speed(100) + } +} + +positioned!(Character); diff --git a/src/entities/mod.rs b/src/entities/mod.rs new file mode 100644 index 000000000000..78891226662a --- /dev/null +++ b/src/entities/mod.rs @@ -0,0 +1 @@ +pub mod character; diff --git a/src/game.rs b/src/game.rs new file mode 100644 index 000000000000..a41d7f73fd75 --- /dev/null +++ b/src/game.rs @@ -0,0 +1,118 @@ +use std::thread; +use crate::settings::Settings; +use crate::types::{BoundingBox, Dimensions, Position}; +use std::io::{self, StdinLock, StdoutLock, Write}; +use termion::cursor; +use termion::input::Keys; +use termion::input::TermRead; +use termion::raw::RawTerminal; + +use crate::display; +use crate::types::command::Command; + +/// The full state of a running Game +pub struct Game<'a> { + settings: Settings, + + /// The box describing the viewport. Generally the size of the terminal, and + /// positioned at 0, 0 + viewport: BoundingBox, + + /// An iterator on keypresses from the user + keys: Keys<StdinLock<'a>>, + + stdout: RawTerminal<StdoutLock<'a>>, + + /// The position of the character + character: Position, +} + +impl<'a> Game<'a> { + pub fn new( + settings: Settings, + stdout: RawTerminal<StdoutLock<'a>>, + stdin: StdinLock<'a>, + w: u16, + h: u16, + ) -> Game<'a> { + Game { + settings: settings, + viewport: BoundingBox::at_origin(Dimensions { w, h }), + keys: stdin.keys(), + stdout: stdout, + character: Position { x: 1, y: 1 }, + } + } + + /// Returns true if there's a collision in the game at the given Position + fn collision_at(&self, pos: Position) -> bool { + !pos.within(self.viewport.inner()) + } + + /// Run the game + pub fn run(mut self) { + info!("Running game"); + write!( + self, + "{}{}@{}", + display::make_box( + display::BoxStyle::Thin, + self.viewport.dimensions + ), + cursor::Goto(2, 2), + cursor::Left(1), + ) + .unwrap(); + self.flush().unwrap(); + loop { + let mut character_moved = false; + match Command::from_key(self.keys.next().unwrap().unwrap()) { + Some(Command::Quit) => { + info!("Quitting game due to user request"); + break; + } + + Some(Command::Move(direction)) => { + let new_pos = self.character + direction; + if !self.collision_at(new_pos) { + self.character = new_pos; + character_moved = true; + } + } + _ => (), + } + + if character_moved { + debug!("char: {:?}", self.character); + write!( + self, + " {}@{}", + cursor::Goto(self.character.x + 1, self.character.y + 1,), + cursor::Left(1) + ) + .unwrap(); + } + self.flush().unwrap(); + } + } +} + +impl<'a> Drop for Game<'a> { + fn drop(&mut self) { + display::clear(self).unwrap(); + } +} + +impl<'a> Write for Game<'a> { + fn write(&mut self, buf: &[u8]) -> io::Result<usize> { + self.stdout.write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.stdout.flush() + } + + fn write_all(&mut self, buf: &[u8]) -> io::Result<()> { + self.stdout.write_all(buf) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 000000000000..24d1bbba29ca --- /dev/null +++ b/src/main.rs @@ -0,0 +1,73 @@ +extern crate termion; +#[macro_use] +extern crate log; +extern crate config; +extern crate log4rs; +#[macro_use] +extern crate serde_derive; +#[macro_use] +extern crate clap; +#[macro_use] +extern crate prettytable; + +mod display; +mod game; +#[macro_use] +mod types; +mod entities; +mod settings; + +use clap::App; +use game::Game; +use prettytable::format::consts::FORMAT_BOX_CHARS; +use settings::Settings; + +use std::io::{self, StdinLock, StdoutLock}; + +use termion::raw::IntoRawMode; +use termion::raw::RawTerminal; + +fn init( + settings: Settings, + stdout: RawTerminal<StdoutLock<'_>>, + stdin: StdinLock<'_>, + w: u16, + h: u16, +) { + let game = Game::new(settings, stdout, stdin, w, h); + game.run() +} + +fn main() { + let yaml = load_yaml!("cli.yml"); + let matches = App::from_yaml(yaml).get_matches(); + let settings = Settings::load().unwrap(); + settings.logging.init_log(); + let stdout = io::stdout(); + let stdout = stdout.lock(); + + let stdin = io::stdin(); + let stdin = stdin.lock(); + + let termsize = termion::terminal_size().ok(); + // let termwidth = termsize.map(|(w, _)| w - 2).unwrap_or(70); + // let termheight = termsize.map(|(_, h)| h - 2).unwrap_or(40); + let (termwidth, termheight) = termsize.unwrap_or((70, 40)); + + match matches.subcommand() { + ("debug", _) => { + let mut table = table!( + [br->"termwidth", termwidth], + [br->"termheight", termheight], + [br->"logfile", settings.logging.file], + [br->"loglevel", settings.logging.level] + ); + table.set_format(*FORMAT_BOX_CHARS); + table.printstd(); + } + _ => { + let stdout = stdout.into_raw_mode().unwrap(); + init(settings, stdout, stdin, termwidth, termheight); + } + } +} diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 000000000000..06f0d4e9d7de --- /dev/null +++ b/src/settings.rs @@ -0,0 +1,61 @@ +use config::{Config, ConfigError}; +use log::LevelFilter; +use log4rs::append::file::FileAppender; +use log4rs::config::{Appender, Root}; +use log4rs::encode::pattern::PatternEncoder; + +#[derive(Debug, Deserialize)] +pub struct Logging { + #[serde(default = "Logging::default_level")] + pub level: LevelFilter, + + #[serde(default = "Logging::default_file")] + pub file: String, +} + +impl Default for Logging { + fn default() -> Self { + Logging { + level: LevelFilter::Off, + file: "debug.log".to_string(), + } + } +} + +impl Logging { + pub fn init_log(&self) { + let logfile = FileAppender::builder() + .encoder(Box::new(PatternEncoder::new("{d} {l} - {m}\n"))) + .build(self.file.clone()) + .unwrap(); + + let config = log4rs::config::Config::builder() + .appender(Appender::builder().build("logfile", Box::new(logfile))) + .build(Root::builder().appender("logfile").build(self.level)) + .unwrap(); + + log4rs::init_config(config).unwrap(); + } + + fn default_level() -> LevelFilter { + Logging::default().level + } + + fn default_file() -> String { + Logging::default().file + } +} + +#[derive(Debug, Deserialize)] +pub struct Settings { + pub logging: Logging, +} + +impl Settings { + pub fn load() -> Result<Self, ConfigError> { + let mut s = Config::new(); + s.merge(config::File::with_name("Config").required(false))?; + s.merge(config::Environment::with_prefix("XAN"))?; + s.try_into() + } +} diff --git a/src/types/command.rs b/src/types/command.rs new file mode 100644 index 000000000000..86f83a12c181 --- /dev/null +++ b/src/types/command.rs @@ -0,0 +1,23 @@ +use super::Direction; +use super::Direction::*; +use termion::event::Key; +use termion::event::Key::Char; + +pub enum Command { + Quit, + Move(Direction), +} + +impl Command { + pub fn from_key(k: Key) -> Option<Command> { + use Command::*; + match k { + Char('q') => Some(Quit), + Char('h') | Char('a') | Key::Left => Some(Move(Left)), + Char('k') | Char('w') | Key::Up => Some(Move(Up)), + Char('j') | Char('s') | Key::Down => Some(Move(Down)), + Char('l') | Char('d') | Key::Right => Some(Move(Right)), + _ => None, + } + } +} diff --git a/src/types/direction.rs b/src/types/direction.rs new file mode 100644 index 000000000000..5ab660f19317 --- /dev/null +++ b/src/types/direction.rs @@ -0,0 +1,9 @@ +use proptest_derive::Arbitrary; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Arbitrary)] +pub enum Direction { + Left, + Up, + Down, + Right, +} diff --git a/src/types/mod.rs b/src/types/mod.rs new file mode 100644 index 000000000000..331aa236e324 --- /dev/null +++ b/src/types/mod.rs @@ -0,0 +1,296 @@ +use std::cmp::Ordering; +use std::ops; +pub mod command; +pub mod direction; +pub use direction::Direction; +pub use direction::Direction::{Down, Left, Right, Up}; +use proptest_derive::Arbitrary; + +#[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 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 lr_corner(self) -> Position { + self.position + + (Position { + x: self.dimensions.w, + y: self.dimensions.h, + }) + } + + /// 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 + } +} + +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)] +pub struct Position { + /// x (horizontal) position + #[proptest(strategy = "std::ops::Range::<u16>::from(0..100)")] + pub x: u16, + + #[proptest(strategy = "std::ops::Range::<u16>::from(0..100)")] + /// y (vertical) position + pub y: u16, +} + +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()) + } +} + +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 }) +/// ``` +impl ops::Add<Direction> for Position { + type Output = Position; + fn add(self, dir: Direction) -> Position { + match dir { + Left => { + if self.x > 0 { + Position { + x: self.x - 1, + ..self + } + } else { + self + } + } + Right => { + if self.x < std::u16::MAX { + Position { + x: self.x + 1, + ..self + } + } else { + self + } + } + Up => { + if self.y > 0 { + Position { + y: self.y - 1, + ..self + } + } else { + self + } + } + Down => { + if self.y < std::u16::MAX { + Position { + y: self.y + 1, + ..self + } + } else { + self + } + } + } + } +} + +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, + } + } +} + +pub trait Positioned { + fn x(&self) -> u16 { + self.position().x + } + + fn y(&self) -> u16 { + self.position().y + } + + fn position(&self) -> Position { + Position { + x: self.x(), + y: self.y(), + } + } +} + +macro_rules! positioned { + ($name:ident) => { + positioned!($name, position); + }; + ($name:ident, $attr:ident) => { + impl crate::types::Positioned for $name { + fn position(&self) -> Position { + self.$attr + } + } + }; +} + +/// 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)] +pub struct Speed(pub u32); + +impl Speed { + pub fn ticks_to_tiles(self, ticks: Ticks) -> Tiles { + Tiles(ticks.0 as f32 / self.0 as f32) + } +} + +#[cfg(test)] +mod tests { + 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)) + } + } + } +} |