1use utils::prelude::*;
2
3#[derive(Clone, Debug)]
5pub struct Day15 {
6 wall_mask: [u32; 32],
7 units: Vec<Unit>,
8}
9
10#[derive(Clone, Debug)]
11struct Battle {
12 units: Vec<Unit>,
13 grid: [u16; 1024],
14 wall_mask: [u32; 32],
15 unit_masks: [[u32; 32]; 2],
16 bfs_layers: Vec<[u32; 32]>,
17 unit_counts: [usize; 2],
18 attack_power: [u32; 2],
19 elves_must_live: bool,
20}
21
22#[derive(Clone, Debug)]
23#[expect(clippy::struct_field_names)]
24struct Unit {
25 pos: usize,
26 unit_type: UnitType,
27 health: u32,
28}
29
30#[derive(Clone, Copy, Debug, PartialEq)]
31enum UnitType {
32 Elf,
33 Goblin,
34}
35
36impl Day15 {
37 pub fn new(input: &str, _: InputType) -> Result<Self, InputError> {
38 let mut wall_mask = [u32::MAX; 32];
39 let mut units = Vec::with_capacity(32);
40
41 for (row, line) in input.lines().enumerate() {
42 if row >= 32 {
43 return Err(InputError::new(input, line, "too many rows"));
44 }
45 if line.len() > 32 {
46 return Err(InputError::new(input, line, "too many columns"));
47 }
48 for (col, b) in line.bytes().enumerate() {
49 let unit_type = match b {
50 b'.' => None,
51 b'E' => Some(UnitType::Elf),
52 b'G' => Some(UnitType::Goblin),
53 b'#' => continue,
54 _ => {
55 return Err(InputError::new(
56 input,
57 b as char,
58 "expected '.', '#', 'E' or 'G'",
59 ));
60 }
61 };
62
63 wall_mask[row] &= !(1 << col);
64
65 if let Some(unit_type) = unit_type {
66 units.push(Unit {
67 pos: row * 32 + col,
68 unit_type,
69 health: 200,
70 });
71 }
72 }
73 }
74
75 if wall_mask[0] != u32::MAX
76 || wall_mask[31] != u32::MAX
77 || wall_mask.iter().any(|&x| x & 1 == 0 || x & (1 << 31) == 0)
78 {
79 return Err(InputError::new(input, 0, "expected grid to be enclosed"));
80 }
81
82 Ok(Self { wall_mask, units })
83 }
84
85 #[must_use]
86 pub fn part1(&self) -> u32 {
87 self.battle_outcome(3, false).unwrap()
88 }
89
90 #[must_use]
91 pub fn part2(&self) -> u32 {
92 for attack in 4..=200 {
93 if let Some(score) = self.battle_outcome(attack, true) {
94 return score;
95 }
96 }
97 panic!("no solution found");
98 }
99
100 fn battle_outcome(&self, elf_attack: u32, elves_must_live: bool) -> Option<u32> {
101 let mut b = Battle {
102 units: self.units.clone(),
103 grid: [u16::MAX; 1024],
104 wall_mask: self.wall_mask,
105 unit_masks: [[0u32; 32]; 2],
106 unit_counts: self.units.iter().fold([0, 0], |mut acc, u| {
107 acc[u.unit_type as usize] += 1;
108 acc
109 }),
110 attack_power: [elf_attack, 3],
111 bfs_layers: vec![[0u32; 32]; 64],
112 elves_must_live,
113 };
114 for i in 0..self.units.len() {
115 b.add_unit_to_grid(i);
116 }
117 b.outcome()
118 }
119}
120
121impl Battle {
122 fn outcome(mut self) -> Option<u32> {
123 for round in 0.. {
124 for unit_idx in 0..self.units.len() {
125 if self.units[unit_idx].health == 0 {
126 continue;
127 }
128
129 let mut adjacent_enemy = self.adjacent_enemy(unit_idx);
131 if adjacent_enemy.is_none()
132 && let Some(next) = self.next_move(unit_idx)
133 {
134 self.remove_unit_from_grid(unit_idx);
135 self.units[unit_idx].pos = next;
136 self.add_unit_to_grid(unit_idx);
137
138 adjacent_enemy = self.adjacent_enemy(unit_idx);
139 }
140
141 let Some(enemy_idx) = adjacent_enemy else {
143 continue;
144 };
145
146 let attack = self.attack_power[self.units[unit_idx].unit_type as usize];
147 self.units[enemy_idx].health = self.units[enemy_idx].health.saturating_sub(attack);
148
149 if self.units[enemy_idx].health == 0 {
150 self.remove_unit_from_grid(enemy_idx);
151
152 let enemy_type = self.units[enemy_idx].unit_type;
153 if enemy_type == UnitType::Elf && self.elves_must_live {
154 return None;
155 }
156
157 self.unit_counts[enemy_type as usize] -= 1;
158 if self.unit_counts[enemy_type as usize] == 0 {
159 let round_complete =
160 self.units[unit_idx + 1..].iter().all(|u| u.health == 0);
161 let full_rounds = round + u32::from(round_complete);
162 let remaining_health = self.units.iter().map(|u| u.health).sum::<u32>();
163 return Some(full_rounds * remaining_health);
164 }
165 }
166 }
167
168 self.units.retain(|u| u.health > 0);
169 self.units.sort_unstable_by_key(|u| u.pos);
170 for (i, u) in self.units.iter().enumerate() {
171 self.grid[u.pos] = i as u16;
172 }
173 }
174
175 unreachable!();
176 }
177
178 fn adjacent_enemy(&self, unit_idx: usize) -> Option<usize> {
179 let (mut enemy_index, mut enemy_health) = (None, u32::MAX);
180
181 let Unit { pos, unit_type, .. } = self.units[unit_idx];
182 for enemy_pos in [pos - 32, pos - 1, pos + 1, pos + 32] {
183 if let Some(&idx @ 0..1024) = self.grid.get(enemy_pos)
184 && self.units[idx as usize].unit_type != unit_type
185 && self.units[idx as usize].health < enemy_health
186 {
187 enemy_index = Some(idx as usize);
188 enemy_health = self.units[idx as usize].health;
189 }
190 }
191
192 enemy_index
193 }
194
195 fn next_move(&mut self, unit_idx: usize) -> Option<usize> {
196 let unit_type = self.units[unit_idx].unit_type;
197 let ally_mask = &self.unit_masks[unit_type as usize];
198 let enemy_mask = self.unit_masks[unit_type.enemy() as usize];
199
200 let mut enemy_adjacent_mask = [0u32; 32];
201 for i in 1..31 {
202 enemy_adjacent_mask[i] =
203 (enemy_mask[i] << 1) | (enemy_mask[i] >> 1) | enemy_mask[i - 1] | enemy_mask[i + 1];
204 }
205
206 let unit_pos = self.units[unit_idx].pos;
207 let unit_adjacent = [unit_pos - 32, unit_pos - 1, unit_pos + 1, unit_pos + 32];
208
209 self.bfs_layers[0].fill(0);
212 for pos in unit_adjacent {
213 let (row, col) = (pos / 32, pos % 32);
214 if (self.wall_mask[row] | enemy_mask[row] | ally_mask[row]) & (1 << col) != 0 {
215 continue;
216 }
217 if enemy_adjacent_mask[row] & (1 << col) != 0 {
218 return Some(pos);
219 }
220 self.bfs_layers[0][row] |= 1 << col;
221 }
222
223 let mut steps = 0;
224 let (target_row, target_col) = 'forward: loop {
225 steps += 1;
226 if steps >= self.bfs_layers.len() {
227 self.bfs_layers.push([0u32; 32]);
228 }
229
230 let [prev, next] = self
231 .bfs_layers
232 .get_disjoint_mut([steps - 1, steps])
233 .unwrap();
234 for i in 1..31 {
235 next[i] = (prev[i] | (prev[i] << 1) | (prev[i] >> 1) | prev[i - 1] | prev[i + 1])
236 & !(self.wall_mask[i] | ally_mask[i]);
237 }
238
239 for row in 1..31 {
243 let mask = next[row] & enemy_adjacent_mask[row];
244 if mask != 0 {
245 break 'forward (row, mask.trailing_zeros() as usize);
246 }
247 }
248
249 if next == prev {
250 return None;
251 }
252 };
253
254 self.bfs_layers[steps].fill(0);
257 self.bfs_layers[steps][target_row] |= 1 << target_col;
258
259 for step in (1..=steps).rev() {
260 let [next, prev] = self.bfs_layers.get_disjoint_mut([step - 1, step]).unwrap();
261
262 for i in 1..31 {
263 next[i] &= (prev[i] << 1) | (prev[i] >> 1) | prev[i - 1] | prev[i + 1];
264 }
265 }
266
267 for pos in unit_adjacent {
269 if self.bfs_layers[0][pos / 32] & (1 << (pos % 32)) != 0 {
270 return Some(pos);
271 }
272 }
273
274 unreachable!("forward bfs succeeded, so there must be a reverse path");
275 }
276
277 fn remove_unit_from_grid(&mut self, unit_index: usize) {
278 let Unit { pos, unit_type, .. } = self.units[unit_index];
279
280 self.grid[pos] = u16::MAX;
281 self.unit_masks[unit_type as usize][pos / 32] &= !(1 << (pos % 32));
282 }
283
284 fn add_unit_to_grid(&mut self, unit_index: usize) {
285 let Unit { pos, unit_type, .. } = self.units[unit_index];
286
287 self.grid[pos] = unit_index as u16;
288 self.unit_masks[unit_type as usize][pos / 32] |= 1 << (pos % 32);
289 }
290}
291
292impl UnitType {
293 fn enemy(self) -> Self {
294 match self {
295 UnitType::Elf => UnitType::Goblin,
296 UnitType::Goblin => UnitType::Elf,
297 }
298 }
299}
300
301examples!(Day15 -> (u32, u32) [
302 {file: "day15_example0.txt", part1: 27730, part2: 4988},
303 {file: "day15_example1.txt", part1: 36334},
304 {file: "day15_example2.txt", part1: 39514, part2: 31284},
305 {file: "day15_example3.txt", part1: 27755, part2: 3478},
306 {file: "day15_example4.txt", part1: 28944, part2: 6474},
307 {file: "day15_example5.txt", part1: 18740, part2: 1140},
308]);