var AEGIS = function(m) { /* Headquarters * Deal with high level logic for the AI. Most of the interesting stuff gets done here. * Some tasks: -defining RESS needs -BO decisions. > training workers > building stuff (though we'll send that to bases) > researching -picking strategy (specific manager?) -diplomacy (specific manager?) -planning attacks -picking new CC locations. */ m.HQ = function(Config) { this.Config = Config; this.targetNumBuilders = this.Config.Economy.targetNumBuilders; // number of workers we want building stuff this.dockStartTime = this.Config.Economy.dockStartTime * 1000; this.techStartTime = this.Config.Economy.techStartTime * 1000; this.dockFailed = false; // sanity check this.waterMap = false; // set by the aegis.js file. this.econState = "growth"; // existing values: growth, townPhasing. // tell if we can't gather from a resource type for sanity checks. this.outOf = { "food" : false, "wood" : false, "stone" : false, "metal" : false }; this.baseManagers = {}; // cache the rates. this.wantedRates = { "food": 0, "wood": 0, "stone":0, "metal": 0 }; this.currentRates = { "food": 0, "wood": 0, "stone":0, "metal": 0 }; this.currentRateLastUpdateTime = 0; // this means we'll have about a big third of women, and thus we can maximize resource gathering rates. this.femaleRatio = this.Config.Economy.femaleRatio; this.fortressStartTime = 0; this.fortressLapseTime = this.Config.Military.fortressLapseTime * 1000; this.defenceBuildingTime = this.Config.Military.defenceBuildingTime * 1000; this.attackPlansStartTime = this.Config.Military.attackPlansStartTime * 1000; this.defenceManager = new m.Defence(this.Config); this.navalManager = new m.NavalManager(); this.TotalAttackNumber = 0; this.upcomingAttacks = { "CityAttack" : [], "Rush" : [] }; this.startedAttacks = { "CityAttack" : [], "Rush" : [] }; }; // More initialisation for stuff that needs the gameState m.HQ.prototype.init = function(gameState, queues){ // initialize base map. Each pixel is a base ID, or 0 if none this.basesMap = new API3.Map(gameState.sharedScript, new Uint8Array(gameState.getMap().data.length)); this.basesMap.setMaxVal(255); if (this.Config.Economy.targetNumWorkers) this.targetNumWorkers = this.Config.Economy.targetNumWorkers; else if (this.targetNumWorkers === undefined) this.targetNumWorkers = Math.max(Math.floor(gameState.getPopulationMax()*(0.2 + Math.min(+(this.Config.difficulty)*0.125,0.3))), 1); // Let's get our initial situation here. // TODO: improve on this. // TODO: aknowledge bases, assign workers already. var ents = gameState.getEntities().filter(API3.Filters.byOwner(PlayerID)); var workersNB = 0; var hasScout = false; var treasureAmount = { 'food': 0, 'wood': 0, 'stone': 0, 'metal': 0 }; var hasCC = false; if (ents.filter(API3.Filters.byClass("CivCentre")).length > 0) hasCC = true; workersNB = ents.filter(API3.Filters.byClass("Worker")).length; if (ents.filter(API3.Filters.byClass("Cavalry")).length > 0) hasScout = true; // TODO: take multiple CCs into account. if (hasCC) { var CC = ents.filter(API3.Filters.byClass("CivCentre")).toEntityArray()[0]; for (var i in treasureAmount) gameState.getResourceSupplies(i).forEach( function (ent) { if (ent.resourceSupplyType().generic === "treasure" && API3.SquareVectorDistance(ent.position(), CC.position()) < 5000) treasureAmount[i] += ent.resourceSupplyMax(); }); this.baseManagers[1] = new m.BaseManager(this.Config); this.baseManagers[1].init(gameState); this.baseManagers[1].setAnchor(CC); this.baseManagers[1].initTerritory(this, gameState); this.baseManagers[1].initGatheringFunctions(this, gameState); if (m.DebugEnabled()) this.basesMap.dumpIm("basesMap.png"); var self = this; ents.forEach( function (ent) { //}){ self.baseManagers[1].assignEntity(ent); }); } // we now have enough data to decide on a few things. // TODO: here would be where we pick our initial strategy. // immediatly build a wood dropsite if possible. if (this.baseManagers[1]) { if (gameState.ai.queueManager.getAvailableResources(gameState)["wood"] >= 250) { var pos = this.baseManagers[1].findBestDropsiteLocation(gameState, "wood"); if (pos) { queues.dropsites.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_storehouse",{ "base" : 1 }, pos)); queues.minorTech.addItem(new m.ResearchPlan(gameState, "gather_capacity_wheelbarrow")); } } } var map = new API3.Map(gameState.sharedScript, gameState.sharedScript.CCResourceMaps["wood"].map); if (m.DebugEnabled()) map.dumpIm("map_CC_Wood.png"); //this.reassignIdleWorkers(gameState); this.navalManager.init(gameState, queues); this.defenceManager.init(gameState); // 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.moderate){ this.bModerate = this.Config.buildings.moderate[civ]; }else{ this.bModerate = this.Config.buildings.moderate['default']; } if (civ in this.Config.buildings.advanced){ this.bAdvanced = this.Config.buildings.advanced[civ]; }else{ this.bAdvanced = this.Config.buildings.advanced['default']; } if (civ in this.Config.buildings.fort){ this.bFort = this.Config.buildings.fort[civ]; }else{ this.bFort = this.Config.buildings.fort['default']; } for (var i in this.bAdvanced){ this.bAdvanced[i] = gameState.applyCiv(this.bAdvanced[i]); } for (var i in this.bFort){ this.bFort[i] = gameState.applyCiv(this.bFort[i]); } }; 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) { 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 === 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, events, true); this.baseManagers[bID].setAnchor(ent); this.baseManagers[bID].initTerritory(this, gameState); // Let's get a few units out there to build this. // TODO: select the best base, or use multiple bases. var builders = this.bulkPickWorkers(gameState, bID, 10); if (builders !== false) { builders.forEach(function (worker) { worker.setMetadata(PlayerID, "base", bID); worker.setMetadata(PlayerID, "subrole", "builder"); worker.setMetadata(PlayerID, "target-foundation", ent.id()); }); } } } } for (var i in ConstructionEvents) { var 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 === undefined) continue; // happens when this message is right before a "Destroy" one for the same entity. if (ent.isOwn(PlayerID) && ent.getMetadata(PlayerID, "baseAnchor") == true) { var base = ent.getMetadata(PlayerID, "base"); if (this.baseManagers[base].constructing) { this.baseManagers[base].constructing = false; this.baseManagers[base].initGatheringFunctions(this, gameState); } } } } }; // Called by the "town phase" research plan once it's started m.HQ.prototype.OnTownPhase = function(gameState) { if (this.Config.difficulty >= 2 && this.femaleRatio !== 0.4) { this.femaleRatio = 0.4; gameState.ai.queues["villager"].empty(); gameState.ai.queues["citizenSoldier"].empty(); } } // This code trains females and citizen workers, trying to keep close to a ratio of females/CS // TODO: this should choose a base depending on which base need workers // 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); // counting the workers that aren't part of a plan var numWorkers = 0; gameState.getOwnUnits().forEach (function (ent) { if (ent.getMetadata(PlayerID, "role") == "worker" && ent.getMetadata(PlayerID, "plan") == undefined) numWorkers++; }); var numInTraining = 0; gameState.getOwnTrainingFacilities().forEach(function(ent) { ent.trainingQueue().forEach(function(item) { if (item.metadata && item.metadata.role && item.metadata.role == "worker" && item.metadata.plan == undefined) numWorkers += item.count; numInTraining += item.count; }); }); var numQueuedF = queues.villager.countQueuedUnits(); var numQueuedS = queues.citizenSoldier.countQueuedUnits(); var numQueued = numQueuedS + numQueuedF; var numTotal = numWorkers + numQueued; // If we have too few, train more // should plan enough to always have females… // TODO: 15 here should be changed to something more sensible, such as nb of producing buildings. if (numTotal > this.targetNumWorkers || numQueued > 50 || (numQueuedF > 20 && numQueuedS > 20) || numInTraining > 15) return; if (numTotal >= this.Config.Economy.villagePopCap && gameState.currentPhase() === 1 && !gameState.isResearching(gameState.townPhase())) return; // default template and size var template = gameState.applyCiv("units/{civ}_support_female_citizen"); var size = Math.min(5, Math.ceil(numTotal / 10)); // Choose whether we want soldiers instead. // TODO: we might want to adjust our female ratio. if ((numFemales+numQueuedF)/numTotal > this.femaleRatio && numQueuedS < 20) { if (numTotal < 35) template = this.findBestTrainableUnit(gameState, ["CitizenSoldier", "Infantry"], [ ["cost",1], ["speed",0.5], ["costsResource", 0.5, "stone"], ["costsResource", 0.5, "metal"]]); else template = this.findBestTrainableUnit(gameState, ["CitizenSoldier", "Infantry"], [ ["strength",1] ]); if (!template) template = gameState.applyCiv("units/{civ}_support_female_citizen"); else size = Math.min(5, Math.ceil(numTotal / 12)); } // TODO: improve that logic. /* if (numFemales/numWorkers > this.femaleRatio && numQueuedS > 0 && numWorkers > 25) queues.villager.paused = true; else queues.villager.paused = false; */ // TODO: perhaps assign them a default resource and check the base according to that. // base "0" means "auto" if (template === gameState.applyCiv("units/{civ}_support_female_citizen")) queues.villager.addItem(new m.TrainingPlan(gameState, template, { "role" : "worker", "base" : 0 }, size, size )); else queues.citizenSoldier.addItem(new m.TrainingPlan(gameState, template, { "role" : "worker", "base" : 0 }, size, size)); }; // picks the best template based on parameters and classes m.HQ.prototype.findBestTrainableUnit = function(gameState, classes, parameters) { var units = gameState.findTrainableUnits(classes); if (units.length === 0) return undefined; units.sort(function(a, b) {// }) { var aDivParam = 0, bDivParam = 0; var aTopParam = 0, bTopParam = 0; for (var i in parameters) { var param = parameters[i]; if (param[0] == "base") { aTopParam = param[1]; bTopParam = param[1]; } if (param[0] == "strength") { aTopParam += m.getMaxStrength(a[1]) * param[1]; bTopParam += m.getMaxStrength(b[1]) * param[1]; } if (param[0] == "siegeStrength") { aTopParam += m.getMaxStrength(a[1], "Structure") * param[1]; bTopParam += m.getMaxStrength(b[1], "Structure") * param[1]; } if (param[0] == "speed") { aTopParam += a[1].walkSpeed() * param[1]; bTopParam += b[1].walkSpeed() * param[1]; } if (param[0] == "cost") { aDivParam += a[1].costSum() * param[1]; bDivParam += b[1].costSum() * param[1]; } // requires a third parameter which is the resource if (param[0] == "costsResource") { if (a[1].cost()[param[2]]) aTopParam *= param[1]; if (b[1].cost()[param[2]]) bTopParam *= param[1]; } if (param[0] == "canGather") { // checking against wood, could be anything else really. if (a[1].resourceGatherRates() && a[1].resourceGatherRates()["wood.tree"]) aTopParam *= param[1]; if (b[1].resourceGatherRates() && b[1].resourceGatherRates()["wood.tree"]) bTopParam *= param[1]; } } return -(aTopParam/(aDivParam+1)) + (bTopParam/(bDivParam+1)); }); return units[0][0]; }; // Tries to research any available tech // Only one at once. Also does military tech (selection is completely random atm) // TODO: Lots, lots, lots here. m.HQ.prototype.tryResearchTechs = function(gameState, queues) { if (queues.minorTech.length() === 0) { var possibilities = gameState.findAvailableTech(); if (possibilities.length === 0) return; // randomly pick one. No worries about pairs in that case. var p = Math.floor((Math.random()*possibilities.length)); queues.minorTech.addItem(new m.ResearchPlan(gameState, possibilities[p][0])); } } // We're given a worker and a resource type // We'll assign the worker for the best base for that resource type. // TODO: improve choice alogrithm m.HQ.prototype.switchWorkerBase = function(gameState, worker, type) { var bestBase = 0; for (var i in this.baseManagers) { if (this.baseManagers[i].willGather[type] >= 1) { if (this.baseManagers[i].accessIndex === this.baseManagers[worker.getMetadata(PlayerID,"base")].accessIndex || this.navalManager.canReach(gameState, this.baseManagers[i].accessIndex, this.baseManagers[worker.getMetadata(PlayerID,"base")].accessIndex)) { bestBase = i; break; } } } if (bestBase && bestBase !== worker.getMetadata(PlayerID,"base")) { worker.setMetadata(PlayerID,"base",bestBase); return true; } else { return false; } }; // returns an entity collection of workers through BaseManager.pickBuilders // TODO: better the choice algo. // TODO: also can't get over multiple bases right now. m.HQ.prototype.bulkPickWorkers = function(gameState, newBaseID, number) { var accessIndex = this.baseManagers[newBaseID].accessIndex; if (!accessIndex) return false; // sorting bases by whether they are on the same accessindex or not. var baseBest = m.AssocArraytoArray(this.baseManagers).sort(function (a,b) { if (a.accessIndex === accessIndex && b.accessIndex !== accessIndex) return -1; else if (b.accessIndex === accessIndex && a.accessIndex !== accessIndex) return 1; return 0; }); for (var i in baseBest) { if (baseBest[i].workers.length > number) { return baseBest[i].pickBuilders(gameState,number); } } return false; } // returns the current gather rate // This is not per-se exact, it performs a few adjustments ad-hoc to account for travel distance, stuffs like that. m.HQ.prototype.GetCurrentGatherRates = function(gameState) { var self = this; if (gameState.getTimeElapsed() - this.currentRateLastUpdateTime < 10000 && this.currentRateLastUpdateTime !== 0 && gameState.ai.playedTurn > 3) return this.currentRates; this.currentRateLastUpdateTime = gameState.getTimeElapsed(); for (var type in this.wantedRates) this.currentRates[type] = 0; for (var i in this.baseManagers) this.baseManagers[i].getGatherRates(gameState, this.currentRates); return this.currentRates; }; /* Pick the resource which most needs another worker * How this works: * We get the rates we would want to have to be able to deal with our plans * We get our current rates * We compare; we pick the one where the discrepancy is highest. * Need to balance long-term needs and possible short-term needs. */ m.HQ.prototype.pickMostNeededResources = function(gameState) { var self = this; this.wantedRates = gameState.ai.queueManager.wantedGatherRates(gameState); var currentRates = {}; for (var type in this.wantedRates) currentRates[type] = 0; for (var i in this.baseManagers) { var base = this.baseManagers[i]; for (var type in this.wantedRates) { if (gameState.turnCache["gathererAssignementCache-" + type]) currentRates[type] += gameState.turnCache["gathererAssignementCache-" + type]; base.gatherersByType(gameState,type).forEach (function (ent) { //}){ var worker = ent.getMetadata(PlayerID, "worker-object"); if (worker) currentRates[type] += worker.getGatherRate(gameState); }); } } // let's get our ideal number. var types = Object.keys(this.wantedRates); types.sort(function(a, b) { var va = (Math.max(0,self.wantedRates[a] - currentRates[a]))/ (currentRates[a]+1); var vb = (Math.max(0,self.wantedRates[b] - currentRates[b]))/ (currentRates[b]+1); // If they happen to be equal (generally this means "0" aka no need), make it fair. if (va === vb) return (self.wantedRates[b]/(currentRates[b]+1)) - (self.wantedRates[a]/(currentRates[a]+1)); return vb-va; }); return types; }; // If all the CC's are destroyed then build a new one // TODO: rehabilitate. m.HQ.prototype.buildNewCC= function(gameState, queues) { var numCCs = gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_civil_centre"), true); numCCs += queues.civilCentre.length(); // no use trying to lay foundations that will be destroyed if (gameState.defcon() > 2) for (var i = numCCs; i < 1; i++) { gameState.ai.queueManager.clear(); this.baseNeed["food"] = 0; this.baseNeed["wood"] = 50; this.baseNeed["stone"] = 50; this.baseNeed["metal"] = 50; queues.civilCentre.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_civil_centre")); } return (gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_civil_centre"), true) == 0 && gameState.currentPhase() > 1); }; // Returns the best position to build a new Civil Centre // Whose primary function would be to reach new resources of type "resource". m.HQ.prototype.findBestEcoCCLocation = function(gameState, resource){ var CCPlate = gameState.getTemplate("structures/{civ}_civil_centre"); // This builds a map. The procedure is fairly simple. It adds the resource maps // (which are dynamically updated and are made so that they will facilitate DP placement) // Then checks for a good spot in the territory. If none, and town/city phase, checks outside // The AI will currently not build a CC if it wouldn't connect with an existing CC. var territory = m.createTerritoryMap(gameState); var obstructions = m.createObstructionMap(gameState, 0); obstructions.expandInfluences(); // copy the resource map as initialization. var friendlyTiles = new API3.Map(gameState.sharedScript, gameState.sharedScript.CCResourceMaps[resource].map, true); friendlyTiles.setMaxVal(255); var ents = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).toEntityArray(); var eEnts = gameState.getEnemyStructures().filter(API3.Filters.byClass("CivCentre")).toEntityArray(); var dps = gameState.getOwnDropsites().toEntityArray(); for (var j = 0; j < friendlyTiles.length; ++j) { // We check for our other CCs: the distance must not be too big. Anything bigger will result in scrapping. // This ensures territorial continuity. // TODO: maybe whenever I get around to implement multi-base support (details below, requires being part of the team. If you're not, ask wraitii directly by PM). // (see www.wildfiregames.com/forum/index.php?showtopic=16702&#entry255631 ) // TODO: figure out what I was trying to say above. var canBuild = true; var canBuild2 = false; var pos = [j%friendlyTiles.width+0.5, Math.floor(j/friendlyTiles.width)+0.5]; for (var i in ents) { var entPos = ents[i].position(); entPos = [entPos[0]/4.0,entPos[1]/4.0]; var dist = API3.SquareVectorDistance(entPos, pos); if (dist < 3500 || dist > 7900) friendlyTiles.map[j] /= 2.0; if (dist < 2120) { canBuild = false; continue; } else if (dist < 9200 || this.waterMap) canBuild2 = true; } // checking for bases. if (this.basesMap.map[j] !== 0) canBuild = false; if (!canBuild2) canBuild = false; if (canBuild) { // Checking for enemy CCs for (var i in eEnts) { var entPos = eEnts[i].position(); entPos = [entPos[0]/4.0,entPos[1]/4.0]; // 7100 works well as a limit. if (API3.SquareVectorDistance(entPos, pos) < 2500) { canBuild = false; continue; } } } if (!canBuild) { friendlyTiles.map[j] = 0; continue; } for (var i in dps) { var dpPos = dps[i].position(); if (dpPos === undefined) { // Probably a mauryan elephant, skip continue; } dpPos = [dpPos[0]/4.0,dpPos[1]/4.0]; var dist = API3.SquareVectorDistance(dpPos, pos); if (dist < 600) { friendlyTiles.map[j] = 0; continue; } else if (dist < 1500) friendlyTiles.map[j] /= 2.0; } friendlyTiles.map[j] *= 1.5; for (var i in gameState.sharedScript.CCResourceMaps) if (friendlyTiles.map[j] !== 0 && i !== "food") { var val = friendlyTiles.map[j] + gameState.sharedScript.CCResourceMaps[i].map[j]; if (val < 255) friendlyTiles.map[j] = val; else friendlyTiles.map[j] = 255; } } var best = friendlyTiles.findBestTile(6, obstructions); var bestIdx = best[0]; if (m.DebugEnabled()) { friendlyTiles.map[bestIdx] = 270; friendlyTiles.dumpIm("cc_placement_base_" + gameState.getTimeElapsed() + "_" + resource + "_" + best[1] + ".png",301); //obstructions.dumpIm("cc_placement_base_" + gameState.getTimeElapsed() + "_" + resource + "_" + best[1] + "_obs.png", 20); } // not good enough. if (best[1] < 60) return false; var x = ((bestIdx % friendlyTiles.width) + 0.5) * gameState.cellSize; var z = (Math.floor(bestIdx / friendlyTiles.width) + 0.5) * gameState.cellSize; m.debug ("Best for value " + best[1] + " at " + uneval([x,z])); return [x,z]; }; m.HQ.prototype.buildTemple = function(gameState, queues){ if (gameState.currentPhase() >= 2 ) { if (queues.economicBuilding.countQueuedUnits() === 0 && gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_temple"), true) === 0){ queues.economicBuilding.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_temple", { "base" : 1 })); } } }; m.HQ.prototype.buildMarket = function(gameState, queues){ if (gameState.getPopulation() > this.Config.Economy.popForMarket && gameState.currentPhase() >= 2 ) { if (queues.economicBuilding.countQueuedUnitsWithClass("BarterMarket") === 0 && gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_market"), true) === 0){ //only ever build one storehouse/CC/market at a time queues.economicBuilding.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_market", { "base" : 1 })); } } }; // Build a farmstead to go to town phase faster and prepare for research. Only really active on higher diff mode. m.HQ.prototype.buildFarmstead = function(gameState, queues){ if (gameState.getPopulation() > this.Config.Economy.popForFarmstead && (gameState.currentPhase() > 1 || gameState.isResearching(gameState.townPhase()))) { // achtung: "DropsiteFood" does not refer to CCs. if (queues.economicBuilding.countQueuedUnitsWithClass("DropsiteFood") === 0 && gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_farmstead"), true) === 0){ //only ever build one storehouse/CC/market at a time queues.economicBuilding.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_farmstead", { "base" : 1 })); // add the farming plough to the research we want. queues.minorTech.addItem(new m.ResearchPlan(gameState, "gather_farming_plough")); } } }; // TODO: generic this, probably per-base m.HQ.prototype.buildDock = function(gameState, queues){ if (!this.waterMap || this.dockFailed) return; if (gameState.getTimeElapsed() > this.dockStartTime) { if (queues.economicBuilding.countQueuedUnitsWithClass("NavalMarket") === 0 && gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_dock"), true) === 0) { var tp = "" if (gameState.civ() == "cart" && gameState.currentPhase() > 1) tp = "structures/{civ}_super_dock"; else if (gameState.civ() !== "cart") tp = "structures/{civ}_dock"; if (tp !== "") { var remaining = this.navalManager.getUnconnectedSeas(gameState, this.baseManagers[1].accessIndex); queues.economicBuilding.addItem(new m.ConstructionPlan(gameState, tp, { "base" : 1, "sea" : remaining[0] })); } } } }; // Try to barter unneeded resources for needed resources. // once per turn because the info doesn't update between a turn and fixing isn't worth it. m.HQ.prototype.tryBartering = function(gameState) { var markets = gameState.getOwnEntitiesByType(gameState.applyCiv("structures/{civ}_market"), true).toEntityArray(); if (markets.length === 0) return false; // Available resources after account substraction var available = gameState.ai.queueManager.getAvailableResources(gameState); var rates = this.GetCurrentGatherRates(gameState) var prices = gameState.getBarterPrices(); // calculates conversion rates var getBarterRate = function (prices,buy,sell) { return Math.round(100 * prices["sell"][sell] / prices["buy"][buy]); }; // loop through each queues checking if we could barter and finish a queue quickly. for (var j in gameState.ai.queues) { var queue = gameState.ai.queues[j]; if (queue.paused || queue.length() === 0) continue; var account = gameState.ai.queueManager.accounts[j]; var elem = queue.queue[0]; var elemCost = elem.getCost(); for each (var ress in elemCost.types) { if (available[ress] >= 0) continue; // don't care if we still have available resources or our rate is good enough var need = elemCost[ress] - account[ress]; if (need <= 0 || rates[ress] >= need/50) // don't care if we don't need resources for our first item continue; if (ress == "food" && need < 400) continue; // pick the best resource to barter. var bestToBarter = ""; var bestRate = 0; for each (var otherRess in elemCost.types) { if (ress === otherRess) continue; // I wanna keep some if (available[otherRess] < 130 + need) return false; var barterRate = getBarterRate(prices, ress, otherRess); if (barterRate > bestRate) { bestRate = barterRate; bestToBarter = otherRess; } } if (bestToBarter !== "") { markets[0].barter(buy,sell,100); m.debug ("Snipe bartered " + sell +" for " + buy + ", value 100"); return true; } } } // now barter for big needs. var needs = gameState.ai.queueManager.currentNeeds(gameState); for each (var sell in needs.types) { for each (var buy in needs.types) { if (buy != sell && needs[sell] <= 0 && available[sell] > 500) { // if we don't need it and have a buffer if (needs[buy] > rates[buy]*80) { // if we need that other resource terribly. markets[0].barter(buy,sell,100); m.debug ("Gross bartered " +sell +" for " + buy + ", value 100"); return true; } } } } return false; }; // build more houses if needed. // kinda ugly, lots of special cases to both build enough houses but not tooo many… m.HQ.prototype.buildMoreHouses = function(gameState,queues) { if (gameState.getPopulationLimit() < gameState.getPopulationMax()) { var numPlanned = queues.house.length(); if (numPlanned < 3 || (numPlanned < 5 && gameState.getPopulation() > 80)) { var plan = new m.ConstructionPlan(gameState, "structures/{civ}_house", { "base" : 1 }); // make the difficulty available to the isGo function without having to pass it as argument var difficulty = this.Config.difficulty; // change the starting condition to "less than 15 slots left". plan.isGo = function (gameState) { var HouseNb = gameState.countEntitiesByType(gameState.applyCiv("foundation|structures/{civ}_house"), true); var freeSlots = 0; if (gameState.civ() == "gaul" || gameState.civ() == "brit" || gameState.civ() == "iber") freeSlots = gameState.getPopulationLimit() + HouseNb*5 - gameState.getPopulation(); else freeSlots = gameState.getPopulationLimit() + HouseNb*10 - gameState.getPopulation(); if (gameState.getPopulation() > 55 && difficulty > 1) return (freeSlots <= 21); else if (gameState.getPopulation() >= 30 && difficulty !== 0) return (freeSlots <= 15); else return (freeSlots <= 10); } queues.house.addItem(plan); } if (numPlanned > 0 && this.econState == "townPhasing") { var houseQueue = queues.house.queue; var count = gameState.getOwnStructures().filter(API3.Filters.byClass("Village")).length; count += queues.militaryBuilding.length(); // barracks for (var i = 0; i < numPlanned; ++i) { if (houseQueue[i].isGo(gameState)) ++count; else if (count < 5) { houseQueue[i].isGo = function () { return true; } ++count; } } } } }; // checks if we have bases for all resource types (bar food for now) or if we need to expand. m.HQ.prototype.checkBasesRessLevel = function(gameState,queues) { if (gameState.currentPhase() === 1 && !gameState.isResearching(gameState.townPhase())) return; var count = { "wood" : 0, "stone" : 0, "metal" : 0 } var capacity = { "wood" : 0, "stone" : 0, "metal" : 0 } var need = { "wood" : true, "stone" : true, "metal" : true }; var posss = []; for (var i in this.baseManagers) { var base = this.baseManagers[i]; for (var type in count) { if (base.getResourceLevel(gameState, type, "all") > 1500*Math.max(this.Config.difficulty,2)) count[type]++; capacity[type] += base.getWorkerCapacity(gameState, type); if (base.willGather[type] !== 2) need[type] = false; } } for (var type in count) { if (count[type] === 0 || need[type] || capacity[type] < gameState.getOwnUnits().filter(API3.Filters.and(API3.Filters.byMetadata(PlayerID, "subrole", "gatherer"), API3.Filters.byMetadata(PlayerID, "gather-type", type))).length * 1.05) { // plan a new base. if (gameState.countFoundationsByType(gameState.applyCiv("structures/{civ}_civil_centre"), true) === 0 && queues.civilCentre.length() === 0) { if (this.outOf[type] && gameState.ai.playedTurn % 10 !== 0) continue; var pos = this.findBestEcoCCLocation(gameState, type); if (!pos) { // Okay so we'll set us as out of this. this.outOf[type] = true; } else { // base "-1" means new base. queues.civilCentre.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_civil_centre",{ "base" : -1 }, pos)); } } } } }; // Deals with building fortresses and towers. // Currently build towers next to every useful dropsites. // TODO: Fortresses are placed randomly atm. m.HQ.prototype.buildDefences = function(gameState, queues){ var workersNumber = gameState.getOwnEntitiesByRole("worker", true).filter(API3.Filters.not(API3.Filters.byHasMetadata(PlayerID,"plan"))).length; if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv('structures/{civ}_defense_tower'), true) + queues.defenceBuilding.length() < gameState.getEntityLimits()["DefenseTower"] && queues.defenceBuilding.length() === 0 && gameState.currentPhase() > 1) { for (var i in this.baseManagers) { for (var j in this.baseManagers[i].dropsites) { var amnts = this.baseManagers[i].dropsites[j]; var dpEnt = gameState.getEntityById(j); if (dpEnt !== undefined && dpEnt.getMetadata(PlayerID, "defenseTower") !== true) if (amnts["wood"] || amnts["metal"] || amnts["stone"]) { var position = dpEnt.position(); if (position) { queues.defenceBuilding.addItem(new m.ConstructionPlan(gameState, 'structures/{civ}_defense_tower', { "base" : i }, position)); } dpEnt.setMetadata(PlayerID, "defenseTower", true); } } } } var numFortresses = 0; for (var i in this.bFort){ numFortresses += gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bFort[i]), true); } if (queues.defenceBuilding.length() < 1 && (gameState.currentPhase() > 2 || gameState.isResearching("phase_city_generic"))) { if (workersNumber >= 80 && gameState.getTimeElapsed() > numFortresses * this.fortressLapseTime + this.fortressStartTime) { if (!this.fortressStartTime) this.fortressStartTime = gameState.getTimeElapsed(); queues.defenceBuilding.addItem(new m.ConstructionPlan(gameState, this.bFort[0], { "base" : 1 })); m.debug ("Building a fortress"); } } if (gameState.countEntitiesByType(gameState.applyCiv(this.bFort[i]), true) >= 1) { // let's add a siege building plan to the current attack plan if there is none currently. if (this.upcomingAttacks["CityAttack"].length !== 0) { var attack = this.upcomingAttacks["CityAttack"][0]; if (!attack.unitStat["Siege"]) { // no minsize as we don't want the plan to fail at the last minute though. var stat = { "priority" : 1.1, "minSize" : 0, "targetSize" : 4, "batchSize" : 2, "classes" : ["Siege"], "interests" : [ ["siegeStrength", 3], ["cost",1] ] ,"templates" : [] }; if (gameState.civ() == "cart" || gameState.civ() == "maur") stat["classes"] = ["Elephant"]; attack.addBuildOrder(gameState, "Siege", stat, true); } } } }; m.HQ.prototype.buildBlacksmith = function(gameState, queues){ if (gameState.getTimeElapsed() > this.Config.Military.timeForBlacksmith*1000) { if (queues.militaryBuilding.length() === 0 && gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_blacksmith"), true) === 0) { var tp = gameState.getTemplate(gameState.applyCiv("structures/{civ}_blacksmith")); if (tp.available(gameState)) queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_blacksmith", { "base" : 1 })); } } }; // Deals with constructing military buildings (barracks, stables…) // They are mostly defined by Config.js. This is unreliable since changes could be done easily. // TODO: We need to determine these dynamically. Also doesn't build fortresses since the above function does that. // TODO: building placement is bad. Choice of buildings is also fairly dumb. m.HQ.prototype.constructTrainingBuildings = function(gameState, queues) { Engine.ProfileStart("Build buildings"); var workersNumber = gameState.getOwnEntitiesByRole("worker", true).filter(API3.Filters.not(API3.Filters.byHasMetadata(PlayerID, "plan"))).length; var barrackNb = gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bModerate[0]), true); // first barracks. if (workersNumber > this.Config.Military.popForBarracks1 || (this.econState == "townPhasing" && gameState.getOwnStructures().filter(API3.Filters.byClass("Village")).length < 5)) { if (barrackNb + queues.militaryBuilding.length() < 1) { m.debug ("Trying to build barracks"); var plan = new m.ConstructionPlan(gameState, this.bModerate[0], { "base" : 1 }); plan.onStart = function(gameState) { gameState.ai.queueManager.changePriority("militaryBuilding", 130); }; queues.militaryBuilding.addItem(plan); } } // second barracks. if (barrackNb < 2 && workersNumber > this.Config.Military.popForBarracks2) if (queues.militaryBuilding.length() < 1) queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, this.bModerate[0], { "base" : 1 })); // third barracks (optional 4th/5th for some civs as they rely on barracks more.) if (barrackNb === 2 && barrackNb + queues.militaryBuilding.length() < 3 && workersNumber > 125) if (queues.militaryBuilding.length() === 0) { queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, this.bModerate[0], { "base" : 1 })); if (gameState.civ() == "gaul" || gameState.civ() == "brit" || gameState.civ() == "iber") { queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, this.bModerate[0], { "base" : 1 })); queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, this.bModerate[0], { "base" : 1 })); } } //build advanced military buildings if (workersNumber >= this.Config.Military.popForBarracks2 - 15 && gameState.currentPhase() > 2){ if (queues.militaryBuilding.length() === 0){ var inConst = 0; for (var i in this.bAdvanced) inConst += gameState.countFoundationsByType(gameState.applyCiv(this.bAdvanced[i])); if (inConst == 0 && this.bAdvanced && this.bAdvanced.length !== 0) { var i = Math.floor(Math.random() * this.bAdvanced.length); if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bAdvanced[i]), true) < 1){ queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, this.bAdvanced[i], { "base" : 1 })); } } } } // build second advanced building except for some civs. if (gameState.civ() !== "gaul" && gameState.civ() !== "brit" && gameState.civ() !== "iber" && workersNumber > 130 && gameState.currentPhase() > 2) { var Const = 0; for (var i in this.bAdvanced) Const += gameState.countEntitiesByType(gameState.applyCiv(this.bAdvanced[i]), true); if (inConst == 1) { var i = Math.floor(Math.random() * this.bAdvanced.length); if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bAdvanced[i]), true) < 1){ queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, this.bAdvanced[i], { "base" : 1 })); queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, this.bAdvanced[i], { "base" : 1 })); } } } Engine.ProfileStop(); }; // TODO: use pop(). Currently unused as this is too gameable. m.HQ.prototype.garrisonAllFemales = function(gameState) { var buildings = gameState.getOwnStructures().filter(API3.Filters.byCanGarrison()).toEntityArray(); var females = gameState.getOwnUnits().filter(API3.Filters.byClass("Support")); var cache = {}; females.forEach( function (ent) { for (var i in buildings) { if (ent.position()) { var struct = buildings[i]; if (!cache[struct.id()]) cache[struct.id()] = 0; if (struct.garrisoned() && struct.garrisonMax() - struct.garrisoned().length - cache[struct.id()] > 0) { ent.garrison(struct); cache[struct.id()]++; break; } } } }); this.hasGarrisonedFemales = true; }; m.HQ.prototype.ungarrisonAll = function(gameState) { this.hasGarrisonedFemales = false; var buildings = gameState.getOwnStructures().filter(API3.Filters.and(API3.Filters.byClass("Structure"),API3.Filters.byCanGarrison())).toEntityArray(); buildings.forEach( function (struct) { if (struct.garrisoned() && struct.garrisoned().length) struct.unloadAll(); }); }; m.HQ.prototype.pausePlan = function(gameState, planName) { for (var attackType in this.upcomingAttacks) { for (var i in this.upcomingAttacks[attackType]) { var attack = this.upcomingAttacks[attackType][i]; if (attack.getName() == planName) attack.setPaused(gameState, true); } } for (var attackType in this.startedAttacks) { for (var i in this.startedAttacks[attackType]) { var attack = this.startedAttacks[attackType][i]; if (attack.getName() == planName) attack.setPaused(gameState, true); } } } m.HQ.prototype.unpausePlan = function(gameState, planName) { for (var attackType in this.upcomingAttacks) { for (var i in this.upcomingAttacks[attackType]) { var attack = this.upcomingAttacks[attackType][i]; if (attack.getName() == planName) attack.setPaused(gameState, false); } } for (var attackType in this.startedAttacks) { for (var i in this.startedAttacks[attackType]) { var attack = this.startedAttacks[attackType][i]; if (attack.getName() == planName) attack.setPaused(gameState, false); } } } m.HQ.prototype.pauseAllPlans = function(gameState) { for (var attackType in this.upcomingAttacks) { for (var i in this.upcomingAttacks[attackType]) { var attack = this.upcomingAttacks[attackType][i]; attack.setPaused(gameState, true); } } for (var attackType in this.startedAttacks) { for (var i in this.startedAttacks[attackType]) { var attack = this.startedAttacks[attackType][i]; attack.setPaused(gameState, true); } } } m.HQ.prototype.unpauseAllPlans = function(gameState) { for (var attackType in this.upcomingAttacks) { for (var i in this.upcomingAttacks[attackType]) { var attack = this.upcomingAttacks[attackType][i]; attack.setPaused(gameState, false); } } for (var attackType in this.startedAttacks) { for (var i in this.startedAttacks[attackType]) { var attack = this.startedAttacks[attackType][i]; attack.setPaused(gameState, false); } } } // Some functions are run every turn // Others once in a while m.HQ.prototype.update = function(gameState, queues, events) { Engine.ProfileStart("Headquarters update"); this.checkEvents(gameState,events,queues); //this.buildMoreHouses(gameState); this.trainMoreWorkers(gameState, queues); // sandbox doesn't expand. if (this.Config.difficulty !== 0) this.checkBasesRessLevel(gameState, queues); this.buildMoreHouses(gameState,queues); if (gameState.getTimeElapsed() > this.techStartTime && gameState.currentPhase() > 2 ) this.tryResearchTechs(gameState,queues); if (this.Config.difficulty > 1) this.tryBartering(gameState); this.buildFarmstead(gameState, queues); this.buildMarket(gameState, queues); // Deactivated: the temple had no useful purpose for the AI now. //if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_market"), true) === 1) // this.buildTemple(gameState, queues); this.buildDock(gameState, queues); // not if not a water map. Engine.ProfileStart("Constructing military buildings and building defences"); this.constructTrainingBuildings(gameState, queues); this.buildBlacksmith(gameState, queues); if(gameState.getTimeElapsed() > this.defenceBuildingTime) this.buildDefences(gameState, queues); Engine.ProfileStop(); for (var i in this.baseManagers) { this.baseManagers[i].checkEvents(gameState, events, queues) if ( ( (+i + gameState.ai.playedTurn) % (m.playerGlobals[PlayerID].uniqueIDBases - 1)) === 0) this.baseManagers[i].update(gameState, queues, events); } this.navalManager.update(gameState, queues, events); this.defenceManager.update(gameState, events, this); Engine.ProfileStart("Looping through attack plans"); // TODO: bump this into a function. // TODO: implement some form of check before starting a new attack plans. Sometimes it is not the priority. if (1) { for (var attackType in this.upcomingAttacks) { for (var i = 0;i < this.upcomingAttacks[attackType].length; ++i) { var attack = this.upcomingAttacks[attackType][i]; // okay so we'll get the support plan if (!attack.isStarted()) { var updateStep = attack.updatePreparation(gameState, this,events); // now we're gonna check if the preparation time is over if (updateStep === 1 || attack.isPaused() ) { // just chillin' } else if (updateStep === 0 || updateStep === 3) { m.debug ("Military Manager: " +attack.getType() +" plan " +attack.getName() +" aborted."); if (updateStep === 3) { this.attackPlansEncounteredWater = true; m.debug("No attack path found. Aborting."); } attack.Abort(gameState, this); this.upcomingAttacks[attackType].splice(i--,1); } else if (updateStep === 2) { var chatText = "I am launching an attack against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; if (Math.random() < 0.2) chatText = "Attacking " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; else if (Math.random() < 0.3) chatText = "I have sent an army against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; else if (Math.random() < 0.3) chatText = "I'm starting an attack against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; gameState.ai.chatTeam(chatText); m.debug ("Military Manager: Starting " +attack.getType() +" plan " +attack.getName()); attack.StartAttack(gameState,this); this.startedAttacks[attackType].push(attack); this.upcomingAttacks[attackType].splice(i--,1); } } else { var chatText = "I am launching an attack against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; if (Math.random() < 0.2) chatText = "Attacking " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; else if (Math.random() < 0.3) chatText = "I have sent an army against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; else if (Math.random() < 0.3) chatText = "I'm starting an attack against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; gameState.ai.chatTeam(chatText); m.debug ("Military Manager: Starting " +attack.getType() +" plan " +attack.getName()); this.startedAttacks[attackType].push(attack); this.upcomingAttacks[attackType].splice(i--,1); } } } } for (var attackType in this.startedAttacks) { for (var i = 0; i < this.startedAttacks[attackType].length; ++i) { var attack = this.startedAttacks[attackType][i]; // okay so then we'll update the attack. if (!attack.isPaused()) { var remaining = attack.update(gameState,this,events); if (remaining == 0 || remaining == undefined) { m.debug ("Military Manager: " +attack.getType() +" plan " +attack.getName() +" is now finished."); attack.Abort(gameState); this.startedAttacks[attackType].splice(i--,1); } } } } // creating plans after updating because an aborted plan might be reused in that case. // TODO: remove the limitation to attacks when on water maps. if (!this.waterMap && !this.attackPlansEncounteredWater) { if (gameState.ai.aggressiveness > 0.75 && gameState.countEntitiesByType(gameState.applyCiv(this.bModerate[0]), true) >= 1 && gameState.getTimeElapsed() > this.attackPlansStartTime && gameState.getTimeElapsed() < 360000) { if (this.upcomingAttacks["Rush"].length === 0) { // we have a barracks and we want to rush, rush. var AttackPlan = new m.CityAttack(gameState, this, this.Config, this.TotalAttackNumber, -1, "Rush"); m.debug ("Headquarters: Rushing plan " +this.TotalAttackNumber); this.TotalAttackNumber++; this.upcomingAttacks["Rush"].push(AttackPlan); } } // if we have a barracks, there's no water, we're at age >= 1 and we've decided to attack. else if (gameState.countEntitiesByType(gameState.applyCiv(this.bModerate[0]), true) >= 1 && !this.attackPlansEncounteredWater && gameState.getTimeElapsed() > this.attackPlansStartTime && (gameState.currentPhase() > 1 || gameState.isResearching(gameState.townPhase()))) { if (gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_dock"), true) === 0 && this.waterMap) { // wait till we get a dock. } else if (this.upcomingAttacks["CityAttack"].length === 0) { // basically only the first plan, really. var Lalala = undefined; if (gameState.getTimeElapsed() < 12*60000) Lalala = new m.CityAttack(gameState, this, this.Config, this.TotalAttackNumber, -1); else if (this.Config.difficulty !== 0) Lalala = new m.CityAttack(gameState, this, this.Config, this.TotalAttackNumber, -1, "superSized"); if (Lalala.failed) this.attackPlansEncounteredWater = true; // hack else { m.debug ("Military Manager: Creating the plan " +this.TotalAttackNumber); this.TotalAttackNumber++; this.upcomingAttacks["CityAttack"].push(Lalala); } } } } /* // very old relic. This should be reimplemented someday so the code stays here. if (this.HarassRaiding && this.preparingRaidNumber + this.startedRaidNumber < 1 && gameState.getTimeElapsed() < 780000) { var Lalala = new m.CityAttack(gameState, this,this.totalStartedAttackNumber, -1, "harass_raid"); if (!Lalala.createSupportPlans(gameState, this, )) { m.debug ("Military Manager: harrassing plan not a valid option"); this.HarassRaiding = false; } else { m.debug ("Military Manager: Creating the harass raid plan " +this.totalStartedAttackNumber); this.totalStartedAttackNumber++; this.preparingRaidNumber++; this.currentAttacks.push(Lalala); } } */ Engine.ProfileStop(); /* Engine.ProfileStop(); Engine.ProfileStart("Build new Dropsites"); this.buildDropsites(gameState, queues); Engine.ProfileStop(); if (this.Config.difficulty !== 0) this.tryBartering(gameState); this.buildFarmstead(gameState, queues); this.buildMarket(gameState, queues); // Deactivated: the temple had no useful purpose for the AI now. //if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_market"), true === 1) // this.buildTemple(gameState, queues); this.buildDock(gameState, queues); // not if not a water map. */ Engine.ProfileStop(); // Heaquarters update }; return m; }(AEGIS);