an @-sign in a box
This commit is contained in:
commit
de081d7b1d
19 changed files with 2024 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
/target
|
||||
**/*.rs.bk
|
||||
debug.log
|
1145
Cargo.lock
generated
Normal file
1145
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
21
Cargo.toml
Normal file
21
Cargo.toml
Normal 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
2
Config.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
[logging]
|
||||
level = "debug"
|
12
proptest-regressions/display/draw_box.txt
Normal file
12
proptest-regressions/display/draw_box.txt
Normal 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
|
7
proptest-regressions/types/mod.txt
Normal file
7
proptest-regressions/types/mod.txt
Normal 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
1
rustfmt.toml
Normal file
|
@ -0,0 +1 @@
|
|||
max_width = 80
|
14
src/cli.yml
Normal file
14
src/cli.yml
Normal 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
205
src/display/draw_box.rs
Normal 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
9
src/display/mod.rs
Normal 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
9
src/display/utils.rs
Normal 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
15
src/entities/character.rs
Normal 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
1
src/entities/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod character;
|
118
src/game.rs
Normal file
118
src/game.rs
Normal 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
73
src/main.rs
Normal 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
61
src/settings.rs
Normal 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
23
src/types/command.rs
Normal 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
9
src/types/direction.rs
Normal 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
296
src/types/mod.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue