diff --git a/init.lua b/init.lua index 79df56a..ddf3453 100644 --- a/init.lua +++ b/init.lua @@ -40,6 +40,7 @@ local _batteries = { pretty = require_relative("pretty"), measure = require_relative("measure"), make_pooled = require_relative("make_pooled"), + pathfind = require_relative("pathfind"), } --assign aliases diff --git a/pathfind.lua b/pathfind.lua new file mode 100644 index 0000000..cdabecd --- /dev/null +++ b/pathfind.lua @@ -0,0 +1,109 @@ +--[[ + general pathfinding with a general goal +]] + +local path = (...):gsub("pathfind", "") +local sort = require(path .. "sort") + +--helper to generate a constant value for default weight, heuristic +local generate_constant = function() return 1 end + +--find a path in a weighted graph +-- uses A* algorithm internally +--returns a table of all the nodes from the start to the goal, +-- or false if no path was found +--arguments table requires the following fields +-- start - the value of the node to start from +-- alias: start_node +-- is_goal - a function which takes a node, and returns true is a goal +-- alias: goal +-- neighbours - a function which takes a node, and returns a table of neighbour nodes +-- alias: generate_neighbours +-- distance - a function which given two nodes, returns the distance between them +-- defaults to a constant distance, which means all steps between nodes have the same cost +-- alias: weight, g +-- heuristic - a function which given a node, returns a heuristic indicative of the distance to the goal; +-- can be used to speed up searches at the cost of accuracy by returning some proportion higher than the actual distance +-- defaults to a constant value, which means the A* algorithm degrades to breadth-first search +-- alias: h +function pathfind(args) + local start = args.start or args.start_node + local is_goal = args.is_goal or args.goal + local neighbours = args.neighbours or args.generate_neighbours + local distance = args.distance or args.weight or args.g or generate_constant + local heuristic = args.heuristic or args.h or generate_constant + + local predecessor = {} + local seen = {} + local f_score = {[start] = 0} + local g_score = {[start] = 0} + + local function search_compare(a, b) + return + (f_score[a] or math.huge) > + (f_score[b] or math.huge) + end + local to_search = {} + + local current = start + while current and not is_goal(current) do + seen[current] = true + for i, node in ipairs(neighbours(current)) do + if not seen[node] then + local tentative_g_score = (g_score[current] or math.huge) + distance(current, node) + if g_score[node] == nil then + if tentative_g_score < (g_score[node] or math.huge) then + predecessor[node] = current + g_score[node] = tentative_g_score + f_score[node] = tentative_g_score + heuristic(node) + end + table.insert(to_search, node) + sort.insertion_sort(to_search, search_compare) + end + end + end + current = table.remove(to_search) + end + + --didn't make it to the goal + if not current or not is_goal(current) then + return false + end + + --build up the ordering + local path = {} + while current do + table.insert(path, 1, current) + current = predecessor[current] + end + + --return a path + return path +end + +-- Based on https://github.com/lewtds/pathfinder.lua/blob/master/pathfinder.lua +-- with modification to allow for generalised goals/heuristics +-- Modified 2022 Max Cahill + +-- Original License: +-- Copyright © 2016 Trung Ngo + +-- Permission is hereby granted, free of charge, to any person obtaining a +-- copy of this software and associated documentation files (the \"Software\"), +-- to deal in the Software without restriction, including without limitation +-- the rights to use, copy, modify, merge, publish, distribute, sublicense, +-- and/or sell copies of the Software, and to permit persons to whom the +-- Software is furnished to do so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in +-- all copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +-- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +-- DEALINGS IN THE SOFTWARE. + +return pathfind