summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock114
-rw-r--r--Cargo.toml4
-rw-r--r--src/entity.rs11
-rw-r--r--src/entity/player.rs82
-rw-r--r--src/error.rs10
-rw-r--r--src/game.rs98
-rw-r--r--src/lib.rs57
-rw-r--r--src/main.rs22
-rw-r--r--src/physics.rs64
-rw-r--r--src/structure.rs24
-rw-r--r--src/structure/building.rs119
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"
diff --git a/Cargo.toml b/Cargo.toml
index ecf1e28..3b3aaca 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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);
+ }
+}