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.
This commit is contained in:
Griffin Smith 2019-07-06 15:32:38 -04:00
parent de081d7b1d
commit 78a52142d1
8 changed files with 267 additions and 59 deletions

View 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 b84a5a6dbba5cfc69329a119d9e20328c0372e0db2b72e5d71d971e3f13f8749 # shrinks to pos = Position { x: 0, y: 0 }, outer = BoundingBox { dimensions: Dimensions { w: 0, h: 0 }, position: Position { x: 0, y: 0 } }

View file

@ -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<()>;
}

138
src/display/viewport.rs Normal file
View file

@ -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));
// }
// }
}

View file

@ -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),
)
}
}

View file

@ -1 +1,2 @@
pub mod character;
pub use character::Character;

View file

@ -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 }
}
}

View file

@ -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() {

View file

@ -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
}