diff --git a/README.md b/README.md index 0ee521c..878de91 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Thank you to [Eric Wastl](http://was.tl) for running this incredible yearly even - [Day 12: Garden Groups](aoc_2024/src/day_12.rs) - [Day 13: Claw Contraption](aoc_2024/src/day_13.rs) - [Day 14: Restroom Redoubt](aoc_2024/src/day_14.rs) +- [Day 15: Warehouse Woes](aoc_2024/src/day_15.rs) ## [2023](https://adventofcode.com/2023) [![aoc_2023](https://github.com/connorslade/advent-of-code/actions/workflows/aoc_2023.yml/badge.svg)](https://github.com/connorslade/advent-of-code/actions/workflows/aoc_2023.yml) diff --git a/aoc_2024/src/day_15.rs b/aoc_2024/src/day_15.rs new file mode 100644 index 0000000..52cad9b --- /dev/null +++ b/aoc_2024/src/day_15.rs @@ -0,0 +1,271 @@ +use std::fmt::{Debug, Write}; + +use aoc_lib::{direction::cardinal::Direction, matrix::Matrix}; +use common::{solution, Answer}; +use nd_vec::{vector, Vec2}; + +solution!("Warehouse Woes", 15); + +fn part_a(input: &str) -> Answer { + let mut problem = Problem::parse(input, false); + problem.tick_all(false); + problem.score().into() +} + +fn part_b(input: &str) -> Answer { + let mut problem = Problem::parse(input, true); + problem.tick_all(true); + problem.score().into() +} + +struct Problem { + pos: Vec2, + idx: usize, + + board: Matrix, + instructions: Vec, +} + +#[derive(PartialEq, Eq, Clone, Copy)] +enum Tile { + Robot, + Wall, + Box, + BoxRight, + Empty, +} + +impl Problem { + fn parse(input: &str, part_b: bool) -> Self { + let (board, instructions) = input.split_once("\n\n").unwrap(); + let mut board = Matrix::new_chars(board, |chr| match chr { + '@' => Tile::Robot, + '#' => Tile::Wall, + 'O' => Tile::Box, + '.' => Tile::Empty, + _ => panic!(), + }); + + if part_b { + board.size = vector!(board.size.x() * 2, board.size.y()); + let mut new = Vec::new(); + for data in board.data { + new.push(data); + new.push(match data { + Tile::Box => Tile::BoxRight, + Tile::Robot => Tile::Empty, + _ => data, + }); + } + board.data = new; + } + + let instructions = instructions + .chars() + .filter_map(|x| { + Some(match x { + '<' => Direction::Left, + '>' => Direction::Right, + '^' => Direction::Up, + 'v' => Direction::Down, + _ => return None, + }) + }) + .collect::>(); + + let pos = board.find(Tile::Robot).unwrap(); + board.set(pos, Tile::Empty); + + Self { + pos, + idx: 0, + + board, + instructions, + } + } + + fn tick_all(&mut self, part_b: bool) { + for _ in 0..self.instructions.len() { + self.tick(part_b); + } + } + + fn tick(&mut self, part_b: bool) { + let dir = self.instructions[self.idx]; + self.idx += 1; + + let new = dir.advance(self.pos); + if { + if part_b { + self.push_b(new, dir) + } else { + self.push(new, dir) + } + } { + self.pos = new; + } + } + + // -> was successful + fn push(&mut self, pos: Vec2, dir: Direction) -> bool { + // if we are air, return true + let value = self.board[pos]; + match value { + Tile::Empty => return true, + Tile::Wall => return false, + _ => {} + } + + // if where we want to move is full, try to move that + let new = dir.wrapping_advance(pos); + if !self.board.contains(new) { + return false; + } + + if self.board[new] == Tile::Empty || self.push(new, dir) { + self.board.set(new, value); + self.board.set(pos, Tile::Empty); + true + } else { + false + } + } + + fn can_push(&self, pos: Vec2, dir: Direction) -> bool { + // println!("{pos:?}, {dir:?}"); + let value = self.board[pos]; + match value { + Tile::Empty => return true, + Tile::Wall => return false, + Tile::Box | Tile::BoxRight => {} + Tile::Robot => unreachable!(), + } + + let other_box = match value { + Tile::Box => pos + vector!(1, 0), + Tile::BoxRight => pos - vector!(1, 0), + _ => unreachable!(), + }; + + // if where we want to move is full, try to move that + let new_a = dir.wrapping_advance(pos); + let new_b = dir.wrapping_advance(other_box); + if !(self.board.contains(new_a) && self.board.contains(new_b)) { + return false; + } + + (self.board[new_a] == Tile::Empty && self.board[new_b] == Tile::Empty) + || ((new_a == other_box || self.can_push(new_a, dir)) + && (new_b == pos || self.can_push(new_b, dir))) + } + + fn push_b(&mut self, pos: Vec2, dir: Direction) -> bool { + if self.can_push(pos, dir) { + let value = self.board[pos]; + if value == Tile::Empty { + return true; + } + + assert!(matches!(value, Tile::Box | Tile::BoxRight)); + let other_box = match value { + Tile::Box => pos + vector!(1, 0), + Tile::BoxRight => pos - vector!(1, 0), + _ => unreachable!(), + }; + let other_value = self.board[other_box]; + + let new_a = dir.wrapping_advance(pos); + let new_b = dir.wrapping_advance(other_box); + if !(self.board.contains(new_a) && self.board.contains(new_b)) { + return false; + } + + if (self.board[new_a] == Tile::Empty && self.board[new_b] == Tile::Empty) + || ((new_a == other_box || self.push_b(new_a, dir)) + && (new_b == pos || self.push_b(new_b, dir))) + { + // do push + self.board.set(new_a, value); + self.board.set(pos, Tile::Empty); + + self.board.set(new_b, other_value); + if other_box != new_a { + self.board.set(other_box, Tile::Empty); + } + true + } else { + false + } + } else { + false + } + } + + fn score(&self) -> u32 { + let mut score = 0; + + for (pos, _) in self.board.iter().filter(|x| *x.1 == Tile::Box) { + score += (100 * pos.y() + pos.x()) as u32; + } + + score + } + + fn debug(&self) { + let mut dbg = self.board.clone(); + dbg.set(self.pos, Tile::Robot); + println!("{:?}", dbg); + } +} + +impl Debug for Tile { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Tile::Robot => f.write_char('@'), + Tile::Wall => f.write_char('#'), + Tile::Box => f.write_char('['), + Tile::BoxRight => f.write_char(']'), + Tile::Empty => f.write_char('.'), + } + } +} + +#[cfg(test)] +mod test { + use indoc::indoc; + + const CASE: &str = indoc! {" + ########## + #..O..O.O# + #......O.# + #.OO..O.O# + #..O@..O.# + #O#..O...# + #O..O..O.# + #.OO.O.OO# + #....O...# + ########## + + ^v>^vv^v>v<>v^v<<><>>v^v^>^<<<><^ + vvv<<^>^v^^><<>>><>^<<><^vv^^<>vvv<>><^^v>^>vv<>v<<<^<^^>>>^<>vv>v^v^<>><>>>><^^>vv>v<^^^>>v^v^<^^>v^^>v^<^v>v<>>v^v^v^^<^^vv< + <>^^^^>>>v^<>vvv^>^^^vv^^>v<^^^^v<>^>vvvv><>>v^<<^^^^^ + ^><^><>>><>^^<<^^v>>><^^>v>>>^v><>^v><<<>vvvv>^<><<>^>< + ^>><>^v<><^vvv<^^<><^v<<<><<<^^<^>>^<<<^>>^v^>>^v>vv>^<<^v<>><<><<>v<^vv<<<>^^v^>^^>>><<^v>>v^v><^^>>^<>vv^ + <><^^>^^^<>^vv<<^><<><<><<<^^<<<^<<>><<><^^^>^^<>^>v<> + ^^>vv<^v^v^<>^^^>>>^^vvv^>vvv<>>>^<^>>>>>^<<^v>^vvv<>^<>< + v^^>>><<^^<>>^v^v^<<>^<^v^v><^<<<><<^vv>>v>v^<<^ + "}; + + #[test] + fn part_a() { + assert_eq!(super::part_a(CASE), 10092.into()); + } + + #[test] + fn part_b() { + assert_eq!(super::part_b(CASE), 9021.into()); + } +} diff --git a/aoc_2024/src/lib.rs b/aoc_2024/src/lib.rs index d7f484f..154c9dd 100644 --- a/aoc_2024/src/lib.rs +++ b/aoc_2024/src/lib.rs @@ -14,6 +14,7 @@ mod day_11; mod day_12; mod day_13; mod day_14; +mod day_15; // [import_marker] pub const SOLUTIONS: &[Solution] = &[ @@ -31,5 +32,6 @@ pub const SOLUTIONS: &[Solution] = &[ day_12::SOLUTION, day_13::SOLUTION, day_14::SOLUTION, + day_15::SOLUTION, // [list_marker] ];