1use num_rational::*;
2
3
4pub type Pos = (isize, isize);
7
8
9pub fn compute_fov<F, G>(origin: Pos, is_blocking: &mut F, mark_visible: &mut G)
23 where F: FnMut(Pos) -> bool,
24 G: FnMut(Pos), {
25
26 mark_visible(origin);
27
28 for i in 0..4 {
29 let quadrant = Quadrant::new(Cardinal::from_index(i), origin);
30
31 let first_row = Row::new(1, Rational::new(-1, 1), Rational::new(1, 1));
32
33 scan(first_row, quadrant, is_blocking, mark_visible);
34 }
35}
36
37fn scan<F, G>(row: Row, quadrant: Quadrant, is_blocking: &mut F, mark_visible: &mut G)
38 where F: FnMut(Pos) -> bool,
39 G: FnMut(Pos), {
40 let mut prev_tile = None;
41
42 let mut row = row;
43
44 for tile in row.tiles() {
45 let tile_is_wall = is_blocking(quadrant.transform(tile));
46 let tile_is_floor = !tile_is_wall;
47
48 let prev_is_wall = prev_tile.map_or(false, |prev| is_blocking(quadrant.transform(prev)));
49 let prev_is_floor = prev_tile.map_or(false, |prev| !is_blocking(quadrant.transform(prev)));
50
51 if tile_is_wall || is_symmetric(row, tile) {
52 let pos = quadrant.transform(tile);
53
54 mark_visible(pos);
55 }
56
57 if prev_is_wall && tile_is_floor {
58 row.start_slope = slope(tile);
59 }
60
61 if prev_is_floor && tile_is_wall {
62 let mut next_row = row.next();
63
64 next_row.end_slope = slope(tile);
65
66 scan(next_row, quadrant, is_blocking, mark_visible);
67 }
68
69 prev_tile = Some(tile);
70 }
71
72 if prev_tile.map_or(false, |tile| !is_blocking(quadrant.transform(tile))) {
73 scan(row.next(), quadrant, is_blocking, mark_visible);
74 }
75}
76
77
78
79#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
80enum Cardinal {
81 North,
82 East,
83 South,
84 West,
85}
86
87impl Cardinal {
88 fn from_index(index: usize) -> Cardinal {
89 use Cardinal::*;
90 return [North, East, South, West][index];
91 }
92}
93
94#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
95struct Quadrant {
96 cardinal: Cardinal,
97 ox: isize,
98 oy: isize,
99}
100
101impl Quadrant {
102 fn new(cardinal: Cardinal, origin: Pos) -> Quadrant {
103 return Quadrant { cardinal, ox: origin.0, oy: origin.1 };
104 }
105
106 fn transform(&self, tile: Pos) -> Pos{
107 let (row, col) = tile;
108
109 match self.cardinal {
110 Cardinal::North => {
111 return (self.ox + col, self.oy - row);
112 }
113
114 Cardinal::South => {
115 return (self.ox + col, self.oy + row);
116 }
117
118 Cardinal::East => {
119 return (self.ox + row, self.oy + col);
120 }
121
122 Cardinal::West => {
123 return (self.ox - row, self.oy + col);
124 }
125 }
126 }
127}
128
129#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
130struct Row {
131 depth: isize,
132 start_slope: Rational,
133 end_slope: Rational,
134}
135
136impl Row {
137 fn new(depth: isize, start_slope: Rational, end_slope: Rational) -> Row {
138 return Row { depth, start_slope, end_slope, };
139 }
140
141 fn tiles(&self) -> impl Iterator<Item=Pos> {
142 let depth_times_start = Rational::new(self.depth, 1) * self.start_slope;
143 let depth_times_end = Rational::new(self.depth, 1) * self.end_slope;
144
145 let min_col = round_ties_up(depth_times_start);
146
147 let max_col = round_ties_down(depth_times_end);
148
149 let depth = self.depth;
150
151 return (min_col..=max_col).map(move |col| (depth, col));
152 }
153
154 fn next(&self) -> Row {
155 return Row::new(self.depth + 1, self.start_slope, self.end_slope);
156 }
157}
158
159fn slope(tile: Pos) -> Rational {
160 let (row_depth, col) = tile;
161 return Rational::new(2 * col - 1, 2 * row_depth);
162}
163
164fn is_symmetric(row: Row, tile: Pos) -> bool {
165 let (_row_depth, col) = tile;
166
167 let depth_times_start = Rational::new(row.depth, 1) * row.start_slope;
168 let depth_times_end = Rational::new(row.depth, 1) * row.end_slope;
169
170 let col_rat = Rational::new(col, 1);
171
172 let symmetric = col_rat >= depth_times_start && col_rat <= depth_times_end;
173
174 return symmetric;
175}
176
177fn round_ties_up(n: Rational) -> isize {
178 return (n + Rational::new(1, 2)).floor().to_integer();
179}
180
181fn round_ties_down(n: Rational) -> isize {
182 return (n - Rational::new(1, 2)).ceil().to_integer();
183}
184
185#[cfg(test)]
186fn inside_map<T>(pos: Pos, map: &Vec<Vec<T>>) -> bool {
187 let is_inside = (pos.1 as usize) < map.len() && (pos.0 as usize) < map[0].len();
188 return is_inside;
189}
190
191#[cfg(test)]
192fn matching_visible(expected: Vec<Vec<usize>>, visible: Vec<(isize, isize)>) {
193 for y in 0..expected.len() {
194 for x in 0..expected[0].len() {
195 if visible.contains(&(x as isize, y as isize)) {
196 print!("1");
197 } else {
198 print!("0");
199 }
200 assert_eq!(expected[y][x] == 1, visible.contains(&(x as isize, y as isize)));
201 }
202 println!();
203 }
204}
205
206#[test]
207fn test_expansive_walls() {
208 let origin = (1, 2);
209
210 let tiles = vec!(vec!(1, 1, 1, 1, 1, 1, 1),
211 vec!(1, 0, 0, 0, 0, 0, 1),
212 vec!(1, 0, 0, 0, 0, 0, 1),
213 vec!(1, 1, 1, 1, 1, 1, 1));
214
215 let mut is_blocking = |pos: Pos| {
216 return !inside_map(pos, &tiles) || tiles[pos.1 as usize][pos.0 as usize] == 1;
217 };
218
219 let mut visible = Vec::new();
220 let mut mark_visible = |pos: Pos| {
221 if inside_map(pos, &tiles) && !visible.contains(&pos) {
222 visible.push(pos);
223 }
224 };
225
226 compute_fov(origin, &mut is_blocking, &mut mark_visible);
227
228 let expected = vec!(vec!(1, 1, 1, 1, 1, 1, 1),
229 vec!(1, 1, 1, 1, 1, 1, 1),
230 vec!(1, 1, 1, 1, 1, 1, 1),
231 vec!(1, 1, 1, 1, 1, 1, 1));
232 matching_visible(expected, visible);
233}
234
235
236#[test]
237fn test_expanding_shadows() {
238 let origin = (0, 0);
239
240 let tiles = vec!(vec!(0, 0, 0, 0, 0, 0, 0),
241 vec!(0, 1, 0, 0, 0, 0, 0),
242 vec!(0, 0, 0, 0, 0, 0, 0),
243 vec!(0, 0, 0, 0, 0, 0, 0),
244 vec!(0, 0, 0, 0, 0, 0, 0));
245
246 let mut is_blocking = |pos: Pos| {
247 return !inside_map(pos, &tiles) || tiles[pos.1 as usize][pos.0 as usize] == 1;
248 };
249
250 let mut visible = Vec::new();
251 let mut mark_visible = |pos: Pos| {
252 if inside_map(pos, &tiles) && !visible.contains(&pos) {
253 visible.push(pos);
254 }
255 };
256
257 compute_fov(origin, &mut is_blocking, &mut mark_visible);
258
259 let expected = vec!(vec!(1, 1, 1, 1, 1, 1, 1),
260 vec!(1, 1, 1, 1, 1, 1, 1),
261 vec!(1, 1, 0, 0, 1, 1, 1),
262 vec!(1, 1, 0, 0, 0, 0, 1),
263 vec!(1, 1, 1, 0, 0, 0, 0));
264 matching_visible(expected, visible);
265}
266
267#[test]
268fn test_no_blind_corners() {
269 let origin = (3, 0);
270
271 let tiles = vec!(vec!(0, 0, 0, 0, 0, 0, 0),
272 vec!(1, 1, 1, 1, 0, 0, 0),
273 vec!(0, 0, 0, 1, 0, 0, 0),
274 vec!(0, 0, 0, 1, 0, 0, 0));
275
276 let mut is_blocking = |pos: Pos| {
277 let outside = (pos.1 as usize) >= tiles.len() || (pos.0 as usize) >= tiles[0].len();
278 return outside || tiles[pos.1 as usize][pos.0 as usize] == 1;
279 };
280
281 let mut visible = Vec::new();
282 let mut mark_visible = |pos: Pos| {
283 let outside = (pos.1 as usize) >= tiles.len() || (pos.0 as usize) >= tiles[0].len();
284
285 if !outside && !visible.contains(&pos) {
286 visible.push(pos);
287 }
288 };
289
290 compute_fov(origin, &mut is_blocking, &mut mark_visible);
291
292 let expected = vec!(vec!(1, 1, 1, 1, 1, 1, 1),
293 vec!(1, 1, 1, 1, 1, 1, 1),
294 vec!(0, 0, 0, 0, 1, 1, 1),
295 vec!(0, 0, 0, 0, 0, 1, 1));
296 matching_visible(expected, visible);
297}