use std::{cell::Cell, marker::PhantomData, ops::Index}; use crate::common::file::read_lines; use super::template::{DayTrait, ResultType}; use itertools::Itertools; use thiserror::Error; const DAY_NUMBER: usize = 17; const STACK_WIDTH: usize = 7; pub struct Day; impl DayTrait for Day { fn get_day_number(&self) -> usize { DAY_NUMBER } fn part1(&self, lines: &[String]) -> anyhow::Result { let pushes = Dispenser::new(lines[0].chars().map(|c| Push::parse(c)).try_collect()?); let result = Day::run(pushes, 2022)?; Ok(ResultType::Integer(result as i64)) } fn part2(&self, lines: &[String]) -> anyhow::Result { let pushes = Dispenser::new(lines[0].chars().map(|c| Push::parse(c)).try_collect()?); let result = Day::run(pushes, 1000000000000)?; Ok(ResultType::Integer(result as i64)) } } impl Day { fn run(push_cycle: Dispenser, max_cycles: i64) -> Result { let raw = read_lines(DAY_NUMBER, "blocks.txt")?; let rock_cycle = Dispenser::new(Rock::parse(&raw)?); let mut cycle = 0; let mut stack = Stack::new(); let mut last_drop = None; let mut prev_bottom = 0; let mut additional_height = None; while cycle < max_cycles { let bottom = stack.one_rock(rock_cycle.next(), &push_cycle); if bottom < prev_bottom { let drop_distance = prev_bottom - bottom; match last_drop { None => { last_drop = Some((bottom, drop_distance, cycle, None)); } Some((_, last_distance, _, _)) if last_distance < drop_distance => { last_drop = Some((bottom, drop_distance, cycle, None)); } Some((last_bottom, last_distance, last_cycle, None)) if last_distance == drop_distance => { last_drop = Some(( bottom, drop_distance, cycle, Some((bottom - last_bottom, cycle - last_cycle)), )); } Some((last_bottom, last_distance, last_cycle, Some((growth, period)))) if last_distance == drop_distance => { if growth == bottom - last_bottom && period == cycle - last_cycle { let first_bottom = last_bottom - growth; let all_equal = (0..growth).all(|row| { stack.0[first_bottom + row] == stack.0[last_bottom + row] }); if all_equal { let iterations = (max_cycles - cycle) / period; additional_height = Some(iterations * growth as i64); cycle += iterations * period; } } else { last_drop = Some(( bottom, drop_distance, cycle, Some((bottom - last_bottom, cycle - last_cycle)), )) } } _ => {} } } prev_bottom = bottom; cycle += 1; } match additional_height { None => Ok(stack.height() as i64), Some(height) => Ok(stack.height() as i64 + height), } } } #[derive(Debug, Error)] enum RockError { #[error("IO Error")] RockIOError(#[from] std::io::Error), #[error("A rock must not be zero length or hight")] MustNotBeEmpty, #[error("All lines in a block must be the same length")] AllLinesSameLength, #[error("Unknown direction: {0}")] UnknownDirection(char), } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] struct Rock { positions: Vec<(usize, usize)>, width: usize, height: usize, } impl Rock { pub fn new(blocks: Vec>) -> Result { let height = blocks.len(); if height == 0 { return Err(RockError::MustNotBeEmpty); } let width = blocks.iter().map(|line| line.len()).max().unwrap_or(0); if width == 0 { return Err(RockError::MustNotBeEmpty); } for line in blocks.iter() { if line.len() != width { return Err(RockError::AllLinesSameLength); } } let positions = blocks .iter() .enumerate() .map(|(y, line)| { line.iter() .enumerate() .filter_map(move |(x, block)| block.then_some((x, height - 1 - y))) }) .flatten() .collect_vec(); Ok(Rock { positions, width, height, }) } pub fn parse(lines: &[String]) -> Result, RockError> { lines .iter() .batching(|it| { let blocks = it .skip_while(|line| line.is_empty()) .take_while(|line| !line.is_empty()) .map(|line| line.chars().map(|c| c == '#').collect_vec()) .collect_vec(); if blocks.is_empty() { None } else { Some(Rock::new(blocks)) } }) .try_collect() } } #[derive(Debug, Clone, Copy)] enum Push { Left, Right, } impl Push { pub fn parse(c: char) -> Result { match c { '<' => Ok(Push::Left), '>' => Ok(Push::Right), _ => Err(RockError::UnknownDirection(c)), } } } struct FallingRock<'a> { rock: &'a Rock, bottom: usize, left: usize, } impl<'a> FallingRock<'a> { pub fn new(rock: &'a Rock, stack_height: usize) -> FallingRock<'a> { FallingRock { rock, bottom: stack_height + 3, left: 2, } } pub fn try_push(mut self, push: &Push, stack: &Stack) -> Self { let left = match push { Push::Left => { if self.left == 0 { return self; } self.left - 1 } Push::Right => { if self.left + self.rock.width >= STACK_WIDTH { return self; } self.left + 1 } }; if self.check_position(left, self.bottom, stack) { self.left = left; } self } pub fn try_drop(mut self, stack: &Stack) -> Result { if self.bottom == 0 { return Err(self); } let bottom = self.bottom - 1; if self.check_position(self.left, bottom, stack) { self.bottom = bottom; Ok(self) } else { Err(self) } } fn check_position(&self, left: usize, bottom: usize, stack: &Stack) -> bool { self.rock .positions .iter() .all(|(x, y)| *y + bottom >= stack.height() || !stack[*y + bottom][*x + left]) } fn reach(&self) -> usize { self.bottom + self.rock.height } fn positions(&'a self) -> impl Iterator + 'a { self.rock .positions .iter() .map(|(x, y)| (*x + self.left, *y + self.bottom)) } } struct Stack(Vec<[bool; STACK_WIDTH]>); impl Stack { pub fn new() -> Stack { Stack(vec![]) } #[inline] pub fn height(&self) -> usize { self.0.len() } pub fn settle_rock(&mut self, rock: FallingRock<'_>) { for _ in self.0.len()..rock.reach() { self.0.push([false; STACK_WIDTH]); } for (x, y) in rock.positions() { self.0[y][x] = true } } fn one_rock(&mut self, rock: &Rock, push_cycle: &Dispenser) -> usize { let mut rock = FallingRock::new(rock, self.height()); loop { let push = push_cycle.next(); rock = rock.try_push(push, &self); match rock.try_drop(&self) { Ok(next_rock) => rock = next_rock, Err(rock) => { let bottom = rock.bottom; self.settle_rock(rock); return bottom; } } } } } impl std::fmt::Display for Stack { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let text = self .0 .iter() .rev() .map(|line| line.iter().map(|b| if *b { "#" } else { " " }).join("")) .join("\n"); write!(f, "{}", text) } } impl Index for Stack { type Output = [bool; STACK_WIDTH]; fn index(&self, index: usize) -> &Self::Output { &self.0[index] } } struct Dispenser<'a, T> where T: 'a, { data: Vec, current: Cell, _marker: PhantomData<&'a T>, } impl<'a, T> Dispenser<'a, T> { pub fn new(data: Vec) -> Self { Dispenser { data, current: Cell::new(0), _marker: PhantomData, } } fn next(&'a self) -> &'a T { let idx = self.current.get(); self.current.set(self.current.get() + 1); if self.current.get() >= self.data.len() { self.current.set(0); } self.data.get(idx).unwrap() } } #[cfg(test)] mod test { use super::*; use crate::common::file::read_lines; use anyhow::Result; #[test] fn test_part1() -> Result<()> { let day = Day {}; let lines = read_lines(day.get_day_number(), "example01.txt")?; let expected = ResultType::Integer(3068); let result = day.part1(&lines)?; assert_eq!(result, expected); Ok(()) } #[test] fn test_part2() -> Result<()> { let day = Day {}; let lines = read_lines(day.get_day_number(), "example01.txt")?; let expected = ResultType::Integer(1514285714288); let result = day.part2(&lines)?; assert_eq!(result, expected); Ok(()) } #[test] fn read_blocks() -> Result<()> { let day = Day {}; let raw = read_lines(day.get_day_number(), "blocks.txt")?; let rocks = Rock::parse(&raw)?; let expected = Rock::new(vec![vec![true]; 4])?; assert_eq!(rocks[3], expected); let angle_rock = &rocks[2]; assert_eq!( angle_rock.positions, vec![(2, 2), (2, 1), (0, 0), (1, 0), (2, 0)] ); Ok(()) } #[test] fn drop_one() -> Result<()> { let day = Day {}; let lines = read_lines(day.get_day_number(), "example01.txt")?; let pushes = Dispenser::new(lines[0].chars().map(|c| Push::parse(c)).try_collect()?); let raw = read_lines(day.get_day_number(), "blocks.txt")?; let rocks = Dispenser::new(Rock::parse(&raw)?); let mut stack = Stack::new(); stack.one_rock(rocks.next(), &pushes); assert_eq!(stack.0, vec![[false, false, true, true, true, true, false]]); Ok(()) } #[test] fn drop_some() -> Result<()> { let day = Day {}; let lines = read_lines(day.get_day_number(), "example01.txt")?; let pushes = Dispenser::new(lines[0].chars().map(|c| Push::parse(c)).try_collect()?); let result = Day::run(pushes, 10)?; assert_eq!(result, 17); Ok(()) } }