taxiDriverAI - Germanunkol/trAInsported GitHub Wiki

taxiDriver-AI by maxxst / FU-Berlin

-- taxiDriver1.lua

--require '_utilities/utilities.lua'	-- moved to end of file
--require '_jumper/jumper.lua'

AI ="taxiDriver by maxxst"  -- FU-Berlin
-- Example-AI from https://github.com/maxxst/trAInsported-starterkit / 2013-07
-- All-in-1-file, translation, added comments, TODOs & debug:  hajo4  2014-02-26

local dbg=0

--
-- Without a passenger, the TaxiDriver-Bot drives randomly around the map.
-- When encountering passengers, he picks up the first, then searches 
-- the shortest path to its destination.
--

-- TODO: ai.passengerBoarded(), ai.enoughMoney(), ai.blocked()
-- TODO: use getNumberOfLines() to check cpu-quota on bigger maps
--  Note: on "Compete"-maps as small as 12x10, we get errors "Taking too long"
-- TODO: Do some of the obvious improvements :-)

-- #### #### ####

railMap = {}

-- Gets called at the start of the game.  Buy a train here!
function ai.init(map, money, maximumTrains)
	print('AI:', AI)
	railMap = map2railMap(map)

	if dbg >= 5 then  
		print('railMap:')
		printTable(railMap,2)  
		print("used codelines A:", getNumberOfLines() )  -- check cpu-quota
	end

    -- init. pathfinding-library:
    jumperMap = mapToJumperMap(map)
    local walkable = 1
    local Grid = Jumper.Grid  
    local Pathfinder = Jumper.Pathfinder 
    local grid = Grid(jumperMap) 
    myFinder = Pathfinder(grid, 'BFS', walkable)

    if dbg >= 5 then  print("used codelines B:", getNumberOfLines() )  end

    --TODO: buy train near passenger
    buyTrain(random(map.width), random(map.height))
end

-- Gets called when the train reaches a tile with passengers:
function ai.foundPassengers( train, passengers )
    local p = nil 
    if train.passenger then
        -- if the train is full, dont stop.
        -- TODO: look for VIPs
	-- TODO: remember passengers
        myPrint(5,'Train is full !')
    else
        p = passengers[1]			--take first passenger from list
        myPrint(3,"pickup:", p.name)
	-- TODO: choose the "best" passenger
    end  
    return p
end

-- Gets called when the train reaches the tile in front of a junction.
-- Should return one of the possible directions ("N", "S", "W", "E").
function ai.chooseDirection(train, possibleDirections)

    local dir = chooseRandomDir(possibleDirections) 	-- without passenger: random move
    if train.passenger then				-- with passenger: take shortest path to dest.
        -- calculate best direction
        local startx, starty = train.x ,train.y
        local endx, endy = train.passenger.destX, train.passenger.destY
        local path = myFinder:getPath(startx, starty, endx, endy) 
        if not (endx == path[2].x and endy == path[2].y) then -- avoid error
          kreuzung = makeLoc( path[2].x, path[2].y, train.dir)
          nachKreuzung = makeLoc( path[3].x, path[3].y, train.dir )
          for d, bool in pairs(possibleDirections) do
            local test_loc = goDir( railMap, kreuzung, d )
            if test_loc.x == nachKreuzung.x and test_loc.y == nachKreuzung.y then
              dir = d 
              break 
            end
          end
        end
    end

    if dbg >= 5 then  print("used codelines C:", getNumberOfLines() )  end

    return dir
end

-- Gets called  when a new passenger gets placed on the map:
function ai.newPassenger(name, x, y, destX, destY, vipTime)
    myPrint(1,"new passenger at ("..x..", "..y..")")
    --TODO: remember passengers
end

-- Gets called when the train arrives at the destination:
function ai.foundDestination( train )
	myPrint(3,"drop")
	dropPassenger(train)
	print("Money: " .. getMoney())
end

