vis.rs 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. use crate::{Board, Coord};
  2. // This includes an implementation of the visibility algorithm
  3. // described at
  4. // https://journal.stuffwithstuff.com/2015/09/07/what-the-hero-sees/
  5. /// In many roguelikes, there are three possible visibility states:
  6. /// completely unseen (represented as undrawn black squares), actively
  7. /// visible (represented as brightly-drawn squares with visible
  8. /// monsters), and discovered-but-currently-occluded (represented as
  9. /// dim squares without drawn monsters).
  10. #[derive(Debug, PartialEq, Eq, Clone, Copy)]
  11. pub enum Visibility {
  12. /// The square is in view of the current POV
  13. Visible,
  14. /// The square has been viewed before but is not currently visible
  15. Seen,
  16. /// The square has not been viewed before
  17. Unseen,
  18. }
  19. /// The algorithm as described uses a number from 0 to 8 for
  20. /// this. This is rarely used and doesn't get us much safety, but it
  21. /// felt better to me to have it be a proper enum.
  22. #[derive(Copy, Clone)]
  23. enum Octant {
  24. NE,
  25. EN,
  26. ES,
  27. SE,
  28. SW,
  29. WS,
  30. WN,
  31. NW,
  32. }
  33. const ALL_OCTANTS: &[Octant] = &[
  34. Octant::NE,
  35. Octant::EN,
  36. Octant::ES,
  37. Octant::SE,
  38. Octant::SW,
  39. Octant::WS,
  40. Octant::WN,
  41. Octant::NW,
  42. ];
  43. impl Octant {
  44. fn translate(&self, column: usize, row: usize) -> (isize, isize) {
  45. let x = column as isize;
  46. let y = row as isize;
  47. match &self {
  48. Octant::NE => (x, -y),
  49. Octant::EN => (y, -x),
  50. Octant::ES => (y, x),
  51. Octant::SE => (x, y),
  52. Octant::SW => (-x, y),
  53. Octant::WS => (-y, x),
  54. Octant::WN => (-y, -x),
  55. Octant::NW => (-x, -y),
  56. }
  57. }
  58. }
  59. /// A shadow represents a single occluded section along an octant. We
  60. /// maintain the invariant that both `start` and `end` in the
  61. /// inclusive range `[0.0, 1.0]` and also that `start < end`.
  62. #[derive(Debug, PartialEq, Clone, Copy)]
  63. struct Shadow {
  64. start: f32,
  65. end: f32,
  66. }
  67. impl Shadow {
  68. fn new(start: f32, end: f32) -> Shadow {
  69. Shadow { start, end }
  70. }
  71. fn project_from(coord: Coord) -> Shadow {
  72. let Coord { x, y } = coord;
  73. let top_left = y as f32 / (x as f32 + 2.0);
  74. let bottom_right = (y as f32 + 1.0) / (x as f32 + 1.0);
  75. Shadow::new(top_left, bottom_right)
  76. }
  77. fn contains(&self, other: &Shadow) -> bool {
  78. self.start <= other.start && self.end >= other.end
  79. }
  80. }
  81. #[derive(Debug, PartialEq)]
  82. struct ShadowLine {
  83. shadows: Vec<Shadow>,
  84. }
  85. impl ShadowLine {
  86. fn in_shadow(&self, other: &Shadow) -> bool {
  87. self.shadows.iter().any(|sh| sh.contains(other))
  88. }
  89. fn is_full_shadow(&self) -> bool {
  90. self.shadows.len() == 1 && self.shadows[0].start.eq(&0.0) && self.shadows[0].end.eq(&1.0)
  91. }
  92. fn add(&mut self, other: Shadow) {
  93. let index = {
  94. let mut index = 0;
  95. for (i, shadow) in self.shadows.iter().enumerate() {
  96. if shadow.start >= other.start {
  97. index = i;
  98. break;
  99. }
  100. }
  101. index
  102. };
  103. // find whether there's an overlapping previous and next
  104. // shadow segment
  105. // we gotta check here because `index` is unsigned; if we just
  106. // subtracted and `index` was 0 then we'd get a panic.
  107. let previous = if index == 0 {
  108. None
  109. } else {
  110. self.shadows
  111. .get(index - 1)
  112. .filter(|sh| sh.end > other.start)
  113. };
  114. let next = self.shadows.get(index).filter(|sh| sh.start < other.end);
  115. match (previous, next) {
  116. // two overlapping segments: join them together
  117. (Some(_), Some(n)) => {
  118. self.shadows[index - 1].end = n.end;
  119. self.shadows.remove(index);
  120. }
  121. // just one overlapping segment: extend the segment in the
  122. // appropriate direction
  123. (None, Some(_)) => {
  124. self.shadows[index].end = other.end;
  125. }
  126. (Some(_), None) => {
  127. self.shadows[index - 1].start = other.start;
  128. }
  129. // no overlapping segments: add this one
  130. (None, None) => {
  131. self.shadows.insert(index, other);
  132. }
  133. }
  134. }
  135. }
  136. pub struct Viewshed<T> {
  137. pub vis: Board<Visibility>,
  138. blocking: Box<fn(&T) -> bool>,
  139. }
  140. impl<T> Viewshed<T> {
  141. pub fn create(original: &Board<T>, blocking: fn(&T) -> bool) -> Viewshed<T> {
  142. let vis = Board::new_from(original.width(), original.height(), |_, _| {
  143. Visibility::Unseen
  144. });
  145. let blocking = Box::new(blocking);
  146. Viewshed { vis, blocking }
  147. }
  148. pub fn visibility(&self, coord: impl Into<Coord>) -> Visibility {
  149. self.vis[coord.into()]
  150. }
  151. pub fn calculate_from(&mut self, board: &Board<T>, coord: Coord) {
  152. self.set_visibility(coord, true);
  153. for octant in ALL_OCTANTS {
  154. self.refresh_octant(*octant, board, coord);
  155. }
  156. }
  157. fn refresh_octant(&mut self, octant: Octant, board: &Board<T>, coord: Coord) {
  158. let mut line = ShadowLine {
  159. shadows: Vec::new(),
  160. };
  161. let mut full_shadow = false;
  162. for row in 1.. {
  163. let pos = coord + octant.translate(row, 0);
  164. if !board.contains(pos) {
  165. break;
  166. }
  167. for col in 0..=row {
  168. let pos = coord + octant.translate(row, col);
  169. if !board.contains(pos) {
  170. break;
  171. }
  172. if full_shadow {
  173. self.set_visibility(pos, false);
  174. continue;
  175. }
  176. let projection = Shadow::project_from(Coord::new(row, col));
  177. let visible = !line.in_shadow(&projection);
  178. self.set_visibility(pos, visible);
  179. if visible && (self.blocking)(&board[pos]) {
  180. line.add(projection);
  181. full_shadow = line.is_full_shadow();
  182. }
  183. }
  184. }
  185. }
  186. fn set_visibility(&mut self, coord: Coord, visible: bool) {
  187. if visible {
  188. self.vis[coord] = Visibility::Visible;
  189. } else if self.vis[coord] == Visibility::Unseen {
  190. self.vis[coord] = Visibility::Unseen
  191. } else {
  192. self.vis[coord] = Visibility::Seen
  193. }
  194. }
  195. }
  196. #[cfg(test)]
  197. mod test {
  198. use crate::{Board, Coord, Viewshed, Visibility};
  199. macro_rules! board_from_vec {
  200. ($w:expr, $h:expr; [$($vec:tt)*]) => {
  201. {
  202. let w = $w;
  203. let h = $h;
  204. let v = vec![$($vec)*];
  205. assert!(v.len() == w * h);
  206. Board::new_from(w, h, |x, y| {
  207. v.get(x + y * w).unwrap().clone()
  208. })
  209. }
  210. }
  211. }
  212. const V: Visibility = Visibility::Visible;
  213. const U: Visibility = Visibility::Unseen;
  214. const S: Visibility = Visibility::Seen;
  215. #[test]
  216. fn add_shadow_line() {
  217. let mut line = super::ShadowLine {
  218. shadows: Vec::new(),
  219. };
  220. assert_eq!(line.shadows.len(), 0);
  221. line.add(super::Shadow::new(0.0, 0.6));
  222. assert_eq!(line.shadows.len(), 1);
  223. assert_eq!(line.shadows[0], super::Shadow::new(0.0, 0.6));
  224. line.add(super::Shadow::new(0.4, 1.0));
  225. assert_eq!(line.shadows.len(), 1);
  226. assert_eq!(line.shadows[0], super::Shadow::new(0.0, 1.0));
  227. }
  228. fn to_vis(vis: &super::Board<super::Visibility>) -> String {
  229. let mut buf = String::new();
  230. for x in 0..vis.width() {
  231. for y in 0..vis.height() {
  232. buf.push(match vis[(x, y)] {
  233. V => '.',
  234. S => '#',
  235. U => ' ',
  236. })
  237. }
  238. buf.push('\n')
  239. }
  240. buf
  241. }
  242. #[test]
  243. fn simple_room_visibility() {
  244. let b: Board<isize> = board_from_vec![
  245. 5,5;
  246. [
  247. 0, 0, 0, 0, 0,
  248. 0, 1, 1, 1, 0,
  249. 0, 1, 0, 1, 0,
  250. 0, 1, 1, 1, 0,
  251. 0, 0, 0, 0, 0,
  252. ]
  253. ];
  254. let mut v = Viewshed::create(&b, |n| *n == 1);
  255. v.calculate_from(&b, Coord::new(2, 2));
  256. let exp: Board<Visibility> = board_from_vec![
  257. 5,5;
  258. [
  259. U, U, U, U, U,
  260. U, V, V, V, U,
  261. U, V, V, V, U,
  262. U, V, V, V, U,
  263. U, U, U, U, U,
  264. ]
  265. ];
  266. assert_eq!(to_vis(&v.vis), to_vis(&exp));
  267. }
  268. #[test]
  269. fn last_room_visible() {
  270. let b: Board<isize> = board_from_vec![
  271. 7,5;
  272. [
  273. 0, 0, 0, 0, 0, 0, 0,
  274. 0, 1, 1, 1, 1, 1, 0,
  275. 0, 1, 0, 1, 0, 1, 0,
  276. 0, 1, 1, 1, 1, 1, 0,
  277. 0, 0, 0, 0, 0, 0, 0,
  278. ]
  279. ];
  280. let mut v = Viewshed::create(&b, |n| *n == 1);
  281. v.calculate_from(&b, Coord::new(2, 2));
  282. let exp: Board<Visibility> = board_from_vec![
  283. 7,5;
  284. [
  285. U, U, U, U, U, U, U,
  286. U, V, V, V, U, U, U,
  287. U, V, V, V, U, U, U,
  288. U, V, V, V, U, U, U,
  289. U, U, U, U, U, U, U,
  290. ]
  291. ];
  292. assert_eq!(v.vis, exp);
  293. v.calculate_from(&b, Coord::new(4, 2));
  294. let exp: Board<Visibility> = board_from_vec![
  295. 7,5;
  296. [
  297. U, U, U, U, U, U, U,
  298. U, S, S, V, V, V, U,
  299. U, S, S, V, V, V, U,
  300. U, S, S, V, V, V, U,
  301. U, U, U, U, U, U, U,
  302. ]
  303. ];
  304. assert_eq!(v.vis, exp);
  305. }
  306. }