Skip to content

Commit 7a3a163

Browse files
committed
2018 day 24
1 parent e1b709a commit 7a3a163

File tree

3 files changed

+251
-0
lines changed

3 files changed

+251
-0
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Immune System:
2+
17 units each with 5390 hit points (weak to radiation, bludgeoning) with an attack that does 4507 fire damage at initiative 2
3+
989 units each with 1274 hit points (immune to fire; weak to bludgeoning, slashing) with an attack that does 25 slashing damage at initiative 3
4+
5+
Infection:
6+
801 units each with 4706 hit points (weak to radiation) with an attack that does 116 bludgeoning damage at initiative 1
7+
4485 units each with 2961 hit points (immune to radiation; weak to fire, cold) with an attack that does 12 slashing damage at initiative 4

crates/year2018/src/day24.rs

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
use std::cell::Cell;
2+
use std::cmp::Ordering;
3+
use utils::prelude::*;
4+
5+
/// Simulating fight outcomes between a reindeer's immune system and an infection.
6+
#[derive(Clone, Debug)]
7+
pub struct Day24 {
8+
immune: Vec<Group>,
9+
infection: Vec<Group>,
10+
}
11+
12+
#[derive(Clone, Debug, PartialEq, Eq)]
13+
struct 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)]
23+
enum Modifier {
24+
// Values match damage modifiers
25+
Immune = 0,
26+
#[default]
27+
Normal = 1,
28+
Weak = 2,
29+
}
30+
31+
parser::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)]
44+
enum Army {
45+
Immune,
46+
Infection,
47+
}
48+
49+
impl 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+
212+
impl 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+
224+
impl 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+
234+
impl PartialOrd for Group {
235+
#[inline]
236+
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
237+
Some(self.cmp(other))
238+
}
239+
}
240+
241+
examples!(Day24 -> (u32, u32) [
242+
{file: "day24_example0.txt", part1: 5216, part2: 51},
243+
]);

crates/year2018/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@ utils::year!(2018 => year2018, ${
2727
21 => day21::Day21,
2828
22 => day22::Day22,
2929
23 => day23::Day23,
30+
24 => day24::Day24,
3031
});

0 commit comments

Comments
 (0)