about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock38
-rw-r--r--Cargo.toml1
-rw-r--r--src/display/draw_box.rs50
-rw-r--r--src/display/mod.rs20
-rw-r--r--src/display/viewport.rs12
-rw-r--r--src/entities/entity.rs29
-rw-r--r--src/entities/environment.rs34
-rw-r--r--src/entities/mod.rs3
-rw-r--r--src/entities/util.rs72
-rw-r--r--src/game.rs40
-rw-r--r--src/level_gen/cave_automata.rs4
-rw-r--r--src/level_gen/display.rs17
-rw-r--r--src/level_gen/mod.rs100
-rw-r--r--src/level_gen/util.rs19
-rw-r--r--src/main.rs2
-rw-r--r--src/types/entity_map.rs123
-rw-r--r--src/types/mod.rs55
17 files changed, 557 insertions, 62 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 4c8896cca160..214b78f62542 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -14,6 +14,17 @@ dependencies = [
 ]
 
 [[package]]
+name = "alga"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "approx 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libm 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "num-complex 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
 name = "ansi_term"
 version = "0.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -27,6 +38,14 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
+name = "approx"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
 name = "arc-swap"
 version = "0.3.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -403,6 +422,11 @@ version = "0.2.58"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
+name = "libm"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
 name = "linked-hash-map"
 version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -525,6 +549,15 @@ dependencies = [
 ]
 
 [[package]]
