heroes-of-nerevelon/lib/pathfinder.lua
PeaAshMeter 82d393a064 - proper A* implementation
- not really as I'm a freak
- tiles are now walkable by default
2025-09-03 00:28:46 +03:00

163 lines
4.7 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

local deque = require "lib.utils.deque"
local SQRT2 = math.sqrt(2)
local function isWalkable(point)
-- 1) В пределах уровня
if point.x < 0 or point.y < 0 then return false end
if point.x >= Tree.level.size.x or point.y >= Tree.level.size.y then return false end
-- 2) Клетка не занята персонажем
if Tree.level.characterGrid:get(point) then return false end
-- 3) Клетка проходима по тайлу
local tile = Tree.level.tileGrid:get(point)
if not tile or not tile.walkable then return false end
return true
end
--- @param a Vec3
--- @param b Vec3
--- @return number
local function heuristic(a, b)
local dx = math.abs(a.x - b.x)
local dy = math.abs(a.y - b.y)
return (dx + dy) + (SQRT2 - 2) * math.min(dx, dy)
end
--- @param from Vec3
--- @param to Vec3
--- @return boolean
local function can_step(from, to)
if not isWalkable(to) then return false end
local dx = to.x - from.x
local dy = to.y - from.y
if dx ~= 0 and dy ~= 0 then
-- нельзя просачиваться через диагональ
local viaX = Vec3 { from.x + dx, from.y }
local viaY = Vec3 { from.x, from.y + dy }
if not (isWalkable(viaX) or isWalkable(viaY)) then
return false
end
end
return true
end
--- @param cur Vec3
--- @return Vec3[]
local function neighbors(cur)
local res = {}
for dx = -1, 1 do
for dy = -1, 1 do
if not (dx == 0 and dy == 0) then
local nxt = Vec3 { cur.x + dx, cur.y + dy }
if can_step(cur, nxt) then
res[#res + 1] = nxt
end
end
end
end
return res
end
--- Восстановление пути (включая начало и цель)
--- @param cameFrom table<string, Vec3|nil>
--- @param goal Vec3
--- @return Deque
local function reconstruct_path(cameFrom, goal)
local sequence = {}
local cur = goal
while cur do
sequence[#sequence + 1] = cur
cur = cameFrom[tostring(cur)]
end
local acc = deque.new()
for i = 1, #sequence, 1 do
acc = acc:push_front(sequence[i])
end
return acc
end
--- @param openSet table множество вершин на границе
--- @param closed table уже обработанные
--- @param cameFrom table путь
--- @param gScore table
--- @param fScore table
--- @param goal Vec3
--- @return Deque
local function a_star_step(openSet, closed, cameFrom, gScore, fScore, goal)
-- пусто: пути нет
local anyKey = next(openSet)
if not anyKey then
return deque.new()
end
-- выбрать узел с минимальным fScore
local currentKey, currentNode = anyKey, openSet[anyKey]
local bestF = fScore[anyKey] or math.huge
for k, node in pairs(openSet) do
local f = fScore[k] or math.huge
if f < bestF then
bestF = f
currentKey, currentNode = k, node
end
end
-- достигли цели
if currentNode == goal then
return reconstruct_path(cameFrom, currentNode)
end
openSet[currentKey] = nil
closed[currentKey] = true
local gCurrent = gScore[currentKey] or math.huge
for _, nb in ipairs(neighbors(currentNode)) do
local nk = tostring(nb)
if not closed[nk] then
local dx = nb.x - currentNode.x
local dy = nb.y - currentNode.y
local step = (dx ~= 0 and dy ~= 0) and SQRT2 or 1 -- по диагонали ходить "дороже"
local tentativeG = gCurrent + step
if tentativeG < (gScore[nk] or math.huge) then
cameFrom[nk] = currentNode
gScore[nk] = tentativeG
fScore[nk] = tentativeG + heuristic(nb, goal)
if not openSet[nk] then
openSet[nk] = nb
end
end
end
end
return a_star_step(openSet, closed, cameFrom, gScore, fScore, goal)
end
--- @param from Vec3
--- @param to Vec3
--- @return Deque
local function trace(from, to)
-- не считаем путь до непроходимой точки (ибо нефиг)
if not isWalkable(to) then return deque.new():push_back(from) end
-- тривиальный случай
if from == to then
return deque.new():push_back(from)
end
local openSet = {}
local closed = {}
local cameFrom = {}
local gScore = {}
local fScore = {}
local sk = tostring(from)
openSet[sk] = from
gScore[sk] = 0
fScore[sk] = heuristic(from, to)
return a_star_step(openSet, closed, cameFrom, gScore, fScore, to)
end
return trace