advent_of_code/year2019/
day20.rs

1use crate::input::Input;
2use std::collections::hash_map::Entry;
3use std::collections::{HashMap, HashSet, VecDeque};
4
5const DIRECTIONS: [(i32, i32); 4] = [(0, 1), (0, -1), (-1, 0), (1, 0)];
6
7struct Maze {
8    cols: usize,
9    array: Vec<u8>,
10    portals: HashMap<(i32, i32), ((i32, i32), i32)>,
11    start_location: (i32, i32),
12    end_location: (i32, i32),
13}
14
15impl Maze {
16    fn tile_at(&self, x: i32, y: i32) -> u8 {
17        if x < 0 || y < 0 {
18            return b' ';
19        }
20        *self
21            .array
22            .get((x as usize) + self.cols * (y as usize))
23            .unwrap_or(&b' ')
24    }
25
26    fn set_tile(&mut self, x: usize, y: usize, tile: u8) {
27        self.array[x + self.cols * y] = tile;
28    }
29
30    fn parse(input: &str, part1: bool) -> Result<Self, String> {
31        let rows = input.lines().count();
32        let cols = input
33            .lines()
34            .map(str::len)
35            .max()
36            .ok_or("Internal error: No max line length")?;
37
38        if rows < 5 || cols < 5 {
39            return Err("Too small input - expected at least 5x5".to_string());
40        }
41
42        let array = vec![b' '; rows * cols];
43        let mut maze = Self {
44            cols,
45            array,
46            portals: HashMap::new(),
47            end_location: (0, 0),
48            start_location: (0, 0),
49        };
50        input.lines().enumerate().for_each(|(y, line)| {
51            line.bytes().enumerate().for_each(|(x, tile)| {
52                maze.set_tile(x, y, tile);
53            });
54        });
55
56        let mut current_string = String::new();
57        let mut coming_from_passage = false;
58        let mut portal_name_to_location: HashMap<String, (i32, i32)> = HashMap::new();
59
60        let mut on_tile = |x: i32, y: i32, x_direction| {
61            let tile = if x as usize >= cols || y as usize >= rows {
62                b' '
63            } else {
64                maze.tile_at(x, y)
65            };
66
67            if tile.is_ascii_uppercase() {
68                current_string.push(tile as char);
69            } else {
70                if current_string.len() == 2 {
71                    let portal_x = if x_direction && coming_from_passage {
72                        x - 3
73                    } else {
74                        x
75                    };
76                    let portal_y = if !x_direction && coming_from_passage {
77                        y - 3
78                    } else {
79                        y
80                    };
81
82                    let current_location: (i32, i32) = (portal_x, portal_y);
83
84                    match current_string.as_str() {
85                        "AA" => {
86                            maze.start_location = (portal_x, portal_y);
87                        }
88                        "ZZ" => {
89                            maze.end_location = (portal_x, portal_y);
90                        }
91                        _ => {}
92                    };
93
94                    match portal_name_to_location.entry(current_string.clone()) {
95                        Entry::Occupied(other_location) => {
96                            let level_difference = if part1 {
97                                0
98                            } else if current_location.0 == 2
99                                || current_location.0 == (cols - 3) as i32
100                                || current_location.1 == 2
101                                || current_location.1 == (rows - 3) as i32
102                            {
103                                -1
104                            } else {
105                                1
106                            };
107                            let other_location = *other_location.get();
108                            maze.portals
109                                .insert(current_location, (other_location, level_difference));
110                            maze.portals
111                                .insert(other_location, (current_location, -level_difference));
112                        }
113                        Entry::Vacant(vacant) => {
114                            vacant.insert(current_location);
115                        }
116                    }
117                }
118
119                coming_from_passage = tile == b'.';
120                current_string.clear();
121            }
122        };
123
124        for x in 0..=cols {
125            for y in 0..=rows {
126                on_tile(x as i32, y as i32, false);
127            }
128        }
129        for y in 0..=rows {
130            for x in 0..=cols {
131                on_tile(x as i32, y as i32, true);
132            }
133        }
134
135        if maze.start_location == maze.end_location {
136            return Err("Start location not distinct from end location".to_string());
137        }
138
139        Ok(maze)
140    }
141}
142
143pub fn solve(input: &Input) -> Result<i32, String> {
144    let maze = Maze::parse(input.text, input.is_part_one())?;
145
146    let mut to_visit = VecDeque::new();
147    let mut visited = HashSet::new();
148    // Contains ((x,y), distance, level):
149    to_visit.push_back((maze.start_location, 0, 0));
150    // Contains ((x,y), level):
151    visited.insert((maze.start_location, 0));
152
153    while let Some((visiting, distance, level)) = to_visit.pop_front() {
154        let new_distance = distance + 1;
155
156        for (new_location, level_difference) in DIRECTIONS
157            .iter()
158            .map(|&(dx, dy)| ((visiting.0 + dx, visiting.1 + dy), 0))
159            .chain(
160                if let Some(&(new_location, level_difference)) = maze.portals.get(&visiting) {
161                    Some((new_location, level_difference)).into_iter()
162                } else {
163                    None.into_iter()
164                },
165            )
166        {
167            let new_level = level + level_difference;
168            if new_level >= 0
169                && maze.tile_at(new_location.0, new_location.1) == b'.'
170                && visited.insert((new_location, new_level))
171            {
172                if new_location == maze.end_location && new_level == 0 {
173                    return Ok(new_distance);
174                }
175                to_visit.push_back((new_location, new_distance, new_level));
176            }
177        }
178    }
179    Err("No path found".to_string())
180}
181
182#[test]
183pub fn tests() {
184    use crate::input::{test_part_one, test_part_two};
185
186    let example = include_str!("day20_example.txt");
187    test_part_one!(example => 23);
188
189    let input = include_str!("day20_input.txt");
190    test_part_one!(input => 580);
191    test_part_two!(input => 6362);
192
193    let other_input = include_str!("day20_input_ray.txt");
194    test_part_one!(other_input => 552);
195    test_part_two!(other_input => 6492);
196}