Repository: Yonaba/Jumper Branch: master Commit: 441176b33536 Files: 45 Total size: 242.0 KB Directory structure: gitextract_lrl520tn/ ├── .gitignore ├── LICENSE.txt ├── README.md ├── docs/ │ ├── examples/ │ │ ├── annotatedpathing.lua.html │ │ ├── customheuristics.lua.html │ │ ├── makeclearance.lua.html │ │ └── simpleexample.lua.html │ ├── index.html │ └── modules/ │ ├── core.bheap.html │ ├── core.heuristics.html │ ├── core.node.html │ ├── core.path.html │ ├── grid.html │ └── pathfinder.html ├── examples/ │ ├── annotatedPathing.lua │ ├── customHeuristics.lua │ ├── makeClearance.lua │ └── simpleExample.lua ├── jumper/ │ ├── core/ │ │ ├── assert.lua │ │ ├── bheap.lua │ │ ├── heuristics.lua │ │ ├── lookuptable.lua │ │ ├── node.lua │ │ ├── path.lua │ │ └── utils.lua │ ├── grid.lua │ ├── pathfinder.lua │ └── search/ │ ├── astar.lua │ ├── bfs.lua │ ├── dfs.lua │ ├── dijkstra.lua │ ├── jps.lua │ └── thetastar.lua ├── rockspecs/ │ ├── jumper-1.6-2.rockspec │ ├── jumper-1.6.3-1.rockspec │ ├── jumper-1.7.0-1.rockspec │ ├── jumper-1.8.0.rockspec │ └── jumper-1.8.1-1.rockspec ├── specs/ │ ├── bheap_specs.lua │ ├── grid_specs.lua │ ├── heuristics_specs.lua │ ├── node_specs.lua │ ├── path_specs.lua │ └── pathfinder_specs.lua └── version_history.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.yml *.ld *.bat tsc test* ldoc* telescope* hkl debug* profiler* ================================================ FILE: LICENSE.txt ================================================ Copyright (c) 2012-2013 Roland Yonaba Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ Jumper ====== [![Join the chat at https://gitter.im/Yonaba/Jumper](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Yonaba/Jumper?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://secure.travis-ci.org/Yonaba/Jumper.png)](http://travis-ci.org/Yonaba/Jumper) __Jumper__ is a pathfinding library designed for grid-based games. It aims to be __fast__ and __lightweight__. It features a wide range of search algorithms, built within a clean interface with chaining features which makes it __very friendly and easy to use__.
__Jumper__ is written in pure [Lua][]. Thus, it is not __framework-related__ and can be used in any project embedding [Lua][] code. ## Installation The current repository can be retrieved locally on your computer via: ### Bash ```bash git clone git://github.com/Yonaba/Jumper.git ```` ### Download (latest) * __Development version__: [zip](http://github.com/Yonaba/Jumper/zipball/master) | [tar.gz](http://github.com/Yonaba/Jumper/tarball/master) ( __please do not use this!__ ) * __Latest stable release (1.8.1)__: [zip](http://github.com/Yonaba/Jumper/archive/jumper-1.8.1-1.zip) | [tar.gz](http://github.com/Yonaba/Jumper/archive/jumper-1.8.1-1.tar.gz) ( __Recommended!__ ) * __All stable releases__: [tags](http://github.com/Yonaba/Jumper/tags) ### LuaRocks ```bash luarocks install jumper ```` ### MoonRocks ```bash luarocks install --server=http://rocks.moonscript.org/manifests/Yonaba jumper ```` ## Installing Jumper Copy the contents of the folder named [jumper](http://github.com/Yonaba/Jumper/blob/master/jumper) and its contents and place it inside your projet. Use *require* function to import any module of the library. ## A Simple Example of Use Here is a simple example explaining how to use Jumper: ```lua -- Usage Example -- First, set a collision map local map = { {0,1,0,1,0}, {0,1,0,1,0}, {0,1,1,1,0}, {0,0,0,0,0}, } -- Value for walkable tiles local walkable = 0 -- Library setup local Grid = require ("jumper.grid") -- The grid class local Pathfinder = require ("jumper.pathfinder") -- The pathfinder class -- Creates a grid object local grid = Grid(map) -- Creates a pathfinder object using Jump Point Search local myFinder = Pathfinder(grid, 'JPS', walkable) -- Define start and goal locations coordinates local startx, starty = 1,1 local endx, endy = 5,1 -- Calculates the path, and its length local path = myFinder:getPath(startx, starty, endx, endy) if path then print(('Path found! Length: %.2f'):format(path:getLength())) for node, count in path:nodes() do print(('Step: %d - x: %d - y: %d'):format(count, node:getX(), node:getY())) end end --> Output: --> Path found! Length: 8.83 --> Step: 1 - x: 1 - y: 1 --> Step: 2 - x: 1 - y: 3 --> Step: 3 - x: 2 - y: 4 --> Step: 4 - x: 4 - y: 4 --> Step: 5 - x: 5 - y: 3 --> Step: 6 - x: 5 - y: 1 ```` ## Specs Specs tests have been included.
You can run them using [Telescope](http://github.com/norman/telescope) with the following command from the [root](http://github.com/Yonaba/Jumper/blob/master/jumper) folder: ``` tsc -f specs/* ``` ## Credits and Thanks * [Daniel Harabor][], [Alban Grastien][] : for the [Jump Point Search](http://harablog.wordpress.com/2011/09/07/jump-point-search/) algorithm.
* [XueXiao Xu][], [Nathan Witmer][]: for the [JavaScript port][] of the algorithm.
* [Steve Donovan](http://github.com/stevedonovan): for the awesome documentation generator tool [LDoc](http://github.com/stevedonovan/ldoc/). * [Srdjan Markovic](http://github.com/srdjan-m), for his tremendous feedback. ## License This work is under [MIT-LICENSE][]
Copyright (c) 2012-2013 Roland Yonaba. > Permission is hereby granted, free of charge, to any person obtaining a copy
> of this software and associated documentation files (the "Software"), to deal
> in the Software without restriction, including without limitation the rights
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
> copies of the Software, and to permit persons to whom the Software is
> furnished to do so, subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in
> all copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
> THE SOFTWARE. [Jump Point Search]: http://harablog.wordpress.com/2011/09/07/jump-point-search/ [Lua]: http://lua.org [L�ve]: http://love2d.org [L�ve2d]: http://love2d.org [L�ve 0.8.0 Framework]: http://love2d.org [Dragon Age : Origins]: http://dragonage.bioware.com [Moving AI]: http://movingai.com [Nathan Witmer]: http://github.com/aniero [XueXiao Xu]: http://github.com/qiao [JavaScript port]: http://github.com/qiao/PathFinding.js [Alban Grastien]: http://www.grastien.net/ban/ [Daniel Harabor]: http://users.cecs.anu.edu.au/~dharabor/home.html [the algorithm and the technical papers]: http://users.cecs.anu.edu.au/~dharabor/data/papers/harabor-grastien-aaai11.pdf [MIT-LICENSE]: http://www.opensource.org/licenses/mit-license.php [heuristics.lua]: http://github.com/Yonaba/Jumper/blob/master/Jumper/core/heuristics.lua [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/Yonaba/jumper/trend.png)](https://bitdeli.com/free "Bitdeli Badge") [![githalytics.com alpha](https://cruel-carlota.pagodabox.com/5165385288dcb27776c96dce1a82e33d "githalytics.com")](http://githalytics.com/Yonaba/Jumper) ================================================ FILE: docs/examples/annotatedpathing.lua.html ================================================ Jumper documentation

Example annotatedpathing.lua

-- Tests sample for clearance metrics calculation
-- See Figure 10 at http://aigamedev.com/open/tutorial/clearance-based-pathfinding/
-- Jump Point Search still has some flaws with clearance based pathfinding

local Grid = require 'jumper.grid'
local PF = require 'jumper.pathfinder'
local map = {
	{0,0,0,0,0,0,0,0,0,0},
	{0,0,0,0,0,0,0,0,1,0},
	{0,0,0,0,0,0,0,0,0,0},
	{0,0,0,1,0,0,0,0,0,0},
	{0,0,1,0,0,0,0,0,2,0},
	{0,0,1,1,1,0,0,2,0,0},
	{0,0,0,1,1,0,2,0,0,2},
	{0,0,0,0,1,0,0,0,0,2},
	{0,0,0,0,0,0,0,0,0,0},
	{0,0,0,0,0,0,0,0,0,0}
}
local grid = Grid(map)
local walkable = function(v) return v~=2 end
local finder = PF(grid, 'ASTAR',walkable)
finder:annotateGrid()
local finderNames = PF:getFinders()

local sx, sy = 1,1
local ex, ey = 9,9
local agent_size = 2

for i = 1,#finderNames do
	finder:setFinder(finderNames[i])
	local path = finder:getPath(sx, sy, ex, ey, agent_size)
	print(('Algorithm used: %s - Path %s')
		:format(finder:getFinder(), path and 'found' or 'not found'))
	if path then
		for node, count in path:nodes() do
			print(('  Step %d. (%d,%d)')
				:format(count, node:getPos()))
		end
	end
end
generated by LDoc 1.2
================================================ FILE: docs/examples/customheuristics.lua.html ================================================ Jumper documentation

Example customheuristics.lua

--- Example of use for Heuristics

local Grid = require ("jumper.grid")
local Pathfinder = require ("jumper.pathfinder")

local map = {
  {0,0,0,0,0,0},
  {0,0,0,0,0,0},
  {0,1,1,1,1,0},
  {0,0,0,0,0,0},
  {0,0,0,0,0,0},
}

local walkable = 0
local grid = Grid(map)
local myFinder = Pathfinder(grid, 'ASTAR', walkable)

-- Use Euclidian heuristic to evaluate distance
myFinder:setHeuristic('EUCLIDIAN')
myFinder:setHeuristic('DIAGONAL')
myFinder:setHeuristic('MANHATTAN')

-- Custom
local h = function(nodeA, nodeB)
	return (0.1 * (math.abs(nodeA:getX() - nodeB:getX()))
	      + 0.9 * (math.abs(nodeA:getY() - nodeB:getY())))
end
myFinder:setHeuristic(h)

local p = myFinder:getPath(1,1, 6,5)
for node, count in p:nodes() do
  print(('%d. Node(%d,%d)'):format(count, node:getX(), node:getY()))
end
print(('Path length: %.2f'):format(p:getLength()))

-- etc ...
generated by LDoc 1.2
================================================ FILE: docs/examples/makeclearance.lua.html ================================================ Jumper documentation

Example makeclearance.lua

-- Tests sample for clearance metrics calculation
-- See Figure 10 at http://aigamedev.com/open/tutorial/clearance-based-pathfinding/
local Grid = require 'jumper.grid'
local PF = require 'jumper.pathfinder'
local map = {
	{0,0,0,0,0,0,0,0,0,0},
	{0,0,0,0,0,0,0,0,1,0},
	{0,0,0,0,0,0,0,0,0,0},
	{0,0,0,1,0,0,0,0,0,0},
	{0,0,1,0,0,0,0,0,2,0},
	{0,0,1,1,1,0,0,2,0,0},
	{0,0,0,1,1,0,2,0,0,2},
	{0,0,0,0,1,0,0,0,0,2},
	{0,0,0,0,0,0,0,0,0,0},
	{0,0,0,0,0,0,0,0,0,0}
}
local grid = Grid(map)
local walkable = function(v) return v~=2 end
local finder = PF(grid, 'ASTAR',walkable)
finder:annotateGrid()

for y = 1, #map do
	local s = ''
	for x = 1, #map[y] do
	  local node = grid:getNodeAt(x,y)
		s = (s .. ' ' .. node:getClearance(walkable))
	end
	print(s)
end

-- Expected output
--  6 6 5 5 4 4 4 3 2 1
--  6 5 5 4 4 3 3 3 2 1
--  6 5 4 4 3 3 2 2 2 1
--  6 5 4 3 3 2 2 1 1 1
--  6 5 4 3 2 2 1 1 0 1
--  5 5 4 3 2 1 1 0 1 1
--  4 4 4 3 2 1 0 2 1 0
--  3 3 3 3 3 3 3 2 1 0
--  2 2 2 2 2 2 2 2 2 1
--  1 1 1 1 1 1 1 1 1 1
generated by LDoc 1.2
================================================ FILE: docs/examples/simpleexample.lua.html ================================================ Jumper documentation

Example simpleexample.lua

--- Very minimal usage example for Jumper

-- Set up a collision map
local map = {
	{0,1,0,1,0},
	{0,1,0,1,0},
	{0,1,1,1,0},
	{0,0,0,0,0},
}
-- Value for walkable tiles
local walkable = 0

-- Library setup
-- Calls the grid class
local Grid = require ("jumper.grid")
-- Calls the pathfinder class
local Pathfinder = require ("jumper.pathfinder")

-- Creates a grid object
local grid = Grid(map)

-- Creates a pathfinder object using Jump Point Search algorithm
local myFinder = Pathfinder(grid, 'JPS', walkable)

-- Define start and goal locations coordinates
local startx, starty = 1,1
local endx, endy = 5,1

-- Calculates the path, and its length
local path = myFinder:getPath(startx, starty, endx, endy)

-- Pretty-printing the results
if path then
  print(('Path found! Length: %.2f'):format(path:getLength()))
	for node, count in path:nodes() do
	  print(('Step: %d - x: %d - y: %d'):format(count, node:getX(), node:getY()))
	end
end
generated by LDoc 1.2
================================================ FILE: docs/index.html ================================================ Jumper documentation

Fast and lightweight pathfinding library for grid based games

Modules

core.bheap A light implementation of Binary heaps data structure.
core.heuristics Heuristic functions for search algorithms.
core.node The Node class.
core.path The Path class.
grid The Grid class.
pathfinder The Pathfinder class

Examples

annotatedpathing.lua
customheuristics.lua
makeclearance.lua
simpleexample.lua
generated by LDoc 1.2
================================================ FILE: docs/modules/core.bheap.html ================================================ Jumper documentation

Module core.bheap

A light implementation of Binary heaps data structure.

While running a search, some search algorithms (Astar, Dijkstra, Jump Point Search) have to maintains a list of nodes called open list. Retrieve from this list the lowest cost node can be quite slow, as it normally requires to skim through the full set of nodes stored in this list. This becomes a real problem especially when dozens of nodes are being processed (on large maps).

The current module implements a binary heap data structure, from which the search algorithm will instantiate an open list, and cache the nodes being examined during a search. As such, retrieving the lower-cost node is faster and globally makes the search end up quickly.

This module is internally used by the library on purpose. It should normally not be used explicitely, yet it remains fully accessible.

Class heap

heap:empty () Checks if a heap is empty
heap:clear () Clears the heap (removes all items queued in the heap)
heap:push (item) Adds a new item in the heap
heap:pop () Pops from the heap .
heap:heapify ( [item]) Restores the heap property.


Class heap

The heap class.
This class is callable. Therefore, heap(...) is used to instantiate new heaps.
heap:empty ()
Checks if a heap is empty

Usage:

     if myHeap:empty() then
       print('Heap is empty!')
     end

Returns:

    bool true of no item is queued in the heap, false otherwise
heap:clear ()
Clears the heap (removes all items queued in the heap)

Usage:

    myHeap:clear()

Returns:

    heap self (the calling heap itself, can be chained)
heap:push (item)
Adds a new item in the heap

Parameters:

  • item value a new value to be queued in the heap

Usage:

     myHeap:push(1)
     -- or, with chaining
     myHeap:push(1):push(2):push(4)

Returns:

    heap self (the calling heap itself, can be chained)
heap:pop ()
Pops from the heap . Removes and returns the lowest cost item (with respect to the comparison function being used) from the heap .

Usage:

     while not myHeap:empty() do
       local lowestValue = myHeap:pop()
       ...
     end

Returns:

    value a value previously pushed into the heap
heap:heapify ( [item])
Restores the heap property. Reorders the heap with respect to the comparison function being used. When given argument item (a value existing in the heap ), will sort from that very item in the heap . Otherwise, the whole heap will be cheacked.

Parameters:

  • item value the modified value

Usage:

    myHeap:heapify()

Returns:

    heap self (the calling heap itself, can be chained)
generated by LDoc 1.2
================================================ FILE: docs/modules/core.heuristics.html ================================================ Jumper documentation

Module core.heuristics

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.

Functions

Heuristics.MANHATTAN (nodeA, nodeB) Manhattan distance.
Heuristics.EUCLIDIAN (nodeA, nodeB) Euclidian distance.
Heuristics.DIAGONAL (nodeA, nodeB) Diagonal distance.
Heuristics.CARDINTCARD (nodeA, nodeB) Cardinal/Intercardinal distance.


Functions

Heuristics.MANHATTAN (nodeA, nodeB)
Manhattan distance.
This heuristic is the default one being used by the pathfinder object.
Evaluates as distance = |dx|+|dy|

Parameters:

  • nodeA node a node
  • nodeB node another node

Usage:

     -- First method
     pathfinder:setHeuristic('MANHATTAN')
     -- Second method
     local Distance = require ('jumper.core.heuristics')
     pathfinder:setHeuristic(Distance.MANHATTAN)

Returns:

    number the distance from nodeA to nodeB
Heuristics.EUCLIDIAN (nodeA, nodeB)
Euclidian distance.
Evaluates as distance = squareRoot(dxdx+dydy)

Parameters:

  • nodeA node a node
  • nodeB node another node

Usage:

     -- First method
     pathfinder:setHeuristic('EUCLIDIAN')
     -- Second method
     local Distance = require ('jumper.core.heuristics')
     pathfinder:setHeuristic(Distance.EUCLIDIAN) 

Returns:

    number the distance from nodeA to nodeB
Heuristics.DIAGONAL (nodeA, nodeB)
Diagonal distance.
Evaluates as distance = max(|dx|, abs|dy|)

Parameters:

  • nodeA node a node
  • nodeB node another node

Usage:

     -- First method
     pathfinder:setHeuristic('DIAGONAL')
     -- Second method
     local Distance = require ('jumper.core.heuristics')
     pathfinder:setHeuristic(Distance.DIAGONAL)

Returns:

    number the distance from nodeA to nodeB
Heuristics.CARDINTCARD (nodeA, nodeB)
Cardinal/Intercardinal distance.
Evaluates as distance = min(dx, dy)*squareRoot(2) + max(dx, dy) - min(dx, dy)

Parameters:

  • nodeA node a node
  • nodeB node another node

Usage:

     -- First method
     pathfinder:setHeuristic('CARDINTCARD')
     -- Second method
     local Distance = require ('jumper.core.heuristics')
     pathfinder:setHeuristic(Distance.CARDINTCARD)

Returns:

    number the distance from nodeA to nodeB
generated by LDoc 1.2
================================================ FILE: docs/modules/core.node.html ================================================ Jumper documentation

Module core.node

The Node class.

The node represents a cell (or a tile) on a collision map. Basically, for each single cell (tile) in the collision map passed-in upon initialization, a node object will be generated 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 to the next neighbouring node having the lowest f cost. See core.bheap for more details.

Class Node

Node:new (x, y) Inits a new node
Node:getX () Returns x-coordinate of a node
Node:getY () Returns y-coordinate of a node
Node:getPos () Returns x and y coordinates of a node
Node:getClearance (walkable) Returns the amount of true clearance for a given node
Node:removeClearance (walkable) Removes the clearance value for a given walkable.
Node:reset () Clears temporary cached attributes of a node .


Class Node

The Node class.
This class is callable. Therefore,_ Node(...) acts as a shortcut to Node:new(...).
Node:new (x, y)
Inits a new node

Parameters:

  • x int the x-coordinate of the node on the collision map
  • y int the y-coordinate of the node on the collision map

Usage:

    local node = Node(3,4)

Returns:

    node a new node
Node:getX ()
Returns x-coordinate of a node

Usage:

    local x = node:getX()

Returns:

    number the x-coordinate of the node
Node:getY ()
Returns y-coordinate of a node

Usage:

    local y = node:getY()

Returns:

    number the y-coordinate of the node
Node:getPos ()
Returns x and y coordinates of a node

Usage:

    local x, y = node:getPos()

Returns:

  1. number the x-coordinate of the node
  2. number the y-coordinate of the node
Node:getClearance (walkable)
Returns the amount of true clearance for a given node

Parameters:

  • walkable string, int or func the value for walkable locations in the collision map array.

Usage:

      -- Assuming walkable was 0
     local clearance = node:getClearance(0)		

Returns:

    int the clearance of the node
Node:removeClearance (walkable)
Removes the clearance value for a given walkable.

Parameters:

  • walkable string, int or func the value for walkable locations in the collision map array.

Usage:

      -- Assuming walkable is defined
     node:removeClearance(walkable)	

Returns:

    node self (the calling node itself, can be chained)
Node:reset ()
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.

Usage:

     local thisNode = Node(1,2)
     thisNode:reset()

Returns:

    node self (the calling node itself, can be chained)
generated by LDoc 1.2
================================================ FILE: docs/modules/core.path.html ================================================ Jumper documentation

Module core.path

The Path class.

The path class is a structure which represents a path (ordered set of nodes) from a start location to a goal. An instance from this class would be a result of a request addressed to Pathfinder:getPath.

This module is internally used by the library on purpose. It should normally not be used explicitely, yet it remains fully accessible.

Class Path

Path:new () Inits a new path .
Path:iter () Iterates on each single node along a path .
Path:nodes () Iterates on each single node along a path .
Path:getLength () Evaluates the path length
Path:addNode (node [, index]) Counts the number of steps.
Path:fill () Path filling modifier.
Path:filter () Path compression modifier.
Path:clone () Clones a path .
Path:isEqualTo (p2) Checks if a path is equal to another.
Path:reverse () Reverses a path .
Path:append (p) Appends a given path to self.


Class Path

The Path class.
This class is callable. Therefore, Path(...) acts as a shortcut to Path:new(...).
Path:new ()
Inits a new path .

Usage:

    local p = Path()

Returns:

    path a path
Path:iter ()
Iterates on each single node along a path . At each step of iteration, returns the node plus a count value. Aliased as Path:nodes

Usage:

     for node, count in p:iter() do
       ...
     end

Returns:

  1. node a node
  2. int the count for the number of nodes

see also:

Path:nodes ()
Iterates on each single node along a path . At each step of iteration, returns a node plus a count value. Alias for Path:iter

Usage:

     for node, count in p:nodes() do
       ...
     end	

Returns:

  1. node a node
  2. int the count for the number of nodes

see also:

Path:getLength ()
Evaluates the path length

Usage:

    local len = p:getLength()

Returns:

    number the path length
Path:addNode (node [, index])
Counts the number of steps. Returns the number of waypoints (nodes) in the current path.

Parameters:

  • node node a node to be added to the path
  • index int the index at which the node will be inserted. If omitted, the node will be appended after the last node in the path.

Usage:

    local nSteps = p:countSteps()

Returns:

    path self (the calling path itself, can be chained)
Path:fill ()
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

Usage:

    p:fill()

Returns:

    path self (the calling path itself, can be chained)

see also:

Path:filter ()
Path compression modifier. Given a path , eliminates useless nodes to return a lighter path
consisting of straight moves. Does the opposite of Path:fill

Usage:

    p:filter()

Returns:

    path self (the calling path itself, can be chained)

see also:

Path:clone ()
Clones a path .

Usage:

    local p = path:clone()

Returns:

    path a path
Path:isEqualTo (p2)
Checks if a path is equal to another. It also supports filtered paths (see Path:filter).

Parameters:

Usage:

    print(myPath:isEqualTo(anotherPath))

Returns:

    boolean a boolean
Path:reverse ()
Reverses a path .

Usage:

    myPath:reverse()

Returns:

    path self (the calling path itself, can be chained)
Path:append (p)
Appends a given path to self.

Parameters:

Usage:

    myPath:append(anotherPath)

Returns:

    path self (the calling path itself, can be chained)
generated by LDoc 1.2
================================================ FILE: docs/modules/grid.html ================================================ Jumper documentation

Module grid

The Grid class.

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 object, and the whole set of nodes are tight inside the grid object itself.

Class Grid

Grid:new (map [, cacheNodeAtRuntime]) Inits a new grid
Grid:isWalkableAt (x, y [, walkable [, clearance]]) Checks if node at [x,y] is walkable.
Grid:getWidth () Returns the grid width.
Grid:getHeight () Returns the grid height.
Grid:getMap () Returns the collision map.
Grid:getNodes () Returns the set of nodes.
Grid:getBounds () Returns the grid bounds.
Grid:getNeighbours (node [, walkable [, allowDiagonal [, tunnel [, clearance]]]]) Returns neighbours.
Grid:iter ( [lx [, ly [, ex [, ey]]]]) Grid iterator.
Grid:around (node [, radius]) Grid iterator.
Grid:each (f [, ...]) Each transformation.
Grid:eachRange (lx, ly, ex, ey, f [, ...]) Each (in range) transformation.
Grid:imap (f [, ...]) Map transformation.
Grid:imapRange (lx, ly, ex, ey, f [, ...]) Map in range transformation.
Grid:getNodeAt (x, y) Returns the node at location [x,y].


Class Grid

The Grid class.
This class is callable. Therefore,_ Grid(...) acts as a shortcut to Grid:new(...).
Grid:new (map [, cacheNodeAtRuntime])
Inits a new grid

Parameters:

  • map table or string A collision map - (2D array) with consecutive indices (starting at 0 or 1) or a string with line-break chars (\n or \r) as row delimiters.
  • cacheNodeAtRuntime bool 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.

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)

Returns:

    grid a new grid instance
Grid:isWalkableAt (x, y [, walkable [, clearance]])
Checks if node at [x,y] is walkable. Will check if node at location [x,y] both exists on the collision map and is walkable

Parameters:

  • x int the x-location of the node
  • y int the y-location of the node
  • walkable string, int or func 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.
  • clearance int the amount of clearance needed. Defaults to 1 (normal clearance) when not given.

Usage:

     -- Always true
     print(myGrid:isWalkableAt(2,3))
    
     -- 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))
    

Returns:

    bool true if node exists and is walkable, false otherwise
Grid:getWidth ()
Returns the grid width.

Usage:

    print(myGrid:getWidth())

Returns:

    int the grid width
Grid:getHeight ()
Returns the grid height.

Usage:

    print(myGrid:getHeight())

Returns:

    int the grid height
Grid:getMap ()
Returns the collision map.

Usage:

    local map = myGrid:getMap()

Returns:

    map the collision map (see Grid:new)
Grid:getNodes ()
Returns the set of nodes.

Usage:

    local nodes = myGrid:getNodes()

Returns:

    {{node,...},...} an array of nodes
Grid:getBounds ()
Returns the grid bounds. Returned values corresponds to the upper-left and lower-right coordinates (in tile units) of the actual grid instance.

Usage:

    local left_x, left_y, right_x, right_y = myGrid:getBounds()

Returns:

  1. int the upper-left corner x-coordinate
  2. int the upper-left corner y-coordinate
  3. int the lower-right corner x-coordinate
  4. int the lower-right corner y-coordinate
Grid:getNeighbours (node [, walkable [, allowDiagonal [, tunnel [, clearance]]]])
Returns neighbours. The returned value is an array of walkable nodes neighbouring a given node.

Parameters:

  • node node a given node
  • walkable string, int or func the value for walkable locations in the collision map array (see Grid:new). Defaults to false when omitted.
  • allowDiagonal bool when true, allows adjacent nodes are included (8-neighbours). Defaults to false when omitted.
  • tunnel bool When true, allows the pathfinder to tunnel through walls when heading diagonally.
  • clearance int When given, will prune for the neighbours set all nodes having a clearance value lower than the passed-in value Defaults to false when omitted.

Usage:

     local aNode = myGrid:getNodeAt(5,6)
     local neighbours = myGrid:getNeighbours(aNode, 0, true)

Returns:

    {node,...} an array of nodes neighbouring a given node
Grid:iter ( [lx [, ly [, ex [, ey]]]])
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.

Parameters:

  • lx int the leftmost x-coordinate of the rectangle. Default to the grid leftmost x-coordinate (see Grid:getBounds).
  • ly int the topmost y-coordinate of the rectangle. Default to the grid topmost y-coordinate (see Grid:getBounds).
  • ex int the rightmost x-coordinate of the rectangle. Default to the grid rightmost x-coordinate (see Grid:getBounds).
  • ey int the bottom-most y-coordinate of the rectangle. Default to the grid bottom-most y-coordinate (see Grid:getBounds).

Usage:

     for node, count in myGrid:iter() do
       print(node:getX(), node:getY(), count)
     end

Returns:

  1. node a node on the collision map, upon each iteration step
  2. int the iteration count
Grid:around (node [, radius])
Grid iterator. Iterates on each node along the outline (border) of a squared area centered on the given node.

Parameters:

  • node node a given node
  • radius int the area radius (half-length). Defaults to 1 when not given.

Usage:

     for node in myGrid:around(node, 2) do
       ...
     end

Returns:

    node a node at each iteration step
Grid:each (f [, ...])
Each transformation. Calls the given function on each node in the grid , passing the node as the first argument to function f.

Parameters:

  • f func a function prototyped as f(node,...)
  • ... vararg args to be passed to function f

Usage:

     local function printNode(node)
       print(node:getX(), node:getY())
     end
     myGrid:each(printNode)

Returns:

    grid self (the calling grid itself, can be chained)
Grid:eachRange (lx, ly, ex, ey, f [, ...])
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.

Parameters:

  • lx int the leftmost x-coordinate coordinate of the rectangle
  • ly int the topmost y-coordinate of the rectangle
  • ex int the rightmost x-coordinate of the rectangle
  • ey int the bottom-most y-coordinate of the rectangle
  • f func a function prototyped as f(node,...)
  • ... vararg args to be passed to function f

Usage:

     local function printNode(node)
       print(node:getX(), node:getY())
     end
     myGrid:eachRange(1,1,8,8,printNode)

Returns:

    grid self (the calling grid itself, can be chained)
Grid:imap (f [, ...])
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.

Parameters:

  • f func a function prototyped as f(node,...)
  • ... vararg args to be passed to function f

Usage:

     local function nothing(node)
       return node
     end
     myGrid:imap(nothing)

Returns:

    grid self (the calling grid itself, can be chained)
Grid:imapRange (lx, ly, ex, ey, f [, ...])
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.

Parameters:

  • lx int the leftmost x-coordinate coordinate of the rectangle
  • ly int the topmost y-coordinate of the rectangle
  • ex int the rightmost x-coordinate of the rectangle
  • ey int the bottom-most y-coordinate of the rectangle
  • f func a function prototyped as f(node,...)
  • ... vararg args to be passed to function f

Usage:

     local function nothing(node)
       return node
     end
     myGrid:imap(1,1,6,6,nothing)

Returns:

    grid self (the calling grid itself, can be chained)
Grid:getNodeAt (x, y)
Returns the node at location [x,y].

Parameters:

  • x int the x-coordinate coordinate
  • y int the y-coordinate coordinate

Usage:

    local aNode = myGrid:getNodeAt(2,2)

Returns:

    node a node
generated by LDoc 1.2
================================================ FILE: docs/modules/pathfinder.html ================================================ Jumper documentation

Module pathfinder

The Pathfinder class

Finders

Finders Finders (search algorithms implemented).

Modes

Modes Search modes.

Class Pathfinder

Pathfinder:new (grid [, finderName [, walkable]]) Inits a new pathfinder
Pathfinder:annotateGrid () Evaluates clearance for the whole grid .
Pathfinder:clearAnnotations () Removes clearancevalues.
Pathfinder:setGrid (grid) Sets the grid .
Pathfinder:getGrid () Returns the grid .
Pathfinder:setWalkable (walkable) Sets the walkable value or function.
Pathfinder:getWalkable () Gets the walkable value or function.
Pathfinder:setFinder (finderName) Defines the finder.
Pathfinder:getFinder () Returns the name of the finder being used.
Pathfinder:getFinders () Returns the list of all available finders names.
Pathfinder:setHeuristic (heuristic) Sets a heuristic.
Pathfinder:getHeuristic () Returns the heuristic used.
Pathfinder:getHeuristics () Gets the list of all available heuristics.
Pathfinder:setMode (mode) Defines the search mode.
Pathfinder:getMode () Returns the search mode.
Pathfinder:getModes () Gets the list of all available search modes.
Pathfinder:setTunnelling (bool) Enables tunnelling.
Pathfinder:getTunnelling () Returns tunnelling feature state.
Pathfinder:getPath (startX, startY, endX, endY, clearance) Calculates a path.
Pathfinder:reset () Resets the pathfinder .


Finders

Finders
Finders (search algorithms implemented). Refers to the search algorithms actually implemented in Jumper.

  • A*
  • Dijkstra
  • Theta Astar
  • BFS
  • DFS
  • JPS
  • see also:

    Modes

    Modes
    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.

  • ORTHOGNAL
  • DIAGONAL
  • see also:

    Class Pathfinder

    The Pathfinder class.
    This class is callable. Therefore,_ Pathfinder(...) acts as a shortcut to Pathfinder:new(...).
    Pathfinder:new (grid [, finderName [, walkable]])
    Inits a new pathfinder

    Parameters:

    • grid grid a grid
    • finderName string the name of the Finder (search algorithm) to be used for search. Defaults to ASTAR when not given (see Pathfinder:getFinders).
    • walkable string, int or func 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.

    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)

    Returns:

      pathfinder a new pathfinder instance
    Pathfinder:annotateGrid ()
    Evaluates clearance 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.

    Usage:

      myFinder:annotateGrid()

    Returns:

      pathfinder self (the calling pathfinder itself, can be chained)
    Pathfinder:clearAnnotations ()
    Removes clearancevalues. Clears cached clearance values for the current walkable.

    Usage:

      myFinder:clearAnnotations()

    Returns:

      pathfinder self (the calling pathfinder itself, can be chained)
    Pathfinder:setGrid (grid)
    Sets the grid . Defines the given grid as the one on which the pathfinder will perform the search.

    Parameters:

    Usage:

      myFinder:setGrid(myGrid)

    Returns:

      pathfinder self (the calling pathfinder itself, can be chained)
    Pathfinder:getGrid ()
    Returns the grid . This is a reference to the actual grid used by the pathfinder .

    Usage:

      local myGrid = myFinder:getGrid()

    Returns:

      grid the grid
    Pathfinder:setWalkable (walkable)
    Sets the walkable value or function.

    Parameters:

    Usage:

       -- Value '0' is walkable
       myFinder:setWalkable(0)
      
       -- Any value greater than 0 is walkable
       myFinder:setWalkable(function(n)
         return n>0
       end

    Returns:

      pathfinder self (the calling pathfinder itself, can be chained)
    Pathfinder:getWalkable ()
    Gets the walkable value or function.

    Usage:

      local walkable = myFinder:getWalkable()

    Returns:

      string, int or func the walkable value or function
    Pathfinder:setFinder (finderName)
    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.

    Parameters:

    • finderName string the name of the finder to be used for further searches.

    Usage:

       --To use Breadth-First-Search
       myFinder:setFinder('BFS')

    Returns:

      pathfinder self (the calling pathfinder itself, can be chained)

    see also:

    Pathfinder:getFinder ()
    Returns the name of the finder being used.

    Usage:

      local finderName = myFinder:getFinder()

    Returns:

      string the name of the finder to be used for further searches.
    Pathfinder:getFinders ()
    Returns the list of all available finders names.

    Usage:

       local finders = myFinder:getFinders()
       for i, finderName in ipairs(finders) do
         print(i, finderName)
       end

    Returns:

      {string,...} array of built-in finders names.
    Pathfinder:setHeuristic (heuristic)
    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.

    Parameters:

    • heuristic func or string heuristic function, prototyped as f(dx,dy) or as a string .

    Usage:

      myFinder:setHeuristic('MANHATTAN')

    Returns:

      pathfinder self (the calling pathfinder itself, can be chained)

    see also:

    Pathfinder:getHeuristic ()
    Returns the heuristic used. Returns the function itself.

    Usage:

      local h = myFinder:getHeuristic()

    Returns:

      func the heuristic function being used by the pathfinder

    see also:

    Pathfinder:getHeuristics ()
    Gets the list of all available heuristics.

    Usage:

       local heur = myFinder:getHeuristic()
       for i, heuristicName in ipairs(heur) do
         ...
       end

    Returns:

      {string,...} array of heuristic names.

    see also:

    Pathfinder:setMode (mode)
    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.

    Parameters:

    • mode string the new search mode.

    Usage:

      myFinder:setMode('ORTHOGNAL')

    Returns:

      pathfinder self (the calling pathfinder itself, can be chained)

    see also:

    Pathfinder:getMode ()
    Returns the search mode.

    Usage:

      local mode = myFinder:getMode()

    Returns:

      string the current search mode

    see also:

    Pathfinder:getModes ()
    Gets the list of all available search modes.

    Usage:

       local modes = myFinder:getModes()
       for modeName in ipairs(modes) do
         ...
       end

    Returns:

      {string,...} array of search modes.

    see also:

    Pathfinder:setTunnelling (bool)
    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)

    Parameters:

    • bool bool a boolean

    Usage:

      myFinder:setTunnelling(true)

    Returns:

      pathfinder self (the calling pathfinder itself, can be chained)
    Pathfinder:getTunnelling ()
    Returns tunnelling feature state.

    Usage:

      local isTunnellingEnabled = myFinder:getTunnelling()

    Returns:

      bool tunnelling feature actual state
    Pathfinder:getPath (startX, startY, endX, endY, clearance)
    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.

    Parameters:

    • startX int the x-coordinate for the starting location
    • startY int the y-coordinate for the starting location
    • endX int the x-coordinate for the goal location
    • endY int the y-coordinate for the goal location
    • clearance int the amount of clearance (i.e the pathing agent size) to consider

    Usage:

      local path = myFinder:getPath(1,1,5,5)

    Returns:

      path a path (array of nodes) when found, otherwise nil
    Pathfinder:reset ()
    Resets the pathfinder . This function is called internally between successive pathfinding calls, so you should not use it explicitely, unless under specific circumstances.

    Usage:

      local path, len = myFinder:getPath(1,1,5,5)

    Returns:

      pathfinder self (the calling pathfinder itself, can be chained)
    generated by LDoc 1.2
    ================================================ FILE: examples/annotatedPathing.lua ================================================ -- Tests sample for clearance metrics calculation -- See Figure 10 at http://aigamedev.com/open/tutorial/clearance-based-pathfinding/ -- Jump Point Search still has some flaws with clearance based pathfinding local Grid = require 'jumper.grid' local PF = require 'jumper.pathfinder' local map = { {0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,1,0}, {0,0,0,0,0,0,0,0,0,0}, {0,0,0,1,0,0,0,0,0,0}, {0,0,1,0,0,0,0,0,2,0}, {0,0,1,1,1,0,0,2,0,0}, {0,0,0,1,1,0,2,0,0,2}, {0,0,0,0,1,0,0,0,0,2}, {0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0} } local grid = Grid(map) local walkable = function(v) return v~=2 end local finder = PF(grid, 'ASTAR',walkable) finder:annotateGrid() local finderNames = PF:getFinders() local sx, sy = 1,1 local ex, ey = 9,9 local agent_size = 2 for i = 1,#finderNames do finder:setFinder(finderNames[i]) local path = finder:getPath(sx, sy, ex, ey, agent_size) print(('Algorithm used: %s - Path %s') :format(finder:getFinder(), path and 'found' or 'not found')) if path then for node, count in path:nodes() do print((' Step %d. (%d,%d)') :format(count, node:getPos())) end end end ================================================ FILE: examples/customHeuristics.lua ================================================ --- Example of use for Heuristics local Grid = require ("jumper.grid") local Pathfinder = require ("jumper.pathfinder") local map = { {0,0,0,0,0,0}, {0,0,0,0,0,0}, {0,1,1,1,1,0}, {0,0,0,0,0,0}, {0,0,0,0,0,0}, } local walkable = 0 local grid = Grid(map) local myFinder = Pathfinder(grid, 'ASTAR', walkable) -- Use Euclidian heuristic to evaluate distance myFinder:setHeuristic('EUCLIDIAN') myFinder:setHeuristic('DIAGONAL') myFinder:setHeuristic('MANHATTAN') -- Custom local h = function(nodeA, nodeB) return (0.1 * (math.abs(nodeA:getX() - nodeB:getX())) + 0.9 * (math.abs(nodeA:getY() - nodeB:getY()))) end myFinder:setHeuristic(h) local p = myFinder:getPath(1,1, 6,5) for node, count in p:nodes() do print(('%d. Node(%d,%d)'):format(count, node:getX(), node:getY())) end print(('Path length: %.2f'):format(p:getLength())) -- etc ... ================================================ FILE: examples/makeClearance.lua ================================================ -- Tests sample for clearance metrics calculation -- See Figure 10 at http://aigamedev.com/open/tutorial/clearance-based-pathfinding/ local Grid = require 'jumper.grid' local PF = require 'jumper.pathfinder' local map = { {0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,1,0}, {0,0,0,0,0,0,0,0,0,0}, {0,0,0,1,0,0,0,0,0,0}, {0,0,1,0,0,0,0,0,2,0}, {0,0,1,1,1,0,0,2,0,0}, {0,0,0,1,1,0,2,0,0,2}, {0,0,0,0,1,0,0,0,0,2}, {0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0} } local grid = Grid(map) local walkable = function(v) return v~=2 end local finder = PF(grid, 'ASTAR',walkable) finder:annotateGrid() for y = 1, #map do local s = '' for x = 1, #map[y] do local node = grid:getNodeAt(x,y) s = (s .. ' ' .. node:getClearance(walkable)) end print(s) end -- Expected output -- 6 6 5 5 4 4 4 3 2 1 -- 6 5 5 4 4 3 3 3 2 1 -- 6 5 4 4 3 3 2 2 2 1 -- 6 5 4 3 3 2 2 1 1 1 -- 6 5 4 3 2 2 1 1 0 1 -- 5 5 4 3 2 1 1 0 1 1 -- 4 4 4 3 2 1 0 2 1 0 -- 3 3 3 3 3 3 3 2 1 0 -- 2 2 2 2 2 2 2 2 2 1 -- 1 1 1 1 1 1 1 1 1 1 ================================================ FILE: examples/simpleExample.lua ================================================ --- Very minimal usage example for Jumper -- Set up a collision map local map = { {0,1,0,1,0}, {0,1,0,1,0}, {0,1,1,1,0}, {0,0,0,0,0}, } -- Value for walkable tiles local walkable = 0 -- Library setup -- Calls the grid class local Grid = require ("jumper.grid") -- Calls the pathfinder class local Pathfinder = require ("jumper.pathfinder") -- Creates a grid object local grid = Grid(map) -- Creates a pathfinder object using Jump Point Search algorithm local myFinder = Pathfinder(grid, 'JPS', walkable) -- Define start and goal locations coordinates local startx, starty = 1,1 local endx, endy = 5,1 -- Calculates the path, and its length local path = myFinder:getPath(startx, starty, endx, endy) -- Pretty-printing the results if path then print(('Path found! Length: %.2f'):format(path:getLength())) for node, count in path:nodes() do print(('Step: %d - x: %d - y: %d'):format(count, node:getX(), node:getY())) end end ================================================ FILE: jumper/core/assert.lua ================================================ -- 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 ================================================ FILE: jumper/core/bheap.lua ================================================ --- A light implementation of Binary heaps data structure. -- While running a search, some search algorithms (Astar, Dijkstra, Jump Point Search) have to maintains -- a list of nodes called __open list__. Retrieve from this list the lowest cost node can be quite slow, -- as it normally requires to skim through the full set of nodes stored in this list. This becomes a real -- problem especially when dozens of nodes are being processed (on large maps). -- -- The current module implements a binary heap -- data structure, from which the search algorithm will instantiate an open list, and cache the nodes being -- examined during a search. As such, retrieving the lower-cost node is faster and globally makes the search end -- up quickly. -- -- This module is internally used by the library on purpose. -- It should normally not be used explicitely, yet it remains fully accessible. -- --[[ Notes: This lighter implementation of binary heaps, based on : https://github.com/Yonaba/Binary-Heaps --]] if (...) then -- Dependency local Utils = require((...):gsub('%.bheap$','.utils')) -- Local reference local floor = math.floor -- Default comparison function local function f_min(a,b) return a < b end -- Percolates up local function percolate_up(heap, index) if index == 1 then return end local pIndex if index <= 1 then return end if index%2 == 0 then pIndex = index/2 else pIndex = (index-1)/2 end if not heap._sort(heap._heap[pIndex], heap._heap[index]) then heap._heap[pIndex], heap._heap[index] = heap._heap[index], heap._heap[pIndex] percolate_up(heap, pIndex) end end -- Percolates down local function percolate_down(heap,index) local lfIndex,rtIndex,minIndex lfIndex = 2*index rtIndex = lfIndex + 1 if rtIndex > heap._size then if lfIndex > heap._size then return else minIndex = lfIndex end else if heap._sort(heap._heap[lfIndex],heap._heap[rtIndex]) then minIndex = lfIndex else minIndex = rtIndex end end if not heap._sort(heap._heap[index],heap._heap[minIndex]) then heap._heap[index],heap._heap[minIndex] = heap._heap[minIndex],heap._heap[index] percolate_down(heap,minIndex) end end -- Produces a new heap local function newHeap(template,comp) return setmetatable({_heap = {}, _sort = comp or f_min, _size = 0}, template) end --- The `heap` class.
    -- This class is callable. -- _Therefore,_ heap(...) _is used to instantiate new heaps_. -- @type heap local heap = setmetatable({}, {__call = function(self,...) return newHeap(self,...) end}) heap.__index = heap --- Checks if a `heap` is empty -- @class function -- @treturn bool __true__ of no item is queued in the heap, __false__ otherwise -- @usage -- if myHeap:empty() then -- print('Heap is empty!') -- end function heap:empty() return (self._size==0) end --- Clears the `heap` (removes all items queued in the heap) -- @class function -- @treturn heap self (the calling `heap` itself, can be chained) -- @usage myHeap:clear() function heap:clear() self._heap = {} self._size = 0 self._sort = self._sort or f_min return self end --- Adds a new item in the `heap` -- @class function -- @tparam value item a new value to be queued in the heap -- @treturn heap self (the calling `heap` itself, can be chained) -- @usage -- myHeap:push(1) -- -- or, with chaining -- myHeap:push(1):push(2):push(4) function heap:push(item) if item then self._size = self._size + 1 self._heap[self._size] = item percolate_up(self, self._size) end return self end --- Pops from the `heap`. -- Removes and returns the lowest cost item (with respect to the comparison function being used) from the `heap`. -- @class function -- @treturn value a value previously pushed into the heap -- @usage -- while not myHeap:empty() do -- local lowestValue = myHeap:pop() -- ... -- end function heap:pop() local root if self._size > 0 then root = self._heap[1] self._heap[1] = self._heap[self._size] self._heap[self._size] = nil self._size = self._size-1 if self._size>1 then percolate_down(self, 1) end end return root end --- Restores the `heap` property. -- Reorders the `heap` with respect to the comparison function being used. -- When given argument __item__ (a value existing in the `heap`), will sort from that very item in the `heap`. -- Otherwise, the whole `heap` will be cheacked. -- @class function -- @tparam[opt] value item the modified value -- @treturn heap self (the calling `heap` itself, can be chained) -- @usage myHeap:heapify() function heap:heapify(item) if self._size == 0 then return end if item then local i = Utils.indexOf(self._heap,item) if i then percolate_down(self, i) percolate_up(self, i) end return end for i = floor(self._size/2),1,-1 do percolate_down(self,i) end return self end return heap end ================================================ FILE: jumper/core/heuristics.lua ================================================ --- 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) return (dx + dy) 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 return sqrt(dx*dx+dy*dy) 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 ================================================ FILE: jumper/core/lookuptable.lua ================================================ 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 ================================================ FILE: jumper/core/node.lua ================================================ --- The Node class. -- The `node` represents a cell (or a tile) on a collision map. Basically, for each single cell (tile) -- in the collision map passed-in upon initialization, a `node` object will be generated -- 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 -- 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(...). -- @type Node local Node = {} Node.__index = Node --- Inits a new `node` -- @class function -- @tparam int x the x-coordinate of the node on the collision map -- @tparam int y the y-coordinate of the node on the collision map -- @treturn node a new `node` -- @usage local node = Node(3,4) function Node:new(x,y) return setmetatable({_x = x, _y = y, _clearance = {}}, Node) end -- Enables the use of operator '<' to compare nodes. -- Will be used to sort a collection of nodes in a binary heap on the basis of their F-cost function Node.__lt(A,B) return (A._f < B._f) end --- Returns x-coordinate of a `node` -- @class function -- @treturn number the x-coordinate of the `node` -- @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() function Node:getY() return self._y 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() function Node:getPos() return self._x, self._y 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. -- @class function -- @treturn node self (the calling `node` itself, can be chained) -- @usage -- local thisNode = Node(1,2) -- thisNode:reset() function Node:reset() self._g, self._h, self._f = nil, nil, nil self._opened, self._closed, self._parent = nil, nil, nil return self end return setmetatable(Node, {__call = function(self,...) return Node:new(...) end} ) end ================================================ FILE: jumper/core/path.lua ================================================ --- The Path class. -- The `path` class is a structure which represents a path (ordered set of nodes) from a start location to a goal. -- An instance from this class would be a result of a request addressed to `Pathfinder:getPath`. -- -- This module is internally used by the library on purpose. -- 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(...). -- @type Path local Path = {} Path.__index = Path --- Inits a new `path`. -- @class function -- @treturn path a `path` -- @usage local p = Path() function Path:new() return setmetatable({_nodes = {}}, Path) end --- Iterates on each single `node` along a `path`. At each step of iteration, -- returns the `node` plus a count value. Aliased as @{Path:nodes} -- @class function -- @treturn node a `node` -- @treturn int the count for the number of nodes -- @see Path:nodes -- @usage -- for node, count in p:iter() do -- ... -- end function Path:iter() local i,pathLen = 1,#self._nodes return function() if self._nodes[i] then i = i+1 return self._nodes[i-1],i-1 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,...) return Path:new(...) end }) end ================================================ FILE: jumper/core/utils.lua ================================================ -- Various utilities for Jumper top-level modules 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 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 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] = Node:new(x,y) end end return nodes, (min_x or 0), (max_x or 0), (min_y or 0), (max_y 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, s) local x, y = x0-s, y0-s coroutine.yield(x, y) repeat x = x + 1 coroutine.yield(x,y) until x == x0+s repeat y = y + 1 coroutine.yield(x,y) until y == y0 + s repeat x = x - 1 coroutine.yield(x, y) until x == x0-s repeat y = y - 1 coroutine.yield(x,y) until y == y0-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() path._grid = finder._grid while true do if node._parent then t_insert(path._nodes,1,node) node = node._parent else t_insert(path._nodes,1,startNode) return path end end end -- Lookup for value in a table local indexOf = function(t,v) for i = 1,#t do if t[i] == v then return i end end return nil end -- Is i out of range 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 } end ================================================ FILE: jumper/grid.lua ================================================ --- The Grid class. -- 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` -- object, and the whole set of nodes are tight inside the `grid` object itself. if (...) then -- Dependencies local _PATH = (...):gsub('%.grid$','') -- 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 = { {x = 1, y = 0} --[[W]], {x = -1, y = 0}, --[[E]] {x = 0, y = 1} --[[S]], {x = 0, y = -1}, --[[N]] } -- 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(...). -- @type Grid 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) return self:getNodeAt(x,y) end PostProcessGrid.__call = function (self,x,y,create) if create then return self:getNodeAt(x,y) end return self._nodes[y] and self._nodes[y][x] end --- Inits a new `grid` -- @class function -- @tparam table|string map A collision map - (2D array) with consecutive indices (starting at 0 or 1) -- 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) end --- Checks if `node` at [x,y] is __walkable__. -- Will check if `node` at location [x,y] both *exists* on the collision map and *is walkable* -- @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)) -- -- -- 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, walkable, clearance) local nodeValue = self._map[y] and self._map[y][x] 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) local nodeClearance = node:getClearance(walkable) hasEnoughClearance = (nodeClearance >= clearance) end if self._eval then return walkable(nodeValue) and hasEnoughClearance end return ((nodeValue == walkable) and hasEnoughClearance) end --- Returns the `grid` width. -- @class function -- @treturn int the `grid` width -- @usage print(myGrid:getWidth()) function Grid:getWidth() return self._width end --- Returns the `grid` height. -- @class function -- @treturn int the `grid` height -- @usage print(myGrid:getHeight()) function Grid:getHeight() 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 -- @usage local nodes = myGrid:getNodes() function Grid:getNodes() return self._nodes end --- Returns the `grid` bounds. Returned values corresponds to the upper-left -- and lower-right coordinates (in tile units) of the actual `grid` instance. -- @class function -- @treturn int the upper-left corner x-coordinate -- @treturn int the upper-left corner y-coordinate -- @treturn int the lower-right corner x-coordinate -- @treturn int the lower-right corner y-coordinate -- @usage local left_x, left_y, right_x, right_y = myGrid:getBounds() function Grid:getBounds() return self._min_x, self._min_y,self._max_x, self._max_y end --- 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 -- 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) local neighbours = {} for i = 1,#straightOffsets do local n = self:getNodeAt( node._x + straightOffsets[i].x, node._y + straightOffsets[i].y ) if n and self:isWalkableAt(n._x, n._y, walkable, clearance) 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,ex,ey) local min_x = lx or self._min_x local min_y = ly or self._min_y local max_x = ex or self._max_x local max_y = ey or self._max_y local x, y y = min_y return function() x = not x and min_x or x+1 if x > max_x then x = min_x y = y+1 end if y > max_y then y = nil end return self._nodes[y] and self._nodes[y][x] or self:getNodeAt(x,y) 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 = node._x, node._y radius = radius or 1 local _around = Utils.around() local _nodes = {} repeat local state, x, y = coroutine.resume(_around,x,y,radius) local nodeAt = state and self:getNodeAt(x, y) 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 = Utils.arrayToNodes(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,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]. -- @class function -- @name Grid:getNodeAt -- @tparam int x the x-coordinate coordinate -- @tparam int y the y-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) return self._nodes[y] and self._nodes[y][x] or nil end -- Gets the node at location on a postprocessed grid function PostProcessGrid:getNodeAt(x,y) if not x or not y 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 not self._nodes[y] then self._nodes[y] = {} end if not self._nodes[y][x] then self._nodes[y][x] = Node:new(x,y) end return self._nodes[y][x] end return setmetatable(Grid,{ __call = function(self,...) return self:new(...) end }) end ================================================ FILE: jumper/pathfinder.lua ================================================ --- The Pathfinder class -- -- Implementation of the `pathfinder` class. local _VERSION = "" local _RELEASEDATE = "" 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 --- 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 --- The `Pathfinder` class.
    -- This class is callable. -- Therefore,_ Pathfinder(...) _acts as a shortcut to_ Pathfinder:new(...). -- @type Pathfinder local Pathfinder = {} Pathfinder.__index = Pathfinder --- Inits a new `pathfinder` -- @class function -- @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 newPathfinder = {} setmetatable(newPathfinder, Pathfinder) newPathfinder:setGrid(grid) newPathfinder:setFinder(finderName) newPathfinder:setWalkable(walkable) newPathfinder:setMode('DIAGONAL') newPathfinder:setHeuristic('MANHATTAN') newPathfinder:setTunnelling(false) 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 -- @tparam int startX the x-coordinate for the starting location -- @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, endX, endY, clearance) self:reset() local startNode = self._grid:getNodeAt(startX, startY) local endNode = self._grid:getNodeAt(endX, endY) assert(startNode, ('Invalid location [%d, %d]'):format(startX, startY)) assert(endNode and self._grid:isWalkableAt(endX, endY), ('Invalid or unreachable location [%d, %d]'):format(endX, endY)) local _endNode = Finders[self._finder](self, startNode, endNode, clearance, 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 -- use it explicitely, unless under specific circumstances. -- @class function -- @treturn pathfinder self (the calling `pathfinder` itself, can be chained) -- @usage local path, len = myFinder:getPath(1,1,5,5) function Pathfinder:reset() for node in pairs(toClear) do node:reset() end toClear = {} return self end -- Returns Pathfinder class Pathfinder._VERSION = _VERSION Pathfinder._RELEASEDATE = _RELEASEDATE return setmetatable(Pathfinder,{ __call = function(self,...) return self:new(...) end }) end ================================================ FILE: jumper/search/astar.lua ================================================ -- Astar algorithm -- This actual implementation of A-star is based on -- [Nash A. & al. pseudocode](http://aigamedev.com/open/tutorials/theta-star-any-angle-paths/) 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) local mCost = Heuristics.EUCLIDIAN(neighbour, node) if node._g + mCost < neighbour._g then neighbour._parent = node neighbour._g = node._g + mCost end end -- Updates vertex node-neighbour local function updateVertex(finder, openList, node, neighbour, endNode, clearance, heuristic, overrideCostEval) local oldG = neighbour._g local cmpCost = overrideCostEval or computeCost cmpCost(node, neighbour, finder, clearance) 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 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 local openList = Heap() startNode._g = 0 startNode._h = heuristic(endNode, startNode) startNode._f = startNode._g + startNode._h openList:push(startNode) toClear[startNode] = true startNode._opened = true while not openList:empty() do 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) 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 end updateVertex(finder, openList, node, neighbour, endNode, clearance, heuristic, overrideCostEval) end end end return nil end end ================================================ FILE: jumper/search/bfs.lua ================================================ -- 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 ================================================ FILE: jumper/search/dfs.lua ================================================ -- Depth-First search algorithm. if (...) then -- Internalization local t_remove = table.remove local function depth_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 LIFO queue (simple array) openList[1] = startNode startNode._opened = true toClear[startNode] = true local node while (#openList > 0) do node = openList[#openList] t_remove(openList) node._closed = true if node == endNode then return node end depth_first_search(finder, openList, node, endNode, clearance, toClear) end return nil end end ================================================ FILE: jumper/search/dijkstra.lua ================================================ -- Dijkstra algorithm (Uses Astar implementation) if (...) then local astar_search = require ((...):gsub('%.dijkstra$','.astar')) -- Dijkstra is similar to aStar, with no heuristic local dijkstraHeuristic = function() return 0 end -- Calculates a path. -- Returns the path from location `` to location ``. return function (finder, startNode, endNode, clearance, toClear) return astar_search(finder, startNode, endNode, clearance, toClear, dijkstraHeuristic) end end ================================================ FILE: jumper/search/jps.lua ================================================ -- Jump Point search algorithm if (...) then -- Dependancies local _PATH = (...):match('(.+)%.search.jps$') local Heuristics = require (_PATH .. '.core.heuristics') local Heap = require (_PATH.. '.core.bheap') -- Internalization local max, abs = math.max, math.abs -- Local helpers, these routines will stay private -- As they are internally used by the public interface -- Resets properties of nodes expanded during a search -- This is a lot faster than resetting all nodes -- between consecutive pathfinding requests --[[ Looks for the neighbours of a given node. Returns its natural neighbours plus forced neighbours when the given node has no parent (generally occurs with the starting node). Otherwise, based on the direction of move from the parent, returns neighbours while pruning directions which will lead to symmetric paths. In case diagonal moves are forbidden, when the given node has no parent, we return straight neighbours (up, down, left and right). Otherwise, we add left and right node (perpendicular to the direction of move) in the neighbours list. --]] local function findNeighbours(finder, node, clearance) if node._parent then local neighbours = {} local x,y = node._x, node._y -- Node have a parent, we will prune some neighbours -- Gets the direction of move local dx = (x-node._parent._x)/max(abs(x-node._parent._x),1) local dy = (y-node._parent._y)/max(abs(y-node._parent._y),1) -- Diagonal move case if dx~=0 and dy~=0 then local walkY, walkX -- Natural neighbours if finder._grid:isWalkableAt(x,y+dy,finder._walkable, clearance) then neighbours[#neighbours+1] = finder._grid:getNodeAt(x,y+dy) walkY = true end if finder._grid:isWalkableAt(x+dx,y,finder._walkable, clearance) then neighbours[#neighbours+1] = finder._grid:getNodeAt(x+dx,y) walkX = true end if walkX or walkY then neighbours[#neighbours+1] = finder._grid:getNodeAt(x+dx,y+dy) end -- Forced neighbours if (not finder._grid:isWalkableAt(x-dx,y,finder._walkable, clearance)) and walkY then neighbours[#neighbours+1] = finder._grid:getNodeAt(x-dx,y+dy) end if (not finder._grid:isWalkableAt(x,y-dy,finder._walkable, clearance)) and walkX then neighbours[#neighbours+1] = finder._grid:getNodeAt(x+dx,y-dy) end else -- Move along Y-axis case if dx==0 then local walkY if finder._grid:isWalkableAt(x,y+dy,finder._walkable, clearance) then neighbours[#neighbours+1] = finder._grid:getNodeAt(x,y+dy) -- Forced neighbours are left and right ahead along Y if (not finder._grid:isWalkableAt(x+1,y,finder._walkable, clearance)) then neighbours[#neighbours+1] = finder._grid:getNodeAt(x+1,y+dy) end if (not finder._grid:isWalkableAt(x-1,y,finder._walkable, clearance)) then neighbours[#neighbours+1] = finder._grid:getNodeAt(x-1,y+dy) end end -- In case diagonal moves are forbidden : Needs to be optimized if not finder._allowDiagonal then if finder._grid:isWalkableAt(x+1,y,finder._walkable, clearance) then neighbours[#neighbours+1] = finder._grid:getNodeAt(x+1,y) end if finder._grid:isWalkableAt(x-1,y,finder._walkable, clearance) then neighbours[#neighbours+1] = finder._grid:getNodeAt(x-1,y) end end else -- Move along X-axis case if finder._grid:isWalkableAt(x+dx,y,finder._walkable, clearance) then neighbours[#neighbours+1] = finder._grid:getNodeAt(x+dx,y) -- Forced neighbours are up and down ahead along X if (not finder._grid:isWalkableAt(x,y+1,finder._walkable, clearance)) then neighbours[#neighbours+1] = finder._grid:getNodeAt(x+dx,y+1) end if (not finder._grid:isWalkableAt(x,y-1,finder._walkable, clearance)) then neighbours[#neighbours+1] = finder._grid:getNodeAt(x+dx,y-1) end end -- : In case diagonal moves are forbidden if not finder._allowDiagonal then if finder._grid:isWalkableAt(x,y+1,finder._walkable, clearance) then neighbours[#neighbours+1] = finder._grid:getNodeAt(x,y+1) end if finder._grid:isWalkableAt(x,y-1,finder._walkable, clearance) then neighbours[#neighbours+1] = finder._grid:getNodeAt(x,y-1) end end end end return neighbours end -- Node do not have parent, we return all neighbouring nodes return finder._grid:getNeighbours(node, finder._walkable, finder._allowDiagonal, finder._tunnel, clearance) end --[[ Searches for a jump point (or a turning point) in a specific direction. This is a generic translation of the algorithm 2 in the paper: http://users.cecs.anu.edu.au/~dharabor/data/papers/harabor-grastien-aaai11.pdf The current expanded node is a jump point if near a forced node In case diagonal moves are forbidden, when lateral nodes (perpendicular to the direction of moves are walkable, we force them to be turning points in other to perform a straight move. --]] local function jump(finder, node, parent, endNode, clearance) if not node then return end local x,y = node._x, node._y local dx, dy = x - parent._x,y - parent._y -- If the node to be examined is unwalkable, return nil if not finder._grid:isWalkableAt(x,y,finder._walkable, clearance) then return end -- If the node to be examined is the endNode, return this node if node == endNode then return node end -- Diagonal search case if dx~=0 and dy~=0 then -- Current node is a jump point if one of his leftside/rightside neighbours ahead is forced if (finder._grid:isWalkableAt(x-dx,y+dy,finder._walkable, clearance) and (not finder._grid:isWalkableAt(x-dx,y,finder._walkable, clearance))) or (finder._grid:isWalkableAt(x+dx,y-dy,finder._walkable, clearance) and (not finder._grid:isWalkableAt(x,y-dy,finder._walkable, clearance))) then return node end else -- Search along X-axis case if dx~=0 then if finder._allowDiagonal then -- Current node is a jump point if one of his upside/downside neighbours is forced if (finder._grid:isWalkableAt(x+dx,y+1,finder._walkable, clearance) and (not finder._grid:isWalkableAt(x,y+1,finder._walkable, clearance))) or (finder._grid:isWalkableAt(x+dx,y-1,finder._walkable, clearance) and (not finder._grid:isWalkableAt(x,y-1,finder._walkable, clearance))) then return node end else -- : in case diagonal moves are forbidden if finder._grid:isWalkableAt(x+1,y,finder._walkable, clearance) or finder._grid:isWalkableAt(x-1,y,finder._walkable, clearance) then return node end end else -- Search along Y-axis case -- Current node is a jump point if one of his leftside/rightside neighbours is forced if finder._allowDiagonal then if (finder._grid:isWalkableAt(x+1,y+dy,finder._walkable, clearance) and (not finder._grid:isWalkableAt(x+1,y,finder._walkable, clearance))) or (finder._grid:isWalkableAt(x-1,y+dy,finder._walkable, clearance) and (not finder._grid:isWalkableAt(x-1,y,finder._walkable, clearance))) then return node end else -- : in case diagonal moves are forbidden if finder._grid:isWalkableAt(x,y+1,finder._walkable, clearance) or finder._grid:isWalkableAt(x,y-1,finder._walkable, clearance) then return node end end end end -- Recursive horizontal/vertical search if dx~=0 and dy~=0 then if jump(finder,finder._grid:getNodeAt(x+dx,y),node,endNode, clearance) then return node end if jump(finder,finder._grid:getNodeAt(x,y+dy),node,endNode, clearance) then return node end end -- Recursive diagonal search if finder._allowDiagonal then if finder._grid:isWalkableAt(x+dx,y,finder._walkable, clearance) or finder._grid:isWalkableAt(x,y+dy,finder._walkable, clearance) then return jump(finder,finder._grid:getNodeAt(x+dx,y+dy),node,endNode, clearance) end end end --[[ Searches for successors of a given node in the direction of each of its neighbours. This is a generic translation of the algorithm 1 in the paper: http://users.cecs.anu.edu.au/~dharabor/data/papers/harabor-grastien-aaai11.pdf Also, we notice that processing neighbours in a reverse order producing a natural looking path, as the pathfinder tends to keep heading in the same direction. In case a jump point was found, and this node happened to be diagonal to the node currently expanded in a straight mode search, we skip this jump point. --]] local function identifySuccessors(finder, openList, node, endNode, clearance, toClear) -- Gets the valid neighbours of the given node -- Looks for a jump point in the direction of each neighbour local neighbours = findNeighbours(finder,node, clearance) for i = #neighbours,1,-1 do local skip = false local neighbour = neighbours[i] local jumpNode = jump(finder,neighbour,node,endNode, clearance) -- : in case a diagonal jump point was found in straight mode, skip it. if jumpNode and not finder._allowDiagonal then if ((jumpNode._x ~= node._x) and (jumpNode._y ~= node._y)) then skip = true end end -- Performs regular A-star on a set of jump points if jumpNode and not skip then -- Update the jump node and move it in the closed list if it wasn't there if not jumpNode._closed then local extraG = Heuristics.EUCLIDIAN(jumpNode, node) local newG = node._g + extraG if not jumpNode._opened or newG < jumpNode._g then toClear[jumpNode] = true -- Records this node to reset its properties later. jumpNode._g = newG jumpNode._h = jumpNode._h or (finder._heuristic(jumpNode, endNode)) jumpNode._f = jumpNode._g+jumpNode._h jumpNode._parent = node if not jumpNode._opened then openList:push(jumpNode) jumpNode._opened = true else openList:heapify(jumpNode) end end end end end end -- Calculates a path. -- Returns the path from location `` to location ``. return function(finder, startNode, endNode, clearance, toClear) startNode._g, startNode._f, startNode._h = 0,0,0 local openList = Heap() openList:push(startNode) startNode._opened = true toClear[startNode] = true local node while not openList:empty() do -- Pops the lowest F-cost node, moves it in the closed list node = openList:pop() node._closed = true -- If the popped node is the endNode, return it if node == endNode then return node end -- otherwise, identify successors of the popped node identifySuccessors(finder, openList, node, endNode, clearance, toClear) end -- No path found, return nil return nil end end ================================================ FILE: jumper/search/thetastar.lua ================================================ -- ThetaStar implementation -- See: http://aigamedev.com/open/tutorials/theta-star-any-angle-paths for reference if (...) then local _PATH = (...):gsub('%.search.thetastar$','') -- Depandancies local Heuristics = require (_PATH .. '.core.heuristics') local astar_search = require (_PATH .. '.search.astar') -- Internalization local ipairs = ipairs local huge, abs = math._huge, math.abs -- Line Of Sight (Bresenham's line marching algorithm) -- http://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm local lineOfSight = function (node, neighbour, finder, clearance) local x0, y0 = node._x, node._y local x1, y1 = neighbour._x, neighbour._y local dx = abs(x1-x0) local dy = abs(y1-y0) local err = dx - dy local sx = (x0 < x1) and 1 or -1 local sy = (y0 < y1) and 1 or -1 while true do if not finder._grid:isWalkableAt(x0, y0, finder._walkable, finder._tunnel, clearance) then return false end if x0 == x1 and y0 == y1 then break end local e2 = 2*err if e2 > -dy then err = err - dy x0 = x0 + sx end if e2 < dx then err = err + dx y0 = y0 + sy end end return true end -- Theta star cost evaluation local function computeCost(node, neighbour, finder, clearance) local parent = node._parent or node local mpCost = Heuristics.EUCLIDIAN(neighbour, parent) if lineOfSight(parent, neighbour, finder, clearance) then if parent._g + mpCost < neighbour._g then neighbour._parent = parent neighbour._g = parent._g + mpCost end else local mCost = Heuristics.EUCLIDIAN(neighbour, node) if node._g + mCost < neighbour._g then neighbour._parent = node neighbour._g = node._g + mCost end end end -- Calculates a path. -- Returns the path from location `` to location ``. return function (finder, startNode, endNode, clearance, toClear, overrideHeuristic) return astar_search(finder, startNode, endNode, clearance, toClear, overrideHeuristic, computeCost) end end ================================================ FILE: rockspecs/jumper-1.6-2.rockspec ================================================ package = "jumper" version = "1.6-2" source = { url = "https://github.com/Yonaba/Jumper/archive/jumper-1.6-2.tar.gz", dir = "Jumper-jumper-1.6-2" } description = { summary = "Fast and easy-to-use pathfinding library for 2D grid-bases games", detailed = [[ Jumper is a pathfinding library designed for uniform-cost 2D grid-based games. It features a mix of A-Star, Jump Point Search and Binary-Heaps. It aims to be fast and lightweight. It also features a clean public interface with chaining features which makes it very friendly and easy to use. ]], homepage = "http://github.com/Yonaba/Jumper", license = "MIT " } dependencies = { "lua >= 5.1" } build = { type = "builtin", modules = { ["init"] = "init.lua", ["jumper"] = "jumper.lua", ["core.bheap"] = "core/bheap.lua", ["core.grid"] = "core/grid.lua", ["core.heuristics"] = "core/heuristics.lua", ["core.node"] = "core/node.lua" } } ================================================ FILE: rockspecs/jumper-1.6.3-1.rockspec ================================================ package = "jumper" version = "1.6.3-1" source = { url = "https://github.com/Yonaba/Jumper/archive/jumper-1.6.3-1.tar.gz", dir = "Jumper-jumper-1.6.3-1" } description = { summary = "Fast and easy-to-use pathfinding library for 2D grid-bases games", detailed = [[ Jumper is a pathfinding library designed for uniform-cost 2D grid-based games. It features a mix of A-Star, Jump Point Search and Binary-Heaps. It aims to be fast and lightweight. It also features a clean public interface with chaining features which makes it very friendly and easy to use. ]], homepage = "http://github.com/Yonaba/Jumper", license = "MIT " } dependencies = { "lua >= 5.1" } build = { type = "builtin", modules = { ["init"] = "init.lua", ["jumper"] = "jumper.lua", ["core.bheap"] = "core/bheap.lua", ["core.grid"] = "core/grid.lua", ["core.heuristics"] = "core/heuristics.lua", ["core.node"] = "core/node.lua" } } ================================================ FILE: rockspecs/jumper-1.7.0-1.rockspec ================================================ package = "jumper" version = "1.7.0-1" source = { url = "https://github.com/Yonaba/Jumper/archive/jumper-1.7.0-1.tar.gz", dir = "Jumper-jumper-1.7.0-1" } description = { summary = "Fast and easy-to-use pathfinding library for 2D grid-based games", detailed = [[ Jumper is a pathfinding library designed for 2D grid-based games. It aims to be fast and lightweight. It also features a clean public interface with chaining features which makes it very friendly and easy to use. ]], homepage = "http://github.com/Yonaba/Jumper", license = "MIT ", maintainer = "Roland Yonaba ", } dependencies = { "lua >= 5.1" } build = { type = "builtin", modules = { ["jumper.init"] = "jumper/init.lua", ["jumper.pathfinder"] = "jumper/pathfinder.lua", ["jumper.core.bheap"] = "jumper/core/bheap.lua", ["jumper.core.grid"] = "jumper/core/grid.lua", ["jumper.core.heuristics"] = "jumper/core/heuristics.lua", ["jumper.core.node"] = "jumper/core/node.lua", ["jumper.search.astar"] = "jumper/search/astar.lua", ["jumper.search.jps"] = "jumper/search/jps.lua", }, copy_directories = {"docs"} } ================================================ FILE: rockspecs/jumper-1.8.0.rockspec ================================================ package = "jumper" version = "1.8.0" source = { url = "https://github.com/Yonaba/Jumper/archive/jumper-1.8.0.tar.gz", dir = "Jumper-jumper-1.8.0" } description = { summary = "Fast and easy-to-use pathfinding library for grid-based games", detailed = [[ Jumper is a pathfinding library designed for grid-based games. It aims to be fast and lightweight. It also features wide range of search algorithms, a clean public interface with chaining features which makes it very friendly and easy to use. ]], homepage = "http://github.com/Yonaba/Jumper", license = "MIT ", maintainer = "Roland Yonaba ", } dependencies = { "lua >= 5.1" } build = { type = "builtin", modules = { ["jumper.grid"] = "jumper/grid.lua", ["jumper.pathfinder"] = "jumper/pathfinder.lua", ["jumper.core.bheap"] = "jumper/core/bheap.lua", ["jumper.core.heuristics"] = "jumper/core/heuristics.lua", ["jumper.core.node"] = "jumper/core/node.lua", ["jumper.core.path"] = "jumper/core/path.lua", ["jumper.search.astar"] = "jumper/search/astar.lua", ["jumper.search.jps"] = "jumper/search/jps.lua", ["jumper.search.dijkstra"] = "jumper/search/dijkstra.lua", ["jumper.search.bfs"] = "jumper/search/bfs.lua", ["jumper.search.dfs"] = "jumper/search/dfs.lua", }, copy_directories = {"docs"} } ================================================ FILE: rockspecs/jumper-1.8.1-1.rockspec ================================================ package = "jumper" version = "1.8.1-1" source = { url = "https://github.com/Yonaba/Jumper/archive/jumper-1.8.1-1.tar.gz", dir = "Jumper-jumper-1.8.1-1" } description = { summary = "Fast and easy-to-use pathfinding library for grid-based games", detailed = [[ Jumper is a pathfinding library designed for grid-based games. It aims to be fast and lightweight. It also features wide range of search algorithms, a clean public interface with chaining features which makes it very friendly and easy to use. ]], homepage = "http://github.com/Yonaba/Jumper", license = "MIT ", maintainer = "Roland Yonaba ", } dependencies = { "lua >= 5.1" } build = { type = "builtin", modules = { ["jumper.grid"] = "jumper/grid.lua", ["jumper.pathfinder"] = "jumper/pathfinder.lua", ["jumper.core.bheap"] = "jumper/core/bheap.lua", ["jumper.core.heuristics"] = "jumper/core/heuristics.lua", ["jumper.core.node"] = "jumper/core/node.lua", ["jumper.core.path"] = "jumper/core/path.lua", ["jumper.search.astar"] = "jumper/search/astar.lua", ["jumper.search.jps"] = "jumper/search/jps.lua", ["jumper.search.dijkstra"] = "jumper/search/dijkstra.lua", ["jumper.search.bfs"] = "jumper/search/bfs.lua", ["jumper.search.dfs"] = "jumper/search/dfs.lua", }, copy_directories = {"docs","specs"} } ================================================ FILE: specs/bheap_specs.lua ================================================ context('Module BHeap', function() local BHeap before(function() BHeap = require ('jumper.core.bheap') end) context('BHeap class', function() test('BHeap() instantiates a new heap object', function() assert_equal(getmetatable(BHeap()), BHeap) end) test('the new heap is empty', function() assert_true((BHeap()):empty()) end) test('items can be pushed inside', function() local h = BHeap() h:push(1):push(2):push(3) assert_equal(h._size, 3) end) test('popping returns the lowest element by default (< operator)', function() local h = BHeap() h:push(1):push(2):push(0) assert_equal(h:pop(),0) assert_equal(h:pop(),1) assert_equal(h:pop(),2) assert_nil(h:pop()) end) test('a heap can be cleared', function() local h = BHeap() assert_true(h:empty()) h:push(1):push(2):push(3) assert_false(h:empty()) h:clear() assert_true(h:empty()) end) test('one can define a custom sort function', function() local sort = function(a,b) return a>b end local h = BHeap(sort) h:push(1):push(2):push(3) assert_equal(h:pop(),3) assert_equal(h:pop(),2) assert_equal(h:pop(),1) assert_nil(h:pop()) end) test('items pushed can be objects, with a custom sort function', function() local sortNode = function(a, b) return a.cost < b.cost end local makeObj = function(cost) return {cost = cost} end local h = BHeap(sortNode) h:push(makeObj(1)):push(makeObj(2)):push(makeObj(3)) assert_equal(h:pop().cost,1) assert_equal(h:pop().cost,2) assert_equal(h:pop().cost,3) assert_nil(h:pop()) end) test('pushing a alue that cannot be compared to the previous ones raises an error', function() local h = BHeap() h:push(false) assert_error(pcall(h.push, h, false)) assert_error(pcall(h.push, h, true)) assert_error(pcall(h.push, h, {})) assert_error(pcall(h.push, h, function() end)) end) test('pushing nil does nothing', function() local h = BHeap() h:push() assert_true(h:empty()) h:push(1):push() assert_false(h:empty()) assert_equal(h._size,1) end) test('popping an empty heap returns nil', function() local h = BHeap() assert_nil(h:pop()) end) test('BHeap:heapify() forces a sort of the heap', function() local h = BHeap() local sort = function(a,b) return a.value < b.value end local function makeObj(v) return {value = v} end local h = BHeap(sort) local A, B, C = makeObj(1), makeObj(2), makeObj(3) h:push(A):push(B):push(C) C.value = 0 h:heapify(C) local ret = h:pop() assert_true(ret == C) assert_equal(ret.value,0) local ret = h:pop() assert_true(ret == A) assert_equal(ret.value,1) local ret = h:pop() assert_true(ret == B) assert_equal(ret.value,2) h:push(A):push(B):push(C) C.value, B.value, A.value = 3, 2, 100 h:heapify() local ret = h:pop() assert_true(ret == B) assert_equal(ret.value,2) local ret = h:pop() assert_true(ret == C) assert_equal(ret.value,3) local ret = h:pop() assert_true(ret == A) assert_equal(ret.value,100) end) end) end) ================================================ FILE: specs/grid_specs.lua ================================================ context('Module Grid', function() local Grid, Node before(function() Grid = require ('jumper.grid') Node = require ('jumper.core.node') end) context('Grid:new() or Grid() returns a new Grid object', function() test('Grid:new() or Grid() returns a new Grid object', function() assert_equal(getmetatable(getmetatable(Grid:new({{0}}))),Grid) assert_equal(getmetatable(getmetatable(Grid({{0}}))),Grid) end) test('Grid:new() requires a collision map upon initialization', function() local map = {{0,0},{0,0}} assert_not_nil(Grid:new(map)) end) test('The passed-in map can be a string', function() local map = '00\n00' assert_not_nil(Grid:new(map)) end) test('passing nil to Grid:new() or Grid() causes an error', function() assert_error(pcall(Grid, Grid)) assert_error(pcall(Grid.new, Grid)) end) test('Grid and map should have the same width', function() local map = '00\n00' local map = {{0,0},{0,0}} local grid = Grid(map) local grid2 = Grid(map) assert_equal(grid:getWidth(), 2) assert_equal(grid2:getWidth(), 2) end) test('Grid and map should have the same height', function() local map = '00\n00\n00' local map2 = {{0,0},{0,0},{0,0},{0,0}} local grid = Grid(map) local grid2 = Grid(map2) assert_equal(grid:getHeight(), 3) assert_equal(grid2:getHeight(), 4) end) test('Grid:getBounds() returns the grid corners coordinates', function() local map = {{0,0,0},{0,0,0},{0,0,0},{0,0,0}} local grid = Grid(map) local lx,ly,rx,ry = grid:getBounds() assert_equal(lx,1) assert_equal(ly,1) assert_equal(rx,3) assert_equal(ry,4) local map = {} for y = 0,2 do map[y] = {} for x = 0,2 do map[y][x] = 0 end end grid = Grid(map) local lx,ly,rx,ry = grid:getBounds() assert_equal(lx,0) assert_equal(ly,0) assert_equal(rx,2) assert_equal(ry,2) end) test('Each value on the map matches a node on the grid', function() local map = {{0,0},{0,0},{0,0},{0,0}} local grid = Grid(map) for y in pairs(map) do for x in pairs(map[y]) do local node = grid:getNodeAt(x,y) assert_not_nil(node) assert_equal(getmetatable(node), Node) end end end) test('the passed-in map should have a width greather than 0',function() assert_error(pcall(Grid, Grid, {{},{}})) assert_error(pcall(Grid, Grid, '\n\n')) end) test('the passed-in map should have a height greather than 0',function() assert_error(pcall(Grid, Grid, {})) assert_error(pcall(Grid, Grid, '')) end) test('the passed-in map should have rows with the same width',function() assert_error(pcall(Grid, Grid, {{0},{0,0}})) assert_error(pcall(Grid, Grid, '00\n000')) end) test('values in the map should only be integers or strings',function() assert_error(pcall(Grid, Grid, {{0.1,0,0},{0,0,0}})) assert_error(pcall(Grid, Grid, {{0,function() end,0},{0,0,0}})) end) end) context('Grid types', function() test('passing a 2nd arg to Grid:new() or Grid() returns a safe-memory grid', function() local grid = Grid({{0}}) local pgrid = Grid({{0}},true) assert_not_equal(getmetatable(grid), getmetatable(pgrid)) assert_equal(getmetatable(getmetatable(grid)), getmetatable(getmetatable(pgrid))) end) test('those grids are memory safe, as nodes are cached on purpose', function() local map = {{0,0,0},{0,0,0},{0,0,0}} local pgrid = Grid(map, true) assert_equal(#pgrid:getNodes(), 0) local count = 0 for node in pgrid:iter() do assert_equal(getmetatable(node), Node) count = count+1 end assert_equal(count, pgrid:getWidth()*pgrid:getHeight()) end) end) context('Grid:isWalkablkeAt', function() test('returns whether or not a node is walkable',function() local map = {{0,0},{0,0},{0,1},{0,0}} local grid = Grid(map) local walkable = 1 for y in pairs(map) do for x in pairs(map[y]) do if map[y][x] == walkable then assert_true(grid:isWalkableAt(x,y,walkable)) else assert_false(grid:isWalkableAt(x,y,walkable)) end end end map = 'WXW\nWWW\n' grid = Grid(map) walkable = 'W' for y = 1,2 do for x = 1,3 do if x==2 and y==1 then assert_false(grid:isWalkableAt(x,y,walkable)) else assert_true(grid:isWalkableAt(x,y,walkable)) end end end end) test('All nodes are walkable when no walkability rule was set', function() local map = {{0,0},{0,0}} local grid = Grid(map) for y in pairs(map) do for x in pairs(map[y]) do assert_true(grid:isWalkableAt(x,y,walkable)) end end end) end) context('Grid:getMap()', function() test('returns the collision map',function() local map = {{0,0},{0,0}} local grid = Grid(map) assert_equal(grid:getMap(), map) end) test('returns the array parsed from a given string',function() local map = '00\n00' local grid = Grid(map) assert_equal(type(grid:getMap()), 'table') assert_not_equal(grid:getMap(), map) end) end) context('Grid:getNodeAt()', function() local map, grid before(function() map = { {0,0,0,0}, {0,0,0,0}, } grid = Grid(map) end) test('returns the node at a given position', function() local node = grid:getNodeAt(1,1) assert_equal(getmetatable(node),Node) assert_equal(node._x,1) assert_equal(node._y,1) end) test('returns nil if the node does not exist', function() assert_nil(grid:getNodeAt(0,0)) assert_nil(grid:getNodeAt(5,1)) end) test('returns nil if one of its args is missing', function() assert_nil(grid:getNodeAt(0)) assert_nil(grid:getNodeAt()) end) end) context('Grid:getNodes()', function() test('returns the array of nodes', function() local map = {{0,0},{0,0}} local grid = Grid(map) local nodes = grid:getNodes() assert_equal(type(nodes), 'table') for y in pairs(nodes) do for x in pairs(nodes[y]) do assert_equal(getmetatable(nodes[y][x]),Node) end end end) end) context('grid:getNeighbours()', function() test('returns neighbours of a given node', function() local map = {{0,0},{0,0},} local grid = Grid(map) local walkable = 0 local node = grid:getNodeAt(1,1) local nb = grid:getNeighbours(node, walkable) assert_equal(type(nb), 'table') assert_equal(#nb, 2) assert_equal(nb[1], grid:getNodeAt(2,1)) assert_equal(nb[2], grid:getNodeAt(1,2)) end) test('passing true as a third arg includes ajacent nodes', function() local map = {{0,0},{0,0},} local grid = Grid(map) local walkable, allowDiag = 0, true local node = grid:getNodeAt(1,1) local nb = grid:getNeighbours(node, walkable, allowDiag) assert_equal(type(nb), 'table') assert_equal(#nb, 3) assert_equal(nb[1], grid:getNodeAt(2,1)) assert_equal(nb[2], grid:getNodeAt(1,2)) assert_equal(nb[3], grid:getNodeAt(2,2)) end) test('passing arg tunnel includes adjacent nodes that cannot be reached diagonnally', function() local map = {{0,0,0},{1,0,0},{0,2,0}} local grid = Grid(map) local walkable, allowDiag, tunnel = 0, true, true local node = grid:getNodeAt(1,3) local nb = grid:getNeighbours(node, walkable, allowDiag, tunnel) assert_equal(type(nb), 'table') assert_equal(#nb, 1) assert_equal(nb[1], grid:getNodeAt(2,2)) end) end) context('Grid:iter()', function() test('iterates on all nodes in a grid', function() local map = { {0,0,0,0}, {0,0,0,0}, {0,0,0,0}, {0,0,0,0}, } local grid = Grid(map) local record = {} for n in grid:iter() do assert_equal(getmetatable(n), Node) assert_not_nil(map[n._y] and map[n._y][n._x]) assert_nil(record[n]) record[n] = true end end) test('can iterate only on a rectangle of nodes', function() local map = { {0,0,0,0}, {0,0,0,0}, {0,0,0,0}, {0,0,0,0}, } local grid = Grid(map) local record = {} for n in grid:iter(2,2,3,3) do assert_equal(getmetatable(n), Node) assert_not_nil(map[n._y] and map[n._y][n._x]) assert_gte(n._x, 2) assert_gte(n._y, 2) assert_lte(n._x, 3) assert_lte(n._y, 3) assert_nil(record[n]) record[n] = true end end) end) context('Grid:around()', function() test('iterates on nodes following a square outline pattern', function() local map = { {0,0,0}, {0,0,0}, {0,0,0} } local m = {{1,1},{2,1},{3,1},{3,2},{3,3},{2,3},{1,3},{1,2}} local grid = Grid(map) local record = {} local i = 0 for n in grid:around(Node(2,2),1) do i = i + 1 assert_equal(getmetatable(n), Node) assert_not_nil(map[n._y] and map[n._y][n._x]) assert_equal(n._x, m[i][1]) assert_equal(n._y, m[i][2]) assert_nil(record[n]) record[n] = true end end) test('arg spacing defaults to 1 when not given', function() local map = { {0,0,0}, {0,0,0}, {0,0,0} } local m = {{1,1},{2,1},{3,1},{3,2},{3,3},{2,3},{1,3},{1,2}} local grid = Grid(map) local record = {} local i = 0 for n in grid:around(Node(2,2)) do i = i + 1 assert_equal(getmetatable(n), Node) assert_not_nil(map[n._y] and map[n._y][n._x]) assert_equal(n._x, m[i][1]) assert_equal(n._y, m[i][2]) assert_nil(record[n]) record[n] = true end end) test('skips unexisting nodes', function() local map = { {0,0,0}, {0,0,0}, {0,0,0} } local m = {{2,1},{2,2},{1,2}} local grid = Grid(map) local record = {} local i = 0 for n in grid:around(Node(1,1)) do i = i + 1 assert_equal(getmetatable(n), Node) assert_not_nil(map[n._y] and map[n._y][n._x]) assert_equal(n._x, m[i][1]) assert_equal(n._y, m[i][2]) assert_nil(record[n]) record[n] = true end end) end) context('Grid:each()', function() test('calls a given function on each node in a grid', function() local map = { {0,0,0,0}, {0,0,0,0}, {0,0,0,0}, {0,0,0,0}, } local grid = Grid(map) local record = {} local function f(node,i) node.value = i end grid:each(f, 3) for n in grid:iter() do assert_equal(getmetatable(n), Node) assert_not_nil(map[n._y] and map[n._y][n._x]) assert_equal(n.value,3) assert_nil(record[n]) record[n] = true end end) end) context('Grid:eachRange()', function() test('calls a given function on each node in a range', function() local map = { {0,0,0,0}, {0,0,0,0}, {0,0,0,0}, {0,0,0,0}, } local grid = Grid(map) local record = {} local function f(node,i) node.value = i end grid:eachRange(1,1,2,2,f,3) for n in grid:iter() do if n._x <= 2 and n._y <= 2 then assert_equal(n.value,3) else assert_nil(n.value) end assert_equal(getmetatable(n), Node) assert_not_nil(map[n._y] and map[n._y][n._x]) assert_nil(record[n]) record[n] = true end end) end) context('Grid:imap()', function() test('Maps a given function on each node in a grid', function() local map = { {0,0,0,0}, {0,0,0,0}, {0,0,0,0}, {0,0,0,0}, } local grid = Grid(map) local record = {} local function f(node,i) node.v = i return node end grid:imap(f, 5) for n in grid:iter() do assert_equal(getmetatable(n), Node) assert_not_nil(map[n._y] and map[n._y][n._x]) assert_equal(n.v,5) assert_nil(record[n]) record[n] = true end end) end) context('Grid:imapRange()', function() test('calls a given function on each node in a range', function() local map = { {0,0,0,0}, {0,0,0,0}, {0,0,0,0}, {0,0,0,0}, } local grid = Grid(map) local record = {} local function f(node,i) node.v = i return node end grid:imapRange(3,3,4,4,f,7) for n in grid:iter() do if n._x >= 3 and n._y >= 3 then assert_equal(n.v,7) else assert_nil(n.v) end assert_equal(getmetatable(n), Node) assert_not_nil(map[n._y] and map[n._y][n._x]) assert_nil(record[n]) record[n] = true end end) end) end) ================================================ FILE: specs/heuristics_specs.lua ================================================ context('Module Heuristics', function() local H, Node before(function() H = require ('jumper.core.heuristics') Node = require ('jumper.core.node') end) context('MANHATTAN distance', function() test('is a function',function() assert_type(H.MANHATTAN, 'function') end) test('evaluates as |dx|+|dy|', function() assert_equal(H.MANHATTAN(Node(0,0), Node(0,0)), 0) assert_equal(H.MANHATTAN(Node(1,1), Node(1,3)), 2) assert_equal(H.MANHATTAN(Node(0,0), Node(2,1)), 3) end) test('calling the function with one arg raises an error', function() assert_error(pcall(H.MANHATTAN,Node(0,0))) end) test('calling the function with no args raises an error', function() assert_error(pcall(H.MANHATTAN)) end) end) context('EUCLIDIAN distance', function() test('is a function',function() assert_type(H.EUCLIDIAN, 'function') end) test('evaluates as SQUAREROOT(dx*dx + dy*dy)', function() assert_equal(H.EUCLIDIAN(Node(0,0), Node(0,0)), 0) assert_equal(H.EUCLIDIAN(Node(0,0), Node(2,2)), math.sqrt(8)) assert_equal(H.EUCLIDIAN(Node(0,0), Node(5,3)), math.sqrt(34)) end) test('calling the function with one arg raises an error', function() assert_error(pcall(H.EUCLIDIAN,Node(0,0))) end) test('calling the function with no args raises an error', function() assert_error(pcall(H.EUCLIDIAN)) end) end) context('DIAGONAL distance', function() test('is a function',function() assert_type(H.DIAGONAL, 'function') end) test('evaluates as MAX(|dx|+|dy|)', function() assert_equal(H.DIAGONAL(Node(0,0), Node(0,0)), 0) assert_equal(H.DIAGONAL(Node(0,0), Node(2,2)), 2) assert_equal(H.DIAGONAL(Node(0,0), Node(1,2)), 2) assert_equal(H.DIAGONAL(Node(0,0), Node(3,1)), 3) end) test('calling the function with one arg raises an error', function() assert_error(pcall(H.DIAGONAL,Node(0,0))) end) test('calling the function with no args raises an error', function() assert_error(pcall(H.DIAGONAL)) end) end) context('CARDINTCARD distance', function() test('is a function',function() assert_type(H.CARDINTCARD, 'function') end) test('evaluates as (SQRT(2)-1)*MIN(|dx|+|dy|)+MAX(|dx|+|dy|)', function() assert_equal(H.CARDINTCARD(Node(0,0), Node(0,0)), 0) assert_less_than(H.CARDINTCARD(Node(0,0), Node(1,1))-(math.sqrt(2)),1e-6) assert_less_than(H.CARDINTCARD(Node(0,0), Node(1,2))-(1+math.sqrt(2)),1e-6) assert_less_than(H.CARDINTCARD(Node(0,0), Node(-3,1))-(2+math.sqrt(2)),1e-6) assert_less_than(H.CARDINTCARD(Node(0,0), Node(2,2))-(2*math.sqrt(2)),1e-6) end) test('calling the function with one arg raises an error', function() assert_error(pcall(H.CARDINTCARD,Node(0,0))) end) test('calling the function with no args raises an error', function() assert_error(pcall(H.CARDINTCARD)) end) end) end) ================================================ FILE: specs/node_specs.lua ================================================ context('Module Node', function() local Node before(function() Node = require ('jumper.core.node') end) context('The Node Class', function() test('Node:new() or Node() returns a Node object',function() assert_equal(getmetatable(Node:new(0,0)),Node) assert_equal(getmetatable(Node(0,0)),Node) end) test('A Node has x and y attributes', function() local node = Node:new(1,3) assert_equal(node._x, 1) assert_equal(node._y, 3) end) test('x and y attributes can be retrieved through methods', function() local node = Node:new(5,6) assert_equal(node:getX(), 5) assert_equal(node:getY(), 6) local x, y = node:getPos() assert_equal(x, 5) assert_equal(y, 6) end) test('Nodes can be compared, if they both have an F-cost', function() local nodeA, nodeB = Node(1,2), Node(1,2) nodeA._f, nodeB._f = 1, 2 assert_less_than(nodeA, nodeB) nodeA._f = 3 assert_less_than(nodeB, nodeA) end) end) end) ================================================ FILE: specs/path_specs.lua ================================================ context('Module Path', function() local Path, Node before(function() Path = require ('jumper.core.path') Node = require ('jumper.core.node') end) context('The Path Class', function() test('Path:new() or Path() returns a Path object',function() assert_equal(getmetatable(Path:new()),Path) assert_equal(getmetatable(Path()),Path) end) test('Path:iter() iterates on nodes forming the path',function() local p = Path() for i = 1,10 do p._nodes[#p._nodes+1] = Node(i,i) end local i = 0 for node, count in p:iter() do i = i+1 assert_equal(getmetatable(node), Node) assert_equal(node._x, i) assert_equal(node._y, i) assert_equal(count, i) end end) test('Path:iter() is aliased as Path:nodes()',function() assert_equal(Path.iter, Path.nodes) end) test('Path:getLength() returns the length of the path', function() local p = Path() for i = 1,10 do p._nodes[#p._nodes+1] = Node(i,0) end assert_equal(p:getLength(),9) p = Path() for j = 1,10 do p._nodes[#p._nodes+1] = Node(0,j) end assert_equal(p:getLength(),9) p = Path() for i = 1,10 do p._nodes[#p._nodes+1] = Node(i,i) end assert_less_than(p:getLength()-9*math.sqrt(2),1e-6) end) test('Path:fill() interpolates a path', function() local p = Path() for i = 1,9,2 do p._nodes[#p._nodes+1] = Node(i,i) end p._grid = {getNodeAt = function(self,x,y) return {_x = x, _y = y} end} p:fill() local i = 0 for node, count in p:iter() do i = i+1 assert_equal(node._x, i) assert_equal(node._y, i) assert_equal(count, i) end end) test('Interpolation does not affect the total path length', function() local p = Path() for i = 1,10,3 do p._nodes[#p._nodes+1] = Node(i,i) end local len = p:getLength() p._grid = {getNodeAt = function(self,x,y) return {_x = x, _y = y} end} p:fill() assert_less_than(p:getLength()-len,1e-6) end) test('Path:filter() compresses a path', function() local p = Path() for i = 1,10 do p._nodes[#p._nodes+1] = Node(i,i) end p:filter() assert_equal(p._nodes[1]._x,1) assert_equal(p._nodes[1]._y,1) assert_equal(p._nodes[2]._x,10) assert_equal(p._nodes[2]._y,10) for i = 3,10 do assert_nil(p._nodes[i]) end end) test('Compression does not affect the total path length', function() local p = Path() for i = 1,10 do p._nodes[#p._nodes+1] = Node(i,i) end local len = p:getLength() p:fill() assert_less_than(p:getLength()-len,1e-6) end) end) end) ================================================ FILE: specs/pathfinder_specs.lua ================================================ context('Module Pathfinder', function() local PF, H, Grid, Path, map, grid before(function() PF = require ('jumper.pathfinder') Grid = require ('jumper.grid') H = require ('jumper.core.heuristics') Path = require ('jumper.core.path') map = {{0,0,0},{0,0,0},{0,0,0}} grid = Grid(map) end) context('Pathfinder:new() or Pathfinder()', function() test('Inits a new Pathfinder object', function() assert_equal(getmetatable(PF(grid, 'ASTAR')), PF) assert_equal(getmetatable(PF:new(grid, 'ASTAR')), PF) end) test('First arg is a grid object', function() assert_error(pcall(PF, PF)) assert_error(pcall(PF, PF, map)) assert_equal(getmetatable(PF(grid)), PF) end) test('Second arg, when given must be a valid finderName', function() assert_error(pcall(PF, PF, grid, 'finder')) for i, finder in ipairs(PF:getFinders()) do assert_equal(getmetatable(PF(grid, finder)), PF) end end) test('Defaults to \'ASTAR\' when not given', function() local pf = PF(grid) assert_equal(getmetatable(pf), PF) assert_equal(pf:getFinder(), 'ASTAR') end) test('Third arg walkable can be a string, function, int or nil', function() assert_equal(getmetatable(PF(grid, 'ASTAR', 'A')), PF) assert_equal(getmetatable(PF(grid, 'ASTAR', function() end)), PF) assert_equal(getmetatable(PF(grid, 'ASTAR', 1)), PF) assert_equal(getmetatable(PF(grid, 'ASTAR', nil)), PF) assert_error(pcall(PF, PF, grid, 'ASTAR', 2.2)) assert_error(pcall(PF, PF, grid, 'ASTAR', {})) end) end) context('Pathfinder:getGrid()', function() test('returns the grid object used by the Pathfinder', function() local pf = PF(grid) assert_equal(pf:getGrid(), grid) end) end) context('Pathfinder:setGrid()', function() test('Sets the grid object on which the Pathfinder performs', function() local pf = PF(grid) local newGrid = Grid('00000\n00000') assert_equal(pf:getGrid(), grid) pf:setGrid(newGrid) assert_equal(pf:getGrid(), newGrid) end) test('passing nil raises an error', function() local pf = PF(grid) assert_error(pcall(pf.setGrid, pf, nil)) end) end) context('Pathfinder:getWalkable()', function() test('returns the walkable parameter', function() local pf = PF(grid, 'ASTAR', 1) assert_equal(pf:getWalkable(), 1) end) test('is nil when not given', function() local pf = PF(grid) assert_nil(pf:getWalkable()) end) end) context('Pathfinder:setWalkable()', function() test('sets the string, function, nil or int walkable value', function() local pf = PF(grid, 'ASTAR') assert_nil(pf:getWalkable()) pf:setWalkable('A') assert_equal(pf:getWalkable(), 'A') pf:setWalkable(2) assert_equal(pf:getWalkable(), 2) local f = function() end pf:setWalkable(f) assert_equal(pf:getWalkable(), f) end) test('is nil when not given', function() local pf = PF(grid) assert_nil(pf:getWalkable()) end) test('raises an error when passed-in value is not a string, int, nil or function', function() local pf = PF(grid) assert_error(pcall(pf.setWalkable, pf, {})) assert_error(pcall(pf.setWalkable, pf, 0.4)) end) end) context('Pathfinder:getFinder()', function() test('returns the finder name used', function() local pf = PF(grid, 'JPS') assert_equal(pf:getFinder(), 'JPS') end) end) context('Pathfinder:setFinder()', function() test('sets the finder to be used', function() local pf = PF(grid) pf:setFinder('DFS') assert_equal(pf:getFinder(), 'DFS') end) test('Upon init, the default finder, when not given, is \'ASTAR\'', function() local pf = PF(grid) assert_equal(pf:getFinder(), 'ASTAR') end) test('Passing nil sets \'ASTAR\` as the finder if no previous finder was set, is \'ASTAR\'', function() local pf = PF(grid) pf:setFinder() assert_equal(pf:getFinder(), 'ASTAR') end) test('Passing nil has no effect if a finder was set previously', function() local pf = PF(grid, 'JPS') pf:setFinder() assert_equal(pf:getFinder(), 'JPS') end) end) context('Pathfinder:getFinders()', function() test('returns the list of all existing finders', function() local fs = PF:getFinders() local pf = PF(grid) assert_greater_than(#fs, 0) for i,finder in ipairs(fs) do pf:setFinder(finder) assert_equal(pf:getFinder(), finder) end end) end) context('Pathfinder:getHeuristic()', function() test('returns the heuristic function used', function() local pf = PF(grid) assert_not_nil(pf:getHeuristic()) end) test('default heuristic is \'MANHATTAN\'', function() local pf = PF(grid) assert_equal(pf:getHeuristic(), H.MANHATTAN) end) end) context('Pathfinder:setHeuristic()', function() test('sets the heuristic function to be used', function() local pf = PF(grid) pf:setHeuristic('MANHATTAN') assert_equal(pf:getHeuristic(), H.MANHATTAN) end) test('handles custom heuristic functions', function() local pf = PF(grid) local f = function() end pf:setHeuristic(f) assert_equal(pf:getHeuristic(),f) end) test('passing nil produces an error',function() local pf = PF(grid) assert_error(pcall(pf.setHeuristic, pf)) end) end) context('Pathfinder:getHeuristics()', function() test('returns all available heuristics', function() local hs = PF:getHeuristics() assert_greater_than(#hs, 0) local pf = PF(grid) for i, heur in ipairs(hs) do pf:setHeuristic(heur) assert_equal(pf:getHeuristic(), H[heur]) end end) end) context('Pathfinder:getMode()', function() test('returns the actual search mode', function() local pf = PF(grid) pf:setMode('DIAGONAL') assert_equal(pf:getMode(),'DIAGONAL') end) test('default search mode is \'DIAGONAL\'', function() local pf = PF(grid) assert_equal(pf:getMode(),'DIAGONAL') end) end) context('Pathfinder:setMode()', function() test('sets the search mode', function() local pf = PF(grid) pf:setMode('ORTHOGONAL') assert_equal(pf:getMode(), 'ORTHOGONAL') pf:setMode('DIAGONAL') assert_equal(pf:getMode(), 'DIAGONAL') end) test('passing nil or any other invalid arg causes an error', function() local pf = PF(grid) assert_error(pcall(pf.setMode, pf)) assert_error(pcall(pf.setMode, pf, 'ORTHO')) assert_error(pcall(pf.setMode, pf, function() end)) end) end) context('Pathfinder:getModes()', function() test('returns all available modes', function() local ms = PF:getModes() assert_equal(#ms, 2) local pf = PF(grid) for i, mode in ipairs(ms) do pf:setMode(mode) assert_equal(pf:getMode(),mode) end end) end) context('Pathfinder:setTunnelling()', function() test('Enables or disables tunnelling feature', function() PF:setTunnelling(true) assert_true(PF:getTunnelling()) PF:setTunnelling(false) assert_false(PF:getTunnelling()) end) test('Enables or disables tunnelling feature', function() PF:setTunnelling(true) assert_true(PF:getTunnelling()) PF:setTunnelling(false) assert_false(PF:getTunnelling()) end) test('When on, finder goes through walls heading diagonally', function() local map = {{0,0,0},{1,0,0},{0,2,0}} local grid = Grid(map) local finder = PF(grid, 'ASTAR',0) finder:setTunnelling(true) local path = finder:getPath(1,3,3,1) assert_equal(path._nodes[1]._x,1) assert_equal(path._nodes[1]._y,3) assert_equal(path._nodes[2]._x,2) assert_equal(path._nodes[2]._y,2) assert_equal(path._nodes[3]._x,3) assert_equal(path._nodes[3]._y,1) end) end) context('Pathfinder:getTunnelling()', function() test('Returns the actual state of tunnelling feature', function() assert_false(PF:getTunnelling()) end) end) context('Pathfinder:annotateGrid()', function() test('Calculates clearance for the entire grid', function() local map = { {0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,1,0}, {0,0,0,0,0,0,0,0,0,0}, {0,0,0,1,0,0,0,0,0,0}, {0,0,1,0,0,0,0,0,2,0}, {0,0,1,1,1,0,0,2,0,0}, {0,0,0,1,1,0,2,0,0,2}, {0,0,0,0,1,0,0,0,0,2}, {0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0} } local clearances = { {6,6,5,5,4,4,4,3,2,1}, {6,5,5,4,4,3,3,3,2,1}, {6,5,4,4,3,3,2,2,2,1}, {6,5,4,3,3,2,2,1,1,1}, {6,5,4,3,2,2,1,1,0,1}, {5,5,4,3,2,1,1,0,1,1}, {4,4,4,3,2,1,0,2,1,0}, {3,3,3,3,3,3,3,2,1,0}, {2,2,2,2,2,2,2,2,2,1}, {1,1,1,1,1,1,1,1,1,1} } local grid = Grid(map) local walkable = function(v) return v~=2 end local finder = PF(grid, 'ASTAR', walkable) finder:annotateGrid() for node in grid:iter() do assert_equal(node:getClearance(walkable), clearances[node._y][node._x]) end end) end) context('Pathfinder:clearAnnotations()', function() test('Clears cached clearance values for the entire grid', function() local map = {{0,1,0},{0,0,0},{1,1,0}} local grid = Grid(map) local walkable = 0 local finder = PF(grid, 'ASTAR', walkable) finder:annotateGrid() finder:clearAnnotations() for node in grid:iter() do assert_nil(node:getClearance(walkable)) end end) end) context('Pathfinder:getPath()', function() test('returns a path', function() local pf = PF(grid, 'ASTAR', 0) local path = pf:getPath(1,1,3,3) assert_equal(getmetatable(path), Path) end) test('start and end locations must exist on the map', function() local pf = PF(grid, 'ASTAR', 0) assert_error(pcall(pf.getPath, pf, 0,0, 3, 3)) assert_error(pcall(pf.getPath, pf, 1, 1, 4, 4)) assert_error(pcall(pf.getPath, pf, 0,0, 4, 4)) end) test('goal location must be walkable', function() local pf = PF(grid, 'ASTAR', 0) map[3][3] = 1 assert_error(pcall(pf.getPath, pf, 0,0, 3, 3)) end) end) end) ================================================ FILE: version_history.md ================================================ #Version history# ##1.8.1 (03/01/2013) * Added optionnal `tunneling` feature (not fully compatible with `Jump Point Search` as of now) * Fixed path request failure when stepping from an unwalkable location * Fixed `getPath()` to keep continuously failing right after an wrong path request * Fixed _PATH for compatibility with handheld devices * Added handling for nil values pushed into heaps * Added `Node` as a syntactic shortcut to `Node:new(...)` * Added type & validity checking for grid objects * Added type & validity checking for passed-in maps * Changed pathfinder initialization args order * `PathFinder:setFinder()` now handles nil * New implementation of Astar, reused internally for Dijkstra Algorithm * Added Telescope specs tests * Added Travis-CI validation ##1.8.0 (01/26/2013) * Moved the internal `Grid` module at the top level * Separated path handling logic from the pathfinder class * Added a new `Path` class * Moved Pathfinder:filter and Pathfinder:fill to Path:filter and Path:fill * Changed Pathfinder:new args, to handle the explicit choice of a finder. * Added Pathfinder:setGrid * Added Pathfinder:getGrid * Added Pathfinder:setWalkable * Added Pathfinder:getWalkable * Changed Grid:isWalkableAt to handle a third-parameter for node walkability testing * Added Grid:getWidth * Added Grid:getHeight * Added Grid:getMap * Added Grid:getNodes * Added Grid:getNodes * Added Path:getLength for the `Path` class, for path length self-evaluation, as it fails with finders not handling heuristics. * Added Dijkstra algorithm * Added Breadth-First search algorithm * Added Depth-First search algorithm * Updated README and documentation ##1.7.0 (01/22/13) * Added Astar search algorithm, along with Jump Point Search * Implemented a common interface for the Pathfinder object * Added argument type checking on pathfinder initialization * Added Pathfinder:setFinder * Added Pathfinder:getFinder * Added Pathfinder:getFinders * Added Pathfinder:getHeuristics * Added Pathfinder:getModes * Added Pathfinder:filter for path compression * Removed autoFill feature (setAutoFill, getAutoFill) * Faster heapify method in binary heaps module * Updated docs, README, rockspecs ## 1.6.3 (01/19/13) * Added Grid:iter * Added Grid:each * Added Grid:eachRange * Added Grid:imap * Added Grid:imapRange * Added Grid:__call * Added Pathfinder:version * Added path iterator * Improved node passability handling * Added support for string maps * Various code improvements * Hardcoded documentation, generation with LDoc * Updated README, rockspecs ## 1.6.2 (12/01/12) * Third-party lib 30log replaced by an hardocded class system * Third-party lib binary-heaps replaced by a lighter implementation * Changed initialization pattern : three-args are needed, only the first one is mandatory. * Added support for custom heuristics * Removed getDiagonalMoves() and setDiagonalMoves(), replaced by getMode() and setMode() * Internal improvements, reuse data. * Updated Readme ## 1.6.1 (11/22/12) * Added Cardinal/Intercardinal heuristic ## 1.6.0 (11/05/12) * Added specialized grids : preprocessed/postprocessed grids * Nodes walkability is no longer stored as an attribute, but computed on the fly with respect to the map passed to init Jumper ##1.5.2.2 (11/02/12) * Bugfix on resetting nodes properties (Thanks to Srdjan Marković) * Bugfix on path cost return ##1.5.2.1 (10/27/12) * Bugfix (Thanks to Srdjan Marković) ##1.5.2 (10/25/12) * Fixed "tunneling" issue in diagonal-mode ##1.5.1.3 (10/18/12) * Third-party 30log requiring enhanced * Huge documentation update (See Readme) ##1.5.1.2 (10/17/12) - Fix * Bugfix with the previous commit (requiring third-party would not work with Lve2D, now fixed) ##1.5.1.2 (10/16/12) * Fix on internal grid width calculation * Added path to 30log in package.path * Some code cleaning ##1.5.1.1 (10/15/12) * Smoothing renamed to filling, self-explanatory (See Readme for public interface changes) ##1.5.1 (10/09/12) * Fix for pathfinding with no diagonal moves allowed : paths returned looks more "natural". ##1.5.0 (10/06/12) * Added support for collision maps starting at locations different than (1,1). * Heuristic name CHEBYSHEV was removed, now on will use DIAGONAL instead. * Changes in Jumper's initialization arguments * Various improvements * Updated Readme ##1.4.1 (10/04/12) * Third-parties are now git submodules. * Bugfix with grid reset process * Optimized the grid reset process. Successive calls to pather:getPath() yield faster. * Removed grid:reset() ##1.3.3 (10/01/12) * Removed useless lines of code ##1.3.2 (09/26/12) * Compatibility issue with Gideros : Jumper couldn't be required, due to the way Gideros run projects. * Updated Readme ##1.3.1 (09/25/12) * Jumper no longer uses internally Lua's module function. * Global env pollution bugfix ##1.3 (09/25/12) * added autoSmooth feature : returned paths can now be automatically smoothered on return * searchPath renamed to getPath * Added chaining * Slight enhancements in code, making profit of Lua's multiple return values ability * Updated Readme * Updated demos ##1.2 (08/28/12) * Jumper now uses [30log](http://github.com/Yonaba/30log) as its object orientation library * Global env pollution when requiring jumper now fixed (See init.lua) * Updated Readme ##1.1.1 (08/27/12) * Third party updated (Binary_Heaps v1.5) * Code cleaning, Fixed indentation ##1.1 (08/01/12) * Updated with third-party (with Binary_Heaps ported to 1.4) ##1.0 (06/14/12) * Added Path smoother * Better handling of straight moves * Code cleaning ##0.3 (06/01/12) * Bugfix with internal paths calls to third-parties. ##0.2 (05/28/12) * Updated third-party libraries (Lua Class System, Binary Heaps) * Added version_history.md ##0.1 (05/26/12) * Initial release