mod utils; use std::{ cmp::{max, min}, collections::HashSet, ops::Add, time::Duration, }; use egui::{Color32, Rect, Sense, Stroke}; use nom::{bytes::complete::tag, character::complete::u32, multi::separated_list0, IResult}; fn main() { console_error_panic_hook::set_once(); let input = include_str!("../fake_inputs/day14"); 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"); }); } struct App { structures: Vec, sand_grains: HashSet, speed: usize, finished: bool, } impl App { fn new(input: &str) -> Self { let mut structures = separated_list0(tag("\n"), Structure::parse)(input) .unwrap() .1; let floor_y = structures .iter() .map(|s| s.points.iter().map(|p| p.1).max().unwrap()) .max() .unwrap() + 2; structures.push(Structure::new(vec![ Pos(isize::MIN, floor_y), Pos(isize::MAX, floor_y), ])); App { structures, sand_grains: HashSet::new(), speed: 0, finished: false, } } fn reset(&mut self) { self.sand_grains.clear(); } fn step(&mut self) { let mut pos = Pos(500, 0); if self.sand_grains.contains(&pos) { self.finished = true; return; } loop { // TODO pos = if !self.inhabited(pos + Pos(0, 1)) { pos + Pos(0, 1) } else if !self.inhabited(pos + Pos(-1, 1)) { pos + Pos(-1, 1) } else if !self.inhabited(pos + Pos(1, 1)) { pos + Pos(1, 1) } else { self.sand_grains.insert(pos); return; }; } } fn inhabited(&self, pos: Pos) -> bool { self.structures.iter().any(|s| s.contains(pos)) || self.sand_grains.contains(&pos) } } #[derive(Debug)] struct Structure { points: Vec, bbox_min: Pos, bbox_max: Pos, } impl Structure { fn new(points: Vec) -> Self { let bbox_min = points.iter().fold(Pos(isize::MAX, isize::MAX), |acc, pos| { Pos(min(acc.0, pos.0), min(acc.1, pos.1)) }); let bbox_max = points.iter().fold(Pos(isize::MAX, isize::MAX), |acc, pos| { Pos(max(acc.0, pos.0), max(acc.1, pos.1)) }); Structure { points, bbox_max, bbox_min, } } fn parse(i: &str) -> IResult<&str, Self> { let (i, points) = separated_list0(tag(" -> "), Pos::parse)(i)?; Ok((i, Structure::new(points))) } fn contains(&self, pos: Pos) -> bool { if pos.0 < self.bbox_min.0 || pos.0 > self.bbox_max.0 || pos.1 < self.bbox_min.1 || pos.1 > self.bbox_max.1 { return false; } for (a, b) in self.points.iter().zip(self.points.iter().skip(1)) { if a.0 == b.0 { if pos.0 == a.0 && pos.1 >= min(a.1, b.1) && pos.1 <= max(a.1, b.1) { return true; } } else { // a.1 == b.1 if pos.1 == a.1 && pos.0 >= min(a.0, b.0) && pos.0 <= max(a.0, b.0) { return true; } } } return false; } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] struct Pos(isize, isize); impl Pos { fn parse(i: &str) -> IResult<&str, Self> { let (i, x) = u32(i)?; let (i, _) = tag(",")(i)?; let (i, y) = u32(i)?; Ok((i, Pos(x as _, y as _))) } } impl From for egui::Pos2 { fn from(value: Pos) -> Self { egui::Pos2 { x: value.0 as _, y: value.1 as _, } } } impl Add for Pos { type Output = Pos; fn add(self, rhs: Self) -> Self::Output { Pos(self.0 + rhs.0, self.1 + rhs.1) } } impl eframe::App for App { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { egui::SidePanel::right("instructions").show(ctx, |ui| { egui::ScrollArea::vertical().show(ui, |ui| { if ui.button("Step").clicked() && !self.finished { self.step(); } if ui.button("Reset").clicked() { self.reset() } ui.label(format!( "{} grains, finished: {}", self.sand_grains.len(), self.finished )); ui.add(egui::Slider::new(&mut self.speed, 0..=100).text("Speed")); }) }); if self.speed > 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 RANGE: f32 = 200.0; const SHIFT: egui::Pos2 = egui::pos2(400.0, 5.0); let (_, painter) = ui.allocate_painter(painter_size, Sense::hover()); let transform_point = |p: &Pos| -> egui::Pos2 { egui::pos2( ((p.0 as f32 - SHIFT.x) / RANGE) * painter_size.x, ((p.1 as f32 + SHIFT.y) / RANGE) * painter_size.y, ) }; for structure in self.structures.iter() { for (a, b) in structure.points.iter().zip(structure.points.iter().skip(1)) { painter.line_segment( [transform_point(a), transform_point(b)], Stroke::new(1.0, Color32::RED), ); } } for grain in self.sand_grains.iter() { painter.rect( Rect { min: transform_point(grain), max: transform_point(&Pos(grain.0 + 1, grain.1 + 1)), }, egui::Rounding::none(), Color32::YELLOW, Stroke::NONE, ) } }); } }