feat(pathfinding): add a-star search and line of sight
This commit is contained in:
1 parent
95684a7d76
commit
0e549ff1dc
2 files changed
+121
No files matched your search
@@ -0,0 +1,88 @@
|
||||
-- A* on the 4-connected grid with a binary min-heap open set and Manhattan
|
||||
-- heuristic. Returns the path start..goal, or nil when unreachable.
|
||||
|
||||
local Astar = {}
|
||||
|
||||
local function heapPush(heap, node, priority)
|
||||
heap[#heap + 1] = { node = node, priority = priority }
|
||||
local i = #heap
|
||||
while i > 1 do
|
||||
local parent = i // 2
|
||||
if heap[parent].priority <= heap[i].priority then break end
|
||||
heap[parent], heap[i] = heap[i], heap[parent]
|
||||
i = parent
|
||||
end
|
||||
end
|
||||
|
||||
local function heapPop(heap)
|
||||
local top = heap[1]
|
||||
local last = heap[#heap]
|
||||
heap[#heap] = nil
|
||||
local size = #heap
|
||||
if size > 0 then
|
||||
heap[1] = last
|
||||
local i = 1
|
||||
while true do
|
||||
local left, right = i * 2, i * 2 + 1
|
||||
local smallest = i
|
||||
if left <= size and heap[left].priority < heap[smallest].priority then smallest = left end
|
||||
if right <= size and heap[right].priority < heap[smallest].priority then smallest = right end
|
||||
if smallest == i then break end
|
||||
heap[i], heap[smallest] = heap[smallest], heap[i]
|
||||
i = smallest
|
||||
end
|
||||
end
|
||||
return top.node
|
||||
end
|
||||
|
||||
local DIRS = { { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 } }
|
||||
|
||||
local function key(x, y)
|
||||
return y * 100000 + x
|
||||
end
|
||||
|
||||
function Astar.find(map, startX, startY, goalX, goalY, isBlocked)
|
||||
isBlocked = isBlocked or function(x, y) return not map:isWalkable(x, y) end
|
||||
if isBlocked(goalX, goalY) then return nil end
|
||||
|
||||
local heap = {}
|
||||
local cameFrom = { [key(startX, startY)] = false }
|
||||
local costSoFar = { [key(startX, startY)] = 0 }
|
||||
heapPush(heap, { x = startX, y = startY }, 0)
|
||||
|
||||
while #heap > 0 do
|
||||
local current = heapPop(heap)
|
||||
if current.x == goalX and current.y == goalY then
|
||||
local reversed = {}
|
||||
local cx, cy = goalX, goalY
|
||||
while true do
|
||||
reversed[#reversed + 1] = { x = cx, y = cy }
|
||||
local prev = cameFrom[key(cx, cy)]
|
||||
if not prev then break end
|
||||
cx, cy = prev.x, prev.y
|
||||
end
|
||||
local path = {}
|
||||
for i = #reversed, 1, -1 do
|
||||
path[#path + 1] = reversed[i]
|
||||
end
|
||||
return path
|
||||
end
|
||||
local currentCost = costSoFar[key(current.x, current.y)]
|
||||
for _, dir in ipairs(DIRS) do
|
||||
local nx, ny = current.x + dir[1], current.y + dir[2]
|
||||
if map:inBounds(nx, ny) and not isBlocked(nx, ny) then
|
||||
local nKey = key(nx, ny)
|
||||
local newCost = currentCost + 1
|
||||
if costSoFar[nKey] == nil or newCost < costSoFar[nKey] then
|
||||
costSoFar[nKey] = newCost
|
||||
local heuristic = math.abs(nx - goalX) + math.abs(ny - goalY)
|
||||
heapPush(heap, { x = nx, y = ny }, newCost + heuristic)
|
||||
cameFrom[nKey] = { x = current.x, y = current.y }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
return Astar
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
-- Bresenham line of sight. A shot is clear when no cell strictly between the two
|
||||
-- endpoints blocks sight; the endpoints themselves are never treated as blockers
|
||||
-- so an actor can fire at something standing in a doorway.
|
||||
|
||||
local Los = {}
|
||||
|
||||
function Los.line(x0, y0, x1, y1)
|
||||
local cells = {}
|
||||
local dx = math.abs(x1 - x0)
|
||||
local dy = math.abs(y1 - y0)
|
||||
local sx = x0 < x1 and 1 or -1
|
||||
local sy = y0 < y1 and 1 or -1
|
||||
local err = dx - dy
|
||||
local x, y = x0, y0
|
||||
while true do
|
||||
cells[#cells + 1] = { x = x, y = y }
|
||||
if x == x1 and y == y1 then break end
|
||||
local e2 = 2 * err
|
||||
if e2 > -dy then err = err - dy; x = x + sx end
|
||||
if e2 < dx then err = err + dx; y = y + sy end
|
||||
end
|
||||
return cells
|
||||
end
|
||||
|
||||
function Los.clear(map, x0, y0, x1, y1)
|
||||
local cells = Los.line(x0, y0, x1, y1)
|
||||
for i = 2, #cells - 1 do
|
||||
if map:blocksSight(cells[i].x, cells[i].y) then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
return Los
|
||||
Reference in new issue
Block a user