1
1
forked from 0ad/0ad

AI: start switch from the internal JS pathFinder to the c++ one, thanks to Itms implementation. Still wip

This was SVN commit r16809.
This commit is contained in:
mimo 2015-06-22 17:19:24 +00:00
parent d045bb87d6
commit 7ac4b53cb8
5 changed files with 65 additions and 618 deletions

View File

@ -213,9 +213,8 @@ m.SharedScript.prototype.onUpdate = function(state)
for (var i in this.gameState)
this.gameState[i].update(this,state);
// TODO: merge those two with "ApplyEntitiesDelta" since after all they do the same.
// TODO: merge this with "ApplyEntitiesDelta" since after all they do the same.
this.updateResourceMaps(this, this.events);
this.terrainAnalyzer.updateMapWithEvents(this);
Engine.ProfileStop();
};

View File

@ -1,350 +0,0 @@
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 passMap = gameState.sharedScript.passabilityMap;
var terrMap = gameState.sharedScript.territoryMap;
var ratio = terrMap.cellSize / passMap.cellSize;
var pathObstruction = gameState.sharedScript.passabilityClasses["default"];
this.widthMap = new Uint8Array(this.map.length);
for (var i = 0; i < this.map.length; ++i)
{
let ix = i % passMap.width;
let iz = Math.floor(i / passMap.width);
ix = Math.floor(ix / ratio);
iz = Math.floor(iz / ratio);
let j = ix + iz*terrMap.width;
if (this.map[i] == 0)
this.widthMap[i] = 0;
else if (!disregardEntities && (((terrMap.data[j] & 0x3F) !== gameState.ai.player && (terrMap.data[j] & 0x3F) !== 0
&& (terrMap.data[j] & 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);

View File

@ -4,15 +4,12 @@ var API3 = function(m)
/*
* TerrainAnalysis, inheriting from the Map Component.
*
* This creates a suitable passability map for pathfinding units and provides the findClosestPassablePoint() function.
* This creates a suitable passability map.
* This is part of the Shared Script, and thus should only be used for things that are non-player specific.
* This.map is a map of the world, where particular stuffs are pointed with a value
* For example, impassable land is 0, water is 200, areas near tree (ie forest grounds) are 41
* This is intended for use with 8 bit maps for reduced memory usage.
* Upgraded from QuantumState's original TerrainAnalysis for qBot.
* You may notice a lot of the A* star codes differ only by a few things.
* It's wanted: each does a very slightly different things
* But truly separating optimizes.
*/
m.TerrainAnalysis = function()
@ -21,7 +18,7 @@ m.TerrainAnalysis = function()
m.copyPrototype(m.TerrainAnalysis, m.Map);
m.TerrainAnalysis.prototype.init = function(sharedScript,rawState)
m.TerrainAnalysis.prototype.init = function(sharedScript, rawState)
{
var passabilityMap = rawState.passabilityMap;
this.width = passabilityMap.width;
@ -38,12 +35,9 @@ m.TerrainAnalysis.prototype.init = function(sharedScript,rawState)
200 is deep water (ie non-passable by land units)
201 is shallow water (passable by land units and water units)
255 is land (or extremely shallow water where ships can't go).
40 is "tree".
The following 41-49 range is "near a tree", with the second number showing how many trees this tile neighbors.
30 is "geological component", such as a mine
*/
for (var i = 0; i < passabilityMap.data.length; ++i)
for (let i = 0; i < passabilityMap.data.length; ++i)
{
// If impassable for land units, set to 0, else to 255.
obstructionTiles[i] = (passabilityMap.data[i] & obstructionMaskLand) ? 0 : 255;
@ -54,95 +48,9 @@ m.TerrainAnalysis.prototype.init = function(sharedScript,rawState)
obstructionTiles[i] = 201; // navigable and walkable.
}
var square = [ [-1,-1], [-1,0], [-1, 1], [0,1], [1,1], [1,0], [1,-1], [0,-1], [0,0] ];
var xx = 0;
var yy = 0;
var value = 0;
var pos = [];
var x = 0;
var y = 0;
var radius = 0;
for (let ent of sharedScript._entities.values())
{
if (ent.hasClass("ForestPlant") === true)
{
pos = this.gamePosToMapPos(ent.position());
x = pos[0];
y = pos[1];
// unless it's impassable already, mark it as 40.
if (obstructionTiles[x + y*this.width] !== 0)
obstructionTiles[x + y*this.width] = 40;
for (let sq of square)
{
xx = sq[0];
yy = sq[1];
if (x+xx >= 0 && x+xx < this.width && y+yy >= 0 && y+yy < this.height) {
value = obstructionTiles[(x+xx) + (y+yy)*this.width];
if (value === 255)
obstructionTiles[(x+xx) + (y+yy)*this.width] = 41;
else if (value < 49 && value > 40)
obstructionTiles[(x+xx) + (y+yy)*this.width] = value + 1;
}
}
}
else if (ent.hasClass("Geology") === true)
{
radius = Math.floor(ent.obstructionRadius() / this.cellSize);
pos = this.gamePosToMapPos(ent.position());
x = pos[0];
y = pos[1];
// Unless it's impassable, mark as 30. This takes precedence over trees.
obstructionTiles[x + y*this.width] = obstructionTiles[x + y*this.width] === 0 ? 0 : 30;
for (var xx = -radius; xx <= radius;xx++)
for (var yy = -radius; yy <= radius;yy++)
if (x+xx >= 0 && x+xx < this.width && y+yy >= 0 && y+yy < this.height)
obstructionTiles[(x+xx) + (y+yy)*this.width] = obstructionTiles[(x+xx) + (y+yy)*this.width] === 0 ? 0 : 30;
}
}
// Okay now we have a pretty good knowledge of the map.
this.Map(rawState, "passability", obstructionTiles);
};
// Returns the (approximately) closest point which is passable by searching in a spiral pattern
m.TerrainAnalysis.prototype.findClosestPassablePoint = function(startPoint, onLand, limitDistance, quickscope)
{
var w = this.width;
var p = startPoint;
var direction = 1;
if (p[0] + w*p[1] < 0 || p[0] + w*p[1] >= this.length) {
return undefined;
}
// quickscope
if (this.map[p[0] + w*p[1]] === 255)
if (this.countConnected(p[0] + w*p[1], onLand) >= 2)
return p;
var count = 0;
// search in a spiral pattern. We require a value that is actually accessible in this case, ie 255, 201 or 41 if land, 200/201 if water.
for (var i = 1; i < w; i++)
{
for (var j = 0; j < 2; j++)
{
for (var k = 0; k < i; k++)
{
p[j] += direction;
// if the value is not markedly inaccessible
var index = p[0] + w*p[1];
if (this.map[index] !== 0 && this.map[index] !== 90 && this.map[index] !== 120 && this.map[index] !== 30 && this.map[index] !== 40)
if (quickscope || this.countConnected(index, onLand) >= 2)
return p;
if (limitDistance !== undefined && count > limitDistance)
return undefined;
count++;
}
}
direction *= -1;
}
return undefined;
};
// Returns an estimate of a tile accessibility. It checks neighboring cells over two levels.
// returns a count. It's not integer. About 2 should be fairly accessible already.
m.TerrainAnalysis.prototype.countConnected = function(startIndex, byLand)
@ -179,58 +87,6 @@ m.TerrainAnalysis.prototype.countConnected = function(startIndex, byLand)
return count;
};
// TODO: for now this resets to 255.
m.TerrainAnalysis.prototype.updateMapWithEvents = function(sharedAI)
{
var events = sharedAI.events["Destroy"];
var passabilityMap = sharedAI.passabilityMap;
// looking for creation or destruction of entities, and updates the map accordingly.
for (let e of events)
{
if (!e.entityObj)
continue;
let ent = e.entityObj;
if (ent.hasClass("Geology"))
{
let pos = this.gamePosToMapPos(ent.position());
let x = pos[0];
let y = pos[1];
// remove it. Don't really care about surrounding and possible overlappings.
let radius = Math.floor(ent.obstructionRadius() / this.cellSize);
for (let xx = -radius; xx <= radius;xx++)
for (let yy = -radius; yy <= radius;yy++)
{
if (x+xx >= 0 && x+xx < this.width && y+yy >= 0 && y+yy < this.height && this.map[(x+xx) + (y+yy)*this.width] === 30)
this.map[(x+xx) + (y+yy)*this.width] = 255;
}
}
else if (ent.hasClass("ForestPlant"))
{
let pos = this.gamePosToMapPos(ent.position());
let x = pos[0];
let y = pos[1];
let nbOfNeigh = 0;
for (let xx = -1; xx <= 1;xx++)
for (let yy = -1; yy <= 1;yy++)
{
if (xx == 0 && yy == 0)
continue;
if (this.map[(x+xx) + (y+yy)*this.width] === 40)
nbOfNeigh++;
else if (this.map[(x+xx) + (y+yy)*this.width] === 41)
this.map[(x+xx) + (y+yy)*this.width] = 255;
else if (this.map[(x+xx) + (y+yy)*this.width] > 41 && this.map[(x+xx) + (y+yy)*this.width] < 50)
this.map[(x+xx) + (y+yy)*this.width] = this.map[(x+xx) + (y+yy)*this.width] - 1;
}
if (nbOfNeigh > 0)
this.map[x + y*this.width] = this.map[x + y*this.width] = 40 + nbOfNeigh;
else
this.map[x + y*this.width] = this.map[x + y*this.width] = 255;
}
}
};
/*
* Accessibility inherits from TerrainAnalysis
*

View File

@ -86,7 +86,7 @@ m.AttackManager.prototype.checkEvents = function(gameState, events)
continue;
}
attack.targetPos = attack.target.position();
attack.resetPath(gameState);
attack.resetPath();
}
if (attack.targetPlayer && attack.targetPlayer === targetPlayer)
available += attack.unitCollection.length;

View File

@ -34,7 +34,7 @@ m.AttackPlan = function(gameState, Config, uniqueID, type, data)
}
// get a starting rallyPoint ... will be improved later
var rallyPoint = undefined;
var rallyPoint;
for (let base of gameState.ai.HQ.baseManagers)
{
if (!base.anchor || !base.anchor.position())
@ -166,13 +166,6 @@ m.AttackPlan = function(gameState, Config, uniqueID, type, data)
this.lastPosition = [0,0];
this.position = [0,0];
// get a good path to an estimated target.
this.pathFinder = new API3.aStarPath(gameState, false, false, this.targetPlayer);
//Engine.DumpImage("widthmap.png", this.pathFinder.widthMap, this.pathFinder.width,this.pathFinder.height,255);
this.pathWidth = 6; // prefer a path far from entities. This will avoid units getting stuck in trees and also results in less straight paths.
this.pathSampling = 2;
return true;
};
@ -410,16 +403,13 @@ m.AttackPlan.prototype.updatePreparation = function(gameState)
}
}
// reset the path so that we recompute it for this new target
this.resetPath(gameState);
this.resetPath();
}
// when we have a target, we path to it.
// I'd like a good high width sampling first.
// Thus I will not do everything at once.
// It will probably carry over a few turns but that's no issue.
if (this.path === undefined || this.path === "toBeContinued")
if (!this.path || this.path === "toBeContinued")
{
var ret = this.getPathToTarget(gameState);
let ret = this.getPathToTarget(gameState);
if (ret >= 0)
return ret;
}
@ -540,10 +530,10 @@ m.AttackPlan.prototype.trainMoreUnits = function(gameState)
// let's sort by training advancement, ie 'current size / target size'
// count the number of queued units too.
// substract priority.
for (var i = 0; i < this.buildOrder.length; ++i)
for (let i = 0; i < this.buildOrder.length; ++i)
{
var special = "Plan_" + this.name + "_" + this.buildOrder[i][4];
var aQueued = gameState.countOwnQueuedEntitiesWithMetadata("special", special);
let special = "Plan_" + this.name + "_" + this.buildOrder[i][4];
let aQueued = gameState.countOwnQueuedEntitiesWithMetadata("special", special);
aQueued += this.queue.countQueuedUnitsWithMetadata("special", special);
aQueued += this.queueChamp.countQueuedUnitsWithMetadata("special", special);
aQueued += this.queueSiege.countQueuedUnitsWithMetadata("special", special);
@ -563,13 +553,13 @@ m.AttackPlan.prototype.trainMoreUnits = function(gameState)
{
API3.warn("====================================");
API3.warn("======== build order for plan " + this.name);
for (var order of this.buildOrder)
for (let order of this.buildOrder)
{
var specialData = "Plan_"+this.name+"_"+order[4];
var inTraining = gameState.countOwnQueuedEntitiesWithMetadata("special", specialData);
var queue1 = this.queue.countQueuedUnitsWithMetadata("special", specialData);
var queue2 = this.queueChamp.countQueuedUnitsWithMetadata("special", specialData);
var queue3 = this.queueSiege.countQueuedUnitsWithMetadata("special", specialData);
let specialData = "Plan_"+this.name+"_"+order[4];
let inTraining = gameState.countOwnQueuedEntitiesWithMetadata("special", specialData);
let queue1 = this.queue.countQueuedUnitsWithMetadata("special", specialData);
let queue2 = this.queueChamp.countQueuedUnitsWithMetadata("special", specialData);
let queue3 = this.queueSiege.countQueuedUnitsWithMetadata("special", specialData);
API3.warn(" >>> " + order[4] + " done " + order[2].length + " training " + inTraining
+ " queue " + queue1 + " champ " + queue2 + " siege " + queue3 + " >> need " + order[3].targetSize);
}
@ -735,8 +725,8 @@ m.AttackPlan.prototype.assignUnits = function(gameState)
// Reassign one (at each turn) Cav unit to fasten raid preparation
m.AttackPlan.prototype.reassignCavUnit = function(gameState)
{
var found = undefined;
for (var ent of this.unitCollection.values())
var found;
for (let ent of this.unitCollection.values())
{
if (!ent.position() || ent.getMetadata(PlayerID, "transport") !== undefined)
continue;
@ -770,13 +760,13 @@ m.AttackPlan.prototype.getNearestTarget = function(gameState, position, sameLand
// picking the nearest target
var minDist = -1;
var target = undefined;
for (var ent of targets.values())
for (let ent of targets.values())
{
if (!ent.position())
continue;
if (sameLand && gameState.ai.accessibility.getAccessValue(ent.position()) != land)
continue;
var dist = API3.SquareVectorDistance(ent.position(), position);
let dist = API3.SquareVectorDistance(ent.position(), position);
if (dist < minDist || minDist == -1)
{
minDist = dist;
@ -877,9 +867,9 @@ m.AttackPlan.prototype.rushTargetFinder = function(gameState, playerEnemy)
m.AttackPlan.prototype.raidTargetFinder = function(gameState)
{
var targets = new API3.EntityCollection(gameState.sharedScript);
for (var targetId of gameState.ai.HQ.defenseManager.targetList)
for (let targetId of gameState.ai.HQ.defenseManager.targetList)
{
var target = gameState.getEntityById(targetId);
let target = gameState.getEntityById(targetId);
if (target && target.position())
targets.addEnt(target);
}
@ -888,74 +878,42 @@ m.AttackPlan.prototype.raidTargetFinder = function(gameState)
m.AttackPlan.prototype.getPathToTarget = function(gameState)
{
if (this.path === undefined)
this.path = this.pathFinder.getPath(this.rallyPoint, this.targetPos, this.pathSampling, this.pathWidth, 175);
else if (this.path === "toBeContinued")
this.path = this.pathFinder.continuePath();
if (this.path === undefined)
if (!this.path)
{
if (this.pathWidth == 6)
{
this.pathWidth = 2;
delete this.path;
}
else
{
delete this.pathFinder;
return 3; // no path.
}
Engine.ProfileStart("Compute path");
let startPos = { "x": this.rallyPoint[0], "y": this.rallyPoint[1] };
let endPos = { "x": this.targetPos[0], "y": this.targetPos[1] };
let path = Engine.ComputePath(startPos, endPos, gameState.getPassabilityClassMask("siege-large"));
this.path = [];
this.path.push(this.targetPos);
for (let p in path)
this.path.push([path[p].x, path[p].y])
this.path.push(this.rallyPoint);
this.path.reverse();
// Change the rally point to something useful (should avoid rams getting stuck in our territor)
this.setRallyPoint(gameState);
//API3.warn("new path computed " + uneval(this.path) + " and ralyPoint " + uneval(this.rallyPoint));
Engine.ProfileStop();
}
else if (this.path === "toBeContinued")
{
return 1; // carry on
}
else if (this.path[1] === true && this.pathWidth == 2)
{
// okay so we need a ship.
// Basically we'll add it as a new class to train compulsorily, and we'll recompute our path.
if (!gameState.ai.HQ.navalMap)
{
gameState.ai.HQ.navalMap = true;
return 0;
}
this.pathWidth = 3;
this.pathSampling = 3;
this.path = this.path[0].reverse();
delete this.pathFinder;
// Change the rally point to something useful (should avoid rams getting stuck in our territor)
this.setRallyPoint(gameState);
}
else if (this.path[1] === true && this.pathWidth == 6)
{
// retry with a smaller pathwidth:
this.pathWidth = 2;
delete this.path;
}
else
{
this.path = this.path[0].reverse();
delete this.pathFinder;
// Change the rally point to something useful (should avoid rams getting stuck in our territor)
this.setRallyPoint(gameState);
}
return -1; // ok
return -1;
};
m.AttackPlan.prototype.setRallyPoint = function(gameState)
{
for (var i = 0; i < this.path.length; ++i)
for (let i = 0; i < this.path.length; ++i)
{
// my pathfinder returns arrays in arrays in arrays.
var waypointPos = this.path[i][0];
if (gameState.ai.HQ.territoryMap.getOwner(waypointPos) !== PlayerID || this.path[i][1] === true)
let waypointPos = this.path[i];
if (gameState.ai.HQ.territoryMap.getOwner(waypointPos) !== PlayerID)
{
// Set rally point at the border of our territory
// or where we need to change transportation method.
if (i !== 0)
this.rallyPoint = this.path[i-1][0];
this.rallyPoint = this.path[i-1];
else
this.rallyPoint = this.path[0][0];
this.rallyPoint = this.path[0];
if (i >= 2)
this.path.splice(0, i-1);
@ -1019,20 +977,20 @@ m.AttackPlan.prototype.StartAttack = function(gameState)
var curPos = this.unitCollection.getCentrePosition();
for (var ent of this.unitCollection.values())
for (let ent of this.unitCollection.values())
ent.setMetadata(PlayerID, "subrole", "walking");
this.unitCollection.setStance("aggressive");
if (gameState.ai.accessibility.getAccessValue(this.targetPos) === gameState.ai.accessibility.getAccessValue(this.rallyPoint))
{
if (!this.path[0][0][0] || !this.path[0][0][1])
if (!this.path[0][0] || !this.path[0][1])
{
if (this.Config.debug > 1)
API3.warn("StartAttack: Problem with path " + uneval(this.path));
return false;
}
this.state = "walking";
this.unitCollection.move(this.path[0][0][0], this.path[0][0][1]);
this.unitCollection.move(this.path[0][0], this.path[0][1]);
}
else
{
@ -1042,7 +1000,7 @@ m.AttackPlan.prototype.StartAttack = function(gameState)
var endPos = this.targetPos;
// TODO require a global transport for the collection,
// and put back its state to "walking" when the transport is finished
for (var ent of this.unitCollection.values())
for (let ent of this.unitCollection.values())
gameState.ai.HQ.navalManager.requireTransport(gameState, ent, startIndex, endIndex, endPos);
}
}
@ -1073,7 +1031,7 @@ m.AttackPlan.prototype.update = function(gameState, events)
if (this.state === "transporting")
{
var done = true;
for (var ent of this.unitCollection.values())
for (let ent of this.unitCollection.values())
{
if (this.Config.debug > 1 && ent.getMetadata(PlayerID, "transport") !== undefined)
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [2,2,0]});
@ -1090,7 +1048,7 @@ m.AttackPlan.prototype.update = function(gameState, events)
else
{
// if we are attacked while waiting the rest of the army, retaliate
for (var evt of events["Attacked"])
for (let evt of events["Attacked"])
{
if (IDs.indexOf(evt.target) == -1)
continue;
@ -1098,7 +1056,7 @@ m.AttackPlan.prototype.update = function(gameState, events)
var ourUnit = gameState.getEntityById(evt.target);
if (!attacker || !ourUnit)
continue;
for (var ent of this.unitCollection.values())
for (let ent of this.unitCollection.values())
{
if (ent.getMetadata(PlayerID, "transport") !== undefined)
continue;
@ -1120,7 +1078,7 @@ m.AttackPlan.prototype.update = function(gameState, events)
// or if we reached the enemy base. Different plans may react differently.
var attackedNB = 0;
var attackedUnitNB = 0;
for (var evt of events["Attacked"])
for (let evt of events["Attacked"])
{
if (IDs.indexOf(evt.target) == -1)
continue;
@ -1139,7 +1097,7 @@ m.AttackPlan.prototype.update = function(gameState, events)
if (attackedUnitNB == 0)
{
var siegeNB = 0;
for (var ent of this.unitCollection.values())
for (let ent of this.unitCollection.values())
if (this.isSiegeUnit(gameState, ent))
siegeNB++;
if (siegeNB == 0)
@ -1158,7 +1116,7 @@ m.AttackPlan.prototype.update = function(gameState, events)
var farthest = 0;
var farthestEnt = -1;
this.unitCollection.filter(API3.Filters.byClass("Siege")).forEach (function (ent) {
var dist = API3.SquareVectorDistance(ent.position(), self.position);
let dist = API3.SquareVectorDistance(ent.position(), self.position);
if (dist < farthest)
return;
farthest = dist;
@ -1172,7 +1130,7 @@ m.AttackPlan.prototype.update = function(gameState, events)
if (this.lastPosition && API3.SquareVectorDistance(this.position, this.lastPosition) < 20 && this.path.length > 0)
{
if (!this.path[0][0][0] || !this.path[0][0][1])
if (!this.path[0][0] || !this.path[0][1])
API3.warn("Start: Problem with path " + uneval(this.path));
// We're stuck, presumably. Check if there are no walls just close to us. If so, we're arrived, and we're gonna tear down some serious stone.
var nexttoWalls = false;
@ -1195,8 +1153,8 @@ m.AttackPlan.prototype.update = function(gameState, events)
return 0;
}
else
//this.unitCollection.move(this.path[0][0][0], this.path[0][0][1]);
this.unitCollection.moveIndiv(this.path[0][0][0], this.path[0][0][1]);
//this.unitCollection.move(this.path[0][0], this.path[0][1]);
this.unitCollection.moveIndiv(this.path[0][0], this.path[0][1]);
}
}
@ -1209,11 +1167,11 @@ m.AttackPlan.prototype.update = function(gameState, events)
API3.warn("Attack Plan " + this.type + " " + this.name + " has arrived to destination.");
this.state = "arrived";
}
else if (this.path.length && API3.SquareVectorDistance(this.position, this.path[0][0]) < 1600)
else if (this.path.length && API3.SquareVectorDistance(this.position, this.path[0]) < 1600)
{
this.path.shift();
if (this.path.length)
this.unitCollection.move(this.path[0][0][0], this.path[0][0][1]);
this.unitCollection.move(this.path[0][0], this.path[0][1]);
else
{
if (this.Config.debug > 1)
@ -1719,15 +1677,9 @@ m.AttackPlan.prototype.removeUnit = function(ent, update)
};
// Reset the path so that it can be recomputed for a new target
m.AttackPlan.prototype.resetPath = function(gameState)
m.AttackPlan.prototype.resetPath = function()
{
this.path = undefined;
if (!this.pathFinder)
{
this.pathFinder = new API3.aStarPath(gameState, false, false, this.targetPlayer);
this.pathWidth = 6;
this.pathSampling = 2;
}
};
m.AttackPlan.prototype.checkEvents = function(gameState, events)
@ -1809,16 +1761,11 @@ m.AttackPlan.prototype.Serialize = function()
"position": this.position,
"targetPlayer": this.targetPlayer,
"target": ((this.target !== undefined) ? this.target.id() : undefined),
"targetPos": this.targetPos
"targetPos": this.targetPos,
"path": this.path
};
let path = {
"path": this.path,
"pathSampling": this.pathSampling,
"pathWidth": this.pathWidth
};
return { "properties": properties, "path": path };
return { "properties": properties};
};
m.AttackPlan.prototype.Deserialize = function(gameState, data)
@ -1829,11 +1776,6 @@ m.AttackPlan.prototype.Deserialize = function(gameState, data)
if (this.target)
this.target = gameState.getEntityById(this.target);
// if the path was not fully computed, we will recompute it as it is not serialized
if (data.path.path != "toBeContinued")
for (let key in data.path)
this[key] = data.path[key];
this.failed = undefined;
};