use itertools::Itertools; use std::collections::{HashMap, LinkedList}; use std::fmt::{Display, Write}; use std::fs::File; use std::io::{BufRead, BufReader, Lines}; use std::ops::Range; use std::time::Instant; // BOILERPLATE type InputIter = Lines>; fn get_input() -> InputIter { let f = File::open("input").unwrap(); let br = BufReader::new(f); br.lines() } fn main() { let start = Instant::now(); let ans1 = problem1(get_input()); let duration = start.elapsed(); println!("Problem 1 solution: {} [{}s]", ans1, duration.as_secs_f64()); let start = Instant::now(); let ans2 = problem2(get_input()); let duration = start.elapsed(); println!("Problem 2 solution: {} [{}s]", ans2, duration.as_secs_f64()); } // DATA #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] enum Direction { Left, Right, Up, Down, } impl Direction { const fn all() -> &'static [Self; 4] { &[ Direction::Left, Direction::Right, Direction::Up, Direction::Down, ] } const fn opposite(&self) -> Self { match self { Direction::Left => Direction::Right, Direction::Right => Direction::Left, Direction::Up => Direction::Down, Direction::Down => Direction::Up, } } const fn offset(&self) -> (isize, isize) { match self { Direction::Left => (-1, 0), Direction::Right => (1, 0), Direction::Up => (0, -1), Direction::Down => (0, 1), } } } impl From<&str> for Direction { fn from(s: &str) -> Self { match s { "L" => Direction::Left, "R" => Direction::Right, "U" => Direction::Up, "D" => Direction::Down, s => panic!("{} is not a valid direction", s), } } } impl From<&Direction> for char { fn from(dir: &Direction) -> Self { match dir { Direction::Left => '←', Direction::Right => '→', Direction::Up => '↑', Direction::Down => '↓', } } } #[derive(Debug, Clone)] struct DigInstruction { dir: Direction, count: usize, color: String, } impl From<&str> for DigInstruction { fn from(s: &str) -> Self { let mut parts = s.split_ascii_whitespace(); let (dir, count, color) = ( parts.next().unwrap(), parts.next().unwrap(), parts.next().unwrap(), ); Self { dir: dir.into(), count: count.parse().unwrap(), color: color.into(), } } } #[derive(Debug)] struct DigPlan { instructions: Vec, } #[derive(Debug, Clone)] struct DigTile { position: Position, depth: usize, color: String, } impl Default for DigTile { fn default() -> Self { Self { position: (0, 0), depth: 0, color: "000000".into(), } } } type Position = (isize, isize); #[derive(Debug)] struct DigHole { tiles_loop: LinkedList, tiles_map: HashMap, x_range: Range, y_range: Range, } impl DigHole { fn new() -> Self { DigHole { tiles_loop: LinkedList::new(), tiles_map: HashMap::new(), x_range: 0..0, y_range: 0..0, } } fn pos_offset(&self, pos: Position, offset: (isize, isize)) -> Position { (pos.0 + offset.0, pos.1 + offset.1) } fn run_plan(&mut self, plan: &DigPlan) { // start position is 1m deep let mut cur_pos = (0, 0); let (mut max_x, mut max_y) = (0, 0); self.tiles_loop.push_back(DigTile { position: cur_pos, depth: 1, color: "000000".into(), }); for i in &plan.instructions { println!("instruction: {:?}", i); for _ in 0..i.count { println!(" cur_pos: {:?}", cur_pos); cur_pos = (cur_pos.0 + i.dir.offset().0, cur_pos.1 + i.dir.offset().1); (max_x, max_y) = ( std::cmp::max(cur_pos.0, max_x), std::cmp::max(cur_pos.1, max_y), ); self.tiles_loop.push_back(DigTile { position: cur_pos, depth: 1, color: i.color.to_owned(), }); } } // 2d vec dimension is (max_x+1, max_y+1) for tile in &self.tiles_loop { self.x_range.start = std::cmp::min(self.x_range.start, tile.position.0); self.x_range.end = std::cmp::max(self.x_range.end, tile.position.0 + 1); self.y_range.start = std::cmp::min(self.y_range.start, tile.position.1); self.y_range.end = std::cmp::max(self.y_range.end, tile.position.1 + 1); self.tiles_map.insert(tile.position, tile.clone()); } println!("{}", self); self.fill_at(self.find_first_inside().unwrap()); } fn fill_at(&mut self, pos: Position) { println!("filling at {:?}", pos); let mut to_fill = vec![pos]; while let Some(fill_pos) = to_fill.pop() { if self.tiles_map.contains_key(&fill_pos) { continue; } else { self.tiles_map.insert( fill_pos, DigTile { position: fill_pos, depth: 1, color: "000000".to_owned(), }, ); } for new_pos in Direction::all() .iter() .map(|dir| self.pos_offset(fill_pos, dir.offset())) .filter(|pos| !self.tiles_map.contains_key(&pos)) { to_fill.push(new_pos); } } } fn find_first_inside(&self) -> Option { for y in self.y_range.clone() { let mut inside = false; for x in self.x_range.clone() { if self.tiles_map.contains_key(&(x, y)) && (!self.tiles_map.contains_key(&(x - 1, y)) || !self.tiles_map.contains_key(&(x+1, y))) { inside = !inside; } if inside && !self.tiles_map.contains_key(&(x, y)) { return Some((x as isize, y as isize)); }; } } None } } impl Display for DigHole { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { for y in self.y_range.clone() { for x in self.x_range.clone() { f.write_char(if (x, y) == (133, -267) { '*' } else if self.tiles_map.contains_key(&(x, y)) { '#' } else { '.' })?; } writeln!(f)?; } Ok(()) } } impl From> for DigPlan { fn from(lines: Lines) -> Self { Self { instructions: lines .map(|line| DigInstruction::from(line.unwrap().as_str())) .collect(), } } } // PROBLEM 1 solution fn problem1(input: Lines) -> u64 { let plan = DigPlan::from(input); println!("{:?}", plan); let mut dig = DigHole::new(); dig.run_plan(&plan); println!("{}", dig); dig.tiles_map.iter().count() as u64 } // PROBLEM 2 solution fn problem2(input: Lines) -> u64 { 0 } #[cfg(test)] mod tests { use crate::*; use std::io::Cursor; const EXAMPLE: &str = &"R 6 (#70c710) D 5 (#0dc571) L 2 (#5713f0) D 2 (#d2c081) R 2 (#59c680) D 2 (#411b91) L 5 (#8ceee2) U 2 (#caa173) L 1 (#1b58a2) U 2 (#caa171) R 2 (#7807d2) U 3 (#a77fa3) L 2 (#015232) U 2 (#7a21e3)"; #[test] fn problem1_example() { let c = Cursor::new(EXAMPLE); assert_eq!(problem1(c.lines()), 62); } #[test] fn problem2_example() { let c = Cursor::new(EXAMPLE); assert_eq!(problem2(c.lines()), 0); } }