diff --git a/2022/day17-part2/Cargo.toml b/2022/day17-part2/Cargo.toml new file mode 100644 index 0000000..00d67bd --- /dev/null +++ b/2022/day17-part2/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "day17-part2" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/2022/day17-part2/src/main.rs b/2022/day17-part2/src/main.rs new file mode 100644 index 0000000..f2eb2e7 --- /dev/null +++ b/2022/day17-part2/src/main.rs @@ -0,0 +1,345 @@ +use std::collections::VecDeque; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum RockShape { + Minus, + Plus, + J, + I, + O, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum FallingDirection { + Left, + Right, + Down, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +struct FallingRock { + shape: RockShape, + // Coordinates of the falling rock identifying the bottom left piece of it. + // For the '+'-shape this actually refers to a piece of air. + // y counts up from the bottom layer 0 up to infinity. + // x counts up from left 0 to right 6. + x: usize, + y: usize, +} + +// To allow easy iteration over all coordinates of a falling rock +// I'm providing a custom iterator here. +#[derive(Debug)] +struct FallingRockIterator { + fr: FallingRock, + i: usize, +} + +impl Iterator for FallingRockIterator { + type Item = (usize, usize); + + fn next(&mut self) -> Option { + // The offsets for the different shapes, stored statically. + let offsets: &'static [(usize, usize)] = match &self.fr.shape { + RockShape::Minus => &[(0, 0), (1, 0), (2, 0), (3, 0)], + RockShape::Plus => &[(0, 1), (1, 0), (1, 1), (1, 2), (2, 1)], + RockShape::J => &[(0, 0), (1, 0), (2, 0), (2, 1), (2, 2)], + RockShape::I => &[(0, 0), (0, 1), (0, 2), (0, 3)], + RockShape::O => &[(0, 0), (0, 1), (1, 0), (1, 1)], + }; + // The actual calculation, where we apply rock coordinate + offset at current index i. + // Yields None if i has already iterated over everything. + if self.i < offsets.len() { + // Don't forget to increment the offset index. + let old_i = self.i; + self.i += 1; + Some((self.fr.x + offsets[old_i].0, self.fr.y + offsets[old_i].1)) + } else { + // No incrementing the offset index once we've reached the end. + None + } + } +} + +impl FallingRock { + // Construct a FallingRockIterator allowing us to iterate over all coordinates of this rock. + fn iter(&self) -> FallingRockIterator { + FallingRockIterator { fr: *self, i: 0 } + } + + // Simulate the falling rock within the existing cave, + // and attempt to move it in the specified direction. + // Left and Right will either return a moved FallingRock, + // or return the exact same FallingRock if moving was not possible. + // Down will either return a moved FallingRock or + // or consume the FallingRock and settle it as static Rock-tiles within the Cave. + // + // If the FallingRock has been settled, None will be returned. + // Otherwise, the (possibly moved) Some(FallingRock) will be returned. + fn attempt_move(mut self, cave: &mut Cave, dir: FallingDirection) -> Option { + match dir { + FallingDirection::Left => { + // In order to move the piece ... + // (1) it must not touch the left wall + // (2) all the tiles to the left of it must be air + if self.x > 0 && self.iter().all(|(x, y)| !cave.is_rock(x - 1, y)) { + self.x -= 1; + } + Some(self) + } + FallingDirection::Right => { + // In order to move the piece ... + // (1) it must not touch the right wall + // (2) all the tiles to the right of it must be air + if self.iter().map(|e| e.0).max().unwrap() < 6 + && self.iter().all(|(x, y)| !cave.is_rock(x + 1, y)) + { + self.x += 1; + } + Some(self) + } + FallingDirection::Down => { + // If we've reached the bottom of the cave we have to settle. + // If any of the tiles below the current piece are rocks, we also have to settle. + if self.y == 0 || self.iter().any(|(x, y)| cave.is_rock(x, y - 1)) { + // Ensure the cave itself can hold all the possible coordinates. + // We're adding one extra b/c the topmost line should always be air-only. + cave.extend_to(self.y + 4); + // Actually settle the rock. + for (x, y) in self.iter() { + cave.set(x, y, Tile::Rock); + } + // Consume it. + None + } else { + // Since we're not settling, we *are* moving. + self.y -= 1; + Some(self) + } + } + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum Tile { + Air, + Rock, +} + +// Grows upwards, i.e. the "back" is the "top" of the stack. +struct Cave { + // The actual data - a double-ended queue / ring-buffer of lines. + data: VecDeque<[Tile; 7]>, + // In order to be able to compute to 1 trillion, as required for part 2, + // we're going to regularly clean up "garbage" from the bottom of the tower + // that is no longer needed for the simulation. + // Nonetheless, we have to keep track of the current floor coordinate: + floor: usize, +} + +impl Cave { + // Read an arbitrary coordinate within the cave. + // This uses the simulated coordinates, + // i.e. the y coordinate can become *incredibly* large here. + fn get(&self, x: usize, y: usize) -> Tile { + self.data[y - self.floor][x] + } + + // Write to an arbitrary coordinate within the cave. + // This uses the simulated coordinates, + // i.e. the y coordinate can become *incredibly* large here. + fn set(&mut self, x: usize, y: usize, val: Tile) { + self.data[y - self.floor][x] = val; + } + + // Get the simulated size of the tower. + fn height(&self) -> usize { + self.data.len() + self.floor + } + + // Print the cave to stdout. + // You can optionally provide a falling rock to print as well. + #[allow(dead_code)] + fn print(&self, falling_rock: &Option) { + for y in (self.floor..(self.height() + 10)).rev() { + print!("|"); + for x in 0..7 { + // Air is the default tile. + let mut tile = '.'; + // If the cave has data for this coordinate, check if it's a settled rock. + if y < self.height() && self.get(x, y) == Tile::Rock { + tile = '#'; + } + // Next, check if a falling rock exists here and override the tile with '@' if so. + if let Some(ref fr) = falling_rock { + if fr.iter().any(|(cx, cy)| cx == x && cy == y) { + tile = '@'; + } + } + // Print the determined tile. + print!("{}", tile); + } + println!("|"); + } + if self.floor == 0 { + println!("+-------+\n"); + } + } + + // Checks if a given simulated coordinate refers to a settled rock. + // Also takes into account coordinates that extend beyond the current length of the Vec. + fn is_rock(&self, x: usize, y: usize) -> bool { + if y >= self.height() { + false + } else { + self.get(x, y) == Tile::Rock + } + } + + // Vector not big enough? Ensure that it is big enough for the passed-along y-coordinate. + fn extend_to(&mut self, y: usize) { + // Simply push 7-element arrays of Air-tiles + // onto the Vector until our condition is satisfied. + while y >= self.height() { + self.data.push_back([Tile::Air; 7]); + } + } + + // Returns the simulated y-coordinate of the first rock-free line at the top of the tower. + fn past_the_top(&self) -> usize { + // Iterate through all data-lines, starting from the top. + // Find the first line where there is at least one rock. + // Then, return the y-coordinate that is one bigger, i.e. the previous, air-only line. + // If no line could be found we assume we're at the start of simulation + // where y=0 is the first free line. + self.data + .iter() + .enumerate() + .rev() + .find(|(_, line)| line.iter().any(|e| e == &Tile::Rock)) + .map_or(0, |(i, _)| i + 1 + self.floor) + } + + // In order to pull off 1 trillion lines we have to regularly clean up "garbage" + // at the bottom of the tower that isn't needed anymore for a correct simulation. + // We do this by finding, starting from the top of the tower, the first two lines + // that, when "OR"ed together, yield a wall. + // + // Examples: + // + // Line A: |##....#| |#.#.#.#| |##.....| + // Line B: |.#####.| |.#.#.#.| |...####| + // + // Yields: |#######| |#######| |##.####| + // Therefore: OK OK CONTINUE + // + // Once located, anything below the lower of those two lines can be removed. + // Once cut off the `floor` of the cave has to be incremented accordingly. + fn collect_garbage(&mut self) { + // Zip up a reverse iterator with another reverse-iterator that skips the topmost line + // to iterate over all pairs of lines. + let dy = self + .data + .iter() + .rev() + .zip(self.data.iter().enumerate().rev().skip(1)) + .find(|(a, (_, b))| { + // Find the first pair where every column contains at least one rock. + a.iter() + .zip(b.iter()) + .all(|(ea, eb)| ea == &Tile::Rock || eb == &Tile::Rock) + }) + // We're interested in the coordinate, so map that out. + .map(|(_, (y, _))| y); + + // Didn't find a wall? That's fine, no garbage to clean up then. + if let Some(dy) = dy { + // Drain the range from the bottom and drop the iterator. + // This frees all the elements at the front of the VecDeque in bulk. + drop(self.data.drain(0..dy)); + // Don't forget to adjust the floor. + self.floor += dy; + } + } +} + +fn main() { + // Use command line arguments to specify the input filename. + let args: Vec = std::env::args().collect(); + if args.len() < 2 { + panic!("Usage: ./main \nNo input file provided. Exiting."); + } + + // Next, read the contents of the input file into a string for easier processing. + let input = std::fs::read_to_string(&args[1]).expect("Error opening file"); + + // Create an infinitely-looping iterator for the input directions. + // We're also filtering out any characters that aren't '<' or '>' such as newlines + // and are simulatenously mapping '<' and '>' to FallingDirection::Left and ::Right respectively. + let mut input_directions = input + .chars() + .filter_map(|e| match e { + '<' => Some(FallingDirection::Left), + '>' => Some(FallingDirection::Right), + _ => None, + }) + .cycle(); + + // Also create an infinitely-looping iterator for the rock-types. + let mut rock_shapes = [ + RockShape::Minus, + RockShape::Plus, + RockShape::J, + RockShape::I, + RockShape::O, + ] + .iter() + .cycle(); + + // The cave where all the rocks will settle. + let mut cave = Cave { + data: VecDeque::new(), + floor: 0, + }; + + // Simulate ONE TRILLION rocks. + for i in 0..1_000_000_000_000usize { + // Create the next falling rock. + let mut fr = Some(FallingRock { + shape: *rock_shapes.next().unwrap(), + // Always two spaces from the left wall. + x: 2, + // Always three lines of free space. + y: cave.past_the_top() + 3, + }); + + // Every 1k rocks attempt to clean up garbage. + if i % 1000 == 0 { + cave.collect_garbage(); + } + + if i % 1_000_000 == 0 { + println!("Remaining: {}", 1_000_000_000_000 - i); + } + + // Keep moving l/r and down until the rock settles. + loop { + // Move left / right. + let dir = input_directions.next().unwrap(); + fr = fr.unwrap().attempt_move(&mut cave, dir); + + // Next, move down. + fr = fr.unwrap().attempt_move(&mut cave, FallingDirection::Down); + // Did it settle? If so, move to the next rock. + if fr.is_none() { + break; + } + } + } + + println!( + "Topmost free y-coordinate after 2022 rocks have settled: {}", + cave.past_the_top() + ); +}