function chooseRandom(train, possibleDirections)
  tbl = {}
  for dir,bool in pairs(possibleDirections) do
    tbl[#tbl+1] = dir
  end
  return tbl[random(#tbl)]
end 

function mapToJumperMap(map)
  local jumperMap = {}
  for i=1, map.width do
      for j=1, map.height do
          if not jumperMap[j] then
           jumperMap[j] = {}
          end
          if map[i][j] == 'C' then
              jumperMap[j][i] = 1
          else
              jumperMap[j][i] = 0
          end
      end
  end
  myPrint(5,'jumperMap:')
  if dbg>=5 then  printTable(jumperMap,2)  end
  return jumperMap
end

--Debug-print:
function myPrint(dbgLevel, ...)
	if dbg >= dbgLevel then  print(...)  end
end

-- #### #### #### ####

--[[
For running the AI in matches on the online server,
the include-files need to be appended directly:

--require '_utilities/utilities.lua'
--require '_jumper/jumper.lua'
]]--

-- utilities.lua
function map2railMap(map)
    local railMap = {}
    for i=1, map.width do
        railMap[i] = {}
        for j=1, map.height do
            if map[i][j] == "C" then
                railMap[i][j] = true
            else
                railMap[i][j] = false
            end
        end
    end
    railMap.width  = map.width
    railMap.height = map.height
    return railMap
end

function getDirections( railMap, loc)
    x = loc.x
    y = loc.y
    dir = loc.dir
    local directions = {}
    -- only check if x y is actually a rail
    if railMap[x][y] then
        -- lookup: possible directions
        if y > 1 and railMap[x][y-1] and dir ~= 'S'  then 
            table.insert(directions, "N")
        end
        if y < railMap.height and railMap[x][y+1] and dir ~= 'N' then
            table.insert(directions, "S")
        end
        if x > 1 and railMap[x-1][y] and dir ~= 'E' then 
            table.insert(directions, "W")
        end
        if x < railMap.width and railMap[x+1][y] and dir ~= 'W' then 
            table.insert(directions, "E")
        end
    end
    return directions
end

function chooseRandomDir( directions )
    local tbl = {}
    for dir,bool in pairs(directions) do
        tbl[#tbl+1] = dir
    end
    return tbl[random(#tbl)]
end

function depthFirstSearch( level, railMap, location, target  )
    max_level = 30 -- tinker with this, maybe you can dig deeper! ;)
    if max_level == level then
        return "TOOLONG"
    else
        if location.x == target.x and location.y == target.y then
            return {}
        else
            local path = {}
            local min = max_level+100
            local dirs = getDirections( railMap, location )
            for index, dir in pairs(dirs) do 
                local new_loc = goDir( railMap, location, dir )
                local res = depthFirstSearch( level+1, railMap, new_loc, target )
                if res ~= "TOOLONG" then
                    table.insert(res, {["x"]=location.x, ["y"]=location.y, ["dir"]=dir})
                    if #res < min then
                        path = res
                        min = #path
                    end
                end
            end
            return path
        end
    end
end

function goDir( railMap, loc, dir )
    h = railMap.height
    w = railMap.width
    x = loc.x
    y = loc.y
    if dir == "S" then y = y+1 end
    if dir == "N" then y = y-1 end
    if dir == "W" then x = x-1 end
    if dir == "E" then x = x+1 end
    if x > w or y > h or x < 1 or y < 1 then
        print("goDir: out of range "..x.." "..y)
    else
        return {["x"]=x, ["y"]=y, ["dir"]=dir}
    end
end 

function loc2String( loc )
    return loc.x.." "..loc.y.." "..loc.dir
end

function makeLoc( x, y, dir)
    return { ["x"]=x, ["y"]=y, ["dir"]=dir }
end

function dir2String( map, loc )
    x = loc.x
    y = loc.y
    dir = loc.dir
    dirs = getDirections( map, loc )
    str = "loc("..x.." "..y.." "..dir..") -> "
    if dirs[1] then
        str = str..dirs[1].." "
    end
    if dirs[2] then
        str = str..dirs[2].." "
    end
    if dirs[3] then
        str = str..dirs[2].." "
    end
    if dirs[4] then
        str = str..dirs[2].." "
    end
    return str
end

function manhattanDist(x1, y1, x2, y2)
    return math.abs(x1-x2) + math.abs(y1-y2)
end

-- Ausgabehelfer
-- Function to print a Table
function printTable(table, lvl)
  if type(table) == "table" then
    lvl = lvl or 0
    for k, v in pairs(table) do
      if type(v) == "table" then
        printTable(v, lvl + 1)
      else
        str = ""
        for i = 1,lvl do
          str = str .. "\t"
        end
        print(str, k, v)
      end
    end
  else
    print('Not a table')
  end
end

-- #### #### ####


-- jumper.lua
-- Raw source, to be prettified later

-- Defining core modules
------------------------------  Binary Heaps (local) ------------------------------
-- See: 
-- * http://en.wikipedia.org/wiki/Binary_heap
-- * http://www.policyalmanac.org/games/binaryHeaps.htm

local floor = math.floor

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

-- Default comparison function
local function f_min(a,b)
  	return a.f < b.f
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

local Heap = function()

	local newHeap = {
		__heap = {},
		size = 0,
		sort = comp or f_min,
	}

	function newHeap:empty()
		return (self.size==0)
	end
	
	function newHeap:clear()
		self.__heap = {}
		self.size = 0
		self.sort = self.sort or f_min
		return self
	end

	function newHeap:push(item)
		if item then
			self.size = self.size + 1
			self.__heap[self.size] = item
			percolate_up(self, self.size)
		end
		return self
	end

	function newHeap: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

	function newHeap:heapify(item)
		if item then
			local i = 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 newHeap
	
end

------------------------------  Heuristics (global) ------------------------------

local abs   = math.abs
local sqrt  = math.sqrt
local sqrt2 = sqrt(2)
local max, min = math.max, math.min

local Heuristics = {}

function Heuristics.MANHATTAN(dx,dy) return abs(dx)+abs(dy) end
function Heuristics.EUCLIDIAN(dx,dy) return sqrt(dx*dx+dy*dy) end
function Heuristics.DIAGONAL(dx,dy)  return max(abs(dx),abs(dy)) end
function Heuristics.CARDINTCARD(dx,dy) 
	dx, dy = abs(dx), abs(dy)
	return min(dx,dy) * sqrt2 + max(dx,dy) - min(dx,dy)
end

------------------------------  Node (global) ------------------------------

local Node = function(x,y)
	local newNode = {x = x, y = y}
	return newNode
end

------------------------------  Path (global) ------------------------------

local t_insert, t_remove = table.insert, table.remove

local Path = function()
	local newPath = {}

	function newPath:iter()
		local i,pathLen = 1,#self
		return function()
			if self[i] then
				i = i+1
				return self[i-1],i-1
			end
		end
	end	
	
	newPath.nodes = newPath.iter
	
	function newPath:getLength()
		local len = 0
		for i = 2,#self do
			local dx = self[i].x - self[i-1].x
			local dy = self[i].y - self[i-1].y
			len = len + Heuristics.EUCLIDIAN(dx, dy)
		end
		return len
	end

	function newPath:fill()
		local i = 2
		local xi,yi,dx,dy
		local N = #self
		local incrX, incrY
		while true do
			xi,yi = self[i].x,self[i].y
			dx,dy = xi-self[i-1].x,yi-self[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, i, self.grid:getNodeAt(self[i-1].x + incrX, self[i-1].y +incrY))
				N = N+1
			else i=i+1
			end
			if i>N then break end
		end
	end

	function newPath:filter()
		local i = 2
		local xi,yi,dx,dy, olddx, olddy
		xi,yi = self[i].x, self[i].y
		dx, dy = xi - self[i-1].x, yi-self[i-1].y
		while true do
			olddx, olddy = dx, dy
			if self[i+1] then
				i = i+1
				xi, yi = self[i].x, self[i].y
				dx, dy = xi - self[i-1].x, yi - self[i-1].y
				if olddx == dx and olddy == dy then
					t_remove(self, i-1)
					i = i - 1
				end
			else break end
		end
	end
		
	return newPath
end

-- Search algorithms:

------------------------------  ASTAR ------------------------------
-- See: http://en.wikipedia.org/wiki/A_star

local ipairs = ipairs
local huge = math.huge

-- Updates G-cost
local function computeCost(node, neighbour, finder)
	local mCost = Heuristics.EUCLIDIAN(neighbour.x - node.x, neighbour.y - node.y)
	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, node, neighbour, endNode, heuristic, overrideCostEval)
	local oldG = neighbour.g
	local cmpCost = overrideCostEval or computeCost
	cmpCost(node, neighbour, finder)
	if neighbour.g < oldG then
		if neighbour.opened then
			neighbour.opened = false
		end
		neighbour.h = heuristic(endNode.x - neighbour.x, endNode.y - neighbour.y)
		neighbour.f = neighbour.g + neighbour.h
		finder.openList:push(neighbour)
		neighbour.opened = true
	end	
end

-- Calculates a path.
-- Returns the path from location `<startX, startY>` to location `<endX, endY>`.
local function ASTARSEARCH(finder, startNode, endNode, toClear, tunnel, overrideHeuristic, overrideCostEval)
	local heuristic = overrideHeuristic or finder.heuristic
	
	finder.openList:clear()
	startNode.g = 0
	startNode.h = heuristic(endNode.x - startNode.x, endNode.y - startNode.y)
	startNode.f = startNode.g + startNode.h
	finder.openList:push(startNode)
	toClear[startNode] = true
	startNode.opened = true
	
	while not finder.openList:empty() do
		local node = finder.openList:pop()
		node.closed = true
		if node == endNode then
			return node
		end
		local neighbours = finder.grid:getNeighbours(node, finder.walkable, finder.allowDiagonal, tunnel)
		for i, neighbour in ipairs(neighbours) do
			if not neighbour.closed then
				toClear[neighbour] = true
				if not neighbour.opened then
					neighbour.g = huge
					neighbour.parent = nil					
				end
				updateVertex(finder, node, neighbour, endNode, heuristic, overrideCostEval)
			end			
		end		
	end		
	
	return nil
end

------------------------------  BFS ------------------------------
-- See: http://en.wikipedia.org/wiki/Breadth-first_search
local t_remove = table.remove

-- BFS logic
local function breadth_first_search(finder, node, openList, toClear, tunnel)
	local neighbours = finder.grid:getNeighbours(node, finder.walkable, finder.allowDiagonal, tunnel)
	for i = 1,#neighbours do
		local neighbour = neighbours[i]
		if not neighbour.closed and not neighbour.opened then
			openList[#openList+1] = neighbour
			neighbour.opened = true
			neighbour.parent = node
			toClear[neighbour] = true
		end
	end
end

-- Calculates a path.
-- Returns the path from location `<startX, startY>` to location `<endX, endY>`.
local function BFSEARCH(finder, startNode, endNode, toClear, tunnel)

	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, node, openList, toClear, tunnel)
	end

	return nil
end

------------------------------  DFS ------------------------------
-- See: http://en.wikipedia.org/wiki/Depth-first_search

-- DFS logic
local function depth_first_search(finder, node, openList, toClear)
	local neighbours = finder.grid:getNeighbours(node, finder.walkable, finder.allowDiagonal, tunnel)
	for i = 1,#neighbours do
		local neighbour = neighbours[i]
		if (not neighbour.closed and not neighbour.opened) then
			openList[#openList+1] = neighbour
			neighbour.opened = true
			neighbour.parent = node
			toClear[neighbour] = true
		end
	end

end

-- Calculates a path.
-- Returns the path from location `<startX, startY>` to location `<endX, endY>`.
local function DFSEARCH(finder, startNode, endNode, toClear, tunnel)

	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, node, openList, toClear, tunnel)
	end

	return nil
end

------------------------------  DIJKSTRA ------------------------------
-- See: http://en.wikipedia.org/wiki/Dijkstra%27s_algorithm

local dijkstraHeuristic = function(...) return 0 end

-- Calculates a path.
-- Returns the path from location `<startX, startY>` to location `<endX, endY>`.
local function DIJKSTRASEARCH(finder, startNode, endNode, toClear, tunnel)
	return ASTARSEARCH(finder, startNode, endNode, toClear, tunnel, dijkstraHeuristic)
end
	
------------------------------  JPS ------------------------------
-- See:  http://harablog.wordpress.com/2011/09/07/jump-point-search/

local step_first = false

local function findNeighbours(finder,node, tunnel)

	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) then
				neighbours[#neighbours+1] = finder.grid:getNodeAt(x,y+dy)
				walkY = true
			end
			if finder.grid:isWalkableAt(x+dx,y,finder.walkable) 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)) and walkY then
				neighbours[#neighbours+1] = finder.grid:getNodeAt(x-dx,y+dy)
			end
			if (not finder.grid:isWalkableAt(x,y-dy,finder.walkable)) 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) 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)) then
						neighbours[#neighbours+1] = finder.grid:getNodeAt(x+1,y+dy)
					end
					if (not finder.grid:isWalkableAt(x-1,y,finder.walkable)) 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) then
						neighbours[#neighbours+1] = finder.grid:getNodeAt(x+1,y)
					end
					if finder.grid:isWalkableAt(x-1,y,finder.walkable)
						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) 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)) then
						neighbours[#neighbours+1] = finder.grid:getNodeAt(x+dx,y+1)
					end
					if (not finder.grid:isWalkableAt(x,y-1,finder.walkable)) 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) then
						neighbours[#neighbours+1] = finder.grid:getNodeAt(x,y+1)
					end
					if finder.grid:isWalkableAt(x,y-1,finder.walkable) 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, tunnel)
