Skip to main content

year2018/
day24.rs

1use std::cell::Cell;
2use std::cmp::Ordering;
3use utils::prelude::*;
4
5/// Simulating fight outcomes between a reindeer's immune system and an infection.
6#[derive(Clone, Debug)]
7pub struct Day24 {
8    immune: Vec<Group>,
9    infection: Vec<Group>,
10}
11
12#[derive(Clone, Debug, PartialEq, Eq)]
13struct Group {
14    units: u32,
15    hit_points: u32,
16    modifiers: [Modifier; AttackType::COUNT],
17    damage: u32,
18    damage_type: AttackType,
19    initiative: u32,
20}
21
22#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
23enum Modifier {
24    // Values match damage modifiers
25    Immune = 0,
26    #[default]
27    Normal = 1,
28    Weak = 2,
29}
30
31parser::parsable_enum!(
32    #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
33    enum AttackType {
34        #[default]
35        "bludgeoning" => Bludgeoning,
36        "cold" => Cold,
37        "fire" => Fire,
38        "radiation" => Radiation,
39        "slashing" => Slashing,
40    }
41);
42
43#[derive(Copy, Clone, Debug)]
44enum Army {
45    Immune,
46    Infection,
47}
48
49impl Day24 {
50    pub fn new(input: &str, _: InputType) -> Result<Self, InputError> {
51        let attack_types = AttackType::PARSER.repeat_arrayvec::<{ AttackType::COUNT }, _>(", ", 1);
52        let modifiers = parser::one_of((
53            attack_types
54                .with_prefix("(immune to ")
55                .then(attack_types.with_prefix("; weak to ").optional())
56                .with_suffix(") ")
57                .map(|(immune, weak)| (immune, weak.unwrap_or_default())),
58            attack_types
59                .with_prefix("(weak to ")
60                .then(attack_types.with_prefix("; immune to ").optional())
61                .with_suffix(") ")
62                .map(|(weak, immune)| (immune.unwrap_or_default(), weak)),
63            parser::noop().map(|_| Default::default()),
64        ))
65        .map_res(|(immune, weak)| {
66            let mut modifiers = [Modifier::Normal; AttackType::COUNT];
67            for (attack_types, modifier) in [(immune, Modifier::Immune), (weak, Modifier::Weak)] {
68                for &attack_type in &attack_types {
69                    if modifiers[attack_type] != Modifier::Normal {
70                        return Err("duplicate attack type in modifiers");
71                    }
72                    modifiers[attack_type] = modifier;
73                }
74            }
75            Ok(modifiers)
76        });
77
78        let initiative_mask = Cell::new(0u32);
79        let group = parser::number_range(1..=99999)
80            .with_suffix(" units each with ")
81            .then(parser::number_range(1..=99999).with_suffix(" hit points "))
82            .then(modifiers.with_suffix("with an attack that does "))
83            .then(parser::number_range(1..=99999).with_suffix(b' '))
84            .then(AttackType::PARSER.with_suffix(" damage at initiative "))
85            .then(parser::number_range(1..=31))
86            .map_res(
87                |(units, hit_points, modifiers, damage, damage_type, initiative)| {
88                    let initiative = initiative - 1;
89                    if initiative_mask.get() & (1 << initiative) != 0 {
90                        return Err("duplicate initiative");
91                    }
92                    initiative_mask.set(initiative_mask.get() | (1 << initiative));
93                    Ok(Group {
94                        units,
95                        hit_points,
96                        modifiers,
97                        damage,
98                        damage_type,
99                        initiative,
100                    })
101                },
102            )
103            .repeat(parser::eol(), 1);
104
105        let (immune, infection) = group
106            .with_prefix("Immune System:".with_eol())
107            .with_eol()
108            .with_eol()
109            .then(group.with_prefix("Infection:".with_eol()))
110            .parse_complete(input)?;
111
112        if initiative_mask.get().trailing_ones() as usize != immune.len() + infection.len() {
113            return Err(InputError::new(
114                input,
115                0,
116                "expected initiative values to be sequential",
117            ));
118        }
119
120        Ok(Self { immune, infection })
121    }
122
123    #[must_use]
124    pub fn part1(&self) -> u32 {
125        if let Some((units, _)) = self.fight(0) {
126            return units;
127        }
128        panic!("no solution found");
129    }
130
131    #[must_use]
132    pub fn part2(&self) -> u32 {
133        for boost in 1.. {
134            if let Some((units, Army::Immune)) = self.fight(boost) {
135                return units;
136            }
137        }
138        panic!("no solution found");
139    }
140
141    fn fight(&self, boost: u32) -> Option<(u32, Army)> {
142        let mut immune = self.immune.clone();
143        let mut infection = self.infection.clone();
144        let groups = immune.len() + infection.len();
145
146        immune.iter_mut().for_each(|g| g.damage += boost);
147
148        let mut attacks = [None; 32];
149        loop {
150            immune.sort_unstable();
151            infection.sort_unstable();
152
153            let mut taken = 0u32;
154            for (attacking, defending, army) in [
155                (&immune, &infection, Army::Immune),
156                (&infection, &immune, Army::Infection),
157            ] {
158                for (attack_idx, attack_group) in attacking.iter().enumerate() {
159                    if let Some((defend_idx, defend_group)) = defending
160                        .iter()
161                        .enumerate()
162                        .filter(|(_, defend_group)| {
163                            taken & (1u32 << defend_group.initiative) == 0
164                                && attack_group.damage(defend_group) > 0
165                        })
166                        .max_by_key(|(_, defend_group)| {
167                            (
168                                attack_group.damage(defend_group),
169                                defend_group.effective_power(),
170                                defend_group.initiative,
171                            )
172                        })
173                    {
174                        attacks[attack_group.initiative as usize] =
175                            Some((attack_idx, defend_idx, army));
176                        taken |= 1u32 << defend_group.initiative;
177                    }
178                }
179            }
180
181            let mut changed = false;
182            for (attack_idx, defend_idx, army) in
183                attacks[..groups].iter_mut().rev().flat_map(Option::take)
184            {
185                let (attack_group, defend_group) = match army {
186                    Army::Immune => (&immune[attack_idx], &mut infection[defend_idx]),
187                    Army::Infection => (&infection[attack_idx], &mut immune[defend_idx]),
188                };
189                let killed = attack_group.damage(defend_group) / defend_group.hit_points;
190                defend_group.units = defend_group.units.saturating_sub(killed);
191                changed |= killed > 0;
192            }
193
194            if !changed {
195                return None;
196            }
197
198            immune.retain(|g| g.units > 0);
199            infection.retain(|g| g.units > 0);
200
201            if immune.is_empty() && infection.is_empty() {
202                return None;
203            } else if immune.is_empty() {
204                return Some((infection.iter().map(|g| g.units).sum(), Army::Infection));
205            } else if infection.is_empty() {
206                return Some((immune.iter().map(|g| g.units).sum(), Army::Immune));
207            }
208        }
209    }
210}
211
212impl Group {
213    #[inline]
214    fn effective_power(&self) -> u32 {
215        self.units * self.damage
216    }
217
218    #[inline]
219    fn damage(&self, target: &Group) -> u32 {
220        self.effective_power() * target.modifiers[self.damage_type] as u32
221    }
222}
223
224impl Ord for Group {
225    #[inline]
226    fn cmp(&self, other: &Self) -> Ordering {
227        self.effective_power()
228            .cmp(&other.effective_power())
229            .reverse()
230            .then(self.initiative.cmp(&other.initiative).reverse())
231    }
232}
233
234impl PartialOrd for Group {
235    #[inline]
236    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
237        Some(self.cmp(other))
238    }
239}
240
241examples!(Day24 -> (u32, u32) [
242    {file: "day24_example0.txt", part1: 5216, part2: 51},
243]);