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)]
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                // 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
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]);