0ad/binaries/data/mods/public/simulation/ai/common-api/terrain-analysis-pathfinder.js
2014-12-23 14:17:52 +00:00

345 lines
11 KiB
JavaScript

var API3 = function(m)
{
// An implementation of A* as a pathfinder.
// It's oversamplable, and has a specific "distance from block"
// variable to avoid narrow passages if wanted.
// It can carry a calculation over multiple turns.
// It can work over water, or land.
// Note: while theoretically possible, when this goes from land to water
// It will return the path to the "boarding" point and
// a new path will need to be created.
// this should only be called by an AI player after setting gamestate.ai
// The initializer creates an expanded influence map for checking.
// It's not extraordinarily slow, but it's not exactly fast either.
m.aStarPath = function(gameState, onWater, disregardEntities, targetTerritory)
{
// get the terrain analyzer map as a reference.
this.Map(gameState.ai, "passability", gameState.ai.terrainAnalyzer.map);
// get the accessibility as a reference
this.accessibility = gameState.ai.accessibility;
this.terrainAnalyzer = gameState.ai.terrainAnalyzer;
if (onWater)
this.waterPathfinder = true;
else
this.waterPathfinder = false;
var pathObstruction = gameState.sharedScript.passabilityClasses["pathfinderObstruction"];
var passMap = gameState.sharedScript.passabilityMap;
var terrMap = gameState.sharedScript.territoryMap;
this.widthMap = new Uint8Array(this.map.length);
for (var i = 0; i < this.map.length; ++i)
{
if (this.map[i] == 0)
this.widthMap[i] = 0;
else if (!disregardEntities && (((terrMap.data[i] & 0x3F) !== gameState.ai.player && (terrMap.data[i] & 0x3F) !== 0
&& (terrMap.data[i] & 0x3F) !== targetTerritory) || (passMap.data[i] & pathObstruction)))
this.widthMap[i] = 1; // we try to avoid enemy territory and pathfinder obstructions.
else if (!disregardEntities && this.map[i] == 30)
this.widthMap[i] = 0;
else if (!disregardEntities && this.map[i] == 40)
this.widthMap[i] = 0;
else if (!disregardEntities && this.map[i] == 41)
this.widthMap[i] = 2;
else if (!disregardEntities && this.map[i] == 42)
this.widthMap[i] = 1;
else if (!onWater && this.map[i] == 201)
this.widthMap[i] = 1;
else
this.widthMap[i] = 255;
}
this.expandInfluences(255, this.widthMap);
}
m.copyPrototype(m.aStarPath, m.Map);
// marks some points of the map as impassable. This can be used to create different paths, or to avoid going through some areas.
m.aStarPath.prototype.markImpassableArea = function(cx, cy, Distance)
{
[cx,cy] = this.gamePosToMapPos([cx,cy]);
var x0 = Math.max(0, cx - Distance);
var y0 = Math.max(0, cy - Distance);
var x1 = Math.min(this.width, cx + Distance);
var y1 = Math.min(this.height, cy + Distance);
var maxDist2 = Distance * Distance;
for ( var y = y0; y < y1; ++y)
{
for ( var x = x0; x < x1; ++x)
{
var dx = x - cx;
var dy = y - cy;
var r2 = dx*dx + dy*dy;
if (r2 < maxDist2)
this.widthMap[x + y * this.width] = 0;
}
}
};
// sending gamestate creates a map
// (you run the risk of "jumping" over obstacles or weird behavior.
m.aStarPath.prototype.getPath = function(start, end, Sampling, preferredWidth, iterationLimit, gamestate)
{
this.Sampling = Sampling >= 1 ? Sampling : 1;
this.minWidth = 1;
this.preferredWidth = (preferredWidth !== undefined && preferredWidth >= this.Sampling) ? preferredWidth : this.Sampling;
if (start[0] < 0 || this.gamePosToMapPos(start)[0] >= this.width || start[1] < 0 || this.gamePosToMapPos(start)[1] >= this.height)
return undefined;
var s = this.terrainAnalyzer.findClosestPassablePoint(this.gamePosToMapPos(start), !this.waterPathfinder, 500, false);
var e = this.terrainAnalyzer.findClosestPassablePoint(this.gamePosToMapPos(end), !this.waterPathfinder, 500, true);
var w = this.width;
if (!s || !e)
return undefined;
if (gamestate !== undefined)
{
this.TotorMap = new m.Map(gamestate);
this.TotorMap.addInfluence(s[0], s[1], 1, 200, 'constant');
this.TotorMap.addInfluence(e[0], e[1], 1, 200, 'constant');
}
this.iterationLimit = 9000000000;
if (iterationLimit !== undefined)
this.iterationLimit = iterationLimit;
this.s = s[0] + w*s[1];
this.e = e[0] + w*e[1];
if (this.waterPathfinder && this.map[this.s] != 200 && this.map[this.s] != 201)
{
m.debug ("Trying a path over water, but we are on land, aborting");
return undefined;
}
else if (!this.waterPathfinder && this.map[this.s] == 200)
{
m.debug ("Trying a path over land, but we are over water, aborting");
return undefined;
}
this.onWater = this.waterPathfinder;
this.pathChangesTransport = false;
// We are going to create a map, it's going to take memory. To avoid OOM errors, GC before we do so.
//Engine.ForceGC();
this.openList = [];
this.parentSquare = new Uint32Array(this.map.length);
this.isOpened = new Boolean(this.map.length);
this.fCostArray = new Uint32Array(this.map.length);
this.gCostArray = new Uint32Array(this.map.length);
this.currentSquare = this.s;
this.totalIteration = 0;
this.isOpened[this.s] = true;
this.openList.push(this.s);
this.fCostArray[this.s] = m.SquareVectorDistance([this.s%w, Math.floor(this.s/w)], [this.e%w, Math.floor(this.e/w)]);
this.gCostArray[this.s] = 0;
this.parentSquare[this.s] = this.s;
return this.continuePath(gamestate);
}
// in case it's not over yet, this can carry on the calculation of a path over multiple turn until it's over
m.aStarPath.prototype.continuePath = function(gamestate)
{
var w = this.width;
var h = this.height;
var positions = [[0,1], [0,-1], [1,0], [-1,0], [1,1], [-1,-1], [1,-1], [-1,1]];
var cost = [100,100,100,100,150,150,150,150];
//creation of variables used in the loop
var found = false;
var shortcut = false;
var infinity = Math.min();
var currentDist = infinity;
var e = this.e;
var s = this.s;
var iteration = 0;
var target = [this.e%w, Math.floor(this.e/w)];
var changes = {};
var tIndex = 0;
// on to A*
while (found === false && this.openList.length != 0 && iteration < this.iterationLimit)
{
currentDist = infinity;
if (shortcut === true)
this.currentSquare = this.openList.shift();
else
{
for (var i in this.openList)
{
var sum = this.fCostArray[this.openList[i]] + this.gCostArray[this.openList[i]];
if (sum < currentDist)
{
this.currentSquare = this.openList[i];
tIndex = i;
currentDist = sum;
}
}
this.openList.splice(tIndex,1);
}
if (!this.onWater && this.map[this.currentSquare] == 200)
this.onWater = true;
else if (this.onWater && (this.map[this.currentSquare] != 200 && this.map[this.currentSquare] != 201))
this.onWater = false;
shortcut = false;
this.isOpened[this.currentSquare] = false;
if (gamestate !== undefined)
this.TotorMap.addInfluence(this.currentSquare % w, Math.floor(this.currentSquare / w), 1, 40, 'constant');
for (var i in positions)
{
var index = 0 + this.currentSquare +positions[i][0]*this.Sampling +w*this.Sampling*positions[i][1];
if (this.widthMap[index] >= this.minWidth || (this.onWater && this.map[index] > 0 && this.map[index] != 200 && this.map[index] != 201)
|| (!this.onWater && this.map[index] == 200))
{
if(this.isOpened[index] === undefined)
{
this.parentSquare[index] = this.currentSquare;
this.fCostArray[index] = m.SquareVectorDistance([index%w, Math.floor(index/w)], target);// * cost[i];
this.gCostArray[index] = this.gCostArray[this.currentSquare] + cost[i] * this.Sampling;// - this.map[index];
if (!this.onWater && this.map[index] == 200)
this.gCostArray[index] += 10000;
else if (this.onWater && this.map[index] != 200)
this.gCostArray[index] += 10000;
if (this.widthMap[index] < this.preferredWidth)
this.gCostArray[index] += 200 * (this.preferredWidth-this.widthMap[index]);
if (this.map[index] == 200 || (this.map[index] == 201 && this.onWater))
this.gCostArray[index] += 1000;
if (this.openList[0] !== undefined && this.fCostArray[this.openList[0]] + this.gCostArray[this.openList[0]] > this.fCostArray[index] + this.gCostArray[index])
{
this.openList.unshift(index);
shortcut = true;
}
else
this.openList.push(index);
this.isOpened[index] = true;
if (m.SquareVectorDistance( [index%w, Math.floor(index/w)] , target) <= this.Sampling*this.Sampling-1)
{
if (this.e != index)
this.parentSquare[this.e] = index;
found = true;
break;
}
}
else
{
var addCost = 0;
if (!this.onWater && this.map[index] == 200)
addCost += 10000;
else if (this.onWater && this.map[index] != 200)
addCost += 10000;
if (this.widthMap[index] < this.preferredWidth)
addCost += 200 * (this.preferredWidth-this.widthMap[index]);
if (this.map[index] == 200 || (this.map[index] == 201 && this.onWater))
addCost += 1000;
// already on the Open or closed list
if (this.gCostArray[index] > cost[i] * this.Sampling + addCost + this.gCostArray[this.currentSquare])
{
this.parentSquare[index] = this.currentSquare;
this.gCostArray[index] = cost[i] * this.Sampling + addCost + this.gCostArray[this.currentSquare];
}
}
}
}
iteration++;
}
this.totalIteration += iteration;
if (iteration == this.iterationLimit && found === false && this.openList.length != 0)
{
// we've got to assume that we stopped because we reached the upper limit of iterations
return "toBeContinued";
}
//m.debug (this.totalIteration);
var paths = [];
if (found)
{
this.currentSquare = e;
var lastPosx = 0;
var lastPosy = 0;
while (this.parentSquare[this.currentSquare] !== s)
{
this.currentSquare = this.parentSquare[this.currentSquare];
if (!this.onWater && this.map[this.currentSquare] == 200)
{
//m.debug ("We must cross water, going " +this.currentSquare + " from parent " + this.parentSquare[this.currentSquare]);
this.pathChangesTransport = true;
changes[this.currentSquare] = true;
this.onWater = true;
}
else if (this.onWater && (this.map[this.currentSquare] != 200 && this.map[this.currentSquare] != 201))
{
//m.debug ("We must cross to the ground, going " +this.currentSquare + " from parent " + this.parentSquare[this.currentSquare]);
this.pathChangesTransport = true;
changes[this.currentSquare] = true;
this.onWater = false;
}
if (gamestate !== undefined && changes[this.currentSquare])
this.TotorMap.addInfluence(this.currentSquare % w, Math.floor(this.currentSquare / w), 2, 200, 'constant');
if (gamestate !== undefined)
this.TotorMap.addInfluence(this.currentSquare % w, Math.floor(this.currentSquare / w), 1, 50, 'constant');
if (m.SquareVectorDistance([lastPosx,lastPosy],[this.currentSquare % w, Math.floor(this.currentSquare / w)]) > 300 || changes[this.currentSquare])
{
lastPosx = (this.currentSquare % w);
lastPosy = Math.floor(this.currentSquare / w);
paths.push([ [lastPosx*this.cellSize,lastPosy*this.cellSize], changes[this.currentSquare] ]);
if (gamestate !== undefined)
this.TotorMap.addInfluence(this.currentSquare % w, Math.floor(this.currentSquare / w),1,50 + paths.length,'constant');
}
}
}
else
{
// we have not found a path.
// what do we do then?
}
if (gamestate !== undefined)
this.TotorMap.dumpIm("Path From " +s +" to " +e +".png",255);
delete this.parentSquare;
delete this.isOpened;
delete this.fCostArray;
delete this.gCostArray;
// the return, if defined is [ [path, each waypoint being [position, mustchangeTransport] ], is there any transport change, ]
if (paths.length > 0)
return [paths, this.pathChangesTransport];
else
return undefined;
}
return m;
}(API3);