use std::fmt::Write; use aoc::err; const INPUT: &str = include_str!("../input/day12.txt"); pub fn run() -> aoc::Result { let mut res = String::with_capacity(128); writeln!(res, "part 1: {}", part1(INPUT)?)?; Ok(res) } fn part1(input: &str) -> aoc::Result { let actions: Vec = input .lines() .map(|line| line.parse()) .collect::>()?; let mut ship = Ship::new(); for a in actions { ship.process(a); } Ok(ship.manhattan_distance()) } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Direction { North, South, East, West, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum ActionKind { Move(Direction), Left, Right, Forward, } #[derive(Debug, Clone)] struct Action { kind: ActionKind, arg: u16, } impl std::str::FromStr for Action { type Err = aoc::Error; fn from_str(s: &str) -> aoc::Result { debug_assert!( s.len() >= 2, "tried to parse action but it is too short: `{}`", s ); let letter = s .chars() .next() .ok_or_else(|| err!("couldn't parse action: empty string"))?; let kind = match letter { 'N' => ActionKind::Move(Direction::North), 'S' => ActionKind::Move(Direction::South), 'E' => ActionKind::Move(Direction::East), 'W' => ActionKind::Move(Direction::West), 'L' => ActionKind::Left, 'R' => ActionKind::Right, 'F' => ActionKind::Forward, _ => return Err(err!("couldn't parse action with letter `{}`", letter)), }; let arg = s[1..] .parse() .map_err(|e| err!("couldn't parse action arg: {}", e))?; Ok(Self { kind, arg }) } } #[derive(Debug, Clone)] struct Ship { direction: Direction, x: i64, y: i64, } impl Ship { fn new() -> Self { Self { direction: Direction::East, x: 0, y: 0, } } const CLOCKWISE_DIRECTIONS: &'static [Direction] = &[ Direction::North, Direction::East, Direction::South, Direction::West, ]; fn manhattan_distance(&self) -> i64 { self.x.abs() + self.y.abs() } fn find_direction(iter: I, quarters: usize, current_direction: Direction) -> Direction where I: Iterator, I: std::clone::Clone, { iter // this is litteraly a circle, reaching West and turning 90 degrees right means // facing North again .cycle() // find our current ship direction .skip_while(|dir| *dir != current_direction) // skip as many quarters as needed .nth(quarters) // we can unwrap safely because we called .cycle() on a non empty iterator .unwrap() } fn process(&mut self, action: Action) { match action.kind { ActionKind::Move(dir) => self.forward(dir, action.arg), ActionKind::Right | ActionKind::Left => { debug_assert!(action.arg % 90 == 0, "only right angles are supported"); let quarters = (action.arg / 90) as usize; let directions_iter = Self::CLOCKWISE_DIRECTIONS.iter().copied(); let new_dir = if action.kind == ActionKind::Left { // go through cardinal directions the other way around, anti-clockwise Ship::find_direction(directions_iter.rev(), quarters, self.direction) } else { Ship::find_direction(directions_iter, quarters, self.direction) }; self.direction = new_dir; } ActionKind::Forward => self.forward(self.direction, action.arg), } } fn forward(&mut self, direction: Direction, arg: u16) { let arg = arg as i64; match direction { Direction::North => self.y -= arg, Direction::South => self.y += arg, Direction::West => self.x -= arg, Direction::East => self.x += arg, } } } #[cfg(test)] mod tests { use super::*; const PROVIDED: &str = include_str!("../input/day12_provided.txt"); #[test] fn part1_provided() { assert_eq!(part1(PROVIDED).unwrap(), 25); } #[test] fn part1_real() { assert_eq!(part1(INPUT).unwrap(), 1589); } }