best first search

Data

in lua

Tags

graph traversal, pathfinding

Source Code

``````-- Generic Best First Search algorithm implementation
-- See : http://en.wikipedia.org/wiki/Best-first_search

-- Notes : this is a generic implementation of BFS graph search algorithm.
-- It is devised to be used on any type of graph (point-graph, tile-graph,
-- or whatever. It expects to be initialized with a handler, which acts as
-- an interface between the search algorithm and the search space.

-- This implementation uses internally a binary heap to handle fast retrieval
-- of the lowest-cost node.

-- The BFS class expects a handler to be initialized. Roughly said, the handler
-- is an interface between your search space and the generic search algorithm.
-- This model ensures flexibility, so that this generic implementation can be
-- adapted to search on any kind of space.
-- The passed-in handler should implement those functions.
-- handler.getNode(...)    -> returns a Node (instance of node.lua)
-- handler.distance(a, b)  -> heuristic function which returns the distance
--                            between node a and node b
-- handler.getNeighbors(n) -> returns an array of all nodes that can be reached
--                            via node n (also called successors of node n)

-- The generic Node class provided (see node.lua) should also be implemented
-- through the handler. Basically, it should describe how nodes are labelled
-- and tested for equality for a custom search space.
-- The following functions should be implemented:
-- function Node:initialize(...) -> creates a Node with custom attributes
-- function Node:isEqualTo(n)    -> returns if self is equal to node n
-- function Node:toString()      -> returns a unique string representation of
--                                  the node, for debug purposes

-- See custom handlers for reference (*_hander.lua).

-- Dependencies
local class = require 'utils.class'
local bheap = require 'utils.bheap'

-- Returns the first element in a list matching a predicate
local function find(list, f)
for _, v in ipairs(list) do
if f(v) then return v end
end
end

-- Reverses an array
local function reverse(list)
local l = {}
for i = #list,1,-1 do table.insert(l, list[i]) end
return l
end

-- Clears data between consecutive path requests.
local function resetForNextSearch(bfs)
for node in pairs(bfs.visited) do
node.parent, node.opened, node.closed = nil, nil, nil
node.cost = 0
end
bfs.Q:clear()
bfs.visited = {}
end

-- Builds and returns the path to the goal node
local function backtrace(node)
local path = {}
repeat
table.insert(path, 1, node)
node = node.parent
until not node
return path
end

-- Initializes BFS search with a custom handler
local BFS = class()
function BFS:initialize(handler)
self.handler = handler
self.Q = bheap()
self.heuristic = handler.distance
self.visited = {}
end

-- Returns the path between start and goal locations
-- start  : a Node representing the start location
-- goal   : a Node representing the target location
-- returns: an array of nodes
function BFS:findPath(start, goal)
resetForNextSearch(self)

start.cost = self.heuristic(start, goal)
self.Q:push(start)
self.visited[start] = true

while not self.Q:isEmpty() do
local node = self.Q:pop()
if node == goal then return backtrace(node) end
node.closed = true
local neighbors = self.handler.getNeighbors(node)
for _, neighbor in ipairs(neighbors) do
if not neighbor.closed then
local tentative_cost = self.heuristic(neighbor, goal)
if not neighbor.opened or tentative_cost < neighbor.cost then
neighbor.parent = node
neighbor.cost = tentative_cost
self.visited[neighbor] = true
if not neighbor.opened then
neighbor.opened = true
self.Q:push(neighbor)
else
self.openList:sort(neighbor)
end
end
end
end
end
end

return BFS
``````
``````-- Tests for bfs.lua
local BFS = require 'bfs'

local total, pass = 0, 0

local function dec(str, len)
return #str < len
and str .. (('.'):rep(len-#str))
or str:sub(1,len)
end

local function same(t, p, comp)
for k,v in ipairs(t) do
if not comp(v, p[k]) then return false end
end
return true
end

local function run(message, f)
total = total + 1
local ok, err = pcall(f)
if ok then pass = pass + 1 end
local status = ok and 'PASSED' or 'FAILED'
print(('%02d. %68s: %s'):format(total, dec(message,68), status))
end

run('Testing BFS search on linear graph', function()
local comp = function(a, b) return a.value == b end
local ln_handler = require 'handlers.linear_handler'
ln_handler.init(-2,5)
local bfs = BFS(ln_handler)
local start, goal = ln_handler.getNode(0), ln_handler.getNode(5)
assert(same(bfs:findPath(start, goal),  {0,1,2,3,4,5}, comp))

start, goal = ln_handler.getNode(-2), ln_handler.getNode(2)
assert(same(bfs:findPath(start, goal),  {-2,-1,0,1,2}, comp))
end)

run('Testing BFS search on grid graph', function()
local comp = function(a, b) return a.x == b[1] and a.y == b[2] end
local gm_handler = require 'handlers.gridmap_handler'
local bfs = BFS(gm_handler)
local map = {{0,0,0,0,0},{0,1,1,1,1},{0,0,0,0,0}}
gm_handler.init(map)

gm_handler.diagonal = false
local start, goal = gm_handler.getNode(1,1), gm_handler.getNode(5,3)
assert(same(bfs:findPath(start, goal), {{1,1},{1,2},{1,3},{2,3},{3,3},{4,3},{5,3}}, comp))

gm_handler.diagonal = true
assert(same(bfs:findPath(start, goal), {{1,1},{1,2},{2,3},{3,3},{4,3},{5,3}},       comp))
end)

run('Testing BFS search on point graph', function()
local comp = function(a, b) return a.x == b[1] and a.y == b[2] end
local pg_handler = require 'handlers.point_graph_handler'
local bfs = BFS(pg_handler)

local comp = function(a, b) return a.name == b end
local start, goal = pg_handler.getNode('a'), pg_handler.getNode('e')
assert(same(bfs:findPath(start, goal), {'a','c','d','e'}, comp))

pg_handler.setEdgeWeight('a', 'b', 1)
pg_handler.setEdgeWeight('b', 'e', 1)

assert(same(bfs:findPath(start, goal), {'a','b','e'},     comp))
end)

print(('-'):rep(80))
print(('Total : %02d: Pass: %02d - Failed : %02d - Success: %.2f %%')
:format(total, pass, total-pass, (pass*100/total)))
``````