├── .gitignore ├── example ├── example.gif ├── lua-star-01.png ├── conf.lua └── main.lua ├── .busted ├── .travis.yml ├── LICENSE ├── tests ├── performance.lua └── lua-star_spec.lua ├── README.md └── src └── lua-star.lua /.gitignore: -------------------------------------------------------------------------------- 1 | example/lua-star.lua 2 | tests/lua-star.lua 3 | -------------------------------------------------------------------------------- /example/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wesleywerner/lua-star/HEAD/example/example.gif -------------------------------------------------------------------------------- /example/lua-star-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wesleywerner/lua-star/HEAD/example/lua-star-01.png -------------------------------------------------------------------------------- /example/conf.lua: -------------------------------------------------------------------------------- 1 | function love.conf(t) 2 | t.title = "lua-star example" 3 | t.identity = "lua-star" 4 | end 5 | -------------------------------------------------------------------------------- /.busted: -------------------------------------------------------------------------------- 1 | return { 2 | default = { 3 | verbose = true, 4 | coverage = false, 5 | ROOT = {"tests"}, 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | 4 | env: 5 | - LUA="lua=5.1" 6 | 7 | before_install: 8 | - pip install hererocks 9 | - hererocks lua_install -r^ --$LUA 10 | - export PATH=$PATH:$PWD/lua_install/bin # Add directory with all installed binaries to PATH 11 | 12 | install: 13 | - luarocks install busted 14 | 15 | script: 16 | - busted 17 | 18 | after_success: 19 | - luacov-coveralls --exclude $TRAVIS_BUILD_DIR/lua_install 20 | 21 | branches: 22 | only: 23 | - master 24 | - stable 25 | - beta 26 | except: 27 | - gh-pages 28 | 29 | notifications: 30 | email: 31 | on_success: change 32 | on_failure: always 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Wesley Werner 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /tests/performance.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright 2018 Wesley Werner 3 | 4 | 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: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | 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. 9 | ]]-- 10 | 11 | local luastar = require("lua-star") 12 | local map = { } 13 | local mapsize = 3000 14 | local numberOfTests = 1000 15 | local mapDensity = 0.65 16 | 17 | local seed = os.time() 18 | math.randomseed(seed) 19 | print (string.format("Running with seed %d", seed)) 20 | 21 | print (string.format("Building a map of %dx%d...", mapsize, mapsize)) 22 | for x=1, mapsize do 23 | map[x] = {} 24 | for y=1, mapsize do 25 | map[x][y] = math.random() 26 | end 27 | end 28 | 29 | -- precalculate a bunch of start and goal positions 30 | -- doubled up for each start/goal pair 31 | 32 | print (string.format("Precalculating %d random start/goal positions...", mapsize * 2)) 33 | local testPoints = { } 34 | for i = 1, mapsize * 2 do 35 | table.insert (testPoints, { x = math.random(1, mapsize), y = math.random(1, mapsize)}) 36 | end 37 | 38 | print (string.format("Finding %d paths...", numberOfTests)) 39 | function positionIsOpenFunc(x, y) 40 | return map[x][y] > mapDensity 41 | end 42 | local testStart = os.clock() 43 | for testNumber = 1, numberOfTests do 44 | luastar:find( 45 | mapsize, mapsize, -- map size 46 | table.remove (testPoints), -- start 47 | table.remove (testPoints), -- goal 48 | positionIsOpenFunc) 49 | end 50 | local testEnd = os.clock() 51 | local totalSec = testEnd - testStart 52 | local pathSec = totalSec / numberOfTests 53 | 54 | print (string.format([[ 55 | Done in %.2f seconds. 56 | That is %.4f seconds, or %d milliseconds, per path. 57 | The map has %.1f million locations, with about %d%% open space.]], 58 | totalSec, -- total seconds 59 | pathSec, -- seconds per path 60 | pathSec*1000, -- milliseconds per path 61 | (mapsize*mapsize)/1000000, -- number of locations 62 | mapDensity*100 -- % open space on the map 63 | )) 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lua-star 2 | 3 | [![Build Status](https://www.travis-ci.com/wesleywerner/lua-star.svg?branch=master)](https://www.travis-ci.com/wesleywerner/lua-star) 4 | 5 | Lua-star is a pure Lua A* path-finding library. 6 | 7 | ![lua star example screenshot](example/example.gif) 8 | 9 | # Quick Start 10 | 11 | Easy to use, it will make you more attractive and you feel sensual doing so. 12 | 13 | local luastar = require("lua-star") 14 | 15 | function positionIsOpenFunc(x, y) 16 | -- should return true if the position is open to walk 17 | return mymap[x][y] == walkable 18 | end 19 | 20 | local path = luastar:find(width, height, start, goal, positionIsOpenFunc, useCache, excludeDiagonalMoving) 21 | 22 | `path` will be false if no path was found, otherwise it contains a list of points that travel from `start` to `goal`: 23 | 24 | if path then 25 | for _, p in ipairs(path) do 26 | print(p.x, p.y) 27 | end 28 | end 29 | 30 | Lua star does not care how your map data is arranged, it simply asks you if the map position at `x,y` is walkable via a callback. 31 | 32 | `width` and `height` is your map size. 33 | 34 | `start` and `goal` are tables with at least the `x` and `y` keys. 35 | 36 | local start = { x = 1, y = 10 } 37 | local goal = { x = 10, y = 1 } 38 | 39 | `positionIsOpenFunc(x, y)` is a function that should return true if the position is open to walk. 40 | 41 | `useCache` is optional and defaults to `false` when not given. If you have a map that does not change, caching can give a speed boost. 42 | 43 | If at any time you need to clear all cached paths: 44 | 45 | luastar:clearCached() 46 | 47 | `excludeDiagonalMoving` also optional value defaults to `false`. If you want to exclude the possibility of moving diagonally set the value `true`. i.e, by default, diagonal movement is **enabled** 48 | 49 | # Requirements 50 | 51 | * [Lua 5.x](http://www.lua.org/) 52 | 53 | For running unit tests: 54 | 55 | * [Lua Rocks](https://luarocks.org/) 56 | * busted 57 | 58 | These commands are for apt-based systems, please adapt to them as needed. 59 | 60 | sudo apt-get install luarocks 61 | sudo luarocks install busted 62 | 63 | Unit testing is done with busted, the `.busted` config already defines everything, so simply run: 64 | 65 | busted 66 | 67 | # Performance 68 | 69 | There is a performance measurement tool in `tests/performance.lua`, it calculates the average time to find a path on a large, random map. 70 | 71 | # copy the lib to tests 72 | $ cp ../src/lua-star.lua . 73 | 74 | # measure performance 75 | $ lua performance.lua 76 | Running with seed 1540584306 77 | Building a map of 3000x3000... 78 | Precalculating 6000 random start/goal positions... 79 | Finding 1000 paths... 80 | Done in 16.37 seconds. 81 | That is 0.0164 seconds, or 16 milliseconds, per path. 82 | The map has 9.0 million locations, with about 65% open space. 83 | 84 | 85 | # Example 86 | 87 | There is an [interactive example](example/main.lua) that can be run with [Love](https://love2d.org). 88 | 89 | # License 90 | 91 | See the file [LICENSE](LICENSE) 92 | -------------------------------------------------------------------------------- /example/main.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Lua star example - Run with love (https://love2d.org/) 3 | Copyright 2018 Wesley Werner 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | ]]-- 11 | 12 | local luastar = require("lua-star") 13 | 14 | -- a 2D map where true is open and false is blocked 15 | local map = { } 16 | local mapsize = 25 17 | local screensize = 500 18 | local tilesize = screensize / mapsize 19 | 20 | -- path start and end 21 | local path = nil 22 | local start = { x = 1, y = mapsize } 23 | local goal = { x = mapsize, y = 1 } 24 | 25 | -- remember the tile the mouse is hovered over 26 | local hoveredTile 27 | 28 | -- fonts 29 | local largeFont = love.graphics.newFont (30) 30 | local smallFont = love.graphics.newFont (10) 31 | 32 | -- save a screenshot 33 | local saveScreenshot = false 34 | 35 | function randomizeMap () 36 | 37 | -- build an open map 38 | for x=1, mapsize do 39 | map[x] = {} 40 | for y=1, mapsize do 41 | map[x][y] = true 42 | end 43 | end 44 | 45 | -- add random walls 46 | math.randomseed (os.clock ()) 47 | for i = 1, 45 do 48 | -- start point 49 | local x = math.random (1, mapsize-2) 50 | local y = math.random (1, mapsize-2) 51 | -- vertical or horizontal 52 | if math.random() > .5 then 53 | for n = 1, 5 do 54 | map[x][math.min (mapsize, y+n)] = false 55 | end 56 | else 57 | for n = 1, 5 do 58 | map[math.min (mapsize, x+n)][y] = false 59 | end 60 | end 61 | end 62 | 63 | requestPath() 64 | --saveScreenshot = true 65 | 66 | end 67 | 68 | function love.load () 69 | 70 | love.window.setMode (screensize, screensize) 71 | randomizeMap() 72 | 73 | end 74 | 75 | function love.keypressed (key) 76 | 77 | if key == "escape" then 78 | love.event.quit() 79 | elseif key == "space" then 80 | randomizeMap() 81 | end 82 | 83 | end 84 | 85 | function love.draw () 86 | 87 | -- draw walls 88 | love.graphics.setColor(.6, .6, .6) 89 | for x=1, mapsize do 90 | for y=1, mapsize do 91 | local fillstyle = "line" 92 | if map[x][y] == false then fillstyle = "fill" end 93 | love.graphics.rectangle(fillstyle, (x-1)*tilesize, (y-1)*tilesize, tilesize, tilesize) 94 | end 95 | end 96 | 97 | -- draw the path 98 | love.graphics.setFont (smallFont) 99 | if path then 100 | for i, p in ipairs(path) do 101 | love.graphics.setColor(0, 1, 0) 102 | love.graphics.rectangle("fill", (p.x-1)*tilesize, (p.y-1)*tilesize, tilesize, tilesize) 103 | love.graphics.setColor(0, 0, 0) 104 | love.graphics.print(i, (p.x-1) * tilesize, (p.y-1) * tilesize) 105 | end 106 | end 107 | 108 | -- draw start and end 109 | love.graphics.setColor(1, 0, 0) 110 | love.graphics.setFont (largeFont) 111 | love.graphics.print("*", (start.x-1) * tilesize, (start.y-1) * tilesize) 112 | love.graphics.print("*", (goal.x-1) * tilesize, (goal.y-1) * tilesize) 113 | 114 | if saveScreenshot then 115 | saveScreenshot = false 116 | local filename = string.format("screenshot-%d.png", os.time()) 117 | love.graphics.captureScreenshot(filename) 118 | print (string.format("written %s", filename)) 119 | end 120 | 121 | end 122 | 123 | function love.mousemoved (x, y, dx, dy, istouch) 124 | 125 | local dx = math.floor(x / tilesize) + 1 126 | local dy = math.floor(y / tilesize) + 1 127 | 128 | if hoveredTile then 129 | if hoveredTile.dx == dx and hoveredTile.dy == dy then 130 | return 131 | end 132 | end 133 | 134 | hoveredTile = { dx = dx, dy = dy } 135 | if love.mouse.isDown (1) then 136 | map[dx][dy] = not map[dx][dy] 137 | requestPath() 138 | end 139 | 140 | end 141 | 142 | function love.mousepressed (x, y, button, istouch) 143 | 144 | local dx = math.floor(x / tilesize) + 1 145 | local dy = math.floor(y / tilesize) + 1 146 | map[dx][dy] = not map[dx][dy] 147 | requestPath() 148 | 149 | end 150 | 151 | function positionIsOpenFunc (x, y) 152 | 153 | -- should return true if the position is open to walk 154 | return map[x][y] 155 | 156 | end 157 | 158 | function requestPath () 159 | 160 | path = luastar:find(mapsize, mapsize, start, goal, positionIsOpenFunc) 161 | 162 | end 163 | -------------------------------------------------------------------------------- /src/lua-star.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Lua star example - Run with love (https://love2d.org/) 3 | Copyright 2018 Wesley Werner 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | 11 | References: 12 | https://en.wikipedia.org/wiki/A*_search_algorithm 13 | https://www.redblobgames.com/pathfinding/a-star/introduction.html 14 | https://www.raywenderlich.com/4946/introduction-to-a-pathfinding 15 | ]]-- 16 | 17 | --- Provides easy A* path finding. 18 | -- @module lua-star 19 | 20 | local module = {} 21 | 22 | --- Clears all cached paths. 23 | function module:clearCached() 24 | module.cache = nil 25 | end 26 | 27 | -- (Internal) Returns a unique key for the start and end points. 28 | local function keyOf(start, goal) 29 | return string.format("%d,%d>%d,%d", start.x, start.y, goal.x, goal.y) 30 | end 31 | 32 | -- (Internal) Returns the cached path for start and end points. 33 | local function getCached(start, goal) 34 | if module.cache then 35 | local key = keyOf(start, goal) 36 | return module.cache[key] 37 | end 38 | end 39 | 40 | -- (Internal) Saves a path to the cache. 41 | local function saveCached(start, goal, path) 42 | module.cache = module.cache or { } 43 | local key = keyOf(start, goal) 44 | module.cache[key] = path 45 | end 46 | 47 | -- (Internal) Return the distance between two points. 48 | -- This method doesn't bother getting the square root of s, it is faster 49 | -- and it still works for our use. 50 | local function distance(x1, y1, x2, y2) 51 | local dx = x1 - x2 52 | local dy = y1 - y2 53 | local s = dx * dx + dy * dy 54 | return s 55 | end 56 | 57 | -- (Internal) Clamp a value to a range. 58 | local function clamp(x, min, max) 59 | return x < min and min or (x > max and max or x) 60 | end 61 | 62 | -- (Internal) Return the score of a node. 63 | -- G is the cost from START to this node. 64 | -- H is a heuristic cost, in this case the distance from this node to the goal. 65 | -- Returns F, the sum of G and H. 66 | local function calculateScore(previous, node, goal) 67 | 68 | local G = previous.score + 1 69 | local H = distance(node.x, node.y, goal.x, goal.y) 70 | return G + H, G, H 71 | 72 | end 73 | 74 | -- (Internal) Returns true if the given list contains the specified item. 75 | local function listContains(list, item) 76 | for _, test in ipairs(list) do 77 | if test.x == item.x and test.y == item.y then 78 | return true 79 | end 80 | end 81 | return false 82 | end 83 | 84 | -- (Internal) Returns the item in the given list. 85 | local function listItem(list, item) 86 | for _, test in ipairs(list) do 87 | if test.x == item.x and test.y == item.y then 88 | return test 89 | end 90 | end 91 | end 92 | 93 | -- (Internal) Requests adjacent map values around the given node. 94 | local function getAdjacent(width, height, node, positionIsOpenFunc, includeDiagonals) 95 | 96 | local result = { } 97 | 98 | local positions = { 99 | { x = 0, y = -1 }, -- top 100 | { x = -1, y = 0 }, -- left 101 | { x = 0, y = 1 }, -- bottom 102 | { x = 1, y = 0 }, -- right 103 | } 104 | 105 | if includeDiagonals then 106 | local diagonalMovements = { 107 | { x = -1, y = -1 }, -- top left 108 | { x = 1, y = -1 }, -- top right 109 | { x = -1, y = 1 }, -- bot left 110 | { x = 1, y = 1 }, -- bot right 111 | } 112 | 113 | for _, value in ipairs(diagonalMovements) do 114 | table.insert(positions, value) 115 | end 116 | end 117 | 118 | for _, point in ipairs(positions) do 119 | local px = clamp(node.x + point.x, 1, width) 120 | local py = clamp(node.y + point.y, 1, height) 121 | local value = positionIsOpenFunc( px, py ) 122 | if value then 123 | table.insert( result, { x = px, y = py } ) 124 | end 125 | end 126 | 127 | return result 128 | 129 | end 130 | 131 | -- Returns the path from start to goal, or false if no path exists. 132 | function module:find(width, height, start, goal, positionIsOpenFunc, useCache, excludeDiagonalMoving) 133 | 134 | if useCache then 135 | local cachedPath = getCached(start, goal) 136 | if cachedPath then 137 | return cachedPath 138 | end 139 | end 140 | 141 | local success = false 142 | local open = { } 143 | local closed = { } 144 | 145 | start.score = 0 146 | start.G = 0 147 | start.H = distance(start.x, start.y, goal.x, goal.y) 148 | start.parent = { x = 0, y = 0 } 149 | table.insert(open, start) 150 | 151 | while not success and #open > 0 do 152 | 153 | -- sort by score: high to low 154 | table.sort(open, function(a, b) return a.score > b.score end) 155 | 156 | local current = table.remove(open) 157 | 158 | table.insert(closed, current) 159 | 160 | success = listContains(closed, goal) 161 | 162 | if not success then 163 | 164 | local adjacentList = getAdjacent(width, height, current, positionIsOpenFunc, not excludeDiagonalMoving) 165 | 166 | for _, adjacent in ipairs(adjacentList) do 167 | 168 | if not listContains(closed, adjacent) then 169 | 170 | if not listContains(open, adjacent) then 171 | 172 | adjacent.score = calculateScore(current, adjacent, goal) 173 | adjacent.parent = current 174 | table.insert(open, adjacent) 175 | 176 | end 177 | 178 | end 179 | 180 | end 181 | 182 | end 183 | 184 | end 185 | 186 | if not success then 187 | return false 188 | end 189 | 190 | -- traverse the parents from the last point to get the path 191 | local node = listItem(closed, closed[#closed]) 192 | local path = { } 193 | 194 | while node do 195 | 196 | table.insert(path, 1, { x = node.x, y = node.y } ) 197 | node = listItem(closed, node.parent) 198 | 199 | end 200 | 201 | saveCached(start, goal, path) 202 | 203 | -- reverse the closed list to get the solution 204 | return path 205 | 206 | end 207 | 208 | return module 209 | -------------------------------------------------------------------------------- /tests/lua-star_spec.lua: -------------------------------------------------------------------------------- 1 | describe("Lua star", function() 2 | 3 | -- start is always top left (1,1) 4 | -- goal is always bottom right (10, 10) 5 | local start = { x = 1, y = 1 } 6 | local goal = { x = 10, y = 10 } 7 | local map = nil 8 | 9 | -- define some test maps (10 x 10) 10 | local mapsize = 10 11 | local openmap = [[ 12 | 0000000000 13 | 0000000000 14 | 0000000000 15 | 0000000000 16 | 0000000000 17 | 0000000000 18 | 0000000000 19 | 0000000000 20 | 0000000000 21 | 0000000000 22 | ]] 23 | 24 | local openmapSolution = { 25 | { x = 1, y = 1 }, 26 | { x = 2, y = 2 }, 27 | { x = 3, y = 3 }, 28 | { x = 4, y = 4 }, 29 | { x = 5, y = 5 }, 30 | { x = 6, y = 6 }, 31 | { x = 7, y = 7 }, 32 | { x = 8, y = 8 }, 33 | { x = 9, y = 9 }, 34 | { x = 10, y = 10 }, 35 | } 36 | 37 | local simplemap = [[ 38 | 0000000000 39 | 0000000110 40 | 0000001110 41 | 0000011100 42 | 0000111000 43 | 0001110000 44 | 0011100000 45 | 0111000000 46 | 0000000000 47 | 0000000000 48 | ]] 49 | 50 | local simplemapSolution = { 51 | { x = 1, y = 1 }, 52 | { x = 2, y = 2 }, 53 | { x = 3, y = 3 }, 54 | { x = 4, y = 4 }, 55 | { x = 4, y = 5 }, 56 | { x = 3, y = 6 }, 57 | { x = 2, y = 7 }, 58 | { x = 1, y = 8 }, 59 | { x = 2, y = 9 }, 60 | { x = 3, y = 10 }, 61 | { x = 4, y = 10 }, 62 | { x = 5, y = 10 }, 63 | { x = 6, y = 10 }, 64 | { x = 7, y = 10 }, 65 | { x = 8, y = 10 }, 66 | { x = 9, y = 10 }, 67 | { x = 10, y = 10 }, 68 | } 69 | 70 | local simplemapDiagonalSolution = { 71 | { x = 1, y = 1 }, 72 | { x = 1, y = 2 }, 73 | { x = 1, y = 3 }, 74 | { x = 1, y = 4 }, 75 | { x = 1, y = 5 }, 76 | { x = 1, y = 6 }, 77 | { x = 1, y = 7 }, 78 | { x = 1, y = 8 }, 79 | { x = 1, y = 9 }, 80 | { x = 2, y = 9 }, 81 | { x = 3, y = 9 }, 82 | { x = 4, y = 9 }, 83 | { x = 5, y = 9 }, 84 | { x = 6, y = 9 }, 85 | { x = 7, y = 9 }, 86 | { x = 8, y = 9 }, 87 | { x = 9, y = 9 }, 88 | { x = 9, y = 10 }, 89 | { x = 10, y = 10 }, 90 | } 91 | 92 | local complexmap = [[ 93 | 0000000000 94 | 1111111110 95 | 0000000000 96 | 0111111111 97 | 0100110000 98 | 0101010100 99 | 0001010110 100 | 1111011010 101 | 0000000010 102 | 0000000010 103 | ]] 104 | 105 | local complexmapSolution = { 106 | { x = 1, y = 1 }, 107 | { x = 2, y = 1 }, 108 | { x = 3, y = 1 }, 109 | { x = 4, y = 1 }, 110 | { x = 5, y = 1 }, 111 | { x = 6, y = 1 }, 112 | { x = 7, y = 1 }, 113 | { x = 8, y = 1 }, 114 | { x = 9, y = 1 }, 115 | { x = 10, y = 2 }, 116 | { x = 9, y = 3 }, 117 | { x = 8, y = 3 }, 118 | { x = 7, y = 3 }, 119 | { x = 6, y = 3 }, 120 | { x = 5, y = 3 }, 121 | { x = 4, y = 3 }, 122 | { x = 3, y = 3 }, 123 | { x = 2, y = 3 }, 124 | { x = 1, y = 4 }, 125 | { x = 1, y = 5 }, 126 | { x = 1, y = 6 }, 127 | { x = 2, y = 7 }, 128 | { x = 3, y = 6 }, 129 | { x = 4, y = 5 }, 130 | { x = 5, y = 6 }, 131 | { x = 5, y = 7 }, 132 | { x = 5, y = 8 }, 133 | { x = 6, y = 9 }, 134 | { x = 7, y = 9 }, 135 | { x = 8, y = 8 }, 136 | { x = 7, y = 7 }, 137 | { x = 7, y = 6 }, 138 | { x = 8, y = 5 }, 139 | { x = 9, y = 6 }, 140 | { x = 10, y = 7 }, 141 | { x = 10, y = 8 }, 142 | { x = 10, y = 9 }, 143 | { x = 10, y = 10 }, 144 | } 145 | 146 | local unsolvablemap = [[ 147 | 0000000000 148 | 0000000000 149 | 0000000000 150 | 0000000000 151 | 1111111111 152 | 0000000000 153 | 0000000000 154 | 0000000000 155 | 0000000000 156 | 0000000000 157 | ]] 158 | 159 | -- convert a string map into a table 160 | local function makemap(template) 161 | map = { } 162 | template:gsub(".", function(c) 163 | if c == "0" or c == "1" then 164 | table.insert(map, c) 165 | end 166 | end) 167 | end 168 | 169 | -- get the value at position xy on a map 170 | local function mapTileIsOpen(x, y) 171 | return map[ ((y-1) * 10) + x ] == "0" 172 | end 173 | 174 | local function printSolution(path) 175 | print(#path, "points") 176 | for i, v in ipairs(path) do 177 | print(string.format("{ x = %d, y = %d },", v.x, v.y)) 178 | end 179 | for h=1, mapsize do 180 | for w=1, mapsize do 181 | local walked = false 182 | for _, p in ipairs(path) do 183 | if p.x == w and p.y == h then 184 | walked = true 185 | end 186 | end 187 | if walked then 188 | io.write(".") 189 | else 190 | io.write("#") 191 | end 192 | end 193 | io.write("\n") 194 | end 195 | end 196 | 197 | -- begin tests 198 | 199 | it("find a path with no obstacles", function() 200 | 201 | local luastar = require("lua-star") 202 | makemap(openmap) 203 | local path = luastar:find(mapsize, mapsize, start, goal, mapTileIsOpen) 204 | --printSolution(path) 205 | assert.are.equal(10, #path) 206 | assert.are.same(openmapSolution, path) 207 | 208 | end) 209 | 210 | it("find a path on a simple map", function() 211 | 212 | local luastar = require("lua-star") 213 | makemap(simplemap) 214 | local path = luastar:find(mapsize, mapsize, start, goal, mapTileIsOpen) 215 | --printSolution(path) 216 | assert.are.equal(17, #path) 217 | assert.are.same(simplemapSolution, path) 218 | 219 | end) 220 | 221 | it("find a path on a simple map without diagonam movement", function () 222 | 223 | local luastar = require("lua-star") 224 | local excludeDiagonals = true 225 | makemap(simplemap) 226 | local path = luastar:find(mapsize, mapsize, start, goal, mapTileIsOpen, false, excludeDiagonals) 227 | --printSolution(path) 228 | assert.are.equal(19, #path) 229 | assert.are.same(simplemapDiagonalSolution, path) 230 | 231 | end) 232 | 233 | it("find a path on a complex map", function() 234 | 235 | local luastar = require("lua-star") 236 | makemap(complexmap) 237 | local path = luastar:find(mapsize, mapsize, start, goal, mapTileIsOpen) 238 | --printSolution(path) 239 | assert.are.equal(38, #path) 240 | assert.are.same(complexmapSolution, path) 241 | 242 | end) 243 | 244 | it("find no path", function() 245 | 246 | local luastar = require("lua-star") 247 | makemap(unsolvablemap) 248 | local path = luastar:find(mapsize, mapsize, start, goal, mapTileIsOpen) 249 | assert.is_false(path) 250 | 251 | end) 252 | 253 | it("find no diagonal path", function() 254 | 255 | local luastar = require("lua-star") 256 | local excludeDiagonals = true 257 | makemap(unsolvablemap) 258 | local path = luastar:find(mapsize, mapsize, start, goal, mapTileIsOpen, false, excludeDiagonals) 259 | assert.is_false(path) 260 | 261 | end) 262 | 263 | it("find no diagonal path on a complex map", function() 264 | 265 | local luastar = require("lua-star") 266 | local excludeDiagonals = true 267 | makemap(complexmap) 268 | local path = luastar:find(mapsize, mapsize, start, goal, mapTileIsOpen, false, excludeDiagonals) 269 | assert.is_false(path) 270 | 271 | end) 272 | 273 | it("does not cache paths by default", function() 274 | 275 | local luastar = require("lua-star") 276 | makemap(openmap) 277 | local path = luastar:find(mapsize, mapsize, start, goal, mapTileIsOpen) 278 | local samepath = luastar:find(mapsize, mapsize, start, goal, mapTileIsOpen) 279 | assert.is_not.equal(path, samepath) 280 | 281 | end) 282 | 283 | it("caches paths", function() 284 | 285 | local luastar = require("lua-star") 286 | makemap(openmap) 287 | local path = luastar:find(mapsize, mapsize, start, goal, mapTileIsOpen, true) 288 | local samepath = luastar:find(mapsize, mapsize, start, goal, mapTileIsOpen, true) 289 | assert.are.equal(path, samepath) 290 | 291 | end) 292 | 293 | it("clears cached paths", function() 294 | 295 | local luastar = require("lua-star") 296 | makemap(openmap) 297 | local path = luastar:find(mapsize, mapsize, start, goal, mapTileIsOpen, true) 298 | luastar:clearCached() 299 | assert.is_nil(luastar.cache) 300 | 301 | end) 302 | 303 | end) 304 | 305 | --------------------------------------------------------------------------------