diff options
author | Toby Vincent <tobyv13@gmail.com> | 2022-08-09 15:32:27 -0500 |
---|---|---|
committer | Toby Vincent <tobyv13@gmail.com> | 2022-08-10 12:08:40 -0500 |
commit | 37c88bfc0b6dcb4a32e33b3dc35d8eba456388c4 (patch) | |
tree | 9169e04bd87aa4059861b3cc265ed2c9ef8274ae | |
parent | 26eb05dbf181b22897a43c88e1d3fa75502780ac (diff) |
-rw-r--r-- | Cargo.lock | 114 | ||||
-rw-r--r-- | Cargo.toml | 4 | ||||
-rw-r--r-- | src/entity.rs | 11 | ||||
-rw-r--r-- | src/entity/player.rs | 82 | ||||
-rw-r--r-- | src/error.rs | 10 | ||||
-rw-r--r-- | src/game.rs | 98 | ||||
-rw-r--r-- | src/lib.rs | 57 | ||||
-rw-r--r-- | src/main.rs | 22 | ||||
-rw-r--r-- | src/physics.rs | 64 | ||||
-rw-r--r-- | src/structure.rs | 24 | ||||
-rw-r--r-- | src/structure/building.rs | 119 |
11 files changed, 602 insertions, 3 deletions
diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..415a4b1 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,114 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "libc" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" + +[[package]] +name = "numtoa" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" + +[[package]] +name = "proc-macro2" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_termios" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f" +dependencies = [ + "redox_syscall", +] + +[[package]] +name = "rustic_adventure" +version = "0.1.0" +dependencies = [ + "termion", + "thiserror", +] + +[[package]] +name = "syn" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termion" +version = "1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e" +dependencies = [ + "libc", + "numtoa", + "redox_syscall", + "redox_termios", +] + +[[package]] +name = "thiserror" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5f6586b7f764adc0231f4c79be7b920e766bb2f3e51b3661cdb263828f19994" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12bafc5b54507e0149cdf1b145a5d80ab80a90bcd9275df43d4fff68460f6c21" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" @@ -3,6 +3,6 @@ name = "rustic_adventure" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] +termion = "1.5.6" +thiserror = "1.0.32" diff --git a/src/entity.rs b/src/entity.rs new file mode 100644 index 0000000..25f30b2 --- /dev/null +++ b/src/entity.rs @@ -0,0 +1,11 @@ +pub use self::player::Player; + +mod player; + +pub(crate) trait Entity { + fn render_map(&self) -> std::collections::HashMap<crate::physics::Point, char>; +} + +pub(crate) trait Controllable { + fn handle_input(&mut self, key: termion::event::Key) -> Option<crate::game::Command>; +} diff --git a/src/entity/player.rs b/src/entity/player.rs new file mode 100644 index 0000000..45f3e13 --- /dev/null +++ b/src/entity/player.rs @@ -0,0 +1,82 @@ +use std::{ + collections::HashMap, + fmt::{Display, Formatter}, +}; + +use termion::event::Key; + +use crate::{ + game::Command, + physics::{Point, Vector}, + Position, Render, Velocity, +}; + +use crate::entity::Controllable; + +#[derive(Debug, Default)] +pub struct Player { + pub position: Point, + pub velocity: Vector, + render_map: HashMap<Point, char>, +} + +impl Player { + pub fn new(position: Point, velocity: Vector) -> Self { + Self { + position, + velocity, + render_map: HashMap::from([(position, '@')]), + } + } +} + +impl Display for Player { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + return write!( + f, + "{}@", + termion::cursor::Goto(self.position.x, self.position.y) + ); + } +} + +impl Render for Player { + fn render_map(&self) -> &HashMap<Point, char> { + &self.render_map + } +} + +impl Controllable for Player { + fn handle_input(&mut self, key: Key) -> Option<Command> { + match key { + Key::Up | Key::Char('w') | Key::Char('k') => self.velocity.y -= 1, + Key::Down | Key::Char('s') | Key::Char('j') => self.velocity.y += 1, + Key::Left | Key::Char('a') | Key::Char('h') => self.velocity.x -= 1, + Key::Right | Key::Char('d') | Key::Char('l') => self.velocity.x += 1, + _ => return None, + }; + Some(Command::UpdatePlayer) + } +} + +impl Position for Player { + fn get_position(&self) -> Point { + self.position + } + + fn set_position<P: Into<crate::physics::Point>>(&mut self, position: P) -> crate::Result<()> { + self.position = position.into(); + Ok(()) + } +} + +impl Velocity for Player { + fn get_velocity(&self) -> Vector { + self.velocity + } + + fn set_velocity<V: Into<crate::physics::Vector>>(&mut self, velocity: V) -> crate::Result<()> { + self.velocity = velocity.into(); + Ok(()) + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..81dd4fa --- /dev/null +++ b/src/error.rs @@ -0,0 +1,10 @@ +pub type Result<T> = std::result::Result<T, Error>; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Failed to parse key input: {0:?}")] + IO(#[from] std::io::Error), + + #[error("Failed to write to screen: {0:?}")] + Fmt(#[from] std::fmt::Error), +} diff --git a/src/game.rs b/src/game.rs new file mode 100644 index 0000000..0358638 --- /dev/null +++ b/src/game.rs @@ -0,0 +1,98 @@ +use std::io::{Read, Write}; + +use termion::{ + cursor::HideCursor, event::Key, input::TermRead, raw::IntoRawMode, screen::AlternateScreen, +}; + +use crate::{ + entity::{Controllable, Player}, + error::Result, + physics::Vector, + structure::Building, + Render, Velocity, +}; + +#[derive(Debug)] +pub(crate) enum Command { + Quit, + UpdatePlayer, +} + +#[derive(Debug, Default)] +pub struct Game { + pub player: Player, + pub structures: Vec<Building>, +} + +impl Game { + pub fn run_loop<R: Read, W: IntoRawMode>(&mut self, reader: R, writer: W) -> Result<()> { + let mut screen = AlternateScreen::from(HideCursor::from(writer).into_raw_mode().unwrap()); + self.write_screen(&mut screen)?; + + for key in reader.keys() { + match self.handle_input(key?) { + Some(Command::Quit) => break, + Some(Command::UpdatePlayer) => &mut self.update()?, + None => continue, + }; + self.write_screen(&mut screen)?; + } + + write!(screen, "{}", termion::cursor::Show).map_err(From::from) + } + + fn write_screen(&self, writer: &mut impl Write) -> Result<()> { + write!(writer, "{}", termion::clear::All)?; + + self.player.render(writer)?; + + for entity in &self.structures { + entity.render(writer)?; + } + + writer.flush().map_err(From::from) + } + + fn handle_input(&mut self, key: Key) -> Option<Command> { + match key { + Key::Char('q') => Some(Command::Quit), + _ => self.player.handle_input(key), + } + } + + fn update(&mut self) -> Result<()> { + self.player.tick_time()?; + self.player.set_velocity(Vector::default()) + } +} + +#[cfg(test)] +mod tests { + use std::io::stdout; + + use crate::{structure::Building, Game, Position}; + + #[test] + fn run_game() { + let stdin = "q".as_bytes(); + let stdout = stdout(); + + let mut game = Game::default(); + + if let Err(err) = game.run_loop(stdin, stdout) { + eprintln!("{}", err) + } + } + + #[test] + fn move_player() { + let stdin = "sssdddq".as_bytes(); + let stdout = stdout(); + + let mut game = Game::default(); + + if let Err(err) = game.run_loop(stdin, stdout) { + eprintln!("{}", err) + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..aed9bcc --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,57 @@ +pub use crate::error::{Error, Result}; +pub use crate::game::Game; + +pub mod entity; +pub mod structure; + +mod error; +mod game; +mod physics; + +pub trait Render { + fn render_map(&self) -> &std::collections::HashMap<crate::physics::Point, char>; + fn render<W: std::io::Write>(&self, w: &mut W) -> crate::Result<()> { + for (p, c) in self.render_map() { + write!(w, "{}{}", termion::cursor::Goto(p.x, p.y), c)? + } + Ok(()) + } +} + +pub trait Derender { + fn derender_map(&self) -> &std::collections::HashMap<crate::physics::Point, char>; + fn derender<W: std::io::Write>(&self, w: &mut W) -> crate::Result<()> { + for p in self.derender_map().keys() { + write!(w, "{}\x08", termion::cursor::Goto(p.x, p.y))? + } + Ok(()) + } +} + +impl<T: Render> Derender for T { + fn derender_map(&self) -> &std::collections::HashMap<crate::physics::Point, char> { + self.render_map() + } +} + +pub trait Collision { + fn collision_map(&self) -> &Vec<crate::physics::Point>; + fn is_collision<P: Into<crate::physics::Point>>(&self, point: P) -> bool { + self.collision_map().contains(&point.into()) + } +} + +pub trait Position { + fn get_position(&self) -> crate::physics::Point; + fn set_position<P: Into<crate::physics::Point>>(&mut self, position: P) -> crate::Result<()>; +} + +pub trait Velocity: Position { + fn get_velocity(&self) -> crate::physics::Vector; + + fn set_velocity<V: Into<crate::physics::Vector>>(&mut self, velocity: V) -> crate::Result<()>; + + fn tick_time(&mut self) -> Result<()> { + self.set_position(self.get_position() + self.get_velocity()) + } +} diff --git a/src/main.rs b/src/main.rs index e7a11a9..95b6ea3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,23 @@ +use std::io::{stdin, stdout}; + +use rustic_adventure::{structure::Building, Game, Position}; + fn main() { - println!("Hello, world!"); + let stdin = stdin(); + let stdout = stdout(); + + let mut game = Game::default(); + + let building = Building::builder() + .width(50) + .height(50) + .position((10, 10)) + .build(); + + game.structures.push(building); + game.player.set_position((5, 5)).unwrap(); + + if let Err(err) = game.run_loop(stdin, stdout) { + eprintln!("{}", err) + } } diff --git a/src/physics.rs b/src/physics.rs new file mode 100644 index 0000000..de09a2f --- /dev/null +++ b/src/physics.rs @@ -0,0 +1,64 @@ +use std::ops::Add; + +#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)] +pub struct Point { + pub x: u16, + pub y: u16, +} + +impl Point { + pub fn new(x: u16, y: u16) -> Self { + Self { x, y } + } + + pub fn add_velocity(&mut self, velocity: Vector) { + self.x += velocity.x; + self.y += velocity.y; + } +} + +impl From<(u16, u16)> for Point { + fn from((x, y): (u16, u16)) -> Self { + Self { x, y } + } +} + +impl From<Point> for (u16, u16) { + fn from(point: Point) -> Self { + (point.x, point.y) + } +} + +impl Add<Vector> for Point { + type Output = Self; + + fn add(self, rhs: Vector) -> Self::Output { + Self { + x: self.x + rhs.x, + y: self.y + rhs.y, + } + } +} + +#[derive(Debug, Default, Clone, Copy)] +pub struct Vector { + pub x: u16, + pub y: u16, +} + +impl Vector { + pub fn new(x: u16, y: u16) -> Self { + Self { x, y } + } +} + +impl Add<Vector> for Vector { + type Output = Self; + + fn add(self, rhs: Vector) -> Self::Output { + Self { + x: self.x + rhs.x, + y: self.y + rhs.y, + } + } +} diff --git a/src/structure.rs b/src/structure.rs new file mode 100644 index 0000000..2137766 --- /dev/null +++ b/src/structure.rs @@ -0,0 +1,24 @@ +use std::collections::HashMap; + +pub use building::Building; + +use crate::{physics::Point, Collision, Render}; + +mod building; + +pub struct Structure { + render_map: HashMap<Point, char>, + collision_map: Vec<Point>, +} + +impl Render for Structure { + fn render_map(&self) -> &HashMap<Point, char> { + &self.render_map + } +} + +impl Collision for Structure { + fn collision_map(&self) -> &Vec<crate::physics::Point> { + &self.collision_map + } +} diff --git a/src/structure/building.rs b/src/structure/building.rs new file mode 100644 index 0000000..b6c6346 --- /dev/null +++ b/src/structure/building.rs @@ -0,0 +1,119 @@ +use std::collections::HashMap; + +use crate::{physics::Point, Render}; + +#[derive(Debug, PartialEq, Eq)] +pub struct Building { + pub position: Point, + pub width: u16, + pub height: u16, + render_map: HashMap<Point, char>, +} + +impl Building { + pub fn builder() -> BuildingBuilder { + BuildingBuilder::default() + } +} + +#[derive(Debug, Default)] +pub struct BuildingBuilder { + position: Option<Point>, + width: Option<u16>, + height: Option<u16>, +} + +impl BuildingBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn position<P: Into<Point>>(mut self, position: P) -> BuildingBuilder { + self.position = Some(position.into()); + self + } + + pub fn width(mut self, width: u16) -> BuildingBuilder { + self.width = Some(width); + self + } + + pub fn height(mut self, height: u16) -> BuildingBuilder { + self.height = Some(height); + self + } + + pub fn build(self) -> Building { + let position = self.position.unwrap_or_default(); + let width = self.width.unwrap_or_default(); + let height = self.height.unwrap_or_default(); + let mut render_map = HashMap::new(); + + render_map.insert(position, '┌'); + render_map.insert((position.x, height).into(), '└'); + render_map.insert((width, position.y).into(), '┐'); + render_map.insert((width, height).into(), '┘'); + + for x in position.x + 1..width { + render_map.insert((x, position.y).into(), '─'); + render_map.insert((x, height).into(), '─'); + } + + for y in position.y + 1..height { + render_map.insert((position.x, y).into(), '│'); + render_map.insert((width, y).into(), '│'); + } + + Building { + position, + width, + height, + render_map, + } + } +} + +impl Render for Building { + fn render_map(&self) -> &std::collections::HashMap<crate::physics::Point, char> { + &self.render_map + } +} +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use super::*; + use crate::physics::Point; + + #[test] + fn building_builder() { + let position = Point { x: 1, y: 1 }; + let width = 3u16; + let height = 3u16; + let render_map = HashMap::from([ + (position, '┌'), + ((position.x, height).into(), '└'), + ((width, position.y).into(), '┐'), + ((width, height).into(), '┘'), + ((position.x + 1, position.y).into(), '─'), + ((position.x + 1, height).into(), '─'), + ((position.x, position.y + 1).into(), '│'), + ((width, position.y + 1).into(), '│'), + ]); + + let building = Building { + position, + width, + height, + render_map, + }; + + let building_from_builder = BuildingBuilder::new() + .width(width) + .height(height) + .position(position) + .build(); + + assert_eq!(building, building_from_builder); + } +} |