about summary refs log tree commit diff
diff options
context:
space:
mode:
authorGriffin Smith <root@gws.fyi>2019-07-06T19·32-0400
committerGriffin Smith <root@gws.fyi>2019-07-06T19·32-0400
commit78a52142d191d25a74cb2124d5cca8a69d51ba7f (patch)
treedd023a823bae6cc427e32a497bd68db85bbfab4b
parentde081d7b1d0b791b2e61f9cde7369ea11647e0ae (diff)
Make all drawing happen to a viewport
We now have an inner and outer viewport, and entity positions are
relative to the inner one while drawing happens to the outer one.
-rw-r--r--proptest-regressions/display/viewport.txt7
-rw-r--r--src/display/mod.rs9
-rw-r--r--src/display/viewport.rs138
-rw-r--r--src/entities/character.rs25
-rw-r--r--src/entities/mod.rs1
-rw-r--r--src/game.rs94
-rw-r--r--src/main.rs4
-rw-r--r--src/types/mod.rs48
8 files changed, 267 insertions, 59 deletions
diff --git a/proptest-regressions/display/viewport.txt b/proptest-regressions/display/viewport.txt
new file mode 100644
index 000000000000..e38056d975ff
--- /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 5dba48b44d9a..664aaf319cc7 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<T: Write>(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<W: Write>(&self, out: &mut W) -> io::Result<()>;
+}
diff --git a/src/display/viewport.rs b/src/display/viewport.rs
new file mode 100644
index 000000000000..bd2fac07146f
--- /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<W> {
+    /// 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<W> Debug for Viewport<W> {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(
+            f,
+            "Viewport {{ outer: {:?}, inner: {:?}, out: <OUT> }}",
+            self.outer, self.inner
+        )
+    }
+}
+
+impl<W> Viewport<W> {
+    /// Returns true if the (inner-relative) position of the given entity is
+    /// visible within this viewport
+    fn visible<E: Positioned>(&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<W: Write> Viewport<W> {
+    /// Draw the given entity to the viewport at its position, if visible
+    pub fn draw<T: 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<W> Positioned for Viewport<W> {
+    fn position(&self) -> Position {
+        self.outer.position
+    }
+}
+
+impl<W: Write> Write for Viewport<W> {
+    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+        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 e40b7b988e8d..f436608ea5e7 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<W: Write>(&self, out: &mut W) -> io::Result<()> {
+        write!(
+            out,
+            "@{}",
+            cursor::Left(1),
+        )
+    }
+}
diff --git a/src/entities/mod.rs b/src/entities/mod.rs
index 78891226662a..0320f2ddd9c1 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 a41d7f73fd75..6274ef573f58 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<StdoutLock<'a>>;
+
 /// 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<Stdout<'a>>,
 
     /// An iterator on keypresses from the user
     keys: Keys<StdinLock<'a>>,
 
-    stdout: RawTerminal<StdoutLock<'a>>,
-
-    /// 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<usize> {
-        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 24d1bbba29ca..f2c3d00f96d7 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 331aa236e324..146dfac9d99b 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<Dimensions> for BoundingBox {
 #[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::<i16>::from(0..100)")]
+    pub x: i16,
 
-    #[proptest(strategy = "std::ops::Range::<u16>::from(0..100)")]
+    #[proptest(strategy = "std::ops::Range::<i16>::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<Direction> 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<Direction> 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<Direction> 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<Direction> 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<Position> 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
     }