defmodule Adventurous.Graph do defmodule NeighbourCost do defstruct vertex: nil, cost: 0 end defp compare_neighbours(%NeighbourCost{cost: c1}, %NeighbourCost{cost: c2}) do cond do c1 < c2 -> :lt c1 > c2 -> :gt true -> :eq end end def a_star(start, goal, neighbour_func, vertex_cost) do open_set = Prioqueue.new([%NeighbourCost{vertex: start}], cmp_fun: &compare_neighbours/2) came_from = Map.new() g_score = Map.new() |> Map.put(start, 0) stream = Stream.repeatedly(fn -> 1 end) {_, came_from, score} = Enum.reduce_while(stream, {open_set, came_from, g_score}, fn _, {open_set, came_from, g_score} -> case Prioqueue.extract_min(open_set) do {:error, _} -> {:halt, :error} {:ok, {%NeighbourCost{vertex: ^goal}, _}} -> {:halt, {nil, came_from, g_score}} {:ok, {%NeighbourCost{vertex: current_vertex}, new_open_set}} -> neighbour_costs = get_neighbours_costs(current_vertex, g_score, neighbour_func, vertex_cost) {new_open_set, new_came_from, new_g_score} = process_neighbours(current_vertex, neighbour_costs, new_open_set, came_from, g_score) {:cont, {new_open_set, new_came_from, new_g_score}} end end) {Map.get(score, goal), came_from} end defp process_neighbours(current_vertex, neighbour_costs, open_set, came_from, g_score) do Enum.reduce(neighbour_costs, {open_set, came_from, g_score}, fn neighbour_cost, {open_set, came_from, g_score} -> prev_cost = Map.get(g_score, neighbour_cost.point, :infinity) if prev_cost > neighbour_cost.cost do new_open_set = update_open_set(open_set, neighbour_cost, prev_cost) new_came_from = Map.put(came_from, neighbour_cost.vertex, current_vertex) new_g_score = Map.put(g_score, neighbour_cost.vertex, neighbour_cost.cost) {new_open_set, new_came_from, new_g_score} else {open_set, came_from, g_score} end end) end defp get_neighbours_costs(current_vertex, g_score, neighbour_func, cost_func) do neighbours = neighbour_func.(current_vertex) Enum.map(neighbours, fn neighbour -> prev_cost = Map.get(g_score, current_vertex, :infinity) cost = cost_func.(neighbour) %NeighbourCost{vertex: neighbour, cost: prev_cost + cost} end) end defp update_open_set(open_set, neighbour_cost, prev_cost) do new_open_set = if Prioqueue.member?(open_set, %NeighbourCost{vertex: neighbour_cost.vertex, cost: prev_cost}) do Prioqueue.to_list(open_set) |> Enum.filter(fn nc -> nc.point != neighbour_cost.point end) |> Prioqueue.new(cmp_fun: &compare_neighbours/2) else open_set end Prioqueue.insert(new_open_set, neighbour_cost) end end