+name = "num-complex"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "autocfg 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
 name = "num-integer"
 version = "0.1.41"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1218,6 +1251,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 name = "xanthous"
 version = "0.1.0"
 dependencies = [
+ "alga 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "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)",
@@ -1259,8 +1293,10 @@ dependencies = [
 [metadata]
 "checksum adler32 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7e522997b529f05601e05166c07ed17789691f562762c7f3b987263d2dedee5c"
 "checksum aho-corasick 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e6f484ae0c99fec2e858eb6134949117399f222608d84cadb3f58c1f97c2364c"
+"checksum alga 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d708cb68c7106ed1844de68f50f0157a7788c2909a6926fad5a87546ef6a4ff8"
 "checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
 "checksum antidote 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "34fde25430d87a9388dadbe6e34d7f72a462c8b43ac8d309b42b0a8505d7e2a5"
+"checksum approx 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f0e60b75072ecd4168020818c0107f2857bb6c4e64252d8d3983f6263b40a5c3"
 "checksum arc-swap 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)" = "bc4662175ead9cd84451d5c35070517777949a2ed84551764129cedb88384841"
 "checksum argon2rs 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3f67b0b6a86dae6e67ff4ca2b6201396074996379fba2b92ff649126f37cb392"
 "checksum arrayvec 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "92c7fb76bc8826a8b33b4ee5bb07a247a81e76764ab4d55e8f73e3a4d8808c71"
@@ -1309,6 +1345,7 @@ dependencies = [
 "checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14"
 "checksum lexical-core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3f8673fab7063c2cac37d299c8a1a7beb720e78f71500098e4a3c137fdf025bf"
 "checksum libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)" = "6281b86796ba5e4366000be6e9e18bf35580adf9e63fbe2294aadb587613a319"
+"checksum libm 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a"
 "checksum linked-hash-map 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6d262045c5b87c0861b3f004610afd0e2c851e2908d08b6c870cbb9d5f494ecd"
 "checksum linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ae91b68aebc4ddb91978b11a1b02ddd8602a05ec19002801c5666000e05e0f83"
 "checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6"
@@ -1323,6 +1360,7 @@ dependencies = [
 "checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945"
 "checksum nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6"
 "checksum nom 5.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e9761d859320e381010a4f7f8ed425f2c924de33ad121ace447367c713ad561b"
+"checksum num-complex 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "fcb0cf31fb3ff77e6d2a6ebd6800df7fdcd106f2ad89113c9130bcd07f93dffc"
 "checksum num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)" = "b85e541ef8255f6cf42bbfe4ef361305c6c135d10919ecc26126c4e5ae94bc09"
 "checksum num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31"
 "checksum num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32"
diff --git a/Cargo.toml b/Cargo.toml
index f382bc23d7c8..1ac8853f472d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,6 +5,7 @@ authors = ["Griffin Smith <root@gws.fyi>"]
 edition = "2018"
 
 [dependencies]
+alga = "0.9.1"
 backtrace = "0.3"
 clap = {version = "^2.33.0", features = ["yaml"]}
 config = "*"
diff --git a/src/display/draw_box.rs b/src/display/draw_box.rs
index 5dc1627a298d..3b2b4aaf4f1d 100644
--- a/src/display/draw_box.rs
+++ b/src/display/draw_box.rs
@@ -2,6 +2,7 @@ use crate::display::utils::clone_times;
 use crate::display::utils::times;
 use crate::types::BoundingBox;
 use crate::types::Dimensions;
+use crate::types::Neighbors;
 use itertools::Itertools;
 use proptest::prelude::Arbitrary;
 use proptest::strategy;
@@ -22,42 +23,50 @@ use std::io::{self, Write};
 static BOX: char = '☐';
 
 static BOX_CHARS: [[char; 16]; 8] = [
+    // 0
     [
         // 0    1    2    3    4    5    6    7    8    9
         '─', '━', '│', '┃', '┄', '┅', '┆', '┇', '┈', '┉',
         // 10
         '┊', '┋', '┌', '┍', '┎', '┏',
     ],
+    // 1
     [
         // 0    1    2    3    4    5    6    7    8    9
         '┐', '┑', '┒', '┓', '└', '┕', '┖', '┗', '┘', '┙',
         '┚', '┛', '├', '┝', '┞', '┟',
     ],
+    // 2
     [
         // 0    1    2    3    4    5    6    7    8    9
         '┠', '┡', '┢', '┣', '┤', '┥', '┦', '┧', '┨', '┩',
         '┪', '┫', '┬', '┭', '┮', '┯',
     ],
+    // 3
     [
         // 0    1    2    3    4    5    6    7    8    9
         '┰', '┱', '┲', '┳', '┴', '┵', '┶', '┷', '┸', '┹',
         '┺', '┻', '┼', '┽', '┾', '┿',
     ],
+    // 4
     [
         // 0    1    2    3    4    5    6    7    8    9
         '╀', '╁', '╂', '╃', '╄', '╅', '╆', '╇', '╈', '╉',
         '╊', '╋', '╌', '╍', '╎', '╏',
     ],
+    // 5
     [
         // 0    1    2    3    4    5    6    7    8    9
         '═', '║', '╒', '╓', '╔', '╕', '╖', '╗', '╘', '╙',
         '╚', '╛', '╜', '╝', '╞', '╟',
     ],
+    // 6
     [
         // 0    1    2    3    4    5    6    7    8    9
         '╠', '╡', '╢', '╣', '╤', '╥', '╦', '╧', '╨', '╩',
         '╪', '╫', '╬', '╭', '╮', '╯',
     ],
+    // 7
     [
         // 0    1    2    3    4    5    6    7    8    9
         '╰', '╱', '╲', '╳', '╴', '╵', '╶', '╷', '╸', '╹',
@@ -85,8 +94,8 @@ impl Arbitrary for BoxStyle {
     }
 }
 
-trait Stylable {
-    fn style(self, style: BoxStyle) -> char;
+pub trait Stylable {
+    fn style(&self, style: BoxStyle) -> char;
 }
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq, Arbitrary)]
@@ -98,7 +107,7 @@ enum Corner {
 }
 
 impl Stylable for Corner {
-    fn style(self, style: BoxStyle) -> char {
+    fn style(&self, style: BoxStyle) -> char {
         use BoxStyle::*;
         use Corner::*;
 
@@ -119,7 +128,7 @@ enum Line {
 }
 
 impl Stylable for Line {
-    fn style(self, style: BoxStyle) -> char {
+    fn style(&self, style: BoxStyle) -> char {
         use BoxStyle::*;
         use Line::*;
         match (self, style) {
@@ -130,6 +139,39 @@ impl Stylable for Line {
     }
 }
 
+impl Stylable for Neighbors<Option<BoxStyle>> {
+    fn style(&self, style: BoxStyle) -> char {
+        use BoxStyle::*;
+        match (self.left, self.right, self.top, self.bottom) {
+            (None, None, None, None) => BOX,
+            (Some(Thin), None, None, None) => BOX_CHARS[7][4],
+            (None, Some(Thin), None, None) => BOX_CHARS[7][6],
+            (None, None, Some(Thin), None) => BOX_CHARS[7][5],
+            (None, None, None, Some(Thin)) => BOX_CHARS[7][7],
+            (Some(Thin), Some(Thin), None, None) => Line::H.style(Thin),
+            (Some(Thin), None, Some(Thin), None) => {
+                Corner::BottomRight.style(Thin)
+            }
+            (Some(Thin), None, None, Some(Thin)) => {
+                Corner::TopRight.style(Thin)
+            }
+            (None, Some(Thin), Some(Thin), None) => {
+                Corner::BottomLeft.style(Thin)
+            }
+            (None, Some(Thin), None, Some(Thin)) => Corner::TopLeft.style(Thin),
+            (None, None, Some(Thin), Some(Thin)) => Line::V.style(Thin),
+            (None, Some(Thin), Some(Thin), Some(Thin)) => BOX_CHARS[1][12],
+            (Some(Thin), None, Some(Thin), Some(Thin)) => BOX_CHARS[2][4],
+            (Some(Thin), Some(Thin), None, Some(Thin)) => BOX_CHARS[2][12],
+            (Some(Thin), Some(Thin), Some(Thin), None) => BOX_CHARS[3][4],
+            (Some(Thin), Some(Thin), Some(Thin), Some(Thin)) => {
+                BOX_CHARS[3][12]
+            }
+            neighs => panic!("unimplemented: {:?}", neighs),
+        }
+    }
+}
+
 #[must_use]
 pub fn make_box(style: BoxStyle, dims: Dimensions) -> String {
     if dims.h == 0 || dims.w == 0 {
diff --git a/src/display/mod.rs b/src/display/mod.rs
index 3e30200ac723..10690284f126 100644
--- a/src/display/mod.rs
+++ b/src/display/mod.rs
@@ -2,6 +2,8 @@ pub mod color;
 pub mod draw_box;
 pub mod utils;
 pub mod viewport;
+use crate::entities::entity::Entity;
+use crate::types::Neighbors;
 use crate::types::Positioned;
 pub use draw_box::{make_box, BoxStyle};
 use std::io::{self, Write};
@@ -29,3 +31,21 @@ impl<T: Draw> Draw for Box<T> {
         (**self).do_draw(out)
     }
 }
+
+pub trait DrawWithNeighbors: Positioned {
+    fn do_draw_with_neighbors<'a, 'b>(
+        &'a self,
+        out: &'b mut Write,
+        neighbors: &'a Neighbors<Vec<&'a Box<dyn Entity>>>,
+    ) -> io::Result<()>;
+}
+
+impl<T: Draw> DrawWithNeighbors for T {
+    fn do_draw_with_neighbors<'a, 'b>(
+        &'a self,
+        out: &'b mut Write,
+        _neighbors: &'a Neighbors<Vec<&'a Box<dyn Entity>>>,
+    ) -> io::Result<()> {
+        self.do_draw(out)
+    }
+}
diff --git a/src/display/viewport.rs b/src/display/viewport.rs
index 372c0a2969d5..9ff7db07be20 100644
--- a/src/display/viewport.rs
+++ b/src/display/viewport.rs
@@ -1,7 +1,9 @@
 use super::BoxStyle;
-use super::Draw;
+use super::DrawWithNeighbors;
 use crate::display::draw_box::draw_box;
 use crate::display::utils::clone_times;
+use crate::entities::entity::Entity;
+use crate::types::Neighbors;
 use crate::types::{pos, BoundingBox, Direction, Position, Positioned};
 use std::fmt::{self, Debug};
 use std::io::{self, Write};
@@ -77,12 +79,16 @@ impl<W> Debug for Viewport<W> {
 
 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<()> {
+    pub fn draw<'a, T: DrawWithNeighbors>(
+        &mut self,
+        entity: &T,
+        neighbors: &Neighbors<Vec<&Box<dyn Entity>>>,
+    ) -> io::Result<()> {
         if !self.visible(entity) {
             return Ok(());
         }
         self.cursor_goto(entity.position())?;
-        entity.do_draw(self)?;
+        entity.do_draw_with_neighbors(self, neighbors)?;
         self.reset_cursor()
     }
 
diff --git a/src/entities/entity.rs b/src/entities/entity.rs
index 30f7ea9a3dae..7fedb77b2562 100644
--- a/src/entities/entity.rs
+++ b/src/entities/entity.rs
@@ -1,5 +1,6 @@
-use crate::display::Draw;
+use crate::display::DrawWithNeighbors;
 use crate::entities::EntityID;
+use crate::types::Neighbors;
 use crate::types::{Positioned, PositionedMut};
 use downcast_rs::Downcast;
 use std::fmt::Debug;
@@ -37,7 +38,7 @@ impl<ID, A: Identified<ID>> Identified<ID> for Box<A> {
 }
 
 pub trait Entity:
-    Positioned + PositionedMut + Identified<EntityID> + Draw + Downcast
+    Positioned + PositionedMut + Identified<EntityID> + DrawWithNeighbors + Downcast
 {
 }
 
@@ -52,10 +53,10 @@ impl Identified<EntityID> for Box<dyn Entity> {
 
 #[macro_export]
 macro_rules! identified {
-    ($name: ident, $typ: ident) => {
+    ($name: ident, $typ: path) => {
         identified!($name, $typ, id);
     };
-    ($name: ident, $typ: ident, $attr: ident) => {
+    ($name: ident, $typ: path, $attr: ident) => {
         impl crate::entities::entity::Identified<$typ> for $name {
             fn opt_id(&self) -> Option<$typ> {
                 self.$attr
@@ -68,20 +69,14 @@ macro_rules! identified {
     };
 }
 
-#[macro_export]
-macro_rules! entity {
-    ($name: ident) => {
-        positioned!($name);
-        positioned_mut!($name);
-        identified!($name, EntityID);
-        impl crate::entities::entity::Entity for $name {}
-    };
-}
-
 impl_downcast!(Entity);
 
-impl Draw for Box<dyn Entity> {
-    fn do_draw(&self, out: &mut Write) -> io::Result<()> {
-        (**self).do_draw(out)
+impl DrawWithNeighbors for Box<dyn Entity> {
+    fn do_draw_with_neighbors<'a, 'b>(
+        &'a self,
+        out: &'b mut Write,
+        neighbors: &'a Neighbors<Vec<&'a Box<dyn Entity>>>,
+    ) -> io::Result<()> {
+        (**self).do_draw_with_neighbors(out, neighbors)
     }
 }
diff --git a/src/entities/environment.rs b/src/entities/environment.rs
new file mode 100644
index 000000000000..64366a505496
--- /dev/null
+++ b/src/entities/environment.rs
@@ -0,0 +1,34 @@
+use crate::display;
+use crate::display::draw_box::{BoxStyle, Stylable};
+use crate::entities::Entity;
+use crate::types::{Neighbors, Position};
+use std::io::{self, Write};
+
+entity! {
+    pub struct Wall {
+        pub style: BoxStyle
+    }
+}
+
+impl Wall {
+    pub fn new(position: Position, style: BoxStyle) -> Self {
+        new_entity!(Wall { position, style })
+    }
+}
+
+impl display::DrawWithNeighbors for Wall {
+    fn do_draw_with_neighbors<'a, 'b>(
+        &'a self,
+        out: &'b mut Write,
+        neighbors: &'a Neighbors<Vec<&'a Box<dyn Entity>>>,
+    ) -> io::Result<()> {
+        let neighbor_styles: Neighbors<Option<BoxStyle>> =
+            neighbors.map(|es| {
+                es.iter()
+                    .filter_map(|e| e.downcast_ref::<Wall>())
+                    .map(|wall| wall.style)
+                    .next()
+            });
+        write!(out, "{}", neighbor_styles.style(self.style))
+    }
+}
diff --git a/src/entities/mod.rs b/src/entities/mod.rs
index c54a587e6aba..3fe84c76f8ef 100644
--- a/src/entities/mod.rs
+++ b/src/entities/mod.rs
@@ -1,8 +1,11 @@
 #[macro_use]
 pub mod entity;
+#[macro_use]
+pub mod util;
 pub mod character;
 pub mod creature;
 pub mod entity_char;
+pub mod environment;
 pub mod item;
 pub mod raw_types;
 pub mod raws;
diff --git a/src/entities/util.rs b/src/entities/util.rs
new file mode 100644
index 000000000000..6c11ffadf994
--- /dev/null
+++ b/src/entities/util.rs
@@ -0,0 +1,72 @@
+#[macro_export]
+macro_rules! new_entity {
+    ($name: ident) => {
+        new_entity!($name, {})
+    };
+
+    ($name: ident { position: $position:expr $(, $fields:tt)* }) => {
+        $name {
+            id: None,
+            position: $position,
+            $($fields)*
+        }
+    };
+
+    ($name: ident { $position:expr $(, $fields:tt)* }) => {
+        $name {
+            id: None,
+            position: $position,
+            $($fields)*
+        }
+    };
+}
+
+#[macro_export]
+macro_rules! boring_entity {
+    ($name:ident) => {
+        entity! {
+            pub struct $name {}
+        }
+
+        impl $name {
+            #[allow(dead_code)]
+            pub fn new(position: $crate::types::Position) -> Self {
+                $name { id: None, position }
+            }
+        }
+    };
+
+    ($name:ident, char: $char: expr) => {
+        boring_entity!($name);
+
+        impl $crate::display::Draw for $name {
+            fn do_draw(&self, out: &mut Write) -> io::Result<()> {
+                write!(out, "{}", $char)
+            }
+        }
+    };
+}
+
+#[macro_export]
+macro_rules! entity {
+    ($name: ident) => {
+        positioned!($name);
+        positioned_mut!($name);
+        identified!($name, $crate::entities::EntityID);
+        impl $crate::entities::entity::Entity for $name {}
+    };
+
+    (pub struct $name:ident { $($struct_contents:tt)* } $($rest:tt)*) => {
+        #[derive(Debug, PartialEq, Eq, Clone)]
+        pub struct $name {
+            pub id: Option<$crate::entities::EntityID>,
+            pub position: $crate::types::Position,
+            $($struct_contents)*
+        }
+
+        entity!($name);
+        entity!($($rest)*);
+    };
+
+    () => {};
+}
diff --git a/src/game.rs b/src/game.rs
index 49068361b5d1..eee0b7c0d513 100644
--- a/src/game.rs
+++ b/src/game.rs
@@ -24,15 +24,15 @@ type Stdout<'a> = RawTerminal<StdoutLock<'a>>;
 
 type Rng = SmallRng;
 
-type AnEntity<'a> = Box<dyn Entity>;
+type AnEntity = Box<dyn Entity>;
 
-impl<'a> Positioned for AnEntity<'a> {
+impl Positioned for AnEntity {
     fn position(&self) -> Position {
         (**self).position()
     }
 }
 
-impl<'a> PositionedMut for AnEntity<'a> {
+impl PositionedMut for AnEntity {
     fn set_position(&mut self, pos: Position) {
         (**self).set_position(pos)
     }
@@ -120,7 +120,7 @@ pub struct Game<'a> {
     input_state: InputState,
 
     /// The map of all the entities in the game
-    entities: EntityMap<AnEntity<'a>>,
+    entities: EntityMap<AnEntity>,
 
     /// The entity ID of the player character
     character_entity_id: EntityID,
@@ -151,7 +151,7 @@ impl<'a> Game<'a> {
             Some(seed) => SmallRng::seed_from_u64(seed),
             None => SmallRng::from_entropy(),
         };
-        let mut entities: EntityMap<AnEntity<'a>> = EntityMap::new();
+        let mut entities: EntityMap<AnEntity> = EntityMap::new();
 
         // TODO make this dynamic
         {
@@ -219,11 +219,27 @@ impl<'a> Game<'a> {
     /// Draw all the game entities to the screen
     fn draw_entities(&mut self) -> io::Result<()> {
         for entity in self.entities.entities() {
-            self.viewport.draw(entity)?;
+            self.viewport.draw(
+                entity,
+                &self.entities.neighbor_entities(entity.position()),
+            )?;
         }
         Ok(())
     }
 
+    /// Draw the game entity with the given ID, if any, to the screen
+    fn draw_entity(&mut self, entity_id: EntityID) -> io::Result<bool> {
+        if let Some(entity) = self.entities.get(entity_id) {
+            self.viewport.draw(
+                entity,
+                &self.entities.neighbor_entities(entity.position()),
+            )?;
+            Ok(true)
+        } else {
+            Ok(false)
+        }
+    }
+
     /// Remove the given entity from the game, drawing over it if it's visible
     fn remove_entity(&mut self, entity_id: EntityID) -> io::Result<()> {
         if let Some(entity) = self.entities.remove(entity_id) {
@@ -418,19 +434,17 @@ impl<'a> Game<'a> {
 
                     match old_position {
                         Some(old_pos) => {
+                            let character = self.character();
+                            self.viewport.game_cursor_position =
+                                character.position;
+                            self.viewport.clear(old_pos)?;
+                            self.draw_entity(self.character_entity_id)?;
                             self.tick(
                                 self.character().speed().tiles_to_ticks(
                                     (old_pos - self.character().position)
                                         .as_tiles(),
                                 ),
                             );
-                            self.viewport.clear(old_pos)?;
-                            self.viewport.game_cursor_position =
-                                self.character().position;
-                            self.viewport.draw(
-                                // TODO this clone feels unnecessary.
-                                &self.character().clone(),
-                            )?;
                         }
                         None => (),
                     }
diff --git a/src/level_gen/cave_automata.rs b/src/level_gen/cave_automata.rs
index 6a237c0303df..de584f4111ab 100644
--- a/src/level_gen/cave_automata.rs
+++ b/src/level_gen/cave_automata.rs
@@ -1,3 +1,4 @@
+use crate::level_gen::util::fill_outer_edges;
 use crate::level_gen::util::rand_initialize;
 use crate::types::Dimensions;
 use rand::Rng;
@@ -61,6 +62,9 @@ pub fn generate<R: Rng + ?Sized>(
     for _ in 0..params.steps {
         step_automata(&mut cells, dimensions, params);
     }
+
+    fill_outer_edges(&mut cells);
+
     cells
 }
 
diff --git a/src/level_gen/display.rs b/src/level_gen/display.rs
deleted file mode 100644
index 4472bf4fe392..000000000000
--- a/src/level_gen/display.rs
+++ /dev/null
@@ -1,17 +0,0 @@
-use std::io::{self, Write};
-
-pub fn print_generated_level<W>(
-    level: &Vec<Vec<bool>>,
-    out: &mut W,
-) -> io::Result<()>
-where
-    W: Write,
-{
-    for row in level {
-        for cell in row {
-            write!(out, "{}", if *cell { "X" } else { " " })?;
-        }
-        write!(out, "\n")?;
-    }
-    Ok(())
-}
diff --git a/src/level_gen/mod.rs b/src/level_gen/mod.rs
index 4df57a408fa8..df742bb3a1b0 100644
--- a/src/level_gen/mod.rs
+++ b/src/level_gen/mod.rs
@@ -1,3 +1,101 @@
+use crate::display::draw_box::BoxStyle;
+use crate::display::utils::clone_times;
+use crate::display::DrawWithNeighbors;
+use crate::entities::entity::Entity;
+use crate::entities::environment::Wall;
+use crate::types::entity_map::EntityMap;
+use crate::types::pos;
+use itertools::Itertools;
+use std::io;
+
 pub mod cave_automata;
-pub mod display;
 pub mod util;
+
+pub fn level_to_entities(level: Vec<Vec<bool>>) -> EntityMap<Box<dyn Entity>> {
+    let mut res: EntityMap<Box<dyn Entity>> = EntityMap::new();
+
+    let xmax = level.len() as i16;
+    let ymax = if xmax == 0 {
+        0i16
+    } else {
+        level[0].len() as i16
+    };
+
+    let get = |mut x: i16, mut y: i16| {
+        if x < 0 {
+            x = 0;
+        }
+        if y < 0 {
+            y = 0;
+        }
+        if x >= xmax - 1 {
+            x = xmax - 1;
+        }
+        if y >= ymax - 1 {
+            y = ymax - 1;
+        }
+        level[x as usize][y as usize]
+    };
+
+    for x in 0..xmax {
+        for y in 0..ymax {
+            if get(x, y) {
+                // don't output walls that are surrounded on all 8 sides by
+                // walls
+                if (x == 0 || get(x - 1, y))
+                    && (y == 0 || get(x, y - 1))
+                    && (x == xmax - 1 || get(x + 1, y))
+                    && (y == ymax - 1 || get(x, y + 1))
+                    && ((x == 0 && y == 0) || get(x - 1, y - 1))
+                    && ((x == 0 && y == ymax - 1) || get(x - 1, y + 1))
+                    && ((x == xmax - 1 && y == 0) || get(x + 1, y - 1))
+                    && ((x == xmax - 1 && y == ymax - 1) || get(x + 1, y + 1))
+                {
+                    continue;
+                }
+                res.insert(Box::new(Wall::new(
+                    pos(y as i16, x as i16),
+                    BoxStyle::Thin,
+                )));
+            }
+        }
+    }
+
+    res
+}
+
+pub fn draw_level<W: io::Write>(
+    level: Vec<Vec<bool>>,
+    out: &mut W,
+) -> io::Result<()> {
+    if level.len() == 0 {
+        return Ok(());
+    }
+
+    let mut lines = clone_times::<Vec<char>, Vec<Vec<char>>>(
+        clone_times(' ', level[0].len() as u16),
+        level.len() as u16,
+    );
+
+    let em = level_to_entities(level);
+
+    for entity in em.entities() {
+        let mut buf = Vec::new();
+        entity.do_draw_with_neighbors(
+            &mut buf,
+            &em.neighbor_entities(entity.position()),
+        )?;
+        let buf_s = std::str::from_utf8(&buf).unwrap();
+        if let Some(chr) = buf_s.chars().next() {
+            lines[entity.position().y as usize][entity.position().x as usize] =
+                chr;
+        }
+    }
+
+    let res = lines
+        .iter()
+        .map(|line| line.iter().collect::<String>())
+        .join("\n");
+
+    write!(out, "{}", res)
+}
diff --git a/src/level_gen/util.rs b/src/level_gen/util.rs
index 629292c430fa..c9cd87309257 100644
--- a/src/level_gen/util.rs
+++ b/src/level_gen/util.rs
@@ -31,3 +31,22 @@ pub fn rand_initialize<R: Rng + ?Sized>(
     }
     ret
 }
+
+/// Fill the outer edges of a generated level with walls
+pub fn fill_outer_edges(level: &mut Vec<Vec<bool>>) {
+    let xmax = level.len();
+    if xmax == 0 {
+        return;
+    }
+    let ymax = level[0].len();
+
+    for x in 0..xmax {
+        level[x][0] = true;
+        level[x][ymax - 1] = true;
+    }
+
+    for y in 0..level[0].len() {
+        level[0][y] = true;
+        level[xmax - 1][y] = true;
+    }
+}
diff --git a/src/main.rs b/src/main.rs
index 2f0d1c3ffb05..b322a969a1be 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -95,7 +95,7 @@ fn generate_level<'a, W: io::Write>(
         ),
         Some(gen) => panic!("Unrecognized generator: {}", gen),
     };
-    level_gen::display::print_generated_level(&level, stdout)
+    level_gen::draw_level(level, stdout)
 }
 
 fn main() -> io::Result<()> {
diff --git a/src/types/entity_map.rs b/src/types/entity_map.rs
index 12deaa57a6eb..3a7a982e4654 100644
--- a/src/types/entity_map.rs
+++ b/src/types/entity_map.rs
@@ -1,13 +1,16 @@
 use crate::entities::entity::Identified;
 use crate::entities::EntityID;
+use crate::types::Neighbors;
 use crate::types::Position;
 use crate::types::Positioned;
 use crate::types::PositionedMut;
-use std::collections::hash_map::HashMap;
-use std::collections::BTreeMap;
+use alga::general::{
+    AbstractMagma, AbstractMonoid, AbstractSemigroup, Additive, Identity,
+};
+use std::collections::{BTreeMap, HashMap};
 use std::iter::FromIterator;
 
-#[derive(Debug)]
+#[derive(Debug, PartialEq, Eq, Clone)]
 pub struct EntityMap<A> {
     by_position: BTreeMap<Position, Vec<EntityID>>,
     by_id: HashMap<EntityID, A>,
@@ -127,6 +130,52 @@ impl<A: Positioned + Identified<EntityID>> EntityMap<A> {
             e
         })
     }
+
+    /// Moves all elements from `other` into `Self`, leathing `other` empty.
+    pub fn append(&mut self, other: &mut Self) {
+        self.by_position.append(&mut other.by_position);
+        self.by_id.reserve(other.len());
+        for (k, v) in other.by_id.drain() {
+            self.by_id.insert(k, v);
+        }
+        self.last_id = self.last_id.max(other.last_id);
+        other.last_id = 0;
+    }
+
+    /// Gets all 8 neighbors of the given position.
+    pub fn neighbors<'a>(
+        &'a self,
+        position: Position,
+    ) -> Neighbors<Vec<(EntityID, &'a A)>> {
+        Neighbors::of_position(position)
+            .map(|pos| self.at(*pos))
+            .mapmap(&|e| (e.id(), *e))
+    }
+
+    pub fn neighbor_entities<'a>(
+        &'a self,
+        position: Position,
+    ) -> Neighbors<Vec<&'a A>> {
+        self.neighbors(position).mapmap(&|(_eid, ent)| *ent)
+    }
+}
+
+impl<'a, A: Positioned + Identified<EntityID>> IntoIterator
+    for &'a EntityMap<A>
+{
+    type Item = (&'a EntityID, &'a A);
+    type IntoIter = std::collections::hash_map::Iter<'a, EntityID, A>;
+    fn into_iter(self) -> Self::IntoIter {
+        (&self.by_id).into_iter()
+    }
+}
+
+impl<A: Positioned + Identified<EntityID>> IntoIterator for EntityMap<A> {
+    type Item = (EntityID, A);
+    type IntoIter = std::collections::hash_map::IntoIter<EntityID, A>;
+    fn into_iter(self) -> Self::IntoIter {
+        self.by_id.into_iter()
+    }
 }
 
 impl<A: Positioned + Identified<EntityID>> FromIterator<A> for EntityMap<A> {
@@ -139,6 +188,44 @@ impl<A: Positioned + Identified<EntityID>> FromIterator<A> for EntityMap<A> {
     }
 }
 
+impl<A: Positioned + Identified<EntityID> + Eq + Clone> AbstractMagma<Additive>
+    for EntityMap<A>
+{
+    fn operate(&self, right: &Self) -> Self {
+        let mut by_position = self.by_position.clone();
+        by_position.append(&mut right.by_position.clone());
+
+        let mut by_id = self.by_id.clone();
+        for (k, v) in right.by_id.clone() {
+            by_id.insert(k, v);
+        }
+
+        EntityMap {
+            by_position,
+            by_id,
+            last_id: self.last_id.max(right.last_id),
+        }
+    }
+}
+
+impl<A: Positioned + Identified<EntityID> + Eq + Clone>
+    AbstractSemigroup<Additive> for EntityMap<A>
+{
+}
+
+impl<A: Positioned + Identified<EntityID> + Eq> Identity<Additive>
+    for EntityMap<A>
+{
+    fn identity() -> Self {
+        EntityMap::new()
+    }
+}
+
+impl<A: Positioned + Identified<EntityID> + Eq + Clone> AbstractMonoid<Additive>
+    for EntityMap<A>
+{
+}
+
 impl<A: PositionedMut> EntityMap<A> {
     pub fn update_position(
         &mut self,
@@ -274,5 +361,35 @@ mod tests {
             em.remove_all_at(pos);
             assert_eq!(em.at(pos).len(), 0);
         }
+
+        #[test]
+        fn test_entity_map_semigroup_laws(
+            em1 in gen_entity_map(),
+            em2 in gen_entity_map(),
+            em3 in gen_entity_map(),
+        ) {
+            assert!(AbstractSemigroup::prop_is_associative((em1, em2, em3)));
+        }
+
+        fn test_entity_map_monoid_laws(
+            em in gen_entity_map(),
+        ) {
+            assert!(
+                AbstractMonoid::prop_operating_identity_element_is_noop((em,))
+            );
+        }
+
+        #[test]
+        fn test_entity_map_append(
+            mut target in gen_entity_map(),
+            mut source in gen_entity_map(),
+        ) {
+            let orig_source = source.clone();
+            target.append(&mut source);
+            assert_eq!(source, EntityMap::new());
+            for (eid, e) in orig_source {
+                assert_eq!(target.get(eid), Some(&e))
+            }
+        }
     }
 }
diff --git a/src/types/mod.rs b/src/types/mod.rs
index e656048e873c..95436fc66034 100644
--- a/src/types/mod.rs
+++ b/src/types/mod.rs
@@ -320,8 +320,8 @@ macro_rules! positioned {
         positioned!($name, position);
     };
     ($name:ident, $attr:ident) => {
-        impl crate::types::Positioned for $name {
-            fn position(&self) -> Position {
+        impl $crate::types::Positioned for $name {
+            fn position(&self) -> $crate::types::Position {
                 self.$attr
             }
         }
@@ -335,7 +335,7 @@ macro_rules! positioned_mut {
     };
     ($name:ident, $attr:ident) => {
         impl crate::types::PositionedMut for $name {
-            fn set_position(&mut self, pos: Position) {
+            fn set_position(&mut self, pos: $crate::types::Position) {
                 self.$attr = pos;
             }
         }
@@ -372,6 +372,55 @@ impl Speed {
     }
 }
 
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Arbitrary)]
+pub struct Neighbors<A> {
+    pub top_left: A,
+    pub top: A,
+    pub top_right: A,
+    pub left: A,
+    pub right: A,
+    pub bottom_left: A,
+    pub bottom: A,
+    pub bottom_right: A,
+}
+
+impl Neighbors<Position> {
+    fn of_position(pos: Position) -> Self {
+        Neighbors {
+            top_left: pos + Direction::UpLeft,
+            top: pos + Direction::Up,
+            top_right: pos + Direction::UpRight,
+            left: pos + Direction::Left,
+            right: pos + Direction::Right,
+            bottom_left: pos + Direction::DownLeft,
+            bottom: pos + Direction::Down,
+            bottom_right: pos + Direction::DownRight,
+        }
+    }
+}
+
+impl<A> Neighbors<A> {
+    /// it's a functor, yo
+    pub fn map<B, F: Fn(&A) -> B>(&self, f: F) -> Neighbors<B> {
+        Neighbors {
+            top_left: f(&self.top_left),
+            top: f(&self.top),
+            top_right: f(&self.top_right),
+            left: f(&self.left),
+            right: f(&self.right),
+            bottom_left: f(&self.bottom_left),
+            bottom: f(&self.bottom),
+            bottom_right: f(&self.bottom_right),
+        }
+    }
+}
+
+impl<A> Neighbors<Vec<A>> {
+    pub fn mapmap<B, F: Fn(&A) -> B>(&self, f: &F) -> Neighbors<Vec<B>> {
+        self.map(|xs| xs.iter().map(f).collect())
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;