Skip to main content

aoc/year2018/
day24.rs

1//! # Immune System Simulator 20XX
2//!
3//! Similar to [`Day 15`] we implement the rules precisely, paying attention to edge cases.
4//!
5//! In particular, during part two, it's possible for a fight to end in a draw, if both armies
6//! become too weak to destroy any further units. As each fight is independent, we find the
7//! minimum boost value with a multithreaded parallel search.
8//!
9//! [`Day 15`]: crate::year2018::day15
10use crate::util::hash::*;
11use crate::util::parse::*;
12use crate::util::thread::*;
13
14pub struct Input {
15    immune: Vec<Group>,
16    infection: Vec<Group>,
17}
18
19#[derive(Clone, Copy, Eq, PartialEq)]
20enum Kind {
21    Immune,
22    Infection,
23    Draw,
24}
25
26#[derive(Clone, Copy)]
27struct Group {
28    units: i32,
29    hit_points: i32,
30    damage: i32,
31    initiative: i32,
32    weak: u32,
33    immune: u32,
34    attack: u32,
35    chosen: u32,
36}
37
38/// Convenience functions.
39impl Group {
40    fn effective_power(&self) -> i32 {
41        self.units * self.damage
42    }
43
44    /// Attack types are stored as a bitmask for quick comparison.
45    fn actual_damage(&self, other: &Self) -> i32 {
46        if self.attack & other.weak != 0 {
47            2 * self.effective_power()
48        } else if self.attack & other.immune == 0 {
49            self.effective_power()
50        } else {
51            0
52        }
53    }
54
55    fn target_selection_order(&self) -> (i32, i32) {
56        (-self.effective_power(), -self.initiative)
57    }
58
59    fn attack(&self, defender: &mut Self) -> i32 {
60        // Clamp damage to 0 as units may be negative,
61        // if this unit was wiped out in an earlier attack.
62        let damage = self.actual_damage(defender).max(0);
63        let amount = damage / defender.hit_points;
64        defender.units -= amount;
65        amount
66    }
67}
68
69pub fn parse(input: &str) -> Input {
70    // Use a bitmask to store each possible attack type.
71    let mut elements = FastMap::new();
72    let mut mask = |key| {
73        let next = 1 << elements.len();
74        *elements.entry(key).or_insert(next)
75    };
76
77    let (first, second) = input.split_once("\n\n").unwrap();
78    let immune = parse_group(first, &mut mask);
79    let infection = parse_group(second, &mut mask);
80    Input { immune, infection }
81}
82
83pub fn part1(input: &Input) -> i32 {
84    let (_, units) = fight(input, 0);
85    units
86}
87
88pub fn part2(input: &Input) -> i32 {
89    let iter = AtomicIter::new(1, 1);
90
91    // Use as many cores as possible to parallelize the search.
92    let result = spawn(|| worker(input, &iter));
93    // Find lowest possible power.
94    result.into_iter().flatten().min_by_key(|&(boost, _)| boost).map(|(_, units)| units).unwrap()
95}
96
97fn worker(input: &Input, iter: &AtomicIter) -> Option<(u32, i32)> {
98    while let Some(boost) = iter.next() {
99        // If the reindeer wins then signal all threads to stop.
100        let (kind, units) = fight(input, boost as i32);
101
102        if kind == Kind::Immune {
103            iter.stop();
104            return Some((boost, units));
105        }
106    }
107
108    None
109}
110
111fn fight(input: &Input, boost: i32) -> (Kind, i32) {
112    let mut immune = input.immune.clone();
113    let mut infection = input.infection.clone();
114    let mut attacks = vec![None; immune.len() + infection.len()];
115
116    // Boost reindeer's immune system.
117    immune.iter_mut().for_each(|group| group.damage += boost);
118
119    for turn in 1.. {
120        // Target selection phase.
121        let mut target_selection = |attacker: &[Group], defender: &mut [Group], kind: Kind| {
122            for (from, group) in attacker.iter().enumerate() {
123                let target = (0..defender.len())
124                    .filter(|&to| {
125                        defender[to].chosen < turn && group.actual_damage(&defender[to]) > 0
126                    })
127                    .max_by_key(|&to| {
128                        (
129                            group.actual_damage(&defender[to]),
130                            defender[to].effective_power(),
131                            defender[to].initiative,
132                        )
133                    });
134
135                if let Some(to) = target {
136                    // Attacks happen in descending order of initiative.
137                    let index = attacks.len() - group.initiative as usize;
138                    defender[to].chosen = turn;
139                    attacks[index] = Some((kind, from, to));
140                }
141            }
142        };
143
144        // Turn order is important.
145        immune.sort_unstable_by_key(Group::target_selection_order);
146        infection.sort_unstable_by_key(Group::target_selection_order);
147
148        target_selection(&immune, &mut infection, Kind::Immune);
149        target_selection(&infection, &mut immune, Kind::Infection);
150
151        // Attacking phase.
152        let mut killed = 0;
153
154        for next in &mut attacks {
155            if let Some((kind, from, to)) = next.take() {
156                if kind == Kind::Immune {
157                    killed += immune[from].attack(&mut infection[to]);
158                } else {
159                    killed += infection[from].attack(&mut immune[to]);
160                }
161            }
162        }
163
164        // It's possible to deadlock if groups become too weak to do any more damage.
165        if killed == 0 {
166            return (Kind::Draw, 0);
167        }
168
169        // Check for winner.
170        immune.retain(|group| group.units > 0);
171        infection.retain(|group| group.units > 0);
172
173        if immune.is_empty() {
174            return (Kind::Infection, infection.iter().map(|group| group.units).sum());
175        }
176        if infection.is_empty() {
177            return (Kind::Immune, immune.iter().map(|group| group.units).sum());
178        }
179    }
180
181    unreachable!()
182}
183
184/// Parsing the input relatively cleanly is a challenge by itself.
185fn parse_group<'a>(input: &'a str, mask: &mut impl FnMut(&'a str) -> u32) -> Vec<Group> {
186    let delimiters = [' ', '(', ')', ',', ';'];
187    input
188        .lines()
189        .skip(1)
190        .map(|line| {
191            let tokens: Vec<_> = line.split(delimiters).collect();
192
193            let units = tokens[0].signed();
194            let hit_points = tokens[4].signed();
195            let damage = tokens[tokens.len() - 6].signed();
196            let initiative = tokens[tokens.len() - 1].signed();
197            let attack = mask(tokens[tokens.len() - 5]);
198            let weak = parse_list(&tokens, "weak", mask);
199            let immune = parse_list(&tokens, "immune", mask);
200            let chosen = 0;
201
202            Group { units, hit_points, damage, initiative, weak, immune, attack, chosen }
203        })
204        .collect()
205}
206
207/// There can be any number of weaknesses or immunities.
208fn parse_list<'a>(tokens: &[&'a str], start: &str, mask: &mut impl FnMut(&'a str) -> u32) -> u32 {
209    let end = ["weak", "immune", "with"];
210    if let Some(index) = tokens.iter().position(|&t| t == start) {
211        // Skip over the "to" that follows, then take element names until the next section starts.
212        tokens[index + 2..]
213            .iter()
214            .take_while(|&&t| !end.contains(&t))
215            .fold(0, |elements, &t| elements | mask(t))
216    } else {
217        0
218    }
219}