Skip to main content

year2018/
day15.rs

1use utils::prelude::*;
2
3/// Simulating battle outcomes between elves and goblins.
4#[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                // Move
130                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                // Attack
142                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        // Forward BFS pass from unit adjacent squares to enemy adjacent squares.
210        // bfs_layers[i] holds the set of squares reachable after i steps.
211        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            // Check if the pass reached an enemy adjacent square.
240            // This must be checked in (row, call) order to select the right target when multiple
241            // are reachable.
242            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        // Reverse BFS pass from the target enemy adjacent square back to one of the 4 unit
255        // adjacent squares the unit can move to.
256        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        // Must be checked in (row, col) order
268        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]);