-module(yog@pathfinding@a_star). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/yog/pathfinding/a_star.gleam"). -export([a_star/7, implicit_a_star_by/8, implicit_a_star/7, a_star_int/4, a_star_float/4]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " [A* (A-Star)](https://en.wikipedia.org/wiki/A*_search_algorithm) search algorithm \n" " for optimal pathfinding with heuristic guidance.\n" "\n" " A* is an informed search algorithm that finds the shortest path from a start node\n" " to a goal node using a heuristic function to guide exploration. It combines the\n" " completeness of Dijkstra's algorithm with the efficiency of greedy best-first search.\n" "\n" " ## Algorithm\n" "\n" " | Algorithm | Function | Complexity | Best For |\n" " |-----------|----------|------------|----------|\n" " | [A* Search](https://en.wikipedia.org/wiki/A*_search_algorithm) | `a_star/7` | O((V + E) log V) | Pathfinding with good heuristics |\n" " | Implicit A* | `implicit_a_star/7` | O((V + E) log V) | Large/infinite graphs generated on-demand |\n" "\n" " ## Key Concepts\n" "\n" " - **Evaluation Function**: f(n) = g(n) + h(n)\n" " - g(n): Actual cost from start to node n\n" " - h(n): Heuristic estimate from n to goal\n" " - f(n): Estimated total cost through n\n" " - **Admissible Heuristic**: h(n) ≤ actual cost (never overestimates)\n" " - **Consistent Heuristic**: h(n) ≤ cost(n→n') + h(n') (triangle inequality)\n" "\n" " ## When to Use A*\n" "\n" " **Use A* when:**\n" " - You have a specific goal node (not single-source to all)\n" " - You can provide a good heuristic estimate\n" " - The heuristic is admissible (underestimates)\n" "\n" " **Use Dijkstra when:**\n" " - No good heuristic available (h(n) = 0 reduces A* to Dijkstra)\n" " - You need shortest paths to all nodes from a source\n" "\n" " ## Heuristic Examples\n" "\n" " | Domain | Heuristic | Admissible? |\n" " |--------|-----------|-------------|\n" " | Grid (4-way) | Manhattan distance | Yes |\n" " | Grid (8-way) | Chebyshev distance | Yes |\n" " | Geospatial | Haversine/great-circle | Yes |\n" " | Road networks | Precomputed landmarks | Yes |\n" "\n" " ## Use Cases\n" "\n" " - **Video games**: NPC pathfinding on game maps\n" " - **GPS navigation**: Route planning with distance estimates\n" " - **Robotics**: Motion planning with obstacle avoidance\n" " - **Puzzle solving**: Sliding puzzles, mazes, labyrinths\n" "\n" " ## References\n" "\n" " - [Wikipedia: A* Search Algorithm](https://en.wikipedia.org/wiki/A*_search_algorithm)\n" " - [Red Blob Games: A* Implementation](https://www.redblobgames.com/pathfinding/a-star/introduction.html)\n" " - [Stanford CS161: A* Lecture](https://web.stanford.edu/class/cs161/lectures/lecture20.pdf)\n" " - [CP-Algorithms: A*](https://cp-algorithms.com/graph/A-star.html)\n" ). -file("src/yog/pathfinding/a_star.gleam", 104). -spec do_a_star( yog@model:graph(any(), JKZ), integer(), yog@internal@pairing_heap:heap({JIR, JIR, list(integer())}), gleam@dict:dict(integer(), JIR), fun((JIR, JKZ) -> JIR), fun((JIR, JIR) -> gleam@order:order()), fun((integer(), integer()) -> JKZ) ) -> gleam@option:option({JIR, list(integer())}). do_a_star(Graph, Goal, Frontier, Visited, Add, Compare, H) -> case yog@internal@priority_queue:pop(Frontier) of {ok, {{_, Dist, [Current | _] = Path}, _}} when Current =:= Goal -> {some, {Dist, Path}}; {ok, {{_, Dist@1, [Current@1 | _] = Path@1}, Rest_frontier}} -> case yog@internal@util:should_explore_node( Visited, Current@1, Dist@1, Compare ) of false -> do_a_star( Graph, Goal, Rest_frontier, Visited, Add, Compare, H ); true -> New_visited = gleam@dict:insert(Visited, Current@1, Dist@1), Successors = yog@model:successors(Graph, Current@1), Next_frontier = begin gleam@list:fold( Successors, Rest_frontier, fun(Acc_h, Neighbor) -> {Next_id, Weight} = Neighbor, Next_dist = Add(Dist@1, Weight), case yog@internal@util:should_explore_node( New_visited, Next_id, Next_dist, Compare ) of true -> F_score = Add( Next_dist, H(Next_id, Goal) ), yog@internal@priority_queue:push( Acc_h, {F_score, Next_dist, [Next_id | Path@1]} ); false -> Acc_h end end ) end, do_a_star( Graph, Goal, Next_frontier, New_visited, Add, Compare, H ) end; _ -> none end. -file("src/yog/pathfinding/a_star.gleam", 83). ?DOC( " Finds the shortest path using A* search with a heuristic function.\n" "\n" " A* is more efficient than Dijkstra when you have a good heuristic estimate\n" " of the remaining distance to the goal.\n" "\n" " **Time Complexity:** O((V + E) log V)\n" "\n" " ## Parameters\n" "\n" " - `zero`: The identity element for addition (e.g., 0 for integers)\n" " - `add`: Function to add two weights\n" " - `compare`: Function to compare two weights\n" " - `heuristic`: A function that estimates distance from any node to the goal.\n" " Must be admissible (h(n) ≤ actual distance).\n" ). -spec a_star( yog@model:graph(any(), JIF), integer(), integer(), JIF, fun((JIF, JIF) -> JIF), fun((JIF, JIF) -> gleam@order:order()), fun((integer(), integer()) -> JIF) ) -> gleam@option:option(yog@pathfinding@path:path(JIF)). a_star(Graph, Start, Goal, Zero, Add, Compare, H) -> Initial_f = H(Start, Goal), Frontier = begin _pipe = yog@internal@priority_queue:new( fun(A, B) -> yog@internal@util:compare_a_star_frontier(A, B, Compare) end ), yog@internal@priority_queue:push(_pipe, {Initial_f, Zero, [Start]}) end, _pipe@1 = do_a_star(Graph, Goal, Frontier, maps:new(), Add, Compare, H), gleam@option:map( _pipe@1, fun(Res) -> {Dist, Path} = Res, {path, lists:reverse(Path), Dist} end ). -file("src/yog/pathfinding/a_star.gleam", 216). -spec do_implicit_a_star_by( yog@internal@pairing_heap:heap({JJD, JJD, JJE}), gleam@dict:dict(JJG, JJD), fun((JJE) -> list({JJE, JJD})), fun((JJE) -> JJG), fun((JJE) -> boolean()), fun((JJE) -> JJD), fun((JJD, JJD) -> JJD), fun((JJD, JJD) -> gleam@order:order()) ) -> gleam@option:option(JJD). do_implicit_a_star_by( Frontier, Distances, Successors, Key_fn, Is_goal, H, Add, Compare ) -> case yog@internal@priority_queue:pop(Frontier) of {ok, {{_, Dist, Current}, Rest_frontier}} -> case Is_goal(Current) of true -> {some, Dist}; false -> Current_key = Key_fn(Current), case yog@internal@util:should_explore_node( Distances, Current_key, Dist, Compare ) of false -> do_implicit_a_star_by( Rest_frontier, Distances, Successors, Key_fn, Is_goal, H, Add, Compare ); true -> New_distances = gleam@dict:insert( Distances, Current_key, Dist ), Next_frontier = begin gleam@list:fold( Successors(Current), Rest_frontier, fun(Frontier_acc, Neighbor) -> {Next_state, Edge_cost} = Neighbor, Next_dist = Add(Dist, Edge_cost), Next_key = Key_fn(Next_state), case yog@internal@util:should_explore_node( New_distances, Next_key, Next_dist, Compare ) of true -> F_score = Add( Next_dist, H(Next_state) ), yog@internal@priority_queue:push( Frontier_acc, {F_score, Next_dist, Next_state} ); false -> Frontier_acc end end ) end, do_implicit_a_star_by( Next_frontier, New_distances, Successors, Key_fn, Is_goal, H, Add, Compare ) end end; _ -> none end. -file("src/yog/pathfinding/a_star.gleam", 187). ?DOC( " Like `implicit_a_star`, but deduplicates visited states by a custom key.\n" "\n" " Essential when your state carries extra data beyond what defines identity.\n" " The `visited_by` function extracts the deduplication key from each state.\n" "\n" " **Time Complexity:** O((V + E) log V) where V and E are measured in unique *keys*\n" ). -spec implicit_a_star_by( JIY, fun((JIY) -> list({JIY, JIZ})), fun((JIY) -> any()), fun((JIY) -> boolean()), fun((JIY) -> JIZ), JIZ, fun((JIZ, JIZ) -> JIZ), fun((JIZ, JIZ) -> gleam@order:order()) ) -> gleam@option:option(JIZ). implicit_a_star_by(Start, Successors, Key_fn, Is_goal, H, Zero, Add, Compare) -> Initial_f = H(Start), Frontier = begin _pipe = yog@internal@priority_queue:new( fun(A, B) -> Compare(erlang:element(1, A), erlang:element(1, B)) end ), yog@internal@priority_queue:push(_pipe, {Initial_f, Zero, Start}) end, do_implicit_a_star_by( Frontier, maps:new(), Successors, Key_fn, Is_goal, H, Add, Compare ). -file("src/yog/pathfinding/a_star.gleam", 160). ?DOC( " Finds the shortest path in an implicit graph using A* search with a heuristic.\n" "\n" " **Time Complexity:** O((V + E) log V)\n" "\n" " ## Parameters\n" "\n" " - `zero`: The identity element for addition (e.g., 0 for integers)\n" " - `add`: Function to add two costs\n" " - `compare`: Function to compare two costs\n" " - `heuristic`: Function that estimates remaining cost from any state to goal.\n" " Must be admissible.\n" ). -spec implicit_a_star( JIU, fun((JIU) -> list({JIU, JIV})), fun((JIU) -> boolean()), fun((JIU) -> JIV), JIV, fun((JIV, JIV) -> JIV), fun((JIV, JIV) -> gleam@order:order()) ) -> gleam@option:option(JIV). implicit_a_star(Start, Successors, Is_goal, H, Zero, Add, Compare) -> implicit_a_star_by( Start, Successors, fun(S) -> S end, Is_goal, H, Zero, Add, Compare ). -file("src/yog/pathfinding/a_star.gleam", 321). ?DOC( " Finds the shortest path using A* with **integer weights**.\n" "\n" " This is a convenience wrapper around `a_star` that uses:\n" " - `0` as the zero element\n" " - `int.add` for addition\n" " - `int.compare` for comparison\n" "\n" " You still need to provide a heuristic function.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " // Grid distance heuristic (Manhattan distance)\n" " let heuristic = fn(from, to) {\n" " let dx = int.absolute_value(from.x - to.x)\n" " let dy = int.absolute_value(from.y - to.y)\n" " dx + dy\n" " }\n" "\n" " a_star.a_star_int(graph, from: start, to: goal, with_heuristic: heuristic)\n" " ```\n" ). -spec a_star_int( yog@model:graph(any(), integer()), integer(), integer(), fun((integer(), integer()) -> integer()) ) -> gleam@option:option(yog@pathfinding@path:path(integer())). a_star_int(Graph, Start, Goal, H) -> a_star( Graph, Start, Goal, 0, fun gleam@int:add/2, fun gleam@int:compare/2, H ). -file("src/yog/pathfinding/a_star.gleam", 346). ?DOC( " Finds the shortest path using A* with **float weights**.\n" "\n" " This is a convenience wrapper around `a_star` that uses:\n" " - `0.0` as the zero element\n" " - `float.add` for addition\n" " - `float.compare` for comparison\n" "\n" " You still need to provide a heuristic function.\n" ). -spec a_star_float( yog@model:graph(any(), float()), integer(), integer(), fun((integer(), integer()) -> float()) ) -> gleam@option:option(yog@pathfinding@path:path(float())). a_star_float(Graph, Start, Goal, H) -> a_star( Graph, Start, Goal, +0.0, fun gleam@float:add/2, fun gleam@float:compare/2, H ).