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