From b17ffaeb7e1c7cef1d0bca37e48687d61ef05ba9 Mon Sep 17 00:00:00 2001 From: mimo Date: Sun, 15 Jun 2014 12:57:12 +0000 Subject: [PATCH] Petra: make better use of garrisoning when attacked This was SVN commit r15364. --- .../mods/public/simulation/ai/petra/config.js | 7 +- .../simulation/ai/petra/defenseManager.js | 12 +- .../simulation/ai/petra/garrisonManager.js | 21 +- .../simulation/ai/petra/headquarters.js | 270 +++++++++++++----- .../public/simulation/ai/petra/map-module.js | 2 +- .../simulation/ai/petra/queueManager.js | 26 +- .../simulation/ai/petra/queueplan-training.js | 13 + 7 files changed, 255 insertions(+), 96 deletions(-) diff --git a/binaries/data/mods/public/simulation/ai/petra/config.js b/binaries/data/mods/public/simulation/ai/petra/config.js index 76f5fbbc50..6c54b0f538 100644 --- a/binaries/data/mods/public/simulation/ai/petra/config.js +++ b/binaries/data/mods/public/simulation/ai/petra/config.js @@ -4,7 +4,7 @@ var PETRA = function(m) // this defines the medium difficulty m.Config = function() { 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 = { "towerLapseTime" : 90, // Time to wait between building 2 towers @@ -69,7 +69,7 @@ m.Config = function() { 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, "trader" : 50, "ships" : 70, @@ -81,7 +81,8 @@ m.Config = function() { "defenseBuilding" : 70, "civilCentre" : 950, "majorTech" : 700, - "minorTech" : 40 + "minorTech" : 40, + "emergency" : 1000 // used only in emergency situations, should be the highest one }; this.personality = diff --git a/binaries/data/mods/public/simulation/ai/petra/defenseManager.js b/binaries/data/mods/public/simulation/ai/petra/defenseManager.js index 9451b249ae..928f0b28df 100644 --- a/binaries/data/mods/public/simulation/ai/petra/defenseManager.js +++ b/binaries/data/mods/public/simulation/ai/petra/defenseManager.js @@ -316,11 +316,13 @@ m.DefenseManager.prototype.assignDefenders = function(gameState, events) break; } - // If shortage of defenders: increase the priority of soldiers queues - if (armiesNeeding.length !== 0) - gameState.ai.HQ.boostSoldiers(gameState); - else - gameState.ai.HQ.unboostSoldiers(gameState); + if (armiesNeeding.length === 0) + return; + // If shortage of defenders, produce ranged infantry garrisoned in nearest civil centre + var armiesPos = []; + 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 diff --git a/binaries/data/mods/public/simulation/ai/petra/garrisonManager.js b/binaries/data/mods/public/simulation/ai/petra/garrisonManager.js index b5232b6c53..ef2e56eff9 100644 --- a/binaries/data/mods/public/simulation/ai/petra/garrisonManager.js +++ b/binaries/data/mods/public/simulation/ai/petra/garrisonManager.js @@ -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 * 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(). - * 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() @@ -27,7 +27,7 @@ m.GarrisonManager.prototype.update = function(gameState, queues) for each (var entId in this.holders[id]) { var ent = gameState.getEntityById(entId); - if (ent && ent.getMetadata(PlayerID, "garrison-holder") === id) + if (ent && ent.getMetadata(PlayerID, "garrisonHolder") === id) this.leaveGarrison(ent); } 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 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")) var range = holder.attackRange("Ranged").max; @@ -68,7 +69,7 @@ m.GarrisonManager.prototype.update = function(gameState, queues) var healer = holder.buffHeal(); - for each (var entId in holder._entity.garrisoned) + for (var entId of holder._entity.garrisoned) { var ent = gameState.getEntityById(entId); if (!this.keepGarrisoned(ent, holder, enemiesAround)) @@ -79,7 +80,7 @@ m.GarrisonManager.prototype.update = function(gameState, queues) var ent = gameState.getEntityById(list[j]); if (this.keepGarrisoned(ent, holder, enemiesAround)) continue; - if (ent.getMetadata(PlayerID, "garrison-holder") === id) + if (ent.getMetadata(PlayerID, "garrisonHolder") === id) this.leaveGarrison(ent); list.splice(j--, 1); } @@ -126,8 +127,8 @@ m.GarrisonManager.prototype.garrison = function(gameState, ent, holder, type) else ent.setMetadata(PlayerID, "plan", -3); ent.setMetadata(PlayerID, "subrole", "garrisoning"); - ent.setMetadata(PlayerID, "garrison-holder", holder.id()); - ent.setMetadata(PlayerID, "garrison-type", type); + ent.setMetadata(PlayerID, "garrisonHolder", holder.id()); + ent.setMetadata(PlayerID, "garrisonType", type); ent.garrison(holder); }; @@ -140,12 +141,12 @@ m.GarrisonManager.prototype.leaveGarrison = function(ent) ent.setMetadata(PlayerID, "plan", -1); else ent.setMetadata(PlayerID, "plan", undefined); - ent.setMetadata(PlayerID, "garrison-holder", undefined); + ent.setMetadata(PlayerID, "garrisonHolder", undefined); }; m.GarrisonManager.prototype.keepGarrisoned = function(ent, holder, enemiesAround) { - switch (ent.getMetadata(PlayerID, "garrison-type")) + switch (ent.getMetadata(PlayerID, "garrisonType")) { case 'force': // force the ungarrisoning return false; @@ -161,7 +162,7 @@ m.GarrisonManager.prototype.keepGarrisoned = function(ent, holder, enemiesAround default: if (ent.getMetadata(PlayerID, "onBoard") === "onBoard") // transport is not (yet ?) managed by garrisonManager return true; - warn("unknown type in garrisonManager " + ent.getMetadata(PlayerID, "garrison-type")); + warn("unknown type in garrisonManager " + ent.getMetadata(PlayerID, "garrisonType")); return true; } }; diff --git a/binaries/data/mods/public/simulation/ai/petra/headquarters.js b/binaries/data/mods/public/simulation/ai/petra/headquarters.js index bf28542782..3cc11c1252 100644 --- a/binaries/data/mods/public/simulation/ai/petra/headquarters.js +++ b/binaries/data/mods/public/simulation/ai/petra/headquarters.js @@ -47,8 +47,6 @@ m.HQ = function(Config) this.navalManager = new m.NavalManager(this.Config); this.researchManager = new m.ResearchManager(this.Config); this.garrisonManager = new m.GarrisonManager(); - - this.boostedSoldiers = undefined; }; // 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) { - // TODO: probably check stuffs like a base destruction. var CreateEvents = events["Create"]; - var ConstructionEvents = events["ConstructionFinished"]; - for (var i in CreateEvents) + for (var evt of CreateEvents) { - var evt = CreateEvents[i]; // 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); - - if (ent === undefined) - continue; // happens when this message is right before a "Destroy" one for the same entity. - - if (ent.isOwn(PlayerID) && ent.getMetadata(PlayerID, "base") === -1) + // 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); + + // Let's get a few units out there to build this. + var builders = this.bulkPickWorkers(gameState, bID, 10); + if (builders !== false) { - // 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); - - // 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()); - }); - } + 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. // TODO: move to the base manager. if (evt.newentity) { var ent = gameState.getEntityById(evt.newentity); + if (!ent || !ent.isOwn(PlayerID)) + continue; - if (ent === undefined) - 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) { - if (ent.getMetadata(PlayerID, "baseAnchor") == true) - { - var base = ent.getMetadata(PlayerID, "base"); - if (this.baseManagers[base].constructing) - this.baseManagers[base].constructing = false; - this.baseManagers[base].anchor = ent; - this.baseManagers[base].buildings.updateEnt(ent); - this.updateTerritories(gameState); - // let us hope this new base will fix our resource shortage - // TODO check it really does so - this.saveResources = undefined; - } - else if (ent.hasTerritoryInfluence()) - this.updateTerritories(gameState); + var base = ent.getMetadata(PlayerID, "base"); + if (this.baseManagers[base].constructing) + this.baseManagers[base].constructing = false; + this.baseManagers[base].anchor = ent; + this.baseManagers[base].buildings.updateEnt(ent); + this.updateTerritories(gameState); + // let us hope this new base will fix our resource shortage + // TODO check it really does so + this.saveResources = undefined; + } + 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. m.HQ.prototype.trainMoreWorkers = function(gameState, queues) { - // Get some data. // Count the workers in the world and in progress 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 numTotal = numWorkers + numQueued; - // If we have too few, train more - if (!this.boostedSoldiers) - { - if (this.saveResources && numTotal > this.Config.Economy.popForTown + 10) - return; - if (numTotal > this.targetNumWorkers || (numTotal >= this.Config.Economy.popForTown - && gameState.currentPhase() === 1 && !gameState.isResearching(gameState.townPhase()))) - return; - } + if (this.saveResources && numTotal > this.Config.Economy.popForTown + 10) + 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) return; @@ -1365,24 +1395,116 @@ m.HQ.prototype.findBestBaseForMilitary = function(gameState) 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) - return; - this.boostedSoldiers = true; - 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(); -}; + var available = gameState.ai.queueManager.getAvailableResources(gameState, false); + var total = gameState.ai.queueManager.getAvailableResources(gameState, true); + var distcut = 20000; -m.HQ.prototype.unboostSoldiers = function(gameState) -{ - if (!this.boostedSoldiers) + var nearestAnchor = undefined; + var templateAnchor = undefined; + 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; - 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) diff --git a/binaries/data/mods/public/simulation/ai/petra/map-module.js b/binaries/data/mods/public/simulation/ai/petra/map-module.js index 2fa852cb59..8018b17cca 100644 --- a/binaries/data/mods/public/simulation/ai/petra/map-module.js +++ b/binaries/data/mods/public/simulation/ai/petra/map-module.js @@ -165,7 +165,7 @@ m.createFrontierMap = function(gameState, borderMap) continue; var ix = 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]); if (jx < 0 || jx >= width) diff --git a/binaries/data/mods/public/simulation/ai/petra/queueManager.js b/binaries/data/mods/public/simulation/ai/petra/queueManager.js index 372c22842b..41f83a4b7b 100644 --- a/binaries/data/mods/public/simulation/ai/petra/queueManager.js +++ b/binaries/data/mods/public/simulation/ai/petra/queueManager.js @@ -260,7 +260,7 @@ m.QueueManager.prototype.update = function(gameState) if (ress === "population") continue; - if (availableRes[ress] > 1) + if (availableRes[ress] > 0) { var totalPriority = 0; 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" // But we'll sometimes allow less if that would overflow. + var available = availableRes[ress]; + var missing = false; for (var j in tempPrio) { // we'll add at much what can be allowed to this queue. var toAdd = Math.floor(availableRes[ress] * tempPrio[j]/totalPriority); - var maxAdd = Math.min(maxNeed[j], toAdd); - this.accounts[j][ress] += maxAdd; + if (toAdd >= maxNeed[j]) + 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 { diff --git a/binaries/data/mods/public/simulation/ai/petra/queueplan-training.js b/binaries/data/mods/public/simulation/ai/petra/queueplan-training.js index 1d9a11109a..d1b1e205a6 100644 --- a/binaries/data/mods/public/simulation/ai/petra/queueplan-training.js +++ b/binaries/data/mods/public/simulation/ai/petra/queueplan-training.js @@ -37,6 +37,19 @@ m.TrainingPlan.prototype.canStart = 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) var trainers = gameState.findTrainers(this.type).filter(API3.Filters.byMetadata(PlayerID, "sea", this.metadata.sea)).toEntityArray(); else if (this.metadata && this.metadata.base)