1use utils::grid;
2use utils::prelude::*;
3
4#[derive(Clone, Debug)]
6pub struct Day15<'a> {
7 cols: usize,
8 grid: Vec<u8>,
9 robot: usize,
10 moves: &'a str,
11}
12
13impl<'a> Day15<'a> {
14 pub fn new(input: &'a str, _: InputType) -> Result<Self, InputError> {
15 let Some((grid, moves)) = input
16 .split_once("\n\n")
17 .or_else(|| input.split_once("\r\n\r\n"))
18 else {
19 return Err(InputError::new(input, 0, "expected grid and moves"));
20 };
21
22 let mut robot = None;
23 let (rows, cols, grid) = grid::parse(
24 grid,
25 0,
26 0,
27 |b| b,
28 |b| matches!(b, b'.' | b'#' | b'O'),
29 |i, b| {
30 if b == b'@' && robot.is_none() {
31 robot = Some(i);
32 Ok(b'.')
33 } else if b == b'@' {
34 Err("expected only one robot")
35 } else {
36 Err("expected '.', '#', 'O' or '@'")
37 }
38 },
39 )?;
40
41 let Some(robot) = robot else {
42 return Err(InputError::new(input, 0, "expected a robot"));
43 };
44
45 if !grid::is_enclosed(rows, cols, &grid, |&b| b == b'#') {
46 return Err(InputError::new(
47 input,
48 0,
49 "expected grid to be enclosed by walls",
50 ));
51 }
52
53 if let Some(idx) = moves.find(|b| !matches!(b, '^' | 'v' | '<' | '>' | '\r' | '\n')) {
54 return Err(InputError::new(
55 input,
56 &moves[idx..],
57 "expected ^, v, <, or >",
58 ));
59 }
60
61 Ok(Self {
62 cols,
63 grid,
64 robot,
65 moves,
66 })
67 }
68
69 #[must_use]
70 pub fn part1(&self) -> u32 {
71 let mut grid = self.grid.clone();
72 let mut robot = self.robot;
73
74 for offset in self.moves_iterator(self.cols) {
75 let next = robot.wrapping_add_signed(offset);
76 if grid[next] == b'.' {
77 robot = next;
78 } else if grid[next] == b'O' {
79 let mut next_free = next.wrapping_add_signed(offset);
80 while grid[next_free] == b'O' {
81 next_free = next_free.wrapping_add_signed(offset);
82 }
83 if grid[next_free] == b'.' {
84 grid[next_free] = b'O';
85 grid[next] = b'.';
86 robot = next;
87 }
88 }
89 }
90
91 Self::sum_box_coords(&grid, self.cols, b'O')
92 }
93
94 #[must_use]
95 pub fn part2(&self) -> u32 {
96 let mut grid = self
97 .grid
98 .iter()
99 .flat_map(|&b| match b {
100 b'#' => [b'#', b'#'],
101 b'.' => [b'.', b'.'],
102 b'O' => [b'[', b']'],
103 _ => unreachable!(),
104 })
105 .collect::<Vec<_>>();
106 let cols = self.cols * 2;
107 let mut robot = (self.robot / self.cols * cols) + (self.robot % self.cols * 2);
108
109 for offset in self.moves_iterator(cols) {
110 let next = robot.wrapping_add_signed(offset);
111 if grid[next] == b'.' {
112 robot = next;
113 } else if (grid[next] == b'[' || grid[next] == b']')
114 && Self::can_move_p2(&grid, next, offset)
115 {
116 Self::move_box_p2(&mut grid, next, offset);
117 robot = next;
118 }
119 }
120
121 Self::sum_box_coords(&grid, cols, b'[')
122 }
123
124 #[inline]
125 fn moves_iterator(&self, cols: usize) -> impl Iterator<Item = isize> + use<'_> {
126 self.moves.bytes().filter_map(move |c| match c {
127 b'^' => Some(-(cols as isize)),
128 b'v' => Some(cols as isize),
129 b'<' => Some(-1),
130 b'>' => Some(1),
131 _ => None,
132 })
133 }
134
135 #[inline]
136 fn sum_box_coords(grid: &[u8], cols: usize, box_byte: u8) -> u32 {
137 grid.iter()
138 .enumerate()
139 .map(|(i, &b)| {
140 if b == box_byte {
141 100 * ((i / cols) as u32) + ((i % cols) as u32)
142 } else {
143 0
144 }
145 })
146 .sum()
147 }
148
149 fn can_move_p2(grid: &[u8], pos: usize, offset: isize) -> bool {
150 let (left, right) = match grid[pos] {
151 b'[' => (pos, pos + 1),
152 b']' => (pos - 1, pos),
153 b'.' => return true,
154 b'#' => return false,
155 _ => unreachable!(),
156 };
157
158 if offset == -1 || offset == 1 {
159 Self::can_move_p2(grid, pos.wrapping_add_signed(offset), offset)
160 } else if grid[left.wrapping_add_signed(offset)] == b'[' {
161 Self::can_move_p2(grid, left.wrapping_add_signed(offset), offset)
163 } else {
164 Self::can_move_p2(grid, left.wrapping_add_signed(offset), offset)
165 && Self::can_move_p2(grid, right.wrapping_add_signed(offset), offset)
166 }
167 }
168
169 fn move_box_p2(grid: &mut [u8], pos: usize, offset: isize) {
170 let (left, right) = match grid[pos] {
171 b'[' => (pos, pos + 1),
172 b']' => (pos - 1, pos),
173 b'.' => return,
174 _ => unreachable!(),
175 };
176
177 if offset == -1 || offset == 1 {
178 Self::move_box_p2(grid, pos.wrapping_add_signed(offset * 2), offset);
179 } else {
180 Self::move_box_p2(grid, left.wrapping_add_signed(offset), offset);
181 Self::move_box_p2(grid, right.wrapping_add_signed(offset), offset);
182 }
183
184 grid[left] = b'.';
185 grid[right] = b'.';
186 grid[left.wrapping_add_signed(offset)] = b'[';
187 grid[right.wrapping_add_signed(offset)] = b']';
188 }
189}
190
191examples!(Day15<'_> -> (u32, u32) [
192 {file: "day15_example0.txt", part1: 10092, part2: 9021},
193 {file: "day15_example1.txt", part1: 2028},
194 {file: "day15_example2.txt", part2: 618},
195]);