aoc/year2022/
day22.rs

1//! # Monkey Map
2//!
3//! Parses any arbitrary cube map calculating the transitions between faces dynamically
4//! using 3D vectors.
5//!
6//! We build the transitions with a BFS over the connected cube map. The first face we find
7//! is labelled A in the diagram below. For each face we define 3 vectors:
8//!
9//! * `i` Horizontal from left to right in the plane of the face.
10//! * `j` Vertical from top to bottom in the plane of the face.
11//! * `k` Perpendicular to the face pointing into the body of the cube.
12//!
13//! ```none
14//!              k (0, 0, 1)
15//!             ^
16//!            /
17//!           /
18//!          -------------+
19//!         /            /|
20//!        /    B       / |
21//!       /            /  |
22//!      +------------+---->i (1, 0, 0)
23//!      |            | C |
24//!      |     A      |  /
25//!      |            | /
26//!      |            |/
27//!      +------------+
28//!      |
29//!      |
30//!      v
31//!      j (0, 1, 0)
32//!
33//! ```
34//!
35//! Then for each neighbouring face we can find its `i`, `j` and `k` vectors depending on which
36//! edge it shares in common. For example if we move from face A to face B along the top edge
37//! then the new vectors are:
38//!
39//! * `i` (1, 0, 0) Remains unchanged
40//! * `j` (0, 0, -1) Minus previous `k`
41//! * `k` (0, 1, 0) Previous `j`
42//!
43//! If face B and C are connected then the vectors for face C are:
44//!
45//! * `i` (0, 1, 0)
46//! * `j` (0, 0, -1)
47//! * `k` (-1, 0, 0)
48//!
49//! However if A and C were connected then the vectors for face C are:
50//!
51//! * `i` (0, 0, 1)
52//! * `j` (0, 1, 0)
53//! * `k` (-1, 0, 0)
54//!
55//! The really neat part is that when we leave the edge of a cube face the next
56//! 3D vector *is always `k`* no matter which edge. We can find the new direction by comparing
57//! the previous `k` against the new `i` and `j` vectors.
58//!
59//! For example say we transition from face `A` to face `B`. Our `k` is (0, 1, 0) which is
60//! equal to minus the new `j`, so we know that we're travelling upwards from the bottom edge.
61//! Then we can use this information to figure out the two dimensional offsets into the new face.
62use crate::util::hash::*;
63use crate::util::math::*;
64use crate::util::parse::*;
65use crate::util::point::*;
66use std::collections::VecDeque;
67use std::ops::Neg;
68
69#[derive(Clone, Copy, PartialEq, Eq)]
70enum Tile {
71    None,
72    Open,
73    Wall,
74}
75
76enum Move {
77    Left,
78    Right,
79    Forward(u32),
80}
81
82pub struct Grid {
83    width: usize,
84    height: usize,
85    tiles: Vec<Tile>,
86    start: i32,
87    block: i32,
88}
89
90/// Return [`Tile::None`] for any point out of bounds.
91impl Grid {
92    fn tile(&self, point: Point) -> Tile {
93        let x = point.x as usize;
94        let y = point.y as usize;
95        if (0..self.width).contains(&x) && (0..self.height).contains(&y) {
96            self.tiles[y * self.width + x]
97        } else {
98            Tile::None
99        }
100    }
101}
102
103/// Minimal 3D vector implementation
104#[derive(Copy, Clone, Hash, PartialEq, Eq)]
105struct Vector {
106    x: i32,
107    y: i32,
108    z: i32,
109}
110
111// Syntactic sugar to implement the `-` operator.
112impl Neg for Vector {
113    type Output = Self;
114
115    fn neg(self) -> Self::Output {
116        Vector { x: -self.x, y: -self.y, z: -self.z }
117    }
118}
119
120/// 2D coordinates of the top left corner plus 3D vectors for the cube face.
121#[derive(Clone, Copy)]
122struct Face {
123    corner: Point,
124    i: Vector,
125    j: Vector,
126    k: Vector,
127}
128
129pub struct Input {
130    grid: Grid,
131    moves: Vec<Move>,
132}
133
134pub fn parse(input: &str) -> Input {
135    let (prefix, suffix) = input.rsplit_once("\n\n").unwrap();
136    let grid = parse_grid(prefix);
137    let moves = parse_moves(suffix);
138    Input { grid, moves }
139}
140
141pub fn part1(input: &Input) -> i32 {
142    let grid = &input.grid;
143    let block = grid.block;
144
145    // Wrap around to the other side of the row or column depending on direction.
146    let handle_none = |position, direction| {
147        let reverse = direction * -block;
148        let mut next = position + reverse;
149
150        while grid.tile(next) != Tile::None {
151            next += reverse;
152        }
153
154        next += direction;
155        (next, direction)
156    };
157
158    password(input, handle_none)
159}
160
161pub fn part2(input: &Input) -> i32 {
162    let grid = &input.grid;
163    let block = grid.block;
164    let edge = block - 1;
165
166    // Build the cube map dynamically.
167    let start = Face {
168        corner: Point::new(grid.start - grid.start % block, 0),
169        i: Vector { x: 1, y: 0, z: 0 },
170        j: Vector { x: 0, y: 1, z: 0 },
171        k: Vector { x: 0, y: 0, z: 1 },
172    };
173    let mut todo = VecDeque::from([start]);
174    let mut faces = FastMap::build([(start.k, start)]);
175    let mut corners = FastMap::build([(start.corner, start)]);
176
177    while let Some(next) = todo.pop_front() {
178        let Face { corner, i, j, k } = next;
179
180        // Define the transitions from each edge.
181        let neighbors = [
182            Face { corner: corner + Point::new(-block, 0), i: -k, j, k: i }, // Left
183            Face { corner: corner + Point::new(block, 0), i: k, j, k: -i },  // Right
184            Face { corner: corner + Point::new(0, -block), i, j: -k, k: j }, // Up
185            Face { corner: corner + Point::new(0, block), i, j: k, k: -j },  // Down
186        ];
187
188        // Potentially add the candidate edge to the frontier.
189        for next in neighbors {
190            if grid.tile(next.corner) != Tile::None && !faces.contains_key(&next.k) {
191                todo.push_back(next);
192                faces.insert(next.k, next);
193                corners.insert(next.corner, next);
194            }
195        }
196    }
197
198    let handle_none = |position: Point, direction| {
199        // Our (x, y) offset within the face.
200        let offset = Point::new(position.x % block, position.y % block);
201        // The (x, y) coordinate of the top left corner of the face.
202        let corner = position - offset;
203        // Lookup the 3D vectors associated with the current face.
204        let Face { i, j, k, .. } = corners[&corner];
205        // These transitions are the same as used during the BFS above.
206        let next_k = match direction {
207            LEFT => i,
208            RIGHT => -i,
209            UP => j,
210            DOWN => -j,
211            _ => unreachable!(),
212        };
213        let Face { corner: next_corner, i: next_i, j: next_j, .. } = faces[&next_k];
214        // Here's the really neat part. Our new 3D direction will *always* be `k`.
215        // We can find the relative orientation in the plane of the face by checking against
216        // `i` and `j`. This also tells us which edge we're entering.
217        let next_direction = if k == next_i {
218            RIGHT
219        } else if k == -next_i {
220            LEFT
221        } else if k == next_j {
222            DOWN
223        } else if k == -next_j {
224            UP
225        } else {
226            unreachable!()
227        };
228        // 4 possible leaving edges and 4 possible entering edges gives 16 total possible
229        // combinations.
230        let next_offset = match (direction, next_direction) {
231            (LEFT, LEFT) => Point::new(edge, offset.y),
232            (LEFT, RIGHT) => Point::new(0, edge - offset.y),
233            (LEFT, DOWN) => Point::new(offset.y, 0),
234            (LEFT, UP) => Point::new(edge - offset.y, edge),
235            (RIGHT, LEFT) => Point::new(edge, edge - offset.y),
236            (RIGHT, RIGHT) => Point::new(0, offset.y),
237            (RIGHT, DOWN) => Point::new(edge - offset.y, 0),
238            (RIGHT, UP) => Point::new(offset.y, edge),
239            (DOWN, LEFT) => Point::new(edge, offset.x),
240            (DOWN, RIGHT) => Point::new(0, edge - offset.x),
241            (DOWN, DOWN) => Point::new(offset.x, 0),
242            (DOWN, UP) => Point::new(edge - offset.x, edge),
243            (UP, LEFT) => Point::new(edge, edge - offset.x),
244            (UP, RIGHT) => Point::new(0, offset.x),
245            (UP, DOWN) => Point::new(edge - offset.x, 0),
246            (UP, UP) => Point::new(offset.x, edge),
247            _ => unreachable!(),
248        };
249        let next_position = next_corner + next_offset;
250        (next_position, next_direction)
251    };
252
253    password(input, handle_none)
254}
255
256fn parse_grid(input: &str) -> Grid {
257    let raw: Vec<_> = input.lines().map(str::as_bytes).collect();
258    // Width is the maximum width of any row
259    let width = raw.iter().map(|line| line.len()).max().unwrap();
260    let height = raw.len();
261    let mut tiles = vec![Tile::None; width * height];
262
263    // Convert ASCII to enums.
264    for (y, row) in raw.iter().enumerate() {
265        for (x, col) in row.iter().enumerate() {
266            let tile = match col {
267                b'.' => Tile::Open,
268                b'#' => Tile::Wall,
269                _ => Tile::None,
270            };
271            tiles[y * width + x] = tile;
272        }
273    }
274
275    // Find the first open tile in the top row.
276    let start = tiles.iter().position(|&t| t == Tile::Open).unwrap() as i32;
277    // Find the size of each face (4 in the sample or 50 in the actual input).
278    let block = width.gcd(height) as i32;
279    Grid { width, height, tiles, start, block }
280}
281
282fn parse_moves(input: &str) -> Vec<Move> {
283    let mut moves = Vec::new();
284    let mut numbers = input.iter_unsigned();
285    let mut letters = input.bytes().filter(u8::is_ascii_uppercase);
286
287    // Numbers and letters alternate, with numbers first.
288    loop {
289        let Some(n) = numbers.next() else {
290            break;
291        };
292        moves.push(Move::Forward(n));
293
294        let Some(d) = letters.next() else {
295            break;
296        };
297        moves.push(if d == b'L' { Move::Left } else { Move::Right });
298    }
299
300    moves
301}
302
303/// Common code shared between part one and two. The `handle_none` closure defines how
304/// to transition when we leave an edge.
305fn password(input: &Input, handle_none: impl Fn(Point, Point) -> (Point, Point)) -> i32 {
306    let Input { grid, moves } = input;
307    let mut position = Point::new(grid.start, 0);
308    let mut direction = Point::new(1, 0);
309
310    for command in moves {
311        match command {
312            Move::Left => direction = direction.counter_clockwise(),
313            Move::Right => direction = direction.clockwise(),
314            Move::Forward(n) => {
315                for _ in 0..*n {
316                    let next = position + direction;
317                    match grid.tile(next) {
318                        // Not possible to move any further so we can break out of the loop.
319                        Tile::Wall => break,
320                        // Move within the 2D cube map.
321                        Tile::Open => position = next,
322                        Tile::None => {
323                            let (next_position, next_direction) = handle_none(position, direction);
324                            // The new position on a different face may be a wall.
325                            if grid.tile(next_position) == Tile::Open {
326                                position = next_position;
327                                direction = next_direction;
328                            } else {
329                                break;
330                            }
331                        }
332                    }
333                }
334            }
335        }
336    }
337
338    // Calculate the final score.
339    let position_score = 1000 * (position.y + 1) + 4 * (position.x + 1);
340    let direction_score = match direction {
341        RIGHT => 0,
342        DOWN => 1,
343        LEFT => 2,
344        UP => 3,
345        _ => unreachable!(),
346    };
347    position_score + direction_score
348}