end

local function jump(finder, node, parent, endNode)
	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) 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) and (not finder.grid:isWalkableAt(x-dx,y,finder.walkable))) or
			 (finder.grid:isWalkableAt(x+dx,y-dy,finder.walkable) and (not finder.grid:isWalkableAt(x,y-dy,finder.walkable))) 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) and (not finder.grid:isWalkableAt(x,y+1,finder.walkable))) or
					 (finder.grid:isWalkableAt(x+dx,y-1,finder.walkable) and (not finder.grid:isWalkableAt(x,y-1,finder.walkable))) then
					return node
				end
			else
				-- : in case diagonal moves are forbidden
				if finder.grid:isWalkableAt(x+1,y,finder.walkable) or finder.grid:isWalkableAt(x-1,y,finder.walkable) 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) and (not finder.grid:isWalkableAt(x+1,y,finder.walkable))) or
					 (finder.grid:isWalkableAt(x-1,y+dy,finder.walkable) and (not finder.grid:isWalkableAt(x-1,y,finder.walkable))) then
					return node
				end
			else
				-- : in case diagonal moves are forbidden
				if finder.grid:isWalkableAt(x,y+1,finder.walkable) or finder.grid:isWalkableAt(x,y-1,finder.walkable) 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) then return node end
		if jump(finder,finder.grid:getNodeAt(x,y+dy),node,endNode) then return node end
	end

	-- Recursive diagonal search
	if finder.allowDiagonal then
		if finder.grid:isWalkableAt(x+dx,y,finder.walkable) or finder.grid:isWalkableAt(x,y+dy,finder.walkable) then
			return jump(finder,finder.grid:getNodeAt(x+dx,y+dy),node,endNode)
		end
	end
