diff options
Diffstat (limited to '2022/src/day09.rs')
-rw-r--r-- | 2022/src/day09.rs | 238 |
1 files changed, 238 insertions, 0 deletions
diff --git a/2022/src/day09.rs b/2022/src/day09.rs new file mode 100644 index 0000000..23b08b3 --- /dev/null +++ b/2022/src/day09.rs @@ -0,0 +1,238 @@ +mod utils; +use std::{ + cmp::min, + collections::{HashSet, VecDeque}, + time::Duration, +}; + +use egui::{Color32, Sense, Stroke}; +use utils::{iter_pair, read_input}; + +#[cfg(target_arch = "wasm32")] +fn main() { + console_error_panic_hook::set_once(); + + let input = include_str!("../fake_inputs/day9"); + let input = parse_moves(&input).collect(); + + let options = eframe::WebOptions::default(); + wasm_bindgen_futures::spawn_local(async { + eframe::start_web("canvas", options, Box::new(|_cc| Box::new(App::new(input)))) + .await + .expect("unable to start eframe"); + }); +} + +#[cfg(not(target_arch = "wasm32"))] +fn main() { + let input = read_input(); + + println!("Part 1: {}", visited_from_moves::<2>(parse_moves(&input))); + println!("Part 2: {}", visited_from_moves::<10>(parse_moves(&input))); +} + +type Pos = (i32, i32); +type Move = (Direction, u8); + +fn visited_from_moves<const N: usize>(moves: impl Iterator<Item = Move>) -> usize { + let mut tail_visited = HashSet::new(); + tail_visited.insert((0, 0)); + let mut knots = [(0, 0); N]; + for mv in moves { + make_move(mv, &mut knots, &mut tail_visited); + } + + tail_visited.len() +} + +fn make_move(mv: Move, knots: &mut [Pos], tail_visited: &mut HashSet<Pos>) { + let (dir, count) = mv; + + for _ in 0..count { + // Move head of rope + match dir { + Direction::Up => knots[0].1 += 1, + Direction::Down => knots[0].1 -= 1, + Direction::Right => knots[0].0 += 1, + Direction::Left => knots[0].0 -= 1, + }; + + for front_idx in 0..knots.len() - 1 { + let (fx, fy) = knots[front_idx]; + let (bx, by) = &mut knots[front_idx + 1]; + let (dx, dy) = (fx - *bx, fy - *by); + if (dx.abs() == 2 && dy == 0) || (dy.abs() == 2 && dx == 0) || (dy.abs() + dx.abs() > 2) + { + *bx += (dx.signum()) * min(dx.abs(), 1); + *by += (dy.signum()) * min(dy.abs(), 1); + } + } + tail_visited.insert(knots[knots.len() - 1]); + } +} + +fn parse_moves(input: &str) -> impl Iterator<Item = Move> + '_ { + input.lines().map(|x| { + let (dir, count) = iter_pair(x.split(' ')); + let count = count.parse().unwrap(); + + ( + match dir { + "L" => Direction::Left, + "R" => Direction::Right, + "U" => Direction::Up, + "D" => Direction::Down, + _ => panic!("invalid direction, {}", dir), + }, + count, + ) + }) +} + +#[derive(Debug, Clone, Copy)] +enum Direction { + Up, + Down, + Left, + Right, +} + +struct App { + orig_input: VecDeque<Move>, + input: VecDeque<Move>, + knots: Vec<Pos>, + tail_visited: HashSet<Pos>, + desired_knot_count: usize, + speed: usize, +} + +impl App { + fn new(input: VecDeque<Move>) -> Self { + App { + orig_input: input.clone(), + input, + knots: vec![], + tail_visited: HashSet::new(), + desired_knot_count: 2, + speed: 0, + } + } + + fn reset_knots(&mut self) { + self.input = self.orig_input.clone(); + self.knots = vec![(0, 0); self.desired_knot_count]; + self.tail_visited.clear(); + self.tail_visited.insert((0, 0)); + } + + fn step(&mut self) { + match self.input.pop_front() { + Some(mv) => make_move(mv, &mut self.knots, &mut self.tail_visited), + None => (), + } + } +} + +impl eframe::App for App { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + if self.desired_knot_count != self.knots.len() { + self.reset_knots() + } + egui::SidePanel::right("instructions").show(ctx, |ui| { + egui::ScrollArea::vertical().show(ui, |ui| { + if ui.button("Step").clicked() { + self.step(); + } + if ui.button("Reset").clicked() { + self.reset_knots() + } + ui.label(format!("Tail Visited: {}", self.tail_visited.len())); + ui.add(egui::Slider::new(&mut self.desired_knot_count, 2..=20).text("Knot count")); + ui.add(egui::Slider::new(&mut self.speed, 0..=20).text("Speed")); + ui.heading("Instructions:"); + for (i, ins) in self.input.iter().take(20).enumerate() { + let arrow = match ins.0 { + Direction::Up => "⬆", + Direction::Down => "⬇", + Direction::Right => "➡", + Direction::Left => "⬅", + }; + if i == 0 { + ui.colored_label(Color32::YELLOW, arrow.repeat(ins.1 as _)); + } else { + ui.label(arrow.repeat(ins.1 as _)); + } + } + if self.input.len() > 20 { + ui.label(format!("+ {} more", self.input.len() - 20)); + } + }) + }); + + if self.speed > 0 && self.input.len() > 0 { + for _ in 0..self.speed { + self.step(); + } + ctx.request_repaint_after(Duration::from_millis(100)) + } + + egui::CentralPanel::default().show(ctx, |ui| { + let mut painter_size = ui.available_size_before_wrap(); + if !painter_size.is_finite() { + painter_size = egui::vec2(500.0, 500.0); + } + + const SIDE: f32 = 5.0; + + let (res, painter) = ui.allocate_painter(painter_size, Sense::hover()); + let center = res.rect.center().to_vec2(); + + let to_panel_pos = |pos: &Pos| { + (egui::vec2(pos.0 as f32 * SIDE, pos.1 as f32 * SIDE) + center).to_pos2() + }; + + let half_width = (painter_size.x / SIDE).floor() as i32; + let half_height = (painter_size.y / SIDE).floor() as i32; + + for x in -half_width..half_width { + for y in -half_height..half_height { + let dot = (x, y); + let color = if dot == (0, 0) { + Color32::WHITE + } else if self.tail_visited.contains(&dot) { + Color32::DARK_RED + } else { + continue; + }; + + let dot_pos = to_panel_pos(&dot); + painter.circle_stroke(dot_pos, 1.0, Stroke::new(1.0, color)); + } + } + + // paint the head + painter.circle_stroke( + to_panel_pos(&self.knots[0]), + SIDE / 2.0, + Stroke::new(SIDE / 2.0, Color32::GREEN), + ); + for knot in self.knots.iter().skip(1) { + painter.circle_stroke( + to_panel_pos(knot), + SIDE / 2.0, + Stroke::new(SIDE / 2.0, Color32::YELLOW), + ); + } + + for (a, b) in self.knots.iter().zip(self.knots.iter().skip(1)) { + let head_pos = to_panel_pos(a); + let tail_pos = to_panel_pos(b); + painter.arrow( + tail_pos, + head_pos - tail_pos, + Stroke::new(1.0, Color32::YELLOW), + ) + } + }); + } +} |