diff --git a/.aoc_tiles/tiles/2024/16.png b/.aoc_tiles/tiles/2024/16.png
index e5449fe..f5551a1 100644
Binary files a/.aoc_tiles/tiles/2024/16.png and b/.aoc_tiles/tiles/2024/16.png differ
diff --git a/README.md b/README.md
index 3aaedd1..39e18b5 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
- 2024 - 30 ⭐ - Rust
+ 2024 - 31 ⭐ - Rust
@@ -47,4 +47,7 @@
+
+
+
diff --git a/src/day16.rs b/src/day16.rs
new file mode 100644
index 0000000..bb4c2cd
--- /dev/null
+++ b/src/day16.rs
@@ -0,0 +1,181 @@
+use aoc_runner_derive::{aoc, aoc_generator};
+use grid::{AsCoord2d, Coord2d, Grid};
+use std::{
+ collections::{BinaryHeap, HashMap},
+ str::FromStr,
+};
+
+#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Ord, PartialOrd)]
+enum FacingDirection {
+ East,
+ South,
+ West,
+ North,
+}
+
+impl FacingDirection {
+ fn ofs(&self) -> (i64, i64) {
+ match self {
+ FacingDirection::East => (1, 0),
+ FacingDirection::South => (0, 1),
+ FacingDirection::West => (-1, 0),
+ FacingDirection::North => (0, -1),
+ }
+ }
+ fn reachable(&self) -> [FacingDirection; 3] {
+ // Can move perpendicularly or the same direction, not backwards
+ match self {
+ FacingDirection::East | FacingDirection::West => [*self, FacingDirection::North, FacingDirection::South],
+ FacingDirection::South | FacingDirection::North => [*self, FacingDirection::East, FacingDirection::West],
+ }
+ }
+}
+
+#[derive(Clone, Eq, PartialEq, Debug)]
+struct State {
+ cost: usize,
+ position: Coord2d,
+ facing: FacingDirection,
+}
+
+impl Ord for State {
+ fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+ other
+ .cost
+ .cmp(&self.cost)
+ .then_with(|| self.position.cmp(&other.position))
+ .then_with(|| self.facing.cmp(&other.facing))
+ }
+}
+
+impl PartialOrd for State {
+ fn partial_cmp(&self, other: &Self) -> Option {
+ Some(self.cmp(other))
+ }
+}
+
+struct Maze {
+ map: Grid,
+}
+
+impl FromStr for Maze {
+ type Err = Box;
+ fn from_str(s: &str) -> Result {
+ let map: Grid = s.parse()?;
+
+ Ok(Self { map })
+ }
+}
+
+impl Maze {
+ fn dijkstra(&mut self) -> usize {
+ let start = self.map.find(&b'S').expect("can't find start").to_coord();
+ let finish = self.map.find(&b'E').expect("can't find finish").to_coord();
+
+ let mut distances = HashMap::new();
+ let mut queue = BinaryHeap::with_capacity(self.map.data.len());
+
+ distances.insert((start, FacingDirection::East), 0);
+ queue.push(State {
+ cost: 0,
+ position: start,
+ facing: FacingDirection::East,
+ });
+
+ while let Some(State { cost, position, facing }) = queue.pop() {
+ if position == finish {
+ return cost;
+ }
+
+ if distances.get(&(position, facing)).is_some_and(|v| cost > *v) {
+ continue;
+ }
+
+ for (new_dir, new_position, new_cost) in facing
+ .reachable()
+ .iter()
+ .map(|dir| (dir, &position + dir.ofs()))
+ .filter(|(_, pos)| self.map.get(pos).is_some_and(|c| *c != b'#'))
+ .map(|(dir, pos)| (dir, pos, if *dir == facing { cost + 1 } else { cost + 1001 }))
+ {
+ if distances
+ .get(&(new_position, *new_dir))
+ .is_none_or(|best_cost| new_cost < *best_cost)
+ {
+ queue.push(State {
+ cost: new_cost,
+ position: new_position,
+ facing: *new_dir,
+ });
+ distances.insert((new_position, *new_dir), new_cost);
+ }
+ }
+ }
+ panic!("no path found");
+ }
+}
+
+fn parse(input: &str) -> Maze {
+ input.parse().unwrap()
+}
+
+#[aoc(day16, part1)]
+pub fn part1(input: &str) -> usize {
+ let mut maze = parse(input);
+ maze.dijkstra()
+}
+
+#[aoc(day16, part2)]
+pub fn part2(input: &str) -> i64 {
+ todo!()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ const EXAMPLE1: &str = "###############
+#.......#....E#
+#.#.###.#.###.#
+#.....#.#...#.#
+#.###.#####.#.#
+#.#.#.......#.#
+#.#.#####.###.#
+#...........#.#
+###.#.#####.#.#
+#...#.....#.#.#
+#.#.#.###.#.#.#
+#.....#...#.#.#
+#.###.#.#.#.#.#
+#S..#.....#...#
+###############";
+
+ const EXAMPLE2: &str = "#################
+#...#...#...#..E#
+#.#.#.#.#.#.#.#.#
+#.#.#.#...#...#.#
+#.#.#.#.###.#.#.#
+#...#.#.#.....#.#
+#.#.#.#.#.#####.#
+#.#...#.#.#.....#
+#.#.#####.#.###.#
+#.#.#.......#...#
+#.#.###.#####.###
+#.#.#...#.....#.#
+#.#.#.#####.###.#
+#.#.#.........#.#
+#.#.#.#########.#
+#S#.............#
+#################";
+
+ #[test]
+ fn part1_example() {
+ assert_eq!(part1(EXAMPLE1), 7036);
+ assert_eq!(part1(EXAMPLE2), 11048);
+ }
+
+ #[test]
+ fn part2_example() {
+ assert_eq!(part2(EXAMPLE1), 0);
+ assert_eq!(part2(EXAMPLE2), 0);
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 52da659..345ea54 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,3 +1,4 @@
+mod day16;
use aoc_runner_derive::aoc_lib;
pub mod day1;
diff --git a/utils/grid/lib.rs b/utils/grid/lib.rs
index 99baf3c..dab4207 100644
--- a/utils/grid/lib.rs
+++ b/utils/grid/lib.rs
@@ -7,7 +7,7 @@ use std::{
str::FromStr,
};
-#[derive(Clone, Debug)]
+#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Coord2d {
pub x: i64,
pub y: i64,
@@ -126,7 +126,7 @@ pub struct Grid {
width: i64,
}
-impl Grid {
+impl Grid {
pub fn new(width: i64) -> Self {
Self {
data: Vec::new(),
@@ -134,7 +134,7 @@ impl Grid {
}
}
/// Returns a new [Grid] with the same shape (width x height) as `self`, filled with `fill`
- pub fn same_shape(&self, fill: NT) -> Grid {
+ pub fn same_shape(&self, fill: NT) -> Grid {
Grid {
data: Vec::from_iter(repeat(fill).take(self.width() * self.height())),
width: self.width,