Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/_petrabot.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/_petrabot.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/_petrabot.js (revision 14865) @@ -0,0 +1,284 @@ +Engine.IncludeModule("common-api"); + +var PETRA = (function() { +var m = {}; + +// "local" global variables for stuffs that will need a unique ID +// Note that since order of loading is alphabetic, this means this file must go before any other file using them. +m.playerGlobals = []; + +m.PetraBot = function PetraBot(settings) +{ + API3.BaseAI.call(this, settings); + + this.turn = 0; + this.playedTurn = 0; + + this.Config = new m.Config(); + this.Config.updateDifficulty(settings.difficulty); + //this.Config.personality = settings.personality; + + this.savedEvents = {}; + + this.defcon = 5; + this.defconChangeTime = -10000000; +}; + +m.PetraBot.prototype = new API3.BaseAI(); + +m.PetraBot.prototype.CustomInit = function(gameState, sharedScript) +{ + this.initPersonality(); + + this.priorities = this.Config.priorities; + // this.queues can only be modified by the queue manager or things will go awry. + this.queues = {}; + for (var i in this.priorities) + this.queues[i] = new m.Queue(); + + this.queueManager = new m.QueueManager(this.Config, this.queues, this.priorities); + + this.HQ = new m.HQ(this.Config); + gameState.Config = this.Config; + + m.playerGlobals[PlayerID] = {}; + m.playerGlobals[PlayerID].uniqueIDBOPlans = 0; // training/building/research plans + m.playerGlobals[PlayerID].uniqueIDBases = 1; // base manager ID. Starts at one because "0" means "no base" on the map + m.playerGlobals[PlayerID].uniqueIDTPlans = 1; // transport plans. starts at 1 because 0 might be used as none. + m.playerGlobals[PlayerID].uniqueIDArmy = 0; + + this.HQ.init(gameState,this.queues); + + var filter = API3.Filters.byClass("CivCentre"); + var myKeyEntities = gameState.getOwnEntities().filter(filter); + if (myKeyEntities.length == 0) + myKeyEntities = gameState.getOwnEntities(); + + var enemyKeyEntities = gameState.getEnemyEntities().filter(filter); + if (enemyKeyEntities.length == 0) + enemyKeyEntities = gameState.getEnemyEntities(); + + this.myIndex = this.accessibility.getAccessValue(myKeyEntities.toEntityArray()[0].position()); + + this.pathFinder = new API3.aStarPath(gameState, false, true); + this.pathsToMe = []; + this.pathInfo = { "angle" : 0, "needboat" : true, "mkeyPos" : myKeyEntities.toEntityArray()[0].position(), "ekeyPos" : enemyKeyEntities.toEntityArray()[0].position() }; + + // First path has a sampling of 3, which ensures we'll get at least one path even on Acropolis. The others are 6 so might fail. + var pos = [this.pathInfo.mkeyPos[0] + 150*Math.cos(this.pathInfo.angle),this.pathInfo.mkeyPos[1] + 150*Math.sin(this.pathInfo.angle)]; + var path = this.pathFinder.getPath(this.pathInfo.ekeyPos, pos, 2, 2);// uncomment for debug:*/, 300000, gameState); + + //Engine.DumpImage("initialPath" + this.player + ".png", this.pathFinder.TotorMap.map, this.pathFinder.TotorMap.width,this.pathFinder.TotorMap.height,255); + + if (path !== undefined && path[1] !== undefined && path[1] == false) { + // path is viable and doesn't require boating. + // blackzone the last two waypoints. + this.pathFinder.markImpassableArea(path[0][0][0],path[0][0][1],20); + this.pathsToMe.push(path[0][0][0]); + this.pathInfo.needboat = false; + } + + this.pathInfo.angle += Math.PI/3.0; +}; + +m.PetraBot.prototype.OnUpdate = function(sharedScript) +{ + if (this.gameFinished) + return; + + for (var i in this.events) + { + if(this.savedEvents[i] !== undefined) + this.savedEvents[i] = this.savedEvents[i].concat(this.events[i]); + else + this.savedEvents[i] = this.events[i]; + } + + // Run the update every n turns, offset depending on player ID to balance the load + if ((this.turn + this.player) % 8 == 5) + { + Engine.ProfileStart("PetraBot bot (player " + this.player +")"); + + this.playedTurn++; + + if (this.gameState.getOwnEntities().length === 0) + { + Engine.ProfileStop(); + return; // With no entities to control the AI cannot do anything + } + + if (this.pathInfo !== undefined) + { + var pos = [this.pathInfo.mkeyPos[0] + 150*Math.cos(this.pathInfo.angle),this.pathInfo.mkeyPos[1] + 150*Math.sin(this.pathInfo.angle)]; + var path = this.pathFinder.getPath(this.pathInfo.ekeyPos, pos, 6, 5);// uncomment for debug:*/, 300000, this.gameState); + if (path !== undefined && path[1] !== undefined && path[1] == false) + { + // path is viable and doesn't require boating. + // blackzone the last two waypoints. + this.pathFinder.markImpassableArea(path[0][0][0],path[0][0][1],20); + this.pathsToMe.push(path[0][0][0]); + this.pathInfo.needboat = false; + } + + this.pathInfo.angle += Math.PI/3.0; + + if (this.pathInfo.angle > Math.PI*2.0) + { + if (this.pathInfo.needboat) + { + m.debug ("Assuming this is a water map"); + this.HQ.waterMap = true; + } + delete this.pathFinder; + delete this.pathInfo; + } + } + + var townPhase = this.gameState.townPhase(); + var cityPhase = this.gameState.cityPhase(); + + // try going up phases. + // TODO: softcode this more + if (this.gameState.canResearch(townPhase,true) && this.gameState.getPopulation() >= this.Config.Economy.villagePopCap - 10 + && this.gameState.findResearchers(townPhase,true).length != 0 && this.queues.majorTech.length() === 0) + { + var plan = new m.ResearchPlan(this.gameState, townPhase, true); + plan.lastIsGo = false; + plan.onStart = function (gameState) { gameState.ai.HQ.econState = "growth"; gameState.ai.HQ.OnTownPhase(gameState) }; + plan.isGo = function (gameState) { + var ret = gameState.getPopulation() >= gameState.Config.Economy.villagePopCap + if (ret && !this.lastIsGo) + this.onGo(gameState); + else if (!ret && this.lastIsGo) + this.onNotGo(gameState); + this.lastIsGo = ret; + return ret; + }; + plan.onGo = function (gameState) { gameState.ai.HQ.econState = "townPhasing"; m.debug ("Trying to reach TownPhase"); }; + plan.onNotGo = function (gameState) { gameState.ai.HQ.econState = "growth"; }; + + this.queues.majorTech.addItem(plan); + } + else if (this.gameState.canResearch(cityPhase,true) && this.gameState.getTimeElapsed() > (this.Config.Economy.cityPhase*1000) + && this.gameState.getOwnEntitiesByRole("worker", true).length > 85 + && this.gameState.findResearchers(cityPhase, true).length != 0 && this.queues.majorTech.length() === 0 + && this.queues.civilCentre.length() === 0) + { + var plan = new m.ResearchPlan(this.gameState, cityPhase, true); + plan.onStart = function (gameState) { gameState.ai.HQ.OnCityPhase(gameState) }; + this.queues.majorTech.addItem(plan); + + } + // defcon cooldown + if (this.defcon < 5 && this.gameState.timeSinceDefconChange() > 20000) + { + this.defcon++; + m.debug ("updefconing to " +this.defcon); + if (this.defcon >= 4 && this.HQ.hasGarrisonedFemales) + this.HQ.ungarrisonAll(this.gameState); + } + + this.HQ.update(this.gameState, this.queues, this.savedEvents); + + this.queueManager.update(this.gameState); + + /* + // Use this to debug informations about the metadata. + if (this.playedTurn % 10 === 0) + { + // some debug informations about units. + var units = this.gameState.getOwnEntities(); + for (var i in units._entities) + { + var ent = units._entities[i]; + if (!ent.isIdle()) + continue; + warn ("Unit " + ent.id() + " is a " + ent._templateName); + if (sharedScript._entityMetadata[PlayerID][ent.id()]) + { + var metadata = sharedScript._entityMetadata[PlayerID][ent.id()]; + for (var j in metadata) + { + warn ("Metadata " + j); + if (typeof(metadata[j]) == "object") + warn ("Object"); + else if (typeof(metadata[j]) == undefined) + warn ("Undefined"); + else + warn(uneval(metadata[j])); + } + } + } + }*/ + + + //if (this.playedTurn % 5 === 0) + // this.queueManager.printQueues(this.gameState); + + // Generate some entropy in the random numbers (against humans) until the engine gets random initialised numbers + // TODO: remove this when the engine gives a random seed + var n = this.savedEvents["Create"].length % 29; + for (var i = 0; i < n; i++) + Math.random(); + + for (var i in this.savedEvents) + this.savedEvents[i] = []; + + Engine.ProfileStop(); + } + + this.turn++; +}; + +// defines our core components strategy-wise. +// TODO: the sky's the limit here. +m.PetraBot.prototype.initPersonality = function() +{ + if (this.Config.difficulty >= 2) + { + this.Config.personality.aggressive = Math.random(); + this.Config.personality.cooperative = Math.random(); + } + + if (this.Config.personality.aggressive > 0.7) + { + this.Config.Military.popForBarracks1 = 0; + this.Config.Economy.villagePopCap = 75; + this.Config.Economy.cityPhase = 900; + this.Config.Economy.popForMarket = 80; + this.Config.Economy.targetNumBuilders = 2; + this.Config.Economy.femaleRatio = 0.3; + this.Config.Defense.prudence = 0.5; + this.Config.priorities.defenseBuilding = 60; + } + + if (this.Config.debug == 0) + return; + warn(" >>> Petra bot: personality = " + uneval(this.Config.personality)); +}; + +/*m.PetraBot.prototype.Deserialize = function(data, sharedScript) +{ +}; + +// Override the default serializer +PetraBot.prototype.Serialize = function() +{ + return {}; +};*/ + +// For the moment we just use the debugging flag and the debugging function from the API. +// Maybe it will make sense in the future to separate them. +m.DebugEnabled = function() +{ + return API3.DebugEnabled; +} + +m.debug = function(output) +{ + API3.debug(output); +} + + +return m; +}()); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/army.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/army.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/army.js (revision 14865) @@ -0,0 +1,377 @@ +var PETRA = function(m) +{ + +/* Defines an army + * An army is a collection of own entities and enemy entities. + * This doesn't use entity collections are they aren't really useful + * and it would probably slow the rest of the system down too much. + * All entities are therefore lists of ID + * Inherited by the defense manager and several of the attack manager's attack plan. + */ + +m.Army = function(gameState, owner, ownEntities, foeEntities) +{ + this.ID = m.playerGlobals[PlayerID].uniqueIDArmy++; + + this.Config = owner.Config; + this.defenseRatio = this.Config.Defense.defenseRatio; + this.compactSize = this.Config.Defense.armyCompactSize; + this.breakawaySize = this.Config.Defense.armyBreakawaySize; + + // average + this.foePosition = [0,0]; + this.ownPosition = [0,0]; + this.positionLastUpdate = gameState.getTimeElapsed(); + + // Some caching + // A list of our defenders that were tasked with attacking a particular unit + // This doesn't mean that they actually are since they could move on to something else on their own. + this.assignedAgainst = {}; + // who we assigned against, for quick removal. + this.assignedTo = {}; + + // For substrengths, format is "name": [classes] + + this.foeEntities = []; + this.foeStrength = 0; + this.foeSubStrength = {}; + + this.ownEntities = []; + this.ownStrength = 0; + this.ownSubStrength = {}; + + // actually add units + for (var i in foeEntities) + this.addFoe(gameState,foeEntities[i], true); + for (var i in ownEntities) + this.addOwn(gameState,ownEntities[i]); + + this.recalculatePosition(gameState, true); + + return true; +} + +// if not forced, will only recalculate if on a different turn. +m.Army.prototype.recalculatePosition = function(gameState, force) +{ + if (!force && this.positionLastUpdate === gameState.getTimeElapsed()) + return; + var pos = [0,0]; + if (this.foeEntities.length !== 0) + { + for each (var id in this.foeEntities) + { + var ent = gameState.getEntityById(id); + var epos = ent.position(); + pos[0] += epos[0]; + pos[1] += epos[1]; + } + this.foePosition[0] = pos[0]/this.foeEntities.length; + this.foePosition[1] = pos[1]/this.foeEntities.length; + } else + this.foePosition = [0,0]; + + pos = [0,0]; + if (this.ownEntities.length !== 0) + { + for each (var id in this.ownEntities) + { + var ent = gameState.getEntityById(id); + var epos = ent.position(); + pos[0] += epos[0]; + pos[1] += epos[1]; + } + this.ownPosition[0] = pos[0]/this.ownEntities.length; + this.ownPosition[1] = pos[1]/this.ownEntities.length; + } else + this.ownPosition = [0,0]; + + this.positionLastUpdate = gameState.getTimeElapsed(); +} + +// helper +m.Army.prototype.recalculateStrengths = function (gameState) +{ + this.ownStrength = 0; + this.foeStrength = 0; + + // todo: deal with specifics. + + for each (var id in this.foeEntities) + this.evaluateStrength(gameState.getEntityById(id)); + for each (var id in this.ownEntities) + this.evaluateStrength(gameState.getEntityById(id), true); +} + +// adds or remove the strength of the entity either to the enemy or to our units. +m.Army.prototype.evaluateStrength = function (ent, isOwn, remove) +{ + var entStrength = m.getMaxStrength(ent); + if (remove) + entStrength *= -1; + + if (isOwn) + this.ownStrength += entStrength; + else + this.foeStrength += entStrength; + + // todo: deal with specifics. +} + +// add an entity to the enemy army +// Will return true if the entity was added and false otherwise. +// won't recalculate our position but will dirty it. +m.Army.prototype.addFoe = function (gameState, enemyID, force) +{ + if (this.foeEntities.indexOf(enemyID) !== -1) + return false; + var ent = gameState.getEntityById(enemyID); + if (ent === undefined || ent.position() === undefined) + return false; + + // check distance + if (!force && API3.SquareVectorDistance(ent.position(), this.foePosition) > this.compactSize) + return false; + + this.foeEntities.push(enemyID); + this.assignedAgainst[enemyID] = []; + this.positionLastUpdate = 0; + this.evaluateStrength(ent); + ent.setMetadata(PlayerID, "PartOfArmy", this.ID); + + return true; +} + +// returns true if the entity was removed and false otherwise. +// TODO: when there is a technology update, we should probably recompute the strengths, or weird stuffs will happen. +m.Army.prototype.removeFoe = function (gameState, enemyID, enemyEntity) +{ + var idx = this.foeEntities.indexOf(enemyID); + if (idx === -1) + return false; + var ent = enemyEntity === undefined ? gameState.getEntityById(enemyID) : enemyEntity; + if (ent === undefined) + { + warn("Trying to remove a non-existing enemy entity, crashing for stacktrace"); + xgzrg(); + } + this.foeEntities.splice(idx, 1); + this.evaluateStrength(ent, false, true); + ent.setMetadata(PlayerID, "PartOfArmy", undefined); + + this.assignedAgainst[enemyID] = undefined; + for (var to in this.assignedTo) + if (this.assignedTo[to] == enemyID) + this.assignedTo[to] = undefined; + + return true; +} + +// adds a defender but doesn't assign him yet. +m.Army.prototype.addOwn = function (gameState, ID) +{ + if (this.ownEntities.indexOf(ID) !== -1) + return false; + var ent = gameState.getEntityById(ID); + if (ent === undefined || ent.position() === undefined) + return false; + + this.ownEntities.push(ID); + this.evaluateStrength(ent, true); + ent.setMetadata(PlayerID, "PartOfArmy", this.ID); + this.assignedTo[ID] = 0; + + var formerSubrole = ent.getMetadata(PlayerID, "subrole"); + if (formerSubrole && formerSubrole === "defender") // can happen when armies are merged for example + return true; + if (formerSubrole !== undefined) + ent.setMetadata(PlayerID, "formerSubrole", formerSubrole); + ent.setMetadata(PlayerID, "subrole", "defender"); + return true; +} + +m.Army.prototype.removeOwn = function (gameState, ID, Entity) +{ + var idx = this.ownEntities.indexOf(ID); + if (idx === -1) + return false; + var ent = Entity === undefined ? gameState.getEntityById(ID) : Entity; + if (ent === undefined) + { + warn( ID); + warn("Trying to remove a non-existing entity, crashing for stacktrace"); + xgzrg(); + } + + this.ownEntities.splice(idx, 1); + this.evaluateStrength(ent, true, true); + ent.setMetadata(PlayerID, "PartOfArmy", undefined); + + if (this.assignedTo[ID] !== 0) + { + var temp = this.assignedAgainst[this.assignedTo[ID]]; + if (temp) + temp.splice(temp.indexOf(ID), 1); + } + this.assignedTo[ID] = undefined; + + + var formerSubrole = ent.getMetadata(PlayerID, "formerSubrole"); + if (formerSubrole !== undefined) + ent.setMetadata(PlayerID, "subrole", formerSubrole); + else + ent.setMetadata(PlayerID, "subrole", undefined); + + return true; +} + +// this one is "undefined entity" proof because it's called at odd times. +// Orders a unit to attack an enemy. +// overridden by specific army classes. +m.Army.prototype.assignUnit = function (gameState, entID) +{ +} + +// resets the army properly. +// assumes we already cleared dead units. +m.Army.prototype.clear = function (gameState, events) +{ + while(this.foeEntities.length > 0) + this.removeFoe(gameState,this.foeEntities[0]); + while(this.ownEntities.length > 0) + this.removeOwn(gameState,this.ownEntities[0]); + + this.assignedAgainst = {}; + this.assignedTo = {}; + + this.recalculateStrengths(gameState); + this.recalculatePosition(gameState); +} + +// merge this army with another properly. +// assumes units are in only one army. +// also assumes that all have been properly cleaned up (no dead units). +m.Army.prototype.merge = function (gameState, otherArmy) +{ + // copy over all parameters. + for (var i in otherArmy.assignedAgainst) + { + if (this.assignedAgainst[i] === undefined) + this.assignedAgainst[i] = otherArmy.assignedAgainst[i]; + else + this.assignedAgainst[i] = this.assignedAgainst[i].concat(otherArmy.assignedAgainst[i]); + } + for (var i in otherArmy.assignedTo) + this.assignedTo[i] = otherArmy.assignedTo[i]; + + for each (var id in otherArmy.foeEntities) + this.addFoe(gameState, id); + // TODO: reassign those ? + for each (var id in otherArmy.ownEntities) + this.addOwn(gameState, id); + + this.recalculatePosition(gameState, true); + this.recalculateStrengths(gameState); + + return true; +} + +// TODO: when there is a technology update, we should probably recompute the strengths, or weird stuffs might happen. +m.Army.prototype.checkEvents = function (gameState, events) +{ + var renameEvents = events["EntityRenamed"]; // take care of promoted and packed units + var destroyEvents = events["Destroy"]; + var convEvents = events["OwnershipChanged"]; + var garriEvents = events["Garrison"]; + + // Warning the metadata is already cloned in shared.js. Futhermore, changes should be done before destroyEvents + // otherwise it would remove the old entity from this army list + // TODO we should may-be reevaluate the strength + for each (var msg in renameEvents) + { + if (this.foeEntities.indexOf(msg.entity) !== -1) + { + var idx = this.foeEntities.indexOf(msg.entity); + this.foeEntities[idx] = msg.newentity; + this.assignedAgainst[msg.newentity] = this.assignedAgainst[msg.entity]; + this.assignedAgainst[msg.entity] = undefined; + for (var to in this.assignedTo) + if (this.assignedTo[to] == msg.entity) + this.assignedTo[to] = msg.newentity; + } + else if (this.ownEntities.indexOf(msg.entity) !== -1) + { + var idx = this.ownEntities.indexOf(msg.entity); + this.ownEntities[idx] = msg.newentity; + this.assignedTo[msg.newentity] = this.assignedTo[msg.entity]; + this.assignedTo[msg.entity] = undefined; + for (var against in this.assignedAgainst) + { + if (!this.assignedAgainst[against]) + continue; + if (this.assignedAgainst[against].indexOf(msg.entity) !== -1) + this.assignedAgainst[against][this.assignedAgainst[against].indexOf(msg.entity)] = msg.newentity; + } + } + } + + for each (var msg in destroyEvents) + { + if (msg.entityObj === undefined) + continue; + if (msg.entityObj._entity.owner === PlayerID) + this.removeOwn(gameState, msg.entity, msg.entityObj); + else + this.removeFoe(gameState, msg.entity, msg.entityObj); + } + + for each (var msg in garriEvents) + this.removeFoe(gameState, msg.entity); + + for each (var msg in convEvents) + { + if (msg.to === PlayerID) + { + // we have converted an enemy, let's assign it as a defender + if (this.removeFoe(gameState, msg.entity)) + this.addOwn(gameState, msg.entity); + } else if (msg.from === PlayerID) + this.removeOwn(gameState, msg.entity); // TODO: add allies + } +} + +// assumes cleaned army. +// this only checks for breakaways. +m.Army.prototype.onUpdate = function (gameState) +{ + var breakaways = []; + // TODO: assign unassigned defenders, cleanup of a few things. + // perhaps occasional strength recomputation + + // occasional update or breakaways, positions… + if (gameState.getTimeElapsed() - this.positionLastUpdate > 5000) + { + this.recalculatePosition(gameState); + this.positionLastUpdate = gameState.getTimeElapsed(); + + // Check for breakaways. + for (var i = 0; i < this.foeEntities.length; ++i) + { + var id = this.foeEntities[i]; + var ent = gameState.getEntityById(id); + if (API3.SquareVectorDistance(ent.position(), this.foePosition) > this.breakawaySize) + { + breakaways.push(id); + if(this.removeFoe(gameState, id)) + i--; + } + } + + this.recalculatePosition(gameState); + } + + return breakaways; +} + +return m; +}(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackManager.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackManager.js (revision 14865) @@ -0,0 +1,286 @@ +var PETRA = function(m) +{ + +/* Attack Manager + */ + +m.AttackManager = function(Config) +{ + this.Config = Config; + + this.totalNumber = 0; + this.attackNumber = 0; + this.rushNumber = 0; + this.raidNumber = 0; + this.upcomingAttacks = { "CityAttack": [], "Rush": [], "Raid": [] }; + this.startedAttacks = { "CityAttack": [], "Rush": [], "Raid": [] }; + this.debugTime = 0; +}; + +// More initialisation for stuff that needs the gameState +m.AttackManager.prototype.init = function(gameState, queues) +{ + this.maxRushes = 0; + if (this.Config.personality.aggressive > 0.9) + this.maxRushes = 2 + else if (this.Config.personality.aggressive > 0.7) + this.maxRushes = 1; + else + this.maxRushes = 0; +}; + +// Some functions are run every turn +// Others once in a while +m.AttackManager.prototype.update = function(gameState, queues, events) +{ + if (this.Config.debug == 2 && gameState.getTimeElapsed() > this.debugTime + 60000) + { + this.debugTime = gameState.getTimeElapsed(); + warn(" upcoming attacks ================="); + for (var attackType in this.upcomingAttacks) + { + for (var i = 0; i < this.upcomingAttacks[attackType].length; ++i) + { + var attack = this.upcomingAttacks[attackType][i]; + warn(" type " + attackType + " state " + attack.state + " paused " + attack.isPaused()); + } + } + warn(" started attacks =================="); + for (var attackType in this.startedAttacks) + { + for (var i = 0; i < this.startedAttacks[attackType].length; ++i) + { + var attack = this.startedAttacks[attackType][i]; + warn(" type " + attackType + " state " + attack.state + " paused " + attack.isPaused()); + } + } + warn(" =================================="); + } + + for (var attackType in this.upcomingAttacks) + { + for (var i = 0; i < this.upcomingAttacks[attackType].length; ++i) + { + var attack = this.upcomingAttacks[attackType][i]; + attack.checkEvents(gameState, events, queues); + + // 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) + { + if (this.Config.debug) + warn("Attack Manager: " + attack.getType() + " plan " + attack.getName() + " aborted."); + if (updateStep === 3) + this.attackPlansEncounteredWater = true; + attack.Abort(gameState, this); + this.upcomingAttacks[attackType].splice(i--,1); + } + else if (updateStep === 2) + { + if (attack.StartAttack(gameState,this)) + { + var targetName = gameState.sharedScript.playersData[attack.targetPlayer].name; + var proba = Math.random(); + if (proba < 0.2) + var chatText = "I am launching an attack against " + targetName + "."; + else if (proba < 0.4) + var chatText = "Attacking " + targetName + "."; + else if (proba < 0.7) + var chatText = "I have sent an army against " + targetName + "."; + else + var chatText = "I'm starting an attack against " + targetName + "."; + gameState.ai.chatTeam(chatText); + + if (this.Config.debug) + warn("Attack Manager: Starting " + attack.getType() + " plan " + attack.getName()); + this.startedAttacks[attackType].push(attack); + } + else + attack.Abort(gameState, this); + this.upcomingAttacks[attackType].splice(i--,1); + } + } + else + { + var targetName = gameState.sharedScript.playersData[attack.targetPlayer].name; + var proba = Math.random(); + if (proba < 0.2) + var chatText = "I am launching an attack against " + targetName + "."; + else if (proba < 0.4) + var chatText = "Attacking " + targetName + "."; + else if (proba < 0.7) + var chatText = "I have sent an army against " + targetName + "."; + else + var chatText = "I'm starting an attack against " + targetName + "."; + gameState.ai.chatTeam(chatText); + + if (this.Config.debug) + warn("Attack 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]; + attack.checkEvents(gameState, events, queues); + // okay so then we'll update the attack. + if (attack.isPaused()) + continue; + var remaining = attack.update(gameState,this,events); + if (!remaining) + { + if (this.Config.debug) + warn("Military Manager: " + attack.getType() + " plan " + attack.getName() + " is finished with remaining " + remaining); + 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 (!gameState.ai.HQ.waterMap && !this.attackPlansEncounteredWater) + { + if (this.rushNumber < this.maxRushes && gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_barracks"), true) >= 1) + { + if (this.upcomingAttacks["Rush"].length === 0) + { + // we have a barracks and we want to rush, rush. + var attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, -1, "Rush"); + if (this.Config.debug) + warn("Headquarters: Rushing plan " + this.totalNumber + " with maxRushes " + this.maxRushes); + this.rushNumber++; + this.totalNumber++; + 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("structures/{civ}_barracks"), true) >= 1 + && (gameState.currentPhase() > 1 || gameState.isResearching(gameState.townPhase()))) + { + if (gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_dock"), true) === 0 && gameState.ai.HQ.waterMap) + { + // wait till we get a dock. + } + else if (this.upcomingAttacks["CityAttack"].length === 0) + { + if (this.attackNumber < 2) + var attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, -1); + else + var attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, -1, "superSized"); + + if (attackPlan.failed) + this.attackPlansEncounteredWater = true; // hack + else + { + if (this.Config.debug) + warn("Military Manager: Creating the plan " + this.totalNumber); + this.attackNumber++; + this.totalNumber++; + this.upcomingAttacks["CityAttack"].push(attackPlan); + } + } + } + + if (this.upcomingAttacks["Raid"].length === 999) + { + var enemyCC = gameState.getEnemyStructures().filter(API3.Filters.and(API3.Filters.byClass("CivCentre"), API3.Filters.isFoundation())); + if (enemyCC.length > 0) + { + // prepare some raid on this CC + var enemy = enemyCC.toEntityArray()[0].owner(); + var attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, enemy, "Raid"); + if (this.Config.debug) + warn("Headquarters: Raiding plan " + this.totalNumber); + this.raidNumber++; + this.totalNumber++; + this.upcomingAttacks["Raid"].push(attackPlan); + } + } + } +}; + +m.AttackManager.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(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(true); + } + } +}; + +m.AttackManager.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(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(false); + } + } +}; + +m.AttackManager.prototype.pauseAllPlans = function(gameState) +{ + for (var attackType in this.upcomingAttacks) + for (var i in this.upcomingAttacks[attackType]) + this.upcomingAttacks[attackType][i].setPaused(true); + + for (var attackType in this.startedAttacks) + for (var i in this.startedAttacks[attackType]) + this.startedAttacks[attackType][i].setPaused(true); +}; + +m.AttackManager.prototype.unpauseAllPlans = function(gameState) +{ + for (var attackType in this.upcomingAttacks) + for (var i in this.upcomingAttacks[attackType]) + this.upcomingAttacks[attackType][i].setPaused(false); + + for (var attackType in this.startedAttacks) + for (var i in this.startedAttacks[attackType]) + this.startedAttacks[attackType][i].setPaused(false); +}; + +return m; +}(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/worker.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/worker.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/worker.js (revision 14865) @@ -0,0 +1,520 @@ +var PETRA = function(m) +{ + +/** + * This class makes a worker do as instructed by the economy manager + */ + +m.Worker = function(ent) { + this.ent = ent; + this.baseID = 0; + this.lastUpdate = undefined; +}; + +m.Worker.prototype.update = function(baseManager, gameState) { + this.lastUpdate = gameState.ai.playedTurn; + this.baseID = baseManager.ID; + var subrole = this.ent.getMetadata(PlayerID, "subrole"); + + if (!this.ent.position() || (this.ent.getMetadata(PlayerID,"fleeing") && gameState.getTimeElapsed() - this.ent.getMetadata(PlayerID,"fleeing") < 8000)){ + // If the worker has no position then no work can be done + return; + } + if (this.ent.getMetadata(PlayerID,"fleeing")) + this.ent.setMetadata(PlayerID,"fleeing", undefined); + + // Okay so we have a few tasks. + // If we're gathering, we'll check that we haven't run idle. + // ANd we'll also check that we're gathering a resource we want to gather. + + // If we're fighting or hunting, let's not start gathering, heh? + if (this.ent.unitAIState().split(".")[1] === "COMBAT" || this.ent.getMetadata(PlayerID, "role") === "transport") + return; + +/* if (this.ent.unitAIState() == "INDIVIDUAL.IDLE" && subrole != "hunter") + { + var role = this.ent.getMetadata(PlayerID, "role"); + var base = this.ent.getMetadata(PlayerID, "base"); + var founda = this.ent.getMetadata(PlayerID, "target-foundation"); + warn(" unit idle " + this.ent.id() + " role " + role + " subrole " + subrole + " base " + base + " founda " + founda); + } */ + + if (subrole === "gatherer") + { + if (this.ent.isIdle()) + { + // if we aren't storing resources or it's the same type as what we're about to gather, + // let's just pick a new resource. + // TODO if we already carry the max we can -> returnresources + if (!this.ent.resourceCarrying() || this.ent.resourceCarrying().length === 0 || + this.ent.resourceCarrying()[0].type === this.ent.getMetadata(PlayerID, "gather-type")) + { + this.startGathering(gameState, baseManager); + } + else if (!this.returnResources(gameState)) // try to deposit resources + { + // no dropsite, abandon old resources and start gathering new ones + this.startGathering(gameState, baseManager); + } + } + else if (this.ent.unitAIState().split(".")[1] === "GATHER") + { + // we're already gathering. But let's check from time to time if there is nothing better + // in case UnitAI did something bad + if (this.ent.unitAIOrderData().length) + { + var supplyId = this.ent.unitAIOrderData()[0]["target"]; + var supply = gameState.getEntityById(supplyId); + if (supply && !supply.hasClass("Field") && !supply.hasClass("Animal") + && supplyId !== this.ent.getMetadata(PlayerID, "supply")) + { + var nbGatherers = supply.resourceSupplyGatherers().length + + m.GetTCGatherer(gameState, supplyId); + if ((nbGatherers > 0 && supply.resourceSupplyAmount()/nbGatherers < 40)) + { + m.RemoveTCGatherer(gameState, supplyId); + this.startGathering(gameState, baseManager); + } + else + { + var gatherType = this.ent.getMetadata(PlayerID, "gather-type"); + var nearby = baseManager.dropsiteSupplies[gatherType]["nearby"]; + var isNearby = nearby.some(function(sup) { + if (sup.id === supplyId) + return true; + return false; + }); + if (nearby.length === 0 || isNearby) + this.ent.setMetadata(PlayerID, "supply", supplyId); + else + { + m.RemoveTCGatherer(gameState, supplyId); + this.startGathering(gameState, baseManager); + } + } + } + } + } + } + else if (subrole === "builder") + { + if (this.ent.unitAIState().split(".")[1] === "REPAIR") + return; + // okay so apparently we aren't working. + // Unless we've been explicitely told to keep our role, make us idle. + var target = gameState.getEntityById(this.ent.getMetadata(PlayerID, "target-foundation")); + if (!target || (target.foundationProgress() === undefined && target.needsRepair() === false)) + { + this.ent.setMetadata(PlayerID, "subrole", "idle"); + this.ent.setMetadata(PlayerID, "target-foundation", undefined); + // If worker elephant, move away to avoid being trapped in between constructions + if (this.ent.hasClass("Elephant")) + this.moveAway(baseManager, gameState); + } + else + { + if (target && target.foundationProgress() === undefined && target.needsRepair() === true) + warn(" target " + target.id() + " needs repair " + target.hitpoints() + " max " + target.maxHitpoints()); + this.ent.repair(target); + } + } + else if (subrole === "hunter") + { + if (this.ent.isIdle()) + this.startHunting(gameState, baseManager); + } +}; + +m.Worker.prototype.startGathering = function(gameState, baseManager) +{ + if (!this.ent.position()) // TODO: work out what to do when entity has no position + return false; + + var resource = this.ent.getMetadata(PlayerID, "gather-type"); + + // Then if we are gathering food, try to hunt + if (resource === "food" && this.startHunting(gameState, baseManager)) + return true; + + var foundSupply = function(ent, supply) { + var ret = false; + for (var i = 0; i < supply.length; ++i) + { + // exhausted resource, remove it from this list + if (!supply[i].ent || !gameState.getEntityById(supply[i].id)) + { + supply.splice(i--, 1); + continue; + } + if (m.IsSupplyFull(gameState, supply[i].ent) === true) + continue; + // check if available resource is worth one additionnal gatherer (except for farms) + var nbGatherers = supply[i].ent.resourceSupplyGatherers().length + + m.GetTCGatherer(gameState, supply[i].id); + if (supply[i].ent.resourceSupplyType()["specific"] !== "grain" + && nbGatherers > 0 && supply[i].ent.resourceSupplyAmount()/(1+nbGatherers) < 40) + continue; + // not in ennemy territory + var territoryOwner = m.createTerritoryMap(gameState).getOwner(supply[i].ent.position()); + if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally + continue; + m.AddTCGatherer(gameState, supply[i].id); + ent.gather(supply[i].ent); + ent.setMetadata(PlayerID, "supply", supply[i].id); + ret = true; + break; + } + return ret; + }; + + var nearby = baseManager.dropsiteSupplies[resource]["nearby"]; + if (foundSupply(this.ent, nearby)) + return true; + + // --> for food, try to gather from fields if any, otherwise build one if any + if (resource === "food" && (this.gatherNearestField(gameState) || this.buildAnyField(gameState))) + return true; + + var medium = baseManager.dropsiteSupplies[resource]["medium"]; + if (foundSupply(this.ent, medium)) + return true; + + // So if we're here we have checked our whole base for a proper resource without success + // --> check other bases before going back to faraway resources + for each (var base in gameState.ai.HQ.baseManagers) + { + if (base.ID === this.baseID) + continue; + var nearby = base.dropsiteSupplies[resource]["nearby"]; + if (foundSupply(this.ent, nearby)) + { + this.ent.setMetadata(PlayerID, "base", base.ID); + return true; + } + } + for each (var base in gameState.ai.HQ.baseManagers) + { + if (base.ID === this.baseID) + continue; + var medium = base.dropsiteSupplies[resource]["medium"]; + if (foundSupply(this.ent, medium)) + { + this.ent.setMetadata(PlayerID, "base", base.ID); + return true; + } + } + + + // Okay so we haven't found any appropriate dropsite anywhere. + // Try to help building one if any foundation available in the same base + var self = this; + var foundations = gameState.getOwnFoundations().toEntityArray(); + var shouldBuild = foundations.some(function(foundation) { + if (!foundation || foundation.getMetadata(PlayerID, "base") != self.baseID) + return false; + if ((resource !== "food" && foundation.hasClass("Storehouse")) || + (resource === "food" && foundation.hasClass("DropsiteFood"))) + { + self.ent.repair(foundation); + return true; + } + return false; + }); + if (shouldBuild) + return true; + + // Still nothing, we look now for faraway resources, first in this base, then in others + var faraway = baseManager.dropsiteSupplies[resource]["faraway"]; + if (foundSupply(this.ent, faraway)) + return true; + for each (var base in gameState.ai.HQ.baseManagers) + { + if (base.ID === this.baseID) + continue; + var faraway = base.dropsiteSupplies[resource]["faraway"]; + if (foundSupply(this.ent, faraway)) + { + this.ent.setMetadata(PlayerID, "base", base.ID); + return true; + } + } + + return false; +}; + +// Makes the worker deposit the currently carried resources at the closest dropsite +m.Worker.prototype.returnResources = function(gameState) +{ + if (!this.ent.resourceCarrying() || this.ent.resourceCarrying().length === 0 || !this.ent.position()) + return false; + + var resource = this.ent.resourceCarrying()[0].type; + var self = this; + + var closestDropsite = undefined; + var dist = Math.min(); + gameState.getOwnDropsites(resource).forEach(function(dropsite){ + if (dropsite.position()) + { + var d = API3.SquareVectorDistance(self.ent.position(), dropsite.position()); + if (d < dist) + { + dist = d; + closestDropsite = dropsite; + } + } + }); + + if (!closestDropsite) + return false; + this.ent.returnResources(closestDropsite); + return true; +}; + +m.Worker.prototype.startHunting = function(gameState, baseManager) +{ + if (!this.ent.position()) + return false; + + // So here we're doing it basic. We check what we can hunt, we hunt it. No fancies. + + var resources = gameState.getHuntableSupplies(); + if (resources.length === 0) + return false; + + var nearestSupplyDist = Math.min(); + var nearestSupply = undefined; + + var isCavalry = this.ent.hasClass("Cavalry"); + var isRanged = this.ent.hasClass("Ranged"); + var entPosition = this.ent.position(); + + var nearestDropsiteDist = function(supply){ + var distMin = 1000000; + var pos = supply.position(); + gameState.getOwnDropsites("food").forEach(function (dropsite){ + if (!dropsite.position()) + return; + var dist = API3.SquareVectorDistance(pos, dropsite.position()); + if (dist < distMin) + distMin = dist; + }); + return distMin; + }; + + resources.forEach(function(supply) + { + if (!supply.position()) + return; + + if (supply.getMetadata(PlayerID, "inaccessible") === true) + return; + + if (m.IsSupplyFull(gameState, supply) === true) + return; + // check if available resource is worth one additionnal gatherer (except for farms) + var nbGatherers = supply.resourceSupplyGatherers().length + + m.GetTCGatherer(gameState, supply.id()); + if (nbGatherers > 0 && supply.resourceSupplyAmount()/(1+nbGatherers) < 40) + return; + + // Only cavalry and range units should hunt fleeing animals + if (!supply.hasClass("Domestic") && !isCavalry && !isRanged) + return; + + // quickscope accessbility check + if (!gameState.ai.accessibility.pathAvailable(gameState, entPosition, supply.position(),false, true)) + return; + + // measure the distance to the resource + var dist = API3.SquareVectorDistance(entPosition, supply.position()); + // Only cavalry should hunt faraway + if (!isCavalry && dist > 25000) + return; + + // some simple check for chickens: if they're in a inaccessible square, we won't gather from them. + // TODO: make sure this works with rounding. + if (supply.footprintRadius() < 1) + { + var fakeMap = new API3.Map(gameState.sharedScript, gameState.getMap().data); + var id = fakeMap.gamePosToMapPos(supply.position())[0] + fakeMap.width*fakeMap.gamePosToMapPos(supply.position())[1]; + if ((gameState.sharedScript.passabilityClasses["pathfinderObstruction"] & gameState.getMap().data[id])) + { + supply.setMetadata(PlayerID, "inaccessible", true) + return; + } + } + + // Avoid ennemy territory + var territoryOwner = m.createTerritoryMap(gameState).getOwner(supply.position()); + if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally + return; + + var dropsiteDist = nearestDropsiteDist(supply); + if (dropsiteDist > 35000) + return; + // Only cavalry should hunt faraway (specially for non domestic animals which flee) + if (!isCavalry && (dropsiteDist > 10000 || ((dropsiteDist > 7000 || territoryOwner == 0 ) && !supply.hasClass("Domestic")))) + return; + + if (dist < nearestSupplyDist) + { + nearestSupplyDist = dist; + nearestSupply = supply; + } + }); + + if (nearestSupply) + { + m.AddTCGatherer(gameState, nearestSupply.id()); + this.ent.gather(nearestSupply); + this.ent.setMetadata(PlayerID, "supply", nearestSupply.id()); + this.ent.setMetadata(PlayerID, "target-foundation", undefined); + return true; + } + else + { + if (this.ent.getMetadata(PlayerID,"subrole") === "hunter") + this.ent.setMetadata(PlayerID, "subrole", "idle"); + return false; + } +}; + +m.Worker.prototype.getResourceType = function(type){ + if (!type || !type.generic) + return undefined; + + if (type.generic === "treasure") + return type.specific; + else + return type.generic; +}; + +m.Worker.prototype.getGatherRate = function(gameState) { + if (this.ent.getMetadata(PlayerID,"subrole") !== "gatherer") + return 0; + var rates = this.ent.resourceGatherRates(); + + if (this.ent.unitAIOrderData().length && this.ent.unitAIState().split(".")[1] === "GATHER" && this.ent.unitAIOrderData()[0]["target"]) + { + var ress = gameState.getEntityById(this.ent.unitAIOrderData()[0]["target"]); + if (!ress) + return 0; + var type = ress.resourceSupplyType(); + if (type.generic == "treasure") + return 1000; + var tstring = type.generic + "." + type.specific; + //m.debug (+rates[tstring] + " for " + tstring + " for " + this.ent._templateName); + if (rates[tstring]) + return rates[tstring]; + return 0; + } + return 0; +}; + +m.Worker.prototype.gatherNearestField = function(gameState){ + if (!this.ent.position()) + return false; + + var self = this; + var ownFields = gameState.getOwnEntitiesByType(gameState.applyCiv("structures/{civ}_field"), true); + var bestFarmEnt = undefined; + var bestFarmDist = 10000000; + + ownFields.forEach(function (field) { + if (m.IsSupplyFull(gameState, field) === true) + return; + var dist = API3.SquareVectorDistance(field.position(), self.ent.position()); + if (dist < bestFarmDist) + { + bestFarmEnt = field; + bestFarmDist = dist; + } + }); + if (bestFarmEnt !== undefined) + { + this.ent.setMetadata(PlayerID, "base", bestFarmEnt.getMetadata(PlayerID, "base")); + m.AddTCGatherer(gameState, bestFarmEnt.id()); + this.ent.gather(bestFarmEnt); + this.ent.setMetadata(PlayerID, "supply", bestFarmEnt.id()); + return true; + } + return false; +}; + +/** + * WARNING with the present options of AI orders, the unit will not gather after building the farm. + * This is done by calling the gatherNearestField function when construction is completed. + */ +m.Worker.prototype.buildAnyField = function(gameState){ + var self = this; + var foundations = gameState.getOwnFoundations(); + var baseFoundations = foundations.filter(API3.Filters.byMetadata(PlayerID, "base", this.baseID)); + + var maxGatherers = gameState.getTemplate(gameState.applyCiv("structures/{civ}_field")).maxGatherers(); + + var bestFarmEnt = undefined; + var bestFarmDist = 10000000; + baseFoundations.forEach(function (found) { + if (found.hasClass("Field")) { + var current = found.getBuildersNb(); + if (current === undefined || current >= maxGatherers) + return; + var dist = API3.SquareVectorDistance(found.position(), self.ent.position()); + if (dist < bestFarmDist) + { + bestFarmEnt = found; + bestFarmDist = dist; + } + } + }); + if (bestFarmEnt !== undefined) + { + this.ent.repair(bestFarmEnt); + return true; + } + // No farms found, search in other bases + foundations.forEach(function (found) { + if (found.hasClass("Field")) { + var current = found.getBuildersNb(); + if (current === undefined || current >= maxGatherers) + return; + var dist = API3.SquareVectorDistance(found.position(), self.ent.position()); + if (dist < bestFarmDist) + { + bestFarmEnt = found; + bestFarmDist = dist; + } + } + }); + if (bestFarmEnt !== undefined) + { + this.ent.repair(bestFarmEnt); + this.ent.setMetadata(PlayerID, "base", bestFarmEnt.getMetadata(PlayerID, "base")); + return true; + } + return false; +}; + +// Workers elephant should move away from the buildings they've built to avoid being trapped in between constructions +// For the time being, we move towards the nearest gatherer (providing him a dropsite) +m.Worker.prototype.moveAway = function(baseManager, gameState){ + var gatherers = baseManager.workersBySubrole(gameState, "gatherer").toEntityArray(); + var pos = this.ent.position(); + var dist = Math.min(); + var destination = pos; + for (var i = 0; i < gatherers.length; ++i) + { + if (gatherers[i].isIdle()) + continue; + var distance = API3.SquareVectorDistance(pos, gatherers[i].position()); + if (distance > dist) + continue; + dist = distance; + destination = gatherers[i].position(); + } + this.ent.move(destination[0], destination[1]); +}; + +return m; +}(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js (revision 14865) @@ -0,0 +1,1346 @@ +var PETRA = function(m) +{ + +/* This is an attack plan (despite the name, it's a relic of older times). + * It deals with everything in an attack, from picking a target to picking a path to it + * To making sure units are built, and pushing elements to the queue manager otherwise + * It also handles the actual attack, though much work is needed on that. + * These should be extremely flexible with only minimal work. + * There is a basic support for naval expeditions here. + */ + +m.AttackPlan = function(gameState, Config, uniqueID, targetEnemy, type , targetFinder) +{ + this.Config = Config; + this.debugTime = 0; + //This is the list of IDs of the units in the plan + this.idList=[]; + + this.state = "unexecuted"; + this.targetPlayer = targetEnemy; + if (this.targetPlayer === -1 || this.targetPlayer === undefined) + { + // let's find our prefered target, basically counting our enemies units. + // TODO: improve this. + var enemyCount = {}; + var enemyDefense = {}; + for (var i = 1; i < gameState.sharedScript.playersData.length; ++i) + { + enemyCount[i] = 0; + enemyDefense[i] = 0; + } + gameState.getEntities().forEach(function(ent) { + if (gameState.isEntityEnemy(ent) && ent.owner() !== 0) + { + enemyCount[ent.owner()]++; + if (ent.hasClass("Tower") || ent.hasClass("Fortress")) + enemyDefense[ent.owner()]++; + } + }); + var max = 0; + for (var i in enemyCount) + { + if (type === "Rush" && enemyDefense[i] > 6) // No rush if enemy too well defended (iberians) + continue; + if (enemyCount[i] > max) + { + this.targetPlayer = +i; + max = enemyCount[i]; + } + } + } + if (this.targetPlayer === undefined || this.targetPlayer === -1) + { + this.failed = true; + return false; + } + + var CCs = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")); + if (CCs.length === 0) + { + this.failed = true; + return false; + } + + this.type = type || "normal"; + this.targetFinder = targetFinder || this.defaultTargetFinder; + + this.name = uniqueID; + + this.paused = false; + this.completingTurn = 0; + this.onArrivalReaction = "proceedOnTargets"; + + // priority of the queues we'll create. + var priority = 70; + + // priority is relative. If all are 0, the only relevant criteria is "currentsize/targetsize". + // if not, this is a "bonus". The higher the priority, the faster this unit will get built. + // Should really be clamped to [0.1-1.5] (assuming 1 is default/the norm) + // Eg: if all are priority 1, and the siege is 0.5, the siege units will get built + // only once every other category is at least 50% of its target size. + // note: siege build order is currently added by the military manager if a fortress is there. + this.unitStat = {}; + + if (type === "Rush") + { + priority = 250; + this.unitStat["Infantry"] = { "priority": 1, "minSize": 10, "targetSize": 26, "batchSize": 2, "classes": ["Infantry"], "interests": [ ["strength",1], ["cost",1], ["costsResource", 0.5, "stone"], ["costsResource", 0.6, "metal"] ] }; + } + else if (type === "Raid") + { + priority = 300; + this.unitStat["Cavalry"] = { "priority": 1, "minSize": 3, "targetSize": 6, "batchSize": 2, "classes": ["Cavalry", "CitizenSoldier"], "interests": [ ["strength",1], ["cost",1] ] }; + } + else if (type === "superSized") + { + priority = 90; + // basically we want a mix of citizen soldiers so our barracks have a purpose, and champion units. + this.unitStat["RangedInfantry"] = { "priority": 0.7, "minSize": 5, "targetSize": 15, "batchSize": 5, "classes": ["Infantry","Ranged", "CitizenSoldier"], "interests": [["strength",3], ["cost",1] ] }; + this.unitStat["MeleeInfantry"] = { "priority": 0.7, "minSize": 5, "targetSize": 15, "batchSize": 5, "classes": ["Infantry","Melee", "CitizenSoldier" ], "interests": [ ["strength",3], ["cost",1] ] }; + this.unitStat["ChampRangedInfantry"] = { "priority": 1, "minSize": 5, "targetSize": 25, "batchSize": 5, "classes": ["Infantry","Ranged", "Champion"], "interests": [["strength",3], ["cost",1] ] }; + this.unitStat["ChampMeleeInfantry"] = { "priority": 1, "minSize": 5, "targetSize": 20, "batchSize": 5, "classes": ["Infantry","Melee", "Champion" ], "interests": [ ["strength",3], ["cost",1] ] }; + this.unitStat["MeleeCavalry"] = { "priority": 0.7, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["Cavalry","Melee", "CitizenSoldier" ], "interests": [ ["strength",2], ["cost",1] ] }; + this.unitStat["RangedCavalry"] = { "priority": 0.7, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["Cavalry","Ranged", "CitizenSoldier"], "interests": [ ["strength",2], ["cost",1] ] }; + this.unitStat["ChampMeleeInfantry"] = { "priority": 1, "minSize": 3, "targetSize": 18, "batchSize": 3, "classes": ["Infantry","Melee", "Champion" ], "interests": [ ["strength",3], ["cost",1] ] }; + this.unitStat["ChampMeleeCavalry"] = { "priority": 1, "minSize": 3, "targetSize": 18, "batchSize": 3, "classes": ["Cavalry","Melee", "Champion" ], "interests": [ ["strength",2], ["cost",1] ] }; + } + else + { + priority = 70; + this.unitStat["RangedInfantry"] = { "priority": 1, "minSize": 6, "targetSize": 18, "batchSize": 3, "classes": ["Infantry","Ranged"], "interests": [ ["canGather", 1], ["strength",1.6], ["cost",1.5], ["costsResource", 0.3, "stone"], ["costsResource", 0.3, "metal"] ] }; + this.unitStat["MeleeInfantry"] = { "priority": 1, "minSize": 6, "targetSize": 18, "batchSize": 3, "classes": ["Infantry","Melee"], "interests": [ ["canGather", 1], ["strength",1.6], ["cost",1.5], ["costsResource", 0.3, "stone"], ["costsResource", 0.3, "metal"] ] }; + } + + // TODO: there should probably be one queue per type of training building + gameState.ai.queueManager.addQueue("plan_" + this.name, priority); + this.queue = gameState.ai.queues["plan_" + this.name]; + gameState.ai.queueManager.addQueue("plan_" + this.name +"_champ", priority+1); + this.queueChamp = gameState.ai.queues["plan_" + this.name +"_champ"]; + /* + this.unitStat["Siege"]["filter"] = function (ent) { + var strength = [ent.attackStrengths("Melee")["crush"],ent.attackStrengths("Ranged")["crush"]]; + return (strength[0] > 15 || strength[1] > 15); + };*/ + + var filter = API3.Filters.and(API3.Filters.byMetadata(PlayerID, "plan", this.name), API3.Filters.byOwner(PlayerID)); + this.unitCollection = gameState.getOwnUnits().filter(filter); + this.unitCollection.registerUpdates(); + + this.unit = {}; + + // each array is [ratio, [associated classes], associated EntityColl, associated unitStat, name ] + this.buildOrder = []; + + // defining the entity collections. Will look for units I own, that are part of this plan. + // Also defining the buildOrders. + for (var unitCat in this.unitStat) { + var cat = unitCat; + var Unit = this.unitStat[cat]; + + filter = API3.Filters.and(API3.Filters.byClassesAnd(Unit["classes"]),API3.Filters.and(API3.Filters.byMetadata(PlayerID, "plan",this.name),API3.Filters.byOwner(PlayerID))); + this.unit[cat] = gameState.getOwnUnits().filter(filter); + this.unit[cat].registerUpdates(); + this.buildOrder.push([0, Unit["classes"], this.unit[cat], Unit, cat]); + } + + var myFortresses = gameState.getOwnTrainingFacilities().filter(API3.Filters.byClass("GarrisonFortress")); + if (myFortresses.length !== 0) + { + // make this our rallypoint + for (var i in myFortresses._entities) + { + if (myFortresses._entities[i].position()) + { + this.rallyPoint = myFortresses._entities[i].position(); + break; + } + } + } + else + { + + if(gameState.ai.pathsToMe.length > 1) + var position = [(gameState.ai.pathsToMe[0][0]+gameState.ai.pathsToMe[1][0])/2.0,(gameState.ai.pathsToMe[0][1]+gameState.ai.pathsToMe[1][1])/2.0]; + else if (gameState.ai.pathsToMe.length !== 0) + var position = [gameState.ai.pathsToMe[0][0],gameState.ai.pathsToMe[0][1]]; + else + var position = [-1,-1]; + + if (gameState.ai.accessibility.getAccessValue(position) !== gameState.ai.myIndex) + var position = [-1,-1]; + + var nearestCCArray = CCs.filterNearest(position, 1).toEntityArray(); + var CCpos = nearestCCArray[0].position(); + this.rallyPoint = [0,0]; + if (position[0] !== -1) + { + this.rallyPoint[0] = position[0]; + this.rallyPoint[1] = position[1]; + } + else + { + this.rallyPoint[0] = CCpos[0]; + this.rallyPoint[1] = CCpos[1]; + } + } + + // some variables for during the attack + this.position5TurnsAgo = [0,0]; + this.lastPosition = [0,0]; + this.position = [0,0]; + + this.assignUnits(gameState); + + // get a good path to an estimated target. + this.pathFinder = new API3.aStarPath(gameState, false, false, this.targetPlayer); + //Engine.DumpImage("widthmap.png", this.pathFinder.widthMap, this.pathFinder.width,this.pathFinder.height,255); + + this.pathWidth = 6; // prefer a path far from entities. This will avoid units getting stuck in trees and also results in less straight paths. + this.pathSampling = 2; + this.onBoat = false; // tells us if our units are loaded on boats. + this.needsShip = false; + + return true; +}; + +m.AttackPlan.prototype.getName = function() +{ + return this.name; +}; + +m.AttackPlan.prototype.getType = function() +{ + return this.type; +}; + +m.AttackPlan.prototype.isStarted = function() +{ + return !(this.state === "unexecuted" || this.state === "completing"); +}; + +m.AttackPlan.prototype.isPaused = function() +{ + return this.paused; +}; + +m.AttackPlan.prototype.setPaused = function(boolValue) +{ + this.paused = boolValue; +}; + +// Returns true if the attack can be executed at the current time +// Basically it checks we have enough units. +m.AttackPlan.prototype.canStart = function(gameState) +{ + for (var unitCat in this.unitStat) + { + var Unit = this.unitStat[unitCat]; + if (this.unit[unitCat].length < Unit["minSize"]) + return false; + } + return true; +}; + +m.AttackPlan.prototype.mustStart = function(gameState) +{ + if (this.isPaused() || this.path === undefined) + return false; + var MaxReachedEverywhere = true; + var MinReachedEverywhere = true; + for (var unitCat in this.unitStat) + { + var Unit = this.unitStat[unitCat]; + if (this.unit[unitCat].length < Unit["targetSize"]) + MaxReachedEverywhere = false; + if (this.unit[unitCat].length < Unit["minSize"]) + { + MinReachedEverywhere = false; + break; + } + } + + if (MaxReachedEverywhere) + return true; + if (MinReachedEverywhere) + { + if ((gameState.getPopulationMax() - gameState.getPopulation() < 10) || + (this.type === "Raid" && this.target && this.target.foundationProgress() && this.target.foundationProgress() > 60)) + return true; + } + return false; +}; + +// Adds a build order. If resetQueue is true, this will reset the queue. +m.AttackPlan.prototype.addBuildOrder = function(gameState, name, unitStats, resetQueue) +{ + if (!this.isStarted()) + { + // no minsize as we don't want the plan to fail at the last minute though. + this.unitStat[name] = unitStats; + var Unit = this.unitStat[name]; + var filter = API3.Filters.and(API3.Filters.byClassesAnd(Unit["classes"]),API3.Filters.and(API3.Filters.byMetadata(PlayerID, "plan",this.name),API3.Filters.byOwner(PlayerID))); + this.unit[name] = gameState.getOwnUnits().filter(filter); + this.unit[name].registerUpdates(); + this.buildOrder.push([0, Unit["classes"], this.unit[name], Unit, name]); + if (resetQueue) + { + this.queue.empty(); + this.queueChamp.empty(); + } + } +}; + +m.AttackPlan.prototype.addSiegeUnits = function(gameState) +{ + if (this.unitStat["Siege"] || this.state !== "unexecuted") + return false; + // no minsize as we don't want the plan to fail at the last minute though. + var stat = { "priority": 1., "minSize": 0, "targetSize": 4, "batchSize": 2, "classes": ["Siege"], + "interests": [ ["siegeStrength", 3], ["cost",1] ] }; + if (gameState.civ() === "maur") + stat["classes"] = ["Elephant", "Champion"]; + this.addBuildOrder(gameState, "Siege", stat, true); + return true; +}; + +// Three returns possible: 1 is "keep going", 0 is "failed plan", 2 is "start" +// 3 is a special case: no valid path returned. Right now I stop attacking alltogether. +m.AttackPlan.prototype.updatePreparation = function(gameState, events) +{ + // the completing step is used to return resources and regroup the units + // so we check that we have no more forced order before starting the attack + if (this.state === "completing") + { + // bloqued units which cannot finish their order should not stop the attack + if (this.completingTurn + 50 < gameState.ai.playedTurn && this.hasForceOrder()) + return 1; + return 2; + } + + if (this.Config.debug > 2 && gameState.ai.playedTurn % 10 === 0) + this.debugAttack(); + + var self = this; + + if (this.path == undefined || this.target == undefined || this.path === "toBeContinued") { + // find our target + if (this.target == undefined) + { + var targets = undefined; + if (this.type === "Rush") + var targets = this.rushTargetFinder(gameState); + else if (this.type === "Raid") + var targets = this.raidTargetFinder(gameState); + if (!targets || targets.length === 0) + { + if (this.type !== "Raid") + var targets = this.targetFinder(gameState); + else + return 0; + } + if (targets.length === 0) + targets = this.defaultTargetFinder(gameState); + + if (targets.length !== 0) { + // picking a target + var maxDist = -1; + var index = 0; + for (var i in targets._entities) + { + // we're sure it has a position as TargetFinder already checked that. + var dist = API3.SquareVectorDistance(targets._entities[i].position(), this.rallyPoint); + if (dist < maxDist || maxDist === -1) + { + maxDist = dist; + index = i; + } + } + this.target = targets._entities[index]; + this.targetPos = this.target.position(); + } + } + // when we have a target, we path to it. + // I'd like a good high width sampling first. + // Thus I will not do everything at once. + // It will probably carry over a few turns but that's no issue. + if (this.path === undefined) + this.path = this.pathFinder.getPath(this.rallyPoint, this.targetPos, this.pathSampling, this.pathWidth, 175); + else if (this.path === "toBeContinued") + this.path = this.pathFinder.continuePath(); + + if (this.path === undefined) { + if (this.pathWidth == 6) + { + this.pathWidth = 2; + delete this.path; + } else { + delete this.pathFinder; + return 3; // no path. + } + } else if (this.path === "toBeContinued") { + // carry on. + } else if (this.path[1] === true && this.pathWidth == 2) { + // okay so we need a ship. + // Basically we'll add it as a new class to train compulsorily, and we'll recompute our path. + if (!gameState.ai.HQ.waterMap) + { + gameState.ai.HQ.waterMap = true; + return 0; + } + this.needsShip = true; + this.pathWidth = 3; + this.pathSampling = 3; + this.path = this.path[0].reverse(); + delete this.pathFinder; + // Change the rally point to something useful (should avoid rams getting stuck in houses in my territory, which is dumb.) + for (var i = 0; i < this.path.length; ++i) + { + // my pathfinder returns arrays in arrays in arrays. + var waypointPos = this.path[i][0]; + var territory = m.createTerritoryMap(gameState); + if (territory.getOwner(waypointPos) !== PlayerID || this.path[i][1] === true) + { + // if we're suddenly out of our territory or this is the point where we change transportation method. + if (i !== 0) + this.rallyPoint = this.path[i-1][0]; + else + this.rallyPoint = this.path[0][0]; + if (i >= 1) + this.path.splice(0,i-1); + break; + } + } + } else if (this.path[1] === true && this.pathWidth == 6) { + // retry with a smaller pathwidth: + this.pathWidth = 2; + delete this.path; + } else { + this.path = this.path[0].reverse(); + delete this.pathFinder; + + // Change the rally point to something useful (should avoid rams getting stuck in houses in my territory, which is dumb.) + for (var i = 0; i < this.path.length; ++i) + { + // my pathfinder returns arrays in arrays in arrays. + var waypointPos = this.path[i][0]; + var territory = m.createTerritoryMap(gameState); + if (territory.getOwner(waypointPos) !== PlayerID || this.path[i][1] === true) + { + // if we're suddenly out of our territory or this is the point where we change transportation method. + if (i !== 0) + { + this.rallyPoint = this.path[i-1][0]; + } else + this.rallyPoint = this.path[0][0]; + if (i >= 1) + this.path.splice(0,i-1); + break; + } + } + } + } + + Engine.ProfileStart("Update Preparation"); + + if (this.Config.debug && gameState.getPopulationMax() - gameState.getPopulation() < 10) + { + warn(" >>> max pop >>> should start ! mustStart " + this.mustStart(gameState) + " canStart " + this.canStart()); + for (var unitCat in this.unitStat) + { + var marker = " "; + if (this.unit[unitCat].length < this.unitStat[unitCat]["minSize"]) + marker = "###"; + warn(marker + " >>> " + unitCat + " needs " + this.unitStat[unitCat]["minSize"] + " but have " + this.unit[unitCat].length); + } + } + + // special case: if we're reached max pop, and we can start the plan, start it. + if ((gameState.getPopulationMax() - gameState.getPopulation() < 10) && this.canStart()) + { + this.assignUnits(gameState); + this.queue.empty(); + this.queueChamp.empty(); + } + else if (this.mustStart(gameState) && (gameState.countOwnQueuedEntitiesWithMetadata("plan", +this.name) > 0)) + { + // keep on while the units finish being trained, then we'll start + this.assignUnits(gameState); + this.queue.empty(); + this.queueChamp.empty(); + + Engine.ProfileStop(); + return 1; + } + else if (!this.mustStart(gameState)) + { + // We still have time left to recruit units and do stuffs. + + this.assignUnits(gameState); + + // let's sort by training advancement, ie 'current size / target size' + // count the number of queued units too. + // substract priority. + for (var i = 0; i < this.buildOrder.length; ++i) + { + var special = "Plan_" + self.name + "_" + this.buildOrder[i][4]; + var aQueued = gameState.countOwnQueuedEntitiesWithMetadata("special", special); + aQueued += self.queue.countQueuedUnitsWithMetadata("special", special); + aQueued += self.queueChamp.countQueuedUnitsWithMetadata("special", special); + this.buildOrder[i][0] = this.buildOrder[i][2].length + aQueued; + } + this.buildOrder.sort(function (a,b) { + var va = a[0]/a[3]["targetSize"] - a[3]["priority"]; + if (a[0] >= a[3]["targetSize"]) + va += 1000; + var vb = b[0]/b[3]["targetSize"] - b[3]["priority"]; + if (b[0] >= b[3]["targetSize"]) + vb += 1000; + return va - vb; + }); + + if (this.Config.debug && gameState.getTimeElapsed() > this.debugTime + 60000) + { + warn("===================================="); + warn("======== build order for plan " + this.name); + for each (var order in this.buildOrder) + { + var specialData = "Plan_"+this.name+"_"+order[4]; + var inTraining = gameState.countOwnQueuedEntitiesWithMetadata("special", specialData); + var queue1 = this.queue.countQueuedUnitsWithMetadata("special", specialData); + var queue2 = this.queueChamp.countQueuedUnitsWithMetadata("special", specialData); + warn(" >>> " + order[4] + " done " + order[2].length + " training " + inTraining + " queue " + queue1 + " champ " + queue2 + " >> need " + order[3].targetSize); + } + warn("------------------------------------"); + gameState.ai.queueManager.printQueues(gameState); + warn("===================================="); + warn("===================================="); + } + + Engine.ProfileStart("Creating units."); + + if (this.buildOrder[0][0] < this.buildOrder[0][3]["targetSize"]) + { + // find the actual queue we want + var queue = this.queue; + if (this.buildOrder[0][3]["classes"].indexOf("Champion") !== -1) + queue = this.queueChamp; + + if (queue.length() <= 5) + { + var template = gameState.ai.HQ.findBestTrainableUnit(gameState, this.buildOrder[0][1], this.buildOrder[0][3]["interests"] ); + // HACK (TODO replace) : if we have no trainable template... Then we'll simply remove the buildOrder, + // effectively removing the unit from the plan. + if (template === undefined) + { + // TODO: this is a complete hack. + delete this.unitStat[this.buildOrder[0][4]]; // deleting the associated unitstat. + this.buildOrder.splice(0,1); + } + else + { + var max = this.buildOrder[0][3]["batchSize"]; + var specialData = "Plan_" + this.name + "_" + this.buildOrder[0][4]; + if (gameState.getTemplate(template).hasClass("CitizenSoldier")) + queue.addItem( new m.TrainingPlan(gameState, template, { "role": "worker", "plan": this.name, "special": specialData, "base": 0 }, max, max) ); + else + queue.addItem( new m.TrainingPlan(gameState, template, { "role": "attack", "plan": this.name, "special": specialData, "base": 0 }, max, max) ); + } + } + } + + if (this.Config.debug && gameState.getTimeElapsed() > this.debugTime + 60000) + this.debugTime = gameState.getTimeElapsed(); + + Engine.ProfileStop(); + Engine.ProfileStop(); + // can happen for now + if (this.buildOrder.length === 0) + { + if (this.Config.debug) + warn("Ending plan: no build orders"); + return 0; // will abort the plan, should return something else + } + return 1; + } + + this.unitCollection.forEach(function (entity) { entity.setMetadata(PlayerID, "role", "attack"); }); + + Engine.ProfileStop(); + // if we're here, it means we must start (and have no units in training left). + // if we can, do, else, abort. + if (this.canStart(gameState)) + { + this.state = "completing"; + this.completingTurn = gameState.ai.playedTurn; + this.unitCollection.forEach(function (entity) { entity.setMetadata(PlayerID, "subrole", "completing"); }); +// this.position = this.unitCollection.getCentrePosition(); +// var nearestCC = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).filterNearest(this.position, 1).toEntityArray(); +// if (nearestCC.length === 0) +// return 0; +// this.unitCollection.forEach(function (ent) { ent.returnResources(nearestCC[0]); }); // units without resources will rally there + this.AllToRallyPoint(gameState); + + // reset all queued units + var plan = this.name; + gameState.ai.queueManager.removeQueue("plan_" + plan); + gameState.ai.queueManager.removeQueue("plan_" + plan + "_champ"); + return 1; + } + return 0; +}; + +m.AttackPlan.prototype.assignUnits = function(gameState) +{ + var plan = this.name; + + // TODO: assign myself units that fit only, right now I'm getting anything. + // Assign all no-roles that fit (after a plan aborts, for example). + if (this.type === "Raid") + { + var candidates = gameState.getOwnUnits().filter(API3.Filters.byClass(["Cavalry"])); + var num = 0; + candidates.forEach(function(ent) { + if (ent.getMetadata(PlayerID, "plan") !== undefined) + return; + if (num++ > 1) + ent.setMetadata(PlayerID, "plan", plan); + }); + return; + } + + var noRole = gameState.getOwnEntitiesByRole(undefined, false).filter(API3.Filters.byClass(["Unit"])); + noRole.forEach(function(ent) { + if (ent.hasClass("Support") || ent.attackTypes() === undefined || ent.getMetadata(PlayerID, "plan") !== undefined) + return; + ent.setMetadata(PlayerID, "plan", plan); + }); + if (this.type !== "Rush") + return; + // For a rush, assign also workers (but keep a minimum number of defenders) + var worker = gameState.getOwnEntitiesByRole("worker", true).filter(API3.Filters.byClass(["Unit"])); + var num = 0; + worker.forEach(function(ent) { + if (ent.hasClass("Support") || ent.attackTypes() === undefined || ent.getMetadata(PlayerID, "plan") !== undefined) + return; + if (num++ > 8) + ent.setMetadata(PlayerID, "plan", plan); + }); +}; + +// this sends all units to the "rally point" by entity collections. +// If units are carrying resources, they return it before. +m.AttackPlan.prototype.AllToRallyPoint = function(gameState) +{ + var rallyPoint = this.rallyPoint; + this.unitCollection.forEach(function (ent) { + var queued = false; + if (ent.resourceCarrying() && ent.resourceCarrying().length) + { + if (!ent.getMetadata(PlayerID, "worker-object")) + ent.setMetadata(PlayerID, "worker-object", new m.Worker(ent)); + queued = ent.getMetadata(PlayerID, "worker-object").returnResources(gameState); + } + ent.move(rallyPoint[0], rallyPoint[1], queued); + }); +}; + +// Default target finder aims for conquest critical targets +m.AttackPlan.prototype.defaultTargetFinder = function(gameState) +{ + var targets = gameState.getEnemyStructures(this.targetPlayer).filter(API3.Filters.byClass("CivCentre")); + if (targets.length == 0) + targets = gameState.getEnemyStructures(this.targetPlayer).filter(API3.Filters.byClass("ConquestCritical")); + // If there's nothing, attack anything else that's less critical + if (targets.length == 0) + targets = gameState.getEnemyStructures(this.targetPlayer).filter(API3.Filters.byClass("Town")); + if (targets.length == 0) + targets = gameState.getEnemyStructures(this.targetPlayer).filter(API3.Filters.byClass("Village")); + // no buildings, attack anything conquest critical, even units (it's assuming it won't move). + if (targets.length == 0) + targets = gameState.getEnemyEntities(this.targetPlayer).filter(API3.Filters.byClass("ConquestCritical")); + return targets; +}; + +// Rush target finder aims at isolated non-defended buildings +m.AttackPlan.prototype.rushTargetFinder = function(gameState) +{ + var targets = new API3.EntityCollection(gameState.sharedScript); + var buildings = gameState.getEnemyStructures().toEntityArray(); + if (buildings.length === 0) + return targets; + + this.position = this.unitCollection.getCentrePosition(); + if (!this.position) + { + warn(" no position for rushTargetFinder " + this.unitCollection.length); + var ourCC = gameState.getEnemyStructures().filter(API3.Filters.byClass("CivCentre")).toEntityArray(); + this.position = ourCC[0].position(); + } + + var minDist = Math.min(); + var target = undefined; + for each (var building in buildings) + { + if (building.owner() === 0) + continue; + // TODO check on Arrow count + if (building.hasClass("CivCentre") || building.hasClass("Tower") || building.hasClass("Fortress")) + continue; + var pos = building.position(); + var defended = false; + for each (var defense in buildings) + { + if (!defense.hasClass("CivCentre") && !defense.hasClass("Tower") && !defense.hasClass("Fortress")) + continue; + var dist = API3.SquareVectorDistance(pos, defense.position()); + if (dist < 4900) // TODO check on defense range rather than this fixed 80*80 + { + defended = true; + break; + } + } + if (defended) + continue; + var dist = API3.SquareVectorDistance(pos, this.position); + if (dist < minDist) + target = building; + } + if (target) + targets.addEnt(target); + return targets; +}; + +// Raid target finder aims at isolated non-defended units or foundations +m.AttackPlan.prototype.raidTargetFinder = function(gameState) +{ + return gameState.getEnemyStructures().filter(API3.Filters.and(API3.Filters.byClass("CivCentre"), API3.Filters.isFoundation())); +}; + +// Raid target finder aims at isolated non-defended units or foundations +m.AttackPlan.prototype.oldraidTargetFinder = function(gameState) +{ + var targets = new API3.EntityCollection(gameState.sharedScript); + var buildings = gameState.getEnemyStructures().toEntityArray(); + if (buildings.length === 0) + return targets; + + this.position = this.unitCollection.getCentrePosition(); + + var minDist = Math.min(); + var target = undefined; + for each (var building in buildings) + { + if (building.owner() === 0) + continue; + if (building.foundationProgress() === undefined) + continue; + var pos = building.position(); + var defended = false; + for each (var defense in buildings) + { + if (!defense.hasClass("CivCentre") && !defense.hasClass("Tower") && !defense.hasClass("Fortress")) + continue; + var dist = API3.SquareVectorDistance(pos, defense.position()); + if (dist < 4900) // TODO check on defense range rather than this fixed 80*80 + { + defended = true; + break; + } + } + if (defended) + continue; + var dist = API3.SquareVectorDistance(pos, this.position); + if (dist < minDist) + target = building; + } + if (target) + { + targets.addEnt(target); + return targets; + } + + var units = gameState.getEnemyUnits().toEntityArray(); + if (units.length === 0) + return targets; + + var minDist = Math.min(); + var target = undefined; + for each (var unit in units) + { + if (unit.owner() === 0) + continue; + var pos = unit.position(); + var defended = false; + for each (var defense in buildings) + { + if (!defense.hasClass("CivCentre") && !defense.hasClass("Tower") && !defense.hasClass("Fortress")) + continue; + var dist = API3.SquareVectorDistance(pos, defense.position()); + if (dist < 4900) // TODO check on defense range rather than this fixed 80*80 + { + defended = true; + break; + } + } + if (defended) + continue; + var dist = API3.SquareVectorDistance(pos, this.position); + if (dist < minDist) + target = unit; + } + if (target) + targets.addEnt(target); + + return targets; +}; + +// Executes the attack plan, after this is executed the update function will be run every turn +// If we're here, it's because we have in our IDlist enough units. +// now the IDlist units are treated turn by turn +m.AttackPlan.prototype.StartAttack = function(gameState) +{ + if (this.Config.debug) + warn("start attack " + this.name + " with type " + this.type); + + if (this.type === "Raid") + { + var targets = this.raidTargetFinder(gameState); + if (targets.length !== 0) + { + for (var i in targets._entities) + this.target = targets._entities[i]; + this.targetPos = this.target.position(); + } + else + return false; + if (this.Config.debug > 0) + warn("startAttack du Raid avec target foundation " + this.target.foundationProgress() + " and path " + this.path); + } + + // check we have a target and a path. + if (this.targetPos && this.path !== undefined) + { + // erase our queue. This will stop any leftover unit from being trained. + gameState.ai.queueManager.removeQueue("plan_" + this.name); + gameState.ai.queueManager.removeQueue("plan_" + this.name + "_champ"); + + var curPos = this.unitCollection.getCentrePosition(); + + this.unitCollection.forEach(function(ent) { ent.setMetadata(PlayerID, "subrole", "walking"); ent.setMetadata(PlayerID, "role", "attack") ;}); + // optimize our collection now. + this.unitCollection.allowQuickIter(); + + this.unitCollection.move(this.path[0][0][0], this.path[0][0][1]); + this.unitCollection.setStance("aggressive"); + this.unitCollection.filter(API3.Filters.byClass("Siege")).setStance("defensive"); + + this.state = "walking"; + } + else + { + gameState.ai.gameFinished = true; + m.debug ("I do not have any target. So I'll just assume I won the game."); + return false; + } + return true; +}; + +// Runs every turn after the attack is executed +m.AttackPlan.prototype.update = function(gameState, events) +{ + if (this.unitCollection.length === 0) + return 0; + + // we're marching towards the target + // Check for attacked units in our band. + // raids don't care about attacks much + + Engine.ProfileStart("Update Attack"); + + this.position = this.unitCollection.getCentrePosition(); + var IDs = this.unitCollection.toIdArray(); + + var self = this; + + // this actually doesn't do anything right now. + if (this.state === "walking") + { + // Let's check if any of our unit has been attacked. In case yes, we'll determine if we're simply off against an enemy army, a lone unit/builing + // or if we reached the enemy base. Different plans may react differently. + var attackedNB = 0; + var attackedEvents = events["Attacked"]; + for (var key in attackedEvents) + { + var e = attackedEvents[key]; + if (IDs.indexOf(e.target) === -1) + continue; + var attacker = gameState.getEntityById(e.attacker); + var ourUnit = gameState.getEntityById(e.target); + + if (attacker && attacker.position() && attacker.hasClass("Unit") && attacker.owner() != 0) + attackedNB++; + // if we're being attacked by a building, flee. + if (attacker && ourUnit && attacker.hasClass("Structure")) + ourUnit.flee(attacker); + } + var territoryMap = m.createTerritoryMap(gameState); + if ((territoryMap.getOwner(this.position) === this.targetPlayer && attackedNB > 1) || attackedNB > 4) + { + // we must assume we've arrived at the end of the trail. + m.debug ("Attack Plan " +this.type +" " +this.name +" has arrived to destination."); + this.state = "arrived"; + } + } + + if (this.state === "walking"){ + + this.position = this.unitCollection.getCentrePosition(); + + // probably not too good. + if (!this.position) { + Engine.ProfileStop(); + return undefined; // should spawn an error. + } + + // basically haven't moved an inch: very likely stuck) + if (API3.SquareVectorDistance(this.position, this.position5TurnsAgo) < 10 && this.path.length > 0 && gameState.ai.playedTurn % 5 === 0) { + // check for stuck siege units + var sieges = this.unitCollection.filter(API3.Filters.byClass("Siege")); + var farthest = 0; + var farthestEnt = -1; + sieges.forEach (function (ent) { + if (API3.SquareVectorDistance(ent.position(),self.position) > farthest) + { + farthest = API3.SquareVectorDistance(ent.position(),self.position); + farthestEnt = ent; + } + }); + if (farthestEnt !== -1) + farthestEnt.destroy(); + } + if (gameState.ai.playedTurn % 5 === 0) + this.position5TurnsAgo = this.position; + + if (this.lastPosition && API3.SquareVectorDistance(this.position, this.lastPosition) < 20 && this.path.length > 0) { + this.unitCollection.moveIndiv(this.path[0][0][0], this.path[0][0][1]); + // We're stuck, presumably. Check if there are no walls just close to us. If so, we're arrived, and we're gonna tear down some serious stone. + var walls = gameState.getEnemyEntities().filter(API3.Filters.and(API3.Filters.byOwner(this.targetPlayer), API3.Filters.byClass("StoneWall"))); + var nexttoWalls = false; + walls.forEach( function (ent) { + if (!nexttoWalls && API3.SquareVectorDistance(self.position, ent.position()) < 800) + nexttoWalls = true; + }); + // there are walls but we can attack + if (nexttoWalls && this.unitCollection.filter(API3.Filters.byCanAttack("StoneWall")).length !== 0) + { + m.debug ("Attack Plan " +this.type +" " +this.name +" has met walls and is not happy."); + this.state = "arrived"; + } else if (nexttoWalls) { + // abort plan. + m.debug ("Attack Plan " +this.type +" " +this.name +" has met walls and gives up."); + Engine.ProfileStop(); + return 0; + } + } + + // check if our land units are close enough from the next waypoint. + if (API3.SquareVectorDistance(this.position, this.targetPos) < 9000 || + API3.SquareVectorDistance(this.position, this.path[0][0]) < 650) { + if (this.unitCollection.filter(API3.Filters.byClass("Siege")).length !== 0 + && API3.SquareVectorDistance(this.position, this.targetPos) >= 9000 + && API3.SquareVectorDistance(this.unitCollection.filter(API3.Filters.byClass("Siege")).getCentrePosition(), this.path[0][0]) >= 650) + { + } else { + // okay so here basically two cases. First case is "we've arrived" + // Second case is "either we need a boat, or we need to unload" + if (this.path[0][1] !== true) + { + this.path.shift(); + if (this.path.length > 0){ + this.unitCollection.move(this.path[0][0][0], this.path[0][0][1]); + } else { + m.debug ("Attack Plan " +this.type +" " +this.name +" has arrived to destination."); + // we must assume we've arrived at the end of the trail. + this.state = "arrived"; + } + } else + { + // TODO: make this require an escort later on. + this.path.shift(); + if (this.path.length === 0) { + m.debug ("Attack Plan " +this.type +" " +this.name +" has arrived to destination."); + // we must assume we've arrived at the end of the trail. + this.state = "arrived"; + } else { + /* + var plan = new m.TransportPlan(gameState, this.unitCollection.toIdArray(), this.path[0][0], false); + this.tpPlanID = plan.ID; + gameState.ai.HQ.navalManager.transportPlans.push(plan); + m.debug ("Transporting over sea"); + this.state = "transporting"; + */ + // TODO: fix this above + //right now we'll abort. + Engine.ProfileStop(); + return 0; + } + } + } + } + } else if (this.state === "transporting") { + // check that we haven't finished transporting, ie the plan + if (!gameState.ai.HQ.navalManager.checkActivePlan(this.tpPlanID)) + { + this.state = "walking"; + } + } + + + // todo: re-implement raiding + if (this.state === "arrived") + { + // let's proceed on with whatever happens now. + // There's a ton of TODOs on this part. + if (this.onArrivalReaction == "proceedOnTargets") + { + this.state = ""; + this.unitCollection.forEach( function (ent) { + ent.stopMoving(); + ent.setMetadata(PlayerID, "subrole", "attacking"); + }); + if (this.type === "Rush") // try to find a better target for rush + { + var targets = this.rushTargetFinder(gameState); + if (targets.length !== 0) + { + for (var i in targets._entities) + this.target = targets._entities[i]; + this.targetPos = this.target.position(); + } + } + } + } + + // basic state of attacking. + if (this.state === "") + { + + // events watch: if siege units are attacked, we'll send some units to deal with enemies. + var attackedEvents = events["Attacked"]; + for (var key in attackedEvents) { + var e = attackedEvents[key]; + if (IDs.indexOf(e.target) === -1) + continue; + var attacker = gameState.getEntityById(e.attacker); + if (!attacker || !attacker.position() || !attacker.hasClass("Unit")) + continue; + var ourUnit = gameState.getEntityById(e.target); + if (!ourUnit.hasClass("Siege")) + continue; + var collec = this.unitCollection.filter(API3.Filters.not(API3.Filters.byClass("Siege"))).filterNearest(ourUnit.position(), 5); + if (collec.length === 0) + continue; + collec.attack(attacker.id()) + } + + var enemyUnits = gameState.getEnemyUnits(this.targetPlayer); + var enemyStructures = gameState.getEnemyStructures(this.targetPlayer); + + if (this.unitCollUpdateArray === undefined || this.unitCollUpdateArray.length === 0) + { + this.unitCollUpdateArray = this.unitCollection.toIdArray(); + } + else + { + // some stuffs for locality and speed + var territoryMap = m.createTerritoryMap(gameState); + var timeElapsed = gameState.getTimeElapsed(); + + // Let's check a few units each time we update. Currently 10 + var lgth = Math.min(this.unitCollUpdateArray.length, 10); + for (var check = 0; check < lgth; check++) + { + var ent = gameState.getEntityById(this.unitCollUpdateArray[check]); + if (!ent) + continue; + + var orderData = ent.unitAIOrderData(); + if (orderData.length !== 0) + orderData = orderData[0]; + else + orderData = undefined; + + // update the order if needed + var needsUpdate = false; + var maybeUpdate = false; + if (ent.isIdle()) + needsUpdate = true; + else if (ent.hasClass("Siege") && orderData && orderData["target"]) + { + var target = gameState.getEntityById(orderData["target"]); + if (!target) + needsUpdate = true; + else if(!target.hasClass("Structure")) + maybeUpdate = true; + } + else if (!ent.hasClass("Cavalry") && !ent.hasClass("Ranged") && orderData && orderData["target"]) + { + var target = gameState.getEntityById(orderData["target"]); + if (!target) + needsUpdate = true; + else if (target.hasClass("Female") && target.unitAIState().split(".")[1] == "FLEEING") + maybeUpdate = true; + } + + // don't update too soon if not necessary + if (!needsUpdate) + { + if (!maybeUpdate) + continue; + var lastAttackPlanUpdateTime = ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime"); + if (lastAttackPlanUpdateTime && (timeElapsed - lastAttackPlanUpdateTime) < 5000) + continue; + } + ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", timeElapsed); + + // let's filter targets further based on this unit. + var mStruct = enemyStructures.filter(function (enemy) { //}){ + if (!enemy.position() || (enemy.hasClass("StoneWall") && ent.canAttackClass("StoneWall"))) { + return false; + } + if (API3.SquareVectorDistance(enemy.position(),ent.position()) > 3000) { + return false; + } + return true; + }); + var nearby = (!ent.hasClass("Cavalry") && !ent.hasClass("Ranged")); + var mUnit = enemyUnits.filter(function (enemy) { + if (!enemy.position()) + return false; + if (nearby && enemy.hasClass("Female") && enemy.unitAIState().split(".")[1] == "FLEEING") + return false; + var dist = API3.SquareVectorDistance(enemy.position(),ent.position()); + if (dist > 10000) + return false; + if (nearby && dist > 3600) + return false; + return true; + }); + // Checking for gates if we're a siege unit. + var isGate = false; + mUnit = mUnit.toEntityArray(); + mStruct = mStruct.toEntityArray(); + if (ent.hasClass("Siege")) { + mStruct.sort(function (structa,structb) { + var vala = structa.costSum(); + if (structa.hasClass("Gates") && ent.canAttackClass("StoneWall")) + { + isGate = true; + vala += 10000; + } + else if (structa.hasClass("ConquestCritical")) + vala += 200; + var valb = structb.costSum(); + if (structb.hasClass("Gates") && ent.canAttackClass("StoneWall")) + { + isGate = true; + valb += 10000; + } + else if (structb.hasClass("ConquestCritical")) + valb += 200; + return (valb - vala); + }); + // TODO: handle ballistas here + if (mStruct.length !== 0) { + if (isGate) + ent.attack(mStruct[0].id()); + else + { + var rand = Math.floor(Math.random() * mStruct.length*0.2); + ent.attack(mStruct[+rand].id()); + } + } + else if (API3.SquareVectorDistance(self.targetPos, ent.position()) > 900) + ent.attackMove(self.targetPos[0], self.targetPos[1]); + } + else + { + if (mUnit.length !== 0) + { + var noFemale = (!ent.hasClass("Cavalry") && !ent.hasClass("Ranged")) + mUnit.sort(function (unitA,unitB) { + var vala = unitA.hasClass("Support") ? 50 : 0; + if (ent.countersClasses(unitA.classes())) + vala += 100; + var valb = unitB.hasClass("Support") ? 50 : 0; + if (ent.countersClasses(unitB.classes())) + valb += 100; + return valb - vala; + }); + var rand = Math.floor(Math.random() * mUnit.length*0.1); + ent.attack(mUnit[rand].id()); + } + else if (API3.SquareVectorDistance(self.targetPos, ent.position()) > 2500 ) + ent.attackMove(self.targetPos[0],self.targetPos[1]); + else if (mStruct.length !== 0) { + mStruct.sort(function (structa,structb) { + var vala = structa.costSum(); + if (structa.hasClass("Gates") && ent.canAttackClass("StoneWall")) + { + isGate = true; + vala += 10000; + } + else if (structa.hasClass("ConquestCritical")) + vala += 100; + var valb = structb.costSum(); + if (structb.hasClass("Gates") && ent.canAttackClass("StoneWall")) + { + isGate = true; + valb += 10000; + } + else if (structb.hasClass("ConquestCritical")) + valb += 100; + return (valb - vala); + }); + if (isGate) + ent.attack(mStruct[0].id()); + else + { + var rand = Math.floor(Math.random() * mStruct.length*0.1); + ent.attack(mStruct[rand].id()); + } + } + } + } + this.unitCollUpdateArray.splice(0, lgth); + } + + // updating targets. + if (!gameState.getEntityById(this.target.id())) + { + this.targetPos = undefined; + var targets = this.targetFinder(gameState); + if (this.type === "Rush") + { + var rushTarget = this.rushTargetFinder(gameState); + if (rushTarget.length !== 0) + targets = rushTarget; + } + if (targets.length === 0) + targets = this.defaultTargetFinder(gameState); + if (targets.length) + { + if (this.Config.debug) + { + warn("Seems like our target has been destroyed. Switching."); + warn("Aiming for " + targets); + } + // picking a target + var targetArray = targets.toEntityArray(); + while (!this.targetPos && targetArray.length) + { + var rand = Math.floor((Math.random()*targetArray.length)); + this.target = targetArray[rand]; + this.targetPos = this.target.position(); + if (!this.targetPos) + targetArray.splice(rand, 1); + } + } + if (!this.targetPos) + { + if (this.Config.debug) + { + warn("No target with a valid position found"); + warn(" remaining units " + this.unitCollection.length); + } + Engine.ProfileStop(); + return false; + } + } + + // regularly update the target position in case it's a unit. + if (this.target.hasClass("Unit")) + this.targetPos = this.target.position(); + } + this.lastPosition = this.position; + Engine.ProfileStop(); + + return this.unitCollection.length; +}; + +m.AttackPlan.prototype.totalCountUnits = function(gameState) +{ + var totalcount = 0; + for (var i in this.idList) + totalcount++; + return totalcount; +}; + +// reset any units +m.AttackPlan.prototype.Abort = function(gameState) +{ + // Do not use QuickIter with forEach when forEach removes elements + this.unitCollection.preventQuickIter(); + this.unitCollection.forEach(function(ent) { + ent.stopMoving(); + ent.setMetadata(PlayerID, "role", undefined); + ent.setMetadata(PlayerID, "subrole", undefined); + ent.setMetadata(PlayerID, "plan", undefined); + }); + + for (var unitCat in this.unitStat) { + delete this.unitStat[unitCat]; + delete this.unit[unitCat]; + } + delete this.unitCollection; + gameState.ai.queueManager.removeQueue("plan_" + this.name); + gameState.ai.queueManager.removeQueue("plan_" + this.name + "_champ"); +}; + +m.AttackPlan.prototype.checkEvents = function(gameState, events, queues) +{ + if (this.state === "unexecuted") + return; + var TrainingEvents = events["TrainingFinished"]; + for (var i in TrainingEvents) + { + var evt = TrainingEvents[i]; + for each (var id in evt.entities) + { + var ent = gameState.getEntityById(id); + if (!ent || ent.getMetadata(PlayerID, "plan") === undefined) + continue; + if (ent.getMetadata(PlayerID, "plan") === this.name) + ent.setMetadata(PlayerID, "plan", undefined); + } + } +}; + +m.AttackPlan.prototype.hasForceOrder = function(data, value) +{ + var forced = false; + this.unitCollection.forEach(function (ent) { + if (data && +(ent.getMetadata(PlayerID, data)) !== value) + return; + var orders = ent.unitAIOrderData(); + for each (var order in orders) + if (order.force) + forced = true; + }); + return forced; +}; + +m.AttackPlan.prototype.debugAttack = function() +{ + warn("---------- attack " + this.name); + for (var unitCat in this.unitStat) + { + var Unit = this.unitStat[unitCat]; + warn(unitCat + " num=" + this.unit[unitCat].length + " min=" + Unit["minSize"] + " need=" + Unit["targetSize"]); + } + warn("------------------------------"); +} + +return m; +}(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/baseManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/baseManager.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/baseManager.js (revision 14865) @@ -0,0 +1,840 @@ +var PETRA = function(m) +{ +/* Base Manager + * Handles lower level economic stuffs. + * Some tasks: + -tasking workers: gathering/hunting/building/repairing?/scouting/plans. + -giving feedback/estimates on GR + -achieving building stuff plans (scouting/getting ressource/building) or other long-staying plans, if I ever get any. + -getting good spots for dropsites + -managing dropsite use in the base + > warning HQ if we'll need more space + -updating whatever needs updating, keeping track of stuffs (rebuilding needs…) + */ + +m.BaseManager = function(Config) +{ + this.Config = Config; + this.ID = m.playerGlobals[PlayerID].uniqueIDBases++; + + // anchor building: seen as the main building of the base. Needs to have territorial influence + this.anchor = undefined; + this.accessIndex = undefined; + + // Maximum distance (from any dropsite) to look for resources + // 3 areas are used: from 0 to max/4, from max/4 to max/2 and from max/2 to max + this.maxDistResourceSquare = 360*360; + + this.constructing = false; + + // vector for iterating, to check one use the HQ map. + this.territoryIndices = []; +}; + +m.BaseManager.prototype.init = function(gameState, unconstructed) +{ + this.constructing = unconstructed; + // entitycollections + this.units = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "base", this.ID)); + this.workers = this.units.filter(API3.Filters.byMetadata(PlayerID,"role","worker")); + this.buildings = gameState.getOwnStructures().filter(API3.Filters.byMetadata(PlayerID, "base", this.ID)); + + this.units.allowQuickIter(); + this.workers.allowQuickIter(); + this.buildings.allowQuickIter(); + + this.units.registerUpdates(); + this.workers.registerUpdates(); + this.buildings.registerUpdates(); + + // array of entity IDs, with each being + this.dropsites = {}; + this.dropsiteSupplies = {}; + this.gatherers = {}; + for each (var type in this.Config.resources) + { + this.dropsiteSupplies[type] = {"nearby": [], "medium": [], "faraway": []}; + this.gatherers[type] = {"nextCheck": 0, "used": 0, "lost": 0}; + } +}; + +m.BaseManager.prototype.assignEntity = function(unit) +{ + unit.setMetadata(PlayerID, "base", this.ID); + this.units.updateEnt(unit); + this.workers.updateEnt(unit); + this.buildings.updateEnt(unit); +}; + +m.BaseManager.prototype.setAnchor = function(gameState, anchorEntity) +{ + if (!anchorEntity.hasClass("Structure") || !anchorEntity.hasTerritoryInfluence()) + { + warn("Error: Petra base " + this.ID + " has been assigned an anchor building that has no territorial influence. Please report this on the forum.") + return false; + } + this.anchor = anchorEntity; + this.anchor.setMetadata(PlayerID, "base", this.ID); + this.anchor.setMetadata(PlayerID, "baseAnchor", true); + this.buildings.updateEnt(this.anchor); + this.accessIndex = gameState.ai.accessibility.getAccessValue(this.anchor.position()); + return true; +}; + +m.BaseManager.prototype.checkEvents = function (gameState, events, queues) +{ + var renameEvents = events["EntityRenamed"]; + var destEvents = events["Destroy"]; + var createEvents = events["Create"]; + var cFinishedEvents = events["ConstructionFinished"]; + + for (var i in renameEvents) + { + var ent = gameState.getEntityById(renameEvents[i].newentity); + var workerObject = ent.getMetadata(PlayerID, "worker-object"); + if (workerObject) + workerObject.ent = ent; + } + + for (var i in destEvents) + { + var evt = destEvents[i]; + // let's check we haven't lost an important building here. + if (evt != undefined && !evt.SuccessfulFoundation && evt.entityObj != undefined && evt.metadata !== undefined && evt.metadata[PlayerID] && + evt.metadata[PlayerID]["base"] !== undefined && evt.metadata[PlayerID]["base"] == this.ID) + { + var ent = evt.entityObj; + if (ent.resourceDropsiteTypes() && !ent.hasClass("Elephant")) + this.removeDropsite(gameState, ent); + if (evt.metadata[PlayerID]["baseAnchor"] && evt.metadata[PlayerID]["baseAnchor"] == true) + { + // sounds like we lost our anchor. Let's try rebuilding it. + // TODO: currently the HQ manager sets us as initgathering, we probably ouht to do it + this.anchor = undefined; + + this.constructing = true; // let's switch mode. + this.workers.forEach( function (worker) { worker.stopMoving(); }); + queues.civilCentre.addItem(new m.ConstructionPlan(gameState, gameState.ai.HQ.bBase[0], { "base": this.ID, "baseAnchor": true }, ent.position())); + } + + } + } + for (var i in cFinishedEvents) + { + var evt = cFinishedEvents[i]; + if (evt && evt.newentity) + { + var ent = gameState.getEntityById(evt.newentity); + if (ent === undefined) + continue; + + if (ent.getMetadata(PlayerID,"base") == this.ID) + { + if (ent.resourceDropsiteTypes() && !ent.hasClass("Elephant")) + this.assignResourceToDropsite(gameState, ent); + if (ent.resourceDropsiteTypes() && !ent.hasClass("Elephant") && this.Config.debug) + for each (var ress in ent.resourceDropsiteTypes()) + warn(" DPresource " + ress + " = " + this.getResourceLevel(gameState, ress)); + } + } + } + for (var i in createEvents) + { + var evt = createEvents[i]; + if (evt && evt.entity) + { + var ent = gameState.getEntityById(evt.entity); + if (ent === undefined) + continue; + + // do necessary stuff here + } + } +}; + +/** + * Assign the resources around the dropsites of this basis in three areas according to distance, and sort them in each area. + * Moving resources (animals) and buildable resources (fields) are treated elsewhere. + */ +m.BaseManager.prototype.assignResourceToDropsite = function (gameState, dropsite) +{ + if (this.dropsites[dropsite.id()]) + { + if (this.Config.debug) + warn("assignResourceToDropsite: dropsite already in the list. Should never happen"); + return; + } + this.dropsites[dropsite.id()] = true; + + var self = this; + for each (var type in dropsite.resourceDropsiteTypes()) + { + var resources = gameState.getResourceSupplies(type); + if (resources.length === 0) + continue; + + var nearby = this.dropsiteSupplies[type]["nearby"]; + var medium = this.dropsiteSupplies[type]["medium"]; + var faraway = this.dropsiteSupplies[type]["faraway"]; + + resources.forEach(function(supply) + { + if (!supply.position()) + return; + if (supply.getMetadata(PlayerID, "inaccessible") === true) + return; + if (supply.hasClass("Animal")) // moving resources are treated differently TODO + return; + if (supply.hasClass("Field")) // fields are treated separately + return; + // quickscope accessibility check + if (!gameState.ai.accessibility.pathAvailable(gameState, dropsite.position(), supply.position(),false, true)) + return; + + var dist = API3.SquareVectorDistance(supply.position(), dropsite.position()); + if (dist < self.maxDistResourceSquare) + { + if (supply.resourceSupplyType()["generic"] == "treasure") + { + if (dist < self.maxDistResourceSquare/4) + dist = 0; + else + dist = self.maxDistResourceSquare/16; + } + if (dist < self.maxDistResourceSquare/16) // distmax/4 + nearby.push({ "dropsite": dropsite.id(), "id": supply.id(), "ent": supply, "dist": dist }); + else if (dist < self.maxDistResourceSquare/4) // distmax/2 + medium.push({ "dropsite": dropsite.id(), "id": supply.id(), "ent": supply, "dist": dist }); + else + faraway.push({ "dropsite": dropsite.id(), "id": supply.id(), "ent": supply, "dist": dist }); + } + }); + + nearby.sort(function(r1, r2) { return (r1.dist - r2.dist);}); + medium.sort(function(r1, r2) { return (r1.dist - r2.dist);}); + faraway.sort(function(r1, r2) { return (r1.dist - r2.dist);}); + +/* var debug = false; + if (debug) + { + faraway.forEach(function(res){ + Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [2,0,0]}); + }); + medium.forEach(function(res){ + Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [0,2,0]}); + }); + nearby.forEach(function(res){ + Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [0,0,2]}); + }); + } */ + } +}; + +// completely remove the dropsite resources from our list. +m.BaseManager.prototype.removeDropsite = function (gameState, ent) +{ + if (!ent.id()) + return; + + var removeSupply = function(entId, supply){ + for (var i = 0; i < supply.length; ++i) + { + // exhausted resource, remove it from this list + if (!supply[i].ent || !gameState.getEntityById(supply[i].id)) + supply.splice(i--, 1); + // resource assigned to the removed dropsite, remove it + else if (supply["dropsite"] === entId) + supply.splice(i--, 1); + } + }; + + for (var type in this.dropsiteSupplies) + { + removeSupply(ent.id(), this.dropsiteSupplies[type]["nearby"]); + removeSupply(ent.id(), this.dropsiteSupplies[type]["medium"]); + removeSupply(ent.id(), this.dropsiteSupplies[type]["faraway"]); + } + + this.dropsites[ent.id()] = undefined; + return; +}; + +// Returns the position of the best place to build a new dropsite for the specified resource +m.BaseManager.prototype.findBestDropsiteLocation = function(gameState, resource) +{ + + var storeHousePlate = gameState.getTemplate(gameState.applyCiv("structures/{civ}_storehouse")); + + // 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 obstructions = m.createObstructionMap(gameState, this.accessIndex, storeHousePlate); + obstructions.expandInfluences(); + + // copy the resource map as initialization. + var locateMap = new API3.Map(gameState.sharedScript, gameState.sharedScript.resourceMaps[resource].map, true); + + var DPFoundations = gameState.getOwnFoundations().filter(API3.Filters.byType(gameState.applyCiv("foundation|structures/{civ}_storehouse"))); + + var ccEnts = gameState.getOwnEntities().filter(API3.Filters.byClass("CivCentre")).toEntityArray(); + + // TODO: might be better to check dropsites someplace else. + // loop over this in this.terrytoryindices. It's usually a little too much, but it's always enough. + for (var p = 0; p < this.territoryIndices.length; ++p) + { + var j = this.territoryIndices[p]; + locateMap.map[j] *= 2; + + // only add where the map is currently not null, ie in our territory and some "Resource" would be close. + // This makes the placement go from "OK" to "human-like". + for (var i in gameState.sharedScript.resourceMaps) + if (locateMap.map[j] !== 0 && i !== "food") + locateMap.map[j] += gameState.sharedScript.resourceMaps[i].map[j]; + + locateMap.map[j] *= 0.7; // Just a normalisation factor as the max is 255 + + var pos = [j%locateMap.width+0.5, Math.floor(j/locateMap.width)+0.5]; + pos = [gameState.cellSize*pos[0], gameState.cellSize*pos[1]]; + for (var i in this.dropsites) + { + if (!gameState.getEntityById(i)) + continue; + var dpPos = gameState.getEntityById(i).position(); + if (!dpPos) + continue; + var dist = API3.SquareVectorDistance(dpPos, pos); + if (dist < 3600) + { + locateMap.map[j] = 0; + break; + } + else if (dist < 6400) + locateMap.map[j] /= 2; + } + if (locateMap.map[j] == 0) + continue; + + for (var i in DPFoundations._entities) + { + var dpPos = gameState.getEntityById(i).position(); + if (!dpPos) + continue; + var dist = API3.SquareVectorDistance(dpPos, pos); + if (dist < 3600) + { + locateMap.map[j] = 0; + break; + } + else if (dist < 6400) + locateMap.map[j] /= 2; + } + if (locateMap.map[j] == 0) + continue; + + for each (var cc in ccEnts) + { + var ccPos = cc.position(); + if (!ccPos) + continue; + var dist = API3.SquareVectorDistance(ccPos, pos); + if (dist < 3600) + { + locateMap.map[j] = 0; + break; + } + else if (dist < 6400) + locateMap.map[j] /= 2; + } + } + + var best = locateMap.findBestTile(2, obstructions); // try to find a spot to place a DP. + var bestIdx = best[0]; + + if (this.Config.debug == 2) + warn("for dropsite best is " + best[1] + " at " + gameState.getTimeElapsed()); + + // tell the dropsite builder we haven't found anything satisfactory. +// var cutbest = 60; +// // being less demanding for first dropsite +// if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_storehouse")) == 0) +// var cutbest = 50; + +// if (best[1] < cutbest) +// return false; + + var quality = best[1]; + if (quality <= 0) + return {"quality": quality, "pos": [0, 0]}; + var x = ((bestIdx % locateMap.width) + 0.5) * gameState.cellSize; + var z = (Math.floor(bestIdx / locateMap.width) + 0.5) * gameState.cellSize; + return {"quality": quality, "pos": [x, z]}; +}; + +m.BaseManager.prototype.getResourceLevel = function (gameState, type) +{ + var count = 0; + var check = {}; + var nearby = this.dropsiteSupplies[type]["nearby"]; + for each (var supply in nearby) + { + if (check[supply.id]) // avoid double counting as same resource can appear several time + continue; + check[supply.id] = true; + count += supply.ent.resourceSupplyAmount(); + } + var medium = this.dropsiteSupplies[type]["medium"]; + for each (var supply in medium) + { + if (check[supply.id]) + continue; + check[supply.id] = true; + count += 0.6*supply.ent.resourceSupplyAmount(); + } + return count; +}; + +// check our resource levels and react accordingly +m.BaseManager.prototype.checkResourceLevels = function (gameState, queues) +{ + for each (var type in this.Config.resources) + { + if (type == "food") + { + var count = this.getResourceLevel(gameState, type); // TODO animals are not accounted, may-be we should + var numFarms = gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_field"), true); + var numFound = gameState.countEntitiesByType(gameState.applyCiv("foundation|structures/{civ}_field"), true); + var numQueue = queues.field.countQueuedUnits(); + + // TODO if not yet farms, add a check on time used/lost and build farmstead if needed + if (count < 1200 && numFarms + numFound + numQueue === 0) // tell the queue manager we'll be trying to build fields shortly. + { + for (var i = 0; i < this.Config.Economy.initialFields; ++i) + { + var plan = new m.ConstructionPlan(gameState, "structures/{civ}_field", { "base" : this.ID }); + plan.isGo = function() { return false; }; // don't start right away. + queues.field.addItem(plan); + } + } + else if (count < 400 && numFarms + numFound === 0) + { + for (var i in queues.field.queue) + queues.field.queue[i].isGo = function() { return true; }; // start them + } + else if(gameState.ai.HQ.canBuild(gameState, "structures/{civ}_field")) // let's see if we need to add new farms. + { + if (numFound < 2 && numFound + numQueue < 3) + queues.field.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_field", { "base" : this.ID })); + } + } + else if (queues.dropsites.length() === 0 && gameState.countFoundationsByType(gameState.applyCiv("structures/{civ}_storehouse"), true) === 0) + { + if (gameState.ai.playedTurn > this.gatherers[type].nextCheck) + { + var self = this; + this.gatherersByType(gameState, type).forEach(function (ent) { + if (ent.unitAIState() === "INDIVIDUAL.GATHER.GATHERING") + ++self.gatherers[type].used; + else if (ent.unitAIState() === "INDIVIDUAL.RETURNRESOURCE.APPROACHING") + ++self.gatherers[type].lost; + }); + // TODO add also a test on remaining resources + var total = this.gatherers[type].used + this.gatherers[type].lost; + if (total > 150 || (total > 60 && type !== "wood")) + { + var ratio = this.gatherers[type].lost / total; + if (ratio > 0.15) + { + var newDP = this.findBestDropsiteLocation(gameState, type); + if (newDP.quality > 50 && gameState.ai.HQ.canBuild(gameState, "structures/{civ}_storehouse")) + { + queues.dropsites.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_storehouse", { "base": this.ID }, newDP.pos)); + if (!gameState.isResearched("gather_capacity_wheelbarrow") && !gameState.isResearching("gather_capacity_wheelbarrow")) + queues.minorTech.addItem(new m.ResearchPlan(gameState, "gather_capacity_wheelbarrow")); + } + else + gameState.ai.HQ.buildNewBase(gameState, queues, type); + } + this.gatherers[type].nextCheck = gameState.ai.playedTurn + 20; + this.gatherers[type].used = 0; + this.gatherers[type].lost = 0; + } + else if (total === 0) + this.gatherers[type].nextCheck = gameState.ai.playedTurn + 10; + } + } + else + { + this.gatherers[type].nextCheck = gameState.ai.playedTurn; + this.gatherers[type].used = 0; + this.gatherers[type].lost = 0; + } + } + +}; + +// let's return the estimated gather rates. +m.BaseManager.prototype.getGatherRates = function(gameState, currentRates) +{ + for (var i in currentRates) + { + // I calculate the exact gathering rate for each unit. + // I must then lower that to account for travel time. + // Given that the faster you gather, the more travel time matters, + // I use some logarithms. + // TODO: this should take into account for unit speed and/or distance to target + + var units = this.gatherersByType(gameState, i); + units.forEach(function (ent) { + var gRate = ent.currentGatherRate(); + if (gRate !== undefined) + currentRates[i] += Math.log(1+gRate)/1.1; + }); + if (i === "food") + { + units = this.workers.filter(API3.Filters.byMetadata(PlayerID, "subrole", "hunter")); + units.forEach(function (ent) { + var gRate = ent.currentGatherRate() + if (gRate !== undefined) + currentRates[i] += Math.log(1+gRate)/1.1; + }); + } + currentRates[i] += 0.5*m.GetTCRessGatherer(gameState, i); + } +}; + +m.BaseManager.prototype.assignRolelessUnits = function(gameState) +{ + // TODO: make this cleverer. + var roleless = this.units.filter(API3.Filters.not(API3.Filters.byHasMetadata(PlayerID, "role"))); + var self = this; + roleless.forEach(function(ent) { + if (ent.hasClass("Worker") || ent.hasClass("CitizenSoldier")) + ent.setMetadata(PlayerID, "role", "worker"); + else if (ent.hasClass("Support") && ent.hasClass("Elephant")) + ent.setMetadata(PlayerID, "role", "worker"); + }); +}; + +// If the numbers of workers on the resources is unbalanced then set some of workers to idle so +// they can be reassigned by reassignIdleWorkers. +// TODO: actually this probably should be in the HQ. +m.BaseManager.prototype.setWorkersIdleByPriority = function(gameState) +{ + if (gameState.currentPhase() < 2) + return; + + var resources = gameState.ai.queueManager.getAvailableResources(gameState); + + var avgOverdraft = 0; + for each (var type in resources.types) + avgOverdraft += resources[type]; + avgOverdraft /= 4; + + var mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState); + for each (var type in resources.types) + { + if (type === mostNeeded[0]) + continue; + if (resources[type] > avgOverdraft + 200 || (resources[type] > avgOverdraft && avgOverdraft > 200)) + { + if (this.gatherersByType(gameState, type).length === 0) + continue; + // TODO: perhaps change this? + var nb = 2; + this.gatherersByType(gameState, type).forEach( function (ent) { + if (nb == 0) + return; + nb--; + // TODO: might want to direct assign. + ent.stopMoving(); + ent.setMetadata(PlayerID, "subrole","idle"); + }); + } + } +}; + +// TODO: work on this. +m.BaseManager.prototype.reassignIdleWorkers = function(gameState) +{ + // Search for idle workers, and tell them to gather resources based on demand + var filter = API3.Filters.or(API3.Filters.byMetadata(PlayerID,"subrole","idle"), API3.Filters.not(API3.Filters.byHasMetadata(PlayerID,"subrole"))); + var idleWorkers = gameState.updatingCollection("idle-workers-base-" + this.ID, filter, this.workers); + + var self = this; + if (idleWorkers.length) { + idleWorkers.forEach(function(ent) + { + // Check that the worker isn't garrisoned + if (ent.position() === undefined) + return; + // Support elephant can only be builders + if (ent.hasClass("Support") && ent.hasClass("Elephant")) { + ent.setMetadata(PlayerID, "subrole", "idle"); + return; + } + if (ent.hasClass("Worker")) + { + if (self.anchor && self.anchor.needsRepair() === true) + ent.repair(self.anchor); + else + { + var types = gameState.ai.HQ.pickMostNeededResources(gameState); + ent.setMetadata(PlayerID, "subrole", "gatherer"); + ent.setMetadata(PlayerID, "gather-type", types[0]); + m.AddTCRessGatherer(gameState, types[0]); + } + } + else if (ent.hasClass("Cavalry")) + ent.setMetadata(PlayerID, "subrole", "hunter"); + }); + } +}; + +m.BaseManager.prototype.workersBySubrole = function(gameState, subrole) +{ + return gameState.updatingCollection("subrole-" + subrole +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "subrole", subrole), this.workers, true); +}; + +m.BaseManager.prototype.gatherersByType = function(gameState, type) +{ + return gameState.updatingCollection("workers-gathering-" + type +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "gather-type", type), this.workersBySubrole(gameState, "gatherer")); +}; + + +// returns an entity collection of workers. +// They are idled immediatly and their subrole set to idle. +m.BaseManager.prototype.pickBuilders = function(gameState, workers, number) +{ + var availableWorkers = this.workers.filter(API3.Filters.not(API3.Filters.byClass("Cavalry"))).toEntityArray(); + availableWorkers.sort(function (a,b) { + var vala = 0, valb = 0; + if (a.getMetadata(PlayerID, "subrole") == "builder") + vala = 100; + if (b.getMetadata(PlayerID, "subrole") == "builder") + valb = 100; + if (a.getMetadata(PlayerID, "subrole") == "idle") + vala = -20; + if (b.getMetadata(PlayerID, "subrole") == "idle") + valb = -20; + if (a.getMetadata(PlayerID, "plan") != undefined) + vala = -100; + if (b.getMetadata(PlayerID, "plan") != undefined) + valb = -100; + return (vala - valb); + }); + var needed = Math.min(number, availableWorkers.length); + for (var i = 0; i < needed; ++i) + { + availableWorkers[i].stopMoving(); + availableWorkers[i].setMetadata(PlayerID, "subrole", "idle"); + workers.addEnt(availableWorkers[i]); + } + return; +}; + +m.BaseManager.prototype.assignToFoundations = function(gameState, noRepair) +{ + // If we have some foundations, and we don't have enough builder-workers, + // try reassigning some other workers who are nearby + // AI tries to use builders sensibly, not completely stopping its econ. + + var self = this; + + // TODO: this is not perfect performance-wise. + var foundations = this.buildings.filter(API3.Filters.and(API3.Filters.isFoundation(),API3.Filters.not(API3.Filters.byClass("Field")))).toEntityArray(); + + var damagedBuildings = this.buildings.filter(function (ent) { + if (ent.foundationProgress() === undefined && ent.needsRepair()) + return true; + return false; + }).toEntityArray(); + + // Check if nothing to build + if (!foundations.length && !damagedBuildings.length){ + return; + } + var workers = this.workers.filter(API3.Filters.not(API3.Filters.byClass("Cavalry"))); + var builderWorkers = this.workersBySubrole(gameState, "builder"); + var idleBuilderWorkers = this.workersBySubrole(gameState, "builder").filter(API3.Filters.isIdle()); + + // if we're constructing and we have the foundations to our base anchor, only try building that. + if (this.constructing == true && this.buildings.filter(API3.Filters.and(API3.Filters.isFoundation(), API3.Filters.byMetadata(PlayerID, "baseAnchor", true))).length !== 0) + { + foundations = this.buildings.filter(API3.Filters.byMetadata(PlayerID, "baseAnchor", true)).toEntityArray(); + var tID = foundations[0].id(); + workers.forEach(function (ent) { //}){ + var target = ent.getMetadata(PlayerID, "target-foundation"); + if (target && target != tID) + { + ent.stopMoving(); + ent.setMetadata(PlayerID, "target-foundation", tID); + } + }); + } + + if (workers.length < 2) + { + var noobs = gameState.ai.HQ.bulkPickWorkers(gameState, this.ID, 2); + if(noobs) + { + noobs.forEach(function (worker) { //}){ + worker.setMetadata(PlayerID,"base", self.ID); + worker.setMetadata(PlayerID,"subrole", "builder"); + workers.updateEnt(worker); + builderWorkers.updateEnt(worker); + idleBuilderWorkers.updateEnt(worker); + }); + } + } + var addedWorkers = 0; + + var maxTotalBuilders = Math.ceil(workers.length * 0.2); + if (this.constructing == true && maxTotalBuilders < 15) + maxTotalBuilders = 15; + + for (var i in foundations) { + var target = foundations[i]; + + if (target.hasClass("Field")) + continue; // we do not build fields + + var assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length; + var targetNB = this.Config.Economy.targetNumBuilders; // TODO: dynamic that. + if (target.hasClass("House")) + targetNB *= 2; + else if (target.hasClass("Barracks") || target.hasClass("Tower")) + targetNB = 4; + else if (target.hasClass("Fortress")) + targetNB = 7; + if (target.getMetadata(PlayerID, "baseAnchor") == true) + targetNB = 15; + + if (assigned < targetNB) { + if (builderWorkers.length - idleBuilderWorkers.length + addedWorkers < maxTotalBuilders) { + + var addedToThis = 0; + + idleBuilderWorkers.forEach(function(ent) { + if (ent.position() && API3.SquareVectorDistance(ent.position(), target.position()) < 10000 && assigned + addedToThis < targetNB) + { + addedWorkers++; + addedToThis++; + ent.setMetadata(PlayerID, "target-foundation", target.id()); + } + }); + if (assigned + addedToThis < targetNB) + { + var nonBuilderWorkers = workers.filter(function(ent) { return (ent.getMetadata(PlayerID, "subrole") !== "builder" && ent.position() !== undefined); }).toEntityArray(); + var time = target.buildTime(); + nonBuilderWorkers.sort(function (workerA,workerB) + { + var coeffA = API3.SquareVectorDistance(target.position(),workerA.position()); + // elephant moves slowly, so when far away they are only useful if build time is long + if (workerA.hasClass("Elephant")) + coeffA *= 0.5 * (1 + (Math.sqrt(coeffA)/150)*(30/time)); + else if (workerA.getMetadata(PlayerID, "gather-type") === "food") + coeffA *= 3; + var coeffB = API3.SquareVectorDistance(target.position(),workerB.position()); + if (workerB.hasClass("Elephant")) + coeffB *= 0.5 * (1 + (Math.sqrt(coeffB)/150)*(30/time)); + else if (workerB.getMetadata(PlayerID, "gather-type") === "food") + coeffB *= 3; + return (coeffA - coeffB); + }); + var current = 0; + while (assigned + addedToThis < targetNB && current < nonBuilderWorkers.length) + { + addedWorkers++; + addedToThis++; + var ent = nonBuilderWorkers[current++]; + ent.stopMoving(); + ent.setMetadata(PlayerID, "subrole", "builder"); + ent.setMetadata(PlayerID, "target-foundation", target.id()); + }; + } + } + } + } + + // don't repair if we're still under attack, unless it's like a vital (civcentre or wall) building that's getting destroyed. + for (var i in damagedBuildings) { + var target = damagedBuildings[i]; + if (gameState.defcon() < 5) { + if (target.healthLevel() > 0.5 || !target.hasClass("CivCentre") || !target.hasClass("StoneWall")) { + continue; + } + } else if (noRepair && !target.hasClass("CivCentre")) + continue; + + var territory = m.createTerritoryMap(gameState); + if (territory.getOwner(target.position()) !== PlayerID || territory.getOwner([target.position()[0] + 5, target.position()[1]]) !== PlayerID) + continue; + + var assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length; + if (assigned < targetNB/3) { + if (builderWorkers.length + addedWorkers < targetNB*2) { + + var nonBuilderWorkers = workers.filter(function(ent) { return (ent.getMetadata(PlayerID, "subrole") !== "builder" && ent.position() !== undefined); }); + if (gameState.defcon() < 5) + nonBuilderWorkers = workers.filter(function(ent) { return (ent.getMetadata(PlayerID, "subrole") !== "builder" && ent.hasClass("Female") && ent.position() !== undefined); }); + var nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), targetNB/3 - assigned); + + nearestNonBuilders.forEach(function(ent) { + ent.stopMoving(); + addedWorkers++; + ent.setMetadata(PlayerID, "subrole", "builder"); + ent.setMetadata(PlayerID, "target-foundation", target.id()); + }); + } + } + } +}; + +m.BaseManager.prototype.update = function(gameState, queues, events) +{ + Engine.ProfileStart("Base update - base " + this.ID); + var self = this; + + this.checkResourceLevels(gameState, queues); + this.assignToFoundations(gameState); + + if (this.constructing && this.anchor) + { + var owner = m.createTerritoryMap(gameState).getOwner(this.anchor.position()); + if(owner !== 0 && !gameState.isPlayerAlly(owner)) + { + // we're in enemy territory. If we're too close from the enemy, destroy us. + var eEnts = gameState.getEnemyStructures().filter(API3.Filters.byClass("CivCentre")).toEntityArray(); + for (var i in eEnts) + { + var entPos = eEnts[i].position(); + if (API3.SquareVectorDistance(entPos, this.anchor.position()) < 8000) + this.anchor.destroy(); + } + } + } + + + if (gameState.ai.playedTurn % 2 === 0) + this.setWorkersIdleByPriority(gameState); + + this.assignRolelessUnits(gameState); + + // should probably be last to avoid reallocations of units that would have done stuffs otherwise. + this.reassignIdleWorkers(gameState); + + // TODO: do this incrementally a la defense.js + this.workers.forEach(function(ent) { + if (!ent.getMetadata(PlayerID, "worker-object")) + ent.setMetadata(PlayerID, "worker-object", new m.Worker(ent)); + ent.getMetadata(PlayerID, "worker-object").update(self, gameState); + }); + + Engine.ProfileStop(); +}; + +return m; + +}(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/config.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/config.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/config.js (revision 14865) @@ -0,0 +1,124 @@ +var PETRA = function(m) +{ + +// this defines the medium difficulty +m.Config = function() { + this.difficulty = 2; // 0 is sandbox, 1 is easy, 2 is medium, 3 is hard, 4 is very hard. + this.debug = 0; + + this.Military = { + "towerLapseTime" : 90, // Time to wait between building 2 towers + "fortressLapseTime" : 420, // Time to wait between building 2 fortresses + "popForBarracks1" : 25, + "popForBarracks2" : 95, + "timeForBlacksmith" : 900, + }; + this.Economy = { + "villagePopCap" : 40, // How many units we want before aging to town. + "cityPhase" : 840, // time to start trying to reach city phase + "popForMarket" : 50, + "dockStartTime" : 240, // Time to wait before building the dock + "targetNumBuilders" : 1.5, // Base number of builders per foundation. + "targetNumTraders" : 4, // Target number of traders + "femaleRatio" : 0.5, // percent of females among the workforce. + "initialFields" : 5 + }; + + // Note: attack settings are set directly in attack_plan.js + // defense + this.Defense = + { + "defenseRatio" : 2, // see defense.js for more info. + "armyCompactSize" : 2000, // squared. Half-diameter of an army. + "armyBreakawaySize" : 3500, // squared. + "armyMergeSize" : 1400, // squared. + "armyStrengthWariness" : 2, // Representation of how important army strength is for its "watch level". + "prudence" : 1 // Representation of how quickly we'll forget about a dangerous army. + }; + + // military + this.buildings = + { + "base" : { + "default" : [ "structures/{civ}_civil_centre" ], + "ptol" : [ "structures/{civ}_military_colony" ], + "sele" : [ "structures/{civ}_military_colony" ] + }, + "advanced" : { + "default" : [], + "hele" : [ "structures/{civ}_gymnasion" ], + "athen" : [ "structures/{civ}_gymnasion" ], + "spart" : [ "structures/{civ}_syssiton" ], + "cart" : [ "structures/{civ}_embassy_celtic", + "structures/{civ}_embassy_iberian", "structures/{civ}_embassy_italiote" ], + "celt" : [ "structures/{civ}_kennel" ], + "pers" : [ "structures/{civ}_fortress", "structures/{civ}_stables", "structures/{civ}_apadana" ], + "rome" : [ "structures/{civ}_army_camp" ], + "mace" : [ "structures/{civ}_siege_workshop"], + "maur" : [ "structures/{civ}_elephant_stables"] + }, + "fort" : { + "default" : [ "structures/{civ}_fortress" ], + "celt" : [ "structures/{civ}_fortress_b", "structures/{civ}_fortress_g" ] + } + }; + + this.priorities = + { + "villager" : 30, // should be slightly lower than the citizen soldier one because otherwise they get all the food + "citizenSoldier" : 60, + "trader" : 50, + "ships" : 70, + "house" : 350, + "dropsites" : 120, + "field" : 500, + "economicBuilding" : 90, + "militaryBuilding" : 240, // set to something lower after the first barracks. + "defenseBuilding" : 70, + "civilCentre" : 950, + "majorTech" : 700, + "minorTech" : 40 + }; + + this.personality = + { + "aggressive": 0.5, + "cooperative": 0.5 + }; + + this.resources = ["food", "wood", "stone", "metal"]; +}; + +//Config.prototype = new BaseConfig(); + +m.Config.prototype.updateDifficulty = function(difficulty) +{ + this.difficulty = difficulty; + // changing settings based on difficulty. + this.targetNumTraders = 2 * this.difficulty; + if (this.difficulty === 1) + { + this.Military.popForBarracks1 = 35; + this.Military.popForBarracks2 = 150; // shouldn't reach it + this.Military.popForBlacksmith = 150; // shouldn't reach it + + this.Economy.cityPhase = 1800; + this.Economy.popForMarket = 80; + this.Economy.femaleRatio = 0.6; + this.Economy.initialFields = 2; + } + else if (this.difficulty === 0) + { + this.Military.popForBarracks1 = 60; + this.Military.popForBarracks2 = 150; // shouldn't reach it + this.Military.popForBlacksmith = 150; // shouldn't reach it + + this.Economy.cityPhase = 240000; + this.Economy.popForMarket = 200; + this.Economy.femaleRatio = 0.7; + this.Economy.initialFields = 1; + } +}; + +return m; +}(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/data.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/data.json (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/data.json (revision 14865) @@ -0,0 +1,7 @@ +{ + "name": "Petra Bot", + "description": "Based on Aegis, but heavily modified and expected to be more robust", + "moduleName" : "PETRA", + "constructor": "PetraBot", + "useShared": true +} Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseArmy.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseArmy.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseArmy.js (revision 14865) @@ -0,0 +1,180 @@ +var PETRA = function(m) +{ + +// Specialization of Armies used by the defense manager. +m.DefenseArmy = function(gameState, defManager, ownEntities, foeEntities) +{ + if (!m.Army.call(this, gameState, defManager, ownEntities, foeEntities)) + return false; + + this.watchTSMultiplicator = this.Config.Defense.armyStrengthWariness; + this.watchDecrement = this.Config.Defense.prudence; + + this.foeSubStrength = { + "spear" : ["Infantry", "Spear"], //also pikemen + "sword" : ["Infantry", "Sword"], + "ranged" : ["Infantry", "Ranged"], + "meleeCav" : ["Cavalry", "Melee"], + "rangedCav" : ["Cavalry", "Ranged"], + "Elephant" : ["Elephant"], + "meleeSiege" : ["Siege", "Melee"], + "rangedSiege" : ["Siege", "Ranged"] + }; + this.ownSubStrength = { + "spear" : ["Infantry", "Spear"], //also pikemen + "sword" : ["Infantry", "Sword"], + "ranged" : ["Infantry", "Ranged"], + "meleeCav" : ["Cavalry", "Melee"], + "rangedCav" : ["Cavalry", "Ranged"], + "Elephant" : ["Elephant"], + "meleeSiege" : ["Siege", "Melee"], + "rangedSiege" : ["Siege", "Ranged"] + }; + + this.checkDangerosity(gameState); // might push us to 1. + this.watchLevel = this.foeStrength * this.watchTSMultiplicator; + + return true; +} + +m.DefenseArmy.prototype = Object.create(m.Army.prototype); + +m.DefenseArmy.prototype.assignUnit = function (gameState, entID) +{ + // we'll assume this defender is ours already. + // we'll also override any previous assignment + + var ent = gameState.getEntityById(entID); + if (!ent || !ent.position()) + return false; + + var idMin = undefined; + var distMin = undefined; + var idMinAll = undefined; + var distMinAll = undefined; + for each (var id in this.foeEntities) + { + var eEnt = gameState.getEntityById(id); + if (!eEnt || !eEnt.position()) // probably can't happen. + continue; + + if (eEnt.unitAIOrderData().length && eEnt.unitAIOrderData()[0]["target"] && + eEnt.unitAIOrderData()[0]["target"] === entID) + { // being attacked >>> target the unit + idMin = id; + break; + } + + var dist = API3.SquareVectorDistance(ent.position(), eEnt.position()); + if (idMinAll === undefined || dist < distMin) + { + idMinAll = id; + distMinAll = dist; + } + if (this.assignedAgainst[id].length > 2) // already enough units against it + continue; + var dist = API3.SquareVectorDistance(ent.position(), eEnt.position()); + if (idMin === undefined || dist < distMin) + { + idMin = id; + distMin = dist; + } + } + + if (idMin !== undefined) + { + this.assignedTo[entID] = idMin; + this.assignedAgainst[idMin].push(entID); + ent.attack(idMin); + return true; + } + else if (idMinAll !== undefined) + { + this.assignedTo[entID] = idMinAll; + this.assignedAgainst[idMinAll].push(entID); + ent.attack(idMinAll); + return true; + } + + this.recalculatePosition(gameState); + ent.attackMove(this.foePosition[0], this.foePosition[1]); + return false; +} + +// TODO: this should return cleverer results ("needs anti-elephant"…) +m.DefenseArmy.prototype.needsDefenders = function (gameState, events) +{ + // some preliminary checks because we don't update for tech + if (this.foeStrength < 0 || this.ownStrength < 0) + this.recalculateStrengths(gameState); + + if (this.foeStrength * this.defenseRatio <= this.ownStrength) + return false; + return this.foeStrength * this.defenseRatio - this.ownStrength; +} + +m.DefenseArmy.prototype.getState = function (gameState) +{ + if (this.foeEntities.length === 0) + return 0; + if (this.state === 2) + return 2; + if (this.watchLevel > 0) + return 1; + return 0; +} + +// check if we should remain at state 2 or drift away +m.DefenseArmy.prototype.checkDangerosity = function (gameState) +{ + this.territoryMap = m.createTerritoryMap(gameState); + // right now we'll check if our position is "enemy" or not. + if (this.territoryMap.getOwner(this.ownPosition) !== PlayerID) + this.state = 1; + else if (this.state === 1) + this.state = 2; +} + +m.DefenseArmy.prototype.update = function (gameState) +{ + var breakaways = this.onUpdate(gameState); + + this.checkDangerosity(gameState); + + var normalWatch = this.foeStrength * this.watchTSMultiplicator; + if (this.state === 2) + this.watchLevel = normalWatch; + else if (this.watchLevel > normalWatch) + this.watchLevel = normalWatch; + else + this.watchLevel -= this.watchDecrement; + + // TODO: deal with watchLevel? + + return breakaways; +} + +m.DefenseArmy.prototype.debug = function (gameState) +{ + m.debug(" "); + m.debug ("Army " + this.ID) +// m.debug ("state " + this.state); +// m.debug ("WatchLevel " + this.watchLevel); +// m.debug ("Entities " + this.foeEntities.length); +// m.debug ("Strength " + this.foeStrength); + // debug (gameState.getEntityById(ent)._templateName + ", ID " + ent); + //debug ("Defenders " + this.ownEntities.length); + for each (ent in this.foeEntities) + { + if (gameState.getEntityById(ent) !== undefined) + { + m.debug (gameState.getEntityById(ent)._templateName + ", ID " + ent); + Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent], "rgb": [0.5,0,0]}); + } else + m.debug("ent " + ent); + } + m.debug (""); + +} +return m; +}(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseManager.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseManager.js (revision 14865) @@ -0,0 +1,445 @@ +var PETRA = function(m) +{ + +m.DefenseManager = function(Config) +{ + this.armies = []; // array of "army" Objects + this.Config = Config; + this.targetList = []; +} + +m.DefenseManager.prototype.init = function(gameState) +{ + this.armyMergeSize = this.Config.Defense.armyMergeSize; +}; + +m.DefenseManager.prototype.update = function(gameState, events) +{ + this.territoryMap = m.createTerritoryMap(gameState); + + this.checkEnemyArmies(gameState, events); + this.checkEnemyUnits(gameState); + this.assignDefenders(gameState); + + this.MessageProcess(gameState,events); +}; + +m.DefenseManager.prototype.makeIntoArmy = function(gameState, entityID) +{ + // Try to add it to an existing army. + for (var o in this.armies) + if (this.armies[o].addFoe(gameState,entityID)) + return; // over + + // Create a new army for it. + var army = new m.DefenseArmy(gameState, this, [], [entityID]); + this.armies.push(army); +}; + +// TODO: this algorithm needs to be improved, sorta. +m.DefenseManager.prototype.isDangerous = function(gameState, entity) +{ + if (!entity.position()) + return false; + + if (this.territoryMap.getOwner(entity.position()) === entity.owner()) + return false; + + // check if the entity is trying to build a new base near our buildings, and if yes, add this base in our target list + if (entity.unitAIState() == "INDIVIDUAL.REPAIR.REPAIRING") + { + var targetId = entity.unitAIOrderData()[0]["target"]; + if (this.targetList.indexOf(targetId) !== -1) + return true; + var target = gameState.getEntityById(targetId); + if (target && target.hasClass("CivCentre")) + { + var myBuildings = gameState.getOwnStructures(); + for (var i in myBuildings._entities) + { + if (API3.SquareVectorDistance(myBuildings._entities[i].position(), entity.position()) > 30000) + continue; + this.targetList.push(targetId); + return true; + } + } + } + + if (entity.attackTypes() === undefined || entity.hasClass("Support")) + return false; + + for (var i = 0; i < this.targetList.length; ++i) + { + var target = gameState.getEntityById(this.targetList[i]); + if (!target || !target.position()) // the enemy base is either destroyed or built + this.targetList.splice(i--, 1); + else if (API3.SquareVectorDistance(target.position(), entity.position()) < 6000) + return true; + } + + var myCCFoundations = gameState.getOwnFoundations().filter(API3.Filters.byClass("CivCentre")); + for (var i in myCCFoundations._entities) + { + if (!myCCFoundations._entities[i].getBuildersNb()) + continue; + if (API3.SquareVectorDistance(myCCFoundations._entities[i].position(), entity.position()) < 6000) + return true; + } + + if (this.Config.personality.cooperative > 0.3) + { + var allyCC = gameState.getAllyEntities().filter(API3.Filters.byClass("CivCentre")); + for (var i in allyCC._entities) + { + if (this.Config.personality.cooperative < 0.6 && allyCC._entities[i].foundationProgress() !== undefined) + continue; + if (API3.SquareVectorDistance(allyCC._entities[i].position(), entity.position()) < 6000) + return true; + } + } + + var myBuildings = gameState.getOwnStructures(); + for (var i in myBuildings._entities) + if (API3.SquareVectorDistance(myBuildings._entities[i].position(), entity.position()) < 6000) + return true; + + return false; +}; + + +m.DefenseManager.prototype.checkEnemyUnits = function(gameState) +{ + var self = this; + + // loop through enemy units + var nbPlayers = gameState.sharedScript.playersData.length - 1; + var i = 1 + gameState.ai.playedTurn % nbPlayers; + if (i === PlayerID || gameState.isPlayerAlly(i)) + return; + + var filter = API3.Filters.and(API3.Filters.byClass("Unit"), API3.Filters.byOwner(i)); + var enemyUnits = gameState.updatingGlobalCollection("player-" +i + "-units", filter); + + enemyUnits.forEach( function (ent) { + // first check: is this unit already part of an army. + if (ent.getMetadata(PlayerID, "PartOfArmy") !== undefined) + return; + + // TODO what to do for ships ? + if (ent.hasClass("Ship") || ent.hasClass("Trader")) + return; + + // check if unit is dangerous "a priori" + if (self.isDangerous(gameState, ent)) + self.makeIntoArmy(gameState, ent.id()); + }); +}; + +m.DefenseManager.prototype.checkEnemyArmies = function(gameState, events) +{ + var self = this; + + for (var o = 0; o < this.armies.length; ++o) + { + var army = this.armies[o]; + army.checkEvents(gameState, events); // must be called every turn for all armies + + // this returns a list of IDs: the units that broke away from the army for being too far. + var breakaways = army.update(gameState); + + for (var u in breakaways) + { + // assume dangerosity + this.makeIntoArmy(gameState,breakaways[u]); + } + + if (army.getState(gameState) === 0) + { + army.clear(gameState); + this.armies.splice(o--,1); + continue; + } + } + // Check if we can't merge it with another. + for (var o = 0; o < this.armies.length; ++o) + { + var army = this.armies[o]; + for (var p = o+1; p < this.armies.length; ++p) + { + var otherArmy = this.armies[p]; + if (otherArmy.state !== army.state) + continue; + + if (API3.SquareVectorDistance(army.foePosition, otherArmy.foePosition) < this.armyMergeSize) + { + // no need to clear here. + army.merge(gameState, otherArmy); + this.armies.splice(p--,1); + } + } + } + + if (gameState.ai.playedTurn % 5 !== 0) + return; + // Check if any army is no more dangerous (possibly because it has defeated us and destroyed our base) + for (var o = 0; o < this.armies.length; ++o) + { + var army = this.armies[o]; + army.recalculatePosition(gameState); + var owner = this.territoryMap.getOwner(army.foePosition); + if (gameState.isPlayerAlly(owner)) + continue; + else if (owner !== 0) // enemy army back in its territory + { + army.clear(gameState); + this.armies.splice(o--,1); + continue; + } + + // army in neutral territory // TODO check smaller distance with all our buildings instead of only ccs with big distance + var stillDangerous = false; + var bases = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).toEntityArray(); + if (this.Config.personality.cooperative > 0.3) + { + var allyCC = gameState.getAllyEntities().filter(API3.Filters.byClass("CivCentre")).toEntityArray(); + bases = bases.concat(allyCC); + } + for (var i in bases) + { + if (API3.SquareVectorDistance(bases[i].position(), army.foePosition) < 40000) + { + if(this.Config.debug > 0) + warn("but still near one of our CC"); + stillDangerous = true; + break; + } + } + if (stillDangerous) + continue; + + army.clear(gameState); + this.armies.splice(o--,1); + } +}; + +m.DefenseManager.prototype.assignDefenders = function(gameState, events) +{ + if (this.armies.length === 0) + return; + + var armiesNeeding = []; + // Okay, let's add defenders + // TODO: this is dumb. + for (var i in this.armies) + { + var army = this.armies[i]; + var needsDef = army.needsDefenders(gameState); + if (needsDef === false) + continue; + + // Okay for now needsDef is the total needed strength. + // we're dumb so we don't choose if we have a defender shortage. + armiesNeeding.push( {"army": army, "need": needsDef} ); + } + + if (armiesNeeding.length === 0) + return; + + // let's get our potential units + var potentialDefenders = []; + gameState.getOwnUnits().forEach(function(ent) { + if (ent.getMetadata(PlayerID, "PartOfArmy") !== undefined) + return; + if (ent.getMetadata(PlayerID, "plan") !== undefined) + { + var subrole = ent.getMetadata(PlayerID, "subrole"); + if (subrole && (subrole === "completing" || subrole === "walking" || subrole === "attacking")) + return; + if (ent.hasClass("Siege")) + return; + } + else + { + if (!ent.hasClass("Infantry") && !ent.hasClass("Cavalry")) + return; + } + potentialDefenders.push(ent.id()); + }); + + for (var a = 0; a < armiesNeeding.length; ++a) + armiesNeeding[a]["army"].recalculatePosition(gameState); + + for (var i = 0; i < potentialDefenders.length; ++i) + { + var ent = gameState.getEntityById(potentialDefenders[i]); + if (!ent.position()) + continue; + var aMin = undefined; + var distMin = undefined; + for (var a = 0; a < armiesNeeding.length; ++a) + { + var dist = API3.SquareVectorDistance(ent.position(), armiesNeeding[a]["army"].foePosition); + if (aMin !== undefined && dist > distMin) + continue; + aMin = a; + distMin = dist; + } + if (aMin === undefined) + { + for (var a = 0; a < armiesNeeding.length; ++a) + warn(" defense/armiesNeeding " + uneval(armiesNeeding[a]["need"])); + } + + var str = m.getMaxStrength(ent); + armiesNeeding[aMin]["need"] -= str; + armiesNeeding[aMin]["army"].addOwn(gameState, potentialDefenders[i]); + armiesNeeding[aMin]["army"].assignUnit(gameState, potentialDefenders[i]); + + if (armiesNeeding[aMin]["need"] <= 0) + armiesNeeding.splice(aMin, 1); + if (armiesNeeding.length === 0) + break; + } + + // If shortage of defenders: increase the priority of soldiers queues + if (armiesNeeding.length !== 0) + gameState.ai.HQ.boostSoldiers(gameState, 10000, true); + else + gameState.ai.HQ.unboostSoldiers(gameState); +}; + +// this processes the attackmessages +// So that a unit that gets attacked will not be completely dumb. +// warning: big levels of indentation coming. +m.DefenseManager.prototype.MessageProcess = function(gameState,events) { +/* var self = this; + var attackedEvents = events["Attacked"]; + for (var key in attackedEvents){ + var e = attackedEvents[key]; + if (gameState.isEntityOwn(gameState.getEntityById(e.target))) { + var attacker = gameState.getEntityById(e.attacker); + var ourUnit = gameState.getEntityById(e.target); + + // the attacker must not be already dead, and it must not be me (think catapults that miss). + if (attacker === undefined || attacker.owner() === PlayerID || attacker.position() === undefined) + continue; + + var mapPos = this.dangerMap.gamePosToMapPos(attacker.position()); + this.dangerMap.addInfluence(mapPos[0], mapPos[1], 4, 1, 'constant'); + + // disregard units from attack plans and defense. + if (ourUnit.getMetadata(PlayerID, "role") == "defense" || ourUnit.getMetadata(PlayerID, "role") == "attack") + continue; + + var territory = this.territoryMap.getOwner(attacker.position()); + + if (attacker.owner() == 0) + { + if (ourUnit !== undefined && ourUnit.hasClass("Unit") && !ourUnit.hasClass("Support")) + ourUnit.attack(e.attacker); + else + { + ourUnit.flee(attacker); + ourUnit.setMetadata(PlayerID,"fleeing", gameState.getTimeElapsed()); + } + if (territory === PlayerID) + { + // anyway we'll register the animal as dangerous, and attack it (note: only on our territory. Don't care otherwise). + this.listOfWantedUnits[attacker.id()] = new EntityCollection(gameState.sharedScript); + this.listOfWantedUnits[attacker.id()].addEnt(attacker); + this.listOfWantedUnits[attacker.id()].freeze(); + this.listOfWantedUnits[attacker.id()].registerUpdates(); + + var filter = Filters.byTargetedEntity(attacker.id()); + this.WantedUnitsAttacker[attacker.id()] = this.myUnits.filter(filter); + this.WantedUnitsAttacker[attacker.id()].registerUpdates(); + } + } // Disregard military units except in our territory. Disregard all calls in enemy territory. + else if (territory == PlayerID || (territory != attacker.owner() && ourUnit.hasClass("Support"))) + { + // TODO: this does not differentiate with buildings... + // These ought to be treated differently. + // units in attack plans will react independently, but we still list the attacks here. + if (attacker.hasClass("Structure")) { + // todo: we ultimately have to check wether it's a danger point or an isolated area, and if it's a danger point, mark it as so. + + // Right now, to make the AI less gameable, we'll mark any surrounding resource as inaccessible. + // usual tower range is 80. Be on the safe side. + var close = gameState.getResourceSupplies("wood").filter(Filters.byDistance(attacker.position(), 90)); + close.forEach(function (supply) { //}){ + supply.setMetadata(PlayerID, "inaccessible", true); + }); + } else { + // TODO: right now a soldier always retaliate... Perhaps it should be set in "Defense" mode. + + // TODO: handle the ship case + if (attacker.hasClass("Ship")) + continue; + + // This unit is dangerous. if it's in an army, it's being dealt with. + // if it's not in an army, it means it's either a lone raider, or it has got friends. + // In which case we must check for other dangerous units around, and perhaps armify them. + // TODO: perhaps someday army detection will have improved and this will require change. + var armyID = attacker.getMetadata(PlayerID, "inArmy"); + if (armyID == undefined || !this.enemyArmy[attacker.owner()] || !this.enemyArmy[attacker.owner()][armyID]) { + if (this.reevaluateEntity(gameState, attacker)) + { + var position = attacker.position(); + var close = HQ.enemyWatchers[attacker.owner()].enemySoldiers.filter(Filters.byDistance(position, self.armyCompactSize)); + + if (close.length > 2 || ourUnit.hasClass("Support") || attacker.hasClass("Siege")) + { + // armify it, then armify units close to him. + this.armify(gameState,attacker); + armyID = attacker.getMetadata(PlayerID, "inArmy"); + + close.forEach(function (ent) { //}){ + if (API3.SquareVectorDistance(position, ent.position()) < self.armyCompactSize) + { + ent.setMetadata(PlayerID, "inArmy", armyID); + self.enemyArmy[ent.owner()][armyID].addEnt(ent); + } + }); + return; // don't use too much processing power. If there are other cases, they'll be processed soon enough. + } + } + // Defensemanager will deal with them in the next turn. + } + if (ourUnit !== undefined && ourUnit.hasClass("Unit")) { + if (ourUnit.hasClass("Support")) { + // let's try to garrison this support unit. + if (ourUnit.position()) + { + var buildings = gameState.getOwnEntities().filter(Filters.byCanGarrison()).filterNearest(ourUnit.position(),4).toEntityArray(); + var garrisoned = false; + for (var i in buildings) + { + var struct = buildings[i]; + if (struct.garrisoned() && struct.garrisonMax() - struct.garrisoned().length > 0) + { + garrisoned = true; + ourUnit.garrison(struct); + break; + } + } + if (!garrisoned) { + ourUnit.flee(attacker); + ourUnit.setMetadata(PlayerID,"fleeing", gameState.getTimeElapsed()); + } + } + } else { + // It's a soldier. Right now we'll retaliate + // TODO: check for stronger units against this type, check for fleeing options, etc. + // Check also for neighboring towers and garrison there perhaps? + ourUnit.attack(e.attacker); + } + } + } + } + } + } + */ +}; // nice sets of closing brackets, isn't it? + +return m; +}(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/entity-extend.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/entity-extend.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/entity-extend.js (revision 14865) @@ -0,0 +1,69 @@ +var PETRA = function(m) +{ + +// returns some sort of DPS * health factor. If you specify a class, it'll use the modifiers against that class too. +m.getMaxStrength = function(ent, againstClass) +{ + var strength = 0.0; + var attackTypes = ent.attackTypes(); + var armourStrength = ent.armourStrengths(); + var hp = ent.maxHitpoints() / 100.0; // some normalization + for (var typeKey in attackTypes) { + var type = attackTypes[typeKey]; + + if (type == "Slaughter" || type == "Charged") + continue; + + var attackStrength = ent.attackStrengths(type); + var attackRange = ent.attackRange(type); + var attackTimes = ent.attackTimes(type); + for (var str in attackStrength) { + var val = parseFloat(attackStrength[str]); + if (againstClass) + val *= ent.getMultiplierAgainst(type, againstClass); + switch (str) { + case "crush": + strength += (val * 0.085) / 3; + break; + case "hack": + strength += (val * 0.075) / 3; + break; + case "pierce": + strength += (val * 0.065) / 3; + break; + } + } + if (attackRange){ + strength += (attackRange.max * 0.0125) ; + } + for (var str in attackTimes) { + var val = parseFloat(attackTimes[str]); + switch (str){ + case "repeat": + strength += (val / 100000); + break; + case "prepare": + strength -= (val / 100000); + break; + } + } + } + for (var str in armourStrength) { + var val = parseFloat(armourStrength[str]); + switch (str) { + case "crush": + strength += (val * 0.085) / 3; + break; + case "hack": + strength += (val * 0.075) / 3; + break; + case "pierce": + strength += (val * 0.065) / 3; + break; + } + } + return strength * hp; +}; + +return m; +}(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/entitycollection-extend.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/entitycollection-extend.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/entitycollection-extend.js (revision 14865) @@ -0,0 +1,16 @@ +var PETRA = function(m) +{ + +m.EntityCollectionFromIds = function(gameState, idList){ + var ents = {}; + for (var i in idList){ + var id = idList[i]; + if (gameState.entities._entities[id]) { + ents[id] = gameState.entities._entities[id]; + } + } + return new API3.EntityCollection(gameState.sharedScript, ents); +} + +return m; +}(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/gamestate-extend.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/gamestate-extend.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/gamestate-extend.js (revision 14865) @@ -0,0 +1,70 @@ +var PETRA = function(m) +{ + +// Some functions that could be part of the gamestate but are Aegis specific. + +// The next three are to register that we assigned a gatherer to a resource this turn. +// expects an entity +m.IsSupplyFull = function(gamestate, supply) +{ + if (supply.isFull() === true) + return true; + var count = supply.resourceSupplyGatherers().length; + if (gamestate.turnCache["ressourceGatherer"] && gamestate.turnCache["ressourceGatherer"][supply.id()]) + count += gamestate.turnCache["ressourceGatherer"][supply.id()]; + if (count >= supply.maxGatherers()) + return true; + return false; +} + +// add a gatherer to the turn cache for this supply. +m.AddTCGatherer = function(gamestate, supplyID) +{ + if (gamestate.turnCache["ressourceGatherer"] && gamestate.turnCache["ressourceGatherer"][supplyID] !== undefined) + ++gamestate.turnCache["ressourceGatherer"][supplyID]; + else + { + if (!gamestate.turnCache["ressourceGatherer"]) + gamestate.turnCache["ressourceGatherer"] = {}; + gamestate.turnCache["ressourceGatherer"][supplyID] = 1; + } +} + +// remove a gatherer to the turn cache for this supply. +m.RemoveTCGatherer = function(gamestate, supplyID) +{ + if (gamestate.turnCache["ressourceGatherer"] && gamestate.turnCache["ressourceGatherer"][supplyID]) + --gamestate.turnCache["ressourceGatherer"][supplyID]; + else + if (!gamestate.turnCache["ressourceGatherer"]) + gamestate.turnCache["ressourceGatherer"] = {}; + gamestate.turnCache["ressourceGatherer"][supplyID] = -1; +} + +m.GetTCGatherer = function(gamestate, supplyID) +{ + if (gamestate.turnCache["ressourceGatherer"] && gamestate.turnCache["ressourceGatherer"][supplyID]) + return gamestate.turnCache["ressourceGatherer"][supplyID]; + else + return 0; +} + +// The next two are to register that we assigned a gatherer to a resource this turn. +m.AddTCRessGatherer = function(gamestate, resource) +{ + if (gamestate.turnCache["ressourceGatherer-" + resource]) + ++gamestate.turnCache["ressourceGatherer-" + resource]; + else + gamestate.turnCache["ressourceGatherer-" + resource] = 1; +} + +m.GetTCRessGatherer = function(gamestate, resource) +{ + if (gamestate.turnCache["ressourceGatherer-" + resource]) + return gamestate.turnCache["ressourceGatherer-" + resource]; + else + return 0; +} + +return m; +}(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js (revision 14865) @@ -0,0 +1,1615 @@ +var PETRA = 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.dockFailed = false; // sanity check + this.waterMap = false; // set by the aegis.js file. + + this.econState = "growth"; // existing values: growth, townPhasing. + this.phaseStarted = undefined; + + // cache the rates. + this.wantedRates = { "food": 0, "wood": 0, "stone":0, "metal": 0 }; + this.currentRates = { "food": 0, "wood": 0, "stone":0, "metal": 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.lastTerritoryUpdate = -1; + this.stopBuilding = []; // list of buildings to stop (temporarily) production because no room + + this.towerStartTime = 0; + this.towerLapseTime = this.Config.Military.towerLapseTime * 1000; + this.fortressStartTime = 0; + this.fortressLapseTime = this.Config.Military.fortressLapseTime * 1000; + + this.baseManagers = {}; + this.attackManager = new m.AttackManager(this.Config); + this.defenseManager = new m.DefenseManager(this.Config); + this.tradeManager = new m.TradeManager(this.Config); + this.navalManager = new m.NavalManager(); + + this.boostedSoldiers = undefined; +}; + +// 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 not or not accessible + this.basesMap = new API3.Map(gameState.sharedScript); + // area of 10 cells on the border of the map : 0=inside map, 1=border map, 2=outside map + this.borderMap = m.createBorderMap(gameState); + // initialize frontier map. Each cell is 2 if on the near frontier, 1 on the frontier and 0 otherwise + this.frontierMap = m.createFrontierMap(gameState, this.borderMap); + + if (this.Config.difficulty === 0) + this.targetNumWorkers = Math.max(1, Math.min(40, Math.floor(gameState.getPopulationMax()))); + else if (this.Config.difficulty === 1) + this.targetNumWorkers = Math.max(1, Math.min(60, Math.floor(gameState.getPopulationMax()))); + else + this.targetNumWorkers = Math.max(1, Math.min(120,Math.floor(gameState.getPopulationMax()/3.0))); + + // 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 ccEnts = ents.filter(API3.Filters.byClass("CivCentre")).toEntityArray(); + + var workersNB = ents.filter(API3.Filters.byClass("Worker")).length; + + for (var i = 0; i < ccEnts.length; ++i) + { + this.baseManagers[i+1] = new m.BaseManager(this.Config); + this.baseManagers[i+1].init(gameState); + this.baseManagers[i+1].setAnchor(gameState, ccEnts[i]); + } + this.updateTerritories(gameState); + + if (this.baseManagers[1]) // Affects units in the different bases + { + var self = this; + var width = gameState.getMap().width; + ents.forEach( function (ent) { + if (ent.hasClass("Trader")) + this.tradeManager.assignTrader(ent); + var pos = ent.position(); + if (!pos) + { + // TODO temporarily assigned to base 1. Certainly a garrisoned unit, + // should assign it to the base of the garrison holder + self.baseManagers[1].assignEntity(ent); + return; + } + var x = Math.round(pos[0] / gameState.cellSize); + var z = Math.round(pos[1] / gameState.cellSize); + var id = x + width*z; + for (var i in self.baseManagers) + { + if (self.baseManagers[i].territoryIndices.indexOf(id) === -1) + continue; + self.baseManagers[i].assignEntity(ent); + if (ent.resourceDropsiteTypes() && !ent.hasClass("Elephant")) + self.baseManagers[i].assignResourceToDropsite(gameState, ent); + return; + } + // entity outside our territory, assign it to base 1 + self.baseManagers[1].assignEntity(ent); + if (ent.resourceDropsiteTypes() && !ent.hasClass("Elephant")) + self.baseManagers[1].assignResourceToDropsite(gameState, ent); + + }); + } + + // we now have enough data to decide on a few things. + + // immediatly build a wood dropsite if possible. + if (this.baseManagers[1]) + { + 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": 1 }, newDP.pos)); + queues.minorTech.addItem(new m.ResearchPlan(gameState, "gather_capacity_wheelbarrow")); + } + } + + this.attackManager.init(gameState, queues); + this.navalManager.init(gameState, queues); + this.defenseManager.init(gameState); + this.tradeManager.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.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']; + + 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.bBase) + this.bBase[i] = gameState.applyCiv(this.bBase[i]); + 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, true); + this.baseManagers[bID].setAnchor(gameState, ent); + + // Let's get a few units out there to build this. + var builders = this.bulkPickWorkers(gameState, bID, 10); + if (builders !== false) + { + builders.forEach(function (worker) { + worker.setMetadata(PlayerID, "base", bID); + worker.setMetadata(PlayerID, "subrole", "builder"); + worker.setMetadata(PlayerID, "target-foundation", ent.id()); + }); + } + } + } + } + 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)) + { + if (ent.getMetadata(PlayerID, "baseAnchor") == true) + { + var base = ent.getMetadata(PlayerID, "base"); + if (this.baseManagers[base].constructing) + this.baseManagers[base].constructing = false; + this.baseManagers[base].anchor = ent; + this.baseManagers[base].buildings.updateEnt(ent); + this.updateTerritories(gameState); + } + else if (ent.hasTerritoryInfluence()) + this.updateTerritories(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; + + this.phaseStarted = 2; +}; + +// Called by the "city phase" research plan once it's started +m.HQ.prototype.OnCityPhase = function(gameState) +{ + if (this.Config.difficulty >= 2 && this.femaleRatio > 0.3) + this.femaleRatio = 0.3; + + this.phaseStarted = 3; +}; + +// 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"); + } + + // 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 (gameState.currentPhase() < 2 || queues.minorTech.length() !== 0) + return; + + var possibilities = gameState.findAvailableTech(); + for (var i = 0; i < possibilities.length; ++i) + { + var techName = possibilities[i][0]; + if (techName.indexOf("attack_tower_watch") !== -1 || techName.indexOf("gather_mining_servants") !== -1 || + techName.indexOf("gather_mining_shaftmining") !== -1) + { + queues.minorTech.addItem(new m.ResearchPlan(gameState, techName)); + return; + } + } + + if (gameState.currentPhase() < 3) + return; + + // remove some tech not yet used by this AI + for (var i = 0; i < possibilities.length; ++i) + { + var techName = possibilities[i][0]; + if (techName.indexOf("heal_rate") !== -1 || techName.indexOf("heal_range") !== -1 || + techName.indexOf("heal_temple") !== -1 || techName.indexOf("unlock_females_house") !== -1) + possibilities.splice(i--, 1); + // temporary hack for upgrade problem TODO fix that + else if (techName.slice(0, 12) === "upgrade_rank") + possibilities.splice(i--, 1); + } + 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])); +}; + + +// returns an entity collection of workers through BaseManager.pickBuilders +// TODO: better the choice algo. +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; + }); + + var needed = number; + var workers = new API3.EntityCollection(gameState.sharedScript); + for (var i in baseBest) + { + baseBest[i].pickBuilders(gameState, workers, needed); + if (workers.length < number) + needed = number - workers.length; + else + break; + } + if (workers.length == 0) + return false; + return workers; +}; + +m.HQ.prototype.GetTotalResourceLevel = function(gameState) +{ + var total = { "food": 0, "wood": 0, "stone": 0, "metal": 0 }; + for (var i in this.baseManagers) + for (var type in total) + total[type] += this.baseManagers[i].getResourceLevel(gameState, type); + + return total; +}; + +// 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) +{ + 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 = this.GetCurrentGatherRates(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(this.bBase[0]), 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, this.bBase[0])); + } + return (gameState.countEntitiesByType(gameState.applyCiv(this.bBase[0]), 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.findEconomicCCLocation = function(gameState, resource) +{ + // 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. + + // create an empty map + var locateMap = new API3.Map(gameState.sharedScript); + locateMap.setMaxVal(255); + // obstruction map + var obstructions = m.createObstructionMap(gameState, 0); + obstructions.expandInfluences(); + // territory map + var territory = m.createTerritoryMap(gameState); + + var ccEnts = gameState.getEntities().filter(API3.Filters.byClass("CivCentre")).toEntityArray(); + var dpEnts = gameState.getOwnDropsites().toEntityArray(); + + for (var j = 0; j < locateMap.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. + + var norm = 0.5; // TODO adjust it, knowing that we will sum 5 maps + if (territory.getOwnerIndex(j) !== 0 || this.borderMap.map[j] === 2) + { + norm = 0; + continue; + } + else if (this.borderMap.map[j] === 1) // disfavor the borders of the map + norm *= 0.5; + + var pos = [j%locateMap.width+0.5, Math.floor(j/locateMap.width)+0.5]; + pos = [gameState.cellSize*pos[0], gameState.cellSize*pos[1]]; + // We require that it is accessible from our starting position + // TODO modify when naval maps + if (gameState.ai.accessibility.getAccessValue(pos) !== gameState.ai.myIndex) + { + norm = 0; + continue; + } + + // checking distance to other cc + var minDist = Math.min(); + for each (var cc in ccEnts) + { + var ccPos = cc.position(); + var dist = API3.SquareVectorDistance(ccPos, pos); + if (dist < 14000) // Reject if too near from any cc + { + norm = 0 + break; + } + if (!gameState.isPlayerAlly(cc.owner())) + continue; + if (dist < 20000) // Reject if too near from an allied cc + { + norm = 0 + break; + } + if (dist < 40000) // Disfavor if quite near an allied cc + norm *= 0.5; + if (dist < minDist) + minDist = dist; + } + if (norm == 0) + continue; + if (minDist > 170000 && !this.waterMap) // Reject if too far from any allied cc (-> not connected) + continue; + else if (minDist > 130000) // Disfavor if quite far from any allied cc + norm *= 0.5; + + for each (var dp in dpEnts) + { + if (dp.hasClass("Elephant")) + continue; + var dpPos = dp.position(); + var dist = API3.SquareVectorDistance(dpPos, pos); + if (dist < 3600) + { + norm = 0; + continue; + } + else if (dist < 6400) + norm *= 0.5; + } + if (norm == 0) + continue; + + var val = 2*gameState.sharedScript.CCResourceMaps[resource].map[j] + + gameState.sharedScript.CCResourceMaps["wood"].map[j] + + gameState.sharedScript.CCResourceMaps["stone"].map[j] + + gameState.sharedScript.CCResourceMaps["metal"].map[j]; + val *= norm; + if (val > 255) + val = 255; + locateMap.map[j] = val; + } + + + var best = locateMap.findBestTile(6, obstructions); + var bestIdx = best[0]; + + if (m.DebugEnabled()) + { + gameState.sharedScript.CCResourceMaps["wood"].dumpIm("woodMap.png", 300); + gameState.sharedScript.CCResourceMaps["stone"].dumpIm("stoneMap.png", 300); + gameState.sharedScript.CCResourceMaps["metal"].dumpIm("metalMap.png", 300); + locateMap.dumpIm("cc_placement_base_" + best[1] + ".png",300); + obstructions.dumpIm("cc_placement_base_" + best[1] + "_obs.png", 20); + } + + if (this.Config.debug) + warn("on a trouve une base avec best (cut=60) = " + best[1]); + // not good enough. + if (best[1] < 60) + return false; + + var bestIdx = best[0]; + var x = ((bestIdx % locateMap.width) + 0.5) * gameState.cellSize; + var z = (Math.floor(bestIdx / locateMap.width) + 0.5) * gameState.cellSize; + if (this.Config.debug) + warn(" avec accessIndex " + gameState.ai.myIndex + " new " + gameState.ai.accessibility.getAccessValue([x,z])); + return [x,z]; +}; + +// Returns the best position to build a new Civil Centre +// Whose primary function would be to assure territorial continuity with our allies +m.HQ.prototype.findStrategicCCLocation = function(gameState) +{ + // This builds a map. The procedure is fairly simple. + // We minimize the Sum((dist-300)**2) where the sum is on all allied CC + // with the constraints that all CC have dist > 200 and at least one have dist < 400 + // This needs at least 2 CC. Otherwise, go back to economic CC. + + // TODO add CC foundations (needed for allied) + var ccEnts = gameState.getEntities().filter(API3.Filters.byClass("CivCentre")).toEntityArray(); + var numAllyCC = 0; + for each (var cc in ccEnts) + if (gameState.isPlayerAlly(cc.owner())) + numAllyCC += 1; + if (numAllyCC < 2) + return this.findEconomicCCLocation(gameState, "wood"); + + // obstruction map + var obstructions = m.createObstructionMap(gameState, 0); + obstructions.expandInfluences(); + // territory map + var territory = m.createTerritoryMap(gameState); + + var map = {}; + var width = territory.width; + + for (var j = 0; j < territory.length; ++j) + { + if (territory.getOwnerIndex(j) !== 0 || this.borderMap.map[j] === 2) + continue; + + var ix = j%width; + var iy = Math.floor(j/width); + var pos = [ix+0.5, iy+0.5]; + pos = [gameState.cellSize*pos[0], gameState.cellSize*pos[1]]; + // We require that it is accessible from our starting position + // TODO modify when naval maps + if (gameState.ai.accessibility.getAccessValue(pos) !== gameState.ai.myIndex) + continue; + + // checking distances to other cc + var minDist = Math.min(); + var sumDelta = 0; + for each (var cc in ccEnts) + { + var ccPos = cc.position(); + var dist = API3.SquareVectorDistance(ccPos, pos); + if (dist < 14000) // Reject if too near from any cc + { + minDist = 0; + break; + } + if (!gameState.isPlayerAlly(cc.owner())) + continue; + if (dist < 40000) // Reject if quite near from ally cc + { + minDist = 0; + break; + } + var delta = Math.sqrt(dist) - 300; + if (cc.owner === PlayerID) // small preference territory continuity with our territory + delta = 1.05*delta; // rather than ally one + sumDelta += delta*delta; + if (dist < minDist) + minDist = dist; + } + if (minDist < 1 || minDist > 160000) + continue; + + map[j] = 10 + sumDelta; + // disfavor border of the map + if (this.borderMap.map[j] === 1) + map[j] = map[j] + 10000; + } + + var bestIdx = undefined; + var bestVal = undefined; + var radius = 6; + for (var i in map) + { + if (obstructions.map[+i] <= radius) + continue; + var v = map[i]; + if (bestVal !== undefined && v > bestVal) + continue; + bestVal = v; + bestIdx = i; + } + + if (this.Config.debug) + warn("on a trouve une base strategic avec bestVal = " + bestVal); + + if (bestVal === undefined) + return undefined; + + var x = (bestIdx%width + 0.5) * gameState.cellSize; + var z = (Math.floor(bestIdx/width) + 0.5) * gameState.cellSize; + if (this.Config.debug) + warn(" avec accessIndex " + gameState.ai.myIndex + " new " + gameState.ai.accessibility.getAccessValue([x,z])); + return [x,z]; +}; + +// Returns the best position to build defensive buildings (fortress and towers) +// Whose primary function is to defend our borders +m.HQ.prototype.findDefensiveLocation = function(gameState, template) +{ + // We take the point in our territory which is the nearest to any enemy cc + // but requiring a minimal distance with our other defensive structures + // and not in range of any enemy defensive structure to avoid building under fire. + + var ownStructures = gameState.getOwnStructures().filter(API3.Filters.byClassesOr(["Fortress", "Tower"])).toEntityArray(); + var enemyStructures = gameState.getEnemyStructures().filter(API3.Filters.byClassesOr(["CivCentre", "Fortress", "Tower"])).toEntityArray(); + + // obstruction map + var obstructions = m.createObstructionMap(gameState, 0); + obstructions.expandInfluences(); + // territory map + var territory = m.createTerritoryMap(gameState); + + var map = {}; + var width = territory.width; + for (var j = 0; j < territory.length; ++j) + { + // do not try if well inside or outside territory + if (this.frontierMap.map[j] === 0) + continue + if (this.frontierMap.map[j] === 1 && template.hasClass("Tower")) + continue; + if (this.basesMap.map[j] === 0) // inaccessible cell + continue; + + var ix = j%width; + var iy = Math.floor(j/width); + var pos = [ix+0.5, iy+0.5]; + pos = [gameState.cellSize*pos[0], gameState.cellSize*pos[1]]; + // checking distances to other structures + var minDist = Math.min(); + for each (var str in enemyStructures) + { + if (str.foundationProgress() !== undefined) + continue; + var strPos = str.position(); + if (!strPos) + continue; + var dist = API3.SquareVectorDistance(strPos, pos); + if (dist < 6400) // TODO check on true attack range instead of this 80*80 + { + minDist = -1; + break; + } + if (str.hasClass("CivCentre") && dist < minDist) + minDist = dist; + } + if (minDist < 0) + continue; + + for each (var str in ownStructures) + { + if ((template.hasClass("Tower") && str.hasClass("Tower")) || (template.hasClass("Fortress") && str.hasClass("Fortress"))) + { + var strPos = str.position(); + if (!strPos) + continue; + var dist = API3.SquareVectorDistance(strPos, pos); + if (dist < 4225) // TODO check on true buildrestrictions instead of this 65*65 + { + minDist = -1; + break; + } + } + } + if (minDist < 0) + continue; + + map[j] = minDist; + } + + var bestIdx = undefined; + var bestVal = undefined; + if (template.hasClass("Fortress")) + var radius = Math.floor(template.obstructionRadius() / gameState.cellSize) + 2; + else + var radius = Math.ceil(template.obstructionRadius() / gameState.cellSize); + + for (var j in map) + { + if (obstructions.map[+j] <= radius) + continue; + var v = map[j]; + if (bestVal !== undefined && v > bestVal) + continue; + bestVal = v; + bestIdx = j; + } + + if (bestVal === undefined) + return undefined; + + var x = (bestIdx%width + 0.5) * gameState.cellSize; + var z = (Math.floor(bestIdx/width) + 0.5) * gameState.cellSize; + return [x, z, this.basesMap.map[bestIdx]]; +}; + +m.HQ.prototype.buildTemple = function(gameState, queues) +{ + if (gameState.currentPhase() < 3 || queues.economicBuilding.countQueuedUnits() !== 0 || + gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_temple"), true) !== 0) + return; + if (!this.canBuild(gameState, "structures/{civ}_temple")) + return; + queues.economicBuilding.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_temple")); + // add the health regeneration to the research we want. + if (!gameState.isResearched("health_regen_units") && !gameState.isResearching("health_regen_units")) + queues.minorTech.addItem(new m.ResearchPlan(gameState, "health_regen_units")); +}; + +m.HQ.prototype.buildMarket = function(gameState, queues) +{ + if (gameState.getPopulation() < this.Config.Economy.popForMarket || gameState.currentPhase() < 2 || + queues.economicBuilding.countQueuedUnitsWithClass("BarterMarket") !== 0 || + gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_market"), true) !== 0) + return; + if (!this.canBuild(gameState, "structures/{civ}_market")) + return; + queues.economicBuilding.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_market")); +}; + +// 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) +{ + // Only build one farmstead for the time being ("DropsiteFood" does not refer to CCs) + if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_farmstead"), true) > 0) + return; + // Wait to have at least one house before the farmstead + if (gameState.countEntitiesByType(gameState.applyCiv("foundation|structures/{civ}_house"), true) == 0) + return; + if (queues.economicBuilding.countQueuedUnitsWithClass("DropsiteFood") > 0) + return; + if (!this.canBuild(gameState, "structures/{civ}_farmstead")) + return; + + queues.economicBuilding.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_farmstead")); + // add the farming plough to the research we want. + if (!gameState.isResearched("gather_farming_plows") && !gameState.isResearching("gather_farming_plows")) + queues.minorTech.addItem(new m.ResearchPlan(gameState, "gather_farming_plows")); +}; + +// 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 !== "" && this.canBuild(gameState, 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 buy in elemCost.types) + { + if (available[buy] >= 0) + continue; // don't care if we still have available resource + var need = elemCost[buy] - account[buy]; + if (need <= 0 || 50*rates[buy] > need) // don't care if we don't need resource or our rate is good enough + continue; + + if (buy == "food" && need < 400) + continue; + + // pick the best resource to barter. + var bestToBarter = undefined; + var bestRate = 0; + for each (var sell in elemCost.types) + { + if (sell === buy) + continue; + // I wanna keep some + if (available[sell] < 130 + need) + continue; + var barterRate = getBarterRate(prices, buy, sell); + if (barterRate > bestRate) + { + bestRate = barterRate; + bestToBarter = otherRess; + } + } + if (bestToBarter !== undefined && bestRate > 10) + { + markets[0].barter(buy, sell, 100); + if (this.Config.debug > 0) + warn("Snipe bartered " + sell +" for " + buy + ", value 100" + " with barterRate " + bestRate); + return true; + } + } + } + // now barter for big needs. + var needs = gameState.ai.queueManager.currentNeeds(gameState); + for each (var buy in needs.types) + { + if (needs[buy] == 0 || needs[buy] < rates[buy]*30) // check if our rate allows to gather it fast enough + continue; + + // pick the best resource to barter. + var bestToSell = undefined; + var bestRate = 0; + for each (var sell in needs.types) + { + if (sell === buy) + continue; + if (needs[sell] > 0 || available[sell] < 500) // do not sell if we need it or do not have enough buffer + continue; + + var barterRateMin = 70; + if (available > 1000) + barterRateMin = 50; + if (sell === "food") + barterRateMin -= 40; + else if (buy === "food") + barterRateMin += 10; + + var barterRate = getBarterRate(prices, buy, sell); + if (barterRate > bestRate && barterRate > barterRateMin) + { + bestRate = barterRate; + bestToSell = sell; + } + } + + if (bestToSell !== undefined) + { + markets[0].barter(buy, bestToSell, 100); + if (this.Config.debug > 0) + warn("Gross bartered: sold " + bestToSell +" for " + buy + " >> need sell " + needs[bestToSell] + + " buy " + needs[buy] + " rate " + rates[buy] + " available sell " + available[bestToSell] + + " buy " + available[buy] + " barterRate " + bestRate); + return true; + } + } + return false; +}; + +// Try to setup trade routes TODO complete it +// TODO use also docks (should be counted in Class("Market"), but may be build one when necessary +m.HQ.prototype.buildTradeRoute = function(gameState, queues) +{ + var market1 = gameState.getOwnStructures().filter(API3.Filters.and(API3.Filters.byClass("Market"), API3.Filters.not(API3.Filters.isFoundation()))).toEntityArray(); + var market2 = gameState.getAllyEntities().filter(API3.Filters.and(API3.Filters.byClass("Market"), API3.Filters.not(API3.Filters.isFoundation()))).toEntityArray(); + if (market1.length < 1) // We have to wait ... the first market will be built when needed conditions are satisfied + return false; + + var needed = 2; + if (market2.length > 0) + var needed = 1; + if (market1.length < needed) + { + // TODO what to do if market1 is invalid ??? should not happen + if (!market1[0] || !market1[0].position()) + return false; + // We require at least two finished bases + if (!this.baseManagers[2] || this.baseManagers[2].constructing) + return false; + if (queues.economicBuilding.countQueuedUnitsWithClass("Market") > 0 || + gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_market"), true) >= needed) + return false; + if (!this.canBuild(gameState, "structures/{civ}_market")) + return false; + // We have to build a second market ... try to put it as far as possible from the first one + // TODO improve, for the time being, we affect it to the farthest base + var marketBase = market1[0].getMetadata(PlayerID, "base"); + var distmax = -1; + var base = -1; + for (var i in this.baseManagers) + { + if (marketBase === +i) + continue; + if (!this.baseManagers[i].anchor || !this.baseManagers[i].anchor.position()) + continue; + var dist = API3.SquareVectorDistance(market1[0].position(), this.baseManagers[i].anchor.position()); + if (dist < distmax) + continue; + distmax = dist; + base = +i; + } + if (distmax > 0) + { + if (this.Config.debug) + warn(" a second market will be built in base " + base); + // TODO build also docks when better + queues.economicBuilding.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_market", { "base": base })); + } + return false; + } + + if (market2.length === 0) + market2 = market1; + var distmax = -1; + var imax = -1; + var jmax = -1; + for each (var m1 in market1) + { + if (!m1.position()) + continue; + for each (var m2 in market2) + { + if (m1.id() === m2.id()) + continue; + if (!m2.position()) + continue; + var dist = API3.SquareVectorDistance(m1.position(), m2.position()); + if (dist < distmax) + continue; + distmax = dist; + this.tradeManager.setTradeRoute(m1, m2); + } + } + if (distmax < 0) + { + if (this.Config.debug) + warn("no trade route possible"); + return false; + } + if (this.Config.debug) + warn("one trade route set"); + return true; +}; + +// 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.getPopulationMax() < gameState.getPopulationLimit()) + { + var numPlanned = queues.house.length(); + if (numPlanned) + warn(" ######## Houses planned while already max pop !! remove them from queue"); + } + if (gameState.getPopulationMax() < gameState.getPopulationLimit()) + return; + + var numPlanned = queues.house.length(); + if (numPlanned < 3 || (numPlanned < 5 && gameState.getPopulation() > 80)) + { + var plan = new m.ConstructionPlan(gameState, "structures/{civ}_house"); + // make the difficulty available to the isGo function without having to pass it as argument + var difficulty = this.Config.difficulty; + var self = this; + // change the starting condition to "less than 15 slots left". + plan.isGo = function (gameState) { + if (!self.canBuild(gameState, "structures/{civ}_house")) + return false; + var HouseNb = gameState.countEntitiesByType(gameState.applyCiv("foundation|structures/{civ}_house"), true); + + var freeSlots = 0; + // TODO get this info from PopulationBonus of houses + if (gameState.civ() == "gaul" || gameState.civ() == "brit" || gameState.civ() == "iber" || + gameState.civ() == "maur" || gameState.civ() == "ptol") + var popBonus = 5; + else + var popBonus = 10; + freeSlots = gameState.getPopulationLimit() + HouseNb*popBonus - 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; + 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 the status of the territory expansion. If no new economic bases created, build some strategic ones. +m.HQ.prototype.checkBaseExpansion = function(gameState,queues) +{ + var numUnits = gameState.getOwnUnits().length; + var numCCs = gameState.countEntitiesByType(gameState.applyCiv(this.bBase[0]), true); + if (Math.floor(numUnits/60) >= numCCs) + this.buildNewBase(gameState, queues); +}; + +m.HQ.prototype.buildNewBase = function(gameState, queues, type) +{ + if (gameState.currentPhase() === 1 && !gameState.isResearching(gameState.townPhase())) + return false; + if (gameState.countFoundationsByType(gameState.applyCiv(this.bBase[0]), true) !== 0 || queues.civilCentre.length() !== 0) + return false; + if (!this.canBuild(gameState, this.bBase[0])) + return false; + + // base "-1" means new base. + if (this.Config.debug > 0) + warn("new base planned with type " + type); + queues.civilCentre.addItem(new m.ConstructionPlan(gameState, this.bBase[0], { "base": -1, "type": type })); + return true; +}; + +// Deals with building fortresses and towers along our border with enemies. +m.HQ.prototype.buildDefenses = function(gameState, queues) +{ + if (gameState.currentPhase() > 2 || gameState.isResearching(gameState.cityPhase())) + { + // try to build fortresses + if (queues.defenseBuilding.length() === 0 && this.canBuild(gameState, this.bFort[0])) + { + var numFortresses = 0; + for (var i in this.bFort) + numFortresses += gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bFort[i]), true); + + if (gameState.getTimeElapsed() > (1 + 0.05*numFortresses)*this.fortressLapseTime + this.fortressStartTime) + { + this.fortressStartTime = gameState.getTimeElapsed(); + // TODO should affect it to the right base + queues.defenseBuilding.addItem(new m.ConstructionPlan(gameState, this.bFort[0])); + } + } + + // let's add a siege building plan to the current attack plan if there is none currently. + var numSiegeBuilder = 0; + if (gameState.civ() === "mace") + numSiegeBuilder = gameState.countEntitiesByType(gameState.applyCiv("siege_workshop"), true); + else + for (var i in this.bFort) + numSiegeBuilder += gameState.countEntitiesByType(gameState.applyCiv(this.bFort[i]), true); + + if (numSiegeBuilder > 0) + { + if (this.attackManager.upcomingAttacks["CityAttack"].length !== 0) + { + var attack = this.attackManager.upcomingAttacks["CityAttack"][0]; + if (!attack.unitStat["Siege"]) + attack.addSiegeUnits(gameState); + } + } + } + + if (gameState.currentPhase() < 2 || queues.defenseBuilding.length() !== 0 || !this.canBuild(gameState, "structures/{civ}_defense_tower")) + return; + + var numTowers = gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_defense_tower"), true); + if (gameState.getTimeElapsed() > (1 + 0.05*numTowers)*this.towerLapseTime + this.towerStartTime) + { + this.towerStartTime = gameState.getTimeElapsed(); + queues.defenseBuilding.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_defense_tower")); + } + // TODO otherwise protect markets and civilcentres +}; + +m.HQ.prototype.buildBlacksmith = function(gameState, queues) +{ + if (gameState.getTimeElapsed() < this.Config.Military.timeForBlacksmith*1000 || queues.militaryBuilding.length() !== 0 + || gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_blacksmith"), true) > 0) + return; + + if (this.canBuild(gameState, "structures/{civ}_blacksmith")) + queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_blacksmith")); +}; + +// 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) +{ + var workersNumber = gameState.getOwnEntitiesByRole("worker", true).filter(API3.Filters.not(API3.Filters.byHasMetadata(PlayerID, "plan"))).length; + + var barrackNb = gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_barracks"), true); + var bestBase = this.findBestBaseForMilitary(gameState); + + if (this.canBuild(gameState, "structures/{civ}_barracks")) + { + // 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) + { + var plan = new m.ConstructionPlan(gameState, "structures/{civ}_barracks", { "base" : bestBase }); + 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, "structures/{civ}_barracks", { "base" : bestBase })); + + // 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, "structures/{civ}_barracks", { "base" : bestBase })); + if (gameState.civ() == "gaul" || gameState.civ() == "brit" || gameState.civ() == "iber") + { + queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_barracks", { "base" : bestBase })); + queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_barracks", { "base" : bestBase })); + } + } + } + + //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 && + this.canBuild(gameState, this.bAdvanced[i])) + queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, this.bAdvanced[i], { "base" : bestBase })); + } + } + } + // build second advanced building except for some civs. + if (gameState.currentPhase() > 2 && gameState.civ() !== "gaul" && gameState.civ() !== "brit" && gameState.civ() !== "iber" && workersNumber > 130) + { + 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 && + this.canBuild(gameState, this.bAdvanced[i])) + queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, this.bAdvanced[i], { "base" : bestBase })); + } + } +}; + +/** + * Construct military building in bases nearest to the ennemies TODO revisit as the nearest one may not be accessible + */ +m.HQ.prototype.findBestBaseForMilitary = function(gameState) +{ + var ccEnts = gameState.getEntities().filter(API3.Filters.byClass("CivCentre")).toEntityArray(); + var bestBase = 1; + var distMin = Math.min(); + for each (var cc in ccEnts) + { + if (cc.owner() != PlayerID) + continue; + for each (var cce in ccEnts) + { + if (gameState.isPlayerAlly(cce.owner())) + continue; + var dist = API3.SquareVectorDistance(cc.position(), cce.position()); + if (dist < distMin) + { + bestBase = cc.getMetadata(PlayerID, "base"); + distMin = dist; + } + } + } + return bestBase; +}; + +m.HQ.prototype.boostSoldiers = function(gameState, val, emergency) +{ + if (this.boostedSoldiers && this.boostedSoldiers >= val) + return; + if (!this.boostedSoldiers) + this.nominalSoldierPriority = this.Config.priorities.citizenSoldier; + this.boostedSoldiers = val; + gameState.ai.queueManager.changePriority("citizenSoldier", val); + if (!emergency) + return; + + // Emergency: reset accounts from all other queues + for (var p in gameState.ai.queueManager.queues) + if (p != "citizenSoldier") + gameState.ai.queueManager.accounts[p].reset(); +}; + +m.HQ.prototype.unboostSoldiers = function(gameState) +{ + if (!this.boostedSoldiers) + return; + gameState.ai.queueManager.changePriority("citizenSoldier", this.nominalSoldierPriority); + this.boostedSoldiers = undefined; +}; + +m.HQ.prototype.canBuild = function(gameState, structure) +{ + var type = gameState.applyCiv(structure); + // available room to build it + if (this.stopBuilding.indexOf(type) !== -1) + return false; + + // build limits + var template = gameState.getTemplate(type); + if (!template.available(gameState)) + return false; + var limits = gameState.getEntityLimits(); + for (var limitClass in limits) + if (template.hasClass(limitClass) && gameState.getOwnStructures().filter(API3.Filters.byClass(limitClass)).length >= limits[limitClass]) + return false; + +/* if (structure.indexOf("embassy") !== -1) + { + warn(" structure " + structure + " type " + type); + for (var limitClass in limits) + { + warn(" limitClass " + limitClass + " template ? " + template.hasClass(limitClass) + " num " + gameState.countEntitiesAndQueuedByType(type, true) + " limite " + limits[limitClass]); + warn(" ---- autre compte " + gameState.getOwnStructures().filter(API3.Filters.byClass(limitClass)).length); + } + } */ + + return true; +}; + +m.HQ.prototype.updateTerritories = function(gameState) +{ + // TODO may-be update also when territory decreases. For the moment, only increases are taking into account + if (this.lastTerritoryUpdate == gameState.ai.playedTurn) + return; + this.lastTerritoryUpdate = gameState.ai.playedTurn; + + var territory = m.createTerritoryMap(gameState); + var width = territory.width; + var expansion = false; + for (var j = 0; j < territory.length; ++j) + { + if (this.borderMap.map[j] === 2) + continue; + if (territory.getOwnerIndex(j) !== PlayerID) + { + if (this.basesMap.map[j] === 0) + continue; + var baseID = this.basesMap.map[j]; + var index = this.baseManagers[baseID].territoryIndices.indexOf(j); + if (index === -1) + { + warn(" problem in headquarters::updateTerritories for base " + baseID); + continue; + } + this.baseManagers[baseID].territoryIndices.splice(index, 1); + this.basesMap.map[j] = 0; + } + else if (this.basesMap.map[j] === 0) + { + var distmin = Math.min(); + var baseID = undefined; + var ix = j%width; + var iy = Math.floor(j/width); + + var pos = [ix+0.5, iy+0.5]; + pos = [gameState.cellSize*pos[0], gameState.cellSize*pos[1]]; + for each (var base in this.baseManagers) + { + if (!base.anchor || !base.anchor.position()) + continue; + if (base.accessIndex !== gameState.ai.accessibility.getAccessValue(pos)) + continue; + var dist = API3.SquareVectorDistance(base.anchor.position(), pos); + if (dist >= distmin) + continue; + distmin = dist; + baseID = base.ID; + } + if (!baseID) + continue; + this.baseManagers[baseID].territoryIndices.push(j); + this.basesMap.map[j] = baseID; + expansion = true; + } + } + + this.frontierMap = m.createFrontierMap(gameState, this.borderMap); + + if (!expansion) + return; + // We've increased our territory, so we may have some new room to build + if (this.Config.debug) + warn(" buildings stopped " + uneval(this.stopBuilding)); + this.stopBuilding = []; +}; + +// 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) { + if (!ent.position()) + return; + for (var i in buildings) + { + 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(); + }); +}; + +// Some functions are run every turn +// Others once in a while +m.HQ.prototype.update = function(gameState, queues, events) +{ + Engine.ProfileStart("Headquarters update"); + + if (this.Config.debug > 0) + { + gameState.getOwnUnits().forEach (function (ent) { + return; + if (!ent.isIdle()) + { + ent.setMetadata(PlayerID, "lastIdle", undefined); + return; + } + if (ent.hasClass("Animal")) + return; + if (!ent.getMetadata(PlayerID, "lastIdle")) + { + ent.setMetadata(PlayerID, "lastIdle", gameState.ai.playedTurn); + return; + } + if (gameState.ai.playedTurn - ent.getMetadata(PlayerID, "lastIdle") < 20) + return; + warn(" unit idle since " + (gameState.ai.playedTurn-ent.getMetadata(PlayerID, "lastIdle")) + " turns"); + warn(" >>> base " + ent.getMetadata(PlayerID, "base")); + warn(" >>> role " + ent.getMetadata(PlayerID, "role")); + warn(" >>> subrole " + ent.getMetadata(PlayerID, "subrole")); + warn(" >>> gather-type " + ent.getMetadata(PlayerID, "gather-type")); + warn(" >>> target-foundation " + ent.getMetadata(PlayerID, "target-foundation")); + warn(" >>> PartOfArmy " + ent.getMetadata(PlayerID, "PartOfArmy")); + warn(" >>> plan " + ent.getMetadata(PlayerID, "plan")); + ent.setMetadata(PlayerID, "lastIdle", gameState.ai.playedTurn); + }); + } + + this.checkEvents(gameState,events,queues); + + // TODO find a better way to update + if (this.phaseStarted && gameState.currentPhase() === this.phaseStarted) + { + this.phaseStarted = undefined; + this.updateTerritories(gameState); + } + else if (gameState.ai.playedTurn - this.lastTerritoryUpdate > 100) + this.updateTerritories(gameState); + + this.trainMoreWorkers(gameState, queues); + + // sandbox doesn't expand. + if (this.Config.difficulty !== 0 && gameState.ai.playedTurn % 10 === 7) + this.checkBaseExpansion(gameState, queues); + + if (gameState.ai.playedTurn % 2 === 0) + this.buildMoreHouses(gameState,queues); + + if (queues.minorTech.length() === 0 && gameState.ai.playedTurn % 5 === 1) + this.tryResearchTechs(gameState,queues); + + if (this.Config.difficulty > 1) + { + this.tryBartering(gameState); + if (!this.tradeManager.hasTradeRoute() && gameState.ai.playedTurn % 5 === 2) + this.buildTradeRoute(gameState, queues); + this.tradeManager.update(gameState, queues); + } + + this.buildFarmstead(gameState, queues); + this.buildMarket(gameState, queues); + this.buildTemple(gameState, queues); + this.buildDock(gameState, queues); // not if not a water map. + + this.constructTrainingBuildings(gameState, queues); + + this.buildBlacksmith(gameState, queues); + + if (this.Config.difficulty > 0) + this.buildDefenses(gameState, queues); + + 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.defenseManager.update(gameState, events, this); + + if (this.Config.difficulty > 0) + this.attackManager.update(gameState, queues, events); + + Engine.ProfileStop(); // Heaquarters update +}; + +return m; + +}(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/map-module.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/map-module.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/map-module.js (revision 14865) @@ -0,0 +1,237 @@ +var PETRA = function(m) +{ + +// other map functions +m.TERRITORY_PLAYER_MASK = 0x3F; + +m.createObstructionMap = function(gameState, accessIndex, template){ + var passabilityMap = gameState.getMap(); + var territoryMap = gameState.ai.territoryMap; + + // default values + var placementType = "land"; + var buildOwn = true; + var buildAlly = true; + var buildNeutral = true; + var buildEnemy = false; + // If there is a template then replace the defaults + if (template) + { + placementType = template.buildPlacementType(); + buildOwn = template.hasBuildTerritory("own"); + buildAlly = template.hasBuildTerritory("ally"); + buildNeutral = template.hasBuildTerritory("neutral"); + buildEnemy = template.hasBuildTerritory("enemy"); + } + + var obstructionMask = gameState.getPassabilityClassMask("foundationObstruction") | gameState.getPassabilityClassMask("building-land"); + + if (placementType == "shore") + { + // TODO: this won't change much, should be cached, it's slow. + var obstructionTiles = new Uint8Array(passabilityMap.data.length); + var okay = false; + for (var x = 0; x < passabilityMap.width; ++x) + { + for (var y = 0; y < passabilityMap.height; ++y) + { + var i = x + y*passabilityMap.width; + var tilePlayer = (territoryMap.data[i] & m.TERRITORY_PLAYER_MASK); + + if (gameState.ai.myIndex !== gameState.ai.accessibility.landPassMap[i]) + { + obstructionTiles[i] = 0; + continue; + } + if (gameState.isPlayerEnemy(tilePlayer) && tilePlayer !== 0) + { + obstructionTiles[i] = 0; + continue; + } + if ((passabilityMap.data[i] & (gameState.getPassabilityClassMask("building-shore") | gameState.getPassabilityClassMask("default")))) + { + obstructionTiles[i] = 0; + continue; + } + + okay = false; + var positions = [[0,1], [1,1], [1,0], [1,-1], [0,-1], [-1,-1], [-1,0], [-1,1]]; + var available = 0; + for each (var stuff in positions) + { + var index = x + stuff[0] + (y+stuff[1])*passabilityMap.width; + var index2 = x + stuff[0]*2 + (y+stuff[1]*2)*passabilityMap.width; + var index3 = x + stuff[0]*3 + (y+stuff[1]*3)*passabilityMap.width; + var index4 = x + stuff[0]*4 + (y+stuff[1]*4)*passabilityMap.width; + + if ((passabilityMap.data[index] & gameState.getPassabilityClassMask("default")) && gameState.ai.accessibility.getRegionSizei(index,true) > 500) + if ((passabilityMap.data[index2] & gameState.getPassabilityClassMask("default")) && gameState.ai.accessibility.getRegionSizei(index2,true) > 500) + if ((passabilityMap.data[index3] & gameState.getPassabilityClassMask("default")) && gameState.ai.accessibility.getRegionSizei(index3,true) > 500) + if ((passabilityMap.data[index4] & gameState.getPassabilityClassMask("default")) && gameState.ai.accessibility.getRegionSizei(index4,true) > 500) + { + if (available < 2) + available++; + else + okay = true; + } + } + // checking for accessibility: if a neighbor is inaccessible, this is too. If it's not on the same "accessible map" as us, we crash-i~u. + var radius = 3; + for (var xx = -radius;xx <= radius; xx++) + for (var yy = -radius;yy <= radius; yy++) + { + var id = x + xx + (y+yy)*passabilityMap.width; + if (id > 0 && id < passabilityMap.data.length) + if (gameState.ai.terrainAnalyzer.map[id] === 0 || gameState.ai.terrainAnalyzer.map[id] == 30 || gameState.ai.terrainAnalyzer.map[id] == 40) + okay = false; + } + obstructionTiles[i] = okay ? 255 : 0; + } + } + } + else + { + var playerID = PlayerID; + + var obstructionTiles = new Uint8Array(passabilityMap.data.length); + for (var i = 0; i < passabilityMap.data.length; ++i) + { + var tilePlayer = (territoryMap.data[i] & m.TERRITORY_PLAYER_MASK); + var invalidTerritory = ( + (!buildOwn && tilePlayer == playerID) || + (!buildAlly && gameState.isPlayerAlly(tilePlayer) && tilePlayer != playerID) || + (!buildNeutral && tilePlayer == 0) || + (!buildEnemy && gameState.isPlayerEnemy(tilePlayer) && tilePlayer != 0) + ); + if (accessIndex) + var tileAccessible = (accessIndex === gameState.ai.accessibility.landPassMap[i]); + else + var tileAccessible = true; + if (placementType === "shore") + tileAccessible = true; + obstructionTiles[i] = (!tileAccessible || invalidTerritory || (passabilityMap.data[i] & obstructionMask)) ? 0 : 255; + } + } + + var map = new API3.Map(gameState.sharedScript, obstructionTiles); + map.setMaxVal(255); + + if (template && template.buildDistance()) { + var minDist = template.buildDistance().MinDistance; + var category = template.buildDistance().FromCategory; + if (minDist !== undefined && category !== undefined){ + gameState.getOwnStructures().forEach(function(ent) { + if (ent.buildCategory() === category && ent.position()){ + var pos = ent.position(); + var x = Math.round(pos[0] / gameState.cellSize); + var z = Math.round(pos[1] / gameState.cellSize); + map.addInfluence(x, z, minDist/gameState.cellSize, -255, 'constant'); + } + }); + } + } + + return map; +}; + + +m.createTerritoryMap = function(gameState) { + var map = gameState.ai.territoryMap; + + var ret = new API3.Map(gameState.sharedScript, map.data); + ret.getOwner = function(p) { return this.point(p) & m.TERRITORY_PLAYER_MASK; }; + ret.getOwnerIndex = function(p) { return this.map[p] & m.TERRITORY_PLAYER_MASK; }; + return ret; +}; + +m.createFrontierMap = function(gameState, borderMap) +{ + var territory = m.createTerritoryMap(gameState); + var 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] ]; + + var map = new API3.Map(gameState.sharedScript); + var width = map.width; + var insideSmall = 10; + var insideLarge = 15; + + for (var j = 0; j < territory.length; ++j) + { + if (territory.getOwnerIndex(j) !== PlayerID || borderMap.map[j] === 2) + continue; + var ix = j%width; + var iz = Math.floor(j/width); + for each (var a in around) + { + var jx = ix + Math.round(insideSmall*a[0]); + if (jx < 0 || jx >= width) + continue; + var jz = iz + Math.round(insideSmall*a[1]); + if (jz < 0 || jz >= width) + continue; + if (borderMap && borderMap.map[jx+width*jz] > 1) + continue; + if (!gameState.isPlayerAlly(territory.getOwnerIndex(jx+width*jz))) + { + map.map[j] = 2; + break; + } + jx = ix + Math.round(insideLarge*a[0]); + if (jx < 0 || jx >= width) + continue; + jz = iz + Math.round(insideLarge*a[1]); + if (jz < 0 || jz >= width) + continue; + if (borderMap && borderMap.map[jx+width*jz] > 1) + continue; + if (!gameState.isPlayerAlly(territory.getOwnerIndex(jx+width*jz))) + map.map[j] = 1; + } + } + +// m.debugMap(gameState, map); + return map; +}; + +// TODO foresee the case of square maps +m.createBorderMap = function(gameState) +{ + var map = new API3.Map(gameState.sharedScript); + var width = map.width; + var ic = (width - 1) / 2; + var radmax = (ic-2)*(ic-2); // we assume two inaccessible cells all around + for (var j = 0; j < map.length; ++j) + { + var dx = j%width - ic; + var dy = Math.floor(j/width) - ic; + var radius = dx*dx + dy*dy; + if (radius > radmax) + map.map[j] = 2; + else if (radius > (ic - 18)*(ic - 18)) + map.map[j] = 1; + } + + return map; +}; + +m.debugMap = function(gameState, map) +{ + var width = map.width; + var cell = map.cellSize; + gameState.getEntities().forEach( function (ent) { + var pos = ent.position(); + if (!pos) + return; + var x = Math.round(pos[0] / cell); + var z = Math.round(pos[1] / cell); + var id = x + width*z; + if (map.map[id] == 1) + Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [2,0,0]}); + else if (map.map[id] == 2) + Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,2,0]}); + else if (map.map[id] == 3) + Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,0,2]}); + }); +}; + +return m; +}(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/navalManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/navalManager.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/navalManager.js (revision 14865) @@ -0,0 +1,290 @@ +var PETRA = function(m) +{ + +/* Naval Manager + Will deal with anything ships. + -Basically trade over water (with fleets and goals commissioned by the economy manager) + -Defense over water (commissioned by the defense manager) + -subtask being patrols, escort, naval superiority. + -Transport of units over water (a few units). + -Scouting, ultimately. + Also deals with handling docks, making sure we have access and stuffs like that. + Does not build them though, that's for the base manager to handle. + */ + +m.NavalManager = function() { + // accessibility zones for which we have a dock. + // Connexion is described as [landindex] = [seaIndexes]; + // technically they also exist for sea zones but I don't care. + this.landZoneDocked = []; + + // list of seas I have a dock on. + this.accessibleSeas = []; + + // ship subCollections. Also exist for land zones, idem, not caring. + this.seaShips = []; + this.seaTpShips = []; + this.seaWarships = []; + + // wanted NB per zone. + this.wantedTpShips = []; + this.wantedWarships = []; + + this.transportPlans = []; + this.askedPlans = []; +}; + +// More initialisation for stuff that needs the gameState +m.NavalManager.prototype.init = function(gameState, queues) { + // finished docks + this.docks = gameState.getOwnStructures().filter(API3.Filters.and(API3.Filters.byClass("Dock"), API3.Filters.not(API3.Filters.isFoundation()))); + this.docks.allowQuickIter(); + this.docks.registerUpdates(); + + this.ships = gameState.getOwnEntities().filter(API3.Filters.byClass("Ship")); + // note: those two can overlap (some transport ships are warships too and vice-versa). + this.tpShips = this.ships.filter(API3.Filters.byCanGarrison()); + this.warships = this.ships.filter(API3.Filters.byClass("Warship")); + + this.ships.registerUpdates(); + this.tpShips.registerUpdates(); + this.warships.registerUpdates(); + + for (var i = 0; i < gameState.ai.accessibility.regionSize.length; ++i) + { + if (gameState.ai.accessibility.regionType[i] !== "water") + { + // push dummies + this.seaShips.push(new API3.EntityCollection(gameState.sharedScript)); + this.seaTpShips.push(new API3.EntityCollection(gameState.sharedScript)); + this.seaWarships.push(new API3.EntityCollection(gameState.sharedScript)); + this.wantedTpShips.push(0); + this.wantedWarships.push(0); + } else { + var collec = this.ships.filter(API3.Filters.byStaticMetadata(PlayerID, "sea", i)); + collec.registerUpdates(); + this.seaShips.push(collec); + collec = this.tpShips.filter(API3.Filters.byStaticMetadata(PlayerID, "sea", i)); + collec.registerUpdates(); + this.seaTpShips.push(collec); + var collec = this.warships.filter(API3.Filters.byStaticMetadata(PlayerID, "sea", i)); + collec.registerUpdates(); + this.seaWarships.push(collec); + + this.wantedTpShips.push(1); + this.wantedWarships.push(1); + } + + this.landZoneDocked.push([]); + } +}; + +m.NavalManager.prototype.getUnconnectedSeas = function (gameState, region) { + var seas = gameState.ai.accessibility.regionLinks[region] + if (seas.length === 0) + return []; + for (var i = 0; i < seas.length; ++i) + { + if (this.landZoneDocked[region].indexOf(seas[i]) !== -1) + seas.splice(i--,1); + } + return seas; +}; + +// returns true if there is a path from A to B and we have docks. +m.NavalManager.prototype.canReach = function (gameState, regionA, regionB) { + var path = gameState.ai.accessibility.getTrajectToIndex(regionA, regionB); + if (!path) + { + return false; + } + for (var i = 0; i < path.length - 1; ++i) + { + if (gameState.ai.accessibility.regionType[path[i]] == "land") + if (this.accessibleSeas.indexOf(path[i+1]) === -1) + { + m.debug ("cannot reach because of " + path[i+1]); + return false; // we wn't be able to board on that sea + } + } + return true; +}; + + +m.NavalManager.prototype.checkEvents = function (gameState, queues, events) { + var evts = events["ConstructionFinished"]; + // TODO: probably check stuffs like a base destruction. + for (var i in evts) + { + var evt = evts[i]; + if (evt && evt.newentity) + { + var entity = gameState.getEntityById(evt.newentity); + if (entity && entity.hasClass("Dock") && entity.isOwn(PlayerID)) + { + // okay we have a dock whose construction is finished. + // let's assign it to us. + var pos = entity.position(); + var li = gameState.ai.accessibility.getAccessValue(pos); + var ni = entity.getMetadata(PlayerID, "sea"); + if (this.landZoneDocked[li].indexOf(ni) === -1) + this.landZoneDocked[li].push(ni); + if (this.accessibleSeas.indexOf(ni) === -1) + this.accessibleSeas.push(ni); + } + } + } +}; + +m.NavalManager.prototype.addPlan = function(plan) { + this.transportPlans.push(plan); +}; + +// will create a plan at the end of the turn. +// many units can call this separately and end up in the same plan +// which can be useful. +m.NavalManager.prototype.askForTransport = function(entity, startPos, endPos) { + this.askedPlans.push([entity, startPos, endPos]); +}; + +// creates aforementionned plans +m.NavalManager.prototype.createPlans = function(gameState) { + var startID = {}; + + for (var i in this.askedPlans) + { + var plan = this.askedPlans[i]; + var startIndex = gameState.ai.accessibility.getAccessValue(plan[1]); + var endIndex = gameState.ai.accessibility.getAccessValue(plan[2]); + if (startIndex === 1 || endIndex === -1) + continue; + if (!startID[startIndex]) + { + startID[startIndex] = {}; + startID[startIndex][endIndex] = { "dest" : plan[2], "units": [plan[0]]}; + } + else if (!startID[startIndex][endIndex]) + startID[startIndex][endIndex] = { "dest" : plan[2], "units": [plan[0]]}; + else + startID[startIndex][endIndex].units.push(plan[0]); + } + for (var i in startID) + for (var k in startID[i]) + { + var tpPlan = new m.TransportPlan(gameState, startID[i][k].units, startID[i][k].dest, false) + this.transportPlans.push (tpPlan); + } +}; + +// TODO: work on this. +m.NavalManager.prototype.maintainFleet = function(gameState, queues, events) { + // check if we have enough transport ships. + // check per region. + for (var i = 0; i < this.seaShips.length; ++i) + { + var tpNb = gameState.countOwnQueuedEntitiesWithMetadata("sea", i); + if (this.accessibleSeas.indexOf(i) !== -1 && this.seaTpShips[i].length < this.wantedTpShips[i] + && tpNb + queues.ships.length() === 0 && gameState.getTemplate(gameState.applyCiv("units/{civ}_ship_bireme")).available(gameState)) + { + // TODO: check our dock can build the wanted ship types, for Carthage. + queues.ships.addItem(new m.TrainingPlan(gameState, "units/{civ}_ship_bireme", { "sea" : i }, 1, 1 )); + } + } +}; + +// bumps up the number of ships we want if we need more. +m.NavalManager.prototype.checkLevels = function(gameState, queues) { + if (queues.ships.length() !== 0) + return; + for (var i = 0; i < this.transportPlans.length; ++i) + { + var plan = this.transportPlans[i]; + if (plan.needTpShips()) + { + var zone = plan.neededShipsZone(); + if (zone && gameState.countOwnQueuedEntitiesWithMetadata("sea", zone) > 0) + continue; + if (zone && this.wantedTpShips[i] === 0) + this.wantedTpShips[i]++; + else if (zone && plan.allAtOnce) + this.wantedTpShips[i]++; + } + } +}; + +// assigns free ships to plans that need some +m.NavalManager.prototype.assignToPlans = function(gameState, queues, events) { + for (var i = 0; i < this.transportPlans.length; ++i) + { + var plan = this.transportPlans[i]; + if (plan.needTpShips()) + { + // assign one per go. + var zone = plan.neededShipsZone(); + if (zone) + { + for each (var ship in this.seaTpShips[zone]._entities) + { + if (!ship.getMetadata(PlayerID, "tpplan")) + { + m.debug ("Assigning ship " + ship.id() + " to plan" + plan.ID); + plan.assignShip(gameState, ship); + return true; + } + } + } + } + } + return false; +}; + +m.NavalManager.prototype.checkActivePlan = function(ID) { + for (var i = 0; i < this.transportPlans.length; ++i) + if (this.transportPlans[i].ID === ID) + return true; + + return false; +}; + +// Some functions are run every turn +// Others once in a while +m.NavalManager.prototype.update = function(gameState, queues, events) { + Engine.ProfileStart("Naval Manager update"); + + this.checkEvents(gameState, queues, events); + + if (gameState.ai.playedTurn % 10 === 0) + { + this.maintainFleet(gameState, queues, events); + this.checkLevels(gameState, queues); + } + + for (var i = 0; i < this.transportPlans.length; ++i) + if (!this.transportPlans[i].carryOn(gameState, this)) + { + // whatever the reason, this plan needs to be ended + // it could be that it's finished though. + var seaZone = this.transportPlans[i].neededShipsZone(); + + var rallyPos = []; + this.docks.forEach(function (dock) { + if (dock.getMetadata(PlayerID,"sea") == seaZone) + rallyPos = dock.position(); + }); + this.transportPlans[i].ships.move(rallyPos); + this.transportPlans[i].releaseAll(gameState); + this.transportPlans.splice(i,1); + --i; + } + + this.assignToPlans(gameState, queues, events); + if (gameState.ai.playedTurn % 10 === 2) + { + this.createPlans(gameState); + this.askedPlans = []; + } + Engine.ProfileStop(); +}; + +return m; +}(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/plan-transport.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/plan-transport.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/plan-transport.js (revision 14865) @@ -0,0 +1,478 @@ +var PETRA = function(m) +{ + +/* + Describes a transport plan + Constructor assign units (units is an ID array, or an ID), a destionation (position, ingame), and a wanted escort size. + If "onlyIfOk" is true, then the plan will only start if the wanted escort size is met. + The naval manager will try to deal with it accordingly. + + By this I mean that the naval manager will find how to go from access point 1 to access point 2 (relying on in-game pathfinder for mvt) + And then carry units from there. + If units are over multiple accessibility indexes (ie different islands) it will first group them + + Note: only assign it units currently over land, or it won't work. + Also: destination should probably be land, otherwise the units will be lost at sea. +*/ + +// TODO: finish the support of multiple accessibility indexes. +// TODO: this doesn't check we can actually reach in the init, which we might want? + +m.TransportPlan = function(gameState, units, destination, allAtOnce, escortSize, onlyIfOK) { + var self = this; + + this.ID = m.playerGlobals[PlayerID].uniqueIDTPlans++; + + var unitsID = []; + if (units.length !== undefined) + unitsID = units; + else + unitsID = [units]; + + this.units = m.EntityCollectionFromIds(gameState, unitsID); + this.units.forEach(function (ent) { //}){ + ent.setMetadata(PlayerID, "tpplan", self.ID); + ent.setMetadata(PlayerID, "formerRole", ent.getMetadata(PlayerID, "role")); + ent.setMetadata(PlayerID, "role", "transport"); + }); + + this.units.freeze(); + this.units.registerUpdates(); + + m.debug ("Starting a new plan with ID " + this.ID + " to " + destination); + m.debug ("units are " + uneval (units)); + + this.destination = destination; + this.destinationIndex = gameState.ai.accessibility.getAccessValue(destination); + + if (allAtOnce) + this.allAtOnce = allAtOnce; + else + this.allAtOnce = false; + + if (escortSize) + this.escortSize = escortSize; + else + this.escortSize = 0; + + if (onlyIfOK) + this.onlyIfOK = onlyIfOK; + else + this.onlyIfOK = false; + + this.state = "unstarted"; + + this.ships = gameState.ai.HQ.navalManager.ships.filter(Filters.byMetadata(PlayerID, "tpplan", this.ID)); + // note: those two can overlap (some transport ships are warships too and vice-versa). + this.transportShips = gameState.ai.HQ.navalManager.tpShips.filter(Filters.byMetadata(PlayerID, "tpplan", this.ID)); + this.escortShips = gameState.ai.HQ.navalManager.warships.filter(Filters.byMetadata(PlayerID, "tpplan", this.ID)); + + this.ships.registerUpdates(); + this.transportShips.registerUpdates(); + this.escortShips.registerUpdates(); +}; + +// count available slots +m.TransportPlan.prototype.countFreeSlots = function(onlyTrulyFree) +{ + var slots = 0; + this.transportShips.forEach(function (ent) { //}){ + slots += ent.garrisonMax(); + if (onlyTrulyFree) + slots -= ent.garrisoned().length; + }); +} + +m.TransportPlan.prototype.assignShip = function(gameState, ship) +{ + ship.setMetadata(PlayerID,"tpplan", this.ID); +} + +m.TransportPlan.prototype.releaseAll = function(gameState) +{ + this.ships.forEach(function (ent) { ent.setMetadata(PlayerID,"tpplan", undefined) }); + this.units.forEach(function (ent) { + var fRole = ent.getMetadata(PlayerID, "formerRole"); + if (fRole) + ent.setMetadata(PlayerID,"role", fRole); + ent.setMetadata(PlayerID,"tpplan", undefined) + }); +} + +m.TransportPlan.prototype.releaseAllShips = function(gameState) +{ + this.ships.forEach(function (ent) { ent.setMetadata(PlayerID,"tpplan", undefined) }); +} + +m.TransportPlan.prototype.needTpShips = function() +{ + if ((this.allAtOnce && this.countFreeSlots() >= this.units.length) || this.transportShips.length > 0) + return false; + return true; +} + +m.TransportPlan.prototype.needEscortShips = function() +{ + return !((this.onlyIfOK && this.escortShips.length < this.escortSize) || !this.onlyIfOK); +} + +// returns the zone for which we are needing our ships +m.TransportPlan.prototype.neededShipsZone = function() +{ + if (!this.seaZone) + return false; + return this.seaZone; +} + + +// try to move on. +/* several states: + "unstarted" is the initial state, and will determine wether we follow basic or grouping path + Basic path: + - "waitingForBoarding" means we wait 'till we have enough transport ships and escort ships to move stuffs. + - "Boarding" means we're trying to board units onto our ships + - "Moving" means we're moving ships + - "Unboarding" means we're unbording + - Once we're unboarded, we either return to boarding point (if we still have units to board) or we clear. + > there is the possibility that we'll be moving units on land, but that's basically a restart too, with more clearing. + Grouping Path is basically the same with "grouping" and we never unboard (unless there is a need to) + */ +m.TransportPlan.prototype.carryOn = function(gameState, navalManager) +{ + if (this.state === "unstarted") + { + // Okay so we can start the plan. + // So what we'll do is check what accessibility indexes our units are. + var unitIndexes = []; + this.units.forEach( function (ent) { //}){ + var idx = gameState.ai.accessibility.getAccessValue(ent.position()); + if (unitIndexes.indexOf(idx) === -1 && idx !== 1) + unitIndexes.push(idx); + }); + + // we have indexes. If there is more than 1, we'll try and regroup them. + if (unitIndexes.length > 1) + { + warn("Transport Plan path is too complicated, aborting"); + return false; + /* + this.state = "waitingForGrouping"; + // get the best index for grouping, ie start by the one farthest away in terms of movement. + var idxLength = {}; + for (var i = 0; i < unitIndexes.length; ++i) + idxLength[unitIndexes[i]] = gameState.ai.accessibility.getTrajectToIndex(unitIndexes[i], this.destinationIndex).length; + var sortedArray = unitIndexes.sort(function (a,b) { //}){ + return idxLength[b] - idxLength[a]; + }); + this.startIndex = sortedArray[0]; + // okay so we'll board units from this index and we'll try to join them with units of the next index. + // this might not be terribly efficient but it won't be efficient anyhow. + return true;*/ + } + this.state = "waitingForBoarding"; + + // let's get our index this turn. + this.startIndex = unitIndexes[0]; + + m.debug ("plan " + this.ID + " from " + this.startIndex); + + return true; + } + if (this.state === "waitingForBoarding") + { + + if (!this.path) + { + this.path = gameState.ai.accessibility.getTrajectToIndex(this.startIndex, this.destinationIndex); + if (!this.path || this.path.length === 0 || this.path.length % 2 === 0) + return false; // TODO: improve error handling + if (this.path[0] !== this.startIndex) + { + warn ("Start point of the path is not the start index, aborting transport plan"); + return false; + } + // we have a path, register the first sea zone. + this.seaZone = this.path[1]; + m.debug ("Plan " + this.ID + " over seazone " + this.seaZone); + } + // if we currently have no baoarding spot, try and find one. + if (!this.boardingSpot) + { + // TODO: improve on this whenever we have danger maps. + // okay so we have units over an accessibility index. + // we'll get a map going on. + var Xibility = gameState.ai.accessibility; + + // custom obstruction map that uses the shore as the obstruction map + // but doesn't really check like for a building. + // created realtime with the other map. + var passabilityMap = gameState.getMap(); + var territoryMap = gameState.ai.territoryMap; + var obstructionMask = gameState.getPassabilityClassMask("foundationObstruction") | gameState.getPassabilityClassMask("building-shore"); + var obstructions = new API3.Map(gameState.sharedScript); + + // wanted map. + var friendlyTiles = new API3.Map(gameState.sharedScript); + + for (var j = 0; j < friendlyTiles.length; ++j) + { + // only on the wanted island + if (Xibility.landPassMap[j] !== this.startIndex) + continue; + + // setting obstructions + var tilePlayer = (territoryMap.data[j] & TERRITORY_PLAYER_MASK); + // invalid is enemy-controlled or not on the right sea/land (we need a shore for this, we might want to check neighbhs instead). + var invalidTerritory = (gameState.isPlayerEnemy(tilePlayer) && tilePlayer != 0) + || (Xibility.navalPassMap[j] !== this.path[1]); + obstructions.map[j] = (invalidTerritory || (passabilityMap.data[j] & obstructionMask)) ? 0 : 255; + + // currently we'll just like better on our territory + if (tilePlayer == PlayerID) + friendlyTiles.map[j] = 100; + } + + obstructions.expandInfluences(); + + var best = friendlyTiles.findBestTile(4, obstructions); + var bestIdx = best[0]; + + // not good enough. + if (best[1] <= 0) + { + best = friendlyTiles.findBestTile(1, obstructions); + bestIdx = best[0]; + if (best[1] <= 0) + return false; // apparently we won't be able to board. + } + + var x = ((bestIdx % friendlyTiles.width) + 0.5) * gameState.cellSize; + var z = (Math.floor(bestIdx / friendlyTiles.width) + 0.5) * gameState.cellSize; + + // we have the spot we want to board at. + this.boardingSpot = [x,z]; + m.debug ("Plan " + this.ID + " new boarding spot is " + this.boardingSpot); + } + + // if all at once we need to be full, else we just need enough escort ships. + if (!this.needTpShips() && !this.needEscortShips()) + { + // preparing variables + // TODO: destroy former entity collection. + this.garrisoningUnits = this.units.filter(Filters.not(Filters.isGarrisoned())); + this.garrisoningUnits.registerUpdates(); + this.garrisoningUnits.freeze(); + + this.garrisonShipID = -1; + + m.debug ("Boarding"); + this.state = "boarding"; + } + return true; + } else if (this.state === "waitingForGrouping") + { + // TODO: this. + return true; + } + if (this.state === "boarding" && gameState.ai.playedTurn % 5 === 0) + { + // TODO: improve error recognition. + if (this.units.length === 0) + return false; + if (!this.boardingSpot) + return false; + if (this.needTpShips()) + { + this.state = "waitingForBoarding"; + return true; + } + if (this.needEscortShips()) + { + this.state = "waitingForBoarding"; + return true; + } + + // check if we aren't actually finished. + if (this.units.getCentrePosition() == undefined || this.countFreeSlots(true) === 0) + { + delete this.boardingSpot; + this.garrisoningUnits.unregister(); + this.state = "moving"; + return true; + } + + // check if we need to move our units and ships closer together + var stillMoving = false; + if (API3.SquareVectorDistance(this.ships.getCentrePosition(),this.boardingSpot) > 1600) + { + this.ships.move(this.boardingSpot[0],this.boardingSpot[1]); + stillMoving = true; // wait till ships are in position + } + if (API3.SquareVectorDistance(this.units.getCentrePosition(),this.boardingSpot) > 1600) + { + this.units.move(this.boardingSpot[0],this.boardingSpot[1]); + stillMoving = true; // wait till units are in position + } + if (stillMoving) + { + return true; // wait. + } + // check if we need to try and board units. + var garrisonShip = gameState.getEntityById(this.garrisonShipID); + var self = this; + // check if ship we're currently garrisoning in is full + if (garrisonShip && garrisonShip.canGarrisonInside()) + { + // okay garrison units + var nbStill = garrisonShip.garrisonMax() - garrisonShip.garrisoned().length; + if (this.garrisoningUnits.length < nbStill) + { + Engine.PostCommand({"type": "garrison", "entities": this.garrisoningUnits.toIdArray(), "target": garrisonShip.id(),"queued": false}); + } + return true; + } else if (garrisonShip) + { + // full ship, abort + this.garrisonShipID = -1; + garrisonShip = false; // will enter next if. + } + if (!garrisonShip) + { + // could have died or could have be full + // we'll pick a new one, one that isn't full + for (var i in this.transportShips._entities) + { + if (this.transportShips._entities[i].canGarrisonInside()) + { + this.garrisonShipID = this.transportShips._entities[i].id(); + break; + } + } + return true; // wait. + } + // could I actually get here? + return true; + } + if (this.state === "moving") + { + if (!this.unboardingSpot) + { + // TODO: improve on this whenever we have danger maps. + // okay so we have units over an accessibility index. + // we'll get a map going on. + var Xibility = gameState.ai.accessibility; + + // custom obstruction map that uses the shore as the obstruction map + // but doesn't really check like for a building. + // created realtime with the other map. + var passabilityMap = gameState.getMap(); + var territoryMap = gameState.ai.territoryMap; + var obstructionMask = gameState.getPassabilityClassMask("foundationObstruction") | gameState.getPassabilityClassMask("building-shore"); + var obstructions = new API3.Map(gameState.sharedScript); + + // wanted map. + var friendlyTiles = new API3.Map(gameState.sharedScript); + + var wantedIndex = -1; + + if (this.path.length >= 3) + { + this.path.splice(0,2); + wantedIndex = this.path[0]; + } else { + m.debug ("too short at " +uneval(this.path)); + return false; // Incomputable + } + + for (var j = 0; j < friendlyTiles.length; ++j) + { + // only on the wanted island + if (Xibility.landPassMap[j] !== wantedIndex) + continue; + + // setting obstructions + var tilePlayer = (territoryMap.data[j] & TERRITORY_PLAYER_MASK); + // invalid is not on the right land (we need a shore for this, we might want to check neighbhs instead). + var invalidTerritory = (Xibility.landPassMap[j] !== wantedIndex); + obstructions.map[j] = (invalidTerritory || (passabilityMap.data[j] & obstructionMask)) ? 0 : 255; + + // currently we'll just like better on our territory + if (tilePlayer == PlayerID) + friendlyTiles.map[j] = 100; + else if (gameState.isPlayerEnemy(tilePlayer) && tilePlayer != 0) + friendlyTiles.map[j] = 4; + else + friendlyTiles.map[j] = 50; + } + + obstructions.expandInfluences(); + + var best = friendlyTiles.findBestTile(4, obstructions); + var bestIdx = best[0]; + + // not good enough. + if (best[1] <= 0) + { + best = friendlyTiles.findBestTile(1, obstructions); + bestIdx = best[0]; + if (best[1] <= 0) + return false; // apparently we won't be able to unboard. + } + + var x = ((bestIdx % friendlyTiles.width) + 0.5) * gameState.cellSize; + var z = (Math.floor(bestIdx / friendlyTiles.width) + 0.5) * gameState.cellSize; + + // we have the spot we want to board at. + this.unboardingSpot = [x,z]; + return true; + } + + // TODO: improve error recognition. + if (this.units.length === 0) + return false; + if (!this.unboardingSpot) + return false; + + // check if we need to move ships + if (API3.SquareVectorDistance(this.ships.getCentrePosition(),this.unboardingSpot) > 400) + { + this.ships.move(this.unboardingSpot[0],this.unboardingSpot[1]); + } else { + this.state = "unboarding"; + return true; + } + return true; + } + if (this.state === "unboarding") + { + // TODO: improve error recognition. + if (this.units.length === 0) + return false; + + // check if we need to move ships + if (API3.SquareVectorDistance(this.ships.getCentrePosition(),this.unboardingSpot) > 400) + { + this.ships.move(this.unboardingSpot[0],this.unboardingSpot[1]); + } else { + this.transportShips.forEach( function (ent) { ent.unloadAll() }); + // TODO: improve on this. + if (this.path.length > 1) + { + m.debug ("plan " + this.ID + " going back for more"); + // basically reset. + delete this.boardingSpot; + delete this.unboardingSpot; + this.state = "unstarted"; + this.releaseAllShips(); + return true; + } + m.debug ("plan " + this.ID + " is finished"); + return false; + } + } + + return true; +} + +return m; +}(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/queue.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/queue.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/queue.js (revision 14865) @@ -0,0 +1,131 @@ +var PETRA = function(m) +{ + +/* + * Holds a list of wanted items to train or construct + */ + +m.Queue = function() +{ + this.queue = []; + this.paused = false; + this.switched = 0; +}; + +m.Queue.prototype.empty = function() +{ + this.queue = []; +}; + +m.Queue.prototype.addItem = function(plan) +{ + if (!plan) warn(" essaie d'ajout d un plan vide"); + if (!plan) + return; + for (var i in this.queue) + { + if (plan.category === "unit" && this.queue[i].type == plan.type && this.queue[i].number + plan.number <= this.queue[i].maxMerge) + { + this.queue[i].addItem(plan.number) + return; + } + } + this.queue.push(plan); +}; + +m.Queue.prototype.check= function(gameState) +{ + while (this.queue.length > 0) + { + if (!this.queue[0].isInvalid(gameState)) + return; + warn(" plan " + this.queue[0].type + " invalid and suppressed"); + this.queue.shift(); + } + return; +}; + +m.Queue.prototype.getNext = function() +{ + if (this.queue.length > 0) + return this.queue[0]; + else + return null; +}; + +m.Queue.prototype.startNext = function(gameState) +{ + if (this.queue.length > 0) + { + this.queue.shift().start(gameState); + return true; + } + else + return false; +}; + +// returns the maximal account we'll accept for this queue. +// Currently all the cost of the first element and fraction of that of the second +m.Queue.prototype.maxAccountWanted = function(gameState, fraction) +{ + var cost = new API3.Resources(); + if (this.queue.length > 0 && this.queue[0].isGo(gameState)) + cost.add(this.queue[0].getCost()); + if (this.queue.length > 1 && this.queue[1].isGo(gameState) && fraction > 0) + { + var costs = this.queue[1].getCost(); + costs.multiply(fraction); + cost.add(costs); + } + return cost; +}; + +m.Queue.prototype.queueCost = function() +{ + var cost = new API3.Resources(); + for (var key in this.queue) + cost.add(this.queue[key].getCost()); + return cost; +}; + +m.Queue.prototype.length = function() +{ + return this.queue.length; +}; + +m.Queue.prototype.countQueuedUnits = function() +{ + var count = 0; + for (var i in this.queue) + count += this.queue[i].number; + return count; +}; + +m.Queue.prototype.countQueuedUnitsWithClass = function(classe) +{ + var count = 0; + for (var i in this.queue) + if (this.queue[i].template && this.queue[i].template.hasClass(classe)) + count += this.queue[i].number; + return count; +}; +m.Queue.prototype.countQueuedUnitsWithMetadata = function(data,value) +{ + var count = 0; + for (var i in this.queue) + if (this.queue[i].metadata[data] && this.queue[i].metadata[data] == value) + count += this.queue[i].number; + return count; +}; + +m.Queue.prototype.countAllByType = function(t) +{ + var count = 0; + for (var i in this.queue) + if (this.queue[i].type === t) + count += this.queue[i].number; + return count; +}; + +return m; +}(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueManager.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueManager.js (revision 14865) @@ -0,0 +1,619 @@ +var PETRA = function(m) +{ + +// This takes the input queues and picks which items to fund with resources until no more resources are left to distribute. +// +// Currently this manager keeps accounts for each queue, split between the 4 main resources +// +// Each time resources are available (ie not in any account), it is split between the different queues +// Mostly based on priority of the queue, and existing needs. +// Each turn, the queue Manager checks if a queue can afford its next item, then it does. +// +// A consequence of the system it's not really revertible. Once a queue has an account of 500 food, it'll keep it +// If for some reason the AI stops getting new food, and this queue lacks, say, wood, no other queues will +// be able to benefit form the 500 food (even if they only needed food). +// This is not to annoying as long as all goes well. If the AI loses many workers, it starts being problematic. +// +// It also has the effect of making the AI more or less always sit on a few hundreds resources since most queues +// get some part of the total, and if all queues have 70% of their needs, nothing gets done +// Particularly noticeable when phasing: the AI often overshoots by a good 200/300 resources before starting. +// +// This system should be improved. It's probably not flexible enough. + +m.QueueManager = function(Config, queues, priorities) +{ + this.Config = Config; + this.queues = queues; + this.priorities = priorities; + this.accounts = {}; + + // the sorting is updated on priority change. + var self = this; + this.queueArrays = []; + for (var p in this.queues) + { + this.accounts[p] = new API3.Resources(); + this.queueArrays.push([p,this.queues[p]]); + } + this.queueArrays.sort(function (a,b) { return (self.priorities[b[0]] - self.priorities[a[0]]) }); + + this.curItemQueue = []; +}; + +m.QueueManager.prototype.getAvailableResources = function(gameState, noAccounts) +{ + var resources = gameState.getResources(); + if (noAccounts) + return resources; + for (var key in this.queues) + resources.subtract(this.accounts[key]); + return resources; +}; + +m.QueueManager.prototype.getTotalAccountedResources = function(gameState) +{ + var resources = new API3.Resources(); + for (var key in this.queues) + resources.add(this.accounts[key]); + return resources; +}; + +m.QueueManager.prototype.currentNeeds = function(gameState) +{ + var needed = new API3.Resources(); + //queueArrays because it's faster. + for (var i in this.queueArrays) + { + var name = this.queueArrays[i][0]; + var queue = this.queueArrays[i][1]; + if (queue.length() == 0 || !queue.queue[0].isGo(gameState)) + continue; + // we need resource if the account is smaller than the cost + var costs = queue.queue[0].getCost(); + for each (var ress in costs.types) + costs[ress] = Math.max(0, costs[ress] - this.accounts[name][ress]); + + needed.add(costs); + } + return needed; +}; + +// calculate the gather rates we'd want to be able to start all elements in our queues +// TODO: many things. +m.QueueManager.prototype.wantedGatherRates = function(gameState) +{ + // get out current resources, not removing accounts. + var current = this.getAvailableResources(gameState, true); + // short queue is the first item of a queue, assumed to be ready in 30s + // medium queue is the second item of a queue, assumed to be ready in 60s + // long queue contains the is the isGo=false items, assumed to be ready in 300s + var totalShort = { "food": 0, "wood": 0, "stone": 0, "metal": 0 }; + var totalMedium = { "food": 0, "wood": 0, "stone": 0, "metal": 0 }; + var totalLong = { "food": 0, "wood": 0, "stone": 0, "metal": 0 }; + var total; + //queueArrays because it's faster. + for (var i in this.queueArrays) + { + var name = this.queueArrays[i][0]; + var queue = this.queueArrays[i][1]; + if (queue.paused) + continue; + for (var j = 0; j < queue.length(); ++j) + { + if (j > 1) + break; + var cost = queue.queue[j].getCost(); + if (queue.queue[j].isGo(gameState)) + { + if (j == 0) + total = totalShort; + else + total = totalMedium; + } + else + total = totalLong; + for (var type in total) + total[type] += cost[type]; + if (!queue.queue[j].isGo(gameState)) + break; + } + } + // global rates + var rates = { "food" : 0, "wood" : 0, "stone" : 0, "metal" : 0 }; + var diff; + for (var type in rates) + { + if (current[type] > 0) + { + diff = Math.min(current[type], totalShort[type]); + totalShort[type] -= diff; + current[type] -= diff; + if (current[type] > 0) + { + diff = Math.min(current[type], totalMedium[type]); + totalMedium[type] -= diff; + current[type] -= diff; + if (current[type] > 0) + { + diff = Math.min(current[type], totalLong[type]); + totalLong[type] -= diff; + current[type] -= diff; + } + } + } + rates[type] = totalShort[type]/30 + totalMedium[type]/60 + totalLong[type]/300; + } + +// if (this.Config.debug) +// { +// var oldRates = this.oldWantedGatherRates(gameState); +// warn(" old Rates " + uneval(oldRates) + " new rates " + uneval(rates)); +// } + return rates; +// return oldRates; +}; + +m.QueueManager.prototype.oldWantedGatherRates = function(gameState) +{ + // global rates + var rates = { "food" : 0, "wood" : 0, "stone" : 0, "metal" : 0 }; + // per-queue. + var qTime = gameState.getTimeElapsed(); + var time = gameState.getTimeElapsed(); + var qCosts = { "food" : 0, "wood" : 0, "stone" : 0, "metal" : 0 }; + + var currentRess = this.getAvailableResources(gameState); + + //queueArrays because it's faster. + for (var i in this.queueArrays) + { + qCosts = { "food" : 0, "wood" : 0, "stone" : 0, "metal" : 0 }; + qTime = gameState.getTimeElapsed(); + var name = this.queueArrays[i][0]; + var queue = this.queueArrays[i][1]; + + // we'll move temporally along the queue. + for (var j = 0; j < queue.length(); ++j) + { + var elem = queue.queue[j]; + var cost = elem.getCost(); + + var timeMultiplier = Math.max(1,(qTime-time)/25000); + + if (!elem.isGo(gameState)) + { + // assume we'll be wanted in four minutes. + // TODO: work on this. + for (var type in qCosts) + qCosts[type] += cost[type] / timeMultiplier; + qTime += 240000; + break; // disregard other stuffs. + } + // Assume we want it in 30 seconds from current time. + // Costs are made higher based on priority and lower based on current time. + // TODO: work on this. + for (var type in qCosts) + { + if (cost[type] === 0) + continue; + qCosts[type] += (cost[type] + Math.min(cost[type],this.priorities[name])) / timeMultiplier; + } + qTime += 30000; // TODO: this needs a lot more work. + } + for (var j in qCosts) + { + qCosts[j] -= this.accounts[name][j]; + var diff = Math.min(qCosts[j], currentRess[j]); + qCosts[j] -= diff; + currentRess[j] -= diff; + rates[j] += qCosts[j]/(qTime/1000); + } + } + + return rates; +}; + +/*m.QueueManager.prototype.logNeeds = function(gameState) { + if (!this.totor) + { + this.totor = []; + this.currentGathR = []; + this.currentGathRWanted = []; + this.ressLev = []; +} + + if (gameState.ai.playedTurn % 10 !== 0) + return; + + + var array = this.wantedGatherRates(gameState); + this.totor.push( array ); + + + var currentRates = {}; + for (var type in array) + currentRates[type] = 0; + for (var i in gameState.ai.HQ.baseManagers) + { + var base = gameState.ai.HQ.baseManagers[i]; + for (var type in array) + { + base.gatherersByType(gameState,type).forEach (function (ent) { //}){ + var worker = ent.getMetadata(PlayerID, "worker-object"); + if (worker) + currentRates[type] += worker.getGatherRate(gameState); + }); + } + } + this.currentGathR.push( currentRates ); + + var types = Object.keys(array); + + types.sort(function(a, b) { + var va = (Math.max(0,array[a] - currentRates[a]))/ (currentRates[a]+1); + var vb = (Math.max(0,array[b] - currentRates[b]))/ (currentRates[b]+1); + if (va === vb) + return (array[b]/(currentRates[b]+1)) - (array[a]/(currentRates[a]+1)); + return vb-va; + }); + this.currentGathRWanted.push( types ); + + var rss = gameState.getResources(); + this.ressLev.push( {"food" : rss["food"],"stone" : rss["stone"],"wood" : rss["wood"],"metal" : rss["metal"]} ); + + if (gameState.getTimeElapsed() > 20*60*1000 && !this.once) + { + this.once = true; + for (var j in array) + { + log (j + ";"); + for (var i = 0; i < this.totor.length; ++i) + { + log (this.totor[i][j] + ";"); + } + } + log(); + for (var j in array) + { + log (j + ";"); + for (var i = 0; i < this.totor.length; ++i) + { + log (this.currentGathR[i][j] + ";"); + } + } + log(); + for (var j in array) + { + log (j + ";"); + for (var i = 0; i < this.totor.length; ++i) + { + log (this.currentGathRWanted[i].indexOf(j) + ";"); + } + } + log(); + for (var j in array) + { + log (j + ";"); + for (var i = 0; i < this.totor.length; ++i) + { + log (this.ressLev[i][j] + ";"); + } + } + } +}; +*/ + +m.QueueManager.prototype.printQueues = function(gameState) +{ + warn("QUEUES"); + for (var i in this.queues) + { + var qStr = ""; + var q = this.queues[i]; + if (q.queue.length > 0) + { + warn(i + ": ( paused " + q.paused + " with priority " + this.priorities[i] +" and accounts " + uneval(this.accounts[i]) +")"); + warn(" while maxAccountWanted(0.6) is " + uneval(q.maxAccountWanted(gameState, 0.6))); + } + for (var j in q.queue) + { + qStr = " " + q.queue[j].type + " "; + if (q.queue[j].number) + qStr += "x" + q.queue[j].number; + qStr += " isGo " + q.queue[j].isGo(gameState); + warn(qStr); + } + } + warn("Accounts"); + for (var p in this.accounts) + warn(p + ": " + uneval(this.accounts[p])); + warn("Current Resources:" + uneval(gameState.getResources())); + warn("Available Resources:" + uneval(this.getAvailableResources(gameState))); + warn("Wanted Gather Rates:" + uneval(this.wantedGatherRates(gameState))); + warn("Aegis Wanted Gather Rates:" + uneval(this.oldWantedGatherRates(gameState))); + warn("Current Gather Rates:" + uneval(gameState.ai.HQ.GetCurrentGatherRates(gameState))); + warn(" - - - - - - - -"); + for (var i in gameState.ai.HQ.baseManagers) + for (var ress in gameState.ai.HQ.wantedRates) + warn(" DPresource " + i + " ress " + ress + " = " + gameState.ai.HQ.baseManagers[i].getResourceLevel(gameState, ress)); + warn(" - - - - - - - -"); +}; + +// nice readable HTML version. +m.QueueManager.prototype.HTMLprintQueues = function(gameState) +{ + if (!m.DebugEnabled()) + return; + var strToSend = []; + strToSend.push(" Aegis Queue Manager "); + for (var i in this.queues) + { + strToSend.push(""); + + var q = this.queues[i]; + var str = ""); + for (var j in q.queue) { + if (q.queue[j].isGo(gameState)) + strToSend.push(""; + strToSend.push(qStr); + } + strToSend.push(""); + } + strToSend.push("
Aegis Build Order
" + i + " (" + this.priorities[i] + ")
"; + for each (var k in this.accounts[i].types) + if(k != "population") + { + str += this.accounts[i][k] + k.substr(0,1).toUpperCase() ; + if (k != "metal") str += " / "; + } + strToSend.push(str + "
"); + else + strToSend.push(""); + + var qStr = ""; + if (q.queue[j].number) + qStr += q.queue[j].number + " "; + qStr += q.queue[j].type; + qStr += "
"; + var costs = q.queue[j].getCost(); + for each (var k in costs.types) + { + qStr += costs[k] + k.substr(0,1).toUpperCase() ; + if (k != "metal") + qStr += " / "; + } + qStr += "
"); + /*strToSend.push("

Accounts

"); + for (var p in this.accounts) + { + strToSend.push("

" + p + ": " + uneval(this.accounts[p]) + "

"); + }*/ + strToSend.push("

Wanted Gather Rate:" + uneval(this.wantedGatherRates(gameState)) + "

"); + strToSend.push("

Current Resources:" + uneval(gameState.getResources()) + "

"); + strToSend.push("

Available Resources:" + uneval(this.getAvailableResources(gameState)) + "

"); + strToSend.push(""); + for each (var logged in strToSend) + log(logged); +}; + +m.QueueManager.prototype.clear = function() +{ + this.curItemQueue = []; + for (var i in this.queues) + this.queues[i].empty(); +}; + +m.QueueManager.prototype.update = function(gameState) +{ + for (var i in this.queues) + { + this.queues[i].check(gameState); // do basic sanity checks on the queue + if (this.priorities[i] > 0) + continue; + warn("QueueManager received bad priorities, please report this error: " + uneval(this.priorities)); + this.priorities[i] = 1; // TODO: make the Queue Manager not die when priorities are zero. + } + + Engine.ProfileStart("Queue Manager"); + + // Let's assign resources to plans that need'em + var availableRes = this.getAvailableResources(gameState); + for (var ress in availableRes) + { + if (ress === "population") + continue; + + if (availableRes[ress] > 0) + { + var totalPriority = 0; + var tempPrio = {}; + var maxNeed = {}; + // Okay so this is where it gets complicated. + // If a queue requires "ress" for the next elements (in the queue) + // And the account is not high enough for it. + // Then we add it to the total priority. + // To try and be clever, we don't want a long queue to hog all resources. So two things: + // -if a queue has enough of resource X for the 1st element, its priority is decreased (factor 2). + // -queues accounts are capped at "resources for the first + 60% of the next" + // This avoids getting a high priority queue with many elements hogging all of one resource + // uselessly while it awaits for other resources. + for (var j in this.queues) + { + // returns exactly the correct amount, ie 0 if we're not go. + var queueCost = this.queues[j].maxAccountWanted(gameState, 0.6); + if (this.queues[j].length() > 0 && this.accounts[j][ress] < queueCost[ress] && !this.queues[j].paused) + { + // adding us to the list of queues that need an update. + tempPrio[j] = this.priorities[j]; + maxNeed[j] = queueCost[ress] - this.accounts[j][ress]; + // if we have enough of that resource for our first item in the queue, diminish our priority. + if (this.accounts[j][ress] >= this.queues[j].getNext().getCost()[ress]) + tempPrio[j] /= 2; + + if (tempPrio[j]) + totalPriority += tempPrio[j]; + } + else if (this.accounts[j][ress] > queueCost[ress]) + this.accounts[j][ress] = queueCost[ress]; + } + // Now we allow resources to the accounts. We can at most allow "TempPriority/totalpriority*available" + // But we'll sometimes allow less if that would overflow. + for (var j in tempPrio) + { + // we'll add at much what can be allowed to this queue. + var toAdd = Math.floor(availableRes[ress] * tempPrio[j]/totalPriority); + var maxAdd = Math.min(maxNeed[j], toAdd); + this.accounts[j][ress] += maxAdd; + } + } + else + { + // We have no available resources, see if we can't "compact" them in one queue. + // compare queues 2 by 2, and if one with a higher priority could be completed by our amount, give it. + // TODO: this isn't perfect compression. + for (var j in this.queues) + { + if (this.queues[j].length() === 0 || this.queues[j].paused) + continue; + + var queue = this.queues[j]; + var queueCost = queue.maxAccountWanted(gameState, 0); + if (this.accounts[j][ress] >= queueCost[ress]) + continue; + + for (var i in this.queues) + { + if (i === j) + continue; + var otherQueue = this.queues[i]; + if (this.priorities[i] >= this.priorities[j] || otherQueue.switched !== 0) + continue; + if (this.accounts[j][ress] + this.accounts[i][ress] < queueCost[ress]) + continue; + + var diff = queueCost[ress] - this.accounts[j][ress]; + this.accounts[j][ress] += diff; + this.accounts[i][ress] -= diff; + ++otherQueue.switched; + if (this.Config.debug) + warn ("switching queue " + ress + " from " + i + " to " + j + " in amount " + diff); + break; + } + } + } + } + + // Start the next item in the queue if we can afford it. + for (var i in this.queueArrays) + { + var name = this.queueArrays[i][0]; + var queue = this.queueArrays[i][1]; + if (queue.length() > 0 && !queue.paused) + { + var item = queue.getNext(); + var total = new API3.Resources(); + total.add(this.accounts[name]); + if (total.canAfford(item.getCost())) + { + if (item.canStart(gameState)) + { + this.accounts[name].subtract(item.getCost()); + queue.startNext(gameState); + queue.switched = 0; + } + } + } + else if (queue.length() === 0) + { + this.accounts[name].reset(); + queue.switched = 0; + } + } + + Engine.ProfileStop(); +}; + +m.QueueManager.prototype.pauseQueue = function(queue, scrapAccounts) +{ + if (this.queues[queue]) + { + this.queues[queue].paused = true; + if (scrapAccounts) + this.accounts[queue].reset(); + } +}; + +m.QueueManager.prototype.unpauseQueue = function(queue) +{ + if (this.queues[queue]) + this.queues[queue].paused = false; +}; + +m.QueueManager.prototype.pauseAll = function(scrapAccounts, but) +{ + for (var p in this.queues) + { + if (p != but) + { + if (scrapAccounts) + this.accounts[p].reset(); + this.queues[p].paused = true; + } + } +}; + +m.QueueManager.prototype.unpauseAll = function(but) +{ + for (var p in this.queues) + if (p != but) + this.queues[p].paused = false; +}; + + +m.QueueManager.prototype.addQueue = function(queueName, priority) +{ + if (this.queues[queueName] == undefined) + { + this.queues[queueName] = new m.Queue(); + this.priorities[queueName] = priority; + this.accounts[queueName] = new API3.Resources(); + + var self = this; + this.queueArrays = []; + for (var p in this.queues) + this.queueArrays.push([p,this.queues[p]]); + this.queueArrays.sort(function (a,b) { return (self.priorities[b[0]] - self.priorities[a[0]]) }); + } +}; + +m.QueueManager.prototype.removeQueue = function(queueName) +{ + if (this.queues[queueName] !== undefined) + { + if (this.curItemQueue.indexOf(queueName) !== -1) + this.curItemQueue.splice(this.curItemQueue.indexOf(queueName),1); + delete this.queues[queueName]; + delete this.priorities[queueName]; + delete this.accounts[queueName]; + + var self = this; + this.queueArrays = []; + for (var p in this.queues) + this.queueArrays.push([p,this.queues[p]]); + this.queueArrays.sort(function (a,b) { return (self.priorities[b[0]] - self.priorities[a[0]]) }); + } +}; + +m.QueueManager.prototype.changePriority = function(queueName, newPriority) +{ + var self = this; + if (this.queues[queueName] !== undefined) + this.priorities[queueName] = newPriority; + this.queueArrays = []; + for (var p in this.queues) + this.queueArrays.push([p,this.queues[p]]); + this.queueArrays.sort(function (a,b) { return (self.priorities[b[0]] - self.priorities[a[0]]) }); +}; + +return m; +}(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplan--.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplan--.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplan--.js (revision 14865) @@ -0,0 +1,81 @@ +var PETRA = function(m) +{ +/* + * Common functions and variables to all queue plans. + * has a "--" suffix because it needs to be loaded before the other queueplan files. + */ + +m.QueuePlan = function(gameState, type, metadata) +{ + this.type = gameState.applyCiv(type); + this.metadata = metadata; + + this.template = gameState.getTemplate(this.type); + if (!this.template) + { + warn("Tried to add the inexisting template " + this.type + " to Petra. Please report this on the forums") + return false; + } + this.ID = m.playerGlobals[PlayerID].uniqueIDBOPlans++; + this.cost = new API3.Resources(this.template.cost()); + this.number = 1; + + this.category = ""; + this.lastIsGo = undefined; + + return true; +}; + +// Check the content of this queue +m.QueuePlan.prototype.isInvalid = function(gameState) +{ + return false; +}; + +// if true, the queue manager will begin increasing this plan's account. +m.QueuePlan.prototype.isGo = function(gameState) +{ + return true; +}; + +// can we start this plan immediately? +m.QueuePlan.prototype.canStart = function(gameState) +{ + return false; +}; + +// process the plan. +m.QueuePlan.prototype.start = function(gameState) +{ + // should call onStart. +}; + +m.QueuePlan.prototype.getCost = function() +{ + var costs = new API3.Resources(); + costs.add(this.cost); + if (this.number !== 1) + costs.multiply(this.number); + return costs; +}; + +// On Event functions. +// Can be used to do some specific stuffs +// Need to be updated to actually do something if you want them to. +// this is called by "Start" if it succeeds. +m.QueuePlan.prototype.onStart = function(gameState) +{ +}; + +// This is called by "isGo()" if it becomes true while it was false. +m.QueuePlan.prototype.onGo = function(gameState) +{ +}; + +// This is called by "isGo()" if it becomes false while it was true. +m.QueuePlan.prototype.onNotGo = function(gameState) +{ +}; + +return m; +}(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplan-building.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplan-building.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplan-building.js (revision 14865) @@ -0,0 +1,262 @@ +var PETRA = function(m) +{ + +// Defines a construction plan, ie a building. +// We'll try to fing a good position if non has been provided + +m.ConstructionPlan = function(gameState, type, metadata, position) +{ + if (!m.QueuePlan.call(this, gameState, type, metadata)) + return false; + + this.position = position ? position : 0; + + this.category = "building"; + + return true; +}; + +m.ConstructionPlan.prototype = Object.create(m.QueuePlan.prototype); + +// checks other than resource ones. +// TODO: change this. +// TODO: if there are specific requirements here, maybe try to do them? +m.ConstructionPlan.prototype.canStart = function(gameState) +{ + if (gameState.buildingsBuilt > 0) // do not start another building if already one this turn + return false; + + if (!this.isGo(gameState)) + return false; + + // TODO: verify numeric limits etc + if (this.template.requiredTech() && !gameState.isResearched(this.template.requiredTech())) + { + return false; + } + var builders = gameState.findBuilders(this.type); + + return (builders.length != 0); +}; + +m.ConstructionPlan.prototype.start = function(gameState) +{ + + var builders = gameState.findBuilders(this.type).toEntityArray(); + + // We don't care which builder we assign, since they won't actually + // do the building themselves - all we care about is that there is + // some unit that can start the foundation + + var pos = this.findGoodPosition(gameState); + if (!pos){ + if (this.template.hasClass("Naval")) + gameState.ai.HQ.dockFailed = true; + gameState.ai.HQ.stopBuilding.push(this.type); + return; + } + this.buildingsBuilt++; + + if (this.metadata === undefined) + this.metadata = { "base": pos.base }; + else if (this.metadata.base === undefined) + this.metadata.base = pos.base; + + if (gameState.getTemplate(this.type).buildCategory() === "Dock") + { + for (var angle = 0; angle < Math.PI * 2; angle += Math.PI/4) + builders[0].construct(this.type, pos.x, pos.z, angle, this.metadata); + } + else + { + // try with the lowest, move towards us unless we're same + if (pos.x == pos.xx && pos.z == pos.zz) + builders[0].construct(this.type, pos.x, pos.z, pos.angle, this.metadata); + else + { + for (var step = 0; step <= 1; step += 0.2) + builders[0].construct(this.type, (step*pos.x + (1-step)*pos.xx), (step*pos.z + (1-step)*pos.zz), pos.angle, this.metadata); + } + } + this.onStart(gameState); +}; + +m.ConstructionPlan.prototype.findGoodPosition = function(gameState) +{ + var template = gameState.getTemplate(this.type); + + if (!this.position) + { + if (template.hasClass("CivCentre")) + { + if (this.metadata.type) + var pos = gameState.ai.HQ.findEconomicCCLocation(gameState, this.metadata.type); + else + var pos = gameState.ai.HQ.findStrategicCCLocation(gameState); + + if (pos) + return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "xx": pos[0], "zz": pos[1], "base": 0 }; + else + return false; + } + else if (template.hasClass("Tower") || template.hasClass("Fortress")) + { + var pos = gameState.ai.HQ.findDefensiveLocation(gameState, template); + + if (pos) + return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "xx": pos[0], "zz": pos[1], "base": pos[2] }; + else + { + gameState.ai.HQ.stopBuilding.push(this.type); + return false; + } + } + } + + var cellSize = gameState.cellSize; // size of each tile + + // First, find all tiles that are far enough away from obstructions: + + var obstructionMap = m.createObstructionMap(gameState, 0, template); + if (template.buildCategory() !== "Dock") + obstructionMap.expandInfluences(); + + //obstructionMap.dumpIm(template.buildCategory() + "_obstructions.png"); + + // Compute each tile's closeness to friendly structures: + + var friendlyTiles = new API3.Map(gameState.sharedScript); + + var alreadyHasHouses = false; + + if (this.position) // If a position was specified then place the building as close to it as possible + { + var x = Math.floor(this.position[0] / cellSize); + var z = Math.floor(this.position[1] / cellSize); + friendlyTiles.addInfluence(x, z, 255); + } + else // No position was specified so try and find a sensible place to build + { + // give a small > 0 level as the result of addInfluence is constrained to be > 0 + if (this.metadata && this.metadata.base !== undefined) + { + var base = this.metadata.base; + for (var j = 0; j < friendlyTiles.map.length; ++j) + if (gameState.ai.HQ.basesMap.map[j] === base) + friendlyTiles.map[j] = 30; + } + else + { + for (var j = 0; j < friendlyTiles.map.length; ++j) + if (gameState.ai.HQ.basesMap.map[j] !== 0) + friendlyTiles.map[j] = 30; + } + + gameState.getOwnStructures().forEach(function(ent) { + var pos = ent.position(); + var x = Math.round(pos[0] / cellSize); + var z = Math.round(pos[1] / cellSize); + + if (ent.resourceDropsiteTypes() && ent.resourceDropsiteTypes().indexOf("food") !== -1) + { + if (template.hasClass("Field")) + friendlyTiles.addInfluence(x, z, 20, 50); + else // If this is not a field add a negative influence because we want to leave this area for fields + friendlyTiles.addInfluence(x, z, 20, -20); + } + else if (template.hasClass("House")) + { + if (ent.hasClass("House")) + { + friendlyTiles.addInfluence(x, z, 15, 40); // houses are close to other houses + alreadyHasHouses = true; + } + else + friendlyTiles.addInfluence(x, z, 15, -40); // and further away from other stuffs + } + else if (template.hasClass("Farmstead") && !ent.hasClass("Field")) + friendlyTiles.addInfluence(x, z, 25, -25); // move farmsteads away to make room. + else if (template.hasClass("GarrisonFortress") && ent.genericName() == "House") + friendlyTiles.addInfluence(x, z, 30, -50); + else if (template.hasClass("Military")) + friendlyTiles.addInfluence(x, z, 10, -40); + }); + + if (template.hasClass("Farmstead")) + { + for (var j = 0; j < friendlyTiles.map.length; ++j) + { + var value = friendlyTiles.map[j] - (gameState.sharedScript.resourceMaps["wood"].map[j])/3; + friendlyTiles.map[j] = value >= 0 ? value : 0; + if (gameState.ai.HQ.borderMap.map[j] > 0) + friendlyTiles.map[j] /= 2; // we need space around farmstead, so disfavor map border + } + } + } + + // requires to be inside our territory, and inside our base territory if required + if (this.metadata && this.metadata.base !== undefined) + { + var base = this.metadata.base; + for (var j = 0; j < friendlyTiles.map.length; ++j) + if (gameState.ai.HQ.basesMap.map[j] !== base) + friendlyTiles.map[j] = 0; + } + else + { + for (var j = 0; j < friendlyTiles.map.length; ++j) + if (gameState.ai.HQ.basesMap.map[j] === 0) + friendlyTiles.map[j] = 0; + } + + // Find target building's approximate obstruction radius, and expand by a bit to make sure we're not too close, this + // allows room for units to walk between buildings. + // note: not for houses and dropsites who ought to be closer to either each other or a resource. + // also not for fields who can be stacked quite a bit + + var radius = 0; + if (template.hasClass("Fortress") || this.type == "structures/{civ}_siege_workshop") + radius = Math.floor(template.obstructionRadius() / cellSize) + 3; + else if (template.buildCategory() === "Dock") + radius = 1; + else if (template.resourceDropsiteTypes() === undefined) + radius = Math.ceil(template.obstructionRadius() / cellSize) + 1; + else + radius = Math.ceil(template.obstructionRadius() / cellSize); + + // Find the best non-obstructed + if (template.hasClass("House") && !alreadyHasHouses) + { + // try to get some space first + var bestTile = friendlyTiles.findBestTile(10, obstructionMap); + var bestIdx = bestTile[0]; + var bestVal = bestTile[1]; + } + + if (bestVal === undefined || bestVal === -1) + { + var bestTile = friendlyTiles.findBestTile(radius, obstructionMap); + var bestIdx = bestTile[0]; + var bestVal = bestTile[1]; + } + + if (bestVal <= 0) + { + gameState.ai.HQ.stopBuilding.push(this.type); + return false; + } + + var x = ((bestIdx % friendlyTiles.width) + 0.5) * cellSize; + var z = (Math.floor(bestIdx / friendlyTiles.width) + 0.5) * cellSize; + + if (template.hasClass("House") || template.hasClass("Field") || template.resourceDropsiteTypes() !== undefined) + var secondBest = obstructionMap.findLowestNeighbor(x,z); + else + var secondBest = [x,z]; + + // default angle = 3*Math.PI/4; + return { "x": x, "z": z, "angle": 3*Math.PI/4, "xx": secondBest[0], "zz": secondBest[1], "base": gameState.ai.HQ.basesMap.map[bestIdx] }; +}; + +return m; +}(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplan-research.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplan-research.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplan-research.js (revision 14865) @@ -0,0 +1,58 @@ +var PETRA = function(m) +{ + +m.ResearchPlan = function(gameState, type, rush) +{ + if (!m.QueuePlan.call(this, gameState, type, {})) + return false; + + if (this.template.researchTime === undefined) + return false; + + this.category = "technology"; + + this.rush = rush ? true : false; + + return true; +}; + +m.ResearchPlan.prototype = Object.create(m.QueuePlan.prototype); + +m.ResearchPlan.prototype.canStart = function(gameState) +{ + // also checks canResearch + return (gameState.findResearchers(this.type).length !== 0); +}; + +m.ResearchPlan.prototype.isInvalid = function(gameState) +{ + return (gameState.isResearched(this.type) || gameState.isResearching(this.type)); +}; + +m.ResearchPlan.prototype.start = function(gameState) +{ + var self = this; + + //m.debug ("Starting the research plan for " + this.type); + var trainers = gameState.findResearchers(this.type).toEntityArray(); + + //for (var i in trainers) + // warn (this.type + " - " +trainers[i].genericName()); + + // Prefer training buildings with short queues + // (TODO: this should also account for units added to the queue by + // plans that have already been executed this turn) + if (trainers.length > 0){ + trainers.sort(function(a, b) { + return (a.trainingQueueTime() - b.trainingQueueTime()); + }); + // drop anything in the queue if we rush it. + if (this.rush) + trainers[0].stopAllProduction(0.45); + trainers[0].research(this.type); + } + this.onStart(gameState); +}; + +return m; +}(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplan-training.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplan-training.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplan-training.js (revision 14865) @@ -0,0 +1,72 @@ +var PETRA = function(m) +{ + +m.TrainingPlan = function(gameState, type, metadata, number, maxMerge) +{ + if (!m.QueuePlan.call(this, gameState, type, metadata)) + { + warn(" Plan training " + type + " canceled"); + return false; + } + + this.category = "unit"; + this.cost = new API3.Resources(this.template.cost(), this.template._template.Cost.Population); + + this.number = number !== undefined ? number : 1; + this.maxMerge = maxMerge !== undefined ? maxMerge : 5; + + return true; +}; + +m.TrainingPlan.prototype = Object.create(m.QueuePlan.prototype); + +m.TrainingPlan.prototype.canStart = function(gameState) +{ + if (this.invalidTemplate) + return false; + + // TODO: we should probably check pop caps + + var trainers = gameState.findTrainers(this.type); + + return (trainers.length != 0); +}; + +m.TrainingPlan.prototype.start = function(gameState) +{ + //warn("Executing TrainingPlan " + uneval(this)); + var self = this; + var trainers = gameState.findTrainers(this.type).toEntityArray(); + + // Prefer training buildings with short queues + // (TODO: this should also account for units added to the queue by + // plans that have already been executed this turn) + if (trainers.length > 0) + { + trainers.sort(function(a, b) { + var aa = a.trainingQueueTime(); + var bb = b.trainingQueueTime(); + if (a.hasClass("Civic") && !self.template.hasClass("Support")) + aa += 0.9; + if (b.hasClass("Civic") && !self.template.hasClass("Support")) + bb += 0.9; + return (aa - bb); + }); + if (this.metadata && this.metadata.base !== undefined && this.metadata.base === 0) + this.metadata.base = trainers[0].getMetadata(PlayerID,"base"); + trainers[0].train(this.type, this.number, this.metadata); + } + else + warn("pas de trainers for this queue " + this.type); + this.onStart(gameState); +}; + +m.TrainingPlan.prototype.addItem = function(amount) +{ + if (amount === undefined) + amount = 1; + this.number += amount; +}; + +return m; +}(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/templateManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/templateManager.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/templateManager.js (revision 14865) @@ -0,0 +1,123 @@ +var PETRA = function(m) +{ + +/* + * Used to know which templates I have, which templates I know I can train, things like that. + * Mostly unused. + */ + +m.TemplateManager = function(gameState) { + var self = this; + + this.knownTemplatesList = []; + this.buildingTemplates = []; + this.unitTemplates = []; + this.templateCounters = {}; + this.templateCounteredBy = {}; + + // this will store templates that exist + this.AcknowledgeTemplates(gameState); + this.getBuildableSubtemplates(gameState); + this.getTrainableSubtemplates(gameState); + this.getBuildableSubtemplates(gameState); + this.getTrainableSubtemplates(gameState); + // should be enough in 100% of the cases. + + this.getTemplateCounters(gameState); + +}; +m.TemplateManager.prototype.AcknowledgeTemplates = function(gameState) +{ + var self = this; + var myEntities = gameState.getOwnEntities(); + myEntities.forEach(function(ent) + { + var template = ent._templateName; + if (self.knownTemplatesList.indexOf(template) === -1) { + self.knownTemplatesList.push(template); + if (ent.hasClass("Unit") && self.unitTemplates.indexOf(template) === -1) + self.unitTemplates.push(template); + else if (self.buildingTemplates.indexOf(template) === -1) + self.buildingTemplates.push(template); + } + + }); +} +m.TemplateManager.prototype.getBuildableSubtemplates = function(gameState) +{ + for each (var templateName in this.knownTemplatesList) { + var template = gameState.getTemplate(templateName); + if (template !== null) { + var buildable = template.buildableEntities(); + if (buildable !== undefined) + for each (var subtpname in buildable) { + if (this.knownTemplatesList.indexOf(subtpname) === -1) { + this.knownTemplatesList.push(subtpname); + var subtemplate = gameState.getTemplate(subtpname); + if (subtemplate.hasClass("Unit") && this.unitTemplates.indexOf(subtpname) === -1) + this.unitTemplates.push(subtpname); + else if (this.buildingTemplates.indexOf(subtpname) === -1) + this.buildingTemplates.push(subtpname); + } + } + } + } +} +m.TemplateManager.prototype.getTrainableSubtemplates = function(gameState) +{ + for each (var templateName in this.knownTemplatesList) { + var template = gameState.getTemplate(templateName); + if (template !== null) { + var trainables = template.trainableEntities(); + if (trainables !== undefined) + for each (var subtpname in trainables) { + if (this.knownTemplatesList.indexOf(subtpname) === -1) { + this.knownTemplatesList.push(subtpname); + var subtemplate = gameState.getTemplate(subtpname); + if (subtemplate.hasClass("Unit") && this.unitTemplates.indexOf(subtpname) === -1) + this.unitTemplates.push(subtpname); + else if (this.buildingTemplates.indexOf(subtpname) === -1) + this.buildingTemplates.push(subtpname); + } + } + } + } +} +m.TemplateManager.prototype.getTemplateCounters = function(gameState) +{ + for (var i in this.unitTemplates) + { + var tp = gameState.getTemplate(this.unitTemplates[i]); + var tpname = this.unitTemplates[i]; + this.templateCounters[tpname] = tp.getCounteredClasses(); + } +} +// features auto-caching +m.TemplateManager.prototype.getCountersToClasses = function(gameState,classes,templateName) +{ + if (templateName !== undefined && this.templateCounteredBy[templateName]) + return this.templateCounteredBy[templateName]; + + var templates = []; + for (var i in this.templateCounters) { + var okay = false; + for each (var ticket in this.templateCounters[i]) { + var okaya = true; + for (var a in ticket[0]) { + if (classes.indexOf(ticket[0][a]) === -1) + okaya = false; + } + if (okaya && templates.indexOf(i) === -1) + templates.push([i, ticket[1]]); + } + } + templates.sort (function (a,b) { return -a[1] + b[1]; }); + + if (templateName !== undefined) + this.templateCounteredBy[templateName] = templates; + return templates; +} + + +return m; +}(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/tradeManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/tradeManager.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/tradeManager.js (revision 14865) @@ -0,0 +1,109 @@ +var PETRA = function(m) +{ + +/** + * Manage the trade + */ + +m.TradeManager = function(Config) +{ + this.Config = Config; + this.tradeRoute = undefined; + this.targetNumTraders = this.Config.Economy.targetNumTraders; +}; + +m.TradeManager.prototype.init = function(gameState) +{ + this.traders = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "role", "trader")); + this.traders.allowQuickIter(); + this.traders.registerUpdates(); +}; + +m.TradeManager.prototype.setTradeRoute = function(market1, market2) +{ + this.tradeRoute = { "source": market1, "target": market2 }; +}; + +m.TradeManager.prototype.hasTradeRoute = function() +{ + return (this.tradeRoute !== undefined); +}; + +m.TradeManager.prototype.assignTrader = function(ent) +{ + unit.setMetadata(PlayerID, "role", "trader"); + this.traders.updateEnt(unit); +}; + +// TODO take trader ships into account +m.TradeManager.prototype.trainMoreTraders = function(gameState, queues) +{ + var numTraders = gameState.countEntitiesAndQueuedByType(gameState.applyCiv("units/{civ}_support_trader"), true); + if (numTraders < this.targetNumTraders && queues.trader.countQueuedUnits() === 0) + { + var template = gameState.applyCiv("units/{civ}_support_trader") + queues.trader.addItem(new m.TrainingPlan(gameState, template, { "role": "trader", "base": 0 }, 1, 1)); + } +}; + +m.TradeManager.prototype.update = function(gameState, queues) +{ + if (!this.tradeRoute) + return; + var self = this; + if (gameState.ai.playedTurn % 100 === 9) + this.setTradingGoods(gameState); + this.trainMoreTraders(gameState, queues); + this.traders.forEach(function(ent) { self.updateTrader(ent) }); +}; + +// TODO deal with garrisoned trader & check if the trade route (i.e. its markets) still exist +m.TradeManager.prototype.updateTrader = function(ent) +{ + if (!ent.isIdle() || !ent.position()) + return; + + if (API3.SquareVectorDistance(this.tradeRoute.target.position(), ent.position()) > API3.SquareVectorDistance(this.tradeRoute.source.position(), ent.position())) + ent.tradeRoute(this.tradeRoute.target, this.tradeRoute.source); + else + ent.tradeRoute(this.tradeRoute.source, this.tradeRoute.target); +}; + +m.TradeManager.prototype.setTradingGoods = function(gameState) +{ + var tradingGoods = { "food": 0, "wood": 0, "stone": 0, "metal": 0 }; + // first, try to anticipate future needs + var stocks = gameState.ai.HQ.GetTotalResourceLevel(gameState); + var remaining = 100; + this.targetNumTraders = this.Config.Economy.targetNumTraders; + for (var type in stocks) + { + if (type == "food") + continue; + if (stocks[type] < 200) + { + tradingGoods[type] = 20; + this.targetNumTraders += 2; + } + else if (stocks[type] < 500) + { + tradingGoods[type] = 10; + this.targetNumTraders += 1; + } + remaining -= tradingGoods[type]; + } + + // then add what is needed now + var mainNeed = Math.floor(remaining * 70 / 100) + var nextNeed = remaining - mainNeed; + + var mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState); + tradingGoods[mostNeeded[0]] += mainNeed; + tradingGoods[mostNeeded[1]] += nextNeed; + Engine.PostCommand(PlayerID, {"type": "set-trading-goods", "tradingGoods": tradingGoods}); + if (this.Config.debug == 2) + warn(" trading goods set to " + uneval(tradingGoods)); +}; + +return m; +}(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/utils-extend.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/utils-extend.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/utils-extend.js (revision 14865) @@ -0,0 +1,32 @@ +var PETRA = function(m) +{ + +m.AssocArraytoArray = function(assocArray) { + var endArray = []; + for (var i in assocArray) + endArray.push(assocArray[i]); + return endArray; +}; + +// A is the reference, B must be in "range" of A +// this supposes the range is already squared +m.inRange = function(a, b, range)// checks for X distance +{ + // will avoid unnecessary checking for position in some rare cases... I'm lazy + if (a === undefined || b === undefined || range === undefined) + return undefined; + + var dx = a[0] - b[0]; + var dz = a[1] - b[1]; + return ((dx*dx + dz*dz ) < range); +} +// slower than SquareVectorDistance, faster than VectorDistance but not exactly accurate. +m.ManhattanDistance = function(a, b) +{ + var dx = a[0] - b[0]; + var dz = a[1] - b[1]; + return Math.abs(dx) + Math.abs(dz); +} + +return m; +}(PETRA);