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::*;
13use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
14use std::sync::mpsc::{Sender, channel};
15
16pub struct Input {
17    immune: Vec<Group>,
18    infection: Vec<Group>,
19}
20
21#[derive(Clone, Copy, PartialEq, Eq)]
22enum Kind {
23    Immune,
24    Infection,
25    Draw,
26}
27
28#[derive(Clone, Copy)]
29struct Group {
30    units: i32,
31    hit_points: i32,
32    damage: i32,
33    initiative: i32,
34    weak: u32,
35    immune: u32,
36    attack: u32,
37    chosen: u32,
38}
39
40/// Convenience functions.
41impl Group {
42    fn effective_power(&self) -> i32 {
43        self.units * self.damage
44    }
45
46    /// Attack types are stored as a bitmask for quick comparison.
47    fn actual_damage(&self, other: &Group) -> i32 {
48        if self.attack & other.weak != 0 {
49            2 * self.effective_power()
50        } else if self.attack & other.immune == 0 {
51            self.effective_power()
52        } else {
53            0
54        }
55    }
56
57    fn target_selection_order(&self) -> (i32, i32) {
58        (-self.effective_power(), -self.initiative)
59    }
60
61    fn attack(&self, defender: &mut Self) -> i32 {
62        // Clamp damage to 0 as units may be negative,
63        // if this unit was wiped out in an earlier attack.
64        let damage = self.actual_damage(defender).max(0);
65        let amount = damage / defender.hit_points;
66        defender.units -= amount;
67        amount
68    }
69}
70
71/// Shared between threads for part two.
72struct Shared {
73    done: AtomicBool,
74    boost: AtomicI32,
75    tx: Sender<(i32, i32)>,
76}
77
78pub fn parse<'a>(input: &'a str) -> Input {
79    // Use a bitmask to store each possible attack type.
80    let mut elements = FastMap::new();
81    let mut mask = |key: &'a str| {
82        let next = 1 << elements.len();
83        *elements.entry(key).or_insert(next)
84    };
85
86    let (first, second) = input.split_once("\n\n").unwrap();
87    let immune = parse_group(first, &mut mask);
88    let infection = parse_group(second, &mut mask);
89    Input { immune, infection }
90}
91
92pub fn part1(input: &Input) -> i32 {
93    let (_, units) = fight(input, 0);
94    units
95}
96
97pub fn part2(input: &Input) -> i32 {
98    let (tx, rx) = channel();
99    let shared = Shared { done: AtomicBool::new(false), boost: AtomicI32::new(1), tx };
100
101    // Use as many cores as possible to parallelize the search.
102    spawn(|| worker(input, &shared));
103
104    // Hang up the channel.
105    drop(shared.tx);
106    // Find lowest possible power.
107    rx.iter().min_by_key(|&(boost, _)| boost).map(|(_, units)| units).unwrap()
108}
109
110fn worker(input: &Input, shared: &Shared) {
111    while !shared.done.load(Ordering::Relaxed) {
112        // Get the next attack boost, incrementing it atomically for the next fight.
113        let boost = shared.boost.fetch_add(1, Ordering::Relaxed);
114
115        // If the reindeer wins then set the score and signal all threads to stop.
116        // Use a channel to queue all potential scores as another thread may already have sent a
117        // different value.
118        let (kind, units) = fight(input, boost);
119
120        if kind == Kind::Immune {
121            shared.done.store(true, Ordering::Relaxed);
122            let _unused = shared.tx.send((boost, units));
123        }
124    }
125}
126
127fn fight(input: &Input, boost: i32) -> (Kind, i32) {
128    let mut immune = input.immune.clone();
129    let mut infection = input.infection.clone();
130    let mut attacks = vec![None; immune.len() + infection.len()];
131
132    // Boost reindeer's immmune system.
133    immune.iter_mut().for_each(|group| group.damage += boost);
134
135    for turn in 1.. {
136        // Target selection phase.
137        let mut target_selection = |attacker: &[Group], defender: &mut [Group], kind: Kind| {
138            for (from, group) in attacker.iter().enumerate() {
139                let target = (0..defender.len())
140                    .filter(|&to| {
141                        defender[to].chosen < turn && group.actual_damage(&defender[to]) > 0
142                    })
143                    .max_by_key(|&to| {
144                        (
145                            group.actual_damage(&defender[to]),
146                            defender[to].effective_power(),
147                            defender[to].initiative,
148                        )
149                    });
150
151                if let Some(to) = target {
152                    // Attacks happen in descending order of initiative.
153                    let index = attacks.len() - group.initiative as usize;
154                    defender[to].chosen = turn;
155                    attacks[index] = Some((kind, from, to));
156                }
157            }
158        };
159
160        // Turn order is important.
161        immune.sort_unstable_by_key(Group::target_selection_order);
162        infection.sort_unstable_by_key(Group::target_selection_order);
163
164        target_selection(&immune, &mut infection, Kind::Immune);
165        target_selection(&infection, &mut immune, Kind::Infection);
166
167        // Attacking phase.
168        let mut killed = 0;
169
170        for next in &mut attacks {
171            if let Some((kind, from, to)) = *next {
172                if kind == Kind::Immune {
173                    killed += immune[from].attack(&mut infection[to]);
174                } else {
175                    killed += infection[from].attack(&mut immune[to]);
176                }
177                *next = None;
178            }
179        }
180
181        // It's possible to deadlock if groups become too weak to do any more damage.
182        if killed == 0 {
183            return (Kind::Draw, 0);
184        }
185
186        // Check for winner.
187        immune.retain(|group| group.units > 0);
188        infection.retain(|group| group.units > 0);
189
190        if immune.is_empty() {
191            return (Kind::Infection, infection.iter().map(|group| group.units).sum());
192        }
193        if infection.is_empty() {
194            return (Kind::Immune, immune.iter().map(|group| group.units).sum());
195        }
196    }
197
198    unreachable!()
199}
200
201/// Parsing the input relatively cleanly is a challenge by itself.
202fn parse_group<'a>(input: &'a str, mask: &mut impl FnMut(&'a str) -> u32) -> Vec<Group> {
203    let delimiters = [' ', '(', ')', ',', ';'];
204    input
205        .lines()
206        .skip(1)
207        .map(|line| {
208            let tokens: Vec<_> = line.split(delimiters).collect();
209
210            let units = tokens[0].signed();
211            let hit_points = tokens[4].signed();
212            let damage = tokens[tokens.len() - 6].signed();
213            let initiative = tokens[tokens.len() - 1].signed();
214            let attack = mask(tokens[tokens.len() - 5]);
215            let weak = parse_list(&tokens, "weak", mask);
216            let immune = parse_list(&tokens, "immune", mask);
217            let chosen = 0;
218
219            Group { units, hit_points, damage, initiative, weak, immune, attack, chosen }
220        })
221        .collect()
222}
223
224/// There can be any amount of weaknesses or immunities.
225fn parse_list<'a>(tokens: &[&'a str], start: &str, mask: &mut impl FnMut(&'a str) -> u32) -> u32 {
226    let end = ["weak", "immune", "with"];
227    let mut elements = 0;
228
229    if let Some(index) = tokens.iter().position(|&t| t == start) {
230        let mut index = index + 2;
231        while !end.contains(&tokens[index]) {
232            elements |= mask(tokens[index]);
233            index += 1;
234        }
235    }
236
237    elements
238}