--- /dev/null
+use std::error::Error;
+use std::path::Path;
+use std::collections::{HashMap, HashSet};
+
+struct Puzzle {
+ path: HashMap<(isize, isize), u32>,
+ walls: HashSet<(isize, isize)>,
+ start: (isize, isize),
+ end: (isize, isize),
+}
+
+impl Puzzle {
+ pub fn new(input: &str) -> Self {
+ let mut path: HashMap<(isize, isize), u32> = HashMap::new();
+ let mut walls: HashSet<(isize, isize)> = HashSet::new();
+ let mut start = (-1, -1);
+ let mut end = (-1, -1);
+ input.lines().enumerate()
+ .for_each(|(row, line)| {
+ line.as_bytes().iter().enumerate()
+ .for_each(|(col, v)| {
+ let (row, col) = (row as isize, col as isize);
+ match *v {
+ b'.' => { path.insert((row, col), 0); },
+ b'#' => { walls.insert((row, col)); },
+ b'S' => {
+ start = (row, col);
+ path.insert((row, col), 0);
+ },
+ b'E' => {
+ end = (row, col);
+ path.insert((row, col), 0);
+ }
+ _ => unreachable!(),
+ }
+ });
+ });
+
+ Puzzle::build_dst(&mut path, start, end);
+
+ Self { path, walls, start, end }
+ }
+
+ /*
+ * There is only one path between S and E.
+ */
+ fn build_dst(path: &mut HashMap<(isize, isize), u32>, start: (isize, isize), end: (isize, isize)) {
+ let mut pos = start;
+ while pos != end {
+ let dst = *path.get(&pos).unwrap();
+ for d in [(-1, 0), (0, 1), (1, 0), (0, -1)] {
+ let pos_next = (pos.0 + d.0, pos.1 + d.1);
+ if let Some(dst_next) = path.get_mut(&pos_next) {
+ if pos_next == start || *dst_next > 0 {
+ continue;
+ }
+ *dst_next = dst + 1;
+ pos = pos_next;
+ break;
+ }
+ }
+ }
+ }
+
+ fn is_cheat_wall_horizontal(&self, wall: (isize, isize)) -> bool {
+ [(0, 1), (0, -1)].into_iter()
+ .map(|dir| (wall.0 + dir.0, wall.1 + dir.1))
+ .all(|pos| self.path.contains_key(&pos))
+ }
+
+ fn is_cheat_wall_vertical(&self, wall: (isize, isize)) -> bool {
+ [(-1, 0), (1, 0)].into_iter()
+ .map(|dir| (wall.0 + dir.0, wall.1 + dir.1))
+ .all(|pos| self.path.contains_key(&pos))
+ }
+
+ fn count_cheats_above(&self, ps: u32) -> u64 {
+ self.walls.iter()
+ .filter(|&w| self.is_cheat_wall_vertical(*w) || self.is_cheat_wall_horizontal(*w))
+ .map(|&w| {
+ let (mut p1, mut p2) = (w, w);
+ let (mut dst1, mut dst2) = (0, 0);
+ if self.is_cheat_wall_vertical(w)
+ {
+ p1.0 -= 1;
+ p2.0 += 1;
+ dst1 = *self.path.get(&p1).unwrap();
+ dst2 = *self.path.get(&p2).unwrap();
+ }
+ else if self.is_cheat_wall_horizontal(w)
+ {
+ p1.1 -= 1;
+ p2.1 += 1;
+ dst1 = *self.path.get(&p1).unwrap();
+ dst2 = *self.path.get(&p2).unwrap();
+ }
+ dst1.abs_diff(dst2)
+ })
+ .filter(|dst| *dst >= ps + 2)
+ .count() as u64
+ }
+}
+
+fn run_part1(input: &str) -> Result<u64, Box<dyn Error>> {
+ println!("Running {} - part 1", get_day());
+ let puzzle = Puzzle::new(input);
+ Ok(puzzle.count_cheats_above(100))
+}
+
+fn run_part2(input: &str) -> Result<u64, Box<dyn Error>> {
+ println!("Running {} - part 2", get_day());
+
+ Ok(0)
+}
+
+pub fn run(input: &str) -> Result<(), Box<dyn Error>> {
+ let res = run_part1(input)?;
+ println!("{res}");
+
+ let res = run_part2(input)?;
+ println!("{res}");
+
+ Ok(())
+}
+
+fn get_day() -> String {
+ let filename = file!();
+ Path::new(filename).file_stem().unwrap().to_str().unwrap().to_string()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ static TEXT_INPUT: &str = "\
+###############
+#...#...#.....#
+#.#.#.#.#.###.#
+#S#...#.#.#...#
+#######.#.#.###
+#######.#.#...#
+#######.#.###.#
+###..E#...#...#
+###.#######.###
+#...###...#...#
+#.#####.#.###.#
+#.#...#.#.#...#
+#.#.#.#.#.#.###
+#...#...#...###
+###############";
+
+ #[test]
+ fn test_part1() {
+ let puzzle = Puzzle::new(TEXT_INPUT);
+ assert_eq!(1, puzzle.count_cheats_above(64));
+ assert_eq!(2, puzzle.count_cheats_above(40));
+ assert_eq!(3, puzzle.count_cheats_above(38));
+ assert_eq!(4, puzzle.count_cheats_above(36));
+ assert_eq!(5, puzzle.count_cheats_above(20));
+ assert_eq!(8, puzzle.count_cheats_above(12));
+ assert_eq!(10, puzzle.count_cheats_above(10));
+ assert_eq!(14, puzzle.count_cheats_above(8));
+ assert_eq!(16, puzzle.count_cheats_above(6));
+ assert_eq!(30, puzzle.count_cheats_above(4));
+ assert_eq!(44, puzzle.count_cheats_above(2));
+ }
+
+ #[test]
+ fn test_part2() {
+ assert_eq!(0, run_part2(TEXT_INPUT).unwrap());
+ }
+}