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:
parent
de081d7b1d
commit
78a52142d1
8 changed files with 267 additions and 59 deletions
7
proptest-regressions/display/viewport.txt
Normal file
7
proptest-regressions/display/viewport.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 b84a5a6dbba5cfc69329a119d9e20328c0372e0db2b72e5d71d971e3f13f8749 # shrinks to pos = Position { x: 0, y: 0 }, outer = BoundingBox { dimensions: Dimensions { w: 0, h: 0 }, position: Position { x: 0, y: 0 } }
|
|
@ -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
138
src/display/viewport.rs
Normal 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));
|
||||
// }
|
||||
// }
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
pub mod character;
|
||||
pub use character::Character;
|
||||
|
|
94
src/game.rs
94
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 }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue