diff --git a/binaries/data/mods/public/simulation/ai/common-api/entity.js b/binaries/data/mods/public/simulation/ai/common-api/entity.js index 7aa7e0a2f6..fb21d8e74a 100644 --- a/binaries/data/mods/public/simulation/ai/common-api/entity.js +++ b/binaries/data/mods/public/simulation/ai/common-api/entity.js @@ -342,11 +342,10 @@ m.Template = m.Class({ return 1; }, - "buildableEntities": function() { + "buildableEntities": function(civ = this.civ()) { let templates = this.get("Builder/Entities/_string"); if (!templates) return []; - let civ = this.civ(); return templates.replace(/\{civ\}/g, civ).split(/\s+/); }, diff --git a/binaries/data/mods/public/simulation/ai/common-api/gamestate.js b/binaries/data/mods/public/simulation/ai/common-api/gamestate.js index ee418c047e..00bf0c7461 100644 --- a/binaries/data/mods/public/simulation/ai/common-api/gamestate.js +++ b/binaries/data/mods/public/simulation/ai/common-api/gamestate.js @@ -872,33 +872,6 @@ m.GameState.prototype.findResearchers = function(templateName, noRequirementChec }); }; -/** - * Get any buildable structure with a given class - * TODO when several available, choose the best one - */ -m.GameState.prototype.findStructureWithClass = function(classes) -{ - let entTemplates = new Set(); - for (let ent of this.getOwnUnits().values()) - { - if (entTemplates.has(ent.templateName())) - continue; - let buildables = ent.buildableEntities(); - for (let buildable of buildables) - { - if (this.isTemplateDisabled(buildable)) - continue; - let template = this.getTemplate(buildable); - if (!template || !template.available(this)) - continue; - if (MatchesClassList(template.classes(), classes)) - return buildable; - } - entTemplates.add(ent.templateName()); - } - return undefined; -}; - m.GameState.prototype.getEntityLimits = function() { return this.playerData.entityLimits; diff --git a/binaries/data/mods/public/simulation/ai/petra/buildManager.js b/binaries/data/mods/public/simulation/ai/petra/buildManager.js new file mode 100644 index 0000000000..ed0520d35e --- /dev/null +++ b/binaries/data/mods/public/simulation/ai/petra/buildManager.js @@ -0,0 +1,171 @@ +var PETRA = function(m) +{ + +/** + * One task of this manager is to cache the list of structures we have builders for, + * to avoid having to loop on all entities each time. + * It also takes care of the structures we can't currently build and should not try to build endlessly. + */ + +m.BuildManager = function() +{ + // List of buildings we have builders for, with number of possible builders. + this.builderCounters = new Map(); + // List of buildings we can't currently build (because no room, no builder or whatever), + // with time we should wait before trying again to build it. + this.unbuildables = new Map(); +}; + +/** Initialization at start of game */ +m.BuildManager.prototype.init = function(gameState) +{ + let civ = gameState.getPlayerCiv(); + for (let ent of gameState.getOwnUnits().values()) + this.incrementBuilderCounters(civ, ent, 1); +}; + +m.BuildManager.prototype.incrementBuilderCounters = function(civ, ent, increment) +{ + for (let buildable of ent.buildableEntities(civ)) + { + if (this.builderCounters.has(buildable)) + { + let count = this.builderCounters.get(buildable) + increment; + if (count < 0) + { + API3.warning(" Petra error in incrementBuilderCounters for " + buildable + " with count < 0"); + continue; + } + this.builderCounters.set(buildable, count); + } + else if (increment > 0) + this.builderCounters.set(buildable, increment); + else + API3.warning(" Petra error in incrementBuilderCounters for " + buildable + " not yet set"); + } +}; + +/** Update the builders counters */ +m.BuildManager.prototype.checkEvents = function(gameState, events) +{ + this.elapsedTime = gameState.ai.elapsedTime; + let civ = gameState.getPlayerCiv(); + + for (let evt of events.Create) + { + let ent = gameState.getEntityById(evt.entity); + if (ent && ent.isOwn(PlayerID) && ent.hasClass("Unit")) + this.incrementBuilderCounters(civ, ent, 1); + } + + for (let evt of events.Destroy) + { + if (!evt.entityObj) + continue; + let ent = evt.entityObj; + if (ent && ent.isOwn(PlayerID) && ent.hasClass("Unit")) + this.incrementBuilderCounters(civ, ent, -1); + } + + for (let evt of events.OwnershipChanged) // capture events + { + let increment; + if (evt.from == PlayerID) + increment = -1; + else if (evt.to == PlayerID) + increment = 1; + else + continue; + let ent = gameState.getEntityById(evt.entity); + if (ent && ent.hasClass("Unit")) + this.incrementBuilderCounters(civ, ent, increment); + } +}; + + +/** + * Get the first buildable structure with a given class + * TODO when several available, choose the best one + */ +m.BuildManager.prototype.findStructureWithClass = function(gameState, classes) +{ + for (let [templateName, count] of this.builderCounters) + { + if (count == 0 || gameState.isTemplateDisabled(templateName)) + continue; + let template = gameState.getTemplate(templateName); + if (!template || !template.available(gameState)) + continue; + if (MatchesClassList(template.classes(), classes)) + return templateName; + } + return undefined; +}; + +m.BuildManager.prototype.hasBuilder = function(template) +{ + let numBuilders = this.builderCounters.get(template); + return numBuilders && numBuilders > 0; +}; + +m.BuildManager.prototype.isUnbuildable = function(gameState, template) +{ + return this.unbuildables.has(template) && this.unbuildables.get(template).time > gameState.ai.elapsedTime; +}; + +m.BuildManager.prototype.setBuildable = function(template) +{ + if (this.unbuildables.has(template)) + this.unbuildables.delete(template); +}; + +/** Time is the duration in second that we will wait before checking again if it is buildable */ +m.BuildManager.prototype.setUnbuildable = function(gameState, template, time = 180, reason = "room") +{ + if (!this.unbuildables.has(template)) + this.unbuildables.set(template, { "reason": reason, "time": gameState.ai.elapsedTime + time }); + else + { + let unbuildable = this.unbuildables.get(template); + if (unbuildable.time < gameState.ai.elapsedTime + time) + { + unbuildable.reason = reason; + unbuildable.time = gameState.ai.elapsedTime + time; + } + } +}; + +/** Return the number of unbuildables due to missing room */ +m.BuildManager.prototype.numberMissingRoom = function(gameState) +{ + let num = 0; + for (let unbuildable of this.unbuildables.values()) + if (unbuildable.reason == "room" && unbuildable.time > gameState.ai.elapsedTime) + ++num; + return num; +}; + +/** Reset the unbuildables due to missing room */ +m.BuildManager.prototype.resetMissingRoom = function(gameState) +{ + for (let [key, unbuildable] of this.unbuildables) + if (unbuildable.reason == "room") + this.unbuildables.delete(key); +}; + +m.BuildManager.prototype.Serialize = function() +{ + return { + "builderCounters": this.builderCounters, + "unbuildables": this.unbuildables + }; +}; + +m.BuildManager.prototype.Deserialize = function(data) +{ + for (let key in data) + this[key] = data[key]; +}; + +return m; +}(PETRA); diff --git a/binaries/data/mods/public/simulation/ai/petra/headquarters.js b/binaries/data/mods/public/simulation/ai/petra/headquarters.js index 26331f2dd1..387433bff1 100644 --- a/binaries/data/mods/public/simulation/ai/petra/headquarters.js +++ b/binaries/data/mods/public/simulation/ai/petra/headquarters.js @@ -31,8 +31,6 @@ m.HQ = function(Config) this.targetNumWorkers = this.Config.Economy.targetNumWorkers; this.supportRatio = this.Config.Economy.supportRatio; - this.stopBuilding = new Map(); // list of buildings to stop (temporarily) production because no room - this.fortStartTime = 180; // sentry defense towers, will start at fortStartTime + towerLapseTime this.towerStartTime = 0; // stone defense towers, will start as soon as available this.towerLapseTime = this.Config.Military.towerLapseTime; @@ -43,6 +41,7 @@ m.HQ = function(Config) this.baseManagers = []; this.attackManager = new m.AttackManager(this.Config); + this.buildManager = new m.BuildManager(); this.defenseManager = new m.DefenseManager(this.Config); this.tradeManager = new m.TradeManager(this.Config); this.navalManager = new m.NavalManager(this.Config); @@ -134,6 +133,8 @@ m.HQ.prototype.getSeaBetweenIndices = function (gameState, index1, index2) m.HQ.prototype.checkEvents = function (gameState, events, queues) { + this.buildManager.checkEvents(gameState, events); + if (events.TerritoriesChanged.length || events.DiplomacyChanged.length) this.updateTerritories(gameState); @@ -469,7 +470,7 @@ m.HQ.prototype.checkPhaseRequirements = function(gameState, queues) queue = entityReq.class === "Wonder" ? "wonder" : "economicBuilding"; if (!queues[queue].hasQueuedUnits()) { - let structure = gameState.findStructureWithClass([entityReq.class]); + let structure = this.buildManager.findStructureWithClass(gameState, [entityReq.class]); if (structure && this.canBuild(gameState, structure)) plan = new m.ConstructionPlan(gameState, structure); } @@ -1533,11 +1534,11 @@ m.HQ.prototype.buildMoreHouses = function(gameState, queues) continue; let count = gameState.getOwnStructures().filter(API3.Filters.byClass(entityReq.class)).length; - if (count < entityReq.count && this.stopBuilding.has(houseTemplateName)) + if (count < entityReq.count && this.buildManager.isUnbuildable(gameState, houseTemplateName)) { if (this.Config.debug > 1) API3.warn("no room to place a house ... try to be less restrictive"); - this.stopBuilding.delete(houseTemplateName); + this.buildManager.setBuildable(houseTemplateName); this.requireHouses = true; } needed = Math.max(needed, entityReq.count - count); @@ -1572,25 +1573,18 @@ m.HQ.prototype.buildMoreHouses = function(gameState, queues) let priority; if (freeSlots < 5) { - if (this.stopBuilding.has(house)) + if (this.buildManager.isUnbuildable(gameState, house)) { - if (this.stopBuilding.get(house) > gameState.ai.elapsedTime) - { - if (this.Config.debug > 1) - API3.warn("no room to place a house ... try to improve with technology"); - this.researchManager.researchPopulationBonus(gameState, queues); - } - else - { - this.stopBuilding.delete(house); - priority = 2*this.Config.priorities.house; - } + if (this.Config.debug > 1) + API3.warn("no room to place a house ... try to improve with technology"); + this.researchManager.researchPopulationBonus(gameState, queues); } else priority = 2*this.Config.priorities.house; } else priority = this.Config.priorities.house; + if (priority && priority != gameState.ai.queueManager.getPriority("house")) gameState.ai.queueManager.changePriority("house", priority); }; @@ -1608,13 +1602,10 @@ m.HQ.prototype.checkBaseExpansion = function(gameState, queues) return; } // Then expand if we have not enough room available for buildings - let nstopped = 0; - for (let stopTime of this.stopBuilding.values()) + if (this.buildManager.numberMissingRoom(gameState) > 1) { - if (stopTime === Infinity || stopTime < gameState.ai.elapsedTime || ++nstopped < 2) - continue; if (this.Config.debug > 2) - API3.warn("try to build a new base because not enough room to build " + uneval(this.stopBuilding)); + API3.warn("try to build a new base because not enough room to build "); this.buildNewBase(gameState, queues); return; } @@ -1985,29 +1976,31 @@ m.HQ.prototype.trainEmergencyUnits = function(gameState, positions) m.HQ.prototype.canBuild = function(gameState, structure, debug = false) { let type = gameState.applyCiv(structure); - // available room to build it - if (this.stopBuilding.has(type)) - { - if (this.stopBuilding.get(type) > gameState.ai.elapsedTime) - return false; - this.stopBuilding.delete(type); - } + if (this.buildManager.isUnbuildable(gameState, type)) + return false; if (gameState.isTemplateDisabled(type)) { - this.stopBuilding.set(type, Infinity); + this.buildManager.setUnbuildable(gameState, type, Infinity, "disabled"); return false; } let template = gameState.getTemplate(type); if (!template) - this.stopBuilding.set(type, Infinity); - if (!template || !template.available(gameState)) - return false; - - if (!gameState.findBuilder(type)) { - this.stopBuilding.set(type, gameState.ai.elapsedTime + 120); + this.buildManager.setUnbuildable(gameState, type, Infinity, "notemplate"); + return false; + } + + if (!template.available(gameState)) + { + this.buildManager.setUnbuildable(gameState, type, 30, "tech"); + return false; + } + + if (!this.buildManager.hasBuilder(type)) + { + this.buildManager.setUnbuildable(gameState, type, 120, "nobuilder"); return false; } @@ -2017,7 +2010,7 @@ m.HQ.prototype.canBuild = function(gameState, structure, debug = false) let buildTerritories = template.buildTerritories(); if (buildTerritories && (!buildTerritories.length || buildTerritories.length === 1 && buildTerritories[0] === "own")) { - this.stopBuilding.set(type, gameState.ai.elapsedTime + 180); + this.buildManager.setUnbuildable(gameState, type, 180, "room"); return false; } } @@ -2027,29 +2020,13 @@ m.HQ.prototype.canBuild = function(gameState, structure, debug = false) let category = template.buildCategory(); if (category && limits[category] !== undefined && gameState.getEntityCounts()[category] >= limits[category]) { - this.stopBuilding.set(type, gameState.ai.elapsedTime + 60); + this.buildManager.setUnbuildable(gameState, type, 90, "limit"); return false; } return true; }; -m.HQ.prototype.stopBuild = function(gameState, structure, time=180) -{ - let type = gameState.applyCiv(structure); - if (this.stopBuilding.has(type)) - this.stopBuilding.set(type, Math.max(this.stopBuilding.get(type), gameState.ai.elapsedTime + time)); - else - this.stopBuilding.set(type, gameState.ai.elapsedTime + time); -}; - -m.HQ.prototype.restartBuild = function(gameState, structure) -{ - let type = gameState.applyCiv(structure); - if (this.stopBuilding.has(type)) - this.stopBuilding.delete(type); -}; - m.HQ.prototype.updateTerritories = function(gameState) { const around = [ [-0.7,0.7], [0,1], [0.7,0.7], [1,0], [0.7,-0.7], [0,-1], [-0.7,-0.7], [-1,0] ]; @@ -2162,9 +2139,7 @@ m.HQ.prototype.updateTerritories = function(gameState) if (!expansion) return; // We've increased our territory, so we may have some new room to build - for (let [type, stopTime] of this.stopBuilding) - if (stopTime !== Infinity) - this.stopBuilding.delete(type); + this.buildManager.resetMissingRoom(gameState); // And if sufficient expansion, check if building a new market would improve our present trade routes let cellArea = this.territoryMap.cellSize * this.territoryMap.cellSize; if (expansion * cellArea > 960) @@ -2492,7 +2467,6 @@ m.HQ.prototype.Serialize = function() "lastFailedGather": this.lastFailedGather, "supportRatio": this.supportRatio, "targetNumWorkers": this.targetNumWorkers, - "stopBuilding": this.stopBuilding, "fortStartTime": this.fortStartTime, "towerStartTime": this.towerStartTime, "fortressStartTime": this.fortressStartTime, @@ -2522,6 +2496,7 @@ m.HQ.prototype.Serialize = function() API3.warn(" properties " + uneval(properties)); API3.warn(" baseManagers " + uneval(baseManagers)); API3.warn(" attackManager " + uneval(this.attackManager.Serialize())); + API3.warn(" buildManager " + uneval(this.buildManager.Serialize())); API3.warn(" defenseManager " + uneval(this.defenseManager.Serialize())); API3.warn(" tradeManager " + uneval(this.tradeManager.Serialize())); API3.warn(" navalManager " + uneval(this.navalManager.Serialize())); @@ -2536,6 +2511,7 @@ m.HQ.prototype.Serialize = function() "baseManagers": baseManagers, "attackManager": this.attackManager.Serialize(), + "buildManager": this.buildManager.Serialize(), "defenseManager": this.defenseManager.Serialize(), "tradeManager": this.tradeManager.Serialize(), "navalManager": this.navalManager.Serialize(), @@ -2571,6 +2547,9 @@ m.HQ.prototype.Deserialize = function(gameState, data) this.attackManager.init(gameState); this.attackManager.Deserialize(gameState, data.attackManager); + this.buildManager = new m.BuildManager(); + this.buildManager.Deserialize(gameState, data.buildManager); + this.defenseManager = new m.DefenseManager(this.Config); this.defenseManager.Deserialize(gameState, data.defenseManager); diff --git a/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js b/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js index 44b8c9a5e9..c7b561f560 100644 --- a/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js +++ b/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js @@ -31,7 +31,7 @@ m.ConstructionPlan.prototype.canStart = function(gameState) if (this.template.requiredTech() && !gameState.isResearched(this.template.requiredTech())) return false; - return gameState.findBuilder(this.type) !== undefined; + return gameState.ai.HQ.buildManager.hasBuilder(this.type); }; m.ConstructionPlan.prototype.start = function(gameState) @@ -46,7 +46,7 @@ m.ConstructionPlan.prototype.start = function(gameState) let pos = this.findGoodPosition(gameState); if (!pos) { - gameState.ai.HQ.stopBuild(gameState, this.type); + gameState.ai.HQ.buildManager.setUnbuildable(gameState, this.type); Engine.ProfileStop(); return; } diff --git a/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js b/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js index 5b52e75f41..a81659493d 100644 --- a/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js +++ b/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js @@ -11,6 +11,7 @@ m.HQ.prototype.gameAnalysis = function(gameState) return; this.attackManager.init(gameState); + this.buildManager.init(gameState); this.navalManager.init(gameState); this.tradeManager.init(gameState); this.diplomacyManager.init(gameState); diff --git a/binaries/data/mods/public/simulation/ai/petra/tradeManager.js b/binaries/data/mods/public/simulation/ai/petra/tradeManager.js index 890cdfab32..8cd534ec54 100644 --- a/binaries/data/mods/public/simulation/ai/petra/tradeManager.js +++ b/binaries/data/mods/public/simulation/ai/petra/tradeManager.js @@ -347,9 +347,7 @@ m.TradeManager.prototype.checkEvents = function(gameState, events) let ent = evt.entityObj; if (!ent || !ent.hasClass("Market") || !gameState.isPlayerAlly(ent.owner())) continue; - this.routeProspection = true; - gameState.ai.HQ.restartBuild(gameState, "structures/{civ}_market"); - gameState.ai.HQ.restartBuild(gameState, "structures/{civ}_dock"); + this.activateProspection(gameState); return true; } @@ -359,9 +357,7 @@ m.TradeManager.prototype.checkEvents = function(gameState, events) let ent = gameState.getEntityById(evt.entity); if (!ent || ent.foundationProgress() !== undefined || !ent.hasClass("Market") || !gameState.isPlayerAlly(ent.owner())) continue; - this.routeProspection = true; - gameState.ai.HQ.restartBuild(gameState, "structures/{civ}_market"); - gameState.ai.HQ.restartBuild(gameState, "structures/{civ}_dock"); + this.activateProspection(gameState); return true; } @@ -374,24 +370,27 @@ m.TradeManager.prototype.checkEvents = function(gameState, events) let ent = gameState.getEntityById(evt.entity); if (!ent || ent.foundationProgress() !== undefined || !ent.hasClass("Market")) continue; - this.routeProspection = true; - gameState.ai.HQ.restartBuild(gameState, "structures/{civ}_market"); - gameState.ai.HQ.restartBuild(gameState, "structures/{civ}_dock"); + this.activateProspection(gameState); return true; } // or if diplomacy changed if (events.DiplomacyChanged.length) { - this.routeProspection = true; - gameState.ai.HQ.restartBuild(gameState, "structures/{civ}_market"); - gameState.ai.HQ.restartBuild(gameState, "structures/{civ}_dock"); + this.activateProspection(gameState); return true; } return false; }; +m.TradeManager.prototype.activateProspection = function(gameState) +{ + this.routeProspection = true; + gameState.ai.HQ.buildManager.setBuildable(gameState.applyCiv("structures/{civ}_market")); + gameState.ai.HQ.buildManager.setBuildable(gameState.applyCiv("structures/{civ}_dock")); +}; + /** * fills the best trade route in this.tradeRoute and the best potential route in this.potentialTradeRoute * If an index is given, it returns the best route with this index or the best land route if index is a land index @@ -579,7 +578,7 @@ m.TradeManager.prototype.prospectForNewMarket = function(gameState, queues) let marketPos = gameState.ai.HQ.findMarketLocation(gameState, template); if (!marketPos || marketPos[3] === 0) // marketPos[3] is the expected gain { // no position found - gameState.ai.HQ.stopBuild(gameState, "structures/{civ}_market"); + gameState.ai.HQ.buildManager.setUnbuildable(gameState, gameState.applyCiv("structures/{civ}_market")); return; } this.routeProspection = false;