Petra: make better use of garrisoning when attacked

This was SVN commit r15364.
This commit is contained in:
mimo 2014-06-15 12:57:12 +00:00
parent 9a743d65fe
commit b17ffaeb7e
7 changed files with 255 additions and 96 deletions

View File

@ -4,7 +4,7 @@ var PETRA = function(m)
// this defines the medium difficulty // this defines the medium difficulty
m.Config = function() { m.Config = function() {
this.difficulty = 2; // 0 is sandbox, 1 is easy, 2 is medium, 3 is hard, 4 is very hard. this.difficulty = 2; // 0 is sandbox, 1 is easy, 2 is medium, 3 is hard, 4 is very hard.
this.debug = 0; this.debug = 1;
this.Military = { this.Military = {
"towerLapseTime" : 90, // Time to wait between building 2 towers "towerLapseTime" : 90, // Time to wait between building 2 towers
@ -69,7 +69,7 @@ m.Config = function() {
this.priorities = this.priorities =
{ {
"villager" : 30, // should be slightly lower than the citizen soldier one because otherwise they get all the food "villager" : 30, // should be slightly lower than the citizen soldier one to not get all the food
"citizenSoldier" : 60, "citizenSoldier" : 60,
"trader" : 50, "trader" : 50,
"ships" : 70, "ships" : 70,
@ -81,7 +81,8 @@ m.Config = function() {
"defenseBuilding" : 70, "defenseBuilding" : 70,
"civilCentre" : 950, "civilCentre" : 950,
"majorTech" : 700, "majorTech" : 700,
"minorTech" : 40 "minorTech" : 40,
"emergency" : 1000 // used only in emergency situations, should be the highest one
}; };
this.personality = this.personality =

View File

@ -316,11 +316,13 @@ m.DefenseManager.prototype.assignDefenders = function(gameState, events)
break; break;
} }
// If shortage of defenders: increase the priority of soldiers queues if (armiesNeeding.length === 0)
if (armiesNeeding.length !== 0) return;
gameState.ai.HQ.boostSoldiers(gameState); // If shortage of defenders, produce ranged infantry garrisoned in nearest civil centre
else var armiesPos = [];
gameState.ai.HQ.unboostSoldiers(gameState); for (var a = 0; a < armiesNeeding.length; ++a)
armiesPos.push(armiesNeeding[a]["army"].foePosition);
gameState.ai.HQ.trainEmergencyUnits(gameState, armiesPos);
}; };
// If our defense structures are attacked, garrison soldiers inside when possible // If our defense structures are attacked, garrison soldiers inside when possible

View File

@ -6,7 +6,7 @@ var PETRA = function(m)
* When a unit is ordered to garrison, it must be done through this.garrison() function so that * When a unit is ordered to garrison, it must be done through this.garrison() function so that
* an object in this.holders is created. This object contains an array with the entities * an object in this.holders is created. This object contains an array with the entities
* in the process of being garrisoned. To have all garrisoned units, we must add those in holder.garrisoned(). * in the process of being garrisoned. To have all garrisoned units, we must add those in holder.garrisoned().
* Futhermore garrison units have a metadata garrison describing its reason (protection, transport, ...) * Futhermore garrison units have a metadata garrisonType describing its reason (protection, transport, ...)
*/ */
m.GarrisonManager = function() m.GarrisonManager = function()
@ -27,7 +27,7 @@ m.GarrisonManager.prototype.update = function(gameState, queues)
for each (var entId in this.holders[id]) for each (var entId in this.holders[id])
{ {
var ent = gameState.getEntityById(entId); var ent = gameState.getEntityById(entId);
if (ent && ent.getMetadata(PlayerID, "garrison-holder") === id) if (ent && ent.getMetadata(PlayerID, "garrisonHolder") === id)
this.leaveGarrison(ent); this.leaveGarrison(ent);
} }
this.holders[id] = undefined; this.holders[id] = undefined;
@ -51,7 +51,8 @@ m.GarrisonManager.prototype.update = function(gameState, queues)
if (!holder.position()) // could happen with siege unit inside a ship if (!holder.position()) // could happen with siege unit inside a ship
continue; continue;
if (gameState.ai.playedTurn - holder.getMetadata(PlayerID, "lastUpdate") > 5) if (holder.getMetadata(PlayerID, "lastUpdate") === undefined
|| gameState.ai.playedTurn - holder.getMetadata(PlayerID, "lastUpdate") > 5)
{ {
if (holder.attackRange("Ranged")) if (holder.attackRange("Ranged"))
var range = holder.attackRange("Ranged").max; var range = holder.attackRange("Ranged").max;
@ -68,7 +69,7 @@ m.GarrisonManager.prototype.update = function(gameState, queues)
var healer = holder.buffHeal(); var healer = holder.buffHeal();
for each (var entId in holder._entity.garrisoned) for (var entId of holder._entity.garrisoned)
{ {
var ent = gameState.getEntityById(entId); var ent = gameState.getEntityById(entId);
if (!this.keepGarrisoned(ent, holder, enemiesAround)) if (!this.keepGarrisoned(ent, holder, enemiesAround))
@ -79,7 +80,7 @@ m.GarrisonManager.prototype.update = function(gameState, queues)
var ent = gameState.getEntityById(list[j]); var ent = gameState.getEntityById(list[j]);
if (this.keepGarrisoned(ent, holder, enemiesAround)) if (this.keepGarrisoned(ent, holder, enemiesAround))
continue; continue;
if (ent.getMetadata(PlayerID, "garrison-holder") === id) if (ent.getMetadata(PlayerID, "garrisonHolder") === id)
this.leaveGarrison(ent); this.leaveGarrison(ent);
list.splice(j--, 1); list.splice(j--, 1);
} }
@ -126,8 +127,8 @@ m.GarrisonManager.prototype.garrison = function(gameState, ent, holder, type)
else else
ent.setMetadata(PlayerID, "plan", -3); ent.setMetadata(PlayerID, "plan", -3);
ent.setMetadata(PlayerID, "subrole", "garrisoning"); ent.setMetadata(PlayerID, "subrole", "garrisoning");
ent.setMetadata(PlayerID, "garrison-holder", holder.id()); ent.setMetadata(PlayerID, "garrisonHolder", holder.id());
ent.setMetadata(PlayerID, "garrison-type", type); ent.setMetadata(PlayerID, "garrisonType", type);
ent.garrison(holder); ent.garrison(holder);
}; };
@ -140,12 +141,12 @@ m.GarrisonManager.prototype.leaveGarrison = function(ent)
ent.setMetadata(PlayerID, "plan", -1); ent.setMetadata(PlayerID, "plan", -1);
else else
ent.setMetadata(PlayerID, "plan", undefined); ent.setMetadata(PlayerID, "plan", undefined);
ent.setMetadata(PlayerID, "garrison-holder", undefined); ent.setMetadata(PlayerID, "garrisonHolder", undefined);
}; };
m.GarrisonManager.prototype.keepGarrisoned = function(ent, holder, enemiesAround) m.GarrisonManager.prototype.keepGarrisoned = function(ent, holder, enemiesAround)
{ {
switch (ent.getMetadata(PlayerID, "garrison-type")) switch (ent.getMetadata(PlayerID, "garrisonType"))
{ {
case 'force': // force the ungarrisoning case 'force': // force the ungarrisoning
return false; return false;
@ -161,7 +162,7 @@ m.GarrisonManager.prototype.keepGarrisoned = function(ent, holder, enemiesAround
default: default:
if (ent.getMetadata(PlayerID, "onBoard") === "onBoard") // transport is not (yet ?) managed by garrisonManager if (ent.getMetadata(PlayerID, "onBoard") === "onBoard") // transport is not (yet ?) managed by garrisonManager
return true; return true;
warn("unknown type in garrisonManager " + ent.getMetadata(PlayerID, "garrison-type")); warn("unknown type in garrisonManager " + ent.getMetadata(PlayerID, "garrisonType"));
return true; return true;
} }
}; };

View File

@ -47,8 +47,6 @@ m.HQ = function(Config)
this.navalManager = new m.NavalManager(this.Config); this.navalManager = new m.NavalManager(this.Config);
this.researchManager = new m.ResearchManager(this.Config); this.researchManager = new m.ResearchManager(this.Config);
this.garrisonManager = new m.GarrisonManager(); this.garrisonManager = new m.GarrisonManager();
this.boostedSoldiers = undefined;
}; };
// More initialisation for stuff that needs the gameState // More initialisation for stuff that needs the gameState
@ -263,69 +261,106 @@ m.HQ.prototype.getSeaIndex = function (gameState, index1, index2)
m.HQ.prototype.checkEvents = function (gameState, events, queues) m.HQ.prototype.checkEvents = function (gameState, events, queues)
{ {
// TODO: probably check stuffs like a base destruction.
var CreateEvents = events["Create"]; var CreateEvents = events["Create"];
var ConstructionEvents = events["ConstructionFinished"]; for (var evt of CreateEvents)
for (var i in CreateEvents)
{ {
var evt = CreateEvents[i];
// Let's check if we have a building set to create a new base. // Let's check if we have a building set to create a new base.
if (evt && evt.entity) var ent = gameState.getEntityById(evt.entity);
if (!ent || !ent.isOwn(PlayerID))
continue;
if (ent.getMetadata(PlayerID, "base") === -1)
{ {
var ent = gameState.getEntityById(evt.entity); // Okay so let's try to create a new base around this.
var bID = m.playerGlobals[PlayerID].uniqueIDBases;
this.baseManagers[bID] = new m.BaseManager(this.Config);
this.baseManagers[bID].init(gameState, true);
this.baseManagers[bID].setAnchor(gameState, ent);
if (ent === undefined) // Let's get a few units out there to build this.
continue; // happens when this message is right before a "Destroy" one for the same entity. var builders = this.bulkPickWorkers(gameState, bID, 10);
if (builders !== false)
if (ent.isOwn(PlayerID) && ent.getMetadata(PlayerID, "base") === -1)
{ {
// Okay so let's try to create a new base around this. builders.forEach(function (worker) {
var bID = m.playerGlobals[PlayerID].uniqueIDBases; worker.setMetadata(PlayerID, "base", bID);
this.baseManagers[bID] = new m.BaseManager(this.Config); worker.setMetadata(PlayerID, "subrole", "builder");
this.baseManagers[bID].init(gameState, true); worker.setMetadata(PlayerID, "target-foundation", ent.id());
this.baseManagers[bID].setAnchor(gameState, ent); });
// Let's get a few units out there to build this.
var builders = this.bulkPickWorkers(gameState, bID, 10);
if (builders !== false)
{
builders.forEach(function (worker) {
worker.setMetadata(PlayerID, "base", bID);
worker.setMetadata(PlayerID, "subrole", "builder");
worker.setMetadata(PlayerID, "target-foundation", ent.id());
});
}
} }
} }
} }
for (var i in ConstructionEvents)
var ConstructionEvents = events["ConstructionFinished"];
for (var evt of ConstructionEvents)
{ {
var evt = ConstructionEvents[i];
// Let's check if we have a building set to create a new base. // Let's check if we have a building set to create a new base.
// TODO: move to the base manager. // TODO: move to the base manager.
if (evt.newentity) if (evt.newentity)
{ {
var ent = gameState.getEntityById(evt.newentity); var ent = gameState.getEntityById(evt.newentity);
if (!ent || !ent.isOwn(PlayerID))
continue;
if (ent === undefined) if (ent.getMetadata(PlayerID, "baseAnchor") == true)
continue; // happens when this message is right before a "Destroy" one for the same entity.
if (ent.isOwn(PlayerID))
{ {
if (ent.getMetadata(PlayerID, "baseAnchor") == true) var base = ent.getMetadata(PlayerID, "base");
{ if (this.baseManagers[base].constructing)
var base = ent.getMetadata(PlayerID, "base"); this.baseManagers[base].constructing = false;
if (this.baseManagers[base].constructing) this.baseManagers[base].anchor = ent;
this.baseManagers[base].constructing = false; this.baseManagers[base].buildings.updateEnt(ent);
this.baseManagers[base].anchor = ent; this.updateTerritories(gameState);
this.baseManagers[base].buildings.updateEnt(ent); // let us hope this new base will fix our resource shortage
this.updateTerritories(gameState); // TODO check it really does so
// let us hope this new base will fix our resource shortage this.saveResources = undefined;
// TODO check it really does so }
this.saveResources = undefined; else if (ent.hasTerritoryInfluence())
} this.updateTerritories(gameState);
else if (ent.hasTerritoryInfluence()) }
this.updateTerritories(gameState); }
// deal with the different rally points of training units: the rally point is set when the training starts
// for the time being, only autogarrison is used
var TrainingEvents = events["TrainingStarted"];
for (var evt of TrainingEvents)
{
var ent = gameState.getEntityById(evt.entity);
if (!ent || !ent.isOwn(PlayerID))
continue;
if (!ent._entity.trainingQueue || !ent._entity.trainingQueue.length)
continue;
var metadata = ent._entity.trainingQueue[0].metadata;
if (metadata.garrisonType)
{
ent.setRallyPoint(ent, "garrison"); // trained units will autogarrison
if (!this.garrisonManager.holders[evt.entity])
this.garrisonManager.holders[evt.entity] = [];
}
else
ent.unsetRallyPoint();
}
var TrainingEvents = events["TrainingFinished"];
for (var evt of TrainingEvents)
{
for (var entId of evt.entities)
{
var ent = gameState.getEntityById(entId);
if (!ent || !ent.isOwn(PlayerID))
continue;
if (!ent.position())
{
// we are autogarrisoned, check that the holder is registered in the garrisonManager
var holderId = ent.unitAIOrderData()[0]["target"];
if (!this.garrisonManager.holders[holderId])
this.garrisonManager.holders[holderId] = [];
}
else if (ent.getMetadata(PlayerID, "garrisonType"))
{
// we were supposed to be autogarrisoned, but this has failed (may-be full)
ent.getMetadata(PlayerID, "garrisonType", undefined);
} }
} }
} }
@ -354,7 +389,6 @@ m.HQ.prototype.OnCityPhase = function(gameState)
// TODO: also there are several things that could be greatly improved here. // TODO: also there are several things that could be greatly improved here.
m.HQ.prototype.trainMoreWorkers = function(gameState, queues) m.HQ.prototype.trainMoreWorkers = function(gameState, queues)
{ {
// Get some data.
// Count the workers in the world and in progress // Count the workers in the world and in progress
var numFemales = gameState.countEntitiesAndQueuedByType(gameState.applyCiv("units/{civ}_support_female_citizen"), true); var numFemales = gameState.countEntitiesAndQueuedByType(gameState.applyCiv("units/{civ}_support_female_citizen"), true);
@ -377,15 +411,11 @@ m.HQ.prototype.trainMoreWorkers = function(gameState, queues)
var numQueued = numQueuedS + numQueuedF; var numQueued = numQueuedS + numQueuedF;
var numTotal = numWorkers + numQueued; var numTotal = numWorkers + numQueued;
// If we have too few, train more if (this.saveResources && numTotal > this.Config.Economy.popForTown + 10)
if (!this.boostedSoldiers) return;
{ if (numTotal > this.targetNumWorkers || (numTotal >= this.Config.Economy.popForTown
if (this.saveResources && numTotal > this.Config.Economy.popForTown + 10) && gameState.currentPhase() === 1 && !gameState.isResearching(gameState.townPhase())))
return; return;
if (numTotal > this.targetNumWorkers || (numTotal >= this.Config.Economy.popForTown
&& gameState.currentPhase() === 1 && !gameState.isResearching(gameState.townPhase())))
return;
}
if (numQueued > 50 || (numQueuedF > 20 && numQueuedS > 20) || numInTraining > 15) if (numQueued > 50 || (numQueuedF > 20 && numQueuedS > 20) || numInTraining > 15)
return; return;
@ -1365,24 +1395,116 @@ m.HQ.prototype.findBestBaseForMilitary = function(gameState)
return bestBase; return bestBase;
}; };
m.HQ.prototype.boostSoldiers = function(gameState) /**
* train with highest priority ranged infantry in the nearest civil centre from a given set of positions
* and garrison them there for defense
*/
m.HQ.prototype.trainEmergencyUnits = function(gameState, positions)
{ {
if (this.boostedSoldiers) var available = gameState.ai.queueManager.getAvailableResources(gameState, false);
return; var total = gameState.ai.queueManager.getAvailableResources(gameState, true);
this.boostedSoldiers = true; var distcut = 20000;
gameState.ai.queueManager.changePriority("citizenSoldier", 5*this.Config.priorities.citizenSoldier);
// Reset accounts from all other queues
for (var p in gameState.ai.queueManager.queues)
if (p != "citizenSoldier")
gameState.ai.queueManager.accounts[p].reset();
};
m.HQ.prototype.unboostSoldiers = function(gameState) var nearestAnchor = undefined;
{ var templateAnchor = undefined;
if (!this.boostedSoldiers) var distmin = undefined;
for (var pos of positions)
{
var access = gameState.ai.accessibility.getAccessValue(pos);
// check nearest base anchor
for each (var base in gameState.ai.HQ.baseManagers)
{
if (!base.anchor || !base.anchor.position())
continue;
if (base.anchor.getMetadata(PlayerID, "access") !== access)
continue;
var queue = base.anchor._entity.trainingQueue
if (queue)
{
var time = 0;
for (var item of queue)
if (item.progress > 0 || (item.metadata && item.metadata.trainer))
time += item.timeRemaining;
if (time/1000 > 5)
continue;
}
var trainable = base.anchor.trainableEntities();
var templateFound = undefined;
for (var i in trainable)
{
var template = gameState.getTemplate(trainable[i]);
if (!template.hasClass("Infantry") || !template.hasClass("Ranged")
|| !template.hasClass("CitizenSoldier"))
continue;
if (!total.canAfford(new API3.Resources(template.cost())))
continue;
templateFound = [trainable[i], template];
break;
}
if (!templateFound)
continue;
var dist = API3.SquareVectorDistance(base.anchor.position(), pos);
if (nearestAnchor && dist > distmin)
continue;
distmin = dist;
nearestAnchor = base.anchor;
templateAnchor = templateFound;
}
}
if (!nearestAnchor || distmin > distcut)
return; return;
gameState.ai.queueManager.changePriority("citizenSoldier", this.Config.priorities.citizenSoldier);
this.boostedSoldiers = undefined; var autogarrison = true;
var numGarrisoned = this.garrisonManager.numberOfGarrisonedUnits(nearestAnchor);
if (nearestAnchor._entity.trainingQueue)
{
for (var item of nearestAnchor._entity.trainingQueue)
{
if (item.metadata && item.metadata["garrisonType"])
numGarrisoned += item.count;
else if (!item.progress || !item.metadata || !item.metadata.trainer)
nearestAnchor.stopProduction(item.id);
}
}
if (numGarrisoned >= nearestAnchor.garrisonMax())
{
// No more room to garrison ... favor a melee unit
autogarrison = false;
var trainables = nearestAnchor.trainableEntities();
for (var trainable of trainables)
{
var template = gameState.getTemplate(trainable);
if (!template.hasClass("Infantry") || !template.hasClass("Melee")
|| !template.hasClass("CitizenSoldier"))
continue;
if (!total.canAfford(new API3.Resources(template.cost())))
continue;
templateFound = [trainable, template];
break;
}
}
// Check first if we can afford it without touching other the accounts
// and if not, take some of ther accounted resources
// TODO substract only what is needed instead of reset
// TODO sort the queues
var cost = new API3.Resources(templateFound[1].cost());
if (!available.canAfford(cost))
{
for (var p in gameState.ai.queueManager.queues)
{
// TODO substract only what is needed instead of reseting
// and do a better sorting of queues
available.add(gameState.ai.queueManager.accounts[p]);
gameState.ai.queueManager.accounts[p].reset();
if (available.canAfford(cost))
break;
}
}
gameState.ai.queueManager.accounts["emergency"].add(cost);
var metadata = { "role": "worker", "base": nearestAnchor.getMetadata(PlayerID, "base"), "trainer": nearestAnchor.id() };
if (autogarrison)
metadata.garrisonType = "protection";
gameState.ai.queues.emergency.addItem(new m.TrainingPlan(gameState, templateFound[0], metadata, 1, 1));
}; };
m.HQ.prototype.canBuild = function(gameState, structure) m.HQ.prototype.canBuild = function(gameState, structure)

View File

@ -165,7 +165,7 @@ m.createFrontierMap = function(gameState, borderMap)
continue; continue;
var ix = j%width; var ix = j%width;
var iz = Math.floor(j/width); var iz = Math.floor(j/width);
for each (var a in around) for (var a of around)
{ {
var jx = ix + Math.round(insideSmall*a[0]); var jx = ix + Math.round(insideSmall*a[0]);
if (jx < 0 || jx >= width) if (jx < 0 || jx >= width)

View File

@ -260,7 +260,7 @@ m.QueueManager.prototype.update = function(gameState)
if (ress === "population") if (ress === "population")
continue; continue;
if (availableRes[ress] > 1) if (availableRes[ress] > 0)
{ {
var totalPriority = 0; var totalPriority = 0;
var tempPrio = {}; var tempPrio = {};
@ -295,13 +295,33 @@ m.QueueManager.prototype.update = function(gameState)
} }
// Now we allow resources to the accounts. We can at most allow "TempPriority/totalpriority*available" // Now we allow resources to the accounts. We can at most allow "TempPriority/totalpriority*available"
// But we'll sometimes allow less if that would overflow. // But we'll sometimes allow less if that would overflow.
var available = availableRes[ress];
var missing = false;
for (var j in tempPrio) for (var j in tempPrio)
{ {
// we'll add at much what can be allowed to this queue. // we'll add at much what can be allowed to this queue.
var toAdd = Math.floor(availableRes[ress] * tempPrio[j]/totalPriority); var toAdd = Math.floor(availableRes[ress] * tempPrio[j]/totalPriority);
var maxAdd = Math.min(maxNeed[j], toAdd); if (toAdd >= maxNeed[j])
this.accounts[j][ress] += maxAdd; toAdd = maxNeed[j];
else
missing = true;
this.accounts[j][ress] += toAdd;
maxNeed[j] -= toAdd;
available -= toAdd;
} }
if (missing && available > 0) // distribute the rest (due to floor) in any queue
{
for (var j in tempPrio)
{
var toAdd = Math.min(maxNeed[j], available);
this.accounts[j][ress] += toAdd;
available -= toAdd;
if (available <= 0)
break;
}
}
if (available < 0)
warn("Petra: problem with remaining " + ress + " in queueManager " + available);
} }
else else
{ {

View File

@ -37,6 +37,19 @@ m.TrainingPlan.prototype.canStart = function(gameState)
m.TrainingPlan.prototype.start = function(gameState) m.TrainingPlan.prototype.start = function(gameState)
{ {
if (this.metadata && this.metadata.trainer)
{
var metadata = {};
for (var key in this.metadata)
if (key !== "trainer")
metadata[key] = this.metadata[key];
var trainer = gameState.getEntityById(this.metadata.trainer);
if (trainer)
trainer.train(this.type, this.number, metadata);
this.onStart(gameState);
return;
}
if (this.metadata && this.metadata.sea) if (this.metadata && this.metadata.sea)
var trainers = gameState.findTrainers(this.type).filter(API3.Filters.byMetadata(PlayerID, "sea", this.metadata.sea)).toEntityArray(); var trainers = gameState.findTrainers(this.type).filter(API3.Filters.byMetadata(PlayerID, "sea", this.metadata.sea)).toEntityArray();
else if (this.metadata && this.metadata.base) else if (this.metadata && this.metadata.base)