end

local function identifySuccessors(finder,node,endNode,toClear, tunnel)

	-- Gets the valid neighbours of the given node
	-- Looks for a jump point in the direction of each neighbour
	local neighbours = findNeighbours(finder,node, tunnel)
	for i = #neighbours,1,-1 do

		local skip = false
		local neighbour = neighbours[i]
		local jumpNode = jump(finder,neighbour,node,endNode)

		-- : 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.x-node.x,jumpNode.y-node.y)
				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.x-endNode.x,jumpNode.y-endNode.y))
					jumpNode.f = jumpNode.g+jumpNode.h
					jumpNode.parent = node
					if not jumpNode.opened then
						finder.openList:push(jumpNode)
						jumpNode.opened = true
						if not step_first then step_first = true end
					else
						finder.openList:heapify(jumpNode)
					end
				end
			end
		end
	end
end

local function JUMPPOINTSEARCH(finder, startNode, endNode, toClear, tunnel)
	step_first = false
	startNode.g, startNode.f = 0,0
	finder.openList:clear()
	finder.openList:push(startNode)
	startNode.opened = true
	toClear[startNode] = true

	local node
	while not finder.openList:empty() do
		-- Pops the lowest F-cost node, moves it in the closed list
		node = finder.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, node, endNode, toClear, tunnel)
	end

	-- No path found, return nil
	return nil
