2 Commits 2847c1bf1e ... 5964aa914b

Author SHA1 Message Date
  Getty Ritter 5964aa914b add visibility algorithm 1 year ago
  Getty Ritter 3e8f222e8c cargo fmt 1 year ago
7 changed files with 450 additions and 62 deletions
  1. 1 1
      carpet/src/board.rs
  2. 4 6
      carpet/src/lib.rs
  3. 72 0
      carpet/src/types.rs
  4. 342 0
      carpet/src/vis.rs
  5. 15 27
      ch2/src/main.rs
  6. 15 27
      ch3/src/main.rs
  7. 1 1
      ch5/src/systems.rs

+ 1 - 1
carpet/src/board.rs

@@ -286,7 +286,7 @@ mod test {
                 let v = vec![$($vec)*];
                 assert!(v.len() == w * h);
                 Board::new_from(w, h, |x, y| {
-                    v[x + y * w]
+                    v.get(x + y * w).unwrap().clone()
                 })
             }
         }

+ 4 - 6
carpet/src/lib.rs

@@ -4,12 +4,14 @@ use ggez::{Context, GameError};
 use specs::WorldExt;
 use std::path::Path;
 
-pub use ggez::input::keyboard::{KeyCode,KeyMods};
+pub use ggez::input::keyboard::{KeyCode, KeyMods};
 
 mod board;
 mod types;
+mod vis;
 pub use board::{Board, BoardIter};
 pub use types::{Coord, Rect, Size};
+pub use vis::{Viewshed, Visibility};
 
 #[derive(Eq, PartialEq, Debug, Copy, Clone)]
 pub enum Color {
@@ -325,11 +327,7 @@ impl<Idx: Tile + 'static> Game<Idx> {
     }
 
     pub fn run(self) -> ggez::GameResult<()> {
-        let Game {
-            world,
-            ctx,
-            evloop,
-        } = self;
+        let Game { world, ctx, evloop } = self;
         ggez::event::run(ctx, evloop, world)
     }
 

+ 72 - 0
carpet/src/types.rs

@@ -22,6 +22,12 @@ pub struct Coord {
     pub y: usize,
 }
 
+impl Coord {
+    pub fn new(x: usize, y: usize) -> Coord {
+        Coord { x, y }
+    }
+}
+
 impl specs::Component for Coord {
     type Storage = specs::VecStorage<Coord>;
 }
@@ -32,6 +38,72 @@ impl From<[usize; 2]> for Coord {
     }
 }
 
