mirror of
https://github.com/bertptrs/adventofcode.git
synced 2025-12-25 21:00:31 +01:00
244 lines
6.8 KiB
Rust
244 lines
6.8 KiB
Rust
use std::cmp::Reverse;
|
|
use std::io::BufRead;
|
|
use std::io::BufReader;
|
|
use std::io::Read;
|
|
|
|
use itertools::Itertools;
|
|
use regex::Regex;
|
|
|
|
use common::Solution;
|
|
|
|
#[derive(Default, Debug, Eq, PartialEq, Clone)]
|
|
struct Group {
|
|
power: u32,
|
|
count: u32,
|
|
hp: u32,
|
|
initiative: i32,
|
|
damage: String,
|
|
weaknesses: Vec<String>,
|
|
immunities: Vec<String>,
|
|
faction: char,
|
|
}
|
|
|
|
impl Group {
|
|
pub fn is_alive(&self) -> bool {
|
|
self.count > 0
|
|
}
|
|
|
|
pub fn damage_to(&self, other: &Group) -> u32 {
|
|
let damage = self.effective_power();
|
|
|
|
if other.weaknesses.contains(&self.damage) {
|
|
damage * 2
|
|
} else if other.immunities.contains(&self.damage) {
|
|
0
|
|
} else {
|
|
damage
|
|
}
|
|
}
|
|
|
|
pub fn effective_power(&self) -> u32 {
|
|
self.power * self.count
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct Day24 {
|
|
units: Vec<Group>,
|
|
}
|
|
|
|
impl Day24 {
|
|
pub fn new() -> Self {
|
|
Default::default()
|
|
}
|
|
|
|
fn read_input(&mut self, input: &mut dyn Read) {
|
|
let matcher = Regex::new(r"(\d+) units each with (\d+) hit points (\(([^)]+)\) )?with an attack that does (\d+) (\w+) damage at initiative (\d+)").unwrap();
|
|
let weakness_matcher = Regex::new(r"(weak|immune) to ([^;)]+)").unwrap();
|
|
let reader = BufReader::new(input);
|
|
let mut fac = 'X';
|
|
|
|
for line in reader.lines() {
|
|
let line = line.unwrap();
|
|
match line.as_str() {
|
|
"Immune System:" => fac = 'D',
|
|
"" => {}
|
|
"Infection:" => fac = 'I',
|
|
line => {
|
|
let caps = matcher.captures(line).unwrap_or_else(|| {
|
|
panic!("{}", line);
|
|
});
|
|
|
|
let mut group = Group {
|
|
count: caps[1].parse().unwrap(),
|
|
hp: caps[2].parse().unwrap(),
|
|
power: caps[5].parse().unwrap(),
|
|
damage: caps[6].to_string(),
|
|
initiative: caps[7].parse().unwrap(),
|
|
faction: fac,
|
|
..Default::default()
|
|
};
|
|
|
|
if let Some(caps) = caps.get(4) {
|
|
for modified in weakness_matcher.captures_iter(caps.as_str()) {
|
|
let target = match &modified[1] {
|
|
"weak" => &mut group.weaknesses,
|
|
"immune" => &mut group.immunities,
|
|
_ => panic!(),
|
|
};
|
|
|
|
for t in modified[2].split(", ") {
|
|
target.push(t.to_string());
|
|
}
|
|
}
|
|
}
|
|
self.units.push(group)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn simulate(&mut self) -> bool {
|
|
let mut order: Vec<usize> = (0..self.units.len()).collect();
|
|
order.sort_unstable_by_key(|&x| {
|
|
Reverse((self.units[x].effective_power(), self.units[x].initiative))
|
|
});
|
|
|
|
// select targets
|
|
let mut targets: Vec<Option<usize>> = vec![None; self.units.len()];
|
|
let mut is_targeted = vec![false; self.units.len()];
|
|
let mut changes = false;
|
|
|
|
for &i in &order {
|
|
let unit = &self.units[i];
|
|
if !unit.is_alive() {
|
|
continue;
|
|
}
|
|
let damage = self.units.iter().map(|x| unit.damage_to(x)).collect_vec();
|
|
let target = (0..self.units.len())
|
|
.filter(|&x| {
|
|
!is_targeted[x]
|
|
&& self.units[x].faction != unit.faction
|
|
&& self.units[x].is_alive()
|
|
&& damage[x] > 0
|
|
})
|
|
.max_by_key(|&x| {
|
|
(
|
|
damage[x],
|
|
self.units[x].effective_power(),
|
|
self.units[x].initiative,
|
|
)
|
|
});
|
|
|
|
if let Some(target) = target {
|
|
targets[i] = Some(target);
|
|
is_targeted[target] = true;
|
|
}
|
|
}
|
|
|
|
order.sort_unstable_by_key(|&x| Reverse(self.units[x].initiative));
|
|
|
|
for attacker in order {
|
|
if !self.units[attacker].is_alive() {
|
|
continue;
|
|
}
|
|
|
|
if let Some(id) = targets[attacker] {
|
|
let damage = self.units[attacker].damage_to(&self.units[id]);
|
|
let defender = &mut self.units[id];
|
|
|
|
let losses = damage / defender.hp;
|
|
if losses > 0 {
|
|
defender.count = defender.count.saturating_sub(losses);
|
|
changes = true;
|
|
}
|
|
}
|
|
}
|
|
changes
|
|
}
|
|
|
|
fn both_alive(&self) -> bool {
|
|
let mut seen = None;
|
|
|
|
for unit in self.units.iter().filter(|g| g.is_alive()) {
|
|
if let Some(seen) = seen {
|
|
if seen != unit.faction {
|
|
return true;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
seen = Some(unit.faction);
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
fn full_simulation(&mut self) {
|
|
let mut changes = true;
|
|
while self.both_alive() && changes {
|
|
changes = self.simulate();
|
|
}
|
|
}
|
|
|
|
fn faction_won(&self, faction: char) -> bool {
|
|
if self.both_alive() {
|
|
false
|
|
} else {
|
|
self.units
|
|
.iter()
|
|
.filter(|x| x.is_alive())
|
|
.all(|x| x.faction == faction)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Solution for Day24 {
|
|
fn part1(&mut self, input: &mut dyn Read) -> String {
|
|
self.read_input(input);
|
|
self.full_simulation();
|
|
|
|
let result: u32 = self.units.iter().map(|x| x.count).sum();
|
|
result.to_string()
|
|
}
|
|
|
|
fn part2(&mut self, input: &mut dyn Read) -> String {
|
|
self.read_input(input);
|
|
let original = self.units.clone();
|
|
|
|
for boost in 1.. {
|
|
self.units = original.clone();
|
|
for unit in self.units.iter_mut().filter(|unit| unit.faction == 'D') {
|
|
unit.power += boost;
|
|
}
|
|
|
|
self.full_simulation();
|
|
if self.faction_won('D') {
|
|
let result: u32 = self.units.iter().map(|x| x.count).sum();
|
|
return result.to_string();
|
|
}
|
|
}
|
|
unreachable!();
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use common::Solution;
|
|
use day24::Day24;
|
|
|
|
const SAMPLE_INPUT: &[u8] = include_bytes!("samples/24.txt");
|
|
|
|
#[test]
|
|
fn sample_part1() {
|
|
let mut instance = Day24::new();
|
|
assert_eq!("5216", instance.part1(&mut SAMPLE_INPUT))
|
|
}
|
|
|
|
#[test]
|
|
fn sample_part2() {
|
|
let mut instance = Day24::new();
|
|
assert_eq!("51", instance.part2(&mut SAMPLE_INPUT))
|
|
}
|
|
}
|