diff --git a/binaries/data/mods/public/simulation/ai/petra/_petrabot.js b/binaries/data/mods/public/simulation/ai/petra/_petrabot.js index 290ca20f88..ffd53af737 100644 --- a/binaries/data/mods/public/simulation/ai/petra/_petrabot.js +++ b/binaries/data/mods/public/simulation/ai/petra/_petrabot.js @@ -61,14 +61,15 @@ m.PetraBot.prototype.CustomInit = function(gameState, sharedScript) this.queues = this.queueManager.queues; this.HQ = new m.HQ(this.Config); - this.HQ.init(gameState, this.queues, true); + this.HQ.init(gameState, this.queues); this.HQ.Deserialize(gameState, this.data.HQ); this.uniqueIDs = this.data.uniqueIDs; this.isDeserialized = false; this.data = undefined; - this.HQ.start(gameState, true); + // initialisation needed after the completion of the deserialization + this.HQ.postinit(gameState); } else { @@ -85,7 +86,8 @@ m.PetraBot.prototype.CustomInit = function(gameState, sharedScript) this.HQ.init(gameState, this.queues); - this.HQ.start(gameState); + // Analyze our starting position and set a strategy + this.HQ.gameAnalysis(gameState); } }; diff --git a/binaries/data/mods/public/simulation/ai/petra/attackPlan.js b/binaries/data/mods/public/simulation/ai/petra/attackPlan.js index b5626505fc..67778357eb 100644 --- a/binaries/data/mods/public/simulation/ai/petra/attackPlan.js +++ b/binaries/data/mods/public/simulation/ai/petra/attackPlan.js @@ -526,12 +526,13 @@ m.AttackPlan.prototype.updatePreparation = function(gameState, events) var rallyPoint = this.rallyPoint; var rallyIndex = gameState.ai.accessibility.getAccessValue(rallyPoint); - this.unitCollection.forEach(function (entity) { + for (var entity of this.unitCollection.values()) + { // For the time being, if occupied in a transport, remove the unit from this plan TODO improve that if (entity.getMetadata(PlayerID, "transport") !== undefined || entity.getMetadata(PlayerID, "transporter") !== undefined) { entity.setMetadata(PlayerID, "plan", -1); - return; + continue; } entity.setMetadata(PlayerID, "role", "attack"); entity.setMetadata(PlayerID, "subrole", "completing"); @@ -543,7 +544,7 @@ m.AttackPlan.prototype.updatePreparation = function(gameState, events) entity.moveToRange(rallyPoint[0], rallyPoint[1], 0, 15, queued); else gameState.ai.HQ.navalManager.requireTransport(gameState, entity, index, rallyIndex, rallyPoint); - }); + } // reset all queued units var plan = this.name; @@ -685,19 +686,20 @@ m.AttackPlan.prototype.assignUnits = function(gameState) } var noRole = gameState.getOwnEntitiesByRole(undefined, false).filter(API3.Filters.byClass("Unit")); - noRole.forEach(function(ent) { + for (var ent of noRole.values()) + { if (!ent.position()) - return; + continue; if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") !== -1) - return; + continue; if (ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined) - return; + continue; if (ent.hasClass("Ship") || ent.hasClass("Support") || ent.attackTypes() === undefined) - return; + continue; ent.setMetadata(PlayerID, "plan", plan); self.unitCollection.updateEnt(ent); added = true; - }); + } // Add units previously in a plan, but which left it because needed for defense or attack finished gameState.ai.HQ.attackManager.outOfPlan.forEach(function(ent) { if (!ent.position()) @@ -715,21 +717,22 @@ m.AttackPlan.prototype.assignUnits = function(gameState) // For a rush, assign also workers (but keep a minimum number of defenders) var worker = gameState.getOwnEntitiesByRole("worker", true); var num = 0; - worker.forEach(function(ent) { + for (var ent of worker.values()) + { if (!ent.position()) - return; + continue; if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") !== -1) - return; + continue; if (ent.getMetadata(PlayerID, "transport") !== undefined) - return; + continue; if (ent.hasClass("Ship") || ent.hasClass("Support") || ent.attackTypes() === undefined) - return; + continue; if (num++ < 9) - return; + continue; ent.setMetadata(PlayerID, "plan", plan); self.unitCollection.updateEnt(ent); added = true; - }); + } return added; }; @@ -737,15 +740,15 @@ m.AttackPlan.prototype.assignUnits = function(gameState) m.AttackPlan.prototype.reassignCavUnit = function(gameState) { var found = undefined; - this.unitCollection.forEach(function(ent) { - if (found) - return; + for (var ent of this.unitCollection.values()) + { if (!ent.position() || ent.getMetadata(PlayerID, "transport") !== undefined) - return; + continue; if (!ent.hasClass("Cavalry") || !ent.hasClass("CitizenSoldier")) - return; + continue; found = ent; - }); + break; + } if (!found) return; let raid = gameState.ai.HQ.attackManager.getAttackInPreparation("Raid"); @@ -771,18 +774,19 @@ m.AttackPlan.prototype.getNearestTarget = function(gameState, position, sameLand // picking the nearest target var minDist = -1; var target = undefined; - targets.forEach(function (ent) { + for (var ent of targets.values()) + { if (!ent.position()) - return; + continue; if (sameLand && gameState.ai.accessibility.getAccessValue(ent.position()) != land) - return; + continue; var dist = API3.SquareVectorDistance(ent.position(), position); if (dist < minDist || minDist == -1) { minDist = dist; target = ent; } - }); + } if (!target) return undefined; // Rushes can change their enemy target if nothing found with the preferred enemy @@ -1012,9 +1016,8 @@ m.AttackPlan.prototype.StartAttack = function(gameState) var curPos = this.unitCollection.getCentrePosition(); - this.unitCollection.forEach(function(ent) { + for (var 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)) @@ -1036,9 +1039,8 @@ 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 - this.unitCollection.forEach(function (entity) { - gameState.ai.HQ.navalManager.requireTransport(gameState, entity, startIndex, endIndex, endPos); - }); + for (var ent of this.unitCollection.values()) + gameState.ai.HQ.navalManager.requireTransport(gameState, ent, startIndex, endIndex, endPos); } } else @@ -1069,16 +1071,17 @@ m.AttackPlan.prototype.update = function(gameState, events) if (this.state === "transporting") { var done = true; - this.unitCollection.forEach(function (entity) { - if (self.Config.debug > 1 && entity.getMetadata(PlayerID, "transport") !== undefined) - Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [entity.id()], "rgb": [2,2,0]}); - else if (self.Config.debug > 1) - Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [entity.id()], "rgb": [1,1,1]}); + for (var 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]}); + else if (this.Config.debug > 1) + Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [1,1,1]}); if (!done) - return; - if (entity.getMetadata(PlayerID, "transport") !== undefined) + continue; + if (ent.getMetadata(PlayerID, "transport") !== undefined) done = false; - }); + } if (done) this.state = "arrived"; @@ -1094,13 +1097,14 @@ m.AttackPlan.prototype.update = function(gameState, events) var ourUnit = gameState.getEntityById(evt.target); if (!attacker || !ourUnit) continue; - this.unitCollection.forEach(function (entity) { - if (entity.getMetadata(PlayerID, "transport") !== undefined) - return; - if (!entity.isIdle()) - return; - entity.attack(attacker.id()); - }); + for (var ent of this.unitCollection.values()) + { + if (ent.getMetadata(PlayerID, "transport") !== undefined) + continue; + if (!ent.isIdle()) + continue; + ent.attack(attacker.id()); + } break; } } @@ -1135,10 +1139,9 @@ m.AttackPlan.prototype.update = function(gameState, events) if (attackedUnitNB == 0) { var siegeNB = 0; - this.unitCollection.forEach( function (ent) { - if (self.isSiegeUnit(gameState, ent)) + for (var ent of this.unitCollection.values()) + if (this.isSiegeUnit(gameState, ent)) siegeNB++; - }); if (siegeNB == 0) maybe = false; } diff --git a/binaries/data/mods/public/simulation/ai/petra/gamestate-extend.js b/binaries/data/mods/public/simulation/ai/petra/gamestate-extend.js index d0c9ffc451..fef7485200 100644 --- a/binaries/data/mods/public/simulation/ai/petra/gamestate-extend.js +++ b/binaries/data/mods/public/simulation/ai/petra/gamestate-extend.js @@ -1,7 +1,7 @@ var PETRA = function(m) { -// Some functions that could be part of the gamestate but are Aegis specific. +// Some functions that could be part of the gamestate. // The next three are to register that we assigned a gatherer to a resource this turn. // expects an entity diff --git a/binaries/data/mods/public/simulation/ai/petra/headquarters.js b/binaries/data/mods/public/simulation/ai/petra/headquarters.js index c7130b8ea4..856d3b13c9 100644 --- a/binaries/data/mods/public/simulation/ai/petra/headquarters.js +++ b/binaries/data/mods/public/simulation/ai/petra/headquarters.js @@ -48,7 +48,7 @@ m.HQ = function(Config) }; // More initialisation for stuff that needs the gameState -m.HQ.prototype.init = function(gameState, queues, deserializing) +m.HQ.prototype.init = function(gameState, queues) { this.territoryMap = m.createTerritoryMap(gameState); // initialize base map. Each pixel is a base ID, or 0 if not or not accessible @@ -77,348 +77,26 @@ m.HQ.prototype.init = function(gameState, queues, deserializing) return false; }); this.treasures.registerUpdates(); - - if (deserializing) - return; - - // determine the main land Index (or water index if none) - let landIndex = undefined; - let seaIndex = undefined; - var ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")); - for (let cc of ccEnts.values()) - { - let land = gameState.ai.accessibility.getAccessValue(cc.position()); - if (land > 1) - { - landIndex = land; - break; - } - } - if (!landIndex) - { - for (let ent of gameState.getOwnEntities().values()) - { - if (!ent.position() || (!ent.hasClass("Unit") && !ent.trainableEntities())) - continue; - let land = gameState.ai.accessibility.getAccessValue(ent.position()); - if (land > 1) - { - landIndex = land; - break; - } - let sea = gameState.ai.accessibility.getAccessValue(ent.position(), true); - if (!seaIndex && sea > 1) - seaIndex = sea; - } - } - if (!landIndex && !seaIndex) - API3.warn("Petra error: it does not know how to interpret this map"); - - var passabilityMap = gameState.getMap(); - var totalSize = passabilityMap.width * passabilityMap.width; - var minLandSize = Math.floor(0.1*totalSize); - var minWaterSize = Math.floor(0.3*totalSize); - var cellArea = passabilityMap.cellSize * passabilityMap.cellSize; - for (var i = 0; i < gameState.ai.accessibility.regionSize.length; ++i) - { - if (landIndex && i == landIndex) - this.landRegions[i] = true; - else if (gameState.ai.accessibility.regionType[i] === "land" && cellArea*gameState.ai.accessibility.regionSize[i] > 320) - { - if (landIndex) - { - var sea = this.getSeaIndex(gameState, landIndex, i); - if (sea && (gameState.ai.accessibility.regionSize[i] > minLandSize || gameState.ai.accessibility.regionSize[sea] > minWaterSize)) - { - this.navalMap = true; - this.landRegions[i] = true; - this.navalRegions[sea] = true; - } - } - else - { - var traject = gameState.ai.accessibility.getTrajectToIndex(seaIndex, i); - if (traject && traject.length === 2) - { - this.navalMap = true; - this.landRegions[i] = true; - this.navalRegions[seaIndex] = true; - } - } - } - else if (gameState.ai.accessibility.regionType[i] === "water" && gameState.ai.accessibility.regionSize[i] > minWaterSize) - { - this.navalMap = true; - this.navalRegions[i] = true; - } - } - if (this.Config.debug > 2) - { - for (var region in this.landRegions) - API3.warn(" >>> zone " + region + " taille " + cellArea*gameState.ai.accessibility.regionSize[region]); - API3.warn(" navalMap " + this.navalMap); - API3.warn(" landRegions " + uneval(this.landRegions)); - API3.warn(" navalRegions " + uneval(this.navalRegions)); - } - - // TODO: change that to something dynamic. - var civ = gameState.playerData.civ; - // load units and buildings from the config files - if (civ in this.Config.buildings.base) - this.bBase = this.Config.buildings.base[civ]; - else - this.bBase = this.Config.buildings.base['default']; - - if (civ in this.Config.buildings.advanced) - this.bAdvanced = this.Config.buildings.advanced[civ]; - else - this.bAdvanced = this.Config.buildings.advanced['default']; - for (var i in this.bBase) - this.bBase[i] = gameState.applyCiv(this.bBase[i]); - for (var i in this.bAdvanced) - this.bAdvanced[i] = gameState.applyCiv(this.bAdvanced[i]); - - // Let's get our initial situation here. - let nobase = new m.BaseManager(gameState, this.Config); - nobase.init(gameState); - nobase.accessIndex = 0; - this.baseManagers.push(nobase); // baseManagers[0] will deal with unit/structure without base - var ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")); - for (let cc of ccEnts.values()) - { - let newbase = new m.BaseManager(gameState, this.Config); - newbase.init(gameState); - newbase.setAnchor(gameState, cc); - this.baseManagers.push(newbase); - } - this.updateTerritories(gameState); - - // Assign entities in the different bases - var defaultbase = (this.numActiveBase() > 0) ? 1 : 0; - var width = gameState.getMap().width; - for (var ent of gameState.getOwnEntities().values()) - { - // make sure we have not rejected small regions with units (TODO should probably also check with other non-gaia units) - if (ent.position()) - { - let pos = gameState.ai.accessibility.gamePosToMapPos(ent.position()); - let index = pos[0] + pos[1]*gameState.ai.accessibility.width; - let land = gameState.ai.accessibility.landPassMap[index]; - if (land > 1 && !this.landRegions[land]) - this.landRegions[land] = true; - let sea = gameState.ai.accessibility.navalPassMap[index]; - if (sea > 1 && !this.navalRegions[sea]) - this.navalRegions[sea] = true; - } - // if garrisoned units inside, ungarrison them except if a ship in which case we will make a transport - if (ent.isGarrisonHolder() && ent.garrisoned().length && !ent.hasClass("Ship")) - for (let id of ent.garrisoned()) - ent.unload(id); - // do not affect merchant ship immediately to trade as they may-be useful for transport - if (ent.hasClass("Trader") && !ent.hasClass("Ship")) - this.tradeManager.assignTrader(ent); - var pos = ent.position(); - if (!pos) - continue; - ent.setMetadata(PlayerID, "access", gameState.ai.accessibility.getAccessValue(pos)); - var x = Math.round(pos[0] / gameState.cellSize); - var z = Math.round(pos[1] / gameState.cellSize); - var id = x + width*z; - var bestbase = undefined; - for (var i = 1; i < this.baseManagers.length; ++i) - { - var base = this.baseManagers[i]; - if (base.territoryIndices.indexOf(id) == -1) - continue; - base.assignEntity(ent); - if (ent.resourceDropsiteTypes() && !ent.hasClass("Elephant")) - base.assignResourceToDropsite(gameState, ent); - bestbase = base; - break; - } - if (!bestbase) - { - // entity outside our territory - var bestbase = m.getBestBase(ent, gameState); - bestbase.assignEntity(ent); - if (bestbase.ID !== this.baseManagers[0].ID && ent.resourceDropsiteTypes() && !ent.hasClass("Elephant")) - bestbase.assignResourceToDropsite(gameState, ent); - } - // now assign entities garrisoned inside this entity - if (ent.isGarrisonHolder() && ent.garrisoned().length) - for (let id of ent.garrisoned()) - bestBase.assignEntity(gameState.getEntityByID(id)); - } - - // we now have enough data to decide on a few things. - - // immediatly build a wood dropsite if possible. - if (this.baseManagers.length > 1 && gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_storehouse"), true) == 0) - { - var newDP = this.baseManagers[1].findBestDropsiteLocation(gameState, "wood"); - if (newDP.quality > 40 && this.canBuild(gameState, "structures/{civ}_storehouse")) - queues.dropsites.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_storehouse", { "base": this.baseManagers[1].ID }, newDP.pos)); - } - - // Check if we will ever be able to produce units - this.canBuildUnits = true; - if (!gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).length) - { - var template = gameState.applyCiv("structures/{civ}_civil_centre"); - if (gameState.isDisabledTemplates(template) || !gameState.getTemplate(template).available(gameState)) - { - if (this.Config.debug > 1) - API3.warn(" this AI is unable to produce any units"); - this.canBuildUnits = false; - var allycc = gameState.getExclusiveAllyEntities().filter(API3.Filters.byClass("CivCentre")).toEntityArray(); - if (allycc.length) - { - // if we have some allies, keep a fraction of our units to defend them - // and devote the rest to atacks - if (this.Config.debug > 1) - API3.warn(" We have allied cc " + allycc.length + " and " + gameState.getOwnUnits().length + " units "); - var units = gameState.getOwnUnits(); - var num = Math.max(Math.min(Math.round(0.08*(1+this.Config.personality.cooperative)*units.length), 20), 5); - var num1 = Math.floor(num / 2); - var num2 = num1; - // first pass to affect ranged infantry - units.filter(API3.Filters.byClassesAnd(["Infantry", "Ranged"])).forEach(function (ent) { - if (!num || !num1) - return; - if (ent.getMetadata(PlayerID, "allied")) - return; - var access = gameState.ai.accessibility.getAccessValue(ent.position()); - for (var cc of allycc) - { - if (!cc.position()) - continue; - if (gameState.ai.accessibility.getAccessValue(cc.position()) != access) - continue; - --num; - --num1; - ent.setMetadata(PlayerID, "allied", true); - var range = 1.5 * cc.footprintRadius(); - ent.moveToRange(cc.position()[0], cc.position()[1], range, range); - break; - } - }); - // second pass to affect melee infantry - units.filter(API3.Filters.byClassesAnd(["Infantry", "Melee"])).forEach(function (ent) { - if (!num || !num2) - return; - if (ent.getMetadata(PlayerID, "allied")) - return; - var access = gameState.ai.accessibility.getAccessValue(ent.position()); - for (var cc of allycc) - { - if (!cc.position()) - continue; - if (gameState.ai.accessibility.getAccessValue(cc.position()) != access) - continue; - --num; - --num2; - ent.setMetadata(PlayerID, "allied", true); - var range = 1.5 * cc.footprintRadius(); - ent.moveToRange(cc.position()[0], cc.position()[1], range, range); - break; - } - }); - // and now complete the affectation, including all support units - units.forEach(function (ent) { - if (!num && !ent.hasClass("Support")) - return; - if (ent.getMetadata(PlayerID, "allied")) - return; - var access = gameState.ai.accessibility.getAccessValue(ent.position()); - for (var cc of allycc) - { - if (!cc.position()) - continue; - if (gameState.ai.accessibility.getAccessValue(cc.position()) != access) - continue; - if (!ent.hasClass("Support")) - --num; - ent.setMetadata(PlayerID, "allied", true); - var range = 1.5 * cc.footprintRadius(); - ent.moveToRange(cc.position()[0], cc.position()[1], range, range); - break; - } - }); - } - } - } - - this.attackManager.init(gameState); - this.navalManager.init(gameState); - this.tradeManager.init(gameState); }; -m.HQ.prototype.start = function(gameState, deserializing) +/** + * initialization needed after deserialization (only called when deserialization) + */ +m.HQ.prototype.postinit = function(gameState) { - if (deserializing) - { - // Rebuild the base maps from the territory indices of each base - this.basesMap = new API3.Map(gameState.sharedScript, "territory"); - for (let base of this.baseManagers) - for (let j of base.territoryIndices) - this.basesMap.map[j] = base.ID; + // Rebuild the base maps from the territory indices of each base + this.basesMap = new API3.Map(gameState.sharedScript, "territory"); + for (let base of this.baseManagers) + for (let j of base.territoryIndices) + this.basesMap.map[j] = base.ID; - for (let ent of gameState.getOwnEntities().values()) - { - if (!ent.resourceDropsiteTypes() || ent.hasClass("Elephant")) - continue; - let base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); - base.assignResourceToDropsite(gameState, ent); - } - return; - } - - // adapt our starting strategy to the available resources - // - if on a small island, favor fishing and require less fields to save room for buildings - var startingSize = 0; - for (let region in this.landRegions) + for (let ent of gameState.getOwnEntities().values()) { - for (let base of this.baseManagers) - { - if (!base.anchor || base.accessIndex != +region) - continue; - startingSize += gameState.ai.accessibility.regionSize[region]; - break; - } + if (!ent.resourceDropsiteTypes() || ent.hasClass("Elephant")) + continue; + let base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); + base.assignResourceToDropsite(gameState, ent); } - var cell = gameState.getMap().cellSize; - startingSize = startingSize * cell * cell; - if (this.Config.debug > 1) - API3.warn("starting size " + startingSize + "(cut at 24000 for fish pushing)"); - if (startingSize < 24000) - { - this.saveSpace = true; - this.Config.Economy.popForDock = Math.min(this.Config.Economy.popForDock, 16); - this.Config.Economy.targetNumFishers = Math.max(this.Config.Economy.targetNumFishers, 2); - } - // - count the available wood resource, and allow rushes only if enough (we should otherwise favor expansion) - var startingWood = gameState.getResources()["wood"]; - var check = {}; - for (var proxim of ["nearby", "medium", "faraway"]) - { - for (let base of this.baseManagers) - { - for (var supply of base.dropsiteSupplies["wood"][proxim]) - { - if (check[supply.id]) // avoid double counting as same resource can appear several time - continue; - check[supply.id] = true; - startingWood += supply.ent.resourceSupplyAmount(); - } - } - } - if (this.Config.debug > 1) - API3.warn("startingWood: " + startingWood + "(cut at 8500 for no rush and 6000 for saveResources)"); - if (startingWood < 6000) - this.saveResources = true; - - if (startingWood > 8500 && this.canBuildUnits) - this.attackManager.setRushes(); }; // returns the sea index linking regions 1 and region 2 (supposed to be different land region) @@ -492,6 +170,8 @@ m.HQ.prototype.checkEvents = function (gameState, events, queues) // TODO: move to the base manager. if (evt.newentity) { + if (evt.newentity === evt.entity) // repaired building + continue; var ent = gameState.getEntityById(evt.newentity); if (!ent || !ent.isOwn(PlayerID)) continue; @@ -505,9 +185,17 @@ m.HQ.prototype.checkEvents = function (gameState, events, queues) base.anchorId = evt.newentity; base.buildings.updateEnt(ent); this.updateTerritories(gameState); - // let us hope this new base will fix our resource shortage - this.saveResources = undefined; - this.saveSpace = undefined; + if (base.ID === this.baseManagers[1].ID) + { + // this is our first base, let us configure our starting resources + this.configFirstBase(gameState); + } + else + { + // let us hope this new base will fix our possible resource shortage + this.saveResources = undefined; + this.saveSpace = undefined; + } } else if (ent.hasTerritoryInfluence()) this.updateTerritories(gameState); diff --git a/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js b/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js new file mode 100644 index 0000000000..08a3a9b001 --- /dev/null +++ b/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js @@ -0,0 +1,378 @@ +var PETRA = function(m) +{ +/** + * determines the strategy to adopt when starting a new game, depending on the initial conditions + */ + +m.HQ.prototype.gameAnalysis = function(gameState) +{ + // Analysis of the terrain and the different access regions + this.regionAnalysis(gameState); + + // Make a list of buildable structures from the config file + this.structureAnalysis(gameState); + + // Let's get our initial situation here. + let nobase = new m.BaseManager(gameState, this.Config); + nobase.init(gameState); + nobase.accessIndex = 0; + this.baseManagers.push(nobase); // baseManagers[0] will deal with unit/structure without base + var ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")); + for (let cc of ccEnts.values()) + { + let newbase = new m.BaseManager(gameState, this.Config); + newbase.init(gameState); + newbase.setAnchor(gameState, cc); + this.baseManagers.push(newbase); + } + this.updateTerritories(gameState); + + // Assign entities and resources in the different bases + this.assignStartingEntities(gameState); + + // Check if we will ever be able to produce units + this.canBuildUnits = true; + if (!gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).length) + { + var template = gameState.applyCiv("structures/{civ}_civil_centre"); + if (gameState.isDisabledTemplates(template) || !gameState.getTemplate(template).available(gameState)) + { + if (this.Config.debug > 1) + API3.warn(" this AI is unable to produce any units"); + this.canBuildUnits = false; + this.dispatchUnits(gameState); + } + } + + this.attackManager.init(gameState); + this.navalManager.init(gameState); + this.tradeManager.init(gameState); + + // configure our first base strategy + if (this.baseManagers.length > 1) + this.configFirstBase(gameState); +}; + +/** + * Assign the starting entities to the different bases + */ +m.HQ.prototype.assignStartingEntities = function(gameState) +{ + var defaultbase = (this.numActiveBase() > 0) ? 1 : 0; + var width = gameState.getMap().width; + for (var ent of gameState.getOwnEntities().values()) + { + // do not affect merchant ship immediately to trade as they may-be useful for transport + if (ent.hasClass("Trader") && !ent.hasClass("Ship")) + this.tradeManager.assignTrader(ent); + + var pos = ent.position(); + if (!pos) + { + // TODO should support recursive garrisoning. Make a warning for now + if (ent.isGarrisonHolder() && ent.garrisoned().length) + API3.warn("Petra warning: support for garrisoned units inside garrisoned holders not yet implemented"); + continue; + } + + // make sure we have not rejected small regions with units (TODO should probably also check with other non-gaia units) + let gamepos = gameState.ai.accessibility.gamePosToMapPos(pos); + let index = gamepos[0] + gamepos[1]*gameState.ai.accessibility.width; + let land = gameState.ai.accessibility.landPassMap[index]; + if (land > 1 && !this.landRegions[land]) + this.landRegions[land] = true; + let sea = gameState.ai.accessibility.navalPassMap[index]; + if (sea > 1 && !this.navalRegions[sea]) + this.navalRegions[sea] = true; + + // if garrisoned units inside, ungarrison them except if a ship in which case we will make a transport + if (ent.isGarrisonHolder() && ent.garrisoned().length && !ent.hasClass("Ship")) + for (let id of ent.garrisoned()) + ent.unload(id); + + ent.setMetadata(PlayerID, "access", gameState.ai.accessibility.getAccessValue(pos)); + var bestbase = undefined; + for (var i = 1; i < this.baseManagers.length; ++i) + { + var base = this.baseManagers[i]; + if (base.territoryIndices.indexOf(index) === -1) + continue; + base.assignEntity(ent); + if (ent.resourceDropsiteTypes() && !ent.hasClass("Elephant")) + base.assignResourceToDropsite(gameState, ent); + bestbase = base; + break; + } + if (!bestbase) + { + // entity outside our territory + var bestbase = m.getBestBase(ent, gameState); + bestbase.assignEntity(ent); + if (bestbase.ID !== this.baseManagers[0].ID && ent.resourceDropsiteTypes() && !ent.hasClass("Elephant")) + bestbase.assignResourceToDropsite(gameState, ent); + } + // now assign entities garrisoned inside this entity + if (ent.isGarrisonHolder() && ent.garrisoned().length) + for (let id of ent.garrisoned()) + bestBase.assignEntity(gameState.getEntityByID(id)); + } +}; + +/** + * determine the main land Index (or water index if none) + * as well as the list of allowed (land andf water) regions + */ +m.HQ.prototype.regionAnalysis = function(gameState) +{ + let landIndex = undefined; + let seaIndex = undefined; + var ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")); + for (let cc of ccEnts.values()) + { + let land = gameState.ai.accessibility.getAccessValue(cc.position()); + if (land > 1) + { + landIndex = land; + break; + } + } + if (!landIndex) + { + for (let ent of gameState.getOwnEntities().values()) + { + if (!ent.position() || (!ent.hasClass("Unit") && !ent.trainableEntities())) + continue; + let land = gameState.ai.accessibility.getAccessValue(ent.position()); + if (land > 1) + { + landIndex = land; + break; + } + let sea = gameState.ai.accessibility.getAccessValue(ent.position(), true); + if (!seaIndex && sea > 1) + seaIndex = sea; + } + } + if (!landIndex && !seaIndex) + API3.warn("Petra error: it does not know how to interpret this map"); + + var passabilityMap = gameState.getMap(); + var totalSize = passabilityMap.width * passabilityMap.width; + var minLandSize = Math.floor(0.1*totalSize); + var minWaterSize = Math.floor(0.3*totalSize); + var cellArea = passabilityMap.cellSize * passabilityMap.cellSize; + for (var i = 0; i < gameState.ai.accessibility.regionSize.length; ++i) + { + if (landIndex && i == landIndex) + this.landRegions[i] = true; + else if (gameState.ai.accessibility.regionType[i] === "land" && cellArea*gameState.ai.accessibility.regionSize[i] > 320) + { + if (landIndex) + { + var sea = this.getSeaIndex(gameState, landIndex, i); + if (sea && (gameState.ai.accessibility.regionSize[i] > minLandSize || gameState.ai.accessibility.regionSize[sea] > minWaterSize)) + { + this.navalMap = true; + this.landRegions[i] = true; + this.navalRegions[sea] = true; + } + } + else + { + var traject = gameState.ai.accessibility.getTrajectToIndex(seaIndex, i); + if (traject && traject.length === 2) + { + this.navalMap = true; + this.landRegions[i] = true; + this.navalRegions[seaIndex] = true; + } + } + } + else if (gameState.ai.accessibility.regionType[i] === "water" && gameState.ai.accessibility.regionSize[i] > minWaterSize) + { + this.navalMap = true; + this.navalRegions[i] = true; + } + } + + if (this.Config.debug < 3) + return; + for (var region in this.landRegions) + API3.warn(" >>> zone " + region + " taille " + cellArea*gameState.ai.accessibility.regionSize[region]); + API3.warn(" navalMap " + this.navalMap); + API3.warn(" landRegions " + uneval(this.landRegions)); + API3.warn(" navalRegions " + uneval(this.navalRegions)); +}; + +/** + * load units and buildings from the config files + * TODO: change that to something dynamic + */ +m.HQ.prototype.structureAnalysis = function(gameState) +{ + var civ = gameState.playerData.civ; + if (civ in this.Config.buildings.base) + this.bBase = this.Config.buildings.base[civ]; + else + this.bBase = this.Config.buildings.base['default']; + + if (civ in this.Config.buildings.advanced) + this.bAdvanced = this.Config.buildings.advanced[civ]; + else + this.bAdvanced = this.Config.buildings.advanced['default']; + for (var i in this.bBase) + this.bBase[i] = gameState.applyCiv(this.bBase[i]); + for (var i in this.bAdvanced) + this.bAdvanced[i] = gameState.applyCiv(this.bAdvanced[i]); +}; + +/** + * set strategy if game without construction: + * - if one of our allies has a cc, affect a small fraction of our army for his defense, the rest will attack + * - otherwise all units will attack + */ +m.HQ.prototype.dispatchUnits = function(gameState) +{ + var allycc = gameState.getExclusiveAllyEntities().filter(API3.Filters.byClass("CivCentre")).toEntityArray(); + if (allycc.length) + { + if (this.Config.debug > 1) + API3.warn(" We have allied cc " + allycc.length + " and " + gameState.getOwnUnits().length + " units "); + var units = gameState.getOwnUnits(); + var num = Math.max(Math.min(Math.round(0.08*(1+this.Config.personality.cooperative)*units.length), 20), 5); + var num1 = Math.floor(num / 2); + var num2 = num1; + // first pass to affect ranged infantry + units.filter(API3.Filters.byClassesAnd(["Infantry", "Ranged"])).forEach(function (ent) { + if (!num || !num1) + return; + if (ent.getMetadata(PlayerID, "allied")) + return; + var access = gameState.ai.accessibility.getAccessValue(ent.position()); + for (var cc of allycc) + { + if (!cc.position()) + continue; + if (gameState.ai.accessibility.getAccessValue(cc.position()) != access) + continue; + --num; + --num1; + ent.setMetadata(PlayerID, "allied", true); + var range = 1.5 * cc.footprintRadius(); + ent.moveToRange(cc.position()[0], cc.position()[1], range, range); + break; + } + }); + // second pass to affect melee infantry + units.filter(API3.Filters.byClassesAnd(["Infantry", "Melee"])).forEach(function (ent) { + if (!num || !num2) + return; + if (ent.getMetadata(PlayerID, "allied")) + return; + var access = gameState.ai.accessibility.getAccessValue(ent.position()); + for (var cc of allycc) + { + if (!cc.position()) + continue; + if (gameState.ai.accessibility.getAccessValue(cc.position()) != access) + continue; + --num; + --num2; + ent.setMetadata(PlayerID, "allied", true); + var range = 1.5 * cc.footprintRadius(); + ent.moveToRange(cc.position()[0], cc.position()[1], range, range); + break; + } + }); + // and now complete the affectation, including all support units + units.forEach(function (ent) { + if (!num && !ent.hasClass("Support")) + return; + if (ent.getMetadata(PlayerID, "allied")) + return; + var access = gameState.ai.accessibility.getAccessValue(ent.position()); + for (var cc of allycc) + { + if (!cc.position()) + continue; + if (gameState.ai.accessibility.getAccessValue(cc.position()) != access) + continue; + if (!ent.hasClass("Support")) + --num; + ent.setMetadata(PlayerID, "allied", true); + var range = 1.5 * cc.footprintRadius(); + ent.moveToRange(cc.position()[0], cc.position()[1], range, range); + break; + } + }); + } +}; + +/** + * configure our first base expansion + * - if on a small island, favor fishing + * - count the available wood resource, and allow rushes only if enough (we should otherwise favor expansion) + */ +m.HQ.prototype.configFirstBase = function(gameState) +{ + if (this.baseManagers.length < 2) + return; + + var startingSize = 0; + for (let region in this.landRegions) + { + for (let base of this.baseManagers) + { + if (!base.anchor || base.accessIndex != +region) + continue; + startingSize += gameState.ai.accessibility.regionSize[region]; + break; + } + } + var cell = gameState.getMap().cellSize; + startingSize = startingSize * cell * cell; + if (this.Config.debug > 1) + API3.warn("starting size " + startingSize + "(cut at 24000 for fish pushing)"); + if (startingSize < 24000) + { + this.saveSpace = true; + this.Config.Economy.popForDock = Math.min(this.Config.Economy.popForDock, 16); + this.Config.Economy.targetNumFishers = Math.max(this.Config.Economy.targetNumFishers, 2); + } + + // - count the available wood resource, and allow rushes only if enough (we should otherwise favor expansion) + var startingWood = gameState.getResources()["wood"]; + var check = {}; + for (var proxim of ["nearby", "medium", "faraway"]) + { + for (let base of this.baseManagers) + { + for (var supply of base.dropsiteSupplies["wood"][proxim]) + { + if (check[supply.id]) // avoid double counting as same resource can appear several time + continue; + check[supply.id] = true; + startingWood += supply.ent.resourceSupplyAmount(); + } + } + } + if (this.Config.debug > 1) + API3.warn("startingWood: " + startingWood + "(cut at 8500 for no rush and 6000 for saveResources)"); + if (startingWood < 6000) + this.saveResources = true; + if (startingWood > 8500 && this.canBuildUnits) + this.attackManager.setRushes(); + + // immediatly build a wood dropsite if possible. + var template = "structures/{civ}_storehouse"; + if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv(template), true) === 0 && this.canBuild(gameState, template)) + { + var newDP = this.baseManagers[1].findBestDropsiteLocation(gameState, "wood"); + if (newDP.quality > 40) + gameState.ai.queues.dropsites.addItem(new m.ConstructionPlan(gameState, template, { "base": this.baseManagers[1].ID }, newDP.pos)); + } +}; + +return m; + +}(PETRA); diff --git a/binaries/data/mods/public/simulation/ai/petra/worker.js b/binaries/data/mods/public/simulation/ai/petra/worker.js index 1ebb125bfb..b24f53736e 100644 --- a/binaries/data/mods/public/simulation/ai/petra/worker.js +++ b/binaries/data/mods/public/simulation/ai/petra/worker.js @@ -678,21 +678,21 @@ m.Worker.prototype.startFishing = function(gameState) m.Worker.prototype.gatherNearestField = function(gameState, baseID) { - var self = this; var ownFields = gameState.getOwnEntitiesByType(gameState.applyCiv("structures/{civ}_field"), true).filter(API3.Filters.byMetadata(PlayerID, "base", baseID)); var bestFarmEnt = false; var bestFarmDist = 10000000; - ownFields.forEach(function (field) { + for (var field of ownFields.values()) + { if (m.IsSupplyFull(gameState, field) === true) - return; - var dist = API3.SquareVectorDistance(field.position(), self.ent.position()); + continue; + var dist = API3.SquareVectorDistance(field.position(), this.ent.position()); if (dist < bestFarmDist) { bestFarmEnt = field; bestFarmDist = dist; } - }); + } if (bestFarmEnt) { m.AddTCGatherer(gameState, bestFarmEnt.id()); @@ -712,18 +712,19 @@ m.Worker.prototype.buildAnyField = function(gameState, baseID) var bestFarmEnt = false; var bestFarmDist = 10000000; var pos = this.ent.position(); - baseFoundations.forEach(function (found) { + for (var found of baseFoundations.values()) + { if (!found.hasClass("Field")) - return; + continue; var current = found.getBuildersNb(); if (current === undefined || current >= maxGatherers) - return; + continue; var dist = API3.SquareVectorDistance(found.position(), pos); if (dist > bestFarmDist) - return; + continue; bestFarmEnt = found; bestFarmDist = dist; - }); + } return bestFarmEnt; };