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