symmetric_shadowcasting/
shadowcast.rs

1use num_rational::*;
2
3
4/// This Pos type is expected to be generate enough to allow the user
5/// to cast into and out of their own type, such as from the euclid crate.
6pub type Pos = (isize, isize);
7
8
9/// Compute FOV information for a given position using the shadow mapping algorithm.
10///
11/// This uses the is_blocking closure, which checks whether a given position is
12/// blocked (such as by a wall), and is expected to capture some kind of grid
13/// or map from the user.
14///
15/// The mark_visible closure provides the ability to collect visible tiles. This
16/// may push them to a vector (captured in the closure's environment), or
17/// modify a cloned version of the map.
18///
19///
20/// I tried to write a nicer API which would modify the map as a separate user
21/// data, but I can't work out the lifetime annotations.
22pub 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}