--- /dev/null
+-- Writeup at https://work.njae.me.uk/2024/12/16/advent-of-code-2024-day-16/
+
+import AoC
+
+-- import Data.Char
+import Linear (V2(..), (^+^), perp)
+import qualified Data.Set as S
+import qualified Data.Map.Strict as M
+import Data.Maybe
+import Data.List
+
+type Position = V2 Int -- r, c
+
+type Walls = S.Set Position
+
+data Track = Track
+ { walls :: Walls
+ , start :: Position
+ , goal :: Position
+ } deriving (Eq, Ord, Show)
+
+type TrackCost = M.Map Position Int
+
+
+main :: IO ()
+main =
+ do dataFileName <- getDataFileName
+ text <- readFile dataFileName
+ let track = mkTrack text
+ print track
+ let costsFromStart = costsFrom track (M.singleton track.start 0) (M.singleton track.start 0)
+ let costsFromGoal = costsFrom track (M.singleton track.goal 0) (M.singleton track.goal 0)
+ -- print costsFromStart
+ -- print costsFromGoal
+ -- print $ fmap (\h -> (h, pathCostWithCheat track costsFromStart costsFromGoal h)) $ S.toList track.walls
+ -- print $ allCheatedCosts track costsFromStart costsFromGoal
+ print $ findSavings track costsFromStart costsFromGoal
+ print $ part1 track costsFromStart costsFromGoal
+ -- let allTrails = allRoutesFrom maze
+ -- -- print allTrails
+ -- print $ part1 allTrails
+ -- print $ part2 allTrails
+
+-- part1, part2 :: [Agendum] -> Int
+-- part1 trails = cost $ head trails
+
+findSavings track costsFromStart costsFromGoal = fmap (\g -> (length g, head g)) $ group $ sort savings
+ where fullCost = costsFromStart M.! track.goal
+ cheatCosts = allCheatedCosts track costsFromStart costsFromGoal
+ savings = filter (> 0) $ fmap (\c -> fullCost - c) cheatCosts
+
+part1 track costsFromStart costsFromGoal = length savings
+ where fullCost = costsFromStart M.! track.goal
+ cheatCosts = allCheatedCosts track costsFromStart costsFromGoal
+ savings = filter (>= 100) $ fmap (\c -> fullCost - c) cheatCosts
+
+mkTrack :: String -> Track
+mkTrack text = Track { walls = walls, start = start, goal = goal }
+ where rows = lines text
+ rMax = length rows - 1
+ cMax = (length $ head rows) - 1
+ walls = S.fromList [ V2 r c | r <- [0..rMax], c <- [0..cMax], rows !! r !! c == '#' ]
+ start = head [ V2 r c | r <- [0..rMax], c <- [0..cMax], rows !! r !! c == 'S' ]
+ goal = head [ V2 r c | r <- [0..rMax], c <- [0..cMax], rows !! r !! c == 'E' ]
+
+
+showTrackWithCheat :: Track -> Position -> String
+showTrackWithCheat track here = unlines $ fmap showRow [0..rMax]
+ where rMax = maximum $ fmap (\(V2 r _) -> r) $ S.toList $ walls track
+ cMax = maximum $ fmap (\(V2 _ c) -> c) $ S.toList $ walls track
+ showRow r = [ showCell (V2 r c) | c <- [0..cMax] ]
+ showCell pos
+ | pos == here = '@'
+ | pos == start track = 'S'
+ | pos == goal track = 'E'
+ | pos `S.member` walls track = '☐'
+ | otherwise = '.'
+
+costsFrom :: Track -> TrackCost -> TrackCost -> TrackCost
+costsFrom track costs boundary
+ | M.null boundary = costs
+ | otherwise = costsFrom track costs' boundary''
+ where boundary' = M.foldlWithKey addBoundary M.empty boundary
+ addBoundary acc here cost =
+ M.union acc $ M.fromList $ zip (neighbours track here) (repeat (cost + 1))
+ boundary'' = boundary' `M.difference` costs
+ costs' = costs `M.union` boundary'
+
+neighbours :: Track -> Position -> [Position]
+neighbours track here =
+ filter (`S.notMember` track.walls)
+ [ here ^+^ V2 0 1, here ^+^ V2 0 (-1),
+ here ^+^ V2 1 0, here ^+^ V2 (-1) 0 ]
+
+
+allCheatedCosts track costsFromStart costsFromGoal =
+ catMaybes [ pathCostWithCheat track costsFromStart costsFromGoal h
+ | h <- S.toList track.walls
+ ]
+
+pathCostWithCheat :: Track -> TrackCost -> TrackCost -> Position -> Maybe Int
+pathCostWithCheat track costsFromStart costsFromGoal here
+ | (not $ null costsToStart) && (not $ null costsToGoal) = Just $ minimum costsToStart + minimum costsToGoal + 2
+ | otherwise = Nothing
+ where
+ nbrs = neighbours track here
+ costsToStart = catMaybes $ fmap (`M.lookup` costsFromStart) nbrs
+ costsToGoal = catMaybes $ fmap (`M.lookup` costsFromGoal) nbrs