diff --git a/2022/src/day19.rs b/2022/src/day19.rs index acd2238..f4ea33b 100644 --- a/2022/src/day19.rs +++ b/2022/src/day19.rs @@ -1,9 +1,188 @@ -use anyhow::Result; +use std::ops::Add; +use std::ops::Sub; -pub fn part1(_input: &[u8]) -> Result { - anyhow::bail!("not implemented") +use anyhow::Result; +use nom::bytes::complete::tag; +use nom::character::complete::multispace1; +use nom::character::streaming::alpha1; +use nom::combinator::map_res; +use nom::combinator::opt; +use nom::multi::many1; +use nom::sequence::delimited; +use nom::sequence::preceded; +use nom::sequence::separated_pair; +use nom::sequence::terminated; +use nom::sequence::tuple; +use nom::IResult; + +use crate::common::parse_input; + +#[repr(usize)] +#[derive(Clone, Copy)] +enum Mineral { + Ore, + Clay, + Obsidian, + Geode, +} + +impl TryFrom<&'_ [u8]> for Mineral { + type Error = String; + + fn try_from(value: &'_ [u8]) -> std::result::Result { + dbg!(String::from_utf8_lossy(value)); + match value { + b"ore" => Ok(Self::Ore), + b"clay" => Ok(Self::Clay), + b"obsidian" => Ok(Self::Obsidian), + b"geode" => Ok(Self::Geode), + other => Err(format!( + "Invalid mineral '{}'", + String::from_utf8_lossy(other) + )), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +struct Resources([u8; 4]); + +impl Resources { + fn enough_for(self, other: Self) -> bool { + self.0.iter().zip(&other.0).all(|(a, b)| a >= b) + } +} + +impl Sub for Resources { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + Self(std::array::from_fn(|i| self.0[i] - rhs.0[i])) + } +} + +impl Add<[u8; 4]> for Resources { + type Output = Self; + + fn add(self, rhs: [u8; 4]) -> Self::Output { + Self(std::array::from_fn(|i| self.0[i] + rhs[i])) + } +} + +#[derive(Debug)] +struct BluePrint { + id: u32, + costs: [Resources; 4], +} + +impl BluePrint { + pub fn max_geodes(&self) -> u8 { + self.max_geodes_recursive(24, 0, [1, 0, 0, 0], Resources::default()) + } + + fn max_geodes_recursive( + &self, + time_left: u32, + // forbidden is a bitset for convenience + forbidden: u8, + machines: [u8; 4], + resources: Resources, + ) -> u8 { + if time_left <= 1 { + return resources.0[3] + machines[3] * (time_left as u8); + } + + let resources_after = resources + machines; + + let mut best = 0; + + let mut can_buy = 0; + + for (i, &cost) in self.costs.iter().enumerate() { + if ((1 << i) & forbidden) == 0 && resources.enough_for(cost) { + can_buy |= 1 << i; + let mut new_machines = machines; + new_machines[i] += 1; + + best = best.max(self.max_geodes_recursive( + time_left - 1, + 0, + new_machines, + resources_after - cost, + )) + } + } + + best.max(self.max_geodes_recursive( + time_left - 1, + forbidden | can_buy, + machines, + resources_after, + )) + } +} + +fn parse_blueprint(input: &[u8]) -> IResult<&[u8], BluePrint> { + use nom::character::complete::u32; + + fn parse_mineral(input: &[u8]) -> IResult<&[u8], Mineral> { + map_res(alpha1, Mineral::try_from)(input) + } + + fn parse_cost(input: &[u8]) -> IResult<&[u8], (u8, Mineral)> { + separated_pair(nom::character::complete::u8, tag(" "), parse_mineral)(input) + } + + let (mut input, id) = + terminated(delimited(tag("Blueprint "), u32, tag(":")), multispace1)(input)?; + + let mut costs: [Resources; 4] = Default::default(); + + let mut parse_robot = terminated( + tuple(( + preceded(tag("Each "), parse_mineral), + preceded(tag(" robot costs "), parse_cost), + terminated(opt(preceded(tag(" and "), parse_cost)), tag(".")), + )), + multispace1, + ); + + for _ in 0..4 { + let (remaining, (element, (amount1, req1), cost2)) = parse_robot(input)?; + input = remaining; + + costs[element as usize].0[req1 as usize] = amount1; + + if let Some((amount2, req2)) = cost2 { + costs[element as usize].0[req2 as usize] = amount2; + } + } + + Ok((input, BluePrint { id, costs })) +} + +pub fn part1(input: &[u8]) -> Result { + let blueprints = parse_input(input, many1(parse_blueprint))?; + + Ok(blueprints + .into_iter() + .map(|bp| dbg!(bp.max_geodes()) as u32 * bp.id) + .sum::() + .to_string()) } pub fn part2(_input: &[u8]) -> Result { anyhow::bail!("not implemented") } + +#[cfg(test)] +mod tests { + use super::*; + + const SAMPLE: &[u8] = include_bytes!("./samples/19.txt"); + + #[test] + fn sample_part1() { + assert_eq!(part1(SAMPLE).unwrap(), "33"); + } +}