Skip to main content

advent_of_code/year2018/
day24.rs

1use 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                // Skip empty line.
56            } else if line == "Infection:" {
57                immune_system = false;
58            } else {
59                // "17 units each with 5390 hit points (weak to radiation, bludgeoning) with
60                // an attack that does 4507 fire damage at initiative 2".
61                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                    // No parenthesis.
75                    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        // Target selection.
153        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                // Only consider attacking non-attacked enemies:
171                .filter(|g| g.immune_system != immune_system && g.attacked_by == -1)
172                // If an attacking group is considering two defending groups to which it would deal equal damage,
173                // it chooses to target the defending group with the largest effective power; if there is still a
174                // tie, it chooses the defending group with the highest initiative:
175                .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 it cannot deal any defending groups damage, it does not choose a target:
187                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        // Attacking.
195        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    test_part_one!("Immune System:
26817 units each with 5390 hit points (weak to radiation, bludgeoning) with an attack that does 4507 fire damage at initiative 2
269989 units each with 1274 hit points (immune to fire; weak to bludgeoning, slashing) with an attack that does 25 slashing damage at initiative 3
270
271Infection:
272801 units each with 4706 hit points (weak to radiation) with an attack that does 116 bludgeoning damage at initiative 1
2734485 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);
274
275    let input = include_str!("day24_input.txt");
276    test_part_one!(input => 26914);
277    test_part_two!(input => 862);
278}