local mapMaxX, mapMaxY = Map.GetGridSize();
local mapMaxXX = mapMaxX; --DB
local mapWrapX = Map.IsWrapX();
--DEBUG_ITERDATA={};
--DEBUG_CNT=0;
function AStar_AddToSet(set, x, y, G, H)
local plotID = x + y * mapMaxX;
if(set[plotID]==nil) then
set[plotID]={};
end
set[plotID].x = x;
set[plotID].y = y;
set[plotID].G = G;
set[plotID].H = H;
set[plotID].F = G + H;
return set, plotID;
end
function AStar_HeuristEst(x1, y1, x2, y2, calc, epsilon)
return calc and (Map.PlotDistance(x1, y1, x2, y2) * epsilon) or 0;
end
function AStar(x1, y1, x2, y2, saturate, moves, moves_reserve, unitstat, zocmap, terrainCostMap)
local timeStart = os.clock();
if(moves_reserve == nil) then moves_reserve=-1000; end
if(unitstat == nil) then
unitstat={};
unitstat.domain = DomainTypes.DOMAIN_LAND;
unitstat.maxMoves = moves;
unitstat.ignoreCost = false;
unitstat.enterImpass = false;
unitstat.coastal = false;
end
--local heuristicsEpsilon = 1.2; --this gives [almost] optimal path and significantly reduces the number of searches performed
local heuristicsEpsilon = 1; --1 gives always ideal solution
local target = nil;
if(saturate) then
x2=-1;
y2=-1;
heuristicsEpsilon = 0;
else
target = (x2 + y2 * mapMaxX);
end
local fail=true;
local ClosedSet={};
local OpenSet={};
local DeadSet={};
local source;
OpenSet, source=AStar_AddToSet(OpenSet, x1, y1, 0, AStar_HeuristEst(x1, y1, x2, y2, not saturate, heuristicsEpsilon)); --add point of origin
OpenSet[source].moves = moves;
local OpenHeap;
OpenHeap = HeapCreate(OpenHeap);
OpenHeap = HeapInsert(OpenHeap, source, OpenSet[source].F);
local Prev={};
--[[DEBUG_CNT=-1; ---DB
DEBUG_ITERDATA={}; ---DB ]]--
while true do
--[[DEBUG_CNT=DEBUG_CNT+1; --DEBUG
DEBUG_ITERDATA[DEBUG_CNT]={};
DEBUG_ITERDATA[DEBUG_CNT].OpenHeap=tcopy(OpenHeap);
DEBUG_ITERDATA[DEBUG_CNT].OpenSet=tcopy(OpenSet);
DEBUG_ITERDATA[DEBUG_CNT].ClosedSet=tcopy(ClosedSet);
DEBUG_ITERDATA[DEBUG_CNT].Prev=tcopy(Prev); -- ]]--
--remove from open set
local tmp_Fval, lowestF;
OpenHeap, tmp_Fval, lowestF = HeapDeleteBest(OpenHeap);
if(lowestF==nil) then --all saturated
fail=not saturate;
break;
end;
--[[print("");
print(DEBUG_CNT,"LowestF:",lowestF,tmp_Fval);
DEBUG_ITERDATA[DEBUG_CNT].lowestF=lowestF;
DEBUG_ITERDATA[DEBUG_CNT].lowestF_F=tmp_Fval; --]]--
--add to closed set
ClosedSet[lowestF] = tcopy(OpenSet[lowestF]);
local lowestF_x = ClosedSet[lowestF].x;
local lowestF_y = ClosedSet[lowestF].y;
local lowestF_G = ClosedSet[lowestF].G;
local lowestF_moves = ClosedSet[lowestF].moves;
--print("LowestF:",lowestF,lowestF_G,ClosedSet[lowestF].moves);
OpenSet[lowestF] = nil;
if(lowestF==target and not saturate) then --reached target
fail=false;
break;
end;
--Events.SerialEventHexHighlight( ToHexFromGrid( Vector2( lowestF_x, lowestF_y ) ), true, Vector4( 0, 0, 1, 1 ));
--EnvelopeSet[lowestF] = nil;
lowestFPlot = Map.GetPlot(lowestF_x, lowestF_y);
if(lowestFPlot~=nil) then
local movesLeft = lowestF_moves;
if(not saturate or movesLeft > 0) then --when saturating, we only care about how far we can reach in a single turn
movesLeft = (movesLeft <= 0) and unitstat.maxMoves or movesLeft; --if not saturating, we should take into account move "reloading" every turn.
--loop over 6 neighbor plots
local oddCorr = (lowestF_y % 2 == 0) and -1 or 1; --correction for odd rows displacement
for i=0,1 do
for j=-1,1,1 do
local xCorr = ((i~=0 and j~=0) and oddCorr or 0) + ((j==0) and (i*2-1) or 0);
local neighborX = lowestF_x + xCorr;
if(mapWrapX) then
neighborX = neighborX % mapMaxXX;
end
local neighborY = lowestF_y + j;
--Events.SerialEventHexHighlight( ToHexFromGrid( Vector2( neighborX, neighborY ) ), true, Vector4( 0, 1, 1, 1 ));
local neighborID = neighborX + neighborY * mapMaxX;
local extraTerrainCost = terrainCostMap ~= nil and terrainCostMap[neighborX][neighborY].val or 0;
--make sure this neighbor is not in ClosedSet
if(ClosedSet[neighborID] == nil) then
if(OpenSet[neighborID] == nil) then
local neighborPlot = Map.GetPlot(neighborX, neighborY);
if(neighborPlot~=nil) then
local neighborPlotFeatures = neighborPlot:GetFeatureType();
local neighborPlotTerrain = neighborPlot:GetTerrainType();
local neighborEnemy = zocmap ~= nil and zocmap[neighborX][neighborY].anchor > 0;
if(neighborEnemy) then
--add to closed set if there is enemy in that tile
ClosedSet = AStar_AddToSet(ClosedSet, neighborX, neighborY, 1000, 1000);
DeadSet[#DeadSet+1] = neighborID; --dead set is to clean impassable terrain from useful ClosedSet later
else
--see if tile is impassable
local neighborImpass = (neighborPlot:IsMountain() or neighborPlotFeatures == GameInfoTypes.FEATURE_ICE or extraTerrainCost >= 1000);
local completelyImpass = (unitstat.domain == DomainTypes.DOMAIN_SEA) and not
((neighborPlotTerrain == GameInfoTypes.TERRAIN_COAST) or (neighborPlotTerrain == GameInfoTypes.TERRAIN_OCEAN and not unitstat.coastal));
if(completelyImpass or (neighborImpass and not unitstat.enterImpass)) then
--add to closed set if impassable
ClosedSet = AStar_AddToSet(ClosedSet, neighborX, neighborY, 1000, 1000);
DeadSet[#DeadSet+1] = neighborID; --dead set is to clean impassable terrain from useful ClosedSet later
--print("Impassable. Add to closed set:", neighborX, neighborY);
--Events.SerialEventHexHighlight( ToHexFromGrid( Vector2( neighborX, neighborY ) ), true, Vector4( 0, 0, 0, 1 ));
else
local neighborDrains = (unitstat.domain ~= DomainTypes.DOMAIN_SEA) and
(neighborPlotTerrain == GameInfoTypes.TERRAIN_COAST or neighborPlotTerrain == GameInfoTypes.TERRAIN_OCEAN);
--add to closed set if drains moves and we are interested in single-turn pathfinding only
if(neighborDrains and saturate) then
ClosedSet = AStar_AddToSet(ClosedSet, neighborX, neighborY, 1000, 1000);
DeadSet[#DeadSet+1] = neighborID;
--print("Drains moves. Add to closed set:", neighborX, neighborY);
--Events.SerialEventHexHighlight( ToHexFromGrid( Vector2( neighborX, neighborY ) ), true, Vector4( 0, 0, 0, 1 ));
else
local neighborAcrossRiver = lowestFPlot:IsRiverCrossingToPlot(neighborPlot);
local neighborZOCMove = zocmap ~= nil and zocmap[neighborX][neighborY].val > 0 and zocmap[lowestF_x][lowestF_y].val > 0;
local drainsNow = neighborDrains or (neighborAcrossRiver and not unitstat.ignoreCost) or neighborZOCMove;
local neighborCost = 1;
if(not unitstat.ignoreCost) then
neighborCost = 1 + (neighborPlot:IsHills() and 1 or 0) +
((neighborPlotFeatures == GameInfoTypes.FEATURE_FOREST or neighborPlotFeatures == GameInfoTypes.FEATURE_JUNGLE or neighborPlotFeatures == GameInfoTypes.FEATURE_FALLOUT) and 1 or 0) +
(neighborPlotFeatures == GameInfoTypes.FEATURE_MARSH and 2 or 0) + extraTerrainCost;
end
local currCost = (drainsNow and movesLeft or neighborCost);
local movesMinusCost = movesLeft - currCost;
-- if saturating and reserve is required (for artillery to fire after moving), discard neighbors which drain moves
if(not saturate or movesMinusCost > moves_reserve) then
local tentative_G = lowestF_G + currCost;
OpenSet = AStar_AddToSet(OpenSet, neighborX, neighborY, tentative_G, AStar_HeuristEst(neighborX, neighborY, x2, y2, not saturate, heuristicsEpsilon));
OpenSet[neighborID].moves = movesMinusCost;
OpenSet[neighborID].cost = neighborCost;
OpenSet[neighborID].drains = neighborDrains;
OpenSet[neighborID].impass = neighborImpass;
OpenHeap = HeapInsert(OpenHeap, neighborID, OpenSet[neighborID].F);
Prev[neighborID] = lowestF; --set F as previous node
--Events.SerialEventHexHighlight( ToHexFromGrid( Vector2( neighborX, neighborY ) ), true, Vector4( 1, 0, 1, 1 ));
--print("Add to open set:", neighborX, neighborY,"G:",tentative_G,"F:",OpenSet[neighborID].F);
end
end
end
end
end
else
local neighborPlot = Map.GetPlot(neighborX, neighborY);
local neighborZOCMove = zocmap ~= nil and zocmap[neighborX][neighborY].val > 0 and zocmap[lowestF_x][lowestF_y].val > 0;
local drainsNow = OpenSet[neighborID].drains or neighborZOCMove or lowestFPlot:IsRiverCrossingToPlot(neighborPlot);
local currCost = (drainsNow and movesLeft or OpenSet[neighborID].cost);
local tentative_G = lowestF_G + currCost;
if(tentative_G < OpenSet[neighborID].G) then
--update existing tile in OpenSet with better "predecessor"
OpenSet = AStar_AddToSet(OpenSet, neighborX, neighborY, tentative_G, AStar_HeuristEst(neighborX, neighborY, x2, y2, not saturate, heuristicsEpsilon));
OpenSet[neighborID].moves = movesLeft - currCost;
OpenHeap = HeapUpdate(OpenHeap, neighborID, OpenSet[neighborID].F);
Prev[neighborID] = lowestF; --set F as previous node
--print("Update open set:", neighborX, neighborY,"G:",tentative_G,"F:",OpenSet[neighborID].F);
end
end
end
end
end
end
end
end
local bestPath = {target};
local cnt = 1;
while bestPath[cnt]~=nil do
Events.SerialEventHexHighlight( ToHexFromGrid( Vector2( bestPath[cnt]%mapMaxX, math.floor(bestPath[cnt]/mapMaxX) ) ), true, Vector4( 0, 1, 0, 1 ));
bestPath[cnt+1] = Prev[bestPath[cnt]];
cnt = cnt + 1;
end
for i,id in ipairs(DeadSet) do
ClosedSet[id] = nil; --remove impassable terrain from ClosedSet
end
local timeEnd = os.clock();
print("PATHFINDING TIME:", timeEnd - timeStart );
return (not fail), ClosedSet, bestPath, Prev;
end
function HeapCreate(heap)
heap = {};
heap.values = {};
heap.values.first = nil;
heap.values.last = nil;
--pre-initializing values container with likely numbers, like 0,...,10 could reduce the number of insertions in-between
--but I don't do it here
heap.hash = {};
return heap;
end
function HeapDeleteBest(heap)
local bestPos=heap.values.first;
local bestVal;
local bestID;
if(bestPos~=nil) then
bestVal = heap.values[bestPos];
local bestTab = heap[bestVal];
bestID = bestTab[bestTab.last]; --For FIFO: set first here
if(bestTab.count > 1) then
--LIFO removal among identical elements. Results in depth-first behavior for A*
bestTab[bestTab.last] = nil; --For FIFO: set first here
bestTab.last = bestTab.last - 1; --For FIFO: set first here
bestTab.count = bestTab.count - 1;
else
--remove whole container as it only has one value
heap[bestVal] = nil;
--update sorted list of values
heap.values[heap.values.first] = nil;
heap.values.first = heap.values.first + 1;
if(heap.values.first>heap.values.last) then
--we have deleted the last value stored in heap
heap.values.first = nil;
heap.values.last = nil;
end
end
else
return heap, nil, nil;
end
--check validity of retrieved value
if(bestID ~= nil) then
heap.hash[bestID] = nil; --clean hash
return heap, bestVal, bestID;
end
--if value was invalid, retrieve another one (use tail call)
return HeapDeleteBest(heap)
end
function HeapInsert(heap, id, val)
if(heap.hash[id] ~= nil) then
print("WARNING: trying to insert a value which already exists in table")
return HeapUpdate(heap, id, val);
end
local index;
if(heap[val] ~= nil) then
index = heap[val].last + 1;
heap[val][index] = id;
heap[val].last = index;
heap[val].count = heap[val].count + 1;
else
local a = heap.values.first;
local b = heap.values.last;
if(a == nil) then
heap.values.first = 1;
heap.values.last = 1;
heap.values[1] = val;
elseif(val < heap.values[a]) then
local insSpot = a - 1;
heap.values[insSpot] = val;
heap.values.first = insSpot;
else
--Value val got to be inserted somewhere in the middle or the end
local ii = heap.values.last;
while heap.values[ii] > val do
heap.values[ii+1] = heap.values[ii];
ii = ii - 1;
end
heap.values.last = heap.values.last + 1;
heap.values[ii+1] = val;
end
--this value didn't occur yet. Create a container for it
index = 1;
heap[val] = {};
heap[val].first = 1;
heap[val].last = 1;
heap[val].count = 1;
heap[val][index] = id;
end
heap.hash[id] = {};
heap.hash[id].val = val;
heap.hash[id].pos = index;
return heap;
end
function HeapUpdate(heap, id, newval)
--potential duplicate entry is stored in heap until retrieval.
--at retrieval, if value is invalid (nil), the value is discarded and next one is retrieved
--because retrieval is always from first position, it is much cheaper to discard then instead of discarding it now from the middle
local hashInfo = heap.hash[id];
heap[hashInfo.val][hashInfo.pos] = nil;
heap.hash[id] = nil; --clean hash so HeapInsert doesn't detect a duplicate entry
heap = HeapInsert(heap, id, newval);
return heap;
end