sync: restore source from Google Drive
Sync changed files to Google Drive / sync (push) Successful in 1h45m13s

This commit is contained in:
admin_d committed 2026-09-12 16:50:30 +05:00
1 parent e2d8247e91
commit 7063d1b1c9
993 files changed
+949976

No files matched your search

+565
View File
@@ -0,0 +1,565 @@
unit KM_PathFinding;
{$I KaM_Remake.inc}
interface
uses
SysUtils, Math, KromUtils,
KM_CommonClasses, KM_Defaults, KM_Houses, KM_Terrain, KM_Points;
const
PATH_CACHE_MAX = 12; //How many paths to cache
PATH_CACHE_NO_ROUTES_AVOID_LOCKED_MAX = 24; //Size of avoid routes cache
PATH_CACHE_INIT_WEIGHT = 5; //New path weight
PATH_CACHE_NODES_MIN_CNT = 30; //Min number of noder to put route in cache
// Minimum distance on the map to start using PathCache. Otherwise we could get some odd paths
// F.e. we want to get just to the next tile and use PFCache, because some of its routes goes through our start and dest
PATH_CACHE_MIN_DIST_TO_USE: Integer = 3;
PATH_CACHE_NO_ROUTES_AVOID_LOCKED_TTL = 100; //AvoidLockedCache item Time to live
type
TKMPathDestination = (
pdLocation, // Walk to location
pdPassability, // Walk to desired passability
pdHouse // Approach house from any side (workers and warriors)
);
TKMPathAvoidLocked = (
palNoAvoid, // Don't avoid any tiles
palAvoidByMovementCost, // Avoid locekd tiles by increasing their movement cost
palAvoidAsUnwalkable // Avoid locked tiles as mark them unwalkable (no route will be made through them)
);
TKMPathRouteKind = (rkRoute, rkAvoid, rkReturnToWalkable);
// This is a helper class for TKMTerrain
// Here should be pathfinding and all associated stuff
// I think we should refactor this unit and move some TKMTerrain methods here
TKMPathFinding = class
private
fCache: array [0 .. PATH_CACHE_MAX - 1] of record
Weight: Word;
Pass: TKMTerrainPassabilitySet;
Route: TKMPointList;
end;
fCacheAvoidLocked: array [0 .. PATH_CACHE_NO_ROUTES_AVOID_LOCKED_MAX] of record
Pass: TKMTerrainPassabilitySet;
LocA: TKMPoint;
LocB: TKMPoint;
TimeToLive: Word;
end;
protected
fPass: TKMTerrainPassabilitySet;
fPassBest: TKMTerrainPassability; // Best passability, used by RouteAvoid
fTargetWalkConnect: TKMWalkConnect;
fTargetNetwork: Byte;
fDistance: Single;
fAvoidLocked: TKMPathAvoidLocked;
fRouteKind: TKMPathRouteKind;
fTargetHouse: TKMHouse;
procedure AddToCache(NodeList: TKMPointList);
function TryRouteFromCache(NodeList: TKMPointList): Boolean;
procedure AddNoRouteAvoidLockedToCache;
function CacheHasNoRouteAvoidLocked: Boolean;
protected
fLocA: TKMPoint;
fLocB: TKMPoint;
fDestination: TKMPathDestination;
function CanWalkTo(const aFrom: TKMPoint; bX, bY: SmallInt): Boolean; virtual;
function DestinationReached(aX, aY: Word): Boolean; virtual;
function IsWalkableTile(aX, aY: Word): Boolean; virtual;
function MovementCost(aFromX, aFromY, aToX, aToY: Word): Cardinal; virtual;
function EstimateToFinish(aX, aY: Word): Cardinal; virtual;
function MakeRoute: Boolean; virtual; abstract;
procedure ReturnRoute(NodeList: TKMPointList); virtual; abstract;
public
constructor Create;
destructor Destroy; override;
function Route_Make(const aLocA, aLocB: TKMPoint; aPass: TKMTerrainPassabilitySet; aDistance: Single;
aTargetHouse: TKMHouse; NodeList: TKMPointList; aAvoidLocked: TKMPathAvoidLocked = palNoAvoid): Boolean;
function Route_MakeAvoid(const aLocA, aLocB: TKMPoint; aPassBest, aPassAlt: TKMTerrainPassability; aDistance: Single; aTargetHouse: TKMHouse; NodeList: TKMPointList): Boolean;
function Route_ReturnToWalkable(const aLocA, aLocB: TKMPoint; aTargetWalkConnect: TKMWalkConnect; aTargetNetwork: Byte;
aPass: TKMTerrainPassabilitySet; NodeList: TKMPointList): Boolean;
procedure Save(SaveStream: TKMemoryStream); virtual;
procedure Load(LoadStream: TKMemoryStream); virtual;
procedure UpdateState;
end;
implementation
uses
KM_Units, KM_DevPerfLog, KM_DevPerfLogTypes;
{ TKMPathFinding }
constructor TKMPathFinding.Create;
var
I: Integer;
begin
inherited;
if CACHE_PATHFINDING then
for I := 0 to PATH_CACHE_MAX - 1 do
fCache[I].Route := TKMPointList.Create;
if CACHE_PATHFINDING_AVOID_LOCKED then
for I := 0 to PATH_CACHE_NO_ROUTES_AVOID_LOCKED_MAX - 1 do
fCacheAvoidLocked[I].TimeToLive := 0;
end;
destructor TKMPathFinding.Destroy;
var
I: Integer;
begin
if CACHE_PATHFINDING then
for I := 0 to PATH_CACHE_MAX - 1 do
FreeAndNil(fCache[I].Route);
inherited;
end;
//Find a route from A to B which meets aPass Passability
//Results should be written as NodeCount of waypoint nodes to Nodes
function TKMPathFinding.Route_Make(const aLocA, aLocB: TKMPoint; aPass: TKMTerrainPassabilitySet; aDistance: Single;
aTargetHouse: TKMHouse; NodeList: TKMPointList; aAvoidLocked: TKMPathAvoidLocked = palNoAvoid): Boolean;
begin
{$IFDEF DBG_PERFLOG}
gPerfLogs.SectionEnter(psPathfinding);
{$ENDIF}
try
Result := False;
fRouteKind := rkRoute;
fLocA := aLocA;
fLocB := aLocB;
fPass := aPass;
fTargetNetwork := 0;
fTargetWalkConnect := wcWalk;
fDistance := aDistance;
fAvoidLocked := aAvoidLocked;
fTargetHouse := aTargetHouse;
if fTargetHouse = nil then
fDestination := pdLocation
else
fDestination := pdHouse;
//Check
if CACHE_PATHFINDING_AVOID_LOCKED
and (aAvoidLocked = palAvoidAsUnwalkable)
and CacheHasNoRouteAvoidLocked then
begin
NodeList.Clear; //No route available
Exit;
end;
//Try to find similar route in cache and reuse it
if CACHE_PATHFINDING
and (KMLengthDiag(fLocA, fLocB) - fDistance > PATH_CACHE_MIN_DIST_TO_USE) // Don't use PF_Cache for very close destinations
and TryRouteFromCache(NodeList) then
Result := True
else
if MakeRoute then
begin
ReturnRoute(NodeList);
Result := True;
end else begin
NodeList.Clear;
if CACHE_PATHFINDING_AVOID_LOCKED
and (aAvoidLocked = palAvoidAsUnwalkable) then
AddNoRouteAvoidLockedToCache;
end;
finally
{$IFDEF DBG_PERFLOG}
gPerfLogs.SectionLeave(psPathfinding);
{$ENDIF}
end;
end;
//We are using Interaction Avoid mode (go around busy units)
function TKMPathFinding.Route_MakeAvoid(const aLocA, aLocB: TKMPoint; aPassBest, aPassAlt: TKMTerrainPassability; aDistance: Single; aTargetHouse: TKMHouse; NodeList: TKMPointList): Boolean;
begin
{$IFDEF DBG_PERFLOG}
gPerfLogs.SectionEnter(psPathfinding);
{$ENDIF}
try
Result := False;
fRouteKind := rkAvoid;
fLocA := aLocA;
fLocB := aLocB;
fPass := [aPassBest] + [aPassAlt] - [tpNone];
fPassBest := aPassBest;
fTargetNetwork := 0;
fTargetWalkConnect := wcWalk;
fDistance := aDistance;
fAvoidLocked := palAvoidByMovementCost;
fTargetHouse := aTargetHouse;
if fTargetHouse = nil then
fDestination := pdLocation
else
fDestination := pdHouse;
if MakeRoute then
begin
ReturnRoute(NodeList);
Result := True;
end;
finally
{$IFDEF DBG_PERFLOG}
gPerfLogs.SectionLeave(psPathfinding);
{$ENDIF}
end;
end;
//Even though we are only going to a road network it is useful to know where our target is so we start off in the right direction (makes algorithm faster/work over long distances)
function TKMPathFinding.Route_ReturnToWalkable(const aLocA, aLocB: TKMPoint; aTargetWalkConnect: TKMWalkConnect;
aTargetNetwork: Byte; aPass: TKMTerrainPassabilitySet; NodeList: TKMPointList): Boolean;
begin
{$IFDEF DBG_PERFLOG}
gPerfLogs.SectionEnter(psPathfinding);
{$ENDIF}
try
Result := False;
fRouteKind := rkReturnToWalkable;
fLocA := aLocA;
fLocB := aLocB;
fPass := aPass; //Should be unused here
fTargetNetwork := aTargetNetwork;
fTargetWalkConnect := aTargetWalkConnect;
fDistance := 0;
fAvoidLocked := palNoAvoid;
fTargetHouse := nil;
fDestination := pdPassability;
if MakeRoute then
begin
ReturnRoute(NodeList);
Result := True;
end else
NodeList.Clear;
finally
{$IFDEF DBG_PERFLOG}
gPerfLogs.SectionLeave(psPathfinding);
{$ENDIF}
end;
end;
function TKMPathFinding.CanWalkTo(const aFrom: TKMPoint; bX, bY: SmallInt): Boolean;
begin
Result := gTerrain.CanWalkDiagonally(aFrom, bX, bY);
end;
function TKMPathFinding.IsWalkableTile(aX, aY: Word): Boolean;
begin
//If cell meets Passability then estimate it
Result := ((fPass * gTerrain.Land^[aY,aX].Passability) <> [])
and ((fAvoidLocked <> palAvoidAsUnwalkable) or not gTerrain.TileIsLocked(TKMPoint.New(aX,aY)));
end;
//How much it costs to move From -> To
function TKMPathFinding.MovementCost(aFromX, aFromY, aToX, aToY: Word): Cardinal;
const
AVOID_ROAD_PENALTY = 25; // 2.5 tiles
AVOID_LOCKED_PENALTY = 200; // 20 tiles
AVOID_UNIT_PENALTY = 15; // 1.5 tiles
var
DX, DY: Word;
U: TKMUnit;
begin
DX := Abs(aFromX - aToX);
DY := Abs(aFromY - aToY);
if DX > DY then
Result := DX * 10 + DY * 4
else
Result := DY * 10 + DX * 4;
//Do not add extra cost if the tile is the target, as it can cause a longer route to be chosen
if (aToX <> fLocB.X) or (aToY <> fLocB.Y) then
begin
U := gTerrain.Land^[aToY,aToX].IsUnit;
// Goind offroad, when unit has to walk on road is possible to avoid locked tiles,
// but unit has to get bakc to the road
if (fRouteKind = rkAvoid) and not gTerrain.CheckPassability(aToX, aToY, fPassBest) then
Inc(Result, AVOID_ROAD_PENALTY);
// Avoid congested areas on roads
if FEAT_AVOID_UNITS_IN_PATH and (U <> nil) and ((tpWalkRoad in fPass) or U.PathfindingShouldAvoid) then
Inc(Result, AVOID_UNIT_PENALTY); //Unit = 1.5 extra tiles
if (fAvoidLocked = palAvoidByMovementCost) and gTerrain.TileIsLocked(TKMPoint.New(aToX,aToY)) then
Inc(Result, AVOID_LOCKED_PENALTY); //In interaction avoid mode, working unit (or warrior attacking house) = 20 tiles
end;
end;
function TKMPathFinding.EstimateToFinish(aX, aY: Word): Cardinal;
var
DX, DY: Word;
begin
//Use Estim even if destination is Passability, as it will make it faster.
//Target should be in the right direction even though it's not our destination.
DX := Abs(fLocB.X - aX);
DY := Abs(fLocB.Y - aY);
if DX > DY then
Result := DX * 10 + DY * 4
else
Result := DY * 10 + DX * 4;
end;
function TKMPathFinding.DestinationReached(aX, aY: Word): Boolean;
begin
case fDestination of
pdLocation: Result := KMLengthDiag(aX, aY, fLocB) <= fDistance;
pdPassability: Result := gTerrain.GetConnectID(fTargetWalkConnect, TKMPoint.New(aX, aY)) = fTargetNetwork;
pdHouse: Result := fTargetHouse.InReach(TKMPoint.New(aX, aY), fDistance);
else Result := True;
end;
end;
//Cache the route incase it is needed soon
procedure TKMPathFinding.AddToCache(NodeList: TKMPointList);
var
I: Integer;
Best: Integer;
begin
//Find cached route with least weight and replace it
Best := 0;
for I := 1 to PATH_CACHE_MAX - 1 do
if fCache[I].Weight < fCache[Best].Weight then
Best := I;
fCache[Best].Weight := PATH_CACHE_INIT_WEIGHT;
fCache[Best].Pass := fPass;
fCache[Best].Route.Copy(NodeList);
end;
procedure TKMPathFinding.AddNoRouteAvoidLockedToCache;
var
I: Integer;
Best: Integer;
begin
//Find cached route with least weight and replace it
Best := 0;
for I := 1 to PATH_CACHE_NO_ROUTES_AVOID_LOCKED_MAX - 1 do
if fCacheAvoidLocked[I].TimeToLive < fCacheAvoidLocked[Best].TimeToLive then
Best := I;
fCacheAvoidLocked[Best].TimeToLive := PATH_CACHE_NO_ROUTES_AVOID_LOCKED_TTL;
fCacheAvoidLocked[Best].Pass := fPass;
fCacheAvoidLocked[Best].LocA := fLocA;
fCacheAvoidLocked[Best].LocB := fLocB;
end;
function TKMPathFinding.CacheHasNoRouteAvoidLocked: Boolean;
var
I: Integer;
Len: Single;
P: TKMPoint;
begin
Result := False;
for I := 0 to PATH_CACHE_NO_ROUTES_AVOID_LOCKED_MAX - 1 do
if (fCacheAvoidLocked[I].TimeToLive > 0)
and (fCacheAvoidLocked[I].Pass = fPass)
and (fCacheAvoidLocked[I].LocB = fLocB) then //Destination should be the same in cache and our path
begin
//But starting point in cache could be near our path starting point
P := fCacheAvoidLocked[I].LocA;
Len := KMLengthDiag(fLocA, P);
if (Len <= 1)
or ((Len < 2) and gTerrain.CanWalkDiagonally(fLocB, P.X, P.Y)) then // Check if we can walk diagonally
Exit(True);
end;
end;
function TKMPathFinding.TryRouteFromCache(NodeList: TKMPointList): Boolean;
const
MIN_POINTS_TO_CHECK_START = 10;
var
I,K: Integer;
BestStart, BestEnd: Integer;
NewL, BestL: Single;
P: TKMPoint;
IsWalkable: Boolean;
begin
//Makes compiler happy
Result := False;
for I := 0 to PATH_CACHE_MAX - 1 do
begin
BestStart := -1;
BestEnd := -1;
if (fCache[I].Route.Count > 0)
and (fCache[I].Pass = fPass) then
begin
//Check if route goes through out position
//We could check almost all points
for K := 0 to Max(MIN_POINTS_TO_CHECK_START, fCache[I].Route.Count - 1 - PATH_CACHE_NODES_MIN_CNT) do
begin
//Restrict cache to go through our starting point,
//otherwise some bad-looking behaviour possible
//F.e. units going in a row could make side step occasionaly,
//cause 1st one change route
if KMLengthDiag(fLocA, fCache[I].Route[K]) = 0 then
begin
BestStart := K;
Break;
end;
end;
if BestStart = -1 then Continue;
//Check if route ends within reach
BestL := MaxSingle;
for K := fCache[I].Route.Count - 1 downto BestStart + 1 do
begin
P := fCache[I].Route[K];
NewL := KMLengthDiag(fLocB, P);
if (NewL <= 1)
or ((NewL < 2) and gTerrain.CanWalkDiagonally(fLocB, P.X, P.Y)) then
begin
BestEnd := K;
BestL := NewL;
if NewL = 0 then //Path goes through our Destination
Break;
end;
end;
if BestL >= 2 then Continue;
//Check if cached path is still walkable
IsWalkable := True;
for K := BestStart to BestEnd do
begin
P := fCache[I].Route[K];
if not IsWalkableTile(P.X, P.Y) then
begin
IsWalkable := False;
Break;
end;
end;
//If not walkable anymore, then mark it as unused
//and continue
if not IsWalkable then
begin
fCache[I].Weight := 0;
fCache[I].Pass := [];
Continue;
end;
//Assemble the route
NodeList.Clear;
//No need to add fLocA, since its equal to BestStart point from Cached Route
for K := BestStart to BestEnd do
NodeList.Add(fCache[I].Route[K]);
if fLocB <> fCache[I].Route[BestEnd] then //No need to duplicate fLocB
NodeList.Add(fLocB);
//Mark the cached route as more useful
//TEMP do not mark route as more usefull, force recalc route after some stime
// Inc(fCache[I].Weight);
Result := True;
Exit;
end;
end;
end;
procedure TKMPathFinding.Save(SaveStream: TKMemoryStream);
var
I: Integer;
begin
SaveStream.PlaceMarker('PathFinding');
if CACHE_PATHFINDING then
for I := 0 to PATH_CACHE_MAX - 1 do
begin
SaveStream.Write(fCache[I].Weight);
SaveStream.Write(fCache[I].Pass, SizeOf(fCache[I].Pass));
fCache[I].Route.SaveToStream(SaveStream);
end;
SaveStream.PlaceMarker('PathFinding_CacheAvoidLocked');
if CACHE_PATHFINDING_AVOID_LOCKED then
for I := 0 to PATH_CACHE_NO_ROUTES_AVOID_LOCKED_MAX - 1 do
begin
SaveStream.Write(fCacheAvoidLocked[I].Pass, SizeOf(fCacheAvoidLocked[I].Pass));
SaveStream.Write(fCacheAvoidLocked[I].LocA, SizeOf(fCacheAvoidLocked[I].LocA));
SaveStream.Write(fCacheAvoidLocked[I].LocB, SizeOf(fCacheAvoidLocked[I].LocB));
SaveStream.Write(fCacheAvoidLocked[I].TimeToLive);
end;
end;
procedure TKMPathFinding.Load(LoadStream: TKMemoryStream);
var
I: Integer;
begin
LoadStream.CheckMarker('PathFinding');
if CACHE_PATHFINDING then
for I := 0 to PATH_CACHE_MAX - 1 do
begin
LoadStream.Read(fCache[I].Weight);
LoadStream.Read(fCache[I].Pass, SizeOf(fCache[I].Pass));
fCache[I].Route.LoadFromStream(LoadStream);
end;
LoadStream.CheckMarker('PathFinding_CacheAvoidLocked');
if CACHE_PATHFINDING_AVOID_LOCKED then
for I := 0 to PATH_CACHE_NO_ROUTES_AVOID_LOCKED_MAX - 1 do
begin
LoadStream.Read(fCacheAvoidLocked[I].Pass, SizeOf(fCacheAvoidLocked[I].Pass));
LoadStream.Read(fCacheAvoidLocked[I].LocA, SizeOf(fCacheAvoidLocked[I].LocA));
LoadStream.Read(fCacheAvoidLocked[I].LocB, SizeOf(fCacheAvoidLocked[I].LocB));
LoadStream.Read(fCacheAvoidLocked[I].TimeToLive);
end;
end;
procedure TKMPathFinding.UpdateState;
var
I: Integer;
begin
{$IFDEF DBG_PERFLOG}
gPerfLogs.SectionEnter(psPathfinding);
{$ENDIF}
try
if CACHE_PATHFINDING then
for I := 0 to PATH_CACHE_MAX - 1 do
fCache[I].Weight := Max(fCache[I].Weight - 1, 0);
if CACHE_PATHFINDING_AVOID_LOCKED then
for I := 0 to PATH_CACHE_NO_ROUTES_AVOID_LOCKED_MAX - 1 do
fCacheAvoidLocked[I].TimeToLive := Max(fCacheAvoidLocked[I].TimeToLive - 1, 0);
finally
{$IFDEF DBG_PERFLOG}
gPerfLogs.SectionLeave(psPathfinding);
{$ENDIF}
end;
end;
end.
@@ -0,0 +1,197 @@
unit KM_PathFindingAStarNew;
{$I KaM_Remake.inc}
interface
uses
SysUtils, Math, KM_PathFinding,
KM_CommonClasses, KM_Defaults, KM_Terrain, KM_Points, BinaryHeap;
type
PANodeRec = ^TANodeRec; // Delphi allows forward declaration for record pointers
TANodeRec = record
RouteID: Cardinal;
X,Y: SmallInt;
CostTo: Cardinal;
Estim: Cardinal;
Parent: PANodeRec;
end;
// This is a helper class for TKMTerrain
// Here should be pathfinding and all associated stuff
// I think we should refactor this unit and move some TKMTerrain methods here
TKMPathFindingAStarNew = class(TKMPathFinding)
private
fHeap: TBinaryHeap;
fMinN: PANodeRec;
fOpenRef: array [0..MAX_MAP_SIZE, 0..MAX_MAP_SIZE] of TANodeRec; //References to OpenList, Sized as map
fRouteID: Cardinal;
function HeapCmp(A,B: Pointer): Boolean;
protected
function GetNodeAt(X,Y: SmallInt): PANodeRec;
function MakeRoute: Boolean; override;
procedure ReturnRoute(NodeList: TKMPointList); override;
public
constructor Create;
destructor Destroy; override;
end;
implementation
{ TKMPathFindingAStarNew }
constructor TKMPathFindingAStarNew.Create;
var
X, Y: Integer;
begin
inherited;
fHeap := TBinaryHeap.Create(High(Word));
fHeap.Cmp := HeapCmp;
for Y := 0 to MAX_MAP_SIZE do
for X := 0 to MAX_MAP_SIZE do
begin
fOpenRef[Y, X].X := X;
fOpenRef[Y, X].Y := Y;
end;
end;
destructor TKMPathFindingAStarNew.Destroy;
begin
fHeap.Free;
inherited;
end;
function TKMPathFindingAStarNew.GetNodeAt(X, Y: SmallInt): PANodeRec;
begin
Result := @fOpenRef[Y, X];
if Result.RouteID <> fRouteID then
Result := nil;
end;
function TKMPathFindingAStarNew.HeapCmp(A, B: Pointer): Boolean;
begin
Result := (A = nil) or (B = nil) or (PANodeRec(A).Estim + PANodeRec(A).CostTo < PANodeRec(B).Estim + PANodeRec(B).CostTo);
end;
function TKMPathFindingAStarNew.MakeRoute: Boolean;
const
C_CLOSED = High(Cardinal);
var
N: PANodeRec;
X, Y: Word;
newCost: Cardinal;
begin
// Do not build the route in case destination is not walkable
if not IsWalkableTile(fLocA.X, fLocA.Y) then Exit(False);
//Use a unique RouteID each run to track whether nodes are valid
//Before it overflows we need to reset, which is still very fast (<1ms)
//and only needs doing once every 4.2 billion paths
if fRouteID = High(fRouteID) then
begin
fRouteID := 0;
for Y := 0 to MAX_MAP_SIZE do
for X := 0 to MAX_MAP_SIZE do
fOpenRef[Y, X].RouteID := 0;
end;
Inc(fRouteID);
//Initialize first element
N := @fOpenRef[fLocA.Y, fLocA.X];
N.RouteID := fRouteID;
N.CostTo := 0;
N.Estim := EstimateToFinish(fLocA.X, fLocA.Y);
N.Parent := nil;
//Seed
fMinN := N;
while (fMinN <> nil) and not DestinationReached(fMinN.X, fMinN.Y) do
begin
fMinN.Estim := C_CLOSED;
//Check all surrounding cells and issue costs to them
for Y := Max(fMinN.Y-1,1) to Min(fMinN.Y+1, gTerrain.MapY-1) do
for X := Max(fMinN.X-1,1) to Min(fMinN.X+1, gTerrain.MapX-1) do
begin
N := @fOpenRef[Y,X];
if N.RouteID <> fRouteID then //Cell is new
begin
if CanWalkTo(TKMPoint.New(fMinN.X, fMinN.Y), X, Y) then
begin
N.RouteID := fRouteID;
N.Parent := fMinN;
if IsWalkableTile(X, Y) then
begin
N.CostTo := fMinN.CostTo + MovementCost(fMinN.X, fMinN.Y, X, Y);
N.Estim := EstimateToFinish(X,Y);
fHeap.Push(N);
end
else //If cell doesn't meets Passability then mark it as Closed
N.Estim := C_CLOSED;
end;
end
else //Else cell is old
begin
//Node N is valid. If route through new cell is shorter than previous
if N.Estim <> C_CLOSED then
if CanWalkTo(TKMPoint.New(fMinN.X, fMinN.Y), X, Y) then
begin
newCost := MovementCost(fMinN.X, fMinN.Y, X, Y);
if fMinN.CostTo + newCost < N.CostTo then
begin
N.Parent := fMinN;
N.CostTo := fMinN.CostTo + newCost;
//todo -cPractical: Cost has changed and the location in the queue needs to change too
//fHeap.UpdateItem(N);
end;
end;
end;
end;
//Find next cell with least (Estim+CostTo)
if fHeap.IsEmpty then
Break;
fMinN := fHeap.Pop;
end;
//Route found, no longer need the lookups
fHeap.Clear;
Result := DestinationReached(fMinN.X, fMinN.Y);
end;
procedure TKMPathFindingAStarNew.ReturnRoute(NodeList: TKMPointList);
var
N: PANodeRec;
begin
NodeList.Clear;
//Assemble the route
N := fMinN;
while N <> nil do
begin
NodeList.Add(TKMPoint.New(N.X, N.Y));
N := N.Parent;
end;
//Reverse the list, since path is assembled LocB > LocA
NodeList.Inverse;
//Cache long paths
if CACHE_PATHFINDING and (NodeList.Count > PATH_CACHE_NODES_MIN_CNT) then
AddToCache(NodeList);
end;
end.
@@ -0,0 +1,149 @@
unit KM_PathFindingAStarOld;
{$I KaM_Remake.inc}
interface
uses
SysUtils, Math, KM_PathFinding,
KM_CommonClasses, KM_Defaults, KM_Terrain, KM_Points;
type
// This is a helper class for TKMTerrain
// Here should be pathfinding and all associated stuff
// I think we should refactor this unit and move some TKMTerrain methods here
TKMPathFindingAStarOld = class(TKMPathFinding)
private
fNewCost: Integer;
fMinCost: record
Cost: Integer;
ID: Word;
Pos: TKMPoint;
end;
ORef: array of array of Word; //References to OpenList, Sized as map
OCount: Word;
OList: array of record //List of checked cells
Pos: TKMPoint;
CostTo: Word;
Estim: Word;
Parent: Word;//Reference to parent
end;
protected
function MakeRoute: Boolean; override;
procedure ReturnRoute(NodeList: TKMPointList); override;
end;
implementation
{ TKMPathFindingAStarOld }
function TKMPathFindingAStarOld.MakeRoute: Boolean;
const
C_CLOSED = 65535;
var
I, X, Y: Integer;
begin
//Erase previous values
SetLength(ORef, 0);
SetLength(ORef, gTerrain.MapY+1, gTerrain.MapX+1);
//Initialize first element
OCount := 1;
ORef[fLocA.Y, fLocA.X] := OCount;
SetLength(OList, OCount + 1);
OList[OCount].Pos := fLocA;
OList[OCount].CostTo := 0;
OList[OCount].Estim := (abs(fLocB.X-fLocA.X) + abs(fLocB.Y-fLocA.Y)) * 10;
OList[OCount].Parent := 0;
//Seed
fMinCost.Cost := 0;
fMinCost.ID := 1;
fMinCost.Pos := fLocA;
while not DestinationReached(fMinCost.Pos.X, fMinCost.Pos.Y) and (fMinCost.Cost <> 65535) do
begin
OList[fMinCost.ID].Estim := C_CLOSED;
//Check all surrounding cells and issue costs to them
for y := Math.max(fMinCost.Pos.Y-1,1) to Math.min(fMinCost.Pos.Y+1, gTerrain.MapY-1) do
for x := Math.max(fMinCost.Pos.X-1,1) to Math.min(fMinCost.Pos.X+1, gTerrain.MapX-1) do
if ORef[y,x] = 0 then //Cell is new
begin
if CanWalkTo(fMinCost.Pos, x, y) then
begin
inc(OCount);
if OCount >= Length(OList) then
SetLength(OList, OCount + 128); //Allocate slightly more space
OList[OCount].Pos := TKMPoint.New(x,y);
if IsWalkableTile(X, Y) then
begin
ORef[y,x] := OCount;
OList[OCount].Parent := ORef[fMinCost.Pos.Y, fMinCost.Pos.X];
OList[OCount].CostTo := OList[OList[OCount].Parent].CostTo + MovementCost(fMinCost.Pos.X, fMinCost.Pos.Y, X, Y);
OList[OCount].Estim := (abs(x-fLocB.X) + abs(y-fLocB.Y)) * 10; //Use Estim even if destination is Passability, as it will make it faster. Target should be in the right direction even though it's not our destination.
end
else //If cell doen't meets Passability then mark it as Closed
OList[OCount].Estim := C_CLOSED;
end;
end
else //Else cell is old
begin
//If route through new cell is shorter than ORef[y,x] then
if OList[ORef[y,x]].Estim <> C_CLOSED then
if CanWalkTo(fMinCost.Pos, x, y) then
begin
fNewCost := MovementCost(fMinCost.Pos.X, fMinCost.Pos.Y, X, Y);
if OList[fMinCost.ID].CostTo + fNewCost < OList[ORef[y,x]].CostTo then
begin
OList[ORef[y,x]].Parent := ORef[fMinCost.Pos.Y,fMinCost.Pos.X];
OList[ORef[y,x]].CostTo := OList[fMinCost.ID].CostTo + fNewCost;
//OList[ORef[y,x]].Estim:=(abs(x-fLocB.X) + abs(y-fLocB.Y))*10;
end;
end;
end;
//Find next cell with least (Estim+CostTo)
fMinCost.Cost := 65535;
for i := OCount downto 1 do //'downto' works faster here
if OList[i].Estim <> C_CLOSED then
if (OList[i].Estim + OList[i].CostTo) < fMinCost.Cost then
begin
fMinCost.Cost := OList[i].Estim + OList[i].CostTo;
fMinCost.ID := i;
fMinCost.Pos := OList[i].Pos;
end;
end;
Result := DestinationReached(fMinCost.Pos.X, fMinCost.Pos.Y);
//Assert(fMinCost.Cost<>65535, 'FloodFill test failed and there''s no possible route A-B');
end;
procedure TKMPathFindingAStarOld.ReturnRoute(NodeList: TKMPointList);
var
I: Integer;
begin
NodeList.Clear;
//Assemble the route
I := fMinCost.ID;
repeat
NodeList.Add(OList[I].Pos);
I := OList[I].Parent;
until I = 0;
//Reverse the list, since path is assembled LocB > LocA
NodeList.Inverse;
//Cache long paths
if CACHE_PATHFINDING and (NodeList.Count > 20) then
AddToCache(NodeList);
end;
end.
+414
View File
@@ -0,0 +1,414 @@
unit KM_PathFindingJPS;
{$I KaM_Remake.inc}
interface
uses
Types, Math, SysUtils, KM_PathFinding, BinaryHeap, KM_Points, KM_CommonClasses, KM_Terrain;
type
TJPSPoint = class
x,y: Word;
Estim: Word; //Estimated distance to End
g, f: Single; //G - CostTo, F - CostTo + Estim
opened, closed: Boolean;
parent: TJPSPoint;
end;
TPointArray = array of TPoint;
// Jump-Point-Search pathfinder
// based on JavaScript implementation by aniero / https://github.com/aniero
TKMPathFindingJPS = class(TKMPathFinding)
private
Nodes: array of array of TJPSPoint;
startNode, endNode: TJPSPoint;
openList: TBinaryHeap;
function HeapCmp(A,B: Pointer): Boolean;
procedure Reset;
function getNodeAt(x, y: SmallInt): TJPSPoint;
procedure identifySuccessors(node: TJPSPoint);
function findNeighbors(const node: TJPSPoint): TPointArray;
function jump(x, y, px, py: SmallInt): TPoint;
protected
function IsWalkableTile(aX, aY: SmallInt): Boolean; reintroduce;
function MakeRoute: Boolean; override;
procedure ReturnRoute(NodeList: TKMPointList); override;
public
constructor Create;
destructor Destroy; override;
end;
implementation
{ TKMPathFindingJPS }
constructor TKMPathFindingJPS.Create;
begin
inherited;
openList := TBinaryHeap.Create(High(Word));
openList.Cmp := HeapCmp;
end;
destructor TKMPathFindingJPS.Destroy;
begin
openList.Free;
inherited;
end;
function TKMPathFindingJPS.HeapCmp(A, B: Pointer): Boolean;
begin
if A = nil then
Result := True
else
Result := (B = nil) or (TJPSPoint(A).f < TJPSPoint(B).f);
end;
procedure TKMPathFindingJPS.Reset;
var
I, K: Integer;
begin
openList.Clear;
for I := 0 to High(Nodes) do
for K := 0 to High(Nodes[I]) do
FreeAndNil(Nodes[I,K]);
end;
function TKMPathFindingJPS.getNodeAt(x, y: SmallInt): TJPSPoint;
begin
if Nodes[y,x] = nil then
begin
Nodes[y,x] := TJPSPoint.Create;
Nodes[y,x].x := x;
Nodes[y,x].y := y;
end;
Result := Nodes[y,x];
end;
//Find and return the the path.
function TKMPathFindingJPS.MakeRoute: Boolean;
var
node: TJPSPoint;
begin
SetLength(Nodes, gTerrain.MapY+1, gTerrain.MapX+1);
startNode := getNodeAt(fLocA.X, fLocA.Y);
endNode := getNodeAt(fLocB.X, fLocB.Y);
// set the `g` and `f` value of the start node to be 0
startNode.g := 0;
startNode.f := 0;
openList.Push(startNode);
startNode.opened := True;
while not openList.IsEmpty do
begin
// pop the position of node which has the minimum `f` value.
node := openList.Pop;
node.closed := True;
if node = endNode then
begin
Result := True;
Exit;
end;
identifySuccessors(node);
end;
// fail to find the path
Reset;
Result := False;
end;
procedure TKMPathFindingJPS.ReturnRoute(NodeList: TKMPointList);
var
node: TJPSPoint;
begin
NodeList.Clear;
node := endNode;
while node <> nil do
begin
NodeList.Add(TKMPoint.New(node.X, node.Y));
node := node.parent;
end;
//Reverse the array
NodeList.Inverse;
NodeList.SparseToDense;
Reset;
end;
{**
* Identify successors for the given node. Runs a jump point search in the
* direction of each available neighbor, adding any points found to the open
* list.
* @protected
*}
procedure TKMPathFindingJPS.identifySuccessors(node: TJPSPoint);
var
I: Integer;
endX, endY: SmallInt;
x, y, jx, jy: SmallInt;
neighbors: TPointArray;
neighbor, jumpPoint: TPoint;
jumpNode: TJPSPoint;
d, ng: Single;
begin
endX := endNode.X;
endY := endNode.Y;
x := node.X;
y := node.Y;
neighbors := findNeighbors(node);
for I := 0 to High(neighbors) do
begin
neighbor := neighbors[i];
jumpPoint := jump(neighbor.X, neighbor.Y, x, y);
if (jumpPoint.X <> -1) then
begin
jx := jumpPoint.X;
jy := jumpPoint.Y;
jumpNode := getNodeAt(jx, jy);
if (jumpNode.closed) then
Continue;
// include distance, as parent may not be immediately adjacent:
d := Sqrt(Sqr(jx - x) + Sqr(jy - y));
ng := node.g + d; // next `g` value
if (not jumpNode.opened) or (ng < jumpNode.g) then
begin
jumpNode.g := ng;
if jumpNode.Estim = 0 then
jumpNode.Estim := (Abs(jx - endX) + Abs(jy - endY));
jumpNode.f := jumpNode.g + jumpNode.Estim;
jumpNode.parent := node;
if not jumpNode.opened then
begin
openList.Push(jumpNode);
jumpNode.opened := True;
end
else
begin
openList.UpdateItem(jumpNode);
end;
end;
end;
end;
end;
function TKMPathFindingJPS.IsWalkableTile(aX, aY: SmallInt): Boolean;
begin
Result := gTerrain.TileInMapCoords(aX, aY) and (inherited IsWalkableTile(aX, aY));
end;
{**
Search recursively in the direction (parent -> child), stopping only when a
* jump point is found.
* @protected
* @return Array.<[number, number]> The x, y coordinate of the jump point
* found, or null if not found
*}
function TKMPathFindingJPS.jump(x, y, px, py: SmallInt): TPoint;
var
dx, dy: SmallInt;
jx, jy: TPoint;
begin
if not IsWalkableTile(x, y) then
begin
Result := Point(-1, -1);
Exit;
end
else
if (getNodeAt(x,y) = endNode) then
begin
Result := Point(x, y);
Exit;
end;
dx := x - px;
dy := y - py;
// check for forced neighbors
// along the diagonal
if (dx <> 0) and (dy <> 0) then
begin
if ((IsWalkableTile(x - dx, y + dy) and not IsWalkableTile(x - dx, y)) or
(IsWalkableTile(x + dx, y - dy) and not IsWalkableTile(x, y - dy))) then
begin
Result := Point(x, y);
Exit;
end;
end
// horizontally/vertically
else
begin
if( dx <> 0 ) then // moving along x
begin
if((IsWalkableTile(x + dx, y + 1) and not IsWalkableTile(x, y + 1)) or
(IsWalkableTile(x + dx, y - 1) and not IsWalkableTile(x, y - 1))) then
begin
Result := Point(x, y);
Exit;
end;
end
else
begin
if((IsWalkableTile(x + 1, y + dy) and not IsWalkableTile(x + 1, y)) or
(IsWalkableTile(x - 1, y + dy) and not IsWalkableTile(x - 1, y))) then
begin
Result := Point(x, y);
Exit;
end;
end;
end;
// when moving diagonally, must check for vertical/horizontal jump points
if (dx <> 0) and (dy <> 0) then
begin
jx := jump(x + dx, y, x, y);
jy := jump(x, y + dy, x, y);
if (jx.x <> -1) or (jy.x <> -1) then
begin
Result := Point(x, y);
Exit;
end;
end;
// moving diagonally, must make sure one of the vertical/horizontal
// neighbors is open to allow the path
if (IsWalkableTile(x + dx, y) or IsWalkableTile(x, y + dy)) then
Result := jump(x + dx, y + dy, x, y)
else
Result := Point(-1, -1);
end;
//Find the neighbors for the given node. If the node has a parent,
//prune the neighbors based on the jump point search algorithm, otherwise
//return all available neighbors.
//@return {Array.<[number, number]>} The neighbors found.
function TKMPathFindingJPS.findNeighbors(const node: TJPSPoint): TPointArray;
var
count: SmallInt;
procedure Push(ax,ay: SmallInt);
begin
Result[count].X := ax;
Result[count].Y := ay;
Inc(count);
end;
var
parent: TJPSPoint;
x, y: SmallInt;
px, py, dx, dy: SmallInt;
begin
count := 0;
SetLength(Result, 8);
parent := node.parent;
x := node.x;
y := node.y;
// directed pruning: can ignore most neighbors, unless forced.
if (parent <> nil) then
begin
px := parent.x;
py := parent.y;
// get the normalized direction of travel
dx := Round((x - px) / max(abs(x - px), 1));
dy := Round((y - py) / max(abs(y - py), 1));
// search diagonally
if (dx <> 0) and (dy <> 0) then
begin
if IsWalkableTile(x, y + dy) then
Push(x, y + dy);
if IsWalkableTile(x + dx, y) then
Push(x + dx, y);
if IsWalkableTile(x, y + dy) or IsWalkableTile(x + dx, y) then
Push(x + dx, y + dy);
if (not IsWalkableTile(x - dx, y)) and IsWalkableTile(x, y + dy) then
Push(x - dx, y + dy);
if (not IsWalkableTile(x, y - dy)) and IsWalkableTile(x + dx, y) then
Push(x + dx, y - dy);
end
// search horizontally/vertically
else
begin
if (dx = 0) then
begin
if IsWalkableTile(x, y + dy) then
begin
if IsWalkableTile(x, y + dy) then
Push(x, y + dy);
if not IsWalkableTile(x + 1, y) then
Push(x + 1, y + dy);
if not IsWalkableTile(x - 1, y) then
Push(x - 1, y + dy);
end;
end
else
begin
if IsWalkableTile(x + dx, y) then
begin
if IsWalkableTile(x + dx, y) then
Push(x + dx, y);
if not IsWalkableTile(x, y + 1) then
Push(x + dx, y + 1);
if not IsWalkableTile(x, y - 1) then
Push(x + dx, y - 1);
end;
end;
end;
end
// return all neighbors (if parent = nil)
else
begin
if IsWalkableTile(x, y-1) then
Push(x, y-1);
if IsWalkableTile(x+1, y) then
Push(x+1, y);
if IsWalkableTile(x, y+1) then
Push(x, y+1);
if IsWalkableTile(x-1, y) then
Push(x-1, y);
if IsWalkableTile(x-1, y-1) then
Push(x-1, y-1);
if IsWalkableTile(x+1, y-1) then
Push(x+1, y-1);
if IsWalkableTile(x+1, y+1) then
Push(x+1, y+1);
if IsWalkableTile(x-1, y+1) then
Push(x-1, y+1);
end;
SetLength(Result, count);
end;
end.
+181
View File
@@ -0,0 +1,181 @@
unit KM_PathFindingRoad;
{$I KaM_Remake.inc}
interface
uses
SysUtils,
KM_CommonClasses, KM_Defaults, KM_PathFinding, KM_PathFindingAStarNew, KM_Points;
type
// Pathfinding that finds a route for a road to be built
//todo -cComplicated: Maybe it is worth trying to make Roadfinder a house-aware algo,
// to prefer connecting to supply/demand houses
TKMPathFindingRoad = class(TKMPathFindingAStarNew)
private
fRoadConnectID: Byte;
protected
fOwner: TKMHandID; // fOwner MUST BE VISIBLE for child-classes
function CanWalkTo(const aFrom: TKMPoint; aToX, aToY: SmallInt): Boolean; override;
function DestinationReached(aX, aY: Word): Boolean; override;
function IsWalkableTile(aX, aY: Word): Boolean; override;
function MovementCost(aFromX, aFromY, aToX, aToY: Word): Cardinal; override;
function EstimateToFinish(aX, aY: Word): Cardinal; override;
public
constructor Create(aOwner: TKMHandID);
procedure OwnerUpdate(aPlayer: TKMHandID);
function Route_Make(const aLocA, aLocB: TKMPoint; NodeList: TKMPointList): Boolean; reintroduce;
function Route_ReturnToWalkable(const aLocA, aLocB: TKMPoint; aRoadConnectID: Byte; NodeList: TKMPointList): Boolean; reintroduce;
procedure Save(SaveStream: TKMemoryStream); override;
procedure Load(LoadStream: TKMemoryStream); override;
end;
// Minor variation on class above for creating shortcuts in AI road network
TKMPathFindingRoadShortcuts = class(TKMPathFindingRoad)
protected
function DestinationReached(aX, aY: Word): Boolean; override;
function MovementCost(aFromX, aFromY, aToX, aToY: Word): Cardinal; override;
end;
implementation
uses
KM_HandsCollection, KM_TerrainTypes, KM_Terrain, KM_Hand;
{ TKMPathFindingRoad }
constructor TKMPathFindingRoad.Create(aOwner: TKMHandID);
begin
inherited Create;
fOwner := aOwner;
end;
procedure TKMPathFindingRoad.OwnerUpdate(aPlayer: TKMHandID);
begin
fOwner := aPlayer;
end;
procedure TKMPathFindingRoad.Save(SaveStream: TKMemoryStream);
begin
inherited;
SaveStream.PlaceMarker('PathFindingRoad');
SaveStream.Write(fOwner);
SaveStream.Write(fRoadConnectID);
end;
procedure TKMPathFindingRoad.Load(LoadStream: TKMemoryStream);
begin
inherited;
LoadStream.CheckMarker('PathFindingRoad');
LoadStream.Read(fOwner);
LoadStream.Read(fRoadConnectID);
end;
function TKMPathFindingRoad.CanWalkTo(const aFrom: TKMPoint; aToX, aToY: SmallInt): Boolean;
begin
//Roads can't go diagonally, only in 90 turns
Result := (aToX = aFrom.X) or (aToY = aFrom.Y);
end;
function TKMPathFindingRoad.MovementCost(aFromX, aFromY, aToX, aToY: Word): Cardinal;
var
isRoad: Boolean;
begin
isRoad := (tpWalkRoad in gTerrain.Land^[aToY, aToX].Passability)
or (gHands[fOwner].Constructions.FieldworksList.HasField(KMPoint(aToX, aToY)) = ftRoad)
or (gTerrain.Land^[aToY, aToX].TileLock = tlRoadWork);
//Since we don't allow roads to be built diagonally we can assume
//path is always 1 tile = 1 point
if isRoad then
Result := 0
else
Result := 1;
//Building roads over fields is discouraged unless unavoidable
if gTerrain.TileIsCornField(KMPoint(aToX, aToY))
or gTerrain.TileIsWineField(KMPoint(aToX, aToY)) then
Inc(Result, 6); //6 tiles penalty
end;
function TKMPathFindingRoad.EstimateToFinish(aX, aY: Word): Cardinal;
begin
case fDestination of
pdLocation: //Rough estimation
Result := (Abs(aX - fLocB.X) + Abs(aY - fLocB.Y));
pdPassability: //Every direction is equaly good
Result := 0;
else Result := 0;
end;
end;
function TKMPathFindingRoad.IsWalkableTile(aX, aY: Word): Boolean;
begin
Result := ( ([tpMakeRoads, tpWalkRoad] * gTerrain.Land^[aY,aX].Passability <> []) OR (gTerrain.Land^[aY, aX].TileLock = tlRoadWork) )
and (gHands[fOwner].Constructions.FieldworksList.HasField(KMPoint(aX, aY)) in [ftNone, ftRoad])
and not gHands[fOwner].Constructions.HousePlanList.HasPlan(KMPoint(aX, aY)); // This will ignore allied plans but I guess that it will not cause trouble
end;
function TKMPathFindingRoad.DestinationReached(aX, aY: Word): Boolean;
begin
Result := ((aX = fLocB.X) and (aY = fLocB.Y)) //We reached destination point
or ((gTerrain.Land^[aY, aX].TileOverlay = toRoad) //We reached destination road network
and (fRoadConnectID <> 0) //No network
and (gTerrain.GetRoadConnectID(KMPoint(aX, aY)) = fRoadConnectID));
end;
function TKMPathFindingRoad.Route_Make(const aLocA, aLocB: TKMPoint; NodeList: TKMPointList): Boolean;
begin
Result := inherited Route_Make(aLocA, aLocB, [tpMakeRoads, tpWalkRoad], 0, nil, NodeList);
end;
//Even though we are only going to a road network it is useful to know where our target is so we start off in the right direction (makes algorithm faster/work over long distances)
function TKMPathFindingRoad.Route_ReturnToWalkable(const aLocA, aLocB: TKMPoint; aRoadConnectID: Byte; NodeList: TKMPointList): Boolean;
begin
fRoadConnectID := aRoadConnectID;
Result := inherited Route_ReturnToWalkable(aLocA, aLocB, wcRoad, 0, [tpMakeRoads, tpWalkRoad], NodeList);
end;
{ TKMPathFindingRoadShortcuts }
function TKMPathFindingRoadShortcuts.MovementCost(aFromX, aFromY, aToX, aToY: Word): Cardinal;
var
isRoad: Boolean;
begin
//Since we don't allow roads to be built diagonally we can assume
//path is always 1 tile
Result := 1;
//Off road costs extra
isRoad := (tpWalkRoad in gTerrain.Land^[aToY, aToX].Passability)
or (gHands[fOwner].Constructions.FieldworksList.HasField(KMPoint(aToX, aToY)) = ftRoad)
or (gTerrain.Land^[aToY, aToX].TileLock = tlRoadWork);
if not isRoad then
Inc(Result, 3);
//Building roads over fields is discouraged unless unavoidable
if gTerrain.TileIsCornField(KMPoint(aToX, aToY))
or gTerrain.TileIsWineField(KMPoint(aToX, aToY)) then
Inc(Result, 4);
end;
function TKMPathFindingRoadShortcuts.DestinationReached(aX, aY: Word): Boolean;
begin
Result := ((aX = fLocB.X) and (aY = fLocB.Y)); //We reached destination point
end;
end.