{lib, pkgs, ...}: init: rec { inherit lib; inherit (lib) splitString mod range; inherit (lib.lists) findFirstIndex imap0; inherit (lib.strings) stringToCharacters; inherit (builtins) elemAt concatStringsSep length elem filter foldl' concatLists floor deepSeq listToAttrs attrValues attrNames mapAttrs hasAttr sort genList; index = i: list: if isNull list || i < 0 || i >= length list then null else elemAt list i; index2d = {x, y}: m: m |> index y |> index x; chartToStr = chart: chart |> (map (concatStringsSep "")) |> concatStringsSep "\n"; rotate' = {x, y}: {y = -x; x = y;}; rotate = {x, y}: {y = x; x = -y;}; addVec = a: b: {x = a.x or 0 + b.x or 0; y = a.y or 0 + b.y or 0;}; multVec = a: m: {x = a.x or 0 * m; y = a.y or 0 * m;}; dirs = { north = {x = 0; y = -1;}; east = {x = 1; y = 0;}; south = {x = 0; y = 1;}; west = {x = -1; y = 0;}; }; search = { pos, dir, score ? 0, }: let fwd = addVec pos dir; in if elem (index2d fwd init.chart) [null "#"] then {pos = null; dist = null;} else if (isCorner fwd || fwd == init.goal || fwd == init.pos) then { pos = (key fwd); dist = score + 1; } else search {pos = fwd; inherit dir; score = score + 1;} ; isCorner = pos: index2d pos init.chart == "." # && # (dirs # |> mapAttrs (name: dir: let n = addVec pos dir; in index2d n init.chart == ".") # |> ({north, east, south, west}: # north && east || east && south || south && west || west && north)) ; key = {x, y}: "${toString x},${toString y}"; # gives locations like vim cursor indicator #"${toString (y + 1)},${toString (x + 1)}"; corners = range 0 (init.width - 1) |> map (x: range 0 (init.height - 1) |> map (y: {inherit x y;}) ) |> concatLists |> filter isCorner |> (nodes: nodes ++ [init.pos init.goal]) |> map (pos: { name = key pos; value = ({ north = search {inherit pos; dir = dirs.north;}; east = search {inherit pos; dir = dirs.east;}; south = search {inherit pos; dir = dirs.south;}; west = search {inherit pos; dir = dirs.west;}; } |> lib.filterAttrs (n: v: !isNull v.pos)); }) |> listToAttrs; notNull = value: !isNull value; nodeGraph = graph: pkgs.runCommand "graph" {} '' mkdir -p $out echo 'digraph { ${graph |> mapAttrs (from: edges: edges |> mapAttrs (dir: edge: if isNull edge.pos or null then "" else '' "${from}" -> "${edge.pos}" [label="${dir} ${toString edge.dist}"] '') |> attrValues |> concatStringsSep "\n" ) |> attrValues |> concatStringsSep "\n"} }' > $out/graph.dot cat $out/graph.dot | ${pkgs.graphviz}/bin/dot -Tsvg > $out/graph.svg ''; graphs = { corners = nodeGraph corners; }; initScores = { ${key init.pos} = { dir = "east"; steps = 0; dist = 0; turns = 0; pos = key init.pos; }; }; getLowest = state: let unvisited = removeAttrs state.scores state.done; in attrValues unvisited |> foldl' (acc: node: if isNull acc.steps then node else if isNull node.steps then acc else if acc.steps < node.steps then acc else node ) {turns = null; steps = null; pos = null;} ; getScores = {done ? [], scores ? initScores}: let prev = getLowest {inherit done scores;}; in if isNull prev.pos then {inherit done scores; impossible = true;} else corners.${prev.pos} |> mapAttrs (dir: edge: let turns = if prev.dir == dir then prev.turns else prev.turns + 1; existing = scores.${edge.pos} or null; steps = prev.steps + edge.dist; # can't evaluate same turns # because we don't know where we will turn next. sameSteps = notNull existing && existing.steps == steps ; isBetter = isNull existing || (existing.steps > steps); newScore = { inherit steps dir turns; inherit (edge) pos dist; prev = prev.pos; }; in { name = edge.pos; value = if sameSteps then existing // {alt = newScore;} else if isBetter then newScore else existing ; }) |> attrValues |> listToAttrs |> (newScores: { scores = scores // newScores; done = done ++ [prev.pos]; }) ; getFastest = goal: s: let next = getScores s; in if next.impossible or false || elem (key goal) (next.done) then next else getFastest goal next; pathScore = steps: turns: steps + turns * 1000; isImpossible = (getFastest init.goal {}).impossible or false; fastestTo = (getFastest init.goal {}).scores; toGoal = fastestTo.${key init.goal}; part1result = pathScore toGoal.steps toGoal.turns; # PART 2 # WIP: # go through the path, add each pos to visited # if point has alt, # TODO: and the alt turns add up to the correct path. # find points on that path. # add length of filtered alt, (-1 for the overlap at end?). # add alt to visited. pathToList = scores: {pos ? key init.goal, visited ? []}: let res = scores.${pos}; prevPath = if res ? prev then pathToList scores {pos = res.prev; visited = visited;} else {path = []; visited = [pos];}; a = pathToList scores {pos = res.alt.prev; visited = visited ++ prevPath.visited;}; altPath = if res ? alt then {path = [thisAltSpot] ++ a.path; inherit (a) visited;} else {path = []; visited = [];} ; thisSpot = {inherit (res) pos steps turns dir dist; # alt = altPath.path; }; thisAltSpot = {inherit (res.alt) pos steps turns dir dist; alt = []; }; in { visited = [pos] ++ prevPath.visited ++ altPath.visited; path = if elem pos visited then [] else [thisSpot] ++ prevPath.path ; }; cornerScores = getFastest init.goal {}; path = (pathToList cornerScores.scores {}).path; pathChart = genList (y: genList (x: if index2d {inherit x y;} init.chart == "#" then "#" else if elem (key {inherit x y;}) (map ({pos,...}: pos) path) then "O" else "." ) init.width) init.height; }