Skip to content

Commit

Permalink
Year 2018 Day 24
Browse files Browse the repository at this point in the history
  • Loading branch information
maneatingape committed Aug 31, 2024
1 parent 959b329 commit 671d2b2
Show file tree
Hide file tree
Showing 7 changed files with 269 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ Performance is reasonable even on older hardware, for example a 2011 MacBook Pro
| 21 | [Chronal Conversion](https://adventofcode.com/2018/day/21) | [Source](src/year2018/day21.rs) | 65 |
| 22 | [Mode Maze](https://adventofcode.com/2018/day/22) | [Source](src/year2018/day22.rs) | 3410 |
| 23 | [Experimental Emergency Teleportation](https://adventofcode.com/2018/day/23) | [Source](src/year2018/day23.rs) | 515 |
| 24 | [Immune System Simulator 20XX](https://adventofcode.com/2018/day/24) | [Source](src/year2018/day24.rs) | 2054 |

## 2017

Expand Down
1 change: 1 addition & 0 deletions benches/benchmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ mod year2018 {
benchmark!(year2018, day21);
benchmark!(year2018, day22);
benchmark!(year2018, day23);
benchmark!(year2018, day24);
}

mod year2019 {
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ pub mod year2018 {
pub mod day21;
pub mod day22;
pub mod day23;
pub mod day24;
}

/// # Rescue Santa from deep space with a solar system voyage.
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ fn year2018() -> Vec<Solution> {
solution!(year2018, day21),
solution!(year2018, day22),
solution!(year2018, day23),
solution!(year2018, day24),
]
}

Expand Down
242 changes: 242 additions & 0 deletions src/year2018/day24.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
//! # Immune System Simulator 20XX
//!
//! Similar to [`Day 15`] we implement the rules precisely, paying attention to edge cases.
//!
//! In particular during part two, it's possible for a fight to end in a draw, if both armies
//! become too weak to destroy any further units. As each fight is independent, we find the
//! minimum boost value with a multithreaded parallel search.
//!
//! [`Day 15`]: crate::year2018::day15
use crate::util::hash::*;
use crate::util::parse::*;
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
use std::sync::mpsc::{channel, Sender};
use std::thread;

pub struct Input {
immune: Vec<Group>,
infection: Vec<Group>,
}

#[derive(Clone, Copy, PartialEq, Eq)]
enum Kind {
Immune,
Infection,
Draw,
}

#[derive(Clone, Copy)]
struct Group {
units: i32,
hit_points: i32,
damage: i32,
initiative: i32,
weak: u32,
immune: u32,
attack: u32,
chosen: u32,
}

/// Convenience functions.
impl Group {
fn effective_power(&self) -> i32 {
self.units * self.damage
}

/// Attack types are stored as a bitmask for quick comparison.
fn actual_damage(&self, other: &Group) -> i32 {
if self.attack & other.weak != 0 {
2 * self.effective_power()
} else if self.attack & other.immune == 0 {
self.effective_power()
} else {
0
}
}

fn target_selection_order(&self) -> (i32, i32) {
(-self.effective_power(), -self.initiative)
}

fn attack(&self, defender: &mut Self) -> i32 {
// Clamp damage to 0 as units may be negative,
// if this unit was wiped out in an earlier attack.
let damage = self.actual_damage(defender).max(0);
let amount = damage / defender.hit_points;
defender.units -= amount;
amount
}
}

/// Shared between threads for part two.
struct Shared {
done: AtomicBool,
boost: AtomicI32,
tx: Sender<(i32, i32)>,
}

pub fn parse<'a>(input: &'a str) -> Input {
// Use a bitmask to store each possible attack type.
let mut elements = FastMap::new();
let mut mask = |key: &'a str| {
let next = 1 << elements.len();
*elements.entry(key).or_insert(next)
};

let (first, second) = input.split_once("\n\n").unwrap();
let immune = parse_group(first, &mut mask);
let infection = parse_group(second, &mut mask);
Input { immune, infection }
}

pub fn part1(input: &Input) -> i32 {
let (_, units) = fight(input, 0);
units
}

pub fn part2(input: &Input) -> i32 {
let (tx, rx) = channel();
let shared = Shared { done: AtomicBool::new(false), boost: AtomicI32::new(1), tx };

// Use as many cores as possible to parallelize the search.
thread::scope(|scope| {
for _ in 0..thread::available_parallelism().unwrap().get() {
scope.spawn(|| worker(input, &shared));
}
});

// Hang up the channel.
drop(shared.tx);
// Find lowest possible power.
rx.iter().min_by_key(|&(boost, _)| boost).map(|(_, units)| units).unwrap()
}

fn worker(input: &Input, shared: &Shared) {
while !shared.done.load(Ordering::Relaxed) {
// Get the next attack boost, incrementing it atomically for the next fight.
let boost = shared.boost.fetch_add(1, Ordering::Relaxed);

// If the reindeer wins then set the score and signal all threads to stop.
// Use a channel to queue all potential scores as another thread may already have sent a
// different value.
let (kind, units) = fight(input, boost);

if kind == Kind::Immune {
shared.done.store(true, Ordering::Relaxed);
let _unused = shared.tx.send((boost, units));
}
}
}

fn fight(input: &Input, boost: i32) -> (Kind, i32) {
let mut immune = input.immune.clone();
let mut infection = input.infection.clone();
let mut attacks = vec![None; immune.len() + infection.len()];

// Boost reindeer's immmune system.
immune.iter_mut().for_each(|group| group.damage += boost);

for turn in 1.. {
// Target selection phase.
let mut target_selection = |attacker: &[Group], defender: &mut [Group], kind: Kind| {
for (from, group) in attacker.iter().enumerate() {
let target = (0..defender.len())
.filter(|&to| {
defender[to].chosen < turn && group.actual_damage(&defender[to]) > 0
})
.max_by_key(|&to| {
(
group.actual_damage(&defender[to]),
defender[to].effective_power(),
defender[to].initiative,
)
});

if let Some(to) = target {
// Attacks happen in descending order of initiative.
let index = attacks.len() - group.initiative as usize;
defender[to].chosen = turn;
attacks[index] = Some((kind, from, to));
}
}
};

// Turn order is important.
immune.sort_unstable_by_key(Group::target_selection_order);
infection.sort_unstable_by_key(Group::target_selection_order);

target_selection(&immune, &mut infection, Kind::Immune);
target_selection(&infection, &mut immune, Kind::Infection);

// Attacking phase.
let mut killed = 0;

for next in &mut attacks {
if let Some((kind, from, to)) = *next {
if kind == Kind::Immune {
killed += immune[from].attack(&mut infection[to]);
} else {
killed += infection[from].attack(&mut immune[to]);
}
*next = None;
}
}

// It's possible to deadlock if groups become too weak to do any more damage.
if killed == 0 {
return (Kind::Draw, 0);
}

// Check for winner.
immune.retain(|group| group.units > 0);
infection.retain(|group| group.units > 0);

if immune.is_empty() {
return (Kind::Infection, infection.iter().map(|group| group.units).sum());
}
if infection.is_empty() {
return (Kind::Immune, immune.iter().map(|group| group.units).sum());
}
}

unreachable!()
}

/// Parsing the input relatively cleanly is a challenge by itself.
fn parse_group<'a>(input: &'a str, mask: &mut impl FnMut(&'a str) -> u32) -> Vec<Group> {
let delimiters = [' ', '(', ')', ',', ';'];
input
.lines()
.skip(1)
.map(|line| {
let tokens: Vec<_> = line.split(delimiters).collect();

let units = tokens[0].signed();
let hit_points = tokens[4].signed();
let damage = tokens[tokens.len() - 6].signed();
let initiative = tokens[tokens.len() - 1].signed();
let attack = mask(tokens[tokens.len() - 5]);
let weak = parse_list(&tokens, "weak", mask);
let immune = parse_list(&tokens, "immune", mask);
let chosen = 0;

Group { units, hit_points, damage, initiative, weak, immune, attack, chosen }
})
.collect()
}

/// There can be any amount of weaknesses or immunities.
fn parse_list<'a>(tokens: &[&'a str], start: &str, mask: &mut impl FnMut(&'a str) -> u32) -> u32 {
let end = ["weak", "immune", "with"];
let mut elements = 0;

if let Some(index) = tokens.iter().position(|&t| t == start) {
let mut index = index + 2;
while !end.contains(&tokens[index]) {
elements |= mask(tokens[index]);
index += 1;
}
}

elements
}
1 change: 1 addition & 0 deletions tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ mod year2018 {
mod day21_test;
mod day22_test;
mod day23_test;
mod day24_test;
}

mod year2019 {
Expand Down
22 changes: 22 additions & 0 deletions tests/year2018/day24_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use aoc::year2018::day24::*;

const EXAMPLE: &str = "\
Immune System:
17 units each with 5390 hit points (weak to radiation, bludgeoning) with an attack that does 4507 fire damage at initiative 2
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
Infection:
801 units each with 4706 hit points (weak to radiation) with an attack that does 116 bludgeoning damage at initiative 1
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";

#[test]
fn part1_test() {
let input = parse(EXAMPLE);
assert_eq!(part1(&input), 5216);
}

#[test]
fn part2_test() {
let input = parse(EXAMPLE);
assert_eq!(part2(&input), 51);
}

0 comments on commit 671d2b2

Please sign in to comment.