diff --git a/sys/apis/event.lua b/sys/apis/event.lua index 26f316c..9e8d90b 100644 --- a/sys/apis/event.lua +++ b/sys/apis/event.lua @@ -24,9 +24,9 @@ function Routine:terminate() end function Routine:resume(event, ...) - if coroutine.status(self.co) == 'running' then - return - end + --if coroutine.status(self.co) == 'running' then + --return + --end if not self.co then error('Cannot resume a dead routine') diff --git a/sys/apis/jumper/core/assert.lua b/sys/apis/jumper/core/assert.lua deleted file mode 100644 index dc5430f..0000000 --- a/sys/apis/jumper/core/assert.lua +++ /dev/null @@ -1,105 +0,0 @@ --- Various assertion function for API methods argument-checking - -if (...) then - - -- Dependancies - local _PATH = (...):gsub('%.core.assert$','') - local Utils = require (_PATH .. '.core.utils') - - -- Local references - local lua_type = type - local floor = math.floor - local concat = table.concat - local next = next - local pairs = pairs - local getmetatable = getmetatable - - -- Is I an integer ? - local function isInteger(i) - return lua_type(i) ==('number') and (floor(i)==i) - end - - -- Override lua_type to return integers - local function type(v) - return isInteger(v) and 'int' or lua_type(v) - end - - -- Does the given array contents match a predicate type ? - local function arrayContentsMatch(t,...) - local n_count = Utils.arraySize(t) - if n_count < 1 then return false end - local init_count = t[0] and 0 or 1 - local n_count = (t[0] and n_count-1 or n_count) - local types = {...} - if types then types = concat(types) end - for i=init_count,n_count,1 do - if not t[i] then return false end - if types then - if not types:match(type(t[i])) then return false end - end - end - return true - end - - -- Checks if arg is a valid array map - local function isMap(m) - if not arrayContentsMatch(m, 'table') then return false end - local lsize = Utils.arraySize(m[next(m)]) - for k,v in pairs(m) do - if not arrayContentsMatch(m[k], 'string', 'int') then return false end - if Utils.arraySize(v)~=lsize then return false end - end - return true - end - - -- Checks if s is a valid string map - local function isStringMap(s) - if lua_type(s) ~= 'string' then return false end - local w - for row in s:gmatch('[^\n\r]+') do - if not row then return false end - w = w or #row - if w ~= #row then return false end - end - return true - end - - -- Does instance derive straight from class - local function derives(instance, class) - return getmetatable(instance) == class - end - - -- Does instance inherits from class - local function inherits(instance, class) - return (getmetatable(getmetatable(instance)) == class) - end - - -- Is arg a boolean - local function isBoolean(b) - return (b==true or b==false) - end - - -- Is arg nil ? - local function isNil(n) - return (n==nil) - end - - local function matchType(value, types) - return types:match(type(value)) - end - - return { - arrayContentsMatch = arrayContentsMatch, - derives = derives, - inherits = inherits, - isInteger = isInteger, - isBool = isBoolean, - isMap = isMap, - isStrMap = isStringMap, - isOutOfRange = isOutOfRange, - isNil = isNil, - type = type, - matchType = matchType - } - -end diff --git a/sys/apis/jumper/core/heuristics.lua b/sys/apis/jumper/core/heuristics.lua deleted file mode 100644 index a7c73bc..0000000 --- a/sys/apis/jumper/core/heuristics.lua +++ /dev/null @@ -1,98 +0,0 @@ ---- Heuristic functions for search algorithms. --- A distance heuristic --- provides an *estimate of the optimal distance cost* from a given location to a target. --- As such, it guides the pathfinder to the goal, helping it to decide which route is the best. --- --- This script holds the definition of some built-in heuristics available through jumper. --- --- Distance functions are internally used by the `pathfinder` to evaluate the optimal path --- from the start location to the goal. These functions share the same prototype: --- local function myHeuristic(nodeA, nodeB) --- -- function body --- end --- Jumper features some built-in distance heuristics, namely `MANHATTAN`, `EUCLIDIAN`, `DIAGONAL`, `CARDINTCARD`. --- You can also supply your own heuristic function, following the same template as above. - - -local abs = math.abs -local sqrt = math.sqrt -local sqrt2 = sqrt(2) -local max, min = math.max, math.min - -local Heuristics = {} - --- Manhattan distance. - --
This heuristic is the default one being used by the `pathfinder` object. - --
Evaluates as distance = |dx|+|dy| - -- @class function - -- @tparam node nodeA a node - -- @tparam node nodeB another node - -- @treturn number the distance from __nodeA__ to __nodeB__ - -- @usage - -- -- First method - -- pathfinder:setHeuristic('MANHATTAN') - -- -- Second method - -- local Distance = require ('jumper.core.heuristics') - -- pathfinder:setHeuristic(Distance.MANHATTAN) - function Heuristics.MANHATTAN(nodeA, nodeB) - local dx = abs(nodeA._x - nodeB._x) - local dy = abs(nodeA._y - nodeB._y) - local dz = abs(nodeA._z - nodeB._z) - return (dx + dy + dz) - end - - --- Euclidian distance. - --
Evaluates as distance = squareRoot(dx*dx+dy*dy) - -- @class function - -- @tparam node nodeA a node - -- @tparam node nodeB another node - -- @treturn number the distance from __nodeA__ to __nodeB__ - -- @usage - -- -- First method - -- pathfinder:setHeuristic('EUCLIDIAN') - -- -- Second method - -- local Distance = require ('jumper.core.heuristics') - -- pathfinder:setHeuristic(Distance.EUCLIDIAN) - function Heuristics.EUCLIDIAN(nodeA, nodeB) - local dx = nodeA._x - nodeB._x - local dy = nodeA._y - nodeB._y - local dz = nodeA._z - nodeB._z - return sqrt(dx*dx+dy*dy+dz*dz) - end - - --- Diagonal distance. - --
Evaluates as distance = max(|dx|, abs|dy|) - -- @class function - -- @tparam node nodeA a node - -- @tparam node nodeB another node - -- @treturn number the distance from __nodeA__ to __nodeB__ - -- @usage - -- -- First method - -- pathfinder:setHeuristic('DIAGONAL') - -- -- Second method - -- local Distance = require ('jumper.core.heuristics') - -- pathfinder:setHeuristic(Distance.DIAGONAL) - function Heuristics.DIAGONAL(nodeA, nodeB) - local dx = abs(nodeA._x - nodeB._x) - local dy = abs(nodeA._y - nodeB._y) - return max(dx,dy) - end - - --- Cardinal/Intercardinal distance. - --
Evaluates as distance = min(dx, dy)*squareRoot(2) + max(dx, dy) - min(dx, dy) - -- @class function - -- @tparam node nodeA a node - -- @tparam node nodeB another node - -- @treturn number the distance from __nodeA__ to __nodeB__ - -- @usage - -- -- First method - -- pathfinder:setHeuristic('CARDINTCARD') - -- -- Second method - -- local Distance = require ('jumper.core.heuristics') - -- pathfinder:setHeuristic(Distance.CARDINTCARD) - function Heuristics.CARDINTCARD(nodeA, nodeB) - local dx = abs(nodeA._x - nodeB._x) - local dy = abs(nodeA._y - nodeB._y) - return min(dx,dy) * sqrt2 + max(dx,dy) - min(dx,dy) - end - -return Heuristics \ No newline at end of file diff --git a/sys/apis/jumper/core/lookuptable.lua b/sys/apis/jumper/core/lookuptable.lua deleted file mode 100644 index 74cf997..0000000 --- a/sys/apis/jumper/core/lookuptable.lua +++ /dev/null @@ -1,32 +0,0 @@ -local addNode(self, node, nextNode, ed) - if not self._pathDB[node] then self._pathDB[node] = {} end - self._pathDB[node][ed] = (nextNode == ed and node or nextNode) -end - --- Path lookupTable -local lookupTable = {} -lookupTable.__index = lookupTable - -function lookupTable:new() - local lut = {_pathDB = {}} - return setmetatable(lut, lookupTable) -end - -function lookupTable:addPath(path) - local st, ed = path._nodes[1], path._nodes[#path._nodes] - for node, count in path:nodes() do - local nextNode = path._nodes[count+1] - if nextNode then addNode(self, node, nextNode, ed) end - end -end - -function lookupTable:hasPath(nodeA, nodeB) - local found - found = self._pathDB[nodeA] and self._path[nodeA][nodeB] - if found then return true, true end - found = self._pathDB[nodeB] and self._path[nodeB][nodeA] - if found then return true, false end - return false -end - -return lookupTable \ No newline at end of file diff --git a/sys/apis/jumper/core/node.lua b/sys/apis/jumper/core/node.lua index 733978c..b0b9284 100644 --- a/sys/apis/jumper/core/node.lua +++ b/sys/apis/jumper/core/node.lua @@ -4,14 +4,12 @@ -- and then cached within the `grid`. -- -- In the following implementation, nodes can be compared using the `<` operator. The comparison is --- made with regards of their `f` cost. From a given node being examined, the `pathfinder` will expand the search +-- made with regards of their `f` cost. From a given node being examined, the `pathfinder` will expand the search -- to the next neighbouring node having the lowest `f` cost. See `core.bheap` for more details. --- +-- if (...) then - local assert = assert - --- The `Node` class.
-- This class is callable. -- Therefore,_ Node(...) _acts as a shortcut to_ Node:new(...). @@ -26,7 +24,7 @@ if (...) then -- @treturn node a new `node` -- @usage local node = Node(3,4) function Node:new(x,y,z) - return setmetatable({_x = x, _y = y, _z = z, _clearance = {}}, Node) + return setmetatable({_x = x, _y = y, _z = z }, Node) end -- Enables the use of operator '<' to compare nodes. @@ -36,48 +34,24 @@ if (...) then --- Returns x-coordinate of a `node` -- @class function -- @treturn number the x-coordinate of the `node` - -- @usage local x = node:getX() + -- @usage local x = node:getX() function Node:getX() return self._x end - + --- Returns y-coordinate of a `node` -- @class function - -- @treturn number the y-coordinate of the `node` - -- @usage local y = node:getY() + -- @treturn number the y-coordinate of the `node` + -- @usage local y = node:getY() function Node:getY() return self._y end function Node:getZ() return self._z end - + --- Returns x and y coordinates of a `node` -- @class function -- @treturn number the x-coordinate of the `node` -- @treturn number the y-coordinate of the `node` - -- @usage local x, y = node:getPos() + -- @usage local x, y = node:getPos() function Node:getPos() return self._x, self._y, self._z end - - --- Returns the amount of true [clearance](http://aigamedev.com/open/tutorial/clearance-based-pathfinding/#TheTrueClearanceMetric) - -- for a given `node` - -- @class function - -- @tparam string|int|func walkable the value for walkable locations in the collision map array. - -- @treturn int the clearance of the `node` - -- @usage - -- -- Assuming walkable was 0 - -- local clearance = node:getClearance(0) - function Node:getClearance(walkable) - return self._clearance[walkable] - end - - --- Removes the clearance value for a given walkable. - -- @class function - -- @tparam string|int|func walkable the value for walkable locations in the collision map array. - -- @treturn node self (the calling `node` itself, can be chained) - -- @usage - -- -- Assuming walkable is defined - -- node:removeClearance(walkable) - function Node:removeClearance(walkable) - self._clearance[walkable] = nil - return self - end - + --- Clears temporary cached attributes of a `node`. -- Deletes the attributes cached within a given node after a pathfinding call. -- This function is internally used by the search algorithms, so you should not use it explicitely. @@ -91,10 +65,10 @@ if (...) then self._opened, self._closed, self._parent = nil, nil, nil return self end - + return setmetatable(Node, - {__call = function(self,...) - return Node:new(...) + {__call = function(_,...) + return Node:new(...) end} ) end \ No newline at end of file diff --git a/sys/apis/jumper/core/path.lua b/sys/apis/jumper/core/path.lua index 826c5e4..e3bb98e 100644 --- a/sys/apis/jumper/core/path.lua +++ b/sys/apis/jumper/core/path.lua @@ -6,17 +6,7 @@ -- It should normally not be used explicitely, yet it remains fully accessible. -- - if (...) then - - -- Dependencies - local _PATH = (...):match('(.+)%.path$') - local Heuristic = require (_PATH .. '.heuristics') - - -- Local references - local abs, max = math.abs, math.max - local t_insert, t_remove = table.insert, table.remove - --- The `Path` class.
-- This class is callable. -- Therefore, Path(...) acts as a shortcut to Path:new(...). @@ -42,8 +32,8 @@ if (...) then -- for node, count in p:iter() do -- ... -- end - function Path:iter() - local i,pathLen = 1,#self._nodes + function Path:nodes() + local i = 1 return function() if self._nodes[i] then i = i+1 @@ -51,150 +41,9 @@ if (...) then end end end - - --- Iterates on each single `node` along a `path`. At each step of iteration, - -- returns a `node` plus a count value. Alias for @{Path:iter} - -- @class function - -- @name Path:nodes - -- @treturn node a `node` - -- @treturn int the count for the number of nodes - -- @see Path:iter - -- @usage - -- for node, count in p:nodes() do - -- ... - -- end - Path.nodes = Path.iter - - --- Evaluates the `path` length - -- @class function - -- @treturn number the `path` length - -- @usage local len = p:getLength() - function Path:getLength() - local len = 0 - for i = 2,#self._nodes do - len = len + Heuristic.EUCLIDIAN(self._nodes[i], self._nodes[i-1]) - end - return len - end - - --- Counts the number of steps. - -- Returns the number of waypoints (nodes) in the current path. - -- @class function - -- @tparam node node a node to be added to the path - -- @tparam[opt] int index the index at which the node will be inserted. If omitted, the node will be appended after the last node in the path. - -- @treturn path self (the calling `path` itself, can be chained) - -- @usage local nSteps = p:countSteps() - function Path:addNode(node, index) - index = index or #self._nodes+1 - t_insert(self._nodes, index, node) - return self - end - - - --- `Path` filling modifier. Interpolates between non contiguous nodes along a `path` - -- to build a fully continuous `path`. This maybe useful when using search algorithms such as Jump Point Search. - -- Does the opposite of @{Path:filter} - -- @class function - -- @treturn path self (the calling `path` itself, can be chained) - -- @see Path:filter - -- @usage p:fill() - function Path:fill() - local i = 2 - local xi,yi,dx,dy - local N = #self._nodes - local incrX, incrY - while true do - xi,yi = self._nodes[i]._x,self._nodes[i]._y - dx,dy = xi-self._nodes[i-1]._x,yi-self._nodes[i-1]._y - if (abs(dx) > 1 or abs(dy) > 1) then - incrX = dx/max(abs(dx),1) - incrY = dy/max(abs(dy),1) - t_insert(self._nodes, i, self._grid:getNodeAt(self._nodes[i-1]._x + incrX, self._nodes[i-1]._y +incrY)) - N = N+1 - else i=i+1 - end - if i>N then break end - end - return self - end - --- `Path` compression modifier. Given a `path`, eliminates useless nodes to return a lighter `path` - -- consisting of straight moves. Does the opposite of @{Path:fill} - -- @class function - -- @treturn path self (the calling `path` itself, can be chained) - -- @see Path:fill - -- @usage p:filter() - function Path:filter() - local i = 2 - local xi,yi,dx,dy, olddx, olddy - xi,yi = self._nodes[i]._x, self._nodes[i]._y - dx, dy = xi - self._nodes[i-1]._x, yi-self._nodes[i-1]._y - while true do - olddx, olddy = dx, dy - if self._nodes[i+1] then - i = i+1 - xi, yi = self._nodes[i]._x, self._nodes[i]._y - dx, dy = xi - self._nodes[i-1]._x, yi - self._nodes[i-1]._y - if olddx == dx and olddy == dy then - t_remove(self._nodes, i-1) - i = i - 1 - end - else break end - end - return self - end - - --- Clones a `path`. - -- @class function - -- @treturn path a `path` - -- @usage local p = path:clone() - function Path:clone() - local p = Path:new() - for node in self:nodes() do p:addNode(node) end - return p - end - - --- Checks if a `path` is equal to another. It also supports *filtered paths* (see @{Path:filter}). - -- @class function - -- @tparam path p2 a path - -- @treturn boolean a boolean - -- @usage print(myPath:isEqualTo(anotherPath)) - function Path:isEqualTo(p2) - local p1 = self:clone():filter() - local p2 = p2:clone():filter() - for node, count in p1:nodes() do - if not p2._nodes[count] then return false end - local n = p2._nodes[count] - if n._x~=node._x or n._y~=node._y then return false end - end - return true - end - - --- Reverses a `path`. - -- @class function - -- @treturn path self (the calling `path` itself, can be chained) - -- @usage myPath:reverse() - function Path:reverse() - local _nodes = {} - for i = #self._nodes,1,-1 do - _nodes[#_nodes+1] = self._nodes[i] - end - self._nodes = _nodes - return self - end - - --- Appends a given `path` to self. - -- @class function - -- @tparam path p a path - -- @treturn path self (the calling `path` itself, can be chained) - -- @usage myPath:append(anotherPath) - function Path:append(p) - for node in p:nodes() do self:addNode(node) end - return self - end - return setmetatable(Path, - {__call = function(self,...) + {__call = function(_,...) return Path:new(...) end }) diff --git a/sys/apis/jumper/core/utils.lua b/sys/apis/jumper/core/utils.lua index 11ef901..1ebf876 100644 --- a/sys/apis/jumper/core/utils.lua +++ b/sys/apis/jumper/core/utils.lua @@ -5,125 +5,20 @@ if (...) then -- Dependencies local _PATH = (...):gsub('%.utils$','') local Path = require (_PATH .. '.path') - local Node = require (_PATH .. '.node') -- Local references local pairs = pairs - local type = type local t_insert = table.insert - local assert = assert - local coroutine = coroutine -- Raw array items count local function arraySize(t) local count = 0 - for k,v in pairs(t) do + for _ in pairs(t) do count = count+1 end return count end - -- Parses a string map and builds an array map - local function stringMapToArray(str) - local map = {} - local w, h - for line in str:gmatch('[^\n\r]+') do - if line then - w = not w and #line or w - assert(#line == w, 'Error parsing map, rows must have the same size!') - h = (h or 0) + 1 - map[h] = {} - for char in line:gmatch('.') do - map[h][#map[h]+1] = char - end - end - end - return map - end - - -- Collects and returns the keys of a given array - local function getKeys(t) - local keys = {} - for k,v in pairs(t) do keys[#keys+1] = k end - return keys - end - - -- Calculates the bounds of a 2d array - local function getArrayBounds(map) - local min_x, max_x - local min_y, max_y - for y in pairs(map) do - min_y = not min_y and y or (ymax_y and y or max_y) - for x in pairs(map[y]) do - min_x = not min_x and x or (xmax_x and x or max_x) - end - end - return min_x,max_x,min_y,max_y - end - - -- Converts an array to a set of nodes - local function arrayToNodes(map) - local min_x, max_x - local min_y, max_y - local min_z, max_z - local nodes = {} - for y in pairs(map) do - min_y = not min_y and y or (ymax_y and y or max_y) - nodes[y] = {} - for x in pairs(map[y]) do - min_x = not min_x and x or (xmax_x and x or max_x) - nodes[y][x] = {} - for z in pairs(map[y][x]) do - min_z = not min_z and z or (zmax_z and z or max_z) - nodes[y][x][z] = Node:new(x,y,z) - end - end - end - return nodes, - (min_x or 0), (max_x or 0), - (min_y or 0), (max_y or 0), - (min_z or 0), (max_z or 0) - end - - -- Iterator, wrapped within a coroutine - -- Iterates around a given position following the outline of a square - local function around() - local iterf = function(x0, y0, z0, s) - local x, y, z = x0-s, y0-s, z0-s - coroutine.yield(x, y, z) - repeat - x = x + 1 - coroutine.yield(x,y,z) - until x == x0+s - repeat - y = y + 1 - coroutine.yield(x,y,z) - until y == y0 + s - repeat - z = z + 1 - coroutine.yield(x,y,z) - until z == z0 + s - repeat - x = x - 1 - coroutine.yield(x, y,z) - until x == x0-s - repeat - y = y - 1 - coroutine.yield(x,y,z) - until y == y0-s+1 - repeat - z = z - 1 - coroutine.yield(x,y,z) - until z == z0-s+1 - end - return coroutine.create(iterf) - end - -- Extract a path from a given start/end position local function traceBackPath(finder, node, startNode) local path = Path:new() @@ -151,17 +46,11 @@ if (...) then local function outOfRange(i,low,up) return (i< low or i > up) end - + return { arraySize = arraySize, - getKeys = getKeys, indexOf = indexOf, outOfRange = outOfRange, - getArrayBounds = getArrayBounds, - arrayToNodes = arrayToNodes, - strToMap = stringMapToArray, - around = around, - drAround = drAround, traceBackPath = traceBackPath } diff --git a/sys/apis/jumper/grid.lua b/sys/apis/jumper/grid.lua index b2cd20f..36f28af 100644 --- a/sys/apis/jumper/grid.lua +++ b/sys/apis/jumper/grid.lua @@ -2,7 +2,8 @@ -- Implementation of the `grid` class. -- The `grid` is a implicit graph which represents the 2D -- world map layout on which the `pathfinder` object will run. --- During a search, the `pathfinder` object needs to save some critical values. These values are cached within each `node` +-- During a search, the `pathfinder` object needs to save some critical values. +-- These values are cached within each `node` -- object, and the whole set of nodes are tight inside the `grid` object itself. if (...) then @@ -12,16 +13,10 @@ if (...) then -- Local references local Utils = require (_PATH .. '.core.utils') - local Assert = require (_PATH .. '.core.assert') local Node = require (_PATH .. '.core.node') -- Local references - local pairs = pairs - local assert = assert - local next = next local setmetatable = setmetatable - local floor = math.floor - local coroutine = coroutine -- Offsets for straights moves local straightOffsets = { @@ -30,12 +25,6 @@ if (...) then {x = 0, y = 0, z = 1} --[[U]], {x = 0, y = -0, z = -1}, --[[D]] } - -- Offsets for diagonal moves - local diagonalOffsets = { - {x = -1, y = -1} --[[NW]], {x = 1, y = -1}, --[[NE]] - {x = -1, y = 1} --[[SW]], {x = 1, y = 1}, --[[SE]] - } - --- The `Grid` class.
-- This class is callable. -- Therefore,_ Grid(...) _acts as a shortcut to_ Grid:new(...). @@ -43,46 +32,21 @@ if (...) then local Grid = {} Grid.__index = Grid - -- Specialized grids - local PreProcessGrid = setmetatable({},Grid) - local PostProcessGrid = setmetatable({},Grid) - PreProcessGrid.__index = PreProcessGrid - PostProcessGrid.__index = PostProcessGrid - PreProcessGrid.__call = function (self,x,y,z) - return self:getNodeAt(x,y,z) - end - PostProcessGrid.__call = function (self,x,y,z,create) - if create then return self:getNodeAt(x,y,z) end - return self._nodes[y] and self._nodes[y][x] and self._nodes[y][x][z] - end - --- Inits a new `grid` -- @class function - -- @tparam table|string map A collision map - (2D array) with consecutive indices (starting at 0 or 1) + -- @tparam table Map dimensions -- or a `string` with line-break chars (\n or \r) as row delimiters. - -- @tparam[opt] bool cacheNodeAtRuntime When __true__, returns an empty `grid` instance, so that - -- later on, indexing a non-cached `node` will cause it to be created and cache within the `grid` on purpose (i.e, when needed). - -- This is a __memory-safe__ option, in case your dealing with some tight memory constraints. - -- Defaults to __false__ when omitted. -- @treturn grid a new `grid` instance - -- @usage - -- -- A simple 3x3 grid - -- local myGrid = Grid:new({{0,0,0},{0,0,0},{0,0,0}}) - -- - -- -- A memory-safe 3x3 grid - -- myGrid = Grid('000\n000\n000', true) - function Grid:new(map, cacheNodeAtRuntime) - if type(map) == 'string' then - assert(Assert.isStrMap(map), 'Wrong argument #1. Not a valid string map') - map = Utils.strToMap(map) - end - --assert(Assert.isMap(map),('Bad argument #1. Not a valid map')) - assert(Assert.isBool(cacheNodeAtRuntime) or Assert.isNil(cacheNodeAtRuntime), - ('Bad argument #2. Expected \'boolean\', got %s.'):format(type(cacheNodeAtRuntime))) - if cacheNodeAtRuntime then - return PostProcessGrid:new(map,walkable) - end - return PreProcessGrid:new(map,walkable) + function Grid:new(dim) + local newGrid = { } + newGrid._min_x, newGrid._max_x = dim.x, dim.ex + newGrid._min_y, newGrid._max_y = dim.y, dim.ey + newGrid._min_z, newGrid._max_z = dim.z, dim.ez + newGrid._nodes = { } + newGrid._width = (newGrid._max_x-newGrid._min_x)+1 + newGrid._height = (newGrid._max_y-newGrid._min_y)+1 + newGrid._length = (newGrid._max_z-newGrid._min_z)+1 + return setmetatable(newGrid,Grid) end --- Checks if `node` at [x,y] is __walkable__. @@ -90,41 +54,11 @@ if (...) then -- @class function -- @tparam int x the x-location of the node -- @tparam int y the y-location of the node - -- @tparam[opt] string|int|func walkable the value for walkable locations in the collision map array (see @{Grid:new}). - -- Defaults to __false__ when omitted. - -- If this parameter is a function, it should be prototyped as __f(value)__ and return a `boolean`: - -- __true__ when value matches a __walkable__ `node`, __false__ otherwise. If this parameter is not given - -- while location [x,y] __is valid__, this actual function returns __true__. - -- @tparam[optchain] int clearance the amount of clearance needed. Defaults to 1 (normal clearance) when not given. - -- @treturn bool __true__ if `node` exists and is __walkable__, __false__ otherwise - -- @usage - -- -- Always true - -- print(myGrid:isWalkableAt(2,3)) + -- @tparam int z the z-location of the node -- - -- -- True if node at [2,3] collision map value is 0 - -- print(myGrid:isWalkableAt(2,3,0)) - -- - -- -- True if node at [2,3] collision map value is 0 and has a clearance higher or equal to 2 - -- print(myGrid:isWalkableAt(2,3,0,2)) - -- - function Grid:isWalkableAt(x, y, z, walkable, clearance) - local nodeValue = self._map[y] and self._map[y][x] and self._map[y][x][z] - if nodeValue then - if not walkable then return true end - else - return false - end - local hasEnoughClearance = not clearance and true or false - if not hasEnoughClearance then - if not self._isAnnotated[walkable] then return false end - local node = self:getNodeAt(x,y,z) - local nodeClearance = node:getClearance(walkable) - hasEnoughClearance = (nodeClearance >= clearance) - end - if self._eval then - return walkable(nodeValue) and hasEnoughClearance - end - return ((nodeValue == walkable) and hasEnoughClearance) + function Grid:isWalkableAt(x, y, z) + local node = self:getNodeAt(x,y,z) + return node and node.walkable ~= 1 end --- Returns the `grid` width. @@ -143,14 +77,6 @@ if (...) then return self._height end - --- Returns the collision map. - -- @class function - -- @treturn map the collision map (see @{Grid:new}) - -- @usage local map = myGrid:getMap() - function Grid:getMap() - return self._map - end - --- Returns the set of nodes. -- @class function -- @treturn {{node,...},...} an array of nodes @@ -174,18 +100,14 @@ if (...) then --- Returns neighbours. The returned value is an array of __walkable__ nodes neighbouring a given `node`. -- @class function -- @tparam node node a given `node` - -- @tparam[opt] string|int|func walkable the value for walkable locations in the collision map array (see @{Grid:new}). - -- Defaults to __false__ when omitted. - -- @tparam[optchain] bool allowDiagonal when __true__, allows adjacent nodes are included (8-neighbours). - -- Defaults to __false__ when omitted. - -- @tparam[optchain] bool tunnel When __true__, allows the `pathfinder` to tunnel through walls when heading diagonally. - -- @tparam[optchain] int clearance When given, will prune for the neighbours set all nodes having a clearance value lower than the passed-in value + -- @tparam[opt] string|int|func walkable the value for walkable locations + -- in the collision map array (see @{Grid:new}). -- Defaults to __false__ when omitted. -- @treturn {node,...} an array of nodes neighbouring a given node -- @usage -- local aNode = myGrid:getNodeAt(5,6) -- local neighbours = myGrid:getNeighbours(aNode, 0, true) - function Grid:getNeighbours(node, walkable, allowDiagonal, tunnel, clearance) + function Grid:getNeighbours(node) local neighbours = {} for i = 1,#straightOffsets do local n = self:getNodeAt( @@ -193,227 +115,31 @@ if (...) then node._y + straightOffsets[i].y, node._z + straightOffsets[i].z ) - if n and self:isWalkableAt(n._x, n._y, n._z, walkable, clearance) then + if n and self:isWalkableAt(n._x, n._y, n._z) then neighbours[#neighbours+1] = n end end - if not allowDiagonal then return neighbours end - - tunnel = not not tunnel - for i = 1,#diagonalOffsets do - local n = self:getNodeAt( - node._x + diagonalOffsets[i].x, - node._y + diagonalOffsets[i].y - ) - if n and self:isWalkableAt(n._x, n._y, walkable, clearance) then - if tunnel then - neighbours[#neighbours+1] = n - else - local skipThisNode = false - local n1 = self:getNodeAt(node._x+diagonalOffsets[i].x, node._y) - local n2 = self:getNodeAt(node._x, node._y+diagonalOffsets[i].y) - if ((n1 and n2) and not self:isWalkableAt(n1._x, n1._y, walkable, clearance) and not self:isWalkableAt(n2._x, n2._y, walkable, clearance)) then - skipThisNode = true - end - if not skipThisNode then neighbours[#neighbours+1] = n end - end - end - end - return neighbours end - --- Grid iterator. Iterates on every single node - -- in the `grid`. Passing __lx, ly, ex, ey__ arguments will iterate - -- only on nodes inside the bounding-rectangle delimited by those given coordinates. - -- @class function - -- @tparam[opt] int lx the leftmost x-coordinate of the rectangle. Default to the `grid` leftmost x-coordinate (see @{Grid:getBounds}). - -- @tparam[optchain] int ly the topmost y-coordinate of the rectangle. Default to the `grid` topmost y-coordinate (see @{Grid:getBounds}). - -- @tparam[optchain] int ex the rightmost x-coordinate of the rectangle. Default to the `grid` rightmost x-coordinate (see @{Grid:getBounds}). - -- @tparam[optchain] int ey the bottom-most y-coordinate of the rectangle. Default to the `grid` bottom-most y-coordinate (see @{Grid:getBounds}). - -- @treturn node a `node` on the collision map, upon each iteration step - -- @treturn int the iteration count - -- @usage - -- for node, count in myGrid:iter() do - -- print(node:getX(), node:getY(), count) - -- end - function Grid:iter(lx,ly,lz,ex,ey,ez) - local min_x = lx or self._min_x - local min_y = ly or self._min_y - local min_z = lz or self._min_z - local max_x = ex or self._max_x - local max_y = ey or self._max_y - local max_z = ez or self._max_z - - local x, y, z - z = min_z - return function() - x = not x and min_x or x+1 - if x > max_x then - x = min_x - y = y+1 - end - y = not y and min_y or y+1 - if y > max_y then - y = min_y - z = z+1 - end - if z > max_z then - z = nil - end - return self._nodes[y] and self._nodes[y][x] and self._nodes[y][x][z] or self:getNodeAt(x,y,z) - end - end - - --- Grid iterator. Iterates on each node along the outline (border) of a squared area - -- centered on the given node. - -- @tparam node node a given `node` - -- @tparam[opt] int radius the area radius (half-length). Defaults to __1__ when not given. - -- @treturn node a `node` at each iteration step - -- @usage - -- for node in myGrid:around(node, 2) do - -- ... - -- end - function Grid:around(node, radius) - local x, y, z = node._x, node._y, node._z - radius = radius or 1 - local _around = Utils.around() - local _nodes = {} - repeat - local state, x, y, z = coroutine.resume(_around,x,y,z,radius) - local nodeAt = state and self:getNodeAt(x, y, z) - if nodeAt then _nodes[#_nodes+1] = nodeAt end - until (not state) - local _i = 0 - return function() - _i = _i+1 - return _nodes[_i] - end - end - - --- Each transformation. Calls the given function on each `node` in the `grid`, - -- passing the `node` as the first argument to function __f__. - -- @class function - -- @tparam func f a function prototyped as __f(node,...)__ - -- @tparam[opt] vararg ... args to be passed to function __f__ - -- @treturn grid self (the calling `grid` itself, can be chained) - -- @usage - -- local function printNode(node) - -- print(node:getX(), node:getY()) - -- end - -- myGrid:each(printNode) - function Grid:each(f,...) - for node in self:iter() do f(node,...) end - return self - end - - --- Each (in range) transformation. Calls a function on each `node` in the range of a rectangle of cells, - -- passing the `node` as the first argument to function __f__. - -- @class function - -- @tparam int lx the leftmost x-coordinate coordinate of the rectangle - -- @tparam int ly the topmost y-coordinate of the rectangle - -- @tparam int ex the rightmost x-coordinate of the rectangle - -- @tparam int ey the bottom-most y-coordinate of the rectangle - -- @tparam func f a function prototyped as __f(node,...)__ - -- @tparam[opt] vararg ... args to be passed to function __f__ - -- @treturn grid self (the calling `grid` itself, can be chained) - -- @usage - -- local function printNode(node) - -- print(node:getX(), node:getY()) - -- end - -- myGrid:eachRange(1,1,8,8,printNode) - function Grid:eachRange(lx,ly,ex,ey,f,...) - for node in self:iter(lx,ly,ex,ey) do f(node,...) end - return self - end - - --- Map transformation. - -- Calls function __f(node,...)__ on each `node` in a given range, passing the `node` as the first arg to function __f__ and replaces - -- it with the returned value. Therefore, the function should return a `node`. - -- @class function - -- @tparam func f a function prototyped as __f(node,...)__ - -- @tparam[opt] vararg ... args to be passed to function __f__ - -- @treturn grid self (the calling `grid` itself, can be chained) - -- @usage - -- local function nothing(node) - -- return node - -- end - -- myGrid:imap(nothing) - function Grid:imap(f,...) - for node in self:iter() do - node = f(node,...) - end - return self - end - - --- Map in range transformation. - -- Calls function __f(node,...)__ on each `node` in a rectangle range, passing the `node` as the first argument to the function and replaces - -- it with the returned value. Therefore, the function should return a `node`. - -- @class function - -- @tparam int lx the leftmost x-coordinate coordinate of the rectangle - -- @tparam int ly the topmost y-coordinate of the rectangle - -- @tparam int ex the rightmost x-coordinate of the rectangle - -- @tparam int ey the bottom-most y-coordinate of the rectangle - -- @tparam func f a function prototyped as __f(node,...)__ - -- @tparam[opt] vararg ... args to be passed to function __f__ - -- @treturn grid self (the calling `grid` itself, can be chained) - -- @usage - -- local function nothing(node) - -- return node - -- end - -- myGrid:imap(1,1,6,6,nothing) - function Grid:imapRange(lx,ly,ex,ey,f,...) - for node in self:iter(lx,ly,ex,ey) do - node = f(node,...) - end - return self - end - - -- Specialized grids - -- Inits a preprocessed grid - function PreProcessGrid:new(map) - local newGrid = {} - newGrid._map = map - newGrid._nodes, newGrid._min_x, newGrid._max_x, newGrid._min_y, newGrid._max_y, newGrid._min_z, newGrid._max_z = Utils.arrayToNodes(newGrid._map) - newGrid._width = (newGrid._max_x-newGrid._min_x)+1 - newGrid._height = (newGrid._max_y-newGrid._min_y)+1 - newGrid._length = (newGrid._max_z-newGrid._min_z)+1 - newGrid._isAnnotated = {} - return setmetatable(newGrid,PreProcessGrid) - end - - -- Inits a postprocessed grid - function PostProcessGrid:new(map) - local newGrid = {} - newGrid._map = map - newGrid._nodes = {} - newGrid._min_x, newGrid._max_x, newGrid._min_y, newGrid._max_y = Utils.getArrayBounds(newGrid._map) - newGrid._width = (newGrid._max_x-newGrid._min_x)+1 - newGrid._height = (newGrid._max_y-newGrid._min_y)+1 - newGrid._isAnnotated = {} - return setmetatable(newGrid,PostProcessGrid) - end - - --- Returns the `node` at location [x,y]. + --- Returns the `node` at location [x,y,z]. -- @class function -- @name Grid:getNodeAt -- @tparam int x the x-coordinate coordinate -- @tparam int y the y-coordinate coordinate + -- @tparam int z the z-coordinate coordinate -- @treturn node a `node` -- @usage local aNode = myGrid:getNodeAt(2,2) -- Gets the node at location on a preprocessed grid - function PreProcessGrid:getNodeAt(x,y,z) - return self._nodes[y] and self._nodes[y][x] and self._nodes[y][x][z] or nil - end - - -- Gets the node at location on a postprocessed grid - function PostProcessGrid:getNodeAt(x,y,z) + function Grid:getNodeAt(x,y,z) if not x or not y or not z then return end if Utils.outOfRange(x,self._min_x,self._max_x) then return end if Utils.outOfRange(y,self._min_y,self._max_y) then return end if Utils.outOfRange(z,self._min_z,self._max_z) then return end + + -- inefficient if not self._nodes[y] then self._nodes[y] = {} end if not self._nodes[y][x] then self._nodes[y][x] = {} end if not self._nodes[y][x][z] then self._nodes[y][x][z] = Node:new(x,y,z) end diff --git a/sys/apis/jumper/pathfinder.lua b/sys/apis/jumper/pathfinder.lua index 1d64265..a209afb 100644 --- a/sys/apis/jumper/pathfinder.lua +++ b/sys/apis/jumper/pathfinder.lua @@ -41,53 +41,24 @@ if (...) then -- Dependencies local _PATH = (...):gsub('%.pathfinder$','') local Utils = require (_PATH .. '.core.utils') - local Assert = require (_PATH .. '.core.assert') - local Heap = require (_PATH .. '.core.bheap') - local Heuristic = require (_PATH .. '.core.heuristics') - local Grid = require (_PATH .. '.grid') - local Path = require (_PATH .. '.core.path') -- Internalization - local t_insert, t_remove = table.insert, table.remove - local floor = math.floor local pairs = pairs local assert = assert - local type = type - local setmetatable, getmetatable = setmetatable, getmetatable + local setmetatable = setmetatable --- Finders (search algorithms implemented). Refers to the search algorithms actually implemented in Jumper. - -- --
  • [A*](http://en.wikipedia.org/wiki/A*_search_algorithm)
  • - --
  • [Dijkstra](http://en.wikipedia.org/wiki/Dijkstra%27s_algorithm)
  • - --
  • [Theta Astar](http://aigamedev.com/open/tutorials/theta-star-any-angle-paths/)
  • - --
  • [BFS](http://en.wikipedia.org/wiki/Breadth-first_search)
  • - --
  • [DFS](http://en.wikipedia.org/wiki/Depth-first_search)
  • - --
  • [JPS](http://harablog.wordpress.com/2011/09/07/jump-point-search/)
  • -- @finder Finders -- @see Pathfinder:getFinders local Finders = { ['ASTAR'] = require (_PATH .. '.search.astar'), --- ['DIJKSTRA'] = require (_PATH .. '.search.dijkstra'), --- ['THETASTAR'] = require (_PATH .. '.search.thetastar'), - ['BFS'] = require (_PATH .. '.search.bfs'), --- ['DFS'] = require (_PATH .. '.search.dfs'), --- ['JPS'] = require (_PATH .. '.search.jps') } -- Will keep track of all nodes expanded during the search -- to easily reset their properties for the next pathfinding call local toClear = {} - --- Search modes. Refers to the search modes. In ORTHOGONAL mode, 4-directions are only possible when moving, - -- including North, East, West, South. In DIAGONAL mode, 8-directions are possible when moving, - -- including North, East, West, South and adjacent directions. - -- - --
  • ORTHOGONAL
  • - --
  • DIAGONAL
  • - -- @mode Modes - -- @see Pathfinder:getModes - local searchModes = {['DIAGONAL'] = true, ['ORTHOGONAL'] = true} - -- Performs a traceback from the goal node to the start node -- Only happens when the path was found @@ -103,259 +74,27 @@ if (...) then -- @tparam grid grid a `grid` -- @tparam[opt] string finderName the name of the `Finder` (search algorithm) to be used for search. -- Defaults to `ASTAR` when not given (see @{Pathfinder:getFinders}). - -- @tparam[optchain] string|int|func walkable the value for __walkable__ nodes. - -- If this parameter is a function, it should be prototyped as __f(value)__, returning a boolean: - -- __true__ when value matches a __walkable__ `node`, __false__ otherwise. -- @treturn pathfinder a new `pathfinder` instance -- @usage - -- -- Example one - -- local finder = Pathfinder:new(myGrid, 'ASTAR', 0) - -- - -- -- Example two - -- local function walkable(value) - -- return value > 0 - -- end - -- local finder = Pathfinder(myGrid, 'JPS', walkable) - function Pathfinder:new(grid, finderName, walkable) + -- local finder = Pathfinder:new(myGrid, 'ASTAR') + function Pathfinder:new(heuristic) local newPathfinder = {} setmetatable(newPathfinder, Pathfinder) - --newPathfinder:setGrid(grid) - newPathfinder:setFinder(finderName) - --newPathfinder:setWalkable(walkable) - newPathfinder:setMode('DIAGONAL') - newPathfinder:setHeuristic('MANHATTAN') - newPathfinder:setTunnelling(false) + self._finder = Finders.ASTAR + self._heuristic = heuristic return newPathfinder end - --- Evaluates [clearance](http://aigamedev.com/open/tutorial/clearance-based-pathfinding/#TheTrueClearanceMetric) - -- for the whole `grid`. It should be called only once, unless the collision map or the - -- __walkable__ attribute changes. The clearance values are calculated and cached within the grid nodes. - -- @class function - -- @treturn pathfinder self (the calling `pathfinder` itself, can be chained) - -- @usage myFinder:annotateGrid() - function Pathfinder:annotateGrid() - assert(self._walkable, 'Finder must implement a walkable value') - for x=self._grid._max_x,self._grid._min_x,-1 do - for y=self._grid._max_y,self._grid._min_y,-1 do - local node = self._grid:getNodeAt(x,y) - if self._grid:isWalkableAt(x,y,self._walkable) then - local nr = self._grid:getNodeAt(node._x+1, node._y) - local nrd = self._grid:getNodeAt(node._x+1, node._y+1) - local nd = self._grid:getNodeAt(node._x, node._y+1) - if nr and nrd and nd then - local m = nrd._clearance[self._walkable] or 0 - m = (nd._clearance[self._walkable] or 0)0 - -- end - function Pathfinder:setWalkable(walkable) - assert(Assert.matchType(walkable,'stringintfunctionnil'), - ('Wrong argument #1. Expected \'string\', \'number\' or \'function\', got %s.'):format(type(walkable))) - self._walkable = walkable - self._grid._eval = type(self._walkable) == 'function' - return self - end - - --- Gets the __walkable__ value or function. - -- @class function - -- @treturn string|int|func the `walkable` value or function - -- @usage local walkable = myFinder:getWalkable() - function Pathfinder:getWalkable() - return self._walkable - end - - --- Defines the `finder`. It refers to the search algorithm used by the `pathfinder`. - -- Default finder is `ASTAR`. Use @{Pathfinder:getFinders} to get the list of available finders. - -- @class function - -- @tparam string finderName the name of the `finder` to be used for further searches. - -- @treturn pathfinder self (the calling `pathfinder` itself, can be chained) - -- @usage - -- --To use Breadth-First-Search - -- myFinder:setFinder('BFS') - -- @see Pathfinder:getFinders - function Pathfinder:setFinder(finderName) - if not finderName then - if not self._finder then - finderName = 'ASTAR' - else return - end - end - assert(Finders[finderName],'Not a valid finder name!') - self._finder = finderName - return self - end - - --- Returns the name of the `finder` being used. - -- @class function - -- @treturn string the name of the `finder` to be used for further searches. - -- @usage local finderName = myFinder:getFinder() - function Pathfinder:getFinder() - return self._finder - end - - --- Returns the list of all available finders names. - -- @class function - -- @treturn {string,...} array of built-in finders names. - -- @usage - -- local finders = myFinder:getFinders() - -- for i, finderName in ipairs(finders) do - -- print(i, finderName) - -- end - function Pathfinder:getFinders() - return Utils.getKeys(Finders) - end - - --- Sets a heuristic. This is a function internally used by the `pathfinder` to find the optimal path during a search. - -- Use @{Pathfinder:getHeuristics} to get the list of all available `heuristics`. One can also define - -- his own `heuristic` function. - -- @class function - -- @tparam func|string heuristic `heuristic` function, prototyped as __f(dx,dy)__ or as a `string`. - -- @treturn pathfinder self (the calling `pathfinder` itself, can be chained) - -- @see Pathfinder:getHeuristics - -- @see core.heuristics - -- @usage myFinder:setHeuristic('MANHATTAN') - function Pathfinder:setHeuristic(heuristic) - assert(Heuristic[heuristic] or (type(heuristic) == 'function'),'Not a valid heuristic!') - self._heuristic = Heuristic[heuristic] or heuristic - return self - end - - --- Returns the `heuristic` used. Returns the function itself. - -- @class function - -- @treturn func the `heuristic` function being used by the `pathfinder` - -- @see core.heuristics - -- @usage local h = myFinder:getHeuristic() - function Pathfinder:getHeuristic() - return self._heuristic - end - - --- Gets the list of all available `heuristics`. - -- @class function - -- @treturn {string,...} array of heuristic names. - -- @see core.heuristics - -- @usage - -- local heur = myFinder:getHeuristic() - -- for i, heuristicName in ipairs(heur) do - -- ... - -- end - function Pathfinder:getHeuristics() - return Utils.getKeys(Heuristic) - end - - --- Defines the search `mode`. - -- The default search mode is the `DIAGONAL` mode, which implies 8-possible directions when moving (north, south, east, west and diagonals). - -- In `ORTHOGONAL` mode, only 4-directions are allowed (north, south, east and west). - -- Use @{Pathfinder:getModes} to get the list of all available search modes. - -- @class function - -- @tparam string mode the new search `mode`. - -- @treturn pathfinder self (the calling `pathfinder` itself, can be chained) - -- @see Pathfinder:getModes - -- @see Modes - -- @usage myFinder:setMode('ORTHOGONAL') - function Pathfinder:setMode(mode) - assert(searchModes[mode],'Invalid mode') - self._allowDiagonal = (mode == 'DIAGONAL') - return self - end - - --- Returns the search mode. - -- @class function - -- @treturn string the current search mode - -- @see Modes - -- @usage local mode = myFinder:getMode() - function Pathfinder:getMode() - return (self._allowDiagonal and 'DIAGONAL' or 'ORTHOGONAL') - end - - --- Gets the list of all available search modes. - -- @class function - -- @treturn {string,...} array of search modes. - -- @see Modes - -- @usage local modes = myFinder:getModes() - -- for modeName in ipairs(modes) do - -- ... - -- end - function Pathfinder:getModes() - return Utils.getKeys(searchModes) - end - - --- Enables tunnelling. Defines the ability for the `pathfinder` to tunnel through walls when heading diagonally. - -- This feature __is not compatible__ with Jump Point Search algorithm (i.e. enabling it will not affect Jump Point Search) - -- @class function - -- @tparam bool bool a boolean - -- @treturn pathfinder self (the calling `pathfinder` itself, can be chained) - -- @usage myFinder:setTunnelling(true) - function Pathfinder:setTunnelling(bool) - assert(Assert.isBool(bool), ('Wrong argument #1. Expected boolean, got %s'):format(type(bool))) - self._tunnel = bool - return self - end - - --- Returns tunnelling feature state. - -- @class function - -- @treturn bool tunnelling feature actual state - -- @usage local isTunnellingEnabled = myFinder:getTunnelling() - function Pathfinder:getTunnelling() - return self._tunnel - end - --- Calculates a `path`. Returns the `path` from location __[startX, startY]__ to location __[endX, endY]__. -- Both locations must exist on the collision map. The starting location can be unwalkable. -- @class function @@ -363,11 +102,9 @@ if (...) then -- @tparam int startY the y-coordinate for the starting location -- @tparam int endX the x-coordinate for the goal location -- @tparam int endY the y-coordinate for the goal location - -- @tparam int clearance the amount of clearance (i.e the pathing agent size) to consider -- @treturn path a path (array of nodes) when found, otherwise nil -- @usage local path = myFinder:getPath(1,1,5,5) - function Pathfinder:getPath(startX, startY, startZ, ih, endX, endY, endZ, oh, clearance) - + function Pathfinder:getPath(startX, startY, startZ, ih, endX, endY, endZ, oh) self:reset() local startNode = self._grid:getNodeAt(startX, startY, startZ) local endNode = self._grid:getNodeAt(endX, endY, endZ) @@ -381,14 +118,15 @@ if (...) then assert(startNode, ('Invalid location [%d, %d, %d]'):format(startX, startY, startZ)) assert(endNode and self._grid:isWalkableAt(endX, endY, endZ), ('Invalid or unreachable location [%d, %d, %d]'):format(endX, endY, endZ)) - local _endNode = Finders[self._finder](self, startNode, endNode, clearance, toClear) + local _endNode = self._finder(self, startNode, endNode, toClear) if _endNode then return Utils.traceBackPath(self, _endNode, startNode) end return nil end - --- Resets the `pathfinder`. This function is called internally between successive pathfinding calls, so you should not + --- Resets the `pathfinder`. This function is called internally between + -- successive pathfinding calls, so you should not -- use it explicitely, unless under specific circumstances. -- @class function -- @treturn pathfinder self (the calling `pathfinder` itself, can be chained) @@ -399,7 +137,6 @@ if (...) then return self end - -- Returns Pathfinder class Pathfinder._VERSION = _VERSION Pathfinder._RELEASEDATE = _RELEASEDATE @@ -408,5 +145,4 @@ if (...) then return self:new(...) end }) - end diff --git a/sys/apis/jumper/search/astar.lua b/sys/apis/jumper/search/astar.lua index 3e1b323..ae730dc 100644 --- a/sys/apis/jumper/search/astar.lua +++ b/sys/apis/jumper/search/astar.lua @@ -5,16 +5,14 @@ if (...) then -- Internalization - local ipairs = ipairs local huge = math.huge -- Dependancies local _PATH = (...):match('(.+)%.search.astar$') - local Heuristics = require (_PATH .. '.core.heuristics') local Heap = require (_PATH.. '.core.bheap') -- Updates G-cost - local function computeCost(node, neighbour, finder, clearance, heuristic) + local function computeCost(node, neighbour, heuristic) local mCost, heading = heuristic(neighbour, node) -- Heuristics.EUCLIDIAN(neighbour, node) if node._g + mCost < neighbour._g then @@ -25,31 +23,24 @@ if (...) then end -- Updates vertex node-neighbour - local function updateVertex(finder, openList, node, neighbour, endNode, clearance, heuristic, overrideCostEval) + local function updateVertex(openList, node, neighbour, endNode, heuristic) local oldG = neighbour._g - local cmpCost = overrideCostEval or computeCost - cmpCost(node, neighbour, finder, clearance, heuristic) + computeCost(node, neighbour, heuristic) if neighbour._g < oldG then - local nClearance = neighbour._clearance[finder._walkable] - local pushThisNode = clearance and nClearance and (nClearance >= clearance) - if (clearance and pushThisNode) or (not clearance) then - if neighbour._opened then neighbour._opened = false end - neighbour._h = heuristic(endNode, neighbour) - neighbour._f = neighbour._g + neighbour._h - openList:push(neighbour) - neighbour._opened = true - end + if neighbour._opened then neighbour._opened = false end + neighbour._h = heuristic(endNode, neighbour) + neighbour._f = neighbour._g + neighbour._h + openList:push(neighbour) + neighbour._opened = true end end -- Calculates a path. -- Returns the path from location `` to location ``. - return function (finder, startNode, endNode, clearance, toClear, overrideHeuristic, overrideCostEval) - - local heuristic = overrideHeuristic or finder._heuristic + return function (finder, startNode, endNode, toClear) local openList = Heap() startNode._g = 0 - startNode._h = heuristic(endNode, startNode) + startNode._h = finder._heuristic(endNode, startNode) startNode._f = startNode._g + startNode._h openList:push(startNode) toClear[startNode] = true @@ -59,17 +50,17 @@ if (...) then local node = openList:pop() node._closed = true if node == endNode then return node end - local neighbours = finder._grid:getNeighbours(node, finder._walkable, finder._allowDiagonal, finder._tunnel) + local neighbours = finder._grid:getNeighbours(node) for i = 1,#neighbours do local neighbour = neighbours[i] if not neighbour._closed then toClear[neighbour] = true if not neighbour._opened then neighbour._g = huge - neighbour._parent = nil + neighbour._parent = nil end - updateVertex(finder, openList, node, neighbour, endNode, clearance, heuristic, overrideCostEval) - end + updateVertex(openList, node, neighbour, endNode, finder._heuristic) + end end --[[ @@ -81,8 +72,6 @@ if (...) then --]] end - - return nil + return nil end - end diff --git a/sys/apis/jumper/search/bfs.lua b/sys/apis/jumper/search/bfs.lua deleted file mode 100644 index e0abf5a..0000000 --- a/sys/apis/jumper/search/bfs.lua +++ /dev/null @@ -1,46 +0,0 @@ --- Breadth-First search algorithm - -if (...) then - -- Internalization - local t_remove = table.remove - - local function breadth_first_search(finder, openList, node, endNode, clearance, toClear) - local neighbours = finder._grid:getNeighbours(node, finder._walkable, finder._allowDiagonal, finder._tunnel) - for i = 1,#neighbours do - local neighbour = neighbours[i] - if not neighbour._closed and not neighbour._opened then - local nClearance = neighbour._clearance[finder._walkable] - local pushThisNode = clearance and nClearance and (nClearance >= clearance) - if (clearance and pushThisNode) or (not clearance) then - openList[#openList+1] = neighbour - neighbour._opened = true - neighbour._parent = node - toClear[neighbour] = true - end - end - end - - end - - -- Calculates a path. - -- Returns the path from location `` to location ``. - return function (finder, startNode, endNode, clearance, toClear) - - local openList = {} -- We'll use a FIFO queue (simple array) - openList[1] = startNode - startNode._opened = true - toClear[startNode] = true - - local node - while (#openList > 0) do - node = openList[1] - t_remove(openList,1) - node._closed = true - if node == endNode then return node end - breadth_first_search(finder, openList, node, endNode, clearance, toClear) - end - - return nil - end - -end \ No newline at end of file diff --git a/sys/apis/turtle/pathfind.lua b/sys/apis/turtle/pathfind.lua index ec6bc79..a39495a 100644 --- a/sys/apis/turtle/pathfind.lua +++ b/sys/apis/turtle/pathfind.lua @@ -7,59 +7,46 @@ local Util = require('util') local turtle = _G.turtle -local WALKABLE = 0 - -local function createMap(dim) - local map = { } - for _ = 1, dim.ez do - local row = {} - for _ = 1, dim.ex do - local col = { } - for _ = 1, dim.ey do - table.insert(col, WALKABLE) - end - table.insert(row, col) +local function addBlock(grid, b, dim) + if Point.inBox(b, dim) then + local node = grid:getNodeAt(b.x, b.y, b.z) + if node then + node.walkable = 1 end - table.insert(map, row) end - - return map -end - -local function addBlock(map, dim, b) - map[b.z + dim.oz][b.x + dim.ox][b.y + dim.oy] = 1 end -- map shrinks/grows depending upon blocks encountered -- the map will encompass any blocks encountered, the turtle position, and the destination -local function mapDimensions(dest, blocks, boundingBox) +local function mapDimensions(dest, blocks, boundingBox, dests) local sx, sz, sy = turtle.point.x, turtle.point.z, turtle.point.y local ex, ez, ey = turtle.point.x, turtle.point.z, turtle.point.y local function adjust(pt) if pt.x < sx then sx = pt.x - end - if pt.z < sz then - sz = pt.z + elseif pt.x > ex then + ex = pt.x end if pt.y < sy then sy = pt.y - end - if pt.x > ex then - ex = pt.x - end - if pt.z > ez then - ez = pt.z - end - if pt.y > ey then + elseif pt.y > ey then ey = pt.y end + if pt.z < sz then + sz = pt.z + elseif pt.z > ez then + ez = pt.z + end end adjust(dest) - for _,b in ipairs(blocks) do + for _,d in pairs(dests) do + adjust(d) + end + + for _,b in pairs(blocks) do adjust(b) end @@ -81,29 +68,23 @@ local function mapDimensions(dest, blocks, boundingBox) end return { - ex = ex - sx + 1, - ez = ez - sz + 1, - ey = ey - sy + 1, - ox = -sx + 1, - oz = -sz + 1, - oy = -sy + 1 + ex = ex, + ez = ez, + ey = ey, + x = sx, + z = sz, + y = sy } end --- shifting and coordinate flipping -local function pointToMap(dim, pt) - return { x = pt.x + dim.ox, z = pt.y + dim.oy, y = pt.z + dim.oz } -end - -local function nodeToPoint(dim, node) - return { x = node:getX() - dim.ox, z = node:getY() - dim.oz, y = node:getZ() - dim.oy } +local function nodeToPoint(node) + return { x = node:getX(), z = node:getZ(), y = node:getY() } end local heuristic = function(n, node) - local m, h = Point.calculateMoves( - { x = node._x, z = node._y, y = node._z, heading = node._heading }, - { x = n._x, z = n._y, y = n._z, heading = n._heading }) + { x = node._x, y = node._y, z = node._z, heading = node._heading }, + { x = n._x, y = n._y, z = n._z, heading = n._heading }) return m, h end @@ -112,9 +93,9 @@ local function dimsAreEqual(d1, d2) return d1.ex == d2.ex and d1.ey == d2.ey and d1.ez == d2.ez and - d1.ox == d2.ox and - d1.oy == d2.oy and - d1.oz == d2.oz + d1.x == d2.x and + d1.y == d2.y and + d1.z == d2.z end -- turtle sensor returns blocks in relation to the world - not turtle orientation @@ -122,7 +103,6 @@ end -- really kinda dumb since it returns the coordinates as offsets of our location -- instead of true coordinates local function addSensorBlocks(blocks, sblocks) - for _,b in pairs(sblocks) do if b.type ~= 'AIR' then local pt = { x = turtle.point.x, y = turtle.point.y + b.y, z = turtle.point.z } @@ -142,28 +122,29 @@ local function addSensorBlocks(blocks, sblocks) end end -local function selectDestination(pts, box, map, dim) - +local function selectDestination(pts, box, grid) + if #pts == 1 then + return pts[1] + end while #pts > 0 do local pt = Point.closest(turtle.point, pts) - - if (box and not Point.inBox(pt, box)) or - map[pt.z + dim.oz][pt.x + dim.ox][pt.y + dim.oy] == 1 then + if box and not Point.inBox(pt, box) then + Util.removeByValue(pts, pt) + else + if grid:isWalkableAt(pt.x, pt.y, pt.z) then + return pt + end Util.removeByValue(pts, pt) - else - return pt end end end local function pathTo(dest, options) - local blocks = options.blocks or turtle.getState().blocks or { } local dests = options.dest or { dest } -- support alternative destinations local box = options.box or turtle.getState().box local lastDim = nil - local map = nil local grid = nil if box then @@ -171,32 +152,26 @@ local function pathTo(dest, options) end -- Creates a pathfinder object - local myFinder = Pathfinder(grid, 'ASTAR', WALKABLE) - - myFinder:setMode('ORTHOGONAL') - myFinder:setHeuristic(heuristic) + local myFinder = Pathfinder(heuristic) while turtle.point.x ~= dest.x or turtle.point.z ~= dest.z or turtle.point.y ~= dest.y do -- map expands as we encounter obstacles - local dim = mapDimensions(dest, blocks, box) + local dim = mapDimensions(dest, blocks, box, dests) -- reuse map if possible if not lastDim or not dimsAreEqual(dim, lastDim) then - map = createMap(dim) -- Creates a grid object - grid = Grid(map) + grid = Grid(dim) myFinder:setGrid(grid) - myFinder:setWalkable(WALKABLE) lastDim = dim end - - for _,b in ipairs(blocks) do - addBlock(map, dim, b) + for _,b in pairs(blocks) do + addBlock(grid, b, dim) end - dest = selectDestination(dests, box, map, dim) + dest = selectDestination(dests, box, grid) if not dest then -- error('failed to reach destination') return false, 'failed to reach destination' @@ -206,8 +181,8 @@ local function pathTo(dest, options) end -- Define start and goal locations coordinates - local startPt = pointToMap(dim, turtle.point) - local endPt = pointToMap(dim, dest) + local startPt = turtle.point + local endPt = dest -- Calculates the path, and its length local path = myFinder:getPath( @@ -218,7 +193,7 @@ local function pathTo(dest, options) Util.removeByValue(dests, dest) else for node in path:nodes() do - local pt = nodeToPoint(dim, node) + local pt = nodeToPoint(node) if turtle.abort then return false, 'aborted' @@ -262,6 +237,12 @@ return { turtle.getState().blocks = blocks end, + addBlock = function(block) + if turtle.getState().blocks then + table.insert(turtle.getState().blocks, block) + end + end, + reset = function() turtle.getState().box = nil turtle.getState().blocks = nil diff --git a/sys/extensions/tl3.lua b/sys/extensions/tl3.lua index 4fb5f56..d51940f 100644 --- a/sys/extensions/tl3.lua +++ b/sys/extensions/tl3.lua @@ -969,8 +969,23 @@ function turtle.abortAction() end -- [[ Pathing ]] -- -function turtle.faceAgainst(pt, options) -- 4 sided +function turtle.setPersistent(isPersistent) + if isPersistent then + Pathing.setBlocks({ }) + else + Pathing.setBlocks() + end +end +function turtle.setPathingBox(box) + Pathing.setBox(box) +end + +function turtle.addWorldBlock(pt) + Pathing.addBlock(pt) +end + +function turtle.faceAgainst(pt, options) -- 4 sided options = options or { } options.dest = { } @@ -989,7 +1004,6 @@ function turtle.faceAgainst(pt, options) -- 4 sided end function turtle.moveAgainst(pt, options) -- 6 sided - options = options or { } options.dest = { } @@ -1090,6 +1104,32 @@ local function _actionUpAt(action, pt, ...) end end +local function _actionXXXAt(action, pt, dir, ...) + local reversed = + { [0] = 2, [1] = 3, [2] = 0, [3] = 1, [4] = 5, [5] = 4, } + dir = reversed[dir] + local apt = { x = pt.x + headings[dir].xd, + y = pt.y + headings[dir].yd, + z = pt.z + headings[dir].zd, } + local direction + + -- ex: place a block at this point, from above, facing east + if dir < 4 then + apt.heading = (dir + 2) % 4 + direction = 'forward' + elseif dir == 4 then + apt.heading = pt.heading + direction = 'down' + elseif dir == 5 then + apt.heading = pt.heading + direction = 'up' + end + + if turtle.pathfind(apt) then + return action[direction](...) + end +end + function turtle.detectAt(pt) return _actionAt(actionsAt.detect, pt) end function turtle.detectDownAt(pt) return _actionDownAt(actionsAt.detect, pt) end function turtle.detectForwardAt(pt) return _actionForwardAt(actionsAt.detect, pt) end @@ -1109,6 +1149,7 @@ function turtle.placeAt(pt, arg) return _actionAt(actionsAt.place, pt, ar function turtle.placeDownAt(pt, arg) return _actionDownAt(actionsAt.place, pt, arg) end function turtle.placeForwardAt(pt, arg) return _actionForwardAt(actionsAt.place, pt, arg) end function turtle.placeUpAt(pt, arg) return _actionUpAt(actionsAt.place, pt, arg) end +function turtle.placeXXXAt(pt, dir, arg) return _actionXXXAt(actionsAt.place, pt, dir, arg) end function turtle.dropAt(pt, ...) return _actionAt(actionsAt.drop, pt, ...) end function turtle.dropDownAt(pt, ...) return _actionDownAt(actionsAt.drop, pt, ...) end diff --git a/sys/network/proxy.lua b/sys/network/proxy.lua index 034f61f..62dab17 100644 --- a/sys/network/proxy.lua +++ b/sys/network/proxy.lua @@ -24,9 +24,10 @@ Event.addRoutine(function() while true do local data = socket:read() if not data then + print('proxy: lost connection from ' .. socket.dhost) break end - socket:write({ proxy[data.fn](unpack(data.args)) }) + socket:write({ proxy[data.fn](table.unpack(data.args)) }) end end end) diff --git a/sys/services/network.lua b/sys/services/network.lua index 3000f83..7ba9dd0 100644 --- a/sys/services/network.lua +++ b/sys/services/network.lua @@ -17,7 +17,7 @@ local function netUp() _G.requireInjector() local Event = require('event') -_G._e2 = _ENV + for _,file in pairs(fs.list('sys/network')) do local fn, msg = Util.run(_ENV, 'sys/network/' .. file) if not fn then @@ -48,8 +48,6 @@ print('Net daemon started') local function startNetwork() print('Starting network services') -_G._e1 = _ENV - local success, msg = Util.runFunction( Util.shallowCopy(_ENV), netUp)