2 Commits 3627a3c0e0 ... a49ef5b665

Author SHA1 Message Date
  Getty Ritter a49ef5b665 fix visibility mostly 1 year ago
  Getty Ritter fbd38938ed integrate viewshed in badly 1 year ago
5 changed files with 227 additions and 66 deletions
  1. 1 1
      carpet/src/lib.rs
  2. 186 62
      carpet/src/vis.rs
  3. 3 0
      ch5/src/main.rs
  4. 15 0
      ch5/src/map.rs
  5. 22 3
      ch5/src/systems.rs

+ 1 - 1
carpet/src/lib.rs

@@ -11,7 +11,7 @@ mod types;
 mod vis;
 pub use board::{Board, BoardIter};
 pub use types::{Coord, Rect, Size};
-pub use vis::{Viewshed, Visibility};
+pub use vis::Viewshed;
 
 #[derive(Eq, PartialEq, Debug, Copy, Clone)]
 pub enum Color {

+ 186 - 62
carpet/src/vis.rs

@@ -4,25 +4,10 @@ use crate::{Board, Coord};
 // described at
 // https://journal.stuffwithstuff.com/2015/09/07/what-the-hero-sees/
 
-/// In many roguelikes, there are three possible visibility states:
-/// completely unseen (represented as undrawn black squares), actively
-/// visible (represented as brightly-drawn squares with visible
-/// monsters), and discovered-but-currently-occluded (represented as
-/// dim squares without drawn monsters).
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub enum Visibility {
-    /// The square is in view of the current POV
-    Visible,
-    /// The square has been viewed before but is not currently visible
-    Seen,
-    /// The square has not been viewed before
-    Unseen,
-}
-
 /// The algorithm as described uses a number from 0 to 8 for
 /// this. This is rarely used and doesn't get us much safety, but it
 /// felt better to me to have it be a proper enum.
-#[derive(Copy, Clone)]
+#[derive(Debug, Copy, Clone)]
 enum Octant {
     NE,
     EN,
@@ -72,14 +57,17 @@ struct Shadow {
 }
 
 impl Shadow {
-    fn new(start: f32, end: f32) -> Shadow {
+    fn new(a: f32, b: f32) -> Shadow {
+        let start = a.min(b);
+        let end = a.max(b);
         Shadow { start, end }
     }
 
-    fn project_from(coord: Coord) -> Shadow {
-        let Coord { x, y } = coord;
-        let top_left = y as f32 / (x as f32 + 2.0);
-        let bottom_right = (y as f32 + 1.0) / (x as f32 + 1.0);
+    fn project_from(row: usize, col: usize) -> Shadow {
+        let row = row as f32;
+        let col = col as f32;
+        let top_left = col / (row + 2.0);
+        let bottom_right = (col + 1.0) / (row + 1.0);
         Shadow::new(top_left, bottom_right)
     }
 
@@ -105,11 +93,11 @@ impl ShadowLine {
     fn add(&mut self, other: Shadow) {
         let index = {
             let mut index = 0;
-            for (i, shadow) in self.shadows.iter().enumerate() {
-                if shadow.start >= other.start {
-                    index = i;
+            while index < self.shadows.len() {
+                if self.shadows[index].start >= other.start {
                     break;
                 }
+                index += 1;
             }
             index
         };
@@ -126,7 +114,11 @@ impl ShadowLine {
                 .get(index - 1)
                 .filter(|sh| sh.end > other.start)
         };
-        let next = self.shadows.get(index).filter(|sh| sh.start < other.end);
+        let next = if index < self.shadows.len() {
+            None
+        } else {
+            self.shadows.get(index).filter(|sh| sh.start < other.end)
+        };
 
         match (previous, next) {
             // two overlapping segments: join them together
@@ -137,10 +129,10 @@ impl ShadowLine {
             // just one overlapping segment: extend the segment in the
             // appropriate direction
             (None, Some(_)) => {
-                self.shadows[index].end = other.end;
+                self.shadows[index].start = other.start;
             }
             (Some(_), None) => {
-                self.shadows[index - 1].start = other.start;
+                self.shadows[index - 1].end = other.end;
             }
             // no overlapping segments: add this one
             (None, None) => {
@@ -151,21 +143,29 @@ impl ShadowLine {
 }
 
 pub struct Viewshed<T> {
-    pub vis: Board<Visibility>,
+    pub vis: Board<bool>,
     blocking: Box<fn(&T) -> bool>,
 }
 
 impl<T> Viewshed<T> {
     pub fn create(original: &Board<T>, blocking: fn(&T) -> bool) -> Viewshed<T> {
         let vis = Board::new_from(original.width(), original.height(), |_, _| {
-            Visibility::Unseen
+            false
         });
         let blocking = Box::new(blocking);
         Viewshed { vis, blocking }
     }
 
+    pub fn visibility(&self, coord: impl Into<Coord>) -> bool {
+        self.vis[coord.into()]
+    }
+
     pub fn calculate_from(&mut self, board: &Board<T>, coord: Coord) {
-        self.set_visibility(coord, true);
+        for (_, _, elem) in self.vis.iter_mut() {
+            *elem = false;
+        }
+
+        self.vis[coord] = true;
         for octant in ALL_OCTANTS {
             self.refresh_octant(*octant, board, coord);
         }
@@ -192,13 +192,13 @@ impl<T> Viewshed<T> {
                 }
 
                 if full_shadow {
-                    self.set_visibility(pos, false);
+                    self.vis[pos] = false;
                     continue;
                 }
 
-                let projection = Shadow::project_from(Coord::new(row, col));
+                let projection = Shadow::project_from(row, col);
                 let visible = !line.in_shadow(&projection);
-                self.set_visibility(pos, visible);
+                self.vis[pos] = visible;
 
                 if visible && (self.blocking)(&board[pos]) {
                     line.add(projection);
@@ -207,21 +207,11 @@ impl<T> Viewshed<T> {
             }
         }
     }
-
-    fn set_visibility(&mut self, coord: Coord, visible: bool) {
-        if visible {
-            self.vis[coord] = Visibility::Visible;
-        } else if self.vis[coord] == Visibility::Unseen {
-            self.vis[coord] = Visibility::Unseen
-        } else {
-            self.vis[coord] = Visibility::Seen
-        }
-    }
 }
 
 #[cfg(test)]
 mod test {
-    use crate::{Board, Coord, Viewshed, Visibility};
+    use crate::{Board, Coord, Viewshed};
 
     macro_rules! board_from_vec {
         ($w:expr, $h:expr; [$($vec:tt)*]) => {
@@ -237,9 +227,14 @@ mod test {
         }
     }
 
-    const V: Visibility = Visibility::Visible;
-    const U: Visibility = Visibility::Unseen;
-    const S: Visibility = Visibility::Seen;
+    const V: bool = true;
+    const U: bool = false;
+
+    fn assert_same_vis(exp: &Board<bool>, actual: &Board<bool>) {
+        if exp != actual {
+            panic!("Expected:\n{}\n========\nActual:\n{}\n", to_vis(exp), to_vis(actual));
+        }
+    }
 
     #[test]
     fn add_shadow_line() {
@@ -258,14 +253,49 @@ mod test {
         assert_eq!(line.shadows[0], super::Shadow::new(0.0, 1.0));
     }
 
-    fn to_vis(vis: &super::Board<super::Visibility>) -> String {
+    #[test]
+    fn add_shadow_line_after() {
+        let mut line = super::ShadowLine {
+            shadows: Vec::new(),
+        };
+
+        assert_eq!(line.shadows.len(), 0);
+
+        line.add(super::Shadow::new(0.0, 0.1));
+        assert_eq!(line.shadows.len(), 1);
+        assert_eq!(line.shadows[0], super::Shadow::new(0.0, 0.1));
+
+        line.add(super::Shadow::new(0.9, 1.0));
+        assert_eq!(line.shadows.len(), 2);
+        assert_eq!(line.shadows[0], super::Shadow::new(0.0, 0.1));
+        assert_eq!(line.shadows[1], super::Shadow::new(0.9, 1.0));
+    }
+
+    #[test]
+    fn add_shadow_line_before() {
+        let mut line = super::ShadowLine {
+            shadows: Vec::new(),
+        };
+
+        assert_eq!(line.shadows.len(), 0);
+
+        line.add(super::Shadow::new(0.9, 1.0));
+        assert_eq!(line.shadows.len(), 1);
+        assert_eq!(line.shadows[0], super::Shadow::new(0.9, 1.0));
+
+        line.add(super::Shadow::new(0.0, 0.1));
+        assert_eq!(line.shadows.len(), 2);
+        assert_eq!(line.shadows[0], super::Shadow::new(0.0, 0.1));
+        assert_eq!(line.shadows[1], super::Shadow::new(0.9, 1.0));
+    }
+
+    fn to_vis(vis: &super::Board<bool>) -> String {
         let mut buf = String::new();
-        for x in 0..vis.width() {
-            for y in 0..vis.height() {
+        for y in 0..vis.height() {
+            for x in 0..vis.width() {
                 buf.push(match vis[(x, y)] {
-                    V => '.',
-                    S => '#',
-                    U => ' ',
+                    true  => '#',
+                    false => '.',
                 })
             }
             buf.push('\n')
@@ -287,7 +317,7 @@ mod test {
         ];
         let mut v = Viewshed::create(&b, |n| *n == 1);
         v.calculate_from(&b, Coord::new(2, 2));
-        let exp: Board<Visibility> = board_from_vec![
+        let exp: Board<bool> = board_from_vec![
             5,5;
             [
                 U, U, U, U, U,
@@ -297,7 +327,69 @@ mod test {
                 U, U, U, U, U,
             ]
         ];
-        assert_eq!(to_vis(&v.vis), to_vis(&exp));
+        assert_same_vis(&exp, &v.vis);
+    }
+
+    #[test]
+    fn big_room_visibility() {
+        let b: Board<isize> = board_from_vec![
+            7,7;
+            [
+                0, 0, 0, 0, 0, 0, 0,
+                0, 1, 1, 1, 1, 1, 0,
+                0, 1, 0, 0, 0, 1, 0,
+                0, 1, 0, 0, 0, 1, 0,
+                0, 1, 0, 0, 0, 1, 0,
+                0, 1, 1, 1, 1, 1, 0,
+                0, 0, 0, 0, 0, 0, 0,
+            ]
+        ];
+        let mut v = Viewshed::create(&b, |n| *n == 1);
+        v.calculate_from(&b, Coord::new(3, 3));
+        let exp: Board<bool> = board_from_vec![
+            7,7;
+            [
+                U, U, U, U, U, U, U,
+                U, V, V, V, V, V, U,
+                U, V, V, V, V, V, U,
+                U, V, V, V, V, V, U,
+                U, V, V, V, V, V, U,
+                U, V, V, V, V, V, U,
+                U, U, U, U, U, U, U,
+            ]
+        ];
+        assert_same_vis(&exp, &v.vis);
+    }
+
+    #[test]
+    fn corridor_visibility() {
+        let b: Board<isize> = board_from_vec![
+            13,7;
+            [
+                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0,
+                0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0,
+                0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0,
+                0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0,
+                0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0,
+                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+            ]
+        ];
+        let mut v = Viewshed::create(&b, |n| *n == 1);
+        v.calculate_from(&b, Coord::new(3, 3));
+        let exp: Board<bool> = board_from_vec![
+            13,7;
+            [
+                U, U, U, U, U, U, U, U, U, U, U, U, U,
+                U, V, V, V, V, V, U, U, U, U, U, U, U,
+                U, V, V, V, V, V, V, V, V, V, V, V, U,
+                U, V, V, V, V, V, V, V, V, V, V, V, U,
+                U, V, V, V, V, V, V, V, V, V, V, V, U,
+                U, V, V, V, V, V, U, U, U, U, U, U, U,
+                U, U, U, U, U, U, U, U, U, U, U, U, U,
+            ]
+        ];
+        assert_same_vis(&exp, &v.vis);
     }
 
     #[test]
@@ -315,7 +407,7 @@ mod test {
 
         let mut v = Viewshed::create(&b, |n| *n == 1);
         v.calculate_from(&b, Coord::new(2, 2));
-        let exp: Board<Visibility> = board_from_vec![
+        let exp: Board<bool> = board_from_vec![
             7,5;
             [
                 U, U, U, U, U, U, U,
@@ -325,19 +417,51 @@ mod test {
                 U, U, U, U, U, U, U,
             ]
         ];
-        assert_eq!(v.vis, exp);
+        assert_same_vis(&exp, &v.vis);
 
         v.calculate_from(&b, Coord::new(4, 2));
-        let exp: Board<Visibility> = board_from_vec![
+        let exp: Board<bool> = board_from_vec![
             7,5;
             [
                 U, U, U, U, U, U, U,
-                U, S, S, V, V, V, U,
-                U, S, S, V, V, V, U,
-                U, S, S, V, V, V, U,
+                U, U, U, V, V, V, U,
+                U, U, U, V, V, V, U,
+                U, U, U, V, V, V, U,
                 U, U, U, U, U, U, U,
             ]
         ];
-        assert_eq!(v.vis, exp);
+        assert_same_vis(&exp, &v.vis);
+    }
+
+    #[test]
+    fn long_room_visible() {
+        let b: Board<isize> = board_from_vec![
+            9,7;
+            [
+                0, 0, 0, 0, 0, 0, 0, 0, 0,
+                0, 1, 1, 1, 1, 1, 1, 1, 0,
+                0, 1, 0, 0, 0, 0, 0, 1, 0,
+                0, 1, 0, 0, 0, 0, 0, 1, 0,
+                0, 1, 0, 0, 0, 0, 0, 1, 0,
+                0, 1, 1, 1, 1, 1, 1, 1, 0,
+                0, 0, 0, 0, 0, 0, 0, 0, 0,
+            ]
+        ];
+
+        let mut v = Viewshed::create(&b, |n| *n == 1);
+        v.calculate_from(&b, Coord::new(2, 2));
+        let exp: Board<bool> = board_from_vec![
+            9,7;
+            [
+                U, U, U, U, U, U, U, U, U,
+                U, V, V, V, V, V, V, V, U,
+                U, V, V, V, V, V, V, V, U,
+                U, V, V, V, V, V, V, V, U,
+                U, V, V, V, V, V, V, V, U,
+                U, V, V, V, V, V, V, V, U,
+                U, U, U, U, U, U, U, U, U,
+            ]
+        ];
+        assert_same_vis(&exp, &v.vis);
     }
 }

+ 3 - 0
ch5/src/main.rs

@@ -38,6 +38,8 @@ fn main() -> Result<(), GameError> {
         .first()
         .map(|r| r.center())
         .unwrap_or_else(|| [40, 25].into());
+    let vs = carpet::Viewshed::create(&map.tiles, map::TileType::blocks_view);
+    game.insert(map::Viewshed { vs, dirty: true });
     game.insert(map);
 
     game.create_entity()
@@ -60,6 +62,7 @@ fn main() -> Result<(), GameError> {
     }
 
     game.run_with_systems(|world| {
+        systems::Visibility.run_now(world);
         systems::Draw.run_now(world);
         systems::Move.run_now(world);
     })

+ 15 - 0
ch5/src/map.rs

@@ -1,5 +1,7 @@
 use rand::Rng;
 
+use specs::prelude::*;
+
 #[derive(PartialEq, Clone, Copy, Debug)]
 pub enum TileType {
     Wall,
@@ -13,6 +15,13 @@ impl TileType {
             TileType::Floor => carpet::CP437::from_u8(0),
         }
     }
+
+    pub fn blocks_view(&self) -> bool {
+        match self {
+            TileType::Wall => true,
+            TileType::Floor => false,
+        }
+    }
 }
 
 pub struct Map {
@@ -74,3 +83,9 @@ impl Map {
         Some(&TileType::Floor) == self.tiles.get(x, y)
     }
 }
+
+#[derive(Component)]
+pub struct Viewshed {
+    pub vs: carpet::Viewshed<TileType>,
+    pub dirty: bool,
+}

+ 22 - 3
ch5/src/systems.rs

@@ -1,16 +1,19 @@
-use crate::components::{Motion, Renderable};
-use crate::map::Map;
+use crate::components::{Motion, Player, Renderable};
+use crate::map::{Map, Viewshed};
 
 system_impl! {
     Draw(
         resource mut game_board: carpet::GameBoard<carpet::CP437>,
         resource map: Map,
+        resource vs: Viewshed,
         renderable: Renderable,
         pos: carpet::Coord,
     ) {
         game_board.clear();
         for (x, y, t) in map.tiles.iter() {
-            game_board.set([x, y], t.glyph());
+            if vs.vs.visibility([x, y]) {
+                game_board.set([x, y], t.glyph());
+            }
         }
         for (p, r) in (&pos, &renderable).join() {
             game_board.set_with_color([p.x, p.y], r.glyph, r.color);
@@ -21,12 +24,14 @@ system_impl! {
 system! {
     Move (
         resource map: Map,
+        resource mut viewshed: Viewshed,
         mut motion: Motion,
         mut pos: carpet::Coord,
     ) {
         let tgt_x = (pos.x as i8 + motion.right) as usize;
         let tgt_y = (pos.y as i8 + motion.down) as usize;
         if map.passable((tgt_x, tgt_y)) {
+            viewshed.dirty = true;
             pos.x = tgt_x;
             pos.y = tgt_y;
         }
@@ -34,3 +39,17 @@ system! {
         motion.clear();
     }
 }
+
+system! {
+    Visibility(
+        resource map: Map,
+        resource mut viewshed: Viewshed,
+        pos: carpet::Coord,
+        _player: Player,
+    ) {
+        if viewshed.dirty {
+            viewshed.vs.calculate_from(&map.tiles, *pos);
+            viewshed.dirty = false;
+        }
+    }
+}