|
@@ -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,25 +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>) -> Visibility {
|
|
|
+ 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);
|
|
|
}
|
|
@@ -196,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);
|
|
@@ -211,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)*]) => {
|
|
@@ -241,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() {
|
|
@@ -262,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')
|
|
@@ -291,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,
|
|
@@ -301,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]
|
|
@@ -319,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,
|
|
@@ -329,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);
|
|
|
}
|
|
|
}
|