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(moves: impl Iterator) -> 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) { 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 + '_ { 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, input: VecDeque, knots: Vec, tail_visited: HashSet, desired_knot_count: usize, speed: usize, } impl App { fn new(input: VecDeque) -> 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), ) } }); } }