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…
	
	Add table
		Add a link
		
	
		Reference in a new issue