Skip to main content

screeps_map_processing/room_connectivity/
exit.rs

1use screeps::{ExitDirection, Terrain, RoomName};
2
3use crate::compressed_terrain::compressed_room_edge_terrain::RoomEdgeTerrain;
4
5
6/// Compact representation of an entire exit along a room edge.
7///
8/// Note: Storing collections of these will not be as efficient as just storing the raw edge
9/// terrain. This structure should be used for when you need to work with and reason about the exit
10/// properties, not for when you need to store all of the exits on an edge. For storing all the
11/// exit data in a compact representation, see [RoomExitsData].
12#[derive(Debug, PartialEq, Clone, Copy)]
13pub struct RoomExit {
14    /// The packed representation of this exit, comprised of a start position and a length, as well
15    /// as an exit direction. The position and length both require 6 bits to store, and the exit
16    /// direction requires 3 bits to store, thus all three can be encoded with a single u16.
17    ///
18    /// Format:
19    /// 0LLLLLLDDDPPPPPP
20    ///
21    /// This allows us to:
22    /// - Get the start coordinate of the exit with a single bitmask operation
23    /// - Get the length with a single bitshift operation
24    /// - Get the exit direction with a combination bitmask and bitshift
25    ///
26    /// The first two are single operations, and thus as fast as possible, while the third is 2
27    /// operations, slightly slower, but still extremely fast. This decision is because we're far
28    /// more likely to want to get the start and length values than we are the exit direction, so
29    /// we should optimize our representation to make those the faster operations.
30    packed: u16,
31}
32
33impl RoomExit {
34    const EXIT_DIRECTION_OFFSET: u16 = 6;
35    const LENGTH_OFFSET: u16 = 9;
36    const START_POSITION_BITMASK: u16 = 0b111111;
37    const EXIT_DIRECTION_BITMASK: u16 = 0b111000000;
38    const EXIT_DIRECTION_INVERTED_BITMASK: u16 = 0b111111000111111;
39
40    /// Creates a new RoomExit from the packed representation.
41    ///
42    /// Note: This will convert the exit direction to ExitDirection::Top if the relevant bits are
43    /// not a valid ExitDirection.
44    pub fn new_from_packed(packed: u16) -> Self {
45        // Safety: Validate the exit direction bits are valid
46        let dir_bits = (packed & Self::EXIT_DIRECTION_BITMASK) >> Self::EXIT_DIRECTION_OFFSET;
47        let final_packed = match dir_bits {
48            1 | 3 | 5 | 7 => packed,
49            _ => (packed & Self::EXIT_DIRECTION_INVERTED_BITMASK) | ((ExitDirection::Top as u16) << Self::EXIT_DIRECTION_OFFSET),
50        };
51
52        Self { packed: final_packed }
53    }
54
55    /// Creates a new RoomExit from the start and length parameters.
56    pub fn new(start: u8, length: u8, direction: ExitDirection) -> Self {
57        let packed = Self::get_packed_from_parameters(start, length, direction);
58
59        Self { packed }
60    }
61
62    /// Helper function to get the packed representation from the start and length parameters.
63    pub fn get_packed_from_parameters(start: u8, length: u8, direction: ExitDirection) -> u16 {
64        let direction_val = direction as u16;
65        ((length as u16) << Self::LENGTH_OFFSET) | (direction_val << Self::EXIT_DIRECTION_OFFSET) | start as u16
66    }
67
68    /// The start position of this exit.
69    pub fn start(&self) -> u8 {
70        // Safety: This is safe to convert, because we're masking out all but the first 6 bits,
71        // which is 2 bits below the u8 limit
72        (self.packed & Self::START_POSITION_BITMASK) as u8
73    }
74
75    /// The length of this exit.
76    pub fn len(&self) -> u8 {
77        // Safety: This is safe to convert, because we're shifting down the most-significant 10
78        // bits, but the most significant 4 bits are always 0, meaning we only have 6 bits of
79        // actual data, which is below the u8 limit
80        (self.packed >> Self::LENGTH_OFFSET) as u8
81    }
82
83    /// The end position of this exit.
84    pub fn end(&self) -> u8 {
85        self.start().saturating_add(self.len()).saturating_sub(1)
86    }
87
88    /// The edge that this exit is on.
89    pub fn exit_direction(&self) -> ExitDirection {
90        let val = (self.packed & Self::EXIT_DIRECTION_BITMASK) >> Self::EXIT_DIRECTION_OFFSET;
91        match val {
92            1 => ExitDirection::Top,
93            3 => ExitDirection::Right,
94            5 => ExitDirection::Bottom,
95            7 => ExitDirection::Left,
96
97            // Safety: We're only extracting values that we set; they shouldn't ever be invalid
98            _ => unreachable!(),
99        }
100    }
101
102    /// The packed representation of this exit.
103    pub fn packed(&self) -> u16 {
104        self.packed
105    }
106
107    /// How much space this exit takes up in memory (in bytes).
108    pub fn memory_size(&self) -> usize {
109        std::mem::size_of::<u16>()
110    }
111
112    /// Extracts the individual exits for each edge from the compressed room edge terrain.
113    ///
114    /// Returned ordering is: Top, Right, Bottom, Left
115    pub fn get_exits_from_edge_terrain(terrain: &RoomEdgeTerrain) -> (Vec<Self>, Vec<Self>, Vec<Self>, Vec<Self>) {
116        let top_terrain = terrain.get_top_edge_terrain();
117        let right_terrain = terrain.get_right_edge_terrain();
118        let bottom_terrain = terrain.get_bottom_edge_terrain();
119        let left_terrain = terrain.get_left_edge_terrain();
120
121        let top_exits = Self::get_exits_from_single_edge(&top_terrain, ExitDirection::Top);
122        let right_exits = Self::get_exits_from_single_edge(&right_terrain, ExitDirection::Right);
123        let bottom_exits = Self::get_exits_from_single_edge(&bottom_terrain, ExitDirection::Bottom);
124        let left_exits = Self::get_exits_from_single_edge(&left_terrain, ExitDirection::Left);
125
126        (top_exits, right_exits, bottom_exits, left_exits)
127    }
128
129    /// Utility function that processes edge terrain into a list of exits.
130    ///
131    /// Returned vector can be empty if the edge is entirely Walls, and thus has no exits.
132    pub fn get_exits_from_single_edge(terrain: &[Terrain; 50], direction: ExitDirection) -> Vec<Self> {
133        let mut exits = Vec::new();
134
135        // These are how we track the current exit that we're processing;
136        // - length will always be non-zero if we're currently processing an exit, and gets reset to
137        //   0 once the exit is finalized and pushed onto the output vector
138        // - start can be any value from 0 to 49; on MMO it won't ever be 0 or 49, but if we're
139        //   using raw terrain data, it can happen
140        let mut current_exit_start = 0;
141        let mut current_exit_length = 0;
142
143        for i in 0..50 {
144            if terrain[i] == Terrain::Wall {
145                // If we've hit a wall, then if we were previously tracking an exit, it's done and
146                // we need to store it
147                if current_exit_length > 0 {
148                    let exit = Self::new(current_exit_start, current_exit_length, direction);
149                    exits.push(exit);
150                    current_exit_start = 0;
151                    current_exit_length = 0;
152                }
153
154                continue;
155            }
156
157            // At this point, we know we're on a exit, so we need to determine if we're on the
158            // first tile of the exit or not
159            let is_new_exit = {
160                if i == 0 {
161                    // If we're on the first tile, then it's guaranteed to be a new exit
162                    true
163                } else {
164                    // If the previous tile was a Wall, then this tile is the start of a new exit
165                    terrain[i-1] == Terrain::Wall
166                }
167            };
168
169            // If we're on a new exit, adjust our tracking variables appropriately
170            if is_new_exit {
171                current_exit_start = i as u8;
172                current_exit_length = 1;
173            } else {
174                current_exit_length += 1;
175            }
176        }
177
178        // Catch a final exit that ends on the 50th tile; this won't happen on MMO, but it could
179        // happen theoretically with raw edge terrain.
180        if current_exit_length > 0 {
181            let exit = Self::new(current_exit_start, current_exit_length, direction);
182            exits.push(exit);
183        }
184
185        exits
186    }
187}
188
189/// Compactly stores information about all the exits in a room.
190#[derive(Debug, Clone, Copy)]
191pub struct RoomExitsData {
192    /// Unfortunately, there really isn't any way to store this better than just 24 raw bytes of
193    /// compressed edge data.
194    data: RoomEdgeTerrain,
195
196    room: RoomName,
197    num_top_exits: usize,
198    num_right_exits: usize,
199    num_bottom_exits: usize,
200    num_left_exits: usize,
201}
202
203impl RoomExitsData {
204    pub fn new_from_compressed_edge_terrain_data(data: RoomEdgeTerrain, room: RoomName) -> Self {
205        let num_top_exits = RoomExit::get_exits_from_single_edge(&data.get_top_edge_terrain(), ExitDirection::Top).len();
206        let num_right_exits = RoomExit::get_exits_from_single_edge(&data.get_right_edge_terrain(), ExitDirection::Right).len();
207        let num_bottom_exits = RoomExit::get_exits_from_single_edge(&data.get_bottom_edge_terrain(), ExitDirection::Bottom).len();
208        let num_left_exits = RoomExit::get_exits_from_single_edge(&data.get_left_edge_terrain(), ExitDirection::Left).len();
209
210        Self {
211            data,
212            room,
213            num_top_exits,
214            num_right_exits,
215            num_bottom_exits,
216            num_left_exits,
217        }
218    }
219
220    /// The amount of memory used to store this data, in bytes.
221    pub fn memory_size(&self) -> usize {
222        self.data.memory_size()
223    }
224
225    /// The exits, if any, along the top edge of the room.
226    pub fn top_edge_exits(&self) -> Vec<RoomExit> {
227        RoomExit::get_exits_from_single_edge(&self.data.get_top_edge_terrain(), ExitDirection::Top)
228    }
229
230    /// The exits, if any, along the right edge of the room.
231    pub fn right_edge_exits(&self) -> Vec<RoomExit> {
232        RoomExit::get_exits_from_single_edge(&self.data.get_right_edge_terrain(), ExitDirection::Right)
233    }
234
235    /// The exits, if any, along the bottom edge of the room.
236    pub fn bottom_edge_exits(&self) -> Vec<RoomExit> {
237        RoomExit::get_exits_from_single_edge(&self.data.get_bottom_edge_terrain(), ExitDirection::Bottom)
238    }
239
240    /// The exits, if any, along the left edge of the room.
241    pub fn left_edge_exits(&self) -> Vec<RoomExit> {
242        RoomExit::get_exits_from_single_edge(&self.data.get_left_edge_terrain(), ExitDirection::Left)
243    }
244
245    /// The number of exits along the top edge of the room.
246    ///
247    /// This is more efficient than constructing all of the exits, if you just need the exit count.
248    pub fn num_top_exits(&self) -> usize {
249        self.num_top_exits
250    }
251
252    /// The number of exits along the right edge of the room.
253    ///
254    /// This is more efficient than constructing all of the exits, if you just need the exit count.
255    pub fn num_right_exits(&self) -> usize {
256        self.num_right_exits
257    }
258
259    /// The number of exits along the bottom edge of the room.
260    ///
261    /// This is more efficient than constructing all of the exits, if you just need the exit count.
262    pub fn num_bottom_exits(&self) -> usize {
263        self.num_bottom_exits
264    }
265
266    /// The number of exits along the left edge of the room.
267    ///
268    /// This is more efficient than constructing all of the exits, if you just need the exit count.
269    pub fn num_left_exits(&self) -> usize {
270        self.num_left_exits
271    }
272
273    /// The total number of exits along all edges of the room.
274    ///
275    /// This is more efficient than constructing all of the exits, if you just need the exit count.
276    pub fn num_exits(&self) -> usize {
277        self.num_top_exits + self.num_right_exits + self.num_bottom_exits + self.num_left_exits
278    }
279
280    /// A reference to the underlying edge terrain data for the room.
281    pub fn edge_terrain_data(&self) -> &RoomEdgeTerrain {
282        &self.data
283    }
284
285    /// Returns true if the top edge has exits and has a neighbor to the north, false otherwise.
286    ///
287    /// This is more efficient than `self.top_edge_exits().len()` if you're just wanting
288    /// connectivity data, as it doesn't create the exits themselves, just checks for their
289    /// existence.
290    pub fn connected_to_top_neighbor(&self) -> bool {
291        (self.num_top_exits > 0) && top_room(self.room).is_some()
292    }
293
294    /// Returns true if the right edge has exits and has a neighbor to the east, false otherwise.
295    ///
296    /// This is more efficient than `self.right_edge_exits().len()` if you're just wanting
297    /// connectivity data, as it doesn't create the exits themselves, just checks for their
298    /// existence.
299    pub fn connected_to_right_neighbor(&self) -> bool {
300        (self.num_right_exits > 0) && right_room(self.room).is_some()
301    }
302
303    /// Returns true if the bottom edge has exits and has a neighbor to the south, false otherwise.
304    ///
305    /// This is more efficient than `self.bottom_edge_exits().len()` if you're just wanting
306    /// connectivity data, as it doesn't create the exits themselves, just checks for their
307    /// existence.
308    pub fn connected_to_bottom_neighbor(&self) -> bool {
309        (self.num_bottom_exits > 0) && bottom_room(self.room).is_some()
310    }
311
312    /// Returns true if the left edge has exits and has a neighbor to the west, false otherwise.
313    ///
314    /// This is more efficient than `self.left_edge_exits().len()` if you're just wanting
315    /// connectivity data, as it doesn't create the exits themselves, just checks for their
316    /// existence.
317    pub fn connected_to_left_neighbor(&self) -> bool {
318        (self.num_left_exits > 0) && left_room(self.room).is_some()
319    }
320
321    /// Returns the room exit identified by iterating through edges clockwise (top, right, bottom,
322    /// left) and then scanning through each edge linearly (left-to-right, top-to-bottom).
323    ///
324    /// Example:
325    /// If a room has no exits along the top edge, but has 2 exits along the right edge, then the
326    /// exit at index 0 would be the exit on the right edge with the lower start position, and the
327    /// exit at index 1 would be the exit on the right edge with the higher start position.
328    ///
329    /// Returns None if the index represents a non-existent exit; this can happen if:
330    /// - There are no exits to the room
331    /// - There are exits to the room, but the index is greater than the number of exits - 1 (since
332    ///   we use a zero-based index)
333    pub fn get_exit_by_index(&self, index: usize) -> Option<RoomExit> {
334        let total_num_exits = self.num_top_exits + self.num_right_exits + self.num_bottom_exits + self.num_left_exits;
335        if total_num_exits == 0 {
336            // If there aren't any exits, no index is valid
337            None
338        } else {
339            let max_index = total_num_exits - 1; // Safety: We know total_num_exits is greater than 0, so this will never underflow
340            if index > max_index {
341                // Index references an exit that doesn't exist
342                None
343            } else {
344                // The index is valid, find the exit
345
346                let min_idx_top = 0;
347                let max_idx_top = if self.num_top_exits > 0 {
348                    min_idx_top + self.num_top_exits - 1
349                } else {
350                    min_idx_top
351                };
352
353                let min_idx_right = if self.num_top_exits > 0 {
354                    max_idx_top + 1
355                } else {
356                    max_idx_top
357                };
358                let max_idx_right = if self.num_right_exits > 0 {
359                    min_idx_right + self.num_right_exits - 1
360                } else {
361                    min_idx_right
362                };
363
364                let min_idx_bottom = if self.num_right_exits > 0 {
365                    max_idx_right + 1
366                } else {
367                    max_idx_right
368                };
369                let max_idx_bottom = if self.num_bottom_exits > 0 {
370                    min_idx_bottom + self.num_bottom_exits - 1
371                } else {
372                    min_idx_bottom
373                };
374
375                let min_idx_left = if self.num_bottom_exits > 0 {
376                    max_idx_bottom + 1
377                } else {
378                    max_idx_bottom
379                };
380                let max_idx_left = if self.num_left_exits > 0 {
381                    min_idx_left + self.num_left_exits - 1
382                } else {
383                    min_idx_left
384                };
385
386                // - If index is greater than the top edge max index, continue onward
387                // - If the index is less than the top edge max index, generate the exits and
388                //   extract the correct one
389                if self.num_top_exits > 0 {
390                    if index <= max_idx_top {
391                        // Index is one of these exits, generate them and return the correct one
392                        let exits = self.top_edge_exits();
393                        let local_index = index - min_idx_top;
394                        return exits.get(local_index).copied();
395                    }
396                }
397
398                // - If index is greater than the right edge max index, continue onward
399                // - If the index is less than the right edge max index, generate the exits and
400                //   extract the correct one
401                if self.num_right_exits > 0 {
402                    if index <= max_idx_right {
403                        // Index is one of these exits, generate them and return the correct one
404                        let exits = self.right_edge_exits();
405                        let local_index = index - min_idx_right;
406                        return exits.get(local_index).copied();
407                    }
408                }
409
410                // - If index is greater than the bottom edge max index, continue onward
411                // - If the index is less than the bottom edge max index, generate the exits and
412                //   extract the correct one
413                if self.num_bottom_exits > 0 {
414                    if index <= max_idx_bottom {
415                        // Index is one of these exits, generate them and return the correct one
416                        let exits = self.bottom_edge_exits();
417                        let local_index = index - min_idx_bottom;
418                        return exits.get(local_index).copied();
419                    }
420                }
421
422                // - If index is greater than the left edge max index, return None (shouldn't ever
423                //   happen, but edge cases are a PITA)
424                // - If the index is less than the left edge max index, generate the exits and
425                //   extract the correct one
426                if self.num_left_exits > 0 {
427                    if index <= max_idx_left {
428                        // Index is one of these exits, generate them and return the correct one
429                        let exits = self.left_edge_exits();
430                        let local_index = index - min_idx_left;
431                        return exits.get(local_index).copied();
432                    }
433                }
434
435                // We should never get here due to pre-checks, but just in case, return None since
436                // we couldn't find the proper exit
437                None
438            }
439        }
440    }
441
442    /// The room this data is for.
443    pub fn room(&self) -> RoomName {
444        self.room
445    }
446
447    /// Returns an iterator over all the exits in the room.
448    pub fn iter(&self) -> RoomExitsIter {
449        RoomExitsIter::new(*self)
450    }
451}
452
453pub struct RoomExitsIter {
454    data: RoomExitsData,
455    current_index: usize,
456    length: usize,
457}
458
459impl RoomExitsIter {
460    fn new(data: RoomExitsData) -> Self {
461        Self {
462            data,
463            current_index: 0,
464            length: data.num_exits(),
465        }
466    }
467}
468
469impl Iterator for RoomExitsIter {
470    type Item = RoomExit;
471
472    fn next(&mut self) -> Option<Self::Item> {
473        if self.current_index >= self.length {
474            None
475        } else {
476            let exit = self.data.get_exit_by_index(self.current_index);
477            self.current_index += 1;
478            exit
479        }
480    }
481}
482
483/// Utility function to return the room above the given room, if it exists.
484pub fn top_room(room: RoomName) -> Option<RoomName> {
485    room.checked_add((0, -1))
486}
487
488/// Utility function to return the room to the right of the given room, if it exists.
489pub fn right_room(room: RoomName) -> Option<RoomName> {
490    room.checked_add((1, 0))
491}
492
493/// Utility function to return the room below the given room, if it exists.
494pub fn bottom_room(room: RoomName) -> Option<RoomName> {
495    room.checked_add((0, 1))
496}
497
498/// Utility function to return the room to the left of the given room, if it exists.
499pub fn left_room(room: RoomName) -> Option<RoomName> {
500    room.checked_add((-1, 0))
501}
502
503#[cfg(test)]
504mod test {
505    use super::*;
506
507    #[test]
508    pub fn room_exit_verify_parameter_packing() {
509        // This is not precisely conformant to MMO, since 0 and 49 are both always Walls. However,
510        // the math should still work out, and this makes the data structure more resilient.
511        let directions = [ExitDirection::Top, ExitDirection::Right, ExitDirection::Bottom, ExitDirection::Left];
512        for start in 0..50 {
513            let max_length = 50 - start;
514            for length in 1..max_length {
515                for direction in directions {
516                    let exit = RoomExit::new(start, length, direction);
517
518                    let packed = exit.packed();
519
520                    let end = start + length - 1; // Since we restrict start and length, this should always be in the range [0, 49], with no over or underflows
521
522                    assert_eq!(exit.start(), start, "Start position unpacking mismatch, ({start}, {length}, {direction:?}), Packed: {packed:b}");
523                    assert_eq!(exit.len(), length, "Length unpacking mismatch, ({start}, {length}, {direction:?}), Packed: {packed:b}");
524                    assert_eq!(exit.end(), end, "End position calculation mismatch, ({start}, {length}, {end}, {direction:?})");
525                    assert_eq!(exit.exit_direction(), direction, "Exit direction calculation mismatch, ({start}, {length}, {direction:?}, Packed: {packed:b})");
526                }
527            }
528        }
529    }
530
531    #[test]
532    pub fn room_exit_new_from_packed_matches_original_data() {
533        let directions = [ExitDirection::Top, ExitDirection::Right, ExitDirection::Bottom, ExitDirection::Left];
534        for start in 0..50 {
535            let max_length = 50 - start;
536            for length in 1..max_length {
537                for direction in directions {
538                    let exit = RoomExit::new(start, length, direction);
539
540                    let packed = exit.packed();
541
542                    let new_exit = RoomExit::new_from_packed(packed);
543
544                    let end = start + length - 1; // Since we restrict start and length, this should always be in the range [0, 49], with no over or underflows
545
546                    assert_eq!(exit.start(), new_exit.start(), "Start position mismatch, ({start}, {length}, {direction:?}), Packed: {packed:b}");
547                    assert_eq!(exit.len(), new_exit.len(), "Length mismatch, ({start}, {length}, {direction:?}), Packed: {packed:b}");
548                    assert_eq!(exit.end(), new_exit.end(), "End position mismatch, ({start}, {length}, {end}, {direction:?})");
549                    assert_eq!(exit.exit_direction(), new_exit.exit_direction(), "Exit direction mismatch, ({start}, {length}, {direction:?}, Packed: {packed:b})");
550                }
551            }
552        }
553    }
554
555    #[test]
556    pub fn room_exit_new_from_packed_sanitizes_invalid_direction_data() {
557        let directions = [ExitDirection::Top, ExitDirection::Right, ExitDirection::Bottom, ExitDirection::Left];
558        let invalid_direction_values: [u16; 4] = [0, 2, 4, 6]; // These are the only invalid values that can fit in 3 bits
559        let direction_include_bitmask = 0b111111000111111;
560        for start in 0..50 {
561            let max_length = 50 - start;
562            for length in 1..max_length {
563                for direction in directions {
564                    for invalid_direction_val in &invalid_direction_values {
565                        let exit = RoomExit::new(start, length, direction);
566
567                        let packed = exit.packed();
568
569                        let invalid_packed = (packed & direction_include_bitmask) | (invalid_direction_val << RoomExit::EXIT_DIRECTION_OFFSET);
570
571                        let new_exit = RoomExit::new_from_packed(invalid_packed);
572
573                        let new_packed = new_exit.packed();
574
575                        let sanitized_dir_bits = (new_packed & RoomExit::EXIT_DIRECTION_BITMASK) >> RoomExit::EXIT_DIRECTION_OFFSET;
576
577                        let end = start + length - 1; // Since we restrict start and length, this should always be in the range [0, 49], with no over or underflows
578
579                        assert_eq!(exit.start(), new_exit.start(), "Start position mismatch, ({start}, {length}, {direction:?}), Packed: {new_packed:b}");
580                        assert_eq!(exit.len(), new_exit.len(), "Length mismatch, ({start}, {length}, {direction:?}), Packed: {new_packed:b}");
581                        assert_eq!(exit.end(), new_exit.end(), "End position mismatch, ({start}, {length}, {end}, {direction:?})");
582                        assert_eq!(sanitized_dir_bits, ExitDirection::Top as u16, "Exit direction not sanitized, ({start}, {length}, {direction:?}, Packed: {new_packed:b}, Invalid Packed: {invalid_packed:b})");
583                        assert_eq!(new_exit.exit_direction(), ExitDirection::Top, "Exit direction not sanitized, ({start}, {length}, {direction:?}, Packed: {new_packed:b})");
584                    }
585                }
586            }
587        }
588    }
589
590    #[test]
591    pub fn room_exit_get_exits_from_single_edge_returns_empty_for_all_walls() {
592        let terrain = [Terrain::Wall; 50];
593
594        let exits = RoomExit::get_exits_from_single_edge(&terrain, ExitDirection::Top);
595
596        assert_eq!(exits.len(), 0);
597    }
598
599    #[test]
600    pub fn room_exit_get_exits_from_single_edge_returns_expected_number_of_exits() {
601        // Single giant exit; e.g. highways, crossroads
602        let mut terrain = [Terrain::Plain; 50];
603        terrain[0] = Terrain::Wall;
604        terrain[49] = Terrain::Wall;
605
606        let exits = RoomExit::get_exits_from_single_edge(&terrain, ExitDirection::Top);
607
608        assert_eq!(exits.len(), 1);
609
610        // Two exits
611        let mut terrain = [Terrain::Plain; 50];
612        terrain[0] = Terrain::Wall;
613        terrain[49] = Terrain::Wall;
614
615        for i in 5..10 {
616            terrain[i] = Terrain::Wall;
617        }
618
619        let exits = RoomExit::get_exits_from_single_edge(&terrain, ExitDirection::Top);
620
621        assert_eq!(exits.len(), 2);
622
623        // Three exits
624        let mut terrain = [Terrain::Plain; 50];
625        terrain[0] = Terrain::Wall;
626        terrain[49] = Terrain::Wall;
627
628        for i in 5..10 {
629            terrain[i] = Terrain::Wall;
630        }
631
632        for i in 23..25 {
633            terrain[i] = Terrain::Wall;
634        }
635
636        let exits = RoomExit::get_exits_from_single_edge(&terrain, ExitDirection::Top);
637
638        assert_eq!(exits.len(), 3);
639    }
640
641    #[test]
642    pub fn room_exits_data_get_exit_by_index_returns_none_for_bad_indices() {
643        let room_name = RoomName::new("W0N0").unwrap();
644
645        // Test cases
646        // - No exits at all
647        let edge = [Terrain::Wall; 50];
648        let terrain = RoomEdgeTerrain::new_from_terrain_slices(&edge, &edge, &edge, &edge).unwrap();
649        let exits_data = RoomExitsData::new_from_compressed_edge_terrain_data(terrain, room_name);
650        assert_eq!(exits_data.get_exit_by_index(0), None, "Exit returned when none exists");
651
652        // - Index larger than max possible index for num exits
653        let edge = [Terrain::Plain; 50];
654        let terrain = RoomEdgeTerrain::new_from_terrain_slices(&edge, &edge, &edge, &edge).unwrap();
655        let exits_data = RoomExitsData::new_from_compressed_edge_terrain_data(terrain, room_name);
656        assert_eq!(exits_data.get_exit_by_index(8), None, "Exit returned for invalid index");
657    }
658
659    #[test]
660    pub fn room_exits_data_get_exit_by_index_returns_some_for_exits_on_4_edges() {
661        let room_name = RoomName::new("W0N0").unwrap();
662
663        let edge = [Terrain::Plain; 50];
664        let terrain = RoomEdgeTerrain::new_from_terrain_slices(&edge, &edge, &edge, &edge).unwrap();
665        let exits_data = RoomExitsData::new_from_compressed_edge_terrain_data(terrain, room_name);
666
667        assert_eq!(1, exits_data.num_top_exits(), "Top exit count mismatch");
668        assert_eq!(1, exits_data.num_right_exits(), "Right exit count mismatch");
669        assert_eq!(1, exits_data.num_bottom_exits(), "Bottom exit count mismatch");
670        assert_eq!(1, exits_data.num_left_exits(), "Left exit count mismatch");
671
672        // -- Top edge
673        let exit_opt = exits_data.get_exit_by_index(0);
674        assert!(exit_opt.is_some(), "Exit should exist");
675
676        let exit = exit_opt.unwrap();
677
678        assert_eq!(exit.start(), 1, "Exit start position invalid");
679        assert_eq!(exit.end(), 48, "Exit end position invalid");
680        assert_eq!(exit.len(), 48, "Exit length invalid");
681        assert_eq!(exit.exit_direction(), ExitDirection::Top, "Exit direction invalid");
682
683        // -- Right edge
684        let exit_opt = exits_data.get_exit_by_index(1);
685        assert!(exit_opt.is_some(), "Exit should exist");
686
687        let exit = exit_opt.unwrap();
688
689        assert_eq!(exit.start(), 1, "Exit start position invalid");
690        assert_eq!(exit.end(), 48, "Exit end position invalid");
691        assert_eq!(exit.len(), 48, "Exit length invalid");
692        assert_eq!(exit.exit_direction(), ExitDirection::Right, "Exit direction invalid");
693
694        // -- Bottom edge
695        let exit_opt = exits_data.get_exit_by_index(2);
696        assert!(exit_opt.is_some(), "Exit should exist");
697
698        let exit = exit_opt.unwrap();
699
700        assert_eq!(exit.start(), 1, "Exit start position invalid");
701        assert_eq!(exit.end(), 48, "Exit end position invalid");
702        assert_eq!(exit.len(), 48, "Exit length invalid");
703        assert_eq!(exit.exit_direction(), ExitDirection::Bottom, "Exit direction invalid");
704
705        // -- Left edge
706        let exit_opt = exits_data.get_exit_by_index(3);
707        assert!(exit_opt.is_some(), "Exit should exist");
708
709        let exit = exit_opt.unwrap();
710
711        assert_eq!(exit.start(), 1, "Exit start position invalid");
712        assert_eq!(exit.end(), 48, "Exit end position invalid");
713        assert_eq!(exit.len(), 48, "Exit length invalid");
714        assert_eq!(exit.exit_direction(), ExitDirection::Left, "Exit direction invalid");
715    }
716
717    #[test]
718    pub fn room_exits_data_get_exit_by_index_returns_some_for_exits_except_top_edge() {
719        let room_name = RoomName::new("W0N0").unwrap();
720
721        let wall_edge = [Terrain::Wall; 50];
722        let edge = [Terrain::Plain; 50];
723        let terrain = RoomEdgeTerrain::new_from_terrain_slices(&wall_edge, &edge, &edge, &edge).unwrap();
724        let exits_data = RoomExitsData::new_from_compressed_edge_terrain_data(terrain, room_name);
725
726        assert_eq!(0, exits_data.num_top_exits(), "Top exit count mismatch");
727        assert_eq!(1, exits_data.num_right_exits(), "Right exit count mismatch");
728        assert_eq!(1, exits_data.num_bottom_exits(), "Bottom exit count mismatch");
729        assert_eq!(1, exits_data.num_left_exits(), "Left exit count mismatch");
730
731        // -- First exit should be the right edge exit
732        let exit_opt = exits_data.get_exit_by_index(0);
733        assert!(exit_opt.is_some(), "Exit should exist");
734
735        let exit = exit_opt.unwrap();
736
737        assert_eq!(exit.start(), 1, "Exit start position invalid");
738        assert_eq!(exit.end(), 48, "Exit end position invalid");
739        assert_eq!(exit.len(), 48, "Exit length invalid");
740        assert_eq!(exit.exit_direction(), ExitDirection::Right, "Exit direction invalid");
741
742        // -- Second exit should be the bottom edge exit
743        let exit_opt = exits_data.get_exit_by_index(1);
744        assert!(exit_opt.is_some(), "Exit should exist");
745
746        let exit = exit_opt.unwrap();
747
748        assert_eq!(exit.start(), 1, "Exit start position invalid");
749        assert_eq!(exit.end(), 48, "Exit end position invalid");
750        assert_eq!(exit.len(), 48, "Exit length invalid");
751        assert_eq!(exit.exit_direction(), ExitDirection::Bottom, "Exit direction invalid");
752
753        // -- Third exit should be the left edge exit
754        let exit_opt = exits_data.get_exit_by_index(2);
755        assert!(exit_opt.is_some(), "Exit should exist");
756
757        let exit = exit_opt.unwrap();
758
759        assert_eq!(exit.start(), 1, "Exit start position invalid");
760        assert_eq!(exit.end(), 48, "Exit end position invalid");
761        assert_eq!(exit.len(), 48, "Exit length invalid");
762        assert_eq!(exit.exit_direction(), ExitDirection::Left, "Exit direction invalid");
763    }
764
765    #[test]
766    pub fn room_exits_data_get_exit_by_index_returns_some_for_exits_except_right_edge() {
767        let room_name = RoomName::new("W0N0").unwrap();
768
769        let wall_edge = [Terrain::Wall; 50];
770        let edge = [Terrain::Plain; 50];
771        let terrain = RoomEdgeTerrain::new_from_terrain_slices(&edge, &wall_edge, &edge, &edge).unwrap();
772        let exits_data = RoomExitsData::new_from_compressed_edge_terrain_data(terrain, room_name);
773
774        assert_eq!(1, exits_data.num_top_exits(), "Top exit count mismatch");
775        assert_eq!(0, exits_data.num_right_exits(), "Right exit count mismatch");
776        assert_eq!(1, exits_data.num_bottom_exits(), "Bottom exit count mismatch");
777        assert_eq!(1, exits_data.num_left_exits(), "Left exit count mismatch");
778
779        // -- First exit should be the top edge exit
780        let exit_opt = exits_data.get_exit_by_index(0);
781        assert!(exit_opt.is_some(), "Exit should exist");
782
783        let exit = exit_opt.unwrap();
784
785        assert_eq!(exit.start(), 1, "Exit start position invalid");
786        assert_eq!(exit.end(), 48, "Exit end position invalid");
787        assert_eq!(exit.len(), 48, "Exit length invalid");
788        assert_eq!(exit.exit_direction(), ExitDirection::Top, "Exit direction invalid");
789
790        // -- Second exit should be the bottom edge exit
791        let exit_opt = exits_data.get_exit_by_index(1);
792        assert!(exit_opt.is_some(), "Exit should exist");
793
794        let exit = exit_opt.unwrap();
795
796        assert_eq!(exit.start(), 1, "Exit start position invalid");
797        assert_eq!(exit.end(), 48, "Exit end position invalid");
798        assert_eq!(exit.len(), 48, "Exit length invalid");
799        assert_eq!(exit.exit_direction(), ExitDirection::Bottom, "Exit direction invalid");
800
801        // -- Third exit should be the left edge exit
802        let exit_opt = exits_data.get_exit_by_index(2);
803        assert!(exit_opt.is_some(), "Exit should exist");
804
805        let exit = exit_opt.unwrap();
806
807        assert_eq!(exit.start(), 1, "Exit start position invalid");
808        assert_eq!(exit.end(), 48, "Exit end position invalid");
809        assert_eq!(exit.len(), 48, "Exit length invalid");
810        assert_eq!(exit.exit_direction(), ExitDirection::Left, "Exit direction invalid");
811    }
812
813    // - No bottom exits
814    #[test]
815    pub fn room_exits_data_get_exit_by_index_returns_some_for_exits_except_bottom_edge() {
816        let room_name = RoomName::new("W0N0").unwrap();
817
818        let wall_edge = [Terrain::Wall; 50];
819        let edge = [Terrain::Plain; 50];
820        let terrain = RoomEdgeTerrain::new_from_terrain_slices(&edge, &edge, &wall_edge, &edge).unwrap();
821        let exits_data = RoomExitsData::new_from_compressed_edge_terrain_data(terrain, room_name);
822
823        assert_eq!(1, exits_data.num_top_exits(), "Top exit count mismatch");
824        assert_eq!(1, exits_data.num_right_exits(), "Right exit count mismatch");
825        assert_eq!(0, exits_data.num_bottom_exits(), "Bottom exit count mismatch");
826        assert_eq!(1, exits_data.num_left_exits(), "Left exit count mismatch");
827
828        // -- First exit should be the top edge exit
829        let exit_opt = exits_data.get_exit_by_index(0);
830        assert!(exit_opt.is_some(), "Exit should exist");
831
832        let exit = exit_opt.unwrap();
833
834        assert_eq!(exit.start(), 1, "Exit start position invalid");
835        assert_eq!(exit.end(), 48, "Exit end position invalid");
836        assert_eq!(exit.len(), 48, "Exit length invalid");
837        assert_eq!(exit.exit_direction(), ExitDirection::Top, "Exit direction invalid");
838
839        // -- Second exit should be the right edge exit
840        let exit_opt = exits_data.get_exit_by_index(1);
841        assert!(exit_opt.is_some(), "Exit should exist");
842
843        let exit = exit_opt.unwrap();
844
845        assert_eq!(exit.start(), 1, "Exit start position invalid");
846        assert_eq!(exit.end(), 48, "Exit end position invalid");
847        assert_eq!(exit.len(), 48, "Exit length invalid");
848        assert_eq!(exit.exit_direction(), ExitDirection::Right, "Exit direction invalid");
849
850        // -- Third exit should be the left edge exit
851        let exit_opt = exits_data.get_exit_by_index(2);
852        assert!(exit_opt.is_some(), "Exit should exist");
853
854        let exit = exit_opt.unwrap();
855
856        assert_eq!(exit.start(), 1, "Exit start position invalid");
857        assert_eq!(exit.end(), 48, "Exit end position invalid");
858        assert_eq!(exit.len(), 48, "Exit length invalid");
859        assert_eq!(exit.exit_direction(), ExitDirection::Left, "Exit direction invalid");
860    }
861
862    // - No left exits
863    #[test]
864    pub fn room_exits_data_get_exit_by_index_returns_some_for_exits_except_left_edge() {
865        let room_name = RoomName::new("W0N0").unwrap();
866
867        let wall_edge = [Terrain::Wall; 50];
868        let edge = [Terrain::Plain; 50];
869        let terrain = RoomEdgeTerrain::new_from_terrain_slices(&edge, &edge, &edge, &wall_edge).unwrap();
870        let exits_data = RoomExitsData::new_from_compressed_edge_terrain_data(terrain, room_name);
871
872        assert_eq!(1, exits_data.num_top_exits(), "Top exit count mismatch");
873        assert_eq!(1, exits_data.num_right_exits(), "Right exit count mismatch");
874        assert_eq!(1, exits_data.num_bottom_exits(), "Bottom exit count mismatch");
875        assert_eq!(0, exits_data.num_left_exits(), "Left exit count mismatch");
876
877        // -- First exit should be the top edge exit
878        let exit_opt = exits_data.get_exit_by_index(0);
879        assert!(exit_opt.is_some(), "Exit should exist");
880
881        let exit = exit_opt.unwrap();
882
883        assert_eq!(exit.start(), 1, "Exit start position invalid");
884        assert_eq!(exit.end(), 48, "Exit end position invalid");
885        assert_eq!(exit.len(), 48, "Exit length invalid");
886        assert_eq!(exit.exit_direction(), ExitDirection::Top, "Exit direction invalid");
887
888        // -- Second exit should be the right edge exit
889        let exit_opt = exits_data.get_exit_by_index(1);
890        assert!(exit_opt.is_some(), "Exit should exist");
891
892        let exit = exit_opt.unwrap();
893
894        assert_eq!(exit.start(), 1, "Exit start position invalid");
895        assert_eq!(exit.end(), 48, "Exit end position invalid");
896        assert_eq!(exit.len(), 48, "Exit length invalid");
897        assert_eq!(exit.exit_direction(), ExitDirection::Right, "Exit direction invalid");
898
899        // -- Third exit should be the bottom edge exit
900        let exit_opt = exits_data.get_exit_by_index(2);
901        assert!(exit_opt.is_some(), "Exit should exist");
902
903        let exit = exit_opt.unwrap();
904
905        assert_eq!(exit.start(), 1, "Exit start position invalid");
906        assert_eq!(exit.end(), 48, "Exit end position invalid");
907        assert_eq!(exit.len(), 48, "Exit length invalid");
908        assert_eq!(exit.exit_direction(), ExitDirection::Bottom, "Exit direction invalid");
909    }
910
911    // - No top or right exits
912    #[test]
913    pub fn room_exits_data_get_exit_by_index_returns_some_for_exits_except_top_or_right() {
914        let room_name = RoomName::new("W0N0").unwrap();
915
916        let wall_edge = [Terrain::Wall; 50];
917        let edge = [Terrain::Plain; 50];
918        let terrain = RoomEdgeTerrain::new_from_terrain_slices(&wall_edge, &wall_edge, &edge, &edge).unwrap();
919        let exits_data = RoomExitsData::new_from_compressed_edge_terrain_data(terrain, room_name);
920
921        assert_eq!(0, exits_data.num_top_exits(), "Top exit count mismatch");
922        assert_eq!(0, exits_data.num_right_exits(), "Right exit count mismatch");
923        assert_eq!(1, exits_data.num_bottom_exits(), "Bottom exit count mismatch");
924        assert_eq!(1, exits_data.num_left_exits(), "Left exit count mismatch");
925
926        // -- Bottom edge
927        let exit_opt = exits_data.get_exit_by_index(0);
928        assert!(exit_opt.is_some(), "Exit should exist");
929
930        let exit = exit_opt.unwrap();
931
932        assert_eq!(exit.start(), 1, "Exit start position invalid");
933        assert_eq!(exit.end(), 48, "Exit end position invalid");
934        assert_eq!(exit.len(), 48, "Exit length invalid");
935        assert_eq!(exit.exit_direction(), ExitDirection::Bottom, "Exit direction invalid");
936
937        // -- Left edge
938        let exit_opt = exits_data.get_exit_by_index(1);
939        assert!(exit_opt.is_some(), "Exit should exist");
940
941        let exit = exit_opt.unwrap();
942
943        assert_eq!(exit.start(), 1, "Exit start position invalid");
944        assert_eq!(exit.end(), 48, "Exit end position invalid");
945        assert_eq!(exit.len(), 48, "Exit length invalid");
946        assert_eq!(exit.exit_direction(), ExitDirection::Left, "Exit direction invalid");
947    }
948
949    // - No top, right, or bottom exits
950    #[test]
951    pub fn room_exits_data_get_exit_by_index_returns_some_for_exits_except_top_or_right_or_bottom() {
952        let room_name = RoomName::new("W0N0").unwrap();
953
954        let wall_edge = [Terrain::Wall; 50];
955        let edge = [Terrain::Plain; 50];
956        let terrain = RoomEdgeTerrain::new_from_terrain_slices(&wall_edge, &wall_edge, &wall_edge, &edge).unwrap();
957        let exits_data = RoomExitsData::new_from_compressed_edge_terrain_data(terrain, room_name);
958
959        assert_eq!(0, exits_data.num_top_exits(), "Top exit count mismatch");
960        assert_eq!(0, exits_data.num_right_exits(), "Right exit count mismatch");
961        assert_eq!(0, exits_data.num_bottom_exits(), "Bottom exit count mismatch");
962        assert_eq!(1, exits_data.num_left_exits(), "Left exit count mismatch");
963
964        // -- Left edge
965        let exit_opt = exits_data.get_exit_by_index(0);
966        assert!(exit_opt.is_some(), "Exit should exist");
967
968        let exit = exit_opt.unwrap();
969
970        assert_eq!(exit.start(), 1, "Exit start position invalid");
971        assert_eq!(exit.end(), 48, "Exit end position invalid");
972        assert_eq!(exit.len(), 48, "Exit length invalid");
973        assert_eq!(exit.exit_direction(), ExitDirection::Left, "Exit direction invalid");
974    }
975}