vis.rs 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  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(), |_, _| Visibility::Unseen);
  143. let blocking = Box::new(blocking);
  144. Viewshed { vis, blocking }
  145. }
  146. pub fn calculate_from(&mut self, board: &Board<T>, coord: Coord) {
  147. self.set_visibility(coord, true);
  148. for octant in ALL_OCTANTS {
  149. self.refresh_octant(*octant, board, coord);
  150. }
  151. }
  152. fn refresh_octant(&mut self, octant: Octant, board: &Board<T>, coord: Coord) {
  153. let mut line = ShadowLine {
  154. shadows: Vec::new(),
  155. };
  156. let mut full_shadow = false;
  157. for row in 1.. {
  158. let pos = coord + octant.translate(row, 0);
  159. if !board.contains(pos) {
  160. break;
  161. }
  162. for col in 0..=row {
  163. let pos = coord + octant.translate(row, col);
  164. if !board.contains(pos) {
  165. break;
  166. }
  167. if full_shadow {
  168. self.set_visibility(pos, false);
  169. continue;
  170. }
  171. let projection = Shadow::project_from(Coord::new(row, col));
  172. let visible = !line.in_shadow(&projection);
  173. self.set_visibility(pos, visible);
  174. if visible && (self.blocking)(&board[pos]) {
  175. line.add(projection);
  176. full_shadow = line.is_full_shadow();
  177. }
  178. }
  179. }
  180. }
  181. fn set_visibility(&mut self, coord: Coord, visible: bool) {
  182. if visible {
  183. self.vis[coord] = Visibility::Visible;
  184. } else if self.vis[coord] == Visibility::Unseen {
  185. self.vis[coord] = Visibility::Unseen
  186. } else {
  187. self.vis[coord] = Visibility::Seen
  188. }
  189. }
  190. }
  191. #[cfg(test)]
  192. mod test {
  193. use crate::{Board, Coord, Visibility, Viewshed};
  194. macro_rules! board_from_vec {
  195. ($w:expr, $h:expr; [$($vec:tt)*]) => {
  196. {
  197. let w = $w;
  198. let h = $h;
  199. let v = vec![$($vec)*];
  200. assert!(v.len() == w * h);
  201. Board::new_from(w, h, |x, y| {
  202. v.get(x + y * w).unwrap().clone()
  203. })
  204. }
  205. }
  206. }
  207. const V: Visibility = Visibility::Visible;
  208. const U: Visibility = Visibility::Unseen;
  209. const S: Visibility = Visibility::Seen;
  210. #[test]
  211. fn add_shadow_line() {
  212. let mut line = super::ShadowLine {
  213. shadows: Vec::new(),
  214. };
  215. assert_eq!(line.shadows.len(), 0);
  216. line.add(super::Shadow::new(0.0, 0.6));
  217. assert_eq!(line.shadows.len(), 1);
  218. assert_eq!(line.shadows[0], super::Shadow::new(0.0, 0.6));
  219. line.add(super::Shadow::new(0.4, 1.0));
  220. assert_eq!(line.shadows.len(), 1);
  221. assert_eq!(line.shadows[0], super::Shadow::new(0.0, 1.0));
  222. }
  223. fn to_vis(vis: &super::Board<super::Visibility>) -> String {
  224. let mut buf = String::new();
  225. for x in 0..vis.width() {
  226. for y in 0..vis.height() {
  227. buf.push(match vis[(x, y)] {
  228. V => '.',
  229. S => '#',
  230. U => ' ',
  231. })
  232. }
  233. buf.push('\n')
  234. }
  235. buf
  236. }
  237. #[test]
  238. fn simple_room_visibility() {
  239. let b: Board<isize> = board_from_vec![
  240. 5,5;
  241. [
  242. 0, 0, 0, 0, 0,
  243. 0, 1, 1, 1, 0,
  244. 0, 1, 0, 1, 0,
  245. 0, 1, 1, 1, 0,
  246. 0, 0, 0, 0, 0,
  247. ]
  248. ];
  249. let mut v = Viewshed::create(&b, |n| *n == 1);
  250. v.calculate_from(&b, Coord::new(2, 2));
  251. let exp: Board<Visibility> = board_from_vec![
  252. 5,5;
  253. [
  254. U, U, U, U, U,
  255. U, V, V, V, U,
  256. U, V, V, V, U,
  257. U, V, V, V, U,
  258. U, U, U, U, U,
  259. ]
  260. ];
  261. assert_eq!(to_vis(&v.vis), to_vis(&exp));
  262. }
  263. #[test]
  264. fn last_room_visible() {
  265. let b: Board<isize> = board_from_vec![
  266. 7,5;
  267. [
  268. 0, 0, 0, 0, 0, 0, 0,
  269. 0, 1, 1, 1, 1, 1, 0,
  270. 0, 1, 0, 1, 0, 1, 0,
  271. 0, 1, 1, 1, 1, 1, 0,
  272. 0, 0, 0, 0, 0, 0, 0,
  273. ]
  274. ];
  275. let mut v = Viewshed::create(&b, |n| *n == 1);
  276. v.calculate_from(&b, Coord::new(2, 2));
  277. let exp: Board<Visibility> = board_from_vec![
  278. 7,5;
  279. [
  280. U, U, U, U, U, U, U,
  281. U, V, V, V, U, U, U,
  282. U, V, V, V, U, U, U,
  283. U, V, V, V, U, U, U,
  284. U, U, U, U, U, U, U,
  285. ]
  286. ];
  287. assert_eq!(v.vis, exp);
  288. v.calculate_from(&b, Coord::new(4, 2));
  289. let exp: Board<Visibility> = board_from_vec![
  290. 7,5;
  291. [
  292. U, U, U, U, U, U, U,
  293. U, S, S, V, V, V, U,
  294. U, S, S, V, V, V, U,
  295. U, S, S, V, V, V, U,
  296. U, U, U, U, U, U, U,
  297. ]
  298. ];
  299. assert_eq!(v.vis, exp);
  300. }
  301. }