- proper A* implementation

- not really as I'm a freak
- tiles are now walkable by default
This commit is contained in:
PeaAshMeter 2025-09-03 00:28:46 +03:00
parent 7000f0fb4d
commit 82d393a064
3 changed files with 160 additions and 20 deletions

View File

@ -19,7 +19,10 @@ end
--- @class Tile
--- @field atlasData TileAtlasData
--- @field position Vec3
local tile = {}
--- @field walkable boolean
local tile = {
walkable = true
}
tile.__index = tile
--- TODO: сделать как love.graphics.draw несколько сигнатур у функции

View File

@ -1,34 +1,162 @@
local deque = require "lib.utils.deque"
local SQRT2 = math.sqrt(2)
--- @param cur Vec3
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
--- @param acc Deque
local function greedy_trace_step(cur, to, acc)
local lengthTable = {}
for x = -1, 1 do
for y = -1, 1 do
local point = Vec3 { cur.x + x, cur.y + y }
table.insert(lengthTable, { point, (point - to):length() })
--- @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
local min = lengthTable[1]
for i = 2, #lengthTable do
if lengthTable[i][2] < min[2] then min = lengthTable[i] end
end
local next = min[1]
return true
end
acc = acc:push_back(cur)
if cur == to then
return acc
--- @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 greedy_trace_step(next, to, acc)
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)
return greedy_trace_step(from, to, deque.new())
-- не считаем путь до непроходимой точки (ибо нефиг)
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

View File

@ -9,7 +9,16 @@ function love.conf(t)
end
function love.load()
character.spawn("Hero", "warrior", Tree.assets.files.sprites.character)
for x = 0, 29, 1 do
for y = 0, 29, 1 do
if math.random() > 0.8 then
local c = character.spawn("Hero", "warrior", Tree.assets.files.sprites.character)
c.logic.mapLogic.position = Vec3 { x, y }
c.logic.mapLogic.displayedPosition = Vec3 { x, y }
end
end
end
-- PlayerFaction.characters = { Hero1, Hero2 }
love.window.setMode(1080, 720, { resizable = true, msaa = 4, vsync = true })