Skip to content

Commit 9146d8e

Browse files
committed
2018 day 15
1 parent 902a2fc commit 9146d8e

File tree

8 files changed

+352
-0
lines changed

8 files changed

+352
-0
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#######
2+
#.G...#
3+
#...EG#
4+
#.#.#G#
5+
#..G#E#
6+
#.....#
7+
#######
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#######
2+
#G..#E#
3+
#E#E.E#
4+
#G.##.#
5+
#...#E#
6+
#...E.#
7+
#######
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#######
2+
#E..EG#
3+
#.#G.E#
4+
#E.##E#
5+
#G..#.#
6+
#..E#.#
7+
#######
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#######
2+
#E.G#.#
3+
#.#G..#
4+
#G.#.G#
5+
#G..#.#
6+
#...E.#
7+
#######
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#######
2+
#.E...#
3+
#.#..G#
4+
#.###.#
5+
#E#G#G#
6+
#...#G#
7+
#######
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#########
2+
#G......#
3+
#.E.#...#
4+
#..##..G#
5+
#...##..#
6+
#...#...#
7+
#.G...G.#
8+
#.....G.#
9+
#########

crates/year2018/src/day15.rs

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
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+
]);

crates/year2018/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ utils::year!(2018 => year2018, ${
1616
12 => day12::Day12,
1717
13 => day13::Day13,
1818
14 => day14::Day14,
19+
15 => day15::Day15,
1920
});

0 commit comments

Comments
 (0)