+impl std::ops::Add for Coord {
+    type Output = Self;
+
+    fn add(self, other: Self) -> Self {
+        Coord {
+            x: self.x + other.x,
+            y: self.y + other.y,
+        }
+    }
+}
+
+impl std::ops::Add<usize> for Coord {
+    type Output = Self;
+
+    fn add(self, other: usize) -> Self {
+        Coord {
+            x: self.x + other,
+            y: self.y + other,
+        }
+    }
+}
+
+impl std::ops::Add<isize> for Coord {
+    type Output = Self;
+
+    fn add(self, other: isize) -> Self {
+        Coord {
+            x: (self.x as isize + other) as usize,
+            y: (self.y as isize + other) as usize,
+        }
+    }
+}
+
+impl std::ops::Add<(isize, isize)> for Coord {
+    type Output = Self;
+
+    fn add(self, other: (isize, isize)) -> Self {
+        Coord {
+            x: (self.x as isize + other.0) as usize,
+            y: (self.y as isize + other.1) as usize,
+        }
+    }
+}
+
+impl std::ops::Sub for Coord {
+    type Output = Self;
+
+    fn sub(self, other: Self) -> Self {
+        Coord {
+            x: self.x - other.x,
+            y: self.y - other.y,
+        }
+    }
+}
+
+impl std::ops::Sub<usize> for Coord {
+    type Output = Self;
+
+    fn sub(self, other: usize) -> Self {
+        Coord {
+            x: self.x - other,
+            y: self.y - other,
+        }
+    }
+}
+
 #[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
 pub struct Rect {
     pub origin: Coord,

+ 342 - 0
carpet/src/vis.rs

@@ -0,0 +1,342 @@
+use crate::{Board, Coord};
+
+// This includes an implementation of the visibility algorithm
+// 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)]
+enum Octant {
+    NE,
+    EN,
+    ES,
+    SE,
+    SW,
+    WS,
+    WN,
+    NW,
+}
+
+const ALL_OCTANTS: &[Octant] = &[
+    Octant::NE,
+    Octant::EN,
+    Octant::ES,
+    Octant::SE,
+    Octant::SW,
+    Octant::WS,
+    Octant::WN,
+    Octant::NW,
+];
+
+impl Octant {
+    fn translate(&self, column: usize, row: usize) -> (isize, isize) {
+        let x = column as isize;
+        let y = row as isize;
+        match &self {
+            Octant::NE => (x, -y),
+            Octant::EN => (y, -x),
+            Octant::ES => (y, x),
+            Octant::SE => (x, y),
+            Octant::SW => (-x, y),
+            Octant::WS => (-y, x),
+            Octant::WN => (-y, -x),
+            Octant::NW => (-x, -y),
+        }
+    }
+}
+
+/// A shadow represents a single occluded section along an octant. We
+/// maintain the invariant that both `start` and `end` in the
+/// inclusive range `[0.0, 1.0]` and also that `start < end`.
+#[derive(Debug, PartialEq, Clone, Copy)]
+struct Shadow {
+    start: f32,
+    end: f32,
+}
+
+impl Shadow {
+    fn new(start: f32, end: f32) -> Shadow {
+        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);
+        Shadow::new(top_left, bottom_right)
+    }
+
+    fn contains(&self, other: &Shadow) -> bool {
+        self.start <= other.start && self.end >= other.end
+    }
+}
+
+#[derive(Debug, PartialEq)]
+struct ShadowLine {
+    shadows: Vec<Shadow>,
+}
+
+impl ShadowLine {
+    fn in_shadow(&self, other: &Shadow) -> bool {
+        self.shadows.iter().any(|sh| sh.contains(other))
+    }
+
+    fn is_full_shadow(&self) -> bool {
+        self.shadows.len() == 1 && self.shadows[0].start.eq(&0.0) && self.shadows[0].end.eq(&1.0)
+    }
+
+    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;
+                    break;
+                }
+            }
+            index
+        };
+
+        // find whether there's an overlapping previous and next
+        // shadow segment
+
+        // we gotta check here because `index` is unsigned; if we just
+        // subtracted and `index` was 0 then we'd get a panic.
+        let previous = if index == 0 {
+            None
+        } else {
+            self.shadows
+                .get(index - 1)
+                .filter(|sh| sh.end > other.start)
+        };
+        let next = self.shadows.get(index).filter(|sh| sh.start < other.end);
+
+        match (previous, next) {
+            // two overlapping segments: join them together
+            (Some(_), Some(n)) => {
+                self.shadows[index - 1].end = n.end;
+                self.shadows.remove(index);
+            }
+            // just one overlapping segment: extend the segment in the
+            // appropriate direction
+            (None, Some(_)) => {
+                self.shadows[index].end = other.end;
+            }
+            (Some(_), None) => {
+                self.shadows[index - 1].start = other.start;
+            }
+            // no overlapping segments: add this one
+            (None, None) => {
+                self.shadows.insert(index, other);
+            }
+        }
+    }
+}
+
+pub struct Viewshed<T> {
+    pub vis: Board<Visibility>,
+    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);
+        let blocking = Box::new(blocking);
+        Viewshed { vis, blocking }
+    }
+
+    pub fn calculate_from(&mut self, board: &Board<T>, coord: Coord) {
+        self.set_visibility(coord, true);
+        for octant in ALL_OCTANTS {
+            self.refresh_octant(*octant, board, coord);
+        }
+    }
+
+    fn refresh_octant(&mut self, octant: Octant, board: &Board<T>, coord: Coord) {
+        let mut line = ShadowLine {
+            shadows: Vec::new(),
+        };
+        let mut full_shadow = false;
+
+        for row in 1.. {
+            let pos = coord + octant.translate(row, 0);
+
+            if !board.contains(pos) {
+                break;
+            }
+
+            for col in 0..=row {
+                let pos = coord + octant.translate(row, col);
+
+                if !board.contains(pos) {
+                    break;
+                }
+
+                if full_shadow {
+                    self.set_visibility(pos, false);
+                    continue;
+                }
+
+                let projection = Shadow::project_from(Coord::new(row, col));
+                let visible = !line.in_shadow(&projection);
+                self.set_visibility(pos, visible);
+
+                if visible && (self.blocking)(&board[pos]) {
+                    line.add(projection);
+                    full_shadow = line.is_full_shadow();
+                }
+            }
+        }
+    }
+
+    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, Visibility, Viewshed};
+
+    macro_rules! board_from_vec {
+        ($w:expr, $h:expr; [$($vec:tt)*]) => {
+            {
+                let w = $w;
+                let h = $h;
+                let v = vec![$($vec)*];
+                assert!(v.len() == w * h);
+                Board::new_from(w, h, |x, y| {
+                    v.get(x + y * w).unwrap().clone()
+                })
+            }
+        }
+    }
+
+    const V: Visibility = Visibility::Visible;
+    const U: Visibility = Visibility::Unseen;
+    const S: Visibility = Visibility::Seen;
+
+    #[test]
+    fn add_shadow_line() {
+        let mut line = super::ShadowLine {
+            shadows: Vec::new(),
+        };
+
+        assert_eq!(line.shadows.len(), 0);
+
+        line.add(super::Shadow::new(0.0, 0.6));
+        assert_eq!(line.shadows.len(), 1);
+        assert_eq!(line.shadows[0], super::Shadow::new(0.0, 0.6));
+
+        line.add(super::Shadow::new(0.4, 1.0));
+        assert_eq!(line.shadows.len(), 1);
+        assert_eq!(line.shadows[0], super::Shadow::new(0.0, 1.0));
+    }
+
+    fn to_vis(vis: &super::Board<super::Visibility>) -> String {
+        let mut buf = String::new();
+        for x in 0..vis.width() {
+            for y in 0..vis.height() {
+                buf.push(match vis[(x, y)] {
+                    V => '.',
+                    S => '#',
+                    U => ' ',
+                })
+            }
+            buf.push('\n')
+        }
+        buf
+    }
+
+    #[test]
+    fn simple_room_visibility() {
+        let b: Board<isize> = board_from_vec![
+            5,5;
+            [
+                0, 0, 0, 0, 0,
+                0, 1, 1, 1, 0,
+                0, 1, 0, 1, 0,
+                0, 1, 1, 1, 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<Visibility> = board_from_vec![
+            5,5;
+            [
+                U, U, U, U, U,
+                U, V, V, V, U,
+                U, V, V, V, U,
+                U, V, V, V, U,
+                U, U, U, U, U,
+            ]
+        ];
+        assert_eq!(to_vis(&v.vis), to_vis(&exp));
+    }
+
+    #[test]
+    fn last_room_visible() {
+        let b: Board<isize> = board_from_vec![
+            7,5;
+            [
+                0, 0, 0, 0, 0, 0, 0,
+                0, 1, 1, 1, 1, 1, 0,
+                0, 1, 0, 1, 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(2, 2));
+        let exp: Board<Visibility> = board_from_vec![
+            7,5;
+            [
+                U, U, U, U, 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, U, U,
+            ]
+        ];
+        assert_eq!(v.vis, exp);
+
+
+        v.calculate_from(&b, Coord::new(4, 2));
+        let exp: Board<Visibility> = 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, U, U, U, U,
+            ]
+        ];
+        assert_eq!(v.vis, exp);
+    }
+}