end

------------------------------  Grid Module (global) ------------------------------
-- See: ...

local pairs = pairs
local next  = next
local otype = type
local _grids = {}
	
-- Local helpers

-- Is i and integer ?
local isInt = function(i)
	return otype(i) =='number' and floor(i)==i
end

-- Override type to report integers
local type = function(v)
	if isInt(v) then return 'int' end
	return otype(v)
end

-- Real count of for values in an array
local size = function(t)
	local count = 0
	for k,v in pairs(t) do count = count+1 end
	return count
end

-- Checks array contents
local check_contents = function(t,...)
	local n_count = size(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 = table.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 m is a regular map
local function isMap(m)
	if not check_contents(m, 'table') then return false end
	local lsize = size(m[next(m)])
	for k,v in pairs(m) do
		if not check_contents(m[k], 'string', 'int') then return false end
		if size(v)~=lsize then return false end
	end
	return true
end

-- Is arg a valid string map
local function isStringMap(s)
	if type(m) ~= '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

-- Parses a map
local function parseStringMap(str)
	local map = {}
	local w, h
	for line in str:gmatch('[^\n\r]+') do
		if line then
			w = not w and #line or w
			if not (#line == w) then 
				error('Error parsing map, rows must have the same size!') 
			end
			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

-- Postprocessing : Get map bounds
local function getBounds(map)
	local min_bound_x, max_bound_x
	local min_bound_y, max_bound_y

		for y in pairs(map) do
			min_bound_y = not min_bound_y and y or (y<min_bound_y and y or min_bound_y)
			max_bound_y = not max_bound_y and y or (y>max_bound_y and y or max_bound_y)
			for x in pairs(map[y]) do
				min_bound_x = not min_bound_x and x or (x<min_bound_x and x or min_bound_x)
				max_bound_x = not max_bound_x and x or (x>max_bound_x and x or max_bound_x)
			end
		end
	return min_bound_x,max_bound_x,min_bound_y,max_bound_y
end

-- Preprocessing
local function buildGrid(map)
	local min_bound_x, max_bound_x
	local min_bound_y, max_bound_y

	local nodes = {}
		for y in pairs(map) do
			min_bound_y = not min_bound_y and y or (y<min_bound_y and y or min_bound_y)
			max_bound_y = not max_bound_y and y or (y>max_bound_y and y or max_bound_y)
			nodes[y] = {}
			for x in pairs(map[y]) do
				min_bound_x = not min_bound_x and x or (x<min_bound_x and x or min_bound_x)
				max_bound_x = not max_bound_x and x or (x>max_bound_x and x or max_bound_x)
				nodes[y][x] = Node(x,y)
			end
		end
	return nodes,
		 (min_bound_x or 0), (max_bound_x or 0),
		 (min_bound_y or 0), (max_bound_y or 0)
end

-- Checks if a value is out of and interval [lowerBound,upperBound]
local function outOfRange(i,lowerBound,upperBound)
	return (i< lowerBound or i > upperBound)
end

-- 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]]
}

