|
| 1 | +use utils::prelude::*; |
| 2 | + |
| 3 | +/// Simulating battle outcomes between elves and goblins. |
| 4 | +#[derive(Clone, Debug)] |
| 5 | +pub struct Day15 { |
| 6 | + wall_mask: [u32; 32], |
| 7 | + units: Vec<Unit>, |
| 8 | +} |
| 9 | + |
| 10 | +#[derive(Clone, Debug)] |
| 11 | +struct 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 | +struct Unit { |
| 24 | + pos: usize, |
| 25 | + unit_type: UnitType, |
| 26 | + health: u32, |
| 27 | +} |
| 28 | + |
| 29 | +#[derive(Clone, Copy, Debug, PartialEq)] |
| 30 | +enum UnitType { |
| 31 | + Elf, |
| 32 | + Goblin, |
| 33 | +} |
| 34 | + |
| 35 | +impl 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 | + |
| 120 | +impl 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 | + // Move |
| 129 | + 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 | + // Attack |
| 141 | + 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 | + // Forward BFS pass from unit adjacent squares to enemy adjacent squares. |
| 209 | + // bfs_layers[i] holds the set of squares reachable after i steps. |
| 210 | + 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 | + // Check if the pass reached an enemy adjacent square. |
| 239 | + // This must be checked in (row, call) order to select the right target when multiple |
| 240 | + // are reachable. |
| 241 | + 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 | + // Reverse BFS pass from the target enemy adjacent square back to one of the 4 unit |
| 254 | + // adjacent squares the unit can move to. |
| 255 | + 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 | + // Must be checked in (row, col) order |
| 267 | + 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 | + |
| 291 | +impl UnitType { |
| 292 | + fn enemy(self) -> Self { |
| 293 | + match self { |
| 294 | + UnitType::Elf => UnitType::Goblin, |
| 295 | + UnitType::Goblin => UnitType::Elf, |
| 296 | + } |
| 297 | + } |
| 298 | +} |
| 299 | + |
| 300 | +examples!(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 | +]); |
0 commit comments