aboutsummaryrefslogtreecommitdiff
path: root/2022/src/day09.rs
diff options
context:
space:
mode:
Diffstat (limited to '2022/src/day09.rs')
-rw-r--r--2022/src/day09.rs238
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),
+ )
+ }
+ });
+ }
+}