an @-sign in a box

This commit is contained in:
Griffin Smith 2019-07-05 22:45:57 -04:00
commit de081d7b1d
19 changed files with 2024 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/target
**/*.rs.bk
debug.log

1145
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

21
Cargo.toml Normal file
View file

@ -0,0 +1,21 @@
[package]
name = "xanthous"
version = "0.1.0"
authors = ["Griffin Smith <root@gws.fyi>"]
edition = "2018"
[dependencies]
config = "*"
itertools = "*"
lazy_static = "*"
log = "*"
log4rs = "*"
proptest = "0.9.3"
proptest-derive = "*"
serde = "^1.0.8"
serde_derive = "^1.0.8"
termion = "*"
clap = {version = "^2.33.0", features = ["yaml"]}
prettytable-rs = "^0.8"
[dev-dependencies]

2
Config.toml Normal file
View file

@ -0,0 +1,2 @@
[logging]
level = "debug"

View file

@ -0,0 +1,12 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 7aff36a9f7b263e62434a3f61ada1d6aaf6ff4545a463548d96815a0e98cf5f1 # shrinks to dims = Dimensions { w: 0, h: 0 }, style = Thin
cc e4d96a13d6a8c7625e49d3545f6076d58152f3b5eb43fae65f0d407d1d34f96c # shrinks to dims = Dimensions { w: 1, h: 1 }, style = Thin
cc b5f0d7cb409896bd6692544c7c1f781174075c287d3b7a3b9dc73526ea489484 # shrinks to dims = Dimensions { w: 1, h: 1 }, style = Thin
cc 103b62b7c29c22adcbc23153638d3b37bad57aeec685d1eab38c49d0deed937f # shrinks to dims = Dimensions { w: 0, h: 1 }, style = Thin
cc 24c3858a543b0d8ff4966517040ec8c183ed311688d6863fd13facb5cdad7aa0 # shrinks to dims = Dimensions { w: 1, h: 1 }, style = Thin
cc 70a53a8b771937976a08a72d870b355a0995cc0251f45de4393c37a56a789b83 # shrinks to dims = Dimensions { w: 0, h: 0 }, style = Thin

View file

@ -0,0 +1,7 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc a51cf37623f0e4024f4ba1450195be296d9b9e8ae954dbbf997ce5b57cd26792 # shrinks to a = Position { x: 44, y: 25 }, b = Position { x: 0, y: 25 }, c = Position { x: 0, y: 0 }

1
rustfmt.toml Normal file
View file

@ -0,0 +1 @@
max_width = 80

14
src/cli.yml Normal file
View file

@ -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

205
src/display/draw_box.rs Normal file
View file

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

9
src/display/mod.rs Normal file
View file

@ -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))
}

9
src/display/utils.rs Normal file
View file

@ -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()
}

15
src/entities/character.rs Normal file
View file

@ -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);

1
src/entities/mod.rs Normal file
View file

@ -0,0 +1 @@
pub mod character;

118
src/game.rs Normal file
View file

@ -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)
}
}

73
src/main.rs Normal file
View file

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

61
src/settings.rs Normal file
View file

@ -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()
}
}

23
src/types/command.rs Normal file
View file

@ -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,
}
}
}

9
src/types/direction.rs Normal file
View file

@ -0,0 +1,9 @@
use proptest_derive::Arbitrary;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Arbitrary)]
pub enum Direction {
Left,
Up,
Down,
Right,
}

296
src/types/mod.rs Normal file
View file

@ -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))
}
}
}
}