-- Specialized grids
-- Inits a preprocessed grid
local function PreProcessGrid(newGrid, map)
	newGrid.map = map
	newGrid.nodes, newGrid.min_bound_x, newGrid.max_bound_x, newGrid.min_bound_y, newGrid.max_bound_y = buildGrid(newGrid.map)
	newGrid.width = (newGrid.max_bound_x-newGrid.min_bound_x)+1
	newGrid.height = (newGrid.max_bound_y-newGrid.min_bound_y)+1

	function newGrid:getNodeAt(x,y)
		return self.nodes[y] and self.nodes[y][x] or nil
	end
	
	return newGrid
	
end

-- Inits a postprocessed grid
local function PostProcessGrid(newGrid, map)
	newGrid.map = map
	newGrid.nodes = {}
	newGrid.min_bound_x, newGrid.max_bound_x, newGrid.min_bound_y, newGrid.max_bound_y = getBounds(newGrid.map)
	newGrid.width = (newGrid.max_bound_x-newGrid.min_bound_x)+1
	newGrid.height = (newGrid.max_bound_y-newGrid.min_bound_y)+1

	function newGrid:getNodeAt(x,y)
		if not x or not y then return end
		if outOfRange(x,self.min_bound_x,self.max_bound_x) then return end
		if outOfRange(y,self.min_bound_y,self.max_bound_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(x,y) end
		return self.nodes[y][x]
	end
	
	return newGrid
	
end

local Grid = function(map, processOnDemand)

	map = type(map)=='string' and parseStringMap(map) or map
	if not (isMap(map) or isStringMap(map)) then
		error('Bad argument #1. Not a valid map')
	end
	if not (type(processOnDemand) == 'boolean' or not processOnDemand) then
		error(('Bad argument #2. Expected \'boolean\', got %s.'):format(type(processOnDemand)))
	end
	
	local newGrid = {}
	
	function newGrid:isWalkableAt(x, y, walkable)
		local nodeValue = self.map[y] and self.map[y][x]
		if nodeValue then
			if not walkable then return true end
		else 
			return false
		end
		if self.__eval then return walkable(nodeValue) end
		return (nodeValue == walkable)
	end
	
	function newGrid:getWidth() return self.width end

	function newGrid:getHeight() return self.height end

	function newGrid:getMap() return self.map end

	function newGrid:getNodes() return self.nodes end
	
	function newGrid:getNeighbours(node, walkable, allowDiagonal, tunnel)
		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) 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) 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) and not self:isWalkableAt(n2.x, n2.y, walkable)) then
						skipThisNode = true
					end
					if not skipThisNode then neighbours[#neighbours+1] = n end
				end
			end
		end

		return neighbours
	end

	function newGrid:iter(lx,ly,ex,ey)
		local min_x = lx or self.min_bound_x
		local min_y = ly or self.min_bound_y
		local max_x = ex or self.max_bound_x
		local max_y = ey or self.max_bound_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

	function newGrid:each(f,...)
		for node in self:iter() do f(node,...) end
	end

	function newGrid:eachRange(lx,ly,ex,ey,f,...)
		for node in self:iter(lx,ly,ex,ey) do f(node,...) end
	end

	function newGrid:imap(f,...)
		for node in self:iter() do
			node = f(node,...)
		end
	end

	function newGrid:imapRange(lx,ly,ex,ey,f,...)
		for node in self:iter(lx,ly,ex,ey) do
			node = f(node,...)
		end
	end
	
	_grids[newGrid] = true
	
	if processOnDemand then
		return PostProcessGrid(newGrid,map)
	end
	
	return PreProcessGrid(newGrid,map)
	
end

	
------------------------------  Pathfinder Module (global) ------------------------------	
-- See: http://en.wikipedia.org/wiki/Pathfinding

local _VERSION = "1.8.1"
local _RELEASEDATE = "03/01/2013"

local function isAGrid(grid)
	return _grids[grid] or false
end
	
local Finders = {
	['ASTAR']     = ASTARSEARCH,	
	['DIJKSTRA']  = DIJKSTRASEARCH,
	['BFS']       = BFSEARCH,
	['DFS']       = DFSEARCH,
	['JPS']       = JUMPPOINTSEARCH,
}	

-- Collect keys in an array
local function collect_keys(t)
	local keys = {}
	for k,v in pairs(t) do keys[#keys+1] = k end
	return keys
end

local toClear = {}

local function reset()
	for node in pairs(toClear) do
		node.g, node.h, node.f = nil, nil, nil
		node.opened, node.closed, node.parent = nil, nil, nil
	end
	toClear = {}
end

-- Keeps track of the last computed path cost
local lastPathCost = 0

-- Availables search modes
local searchModes = {['DIAGONAL'] = true, ['ORTHOGONAL'] = true}

local function traceBackPath(finder, node, startNode)
	local path = Path()
	path.grid = finder.grid
	lastPathCost = node.f or path:getLength()

	while true do
		if node.parent then
			t_insert(path,1,node)
			node = node.parent
		else
			t_insert(path,1,startNode)
			return path
		end
	end
end

local Pathfinder = function(grid, finderName, walkable)
	local newPathfinder = {}

	function newPathfinder:setGrid(grid)
		if not (isAGrid(grid)) then
			error('Bad argument #1. Expected a \'grid\' object')
		end
		self.grid = grid
		self.grid.__eval = self.walkable and type(self.walkable) == 'function'
		return self
	end	
	
	function newPathfinder:getGrid()
		return self.grid
	end

	function newPathfinder:setWalkable(walkable)
		if not (('stringintfunctionnil'):match(type(walkable))) then
			error(('Bad argument #2. Expected \'string\', \'number\' or \'function\', got %s.'):format(type(walkable)))
		end
		self.walkable = walkable
		self.grid.__eval = type(self.walkable) == 'function'
		return self
	end

	function newPathfinder:getWalkable()
		return self.walkable
	end

	function newPathfinder:setFinder(finderName)
		local finderName = finderName
		if not finderName then
			if not self.finder then 
				finderName = 'ASTAR' 
			else return 
			end
		end
		if not (Finders[finderName]) then 
			error('Not a valid finder name!')
		end
		self.finder = finderName
		return self
	end

	function newPathfinder:getFinder()
		return self.finder
	end

	function newPathfinder:getFinders()
		return collect_keys(Finders)
	end

	function newPathfinder:setHeuristic(heuristic)
		if not (Heuristics[heuristic] or (type(heuristic) == 'function')) then
			error('Not a valid heuristic!')
		end
		self.heuristic = Heuristics[heuristic] or heuristic
		return self
	end

	function newPathfinder:getHeuristic()
		return self.heuristic
	end

	function newPathfinder:getHeuristics()
		return collect_keys(Heuristics)
	end

	function newPathfinder:setMode(mode)
		if not (searchModes[mode]) then
			error('Invalid mode')
		end
		self.allowDiagonal = (mode == 'DIAGONAL')
		return self
	end

	function newPathfinder:getMode()
		return (self.allowDiagonal and 'DIAGONAL' or 'ORTHOGONAL')
	end

	function newPathfinder:getModes()
		return collect_keys(searchModes)
	end

	function newPathfinder:version()
		return _VERSION, _RELEASEDATE
	end

	function newPathfinder:getPath(startX, startY, endX, endY, tunnel)
		reset()
		local startNode = self.grid:getNodeAt(startX, startY)
		local endNode = self.grid:getNodeAt(endX, endY)
		if not (startNode) then 
			error(('Invalid location [%d, %d]'):format(startX, startY))
		end
		if not (endNode and self.grid:isWalkableAt(endX, endY)) then
			error(('Invalid or unreachable location [%d, %d]'):format(endX, endY))
		end
		local _endNode = Finders[self.finder](self, startNode, endNode, toClear, tunnel)
		if _endNode then 
			return traceBackPath(self, _endNode, startNode), lastPathCost
		end
		lastPathCost = 0
		return nil, lastPathCost
	end
	
	newPathfinder:setGrid(grid)
	newPathfinder:setFinder(finderName)
	newPathfinder:setWalkable(walkable)
	newPathfinder:setMode('ORTHOGONAL')
	newPathfinder:setHeuristic('MANHATTAN')
	newPathfinder.openList = Heap()
	
	return newPathfinder
	
end

------------------------------  Spawns module into the global env ------------------------------
Jumper = {
	Heuristics = Heuristics,
	Node = Node,
	Path = Path,
	Grid = Grid,
	Pathfinder = Pathfinder
}

-- #### #### ####
⚠️ **GitHub.com Fallback** ⚠️