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, PartialEq, Eq)]
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: &Group) -> 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<'a>(input: &'a str) -> Input {
70    // Use a bitmask to store each possible attack type.
71    let mut elements = FastMap::new();
72    let mut mask = |key: &'a str| {
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 set the score and signal all threads to stop.
100        // Use a channel to queue all potential scores as another thread may already have sent a
101        // different value.
102        let (kind, units) = fight(input, boost as i32);
103
104        if kind == Kind::Immune {
105            iter.stop();
106            return Some((boost, units));
107        }
108    }
109
110    None
111}
112
113fn fight(input: &Input, boost: i32) -> (Kind, i32) {
114    let mut immune = input.immune.clone();
115    let mut infection = input.infection.clone();
116    let mut attacks = vec![None; immune.len() + infection.len()];
117
118    // Boost reindeer's immmune system.
119    immune.iter_mut().for_each(|group| group.damage += boost);
120
121    for turn in 1.. {
122        // Target selection phase.
123        let mut target_selection = |attacker: &[Group], defender: &mut [Group], kind: Kind| {
124            for (from, group) in attacker.iter().enumerate() {
125                let target = (0..defender.len())
126                    .filter(|&to| {
127                        defender[to].chosen < turn && group.actual_damage(&defender[to]) > 0
128                    })
129                    .max_by_key(|&to| {
130                        (
131                            group.actual_damage(&defender[to]),
132                            defender[to].effective_power(),
133                            defender[to].initiative,
134                        )
135                    });
136
137                if let Some(to) = target {
138                    // Attacks happen in descending order of initiative.
139                    let index = attacks.len() - group.initiative as usize;
140                    defender[to].chosen = turn;
141                    attacks[index] = Some((kind, from, to));
142                }
143            }
144        };
145
146        // Turn order is important.
147        immune.sort_unstable_by_key(Group::target_selection_order);
148        infection.sort_unstable_by_key(Group::target_selection_order);
149
150        target_selection(&immune, &mut infection, Kind::Immune);
151        target_selection(&infection, &mut immune, Kind::Infection);
152
153        // Attacking phase.
154        let mut killed = 0;
155
156        for next in &mut attacks {
157            if let Some((kind, from, to)) = *next {
158                if kind == Kind::Immune {
159                    killed += immune[from].attack(&mut infection[to]);
160                } else {
161                    killed += infection[from].attack(&mut immune[to]);
162                }
163                *next = None;
164            }
165        }
166
167        // It's possible to deadlock if groups become too weak to do any more damage.
168        if killed == 0 {
169            return (Kind::Draw, 0);
170        }
171
172        // Check for winner.
173        immune.retain(|group| group.units > 0);
174        infection.retain(|group| group.units > 0);
175
176        if immune.is_empty() {
177            return (Kind::Infection, infection.iter().map(|group| group.units).sum());
178        }
179        if infection.is_empty() {
180            return (Kind::Immune, immune.iter().map(|group| group.units).sum());
181        }
182    }
183
184    unreachable!()
185}
186
187/// Parsing the input relatively cleanly is a challenge by itself.
188fn parse_group<'a>(input: &'a str, mask: &mut impl FnMut(&'a str) -> u32) -> Vec<Group> {
189    let delimiters = [' ', '(', ')', ',', ';'];
190    input
191        .lines()
192        .skip(1)
193        .map(|line| {
194            let tokens: Vec<_> = line.split(delimiters).collect();
195
196            let units = tokens[0].signed();
197            let hit_points = tokens[4].signed();
198            let damage = tokens[tokens.len() - 6].signed();
199            let initiative = tokens[tokens.len() - 1].signed();
200            let attack = mask(tokens[tokens.len() - 5]);
201            let weak = parse_list(&tokens, "weak", mask);
202            let immune = parse_list(&tokens, "immune", mask);
203            let chosen = 0;
204
205            Group { units, hit_points, damage, initiative, weak, immune, attack, chosen }
206        })
207        .collect()
208}
209
210/// There can be any amount of weaknesses or immunities.
211fn parse_list<'a>(tokens: &[&'a str], start: &str, mask: &mut impl FnMut(&'a str) -> u32) -> u32 {
212    let end = ["weak", "immune", "with"];
213    let mut elements = 0;
214
215    if let Some(index) = tokens.iter().position(|&t| t == start) {
216        let mut index = index + 2;
217        while !end.contains(&tokens[index]) {
218            elements |= mask(tokens[index]);
219            index += 1;
220        }
221    }
222
223    elements
224}