diff --git a/proptest-regressions/display/viewport.txt b/proptest-regressions/display/viewport.txt new file mode 100644 index 000000000..e38056d97 --- /dev/null +++ b/proptest-regressions/display/viewport.txt @@ -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 b84a5a6dbba5cfc69329a119d9e20328c0372e0db2b72e5d71d971e3f13f8749 # shrinks to pos = Position { x: 0, y: 0 }, outer = BoundingBox { dimensions: Dimensions { w: 0, h: 0 }, position: Position { x: 0, y: 0 } } diff --git a/src/display/mod.rs b/src/display/mod.rs index 5dba48b44..664aaf319 100644 --- a/src/display/mod.rs +++ b/src/display/mod.rs @@ -1,9 +1,18 @@ pub mod draw_box; pub mod utils; +pub mod viewport; +use crate::types::Positioned; pub use draw_box::{make_box, BoxStyle}; use std::io::{self, Write}; use termion::{clear, cursor, style}; +pub use viewport::Viewport; pub fn clear(out: &mut T) -> io::Result<()> { write!(out, "{}{}{}", clear::All, style::Reset, cursor::Goto(1, 1)) } + +pub trait Draw: Positioned { + /// Draw this entity, assuming the character is already at the correct + /// position + fn do_draw(&self, out: &mut W) -> io::Result<()>; +} diff --git a/src/display/viewport.rs b/src/display/viewport.rs new file mode 100644 index 000000000..bd2fac071 --- /dev/null +++ b/src/display/viewport.rs @@ -0,0 +1,138 @@ +use super::Draw; +use super::{make_box, BoxStyle}; +use crate::types::{BoundingBox, Position, Positioned}; +use std::fmt::{self, Debug}; +use std::io::{self, Write}; + +pub struct Viewport { + /// The box describing the visible part of the viewport. + /// + /// Generally the size of the terminal, and positioned at 0, 0 + pub outer: BoundingBox, + + /// The box describing the inner part of the viewport + /// + /// Its position is relative to `outer.inner()`, and its size should generally not + /// be smaller than outer + pub inner: BoundingBox, + + /// The actual screen that the viewport writes to + pub out: W, +} + +impl Debug for Viewport { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Viewport {{ outer: {:?}, inner: {:?}, out: }}", + self.outer, self.inner + ) + } +} + +impl Viewport { + /// Returns true if the (inner-relative) position of the given entity is + /// visible within this viewport + fn visible(&self, ent: &E) -> bool { + self.on_screen(ent.position()).within(self.outer.inner()) + } + + /// Convert the given inner-relative position to one on the actual screen + fn on_screen(&self, pos: Position) -> Position { + pos + self.inner.position + self.outer.inner().position + } +} + +impl Viewport { + /// Draw the given entity to the viewport at its position, if visible + pub fn draw(&mut self, entity: &T) -> io::Result<()> { + if !self.visible(entity) { + return Ok(()); + } + write!( + self, + "{}", + (entity.position() + + self.inner.position + + self.outer.inner().position) + .cursor_goto() + )?; + entity.do_draw(self) + } + + /// Clear whatever is drawn at the given inner-relative position, if visible + pub fn clear(&mut self, pos: Position) -> io::Result<()> { + write!(self, "{} ", self.on_screen(pos).cursor_goto(),) + } + + /// Initialize this viewport by drawing its outer box to the screen + pub fn init(&mut self) -> io::Result<()> { + write!(self, "{}", make_box(BoxStyle::Thin, self.outer.dimensions)) + } +} + +impl Positioned for Viewport { + fn position(&self) -> Position { + self.outer.position + } +} + +impl Write for Viewport { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.out.write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.out.flush() + } + + fn write_all(&mut self, buf: &[u8]) -> io::Result<()> { + self.out.write_all(buf) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::Dimensions; + // use proptest::prelude::*; + + #[test] + fn test_visible() { + assert!(Viewport { + outer: BoundingBox::at_origin(Dimensions { w: 10, h: 10 }), + inner: BoundingBox { + position: Position { x: -10, y: -10 }, + dimensions: Dimensions { w: 15, h: 15 }, + }, + out: (), + } + .visible(&Position { x: 13, y: 13 })); + + assert!(!Viewport { + outer: BoundingBox::at_origin(Dimensions { w: 10, h: 10 }), + inner: BoundingBox { + position: Position { x: -10, y: -10 }, + dimensions: Dimensions { w: 15, h: 15 }, + }, + out: (), + } + .visible(&Position { x: 1, y: 1 })); + } + + // proptest! { + // #[test] + // fn nothing_is_visible_in_viewport_off_screen(pos: Position, outer: BoundingBox) { + // let invisible_viewport = Viewport { + // outer, + // inner: BoundingBox { + // position: Position {x: -(outer.dimensions.w as i16), y: -(outer.dimensions.h as i16)}, + // dimensions: outer.dimensions, + // }, + // out: () + // }; + + // assert!(!invisible_viewport.visible(&pos)); + // } + // } +} diff --git a/src/entities/character.rs b/src/entities/character.rs index e40b7b988..f436608ea 100644 --- a/src/entities/character.rs +++ b/src/entities/character.rs @@ -1,15 +1,38 @@ +use proptest_derive::Arbitrary; +use std::io::{self, Write}; +use termion::cursor; + +use crate::display; use crate::types::{Position, Speed}; const DEFAULT_SPEED: Speed = Speed(100); +#[derive(Debug, PartialEq, Eq, Arbitrary)] pub struct Character { - position: Position, + /// The position of the character, relative to the game + pub position: Position, } impl Character { + pub fn new() -> Character { + Character { + position: Position { x: 0, y: 0 }, + } + } + pub fn speed(&self) -> Speed { Speed(100) } } positioned!(Character); + +impl display::Draw for Character { + fn do_draw(&self, out: &mut W) -> io::Result<()> { + write!( + out, + "@{}", + cursor::Left(1), + ) + } +} diff --git a/src/entities/mod.rs b/src/entities/mod.rs index 788912266..0320f2ddd 100644 --- a/src/entities/mod.rs +++ b/src/entities/mod.rs @@ -1 +1,2 @@ pub mod character; +pub use character::Character; diff --git a/src/game.rs b/src/game.rs index a41d7f73f..6274ef573 100644 --- a/src/game.rs +++ b/src/game.rs @@ -1,30 +1,28 @@ -use std::thread; use crate::settings::Settings; +use crate::types::Positioned; 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::display::{self, Viewport}; +use crate::entities::Character; use crate::types::command::Command; +type Stdout<'a> = RawTerminal>; + /// 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, + viewport: Viewport>, /// An iterator on keypresses from the user keys: Keys>, - stdout: RawTerminal>, - - /// The position of the character - character: Position, + /// The player character + character: Character, } impl<'a> Game<'a> { @@ -37,35 +35,36 @@ impl<'a> Game<'a> { ) -> Game<'a> { Game { settings: settings, - viewport: BoundingBox::at_origin(Dimensions { w, h }), + viewport: Viewport { + outer: BoundingBox::at_origin(Dimensions { w, h }), + inner: BoundingBox::at_origin(Dimensions { + w: w - 2, + h: h - 2, + }), + out: stdout, + }, keys: stdin.keys(), - stdout: stdout, - character: Position { x: 1, y: 1 }, + character: Character::new(), } } /// 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()) + !pos.within(self.viewport.inner) + } + + fn draw_entities(&mut self) -> io::Result<()> { + self.viewport.draw(&self.character) } /// Run the game - pub fn run(mut self) { + pub fn run(mut self) -> io::Result<()> { 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(); + self.viewport.init()?; + self.draw_entities()?; + self.flush()?; loop { - let mut character_moved = false; + let mut old_position = None; match Command::from_key(self.keys.next().unwrap().unwrap()) { Some(Command::Quit) => { info!("Quitting game due to user request"); @@ -73,46 +72,51 @@ impl<'a> Game<'a> { } Some(Command::Move(direction)) => { - let new_pos = self.character + direction; + let new_pos = self.character.position + direction; if !self.collision_at(new_pos) { - self.character = new_pos; - character_moved = true; + old_position = Some(self.character.position); + self.character.position = new_pos; } } _ => (), } - if character_moved { - debug!("char: {:?}", self.character); - write!( - self, - " {}@{}", - cursor::Goto(self.character.x + 1, self.character.y + 1,), - cursor::Left(1) - ) - .unwrap(); + match old_position { + Some(old_pos) => { + self.viewport.clear(old_pos)?; + self.viewport.draw(&self.character)?; + } + None => () } - self.flush().unwrap(); + self.flush()?; + debug!("{:?}", self.character); } + Ok(()) } } impl<'a> Drop for Game<'a> { fn drop(&mut self) { - display::clear(self).unwrap(); + display::clear(self).unwrap_or(()); } } impl<'a> Write for Game<'a> { fn write(&mut self, buf: &[u8]) -> io::Result { - self.stdout.write(buf) + self.viewport.write(buf) } fn flush(&mut self) -> io::Result<()> { - self.stdout.flush() + self.viewport.flush() } fn write_all(&mut self, buf: &[u8]) -> io::Result<()> { - self.stdout.write_all(buf) + self.viewport.write_all(buf) + } +} + +impl<'a> Positioned for Game<'a> { + fn position(&self) -> Position { + Position { x: 0, y: 0 } } } diff --git a/src/main.rs b/src/main.rs index 24d1bbba2..f2c3d00f9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,6 +23,7 @@ use prettytable::format::consts::FORMAT_BOX_CHARS; use settings::Settings; use std::io::{self, StdinLock, StdoutLock}; +use std::panic; use termion::raw::IntoRawMode; use termion::raw::RawTerminal; @@ -34,8 +35,9 @@ fn init( w: u16, h: u16, ) { + panic::set_hook(Box::new(|info| error!("{}", info))); let game = Game::new(settings, stdout, stdin, w, h); - game.run() + game.run().unwrap() } fn main() { diff --git a/src/types/mod.rs b/src/types/mod.rs index 331aa236e..146dfac9d 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -5,6 +5,7 @@ pub mod direction; pub use direction::Direction; pub use direction::Direction::{Down, Left, Right, Up}; use proptest_derive::Arbitrary; +use termion::cursor; #[derive(Clone, Copy, Debug, PartialEq, Eq, Arbitrary)] pub struct Dimensions { @@ -42,11 +43,21 @@ impl BoundingBox { } } + pub fn from_corners(top_left: Position, lower_right: Position) -> BoundingBox { + BoundingBox { + position: top_left, + dimensions: Dimensions { + w: (lower_right.x - top_left.x) as u16, + h: (lower_right.y - top_left.y) as u16, + } + } + } + pub fn lr_corner(self) -> Position { self.position + (Position { - x: self.dimensions.w, - y: self.dimensions.h, + x: self.dimensions.w as i16, + y: self.dimensions.h as i16, }) } @@ -80,12 +91,12 @@ impl ops::Sub for BoundingBox { #[derive(Clone, Copy, Debug, PartialEq, Eq, Arbitrary)] pub struct Position { /// x (horizontal) position - #[proptest(strategy = "std::ops::Range::::from(0..100)")] - pub x: u16, + #[proptest(strategy = "std::ops::Range::::from(0..100)")] + pub x: i16, - #[proptest(strategy = "std::ops::Range::::from(0..100)")] + #[proptest(strategy = "std::ops::Range::::from(0..100)")] /// y (vertical) position - pub y: u16, + pub y: i16, } pub const ORIGIN: Position = Position { x: 0, y: 0 }; @@ -97,6 +108,13 @@ impl Position { pub fn within(self, b: BoundingBox) -> bool { (self > b.position - UNIT_POSITION) && self < (b.lr_corner()) } + + /// Returns a sequence of ASCII escape characters for moving the cursor to + /// this Position + pub fn cursor_goto(&self) -> cursor::Goto { + // + 1 because Goto is 1-based, but position is 0-based + cursor::Goto(self.x as u16 + 1, self.y as u16 + 1) + } } impl PartialOrd for Position { @@ -131,7 +149,7 @@ impl ops::Add for Position { fn add(self, dir: Direction) -> Position { match dir { Left => { - if self.x > 0 { + if self.x > std::i16::MIN { Position { x: self.x - 1, ..self @@ -141,7 +159,7 @@ impl ops::Add for Position { } } Right => { - if self.x < std::u16::MAX { + if self.x < std::i16::MAX { Position { x: self.x + 1, ..self @@ -151,7 +169,7 @@ impl ops::Add for Position { } } Up => { - if self.y > 0 { + if self.y > std::i16::MIN { Position { y: self.y - 1, ..self @@ -161,7 +179,7 @@ impl ops::Add for Position { } } Down => { - if self.y < std::u16::MAX { + if self.y < std::i16::MAX { Position { y: self.y + 1, ..self @@ -194,12 +212,18 @@ impl ops::Sub for Position { } } +impl Positioned for Position { + fn position(&self) -> Position { + *self + } +} + pub trait Positioned { - fn x(&self) -> u16 { + fn x(&self) -> i16 { self.position().x } - fn y(&self) -> u16 { + fn y(&self) -> i16 { self.position().y }