├── .gitignore ├── LICENSE ├── README.md ├── a-star.lua └── test.lua /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | *.swp 4 | *.lock 5 | profile 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Matthew Smith 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A* for Lua 2 | ========== 3 | 4 | A clean, simple implementation of the A* pathfinding algorithm for Lua. 5 | 6 | This implementation has no dependencies and has a simple interface. It takes a table of nodes, a start and end point and a "valid neighbor" function which makes it easy to adapt the module's behavior, especially in circumstances where valid paths would frequently change. 7 | 8 | The module automatically caches paths which saves CPU time for frequently accessed start and end nodes. Cached paths can be ignored by setting the appropriate parameter in the main function. Cached paths can be purged with the `clear_cached_paths ()` function. 9 | 10 | ## Usage example ## 11 | 12 | -- this function determines which neighbors are valid (e.g., within range) 13 | local valid_node_func = function ( node, neighbor ) 14 | 15 | local MAX_DIST = 300 16 | 17 | -- helper function in the a-star module, returns distance between points 18 | if astar.distance ( node.x, node.y, neighbor.x, neighbor.y ) < MAX_DIST then 19 | return true 20 | end 21 | return false 22 | end 23 | 24 | local ignore = true -- ignore cached paths 25 | 26 | local path = astar.path ( start, end, all_nodes, ignore, valid_node_func ) 27 | 28 | if path then 29 | -- do something with path (a lua table of ordered nodes from start to end) 30 | end 31 | 32 | ## Notes ## 33 | 34 | This assumes that nodes are objects (tables) with (at least) members "x" and "y" that hold the node's coordinates. 35 | 36 | node = {} 37 | node.x = 123 38 | node.y = 456 39 | node.foo = "bar" -------------------------------------------------------------------------------- /a-star.lua: -------------------------------------------------------------------------------- 1 | -- ====================================================================== 2 | -- Copyright (c) 2012 RapidFire Studio Limited 3 | -- All Rights Reserved. 4 | -- http://www.rapidfirestudio.com 5 | 6 | -- Permission is hereby granted, free of charge, to any person obtaining 7 | -- a copy of this software and associated documentation files (the 8 | -- "Software"), to deal in the Software without restriction, including 9 | -- without limitation the rights to use, copy, modify, merge, publish, 10 | -- distribute, sublicense, and/or sell copies of the Software, and to 11 | -- permit persons to whom the Software is furnished to do so, subject to 12 | -- the following conditions: 13 | 14 | -- The above copyright notice and this permission notice shall be 15 | -- included in all copies or substantial portions of the Software. 16 | 17 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | -- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | -- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | -- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | -- CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 | -- TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | -- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -- ====================================================================== 25 | 26 | module ( "astar", package.seeall ) 27 | 28 | ---------------------------------------------------------------- 29 | -- local variables 30 | ---------------------------------------------------------------- 31 | 32 | local INF = 1/0 33 | local cachedPaths = nil 34 | 35 | ---------------------------------------------------------------- 36 | -- local functions 37 | ---------------------------------------------------------------- 38 | 39 | function dist ( x1, y1, x2, y2 ) 40 | 41 | return math.sqrt ( math.pow ( x2 - x1, 2 ) + math.pow ( y2 - y1, 2 ) ) 42 | end 43 | 44 | function dist_between ( nodeA, nodeB ) 45 | 46 | return dist ( nodeA.x, nodeA.y, nodeB.x, nodeB.y ) 47 | end 48 | 49 | function heuristic_cost_estimate ( nodeA, nodeB ) 50 | 51 | return dist ( nodeA.x, nodeA.y, nodeB.x, nodeB.y ) 52 | end 53 | 54 | function is_valid_node ( node, neighbor ) 55 | 56 | return true 57 | end 58 | 59 | function lowest_f_score ( set, f_score ) 60 | 61 | local lowest, bestNode = INF, nil 62 | for _, node in ipairs ( set ) do 63 | local score = f_score [ node ] 64 | if score < lowest then 65 | lowest, bestNode = score, node 66 | end 67 | end 68 | return bestNode 69 | end 70 | 71 | function neighbor_nodes ( theNode, nodes ) 72 | 73 | local neighbors = {} 74 | for _, node in ipairs ( nodes ) do 75 | if theNode ~= node and is_valid_node ( theNode, node ) then 76 | table.insert ( neighbors, node ) 77 | end 78 | end 79 | return neighbors 80 | end 81 | 82 | function not_in ( set, theNode ) 83 | 84 | for _, node in ipairs ( set ) do 85 | if node == theNode then return false end 86 | end 87 | return true 88 | end 89 | 90 | function remove_node ( set, theNode ) 91 | 92 | for i, node in ipairs ( set ) do 93 | if node == theNode then 94 | set [ i ] = set [ #set ] 95 | set [ #set ] = nil 96 | break 97 | end 98 | end 99 | end 100 | 101 | function unwind_path ( flat_path, map, current_node ) 102 | 103 | if map [ current_node ] then 104 | table.insert ( flat_path, 1, map [ current_node ] ) 105 | return unwind_path ( flat_path, map, map [ current_node ] ) 106 | else 107 | return flat_path 108 | end 109 | end 110 | 111 | ---------------------------------------------------------------- 112 | -- pathfinding functions 113 | ---------------------------------------------------------------- 114 | 115 | function a_star ( start, goal, nodes, valid_node_func ) 116 | 117 | local closedset = {} 118 | local openset = { start } 119 | local came_from = {} 120 | 121 | if valid_node_func then is_valid_node = valid_node_func end 122 | 123 | local g_score, f_score = {}, {} 124 | g_score [ start ] = 0 125 | f_score [ start ] = g_score [ start ] + heuristic_cost_estimate ( start, goal ) 126 | 127 | while #openset > 0 do 128 | 129 | local current = lowest_f_score ( openset, f_score ) 130 | if current == goal then 131 | local path = unwind_path ( {}, came_from, goal ) 132 | table.insert ( path, goal ) 133 | return path 134 | end 135 | 136 | remove_node ( openset, current ) 137 | table.insert ( closedset, current ) 138 | 139 | local neighbors = neighbor_nodes ( current, nodes ) 140 | for _, neighbor in ipairs ( neighbors ) do 141 | if not_in ( closedset, neighbor ) then 142 | 143 | local tentative_g_score = g_score [ current ] + dist_between ( current, neighbor ) 144 | 145 | if not_in ( openset, neighbor ) or tentative_g_score < g_score [ neighbor ] then 146 | came_from [ neighbor ] = current 147 | g_score [ neighbor ] = tentative_g_score 148 | f_score [ neighbor ] = g_score [ neighbor ] + heuristic_cost_estimate ( neighbor, goal ) 149 | if not_in ( openset, neighbor ) then 150 | table.insert ( openset, neighbor ) 151 | end 152 | end 153 | end 154 | end 155 | end 156 | return nil -- no valid path 157 | end 158 | 159 | ---------------------------------------------------------------- 160 | -- exposed functions 161 | ---------------------------------------------------------------- 162 | 163 | function clear_cached_paths () 164 | 165 | cachedPaths = nil 166 | end 167 | 168 | function distance ( x1, y1, x2, y2 ) 169 | 170 | return dist ( x1, y1, x2, y2 ) 171 | end 172 | 173 | function path ( start, goal, nodes, ignore_cache, valid_node_func ) 174 | 175 | if not cachedPaths then cachedPaths = {} end 176 | if not cachedPaths [ start ] then 177 | cachedPaths [ start ] = {} 178 | elseif cachedPaths [ start ] [ goal ] and not ignore_cache then 179 | return cachedPaths [ start ] [ goal ] 180 | end 181 | 182 | local resPath = a_star ( start, goal, nodes, valid_node_func ) 183 | if not cachedPaths [ start ] [ goal ] and not ignore_cache then 184 | cachedPaths [ start ] [ goal ] = resPath 185 | end 186 | 187 | return resPath 188 | end 189 | -------------------------------------------------------------------------------- /test.lua: -------------------------------------------------------------------------------- 1 | -- ====================================================================== 2 | -- Copyright (c) 2012 RapidFire Studio Limited 3 | -- All Rights Reserved. 4 | -- http://www.rapidfirestudio.com 5 | 6 | -- Permission is hereby granted, free of charge, to any person obtaining 7 | -- a copy of this software and associated documentation files (the 8 | -- "Software"), to deal in the Software without restriction, including 9 | -- without limitation the rights to use, copy, modify, merge, publish, 10 | -- distribute, sublicense, and/or sell copies of the Software, and to 11 | -- permit persons to whom the Software is furnished to do so, subject to 12 | -- the following conditions: 13 | 14 | -- The above copyright notice and this permission notice shall be 15 | -- included in all copies or substantial portions of the Software. 16 | 17 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | -- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | -- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | -- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | -- CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 | -- TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | -- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -- ====================================================================== 25 | 26 | require "a-star" 27 | 28 | local graph = {} 29 | graph [ 1 ] = {} 30 | graph [ 1 ].id = 1 31 | graph [ 1 ].x = 0 32 | graph [ 1 ].y = 0 33 | graph [ 1 ].player_id = 1 34 | 35 | graph [ 2 ] = {} 36 | graph [ 2 ].id = 2 37 | graph [ 2 ].x = 200 38 | graph [ 2 ].y = 200 39 | graph [ 2 ].player_id = 1 40 | 41 | graph [ 3 ] = {} 42 | graph [ 3 ].id = 3 43 | graph [ 3 ].x = -200 44 | graph [ 3 ].y = 200 45 | graph [ 3 ].player_id = 1 46 | 47 | graph [ 4 ] = {} 48 | graph [ 4 ].id = 4 49 | graph [ 4 ].x = 200 50 | graph [ 4 ].y = -200 51 | graph [ 4 ].player_id = 2 52 | 53 | graph [ 5 ] = {} 54 | graph [ 5 ].id = 5 55 | graph [ 5 ].x = -200 56 | graph [ 5 ].y = -200 57 | graph [ 5 ].player_id = 2 58 | 59 | local valid_node_func = function ( node, neighbor ) 60 | 61 | local MAX_DIST = 300 62 | 63 | if neighbor.player_id == node.player_id and 64 | astar.distance ( node.x, node.y, neighbor.x, neighbor.y ) < MAX_DIST then 65 | return true 66 | end 67 | return false 68 | end 69 | 70 | local path = astar.path ( graph [ 2 ], graph [ 3 ], graph, true, valid_node_func ) 71 | 72 | if not path then 73 | print ( "No valid path found" ) 74 | else 75 | for i, node in ipairs ( path ) do 76 | print ( "Step " .. i .. " >> " .. node.id ) 77 | end 78 | end --------------------------------------------------------------------------------