Implement a global map of entities

Implement a global map of entities, which allows referencing by either
position or ID and updating the positions of existent entities, and put
the character in there.
This commit is contained in:
Griffin Smith 2019-07-08 20:58:51 -04:00
parent 20f1ccb460
commit 5af2429ecb
14 changed files with 465 additions and 36 deletions

8
Cargo.lock generated
View file

@ -249,6 +249,11 @@ dependencies = [
"winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "downcast-rs"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "dtoa"
version = "0.4.4"
@ -1083,8 +1088,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
name = "xanthous"
version = "0.1.0"
dependencies = [
"backtrace 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
"clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
"config 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)",
"downcast-rs 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
"itertools 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1145,6 +1152,7 @@ dependencies = [
"checksum csv 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "37519ccdfd73a75821cac9319d4fce15a81b9fcf75f951df5b9988aa3a0af87d"
"checksum csv-core 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "9b5cadb6b25c77aeff80ba701712494213f4a8418fcda2ee11b6560c3ad0bf4c"
"checksum dirs 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901"
"checksum downcast-rs 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f2b92dfd5c2f75260cbf750572f95d387e7ca0ba5e3fbe9e1a33f23025be020f"
"checksum dtoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "ea57b42383d091c85abcc2706240b94ab2a8fa1fc81c10ff23c4de06e2a90b5e"
"checksum either 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5527cfe0d098f36e3f8839852688e63c8fff1c90b2b405aef730615f9a7bcf7b"
"checksum encode_unicode 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "90b2c9496c001e8cb61827acdefad780795c42264c137744cae6f7d9e3450abd"

View file

@ -5,8 +5,10 @@ authors = ["Griffin Smith <root@gws.fyi>"]
edition = "2018"
[dependencies]
backtrace = "0.3"
clap = {version = "^2.33.0", features = ["yaml"]}
config = "*"
downcast-rs = "^1.0.4"
itertools = "*"
lazy_static = "*"
log = "*"

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 16afe2473971397314ffa77acf7bad62f0c40bc3f591aff7aa9193c29e5a0921 # shrinks to items = [(Position { x: 92, y: 60 }, ""), (Position { x: 92, y: 60 }, "")]

View file

@ -14,5 +14,17 @@ pub fn clear<T: Write>(out: &mut T) -> io::Result<()> {
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<()>;
fn do_draw(&self, out: &mut Write) -> io::Result<()>;
}
impl<T : Draw> Draw for &T {
fn do_draw(&self, out: &mut Write) -> io::Result<()> {
(**self).do_draw(out)
}
}
impl<T : Draw> Draw for Box<T> {
fn do_draw(&self, out: &mut Write) -> io::Result<()> {
(**self).do_draw(out)
}
}

View file

@ -2,7 +2,6 @@ use super::BoxStyle;
use super::Draw;
use crate::display::draw_box::draw_box;
use crate::display::utils::clone_times;
use crate::display::utils::times;
use crate::types::{BoundingBox, Position, Positioned};
use std::fmt::{self, Debug};
use std::io::{self, Write};

View file

@ -1,13 +1,13 @@
use crate::display;
use crate::entities::Entity;
use crate::types::{Position, Speed};
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)]
#[derive(Debug, PartialEq, Eq, Arbitrary, Clone)]
pub struct Character {
/// The position of the character, relative to the game
pub position: Position,
@ -26,13 +26,12 @@ impl Character {
}
positioned!(Character);
positioned_mut!(Character);
impl Entity for Character {}
impl display::Draw for Character {
fn do_draw<W: Write>(&self, out: &mut W) -> io::Result<()> {
write!(
out,
"@{}",
cursor::Left(1),
)
fn do_draw(&self, out: &mut Write) -> io::Result<()> {
write!(out, "@{}", cursor::Left(1),)
}
}

View file

@ -1,2 +1,16 @@
pub mod character;
use crate::display::Draw;
use crate::types::{Positioned, PositionedMut};
pub use character::Character;
use downcast_rs::Downcast;
use std::io::{self, Write};
pub trait Entity: Positioned + PositionedMut + Draw + Downcast {}
impl_downcast!(Entity);
impl Draw for Box<dyn Entity> {
fn do_draw(&self, out: &mut Write) -> io::Result<()> {
(**self).do_draw(out)
}
}

View file

@ -1,10 +1,14 @@
use crate::display::{self, Viewport};
use crate::entities::Character;
use crate::entities::Entity;
use crate::messages::message;
use crate::settings::Settings;
use crate::types::command::Command;
use crate::types::Positioned;
use crate::types::{BoundingBox, Dimensions, Position};
use crate::types::entity_map::EntityID;
use crate::types::entity_map::EntityMap;
use crate::types::{
BoundingBox, Collision, Dimensions, Position, Positioned, PositionedMut,
};
use rand::rngs::SmallRng;
use rand::SeedableRng;
use std::io::{self, StdinLock, StdoutLock, Write};
@ -16,6 +20,20 @@ type Stdout<'a> = RawTerminal<StdoutLock<'a>>;
type Rng = SmallRng;
type AnEntity<'a> = Box<dyn Entity>;
impl<'a> Positioned for AnEntity<'a> {
fn position(&self) -> Position {
(**self).position()
}
}
impl<'a> PositionedMut for AnEntity<'a> {
fn set_position(&mut self, pos: Position) {
(**self).set_position(pos)
}
}
/// The full state of a running Game
pub struct Game<'a> {
settings: Settings,
@ -25,8 +43,11 @@ pub struct Game<'a> {
/// An iterator on keypresses from the user
keys: Keys<StdinLock<'a>>,
/// The player character
character: Character,
/// The map of all the entities in the game
entities: EntityMap<AnEntity<'a>>,
/// The entity ID of the player character
character_entity_id: EntityID,
/// The messages that have been said to the user, in forward time order
messages: Vec<String>,
@ -51,6 +72,7 @@ impl<'a> Game<'a> {
Some(seed) => SmallRng::seed_from_u64(seed),
None => SmallRng::from_entropy(),
};
let mut entities: EntityMap<AnEntity<'a>> = EntityMap::new();
Game {
settings,
rng,
@ -61,19 +83,34 @@ impl<'a> Game<'a> {
stdout,
),
keys: stdin.keys(),
character: Character::new(),
character_entity_id: entities.insert(Box::new(Character::new())),
messages: Vec::new(),
entities,
}
}
/// 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)
/// Returns a collision, if any, at the given Position in the game
fn collision_at(&self, pos: Position) -> Option<Collision> {
if !pos.within(self.viewport.inner) {
Some(Collision::Stop)
} else {
None
}
}
fn character(&self) -> &Character {
debug!("ents: {:?} cid: {:?}", self.entities.ids().map(|id| *id).collect::<Vec<u32>>(), self.character_entity_id);
(*self.entities.get(self.character_entity_id).unwrap())
.downcast_ref()
.unwrap()
}
/// Draw all the game entities to the screen
fn draw_entities(&mut self) -> io::Result<()> {
self.viewport.draw(&self.character)
for entity in self.entities.entities() {
self.viewport.draw(entity)?;
}
Ok(())
}
/// Get a message from the global map based on the rng in this game
@ -104,7 +141,6 @@ impl<'a> Game<'a> {
self.viewport.init()?;
self.draw_entities()?;
self.say("global.welcome")?;
self.say("somethign else")?;
self.flush()?;
loop {
let mut old_position = None;
@ -116,10 +152,18 @@ impl<'a> Game<'a> {
}
Some(Move(direction)) => {
let new_pos = self.character.position + direction;
if !self.collision_at(new_pos) {
old_position = Some(self.character.position);
self.character.position = new_pos;
use Collision::*;
let new_pos = self.character().position + direction;
match self.collision_at(new_pos) {
None => {
old_position = Some(self.character().position);
self.entities.update_position(
self.character_entity_id,
new_pos,
);
}
Some(Combat) => unimplemented!(),
Some(Stop) => (),
}
}
@ -131,12 +175,14 @@ impl<'a> Game<'a> {
match old_position {
Some(old_pos) => {
self.viewport.clear(old_pos)?;
self.viewport.draw(&self.character)?;
self.viewport.draw(
// TODO this clone feels unnecessary.
&self.character().clone())?;
}
None => (),
}
self.flush()?;
debug!("{:?}", self.character);
debug!("{:?}", self.character());
}
Ok(())
}

View file

@ -13,9 +13,12 @@ extern crate clap;
extern crate prettytable;
#[macro_use]
extern crate lazy_static;
#[cfg(test)]
#[macro_use]
extern crate maplit;
#[macro_use]
extern crate downcast_rs;
extern crate backtrace;
mod display;
mod game;
@ -24,12 +27,14 @@ mod types;
mod entities;
mod messages;
mod settings;
mod util;
use clap::App;
use game::Game;
use prettytable::format::consts::FORMAT_BOX_CHARS;
use settings::Settings;
use backtrace::Backtrace;
use std::io::{self, StdinLock, StdoutLock};
use std::panic;
@ -43,7 +48,16 @@ fn init(
w: u16,
h: u16,
) {
panic::set_hook(Box::new(|info| error!("{}", info)));
panic::set_hook(if settings.logging.print_backtrace {
Box::new(|info| {
(error!("{}\n{:#?}", info, Backtrace::new()))
})
} else {
Box::new(|info| {
(error!("{}\n{:#?}", info, Backtrace::new()))
})
});
let game = Game::new(settings, stdout, stdin, w, h);
game.run().unwrap()
}

View file

@ -4,13 +4,16 @@ use log4rs::append::file::FileAppender;
use log4rs::config::{Appender, Root};
use log4rs::encode::pattern::PatternEncoder;
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Clone)]
pub struct Logging {
#[serde(default = "Logging::default_level")]
pub level: LevelFilter,
#[serde(default = "Logging::default_file")]
pub file: String,
#[serde(default = "Logging::default_print_backtrace")]
pub print_backtrace: bool,
}
impl Default for Logging {
@ -18,6 +21,7 @@ impl Default for Logging {
Logging {
level: LevelFilter::Off,
file: "debug.log".to_string(),
print_backtrace: true,
}
}
}
@ -44,9 +48,13 @@ impl Logging {
fn default_file() -> String {
Logging::default().file
}
fn default_print_backtrace() -> bool {
Logging::default().print_backtrace
}
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Clone)]
pub struct Settings {
pub seed: Option<u64>,
pub logging: Logging,

8
src/types/collision.rs Normal file
View file

@ -0,0 +1,8 @@
/// Describes a kind of game collision
pub enum Collision {
/// Stop moving - you can't move there!
Stop,
/// Moving into an entity at the given position indicates combat
Combat,
}

242
src/types/entity_map.rs Normal file
View file

@ -0,0 +1,242 @@
use crate::types::Position;
use crate::types::Positioned;
use crate::types::PositionedMut;
use std::collections::hash_map::HashMap;
use std::collections::BTreeMap;
use std::iter::FromIterator;
pub type EntityID = u32;
#[derive(Debug)]
pub struct EntityMap<A> {
by_position: BTreeMap<Position, Vec<EntityID>>,
by_id: HashMap<EntityID, A>,
last_id: EntityID,
}
// impl<A: Debug> ArbitraryF1<A> for EntityMap<A> {
// type Parameters = ();
// fn lift1_with<AS>(base: AS, _: Self::Parameters) -> BoxedStrategy<Self>
// where
// AS: Strategy<Value = A> + 'static,
// {
// unimplemented!()
// }
// // type Strategy = strategy::Just<Self>;
// // fn arbitrary_with(params : Self::Parameters) -> Self::Strategy;
// }
// impl<A: Arbitrary> Arbitrary for EntityMap<A> {
// type Parameters = A::Parameters;
// type Strategy = BoxedStrategy<Self>;
// fn arbitrary_with(params: Self::Parameters) -> Self::Strategy {
// let a_strat: A::Strategy = Arbitrary::arbitrary_with(params);
// ArbitraryF1::lift1::<A::Strategy>(a_strat)
// }
// }
const BY_POS_INVARIANT: &'static str =
"Invariant: All references in EntityMap.by_position should point to existent references in by_id";
impl<A> EntityMap<A> {
pub fn new() -> EntityMap<A> {
EntityMap {
by_position: BTreeMap::new(),
by_id: HashMap::new(),
last_id: 0,
}
}
pub fn len(&self) -> usize {
self.by_id.len()
}
/// Returns a list of all entities at the given position
pub fn at<'a>(&'a self, pos: Position) -> Vec<&'a A> {
// self.by_position.get(&pos).iter().flat_map(|eids| {
// eids.iter()
// .map(|eid| self.by_id.get(eid).expect(BY_POS_INVARIANT))
// })
// gross.
match self.by_position.get(&pos) {
None => Vec::new(),
Some(eids) => {
let mut res = Vec::new();
for eid in eids {
res.push(self.by_id.get(eid).expect(BY_POS_INVARIANT));
}
res
}
}
}
/// Remove all entities at the given position
pub fn remove_all_at(&mut self, pos: Position) {
self.by_position.remove(&pos).map(|eids| {
eids.iter()
.map(|eid| self.by_id.remove(&eid).expect(BY_POS_INVARIANT));
});
}
pub fn get<'a>(&'a self, id: EntityID) -> Option<&'a A> {
self.by_id.get(&id)
}
pub fn entities<'a>(&'a self) -> impl Iterator<Item = &'a A> {
self.by_id.values()
}
pub fn entities_mut<'a>(&'a mut self) -> impl Iterator<Item = &'a mut A> {
self.by_id.values_mut()
}
pub fn ids(&self) -> impl Iterator<Item = &EntityID> {
self.by_id.keys()
}
fn next_id(&mut self) -> EntityID {
self.last_id += 1;
self.last_id
}
}
impl<A: Positioned> EntityMap<A> {
pub fn insert(&mut self, entity: A) -> EntityID {
let pos = entity.position();
let entity_id = self.next_id();
self.by_id.entry(entity_id).or_insert(entity);
self.by_position
.entry(pos)
.or_insert(Vec::new())
.push(entity_id);
entity_id
}
}
impl<A: Positioned> FromIterator<A> for EntityMap<A> {
fn from_iter<I: IntoIterator<Item = A>>(iter: I) -> Self {
let mut em = EntityMap::new();
for ent in iter {
em.insert(ent);
}
em
}
}
impl<A: PositionedMut> EntityMap<A> {
pub fn update_position(
&mut self,
entity_id: EntityID,
new_position: Position,
) {
let mut old_pos = None;
if let Some(entity) = self.by_id.get_mut(&entity_id) {
if entity.position() == new_position {
return;
}
old_pos = Some(entity.position());
entity.set_position(new_position);
}
old_pos.map(|p| {
self.by_position
.get_mut(&p)
.map(|es| es.retain(|e| *e != entity_id));
self.by_position
.entry(new_position)
.or_insert(Vec::new())
.push(entity_id);
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::PositionedMut;
use proptest::prelude::*;
use proptest_derive::Arbitrary;
#[derive(Debug, Arbitrary, PartialEq, Eq, Clone)]
struct TestEntity {
position: Position,
name: String,
}
impl Positioned for TestEntity {
fn position(&self) -> Position {
self.position
}
}
impl PositionedMut for TestEntity {
fn set_position(&mut self, pos: Position) {
self.position = pos
}
}
fn gen_entity_map() -> BoxedStrategy<EntityMap<TestEntity>> {
any::<Vec<TestEntity>>()
.prop_map(|ents| {
ents.iter()
.map(|e| e.clone())
.collect::<EntityMap<TestEntity>>()
})
.boxed()
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(10))]
#[test]
fn test_entity_map_len(items: Vec<TestEntity>) {
let mut map = EntityMap::new();
assert_eq!(map.len(), 0);
for ent in &items {
map.insert(ent);
}
assert_eq!(map.len(), items.len());
}
#[test]
fn test_entity_map_getset(
mut em in gen_entity_map(),
ent: TestEntity
) {
em.insert(ent.clone());
assert!(em.at(ent.position).iter().any(|e| **e == ent))
}
#[test]
fn test_entity_map_set_iter_contains(
mut em in gen_entity_map(),
ent: TestEntity
) {
em.insert(ent.clone());
assert!(em.entities().any(|e| *e == ent))
}
#[test]
fn test_update_position(
mut em in gen_entity_map(),
ent: TestEntity,
new_position: Position,
) {
let original_position = ent.position();
let entity_id = em.insert(ent.clone());
em.update_position(entity_id, new_position);
if new_position != original_position {
assert_eq!(em.at(original_position).len(), 0);
}
assert_eq!(
em.get(entity_id).map(|e| e.position()),
Some(new_position)
);
assert_eq!(
em.at(new_position).iter().map(|e| e.name.clone()).collect::<Vec<_>>(),
vec![ent.name]
)
}
}
}

View file

@ -1,7 +1,11 @@
use std::cmp::Ordering;
use std::ops;
use std::rc::Rc;
pub mod collision;
pub mod command;
pub mod direction;
pub mod entity_map;
pub use collision::Collision;
pub use direction::Direction;
pub use direction::Direction::{Down, Left, Right, Up};
use proptest_derive::Arbitrary;
@ -43,13 +47,16 @@ impl BoundingBox {
}
}
pub fn from_corners(top_left: Position, lower_right: Position) -> 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,
}
},
}
}
@ -70,7 +77,11 @@ impl BoundingBox {
/// Moves the top right corner of the bounding box by the offset specified
/// by the given position, keeping the lower right corner in place
pub fn move_tr_corner(self, offset: Position) -> BoundingBox {
self + offset - Dimensions { w: offset.x as u16, h: offset.y as u16 }
self + offset
- Dimensions {
w: offset.x as u16,
h: offset.y as u16,
}
}
}
@ -94,7 +105,7 @@ impl ops::Sub<Dimensions> for BoundingBox {
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Arbitrary)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Arbitrary, Hash, Ord)]
pub struct Position {
/// x (horizontal) position
#[proptest(strategy = "std::ops::Range::<i16>::from(0..100)")]
@ -105,6 +116,10 @@ pub struct Position {
pub y: i16,
}
pub fn pos(x: i16, y: i16) -> Position {
Position { x, y }
}
pub const ORIGIN: Position = Position { x: 0, y: 0 };
pub const UNIT_POSITION: Position = Position { x: 1, y: 1 };
@ -241,6 +256,47 @@ pub trait Positioned {
}
}
pub trait PositionedMut: Positioned {
fn set_position(&mut self, pos: Position);
}
// impl<A, I> Positioned for A where A : Deref<Target = I>, I: Positioned {
// fn position(&self) -> Position {
// self.position()
// }
// }
impl<T: Positioned> Positioned for Box<T> {
fn position(&self) -> Position {
(**self).position()
}
}
impl<'a, T: Positioned> Positioned for &'a T {
fn position(&self) -> Position {
(**self).position()
}
}
impl<'a, T: Positioned> Positioned for &'a mut T {
fn position(&self) -> Position {
(**self).position()
}
}
impl<'a, T: Positioned> Positioned for Rc<T> {
fn position(&self) -> Position {
(**self).position()
}
}
impl<'a, T: PositionedMut> PositionedMut for &'a mut T {
fn set_position(&mut self, pos: Position) {
(**self).set_position(pos)
}
}
#[macro_export]
macro_rules! positioned {
($name:ident) => {
positioned!($name, position);
@ -254,6 +310,20 @@ macro_rules! positioned {
};
}
#[macro_export]
macro_rules! positioned_mut {
($name:ident) => {
positioned_mut!($name, position);
};
($name:ident, $attr:ident) => {
impl crate::types::PositionedMut for $name {
fn set_position(&mut self, pos: Position) {
self.$attr = pos;
}
}
};
}
/// A number of ticks
#[derive(Clone, Copy, Debug, PartialEq, Eq, Arbitrary)]
pub struct Ticks(pub u16);

0
src/util/mod.rs Normal file
View file