1use std::cell::Cell;
2use std::cmp::Ordering;
3use utils::prelude::*;
4
5#[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 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]);