petra: revisit the management of constructions for better performances and cleaner implementation

This was SVN commit r20409.
This commit is contained in:
mimo
2017-11-05 13:56:16 +00:00
parent 4562bc998c
commit 86b9159609
7 changed files with 224 additions and 102 deletions
@@ -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+/);
},
@@ -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;
@@ -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);
@@ -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);
@@ -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;
}
@@ -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);
@@ -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;