+ 15 - 27
ch2/src/main.rs

@@ -131,33 +131,21 @@ fn main() -> Result<(), GameError> {
             .build();
     }
 
-    game.on_key(
-        (carpet::KeyCode::W, carpet::KeyMods::NONE),
-        |world| {
-            Motion::move_player(world, -1, 0);
-        },
-    );
-
-    game.on_key(
-        (carpet::KeyCode::A, carpet::KeyMods::NONE),
-        |world| {
-            Motion::move_player(world, 0, -1);
-        },
-    );
-
-    game.on_key(
-        (carpet::KeyCode::S, carpet::KeyMods::NONE),
-        |world| {
-            Motion::move_player(world, 1, 0);
-        },
-    );
-
-    game.on_key(
-        (carpet::KeyCode::D, carpet::KeyMods::NONE),
-        |world| {
-            Motion::move_player(world, 0, 1);
-        },
-    );
+    game.on_key((carpet::KeyCode::W, carpet::KeyMods::NONE), |world| {
+        Motion::move_player(world, -1, 0);
+    });
+
+    game.on_key((carpet::KeyCode::A, carpet::KeyMods::NONE), |world| {
+        Motion::move_player(world, 0, -1);
+    });
+
+    game.on_key((carpet::KeyCode::S, carpet::KeyMods::NONE), |world| {
+        Motion::move_player(world, 1, 0);
+    });
+
+    game.on_key((carpet::KeyCode::D, carpet::KeyMods::NONE), |world| {
+        Motion::move_player(world, 0, 1);
+    });
 
     game.run_with_systems(|world| {
         Draw.run_now(&world);

+ 15 - 27
ch3/src/main.rs

@@ -151,33 +151,21 @@ fn main() -> Result<(), GameError> {
         })
         .build();
 
-    game.on_key(
-        (carpet::KeyCode::W, carpet::KeyMods::NONE),
-        |world| {
-            Motion::move_player(world, -1, 0);
-        },
-    );
-
-    game.on_key(
-        (carpet::KeyCode::A, carpet::KeyMods::NONE),
-        |world| {
-            Motion::move_player(world, 0, -1);
-        },
-    );
-
-    game.on_key(
-        (carpet::KeyCode::S, carpet::KeyMods::NONE),
-        |world| {
-            Motion::move_player(world, 1, 0);
-        },
-    );
-
-    game.on_key(
-        (carpet::KeyCode::D, carpet::KeyMods::NONE),
-        |world| {
-            Motion::move_player(world, 0, 1);
-        },
-    );
+    game.on_key((carpet::KeyCode::W, carpet::KeyMods::NONE), |world| {
+        Motion::move_player(world, -1, 0);
+    });
+
+    game.on_key((carpet::KeyCode::A, carpet::KeyMods::NONE), |world| {
+        Motion::move_player(world, 0, -1);
+    });
+
+    game.on_key((carpet::KeyCode::S, carpet::KeyMods::NONE), |world| {
+        Motion::move_player(world, 1, 0);
+    });
+
+    game.on_key((carpet::KeyCode::D, carpet::KeyMods::NONE), |world| {
+        Motion::move_player(world, 0, 1);
+    });
 
     game.run_with_systems(|world| {
         Draw.run_now(&world);

+ 1 - 1
ch5/src/systems.rs

@@ -1,4 +1,4 @@
-use crate::components::{Renderable, Motion};
+use crate::components::{Motion, Renderable};
 use crate::map::Map;
 
 system_impl! {