advent_of_code/year2018/
day24.rs1use crate::input::Input;
2
3#[derive(Copy, Clone, PartialEq)]
4enum AttackType {
5 Bludgeoning,
6 Cold,
7 Fire,
8 Radiation,
9 Slashing,
10}
11
12impl AttackType {
13 fn new(name: &str) -> Result<Self, String> {
14 Ok(match name {
15 "bludgeoning" => Self::Bludgeoning,
16 "cold" => Self::Cold,
17 "fire" => Self::Fire,
18 "radiation" => Self::Radiation,
19 "slashing" => Self::Slashing,
20 _ => {
21 return Err("Invalid attack type".to_string());
22 }
23 })
24 }
25}
26
27#[derive(Clone)]
28struct ArmyGroup {
29 id: i32,
30 units: i32,
31 hit_points: i32,
32 attack_damage: i32,
33 attack_type: AttackType,
34 initiative: i32,
35 weaknesses: Vec<AttackType>,
36 immunities: Vec<AttackType>,
37 immune_system: bool,
38 attacked_by: i32,
39}
40
41impl ArmyGroup {
42 const fn is_alive(&self) -> bool {
43 self.units > 0
44 }
45
46 fn parse(input_string: &str) -> Result<Vec<Self>, String> {
47 let mut id_generator = 0;
48 let mut immune_system = true;
49 let mut groups: Vec<Self> = Vec::new();
50
51 let error = |_| "Invalid input";
52
53 for line in input_string.lines().skip(1) {
54 if line.is_empty() {
55 } else if line == "Infection:" {
57 immune_system = false;
58 } else {
59 let main_parts: Vec<&str> = line.split(['(', ')']).collect();
62
63 let mut weaknesses = Vec::new();
64 let mut immunities = Vec::new();
65 let attack_damage;
66 let attack_type;
67 let initiative;
68
69 let before_parentheses: Vec<&str> = main_parts[0].split_whitespace().collect();
70 let units = before_parentheses[0].parse::<i32>().map_err(error)?;
71 let hit_points = before_parentheses[4].parse::<i32>().map_err(error)?;
72
73 if main_parts.len() == 1 {
74 let words: Vec<&str> = line.split_whitespace().collect();
76 if words.len() != 18 {
77 return Err("Invalid input".to_string());
78 }
79 attack_damage = words[12].parse::<i32>().map_err(error)?;
80 attack_type = AttackType::new(words[13])?;
81 initiative = words[17].parse::<i32>().map_err(error)?;
82 } else {
83 if main_parts.len() != 3 {
84 return Err("Invalid input".to_string());
85 }
86 let after_parentheses: Vec<&str> = main_parts[2].split_whitespace().collect();
87 if before_parentheses.len() != 7 || after_parentheses.len() != 11 {
88 return Err("Invalid input".to_string());
89 }
90
91 attack_damage = after_parentheses[5].parse::<i32>().map_err(error)?;
92 attack_type = AttackType::new(after_parentheses[6])?;
93 initiative = after_parentheses[10].parse::<i32>().map_err(error)?;
94
95 for part in main_parts[1].split("; ") {
96 if part.starts_with("weak to") {
97 for s in part[8..].split(", ") {
98 weaknesses.push(AttackType::new(s)?);
99 }
100 } else {
101 for s in part[10..].split(", ") {
102 immunities.push(AttackType::new(s)?);
103 }
104 }
105 }
106 }
107
108 id_generator += 1;
109 let group = Self {
110 id: id_generator,
111 units,
112 hit_points,
113 attack_damage,
114 attack_type,
115 initiative,
116 weaknesses,
117 immunities,
118 immune_system,
119 attacked_by: -1,
120 };
121 groups.push(group);
122 }
123 }
124 Ok(groups)
125 }
126
127 const fn effective_power(&self) -> i32 {
128 self.units * self.attack_damage
129 }
130
131 fn damage_when_attacked_by(&self, effective_power: i32, attack_type: AttackType) -> i32 {
132 effective_power
133 * if self.immunities.contains(&attack_type) {
134 0
135 } else if self.weaknesses.contains(&attack_type) {
136 2
137 } else {
138 1
139 }
140 }
141
142 fn resolve_attack(&mut self, attacker_effective_power: i32, attack_type: AttackType) -> bool {
143 let damage = self.damage_when_attacked_by(attacker_effective_power, attack_type);
144 let killed_units = damage / self.hit_points;
145 self.units -= killed_units;
146 killed_units > 0
147 }
148}
149
150fn execute_battle(mut groups: Vec<ArmyGroup>) -> Vec<ArmyGroup> {
151 loop {
152 groups.sort_unstable_by(|a, b| {
154 b.effective_power()
155 .cmp(&a.effective_power())
156 .then_with(|| b.initiative.cmp(&a.initiative))
157 });
158 for g in groups.iter_mut() {
159 g.attacked_by = -1;
160 }
161
162 for i in 0..groups.len() {
163 let (attacker_effective_power, attack_type, attacking_group_id, immune_system) = {
164 let g = &groups[i];
165 (g.effective_power(), g.attack_type, g.id, g.immune_system)
166 };
167
168 if let Some(attacked_group) = groups
169 .iter_mut()
170 .filter(|g| g.immune_system != immune_system && g.attacked_by == -1)
172 .max_by(|a, b| {
176 let damage_to_a =
177 a.damage_when_attacked_by(attacker_effective_power, attack_type);
178 let damage_to_b =
179 b.damage_when_attacked_by(attacker_effective_power, attack_type);
180 damage_to_a
181 .cmp(&damage_to_b)
182 .then_with(|| a.effective_power().cmp(&b.effective_power()))
183 .then_with(|| a.initiative.cmp(&b.initiative))
184 })
185 {
186 if attacked_group.damage_when_attacked_by(attacker_effective_power, attack_type) > 0
188 {
189 attacked_group.attacked_by = attacking_group_id;
190 }
191 }
192 }
193
194 let mut any_killed_units = false;
196 groups.sort_unstable_by(|a, b| b.initiative.cmp(&a.initiative));
197 for i in 0..groups.len() {
198 let (attacking_group_id, is_alive, effective_power, attack_type) = {
199 let g = &groups[i];
200 (g.id, g.is_alive(), g.effective_power(), g.attack_type)
201 };
202 if is_alive {
203 for other_group in groups.iter_mut() {
204 if other_group.attacked_by == attacking_group_id
205 && other_group.resolve_attack(effective_power, attack_type)
206 {
207 any_killed_units = true;
208 }
209 }
210 }
211 }
212
213 if !any_killed_units {
214 break;
215 }
216
217 groups.retain(ArmyGroup::is_alive);
218
219 let alive_sides = groups.iter().fold((false, false), |acc, g| {
220 let mut result = acc;
221 if g.immune_system {
222 result.0 = true;
223 } else {
224 result.1 = true;
225 }
226 result
227 });
228 if alive_sides != (true, true) {
229 break;
230 }
231 }
232
233 groups
234}
235
236pub fn solve(input: &Input) -> Result<i32, String> {
237 let initial_groups = ArmyGroup::parse(input.text)?;
238
239 if input.is_part_one() {
240 let groups = execute_battle(initial_groups);
241 let result = groups.iter().fold(0, |acc, g| acc + g.units);
242 Ok(result)
243 } else {
244 let mut boost = 1;
245 loop {
246 let mut groups = initial_groups.clone();
247 for g in groups.iter_mut() {
248 if g.immune_system {
249 g.attack_damage += boost;
250 }
251 }
252
253 let groups = execute_battle(groups);
254
255 if groups.iter().all(|g| g.immune_system) {
256 let result = groups.iter().fold(0, |acc, g| acc + g.units);
257 return Ok(result);
258 }
259
260 boost += 1;
261 }
262 }
263}
264
265#[test]
266fn tests() {
267 use crate::input::{test_part_one, test_part_two};
268
269 test_part_one!("Immune System:
27017 units each with 5390 hit points (weak to radiation, bludgeoning) with an attack that does 4507 fire damage at initiative 2
271989 units each with 1274 hit points (immune to fire; weak to bludgeoning, slashing) with an attack that does 25 slashing damage at initiative 3
272
273Infection:
274801 units each with 4706 hit points (weak to radiation) with an attack that does 116 bludgeoning damage at initiative 1
2754485 units each with 2961 hit points (immune to radiation; weak to fire, cold) with an attack that does 12 slashing damage at initiative 4" => 5216);
276
277 let input = include_str!("day24_input.txt");
278 test_part_one!(input => 26914);
279 test_part_two!(input => 862);
280}