Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/baseAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/baseAI.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/baseAI.js (revision 14441) @@ -1,89 +1,98 @@ var PlayerID = -1; -function BaseAI(settings) +var API3 = (function() { + +var m = {}; + +m.BaseAI = function(settings) { if (!settings) return; this.player = settings.player; - PlayerID = this.player; // played turn, in case you don't want the AI to play every turn. this.turn = 0; -} +}; //Return a simple object (using no classes etc) that will be serialized into saved games -BaseAI.prototype.Serialize = function() +m.BaseAI.prototype.Serialize = function() { // TODO: ought to get the AI script subclass to serialize its own state // TODO: actually this is part of a larger reflection on wether AIs should or not. return {}; }; //Called after the constructor when loading a saved game, with 'data' being //whatever Serialize() returned -BaseAI.prototype.Deserialize = function(data, sharedScript) +m.BaseAI.prototype.Deserialize = function(data, sharedScript) { // TODO: ought to get the AI script subclass to deserialize its own state // TODO: actually this is part of a larger reflection on wether AIs should or not. this.isDeserialized = true; }; -BaseAI.prototype.Init = function(state, sharedAI) +m.BaseAI.prototype.Init = function(state, playerID, sharedAI) { + PlayerID = playerID; // define some references this.entities = sharedAI.entities; this.templates = sharedAI.templates; this.passabilityClasses = sharedAI.passabilityClasses; this.passabilityMap = sharedAI.passabilityMap; this.territoryMap = sharedAI.territoryMap; this.accessibility = sharedAI.accessibility; this.terrainAnalyzer = sharedAI.terrainAnalyzer; this.techModifications = sharedAI._techModifications[this.player]; this.playerData = sharedAI.playersData[this.player]; - this.gameState = sharedAI.gameState[PlayerID]; + this.gameState = sharedAI.gameState[this.player]; this.gameState.ai = this; this.sharedScript = sharedAI; this.timeElapsed = sharedAI.timeElapsed; this.CustomInit(this.gameState, this.sharedScript); } -BaseAI.prototype.CustomInit = function() +m.BaseAI.prototype.CustomInit = function() { // AIs override this function }; -BaseAI.prototype.HandleMessage = function(state, sharedAI) +m.BaseAI.prototype.HandleMessage = function(state, playerID, sharedAI) { this.events = sharedAI.events; + PlayerID = playerID; if (this.isDeserialized && this.turn !== 0) { this.isDeserialized = false; this.Init(state, sharedAI); warn("AIs don't work completely with saved games yet. You may run into idle units and unused buildings."); } else if (this.isDeserialized) return; this.OnUpdate(sharedAI); }; -BaseAI.prototype.OnUpdate = function() +m.BaseAI.prototype.OnUpdate = function() { // AIs override this function }; -BaseAI.prototype.chat = function(message) +m.BaseAI.prototype.chat = function(message) { - Engine.PostCommand({"type": "chat", "message": message}); + Engine.PostCommand(PlayerID,{"type": "chat", "message": message}); }; -BaseAI.prototype.chatTeam = function(message) +m.BaseAI.prototype.chatTeam = function(message) { - Engine.PostCommand({"type": "chat", "message": "/team " +message}); + Engine.PostCommand(PlayerID,{"type": "chat", "message": "/team " +message}); }; -BaseAI.prototype.chatEnemies = function(message) +m.BaseAI.prototype.chatEnemies = function(message) { - Engine.PostCommand({"type": "chat", "message": "/enemy " +message}); + Engine.PostCommand(PlayerID,{"type": "chat", "message": "/enemy " +message}); }; +return m; + +}()); + Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/entity.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/entity.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/entity.js (revision 14441) @@ -1,740 +1,745 @@ -var EntityTemplate = Class({ +var API3 = function(m) +{ + +m.EntityTemplate = m.Class({ // techModifications should be the tech modifications of only one player. // gamestates handle "GetTemplate" and should push the player's // while entities should push the owner's _init: function(template, techModifications) { this._techModifications = techModifications; this._template = template; }, genericName: function() { if (!this._template.Identity || !this._template.Identity.GenericName) return undefined; return this._template.Identity.GenericName; }, rank: function() { if (!this._template.Identity) return undefined; return this._template.Identity.Rank; }, classes: function() { if (!this._template.Identity || !this._template.Identity.Classes || !this._template.Identity.Classes._string) return undefined; return this._template.Identity.Classes._string.split(/\s+/); }, requiredTech: function() { if (!this._template.Identity || !this._template.Identity.RequiredTechnology) return undefined; return this._template.Identity.RequiredTechnology; }, available: function(gameState) { if (!this._template.Identity || !this._template.Identity.RequiredTechnology) return true; return gameState.isResearched(this._template.Identity.RequiredTechnology); }, // specifically phase: function() { if (!this._template.Identity || !this._template.Identity.RequiredTechnology) return 0; if (this.template.Identity.RequiredTechnology == "phase_village") return 1; if (this.template.Identity.RequiredTechnology == "phase_town") return 2; if (this.template.Identity.RequiredTechnology == "phase_city") return 3; return 0; }, hasClass: function(name) { var classes = this.classes(); return (classes && classes.indexOf(name) != -1); }, hasClasses: function(array) { var classes = this.classes(); if (!classes) return false; for (var i in array) if (classes.indexOf(array[i]) === -1) return false; return true; }, civ: function() { if (!this._template.Identity) return undefined; return this._template.Identity.Civ; }, cost: function() { if (!this._template.Cost) return undefined; var ret = {}; for (var type in this._template.Cost.Resources) ret[type] = GetTechModifiedProperty(this._techModifications, this._template, "Cost/Resources/"+type, +this._template.Cost.Resources[type]); return ret; }, costSum: function() { if (!this._template.Cost) return undefined; var ret = 0; for (var type in this._template.Cost.Resources) ret += +this._template.Cost.Resources[type]; return ret; }, /** * Returns the radius of a circle surrounding this entity's * obstruction shape, or undefined if no obstruction. */ obstructionRadius: function() { if (!this._template.Obstruction) return undefined; if (this._template.Obstruction.Static) { var w = +this._template.Obstruction.Static["@width"]; var h = +this._template.Obstruction.Static["@depth"]; return Math.sqrt(w*w + h*h) / 2; } if (this._template.Obstruction.Unit) return +this._template.Obstruction.Unit["@radius"]; return 0; // this should never happen }, /** * Returns the radius of a circle surrounding this entity's * footprint. */ footprintRadius: function() { if (!this._template.Footprint) return undefined; if (this._template.Footprint.Square) { var w = +this._template.Footprint.Square["@width"]; var h = +this._template.Footprint.Square["@depth"]; return Math.sqrt(w*w + h*h) / 2; } if (this._template.Footprint.Circle) return +this._template.Footprint.Circle["@radius"]; return 0; // this should never happen }, maxHitpoints: function() { if (this._template.Health !== undefined) return this._template.Health.Max; return 0; }, isHealable: function() { if (this._template.Health !== undefined) return this._template.Health.Unhealable !== "true"; return false; }, isRepairable: function() { if (this._template.Health !== undefined) return this._template.Health.Repairable === "true"; return false; }, getPopulationBonus: function() { if (!this._template.Cost || !this._template.Cost.PopulationBonus) return undefined; return this._template.Cost.PopulationBonus; }, armourStrengths: function() { if (!this._template.Armour) return undefined; return { hack: GetTechModifiedProperty(this._techModifications, this._template, "Armour/Hack", +this._template.Armour.Hack), pierce: GetTechModifiedProperty(this._techModifications, this._template, "Armour/Pierce", +this._template.Armour.Pierce), crush: GetTechModifiedProperty(this._techModifications, this._template, "Armour/Crush", +this._template.Armour.Crush) }; }, attackTypes: function() { if (!this._template.Attack) return undefined; var ret = []; for (var type in this._template.Attack) ret.push(type); return ret; }, attackRange: function(type) { if (!this._template.Attack || !this._template.Attack[type]) return undefined; return { max: GetTechModifiedProperty(this._techModifications, this._template, "Attack/MaxRange", +this._template.Attack[type].MaxRange), min: GetTechModifiedProperty(this._techModifications, this._template, "Attack/MinRange", +(this._template.Attack[type].MinRange || 0)) }; }, attackStrengths: function(type) { if (!this._template.Attack || !this._template.Attack[type]) return undefined; return { hack: GetTechModifiedProperty(this._techModifications, this._template, "Attack/"+type+"/Hack", +(this._template.Attack[type].Hack || 0)), pierce: GetTechModifiedProperty(this._techModifications, this._template, "Attack/"+type+"/Pierce", +(this._template.Attack[type].Pierce || 0)), crush: GetTechModifiedProperty(this._techModifications, this._template, "Attack/"+type+"/Crush", +(this._template.Attack[type].Crush || 0)) }; }, attackTimes: function(type) { if (!this._template.Attack || !this._template.Attack[type]) return undefined; return { prepare: GetTechModifiedProperty(this._techModifications, this._template, "Attack/"+type+"/PrepareTime", +(this._template.Attack[type].PrepareTime || 0)), repeat: GetTechModifiedProperty(this._techModifications, this._template, "Attack/"+type+"/RepeatTime", +(this._template.Attack[type].RepeatTime || 1000)) }; }, // returns the classes this templates counters: // Return type is [ [-neededClasses-] , multiplier ]. getCounteredClasses: function() { if (!this._template.Attack) return undefined; var Classes = []; for (var i in this._template.Attack) { if (!this._template.Attack[i].Bonuses) continue; for (var o in this._template.Attack[i].Bonuses) if (this._template.Attack[i].Bonuses[o].Classes) Classes.push([this._template.Attack[i].Bonuses[o].Classes.split(" "), +this._template.Attack[i].Bonuses[o].Multiplier]); } return Classes; }, // returns true if the entity counters those classes. // TODO: refine using the multiplier countersClasses: function(classes) { if (!this._template.Attack) return false; var mcounter = []; for (var i in this._template.Attack) { if (!this._template.Attack[i].Bonuses) continue; for (var o in this._template.Attack[i].Bonuses) if (this._template.Attack[i].Bonuses[o].Classes) mcounter.concat(this._template.Attack[i].Bonuses[o].Classes.split(" ")); } for (var i in classes) { if (mcounter.indexOf(classes[i]) !== -1) return true; } return false; }, // returns, if it exists, the multiplier from each attack against a given class getMultiplierAgainst: function(type, againstClass) { if (!this._template.Attack || !this._template.Attack[type]) return undefined; if (this._template.Attack[type].Bonuses) for (var o in this._template.Attack[type].Bonuses) { if (!this._template.Attack[type].Bonuses[o].Classes) continue; var total = this._template.Attack[type].Bonuses[o].Classes.split(" "); for (var j in total) if (total[j] === againstClass) return this._template.Attack[type].Bonuses[o].Multiplier; } return 1; }, // returns true if the entity can attack the given class canAttackClass: function(saidClass) { if (!this._template.Attack) return false; for (var i in this._template.Attack) { if (!this._template.Attack[i].RestrictedClasses || !this._template.Attack[i].RestrictedClasses._string) continue; var cannotAttack = this._template.Attack[i].RestrictedClasses._string.split(" "); if (cannotAttack.indexOf(saidClass) !== -1) return false; } return true; }, buildableEntities: function() { if (!this._template.Builder) return undefined; if (!this._template.Builder.Entities._string) return []; var civ = this.civ(); var templates = this._template.Builder.Entities._string.replace(/\{civ\}/g, civ).split(/\s+/); return templates; // TODO: map to Entity? }, trainableEntities: function() { if (!this._template.ProductionQueue || !this._template.ProductionQueue.Entities || !this._template.ProductionQueue.Entities._string) return undefined; var civ = this.civ(); var templates = this._template.ProductionQueue.Entities._string.replace(/\{civ\}/g, civ).split(/\s+/); return templates; }, researchableTechs: function() { if (!this._template.ProductionQueue || !this._template.ProductionQueue.Technologies || !this._template.ProductionQueue.Technologies._string) return undefined; var templates = this._template.ProductionQueue.Technologies._string.split(/\s+/); return templates; }, resourceSupplyType: function() { if (!this._template.ResourceSupply) return undefined; var [type, subtype] = this._template.ResourceSupply.Type.split('.'); return { "generic": type, "specific": subtype }; }, // will return either "food", "wood", "stone", "metal" and not treasure. getResourceType: function() { if (!this._template.ResourceSupply) return undefined; var [type, subtype] = this._template.ResourceSupply.Type.split('.'); if (type == "treasure") return subtype; return type; }, resourceSupplyMax: function() { if (!this._template.ResourceSupply) return undefined; return +this._template.ResourceSupply.Amount; }, maxGatherers: function() { if (this._template.ResourceSupply !== undefined) return +this._template.ResourceSupply.MaxGatherers; return 0; }, resourceGatherRates: function() { if (!this._template.ResourceGatherer) return undefined; var ret = {}; var baseSpeed = GetTechModifiedProperty(this._techModifications, this._template, "ResourceGatherer/BaseSpeed", +this._template.ResourceGatherer.BaseSpeed); for (var r in this._template.ResourceGatherer.Rates) ret[r] = GetTechModifiedProperty(this._techModifications, this._template, "ResourceGatherer/Rates/"+r, +this._template.ResourceGatherer.Rates[r]) * baseSpeed; return ret; }, resourceDropsiteTypes: function() { if (!this._template.ResourceDropsite) return undefined; return this._template.ResourceDropsite.Types.split(/\s+/); }, garrisonableClasses: function() { if (!this._template.GarrisonHolder || !this._template.GarrisonHolder.List._string) return undefined; return this._template.GarrisonHolder.List._string.split(/\s+/); }, garrisonMax: function() { if (!this._template.GarrisonHolder) return undefined; return this._template.GarrisonHolder.Max; }, /** * Returns whether this is an animal that is too difficult to hunt. * (Any non domestic currently.) */ isUnhuntable: function() { if (!this._template.UnitAI || !this._template.UnitAI.NaturalBehaviour) return false; // only attack domestic animals since they won't flee nor retaliate. return this._template.UnitAI.NaturalBehaviour !== "domestic"; }, walkSpeed: function() { if (!this._template.UnitMotion || !this._template.UnitMotion.WalkSpeed) return undefined; return this._template.UnitMotion.WalkSpeed; }, buildCategory: function() { if (!this._template.BuildRestrictions || !this._template.BuildRestrictions.Category) return undefined; return this._template.BuildRestrictions.Category; }, buildTime: function() { if (!this._template.Cost || !this._template.Cost.BuildTime) return undefined; return this._template.Cost.BuildTime; }, buildDistance: function() { if (!this._template.BuildRestrictions || !this._template.BuildRestrictions.Distance) return undefined; return this._template.BuildRestrictions.Distance; }, buildPlacementType: function() { if (!this._template.BuildRestrictions || !this._template.BuildRestrictions.PlacementType) return undefined; return this._template.BuildRestrictions.PlacementType; }, buildTerritories: function() { if (!this._template.BuildRestrictions || !this._template.BuildRestrictions.Territory) return undefined; return this._template.BuildRestrictions.Territory.split(/\s+/); }, hasBuildTerritory: function(territory) { var territories = this.buildTerritories(); return (territories && territories.indexOf(territory) != -1); }, hasTerritoryInfluence: function() { return (this._template.TerritoryInfluence !== undefined); }, territoryInfluenceRadius: function() { if (this._template.TerritoryInfluence !== undefined) return (this._template.TerritoryInfluence.Radius); else return -1; }, territoryInfluenceWeight: function() { if (this._template.TerritoryInfluence !== undefined) return (this._template.TerritoryInfluence.Weight); else return -1; }, visionRange: function() { if (!this._template.Vision) return undefined; return this._template.Vision.Range; } }); -var Entity = Class({ - _super: EntityTemplate, +m.Entity = m.Class({ + _super: m.EntityTemplate, _init: function(sharedAI, entity) { this._super.call(this, sharedAI.GetTemplate(entity.template), sharedAI._techModifications[entity.owner]); this._ai = sharedAI; this._templateName = entity.template; this._entity = entity; }, toString: function() { return "[Entity " + this.id() + " " + this.templateName() + "]"; }, id: function() { return this._entity.id; }, templateName: function() { return this._templateName; }, /** * Returns extra data that the AI scripts have associated with this entity, * for arbitrary local annotations. * (This data is not shared with any other AI scripts.) */ getMetadata: function(player, key) { return this._ai.getMetadata(player, this, key); }, /** * Sets extra data to be associated with this entity. */ setMetadata: function(player, key, value) { this._ai.setMetadata(player, this, key, value); }, deleteAllMetadata: function(player) { delete this._ai._entityMetadata[player][this.id()]; }, deleteMetadata: function(player, key) { this._ai.deleteMetadata(player, this, key); }, position: function() { return this._entity.position; }, isIdle: function() { if (typeof this._entity.idle === "undefined") return undefined; return this._entity.idle; }, unitAIState: function() { return this._entity.unitAIState; }, unitAIOrderData: function() { return this._entity.unitAIOrderData; }, hitpoints: function() { if (this._entity.hitpoints !== undefined) return this._entity.hitpoints; return undefined; }, isHurt: function() { return this.hitpoints() < this.maxHitpoints(); }, healthLevel: function() { return (this.hitpoints() / this.maxHitpoints()); }, needsHeal: function() { return this.isHurt() && this.isHealable(); }, needsRepair: function() { return this.isHurt() && this.isRepairable(); }, /** * Returns the current training queue state, of the form * [ { "id": 0, "template": "...", "count": 1, "progress": 0.5, "metadata": ... }, ... ] */ trainingQueue: function() { var queue = this._entity.trainingQueue; return queue; }, trainingQueueTime: function() { var queue = this._entity.trainingQueue; if (!queue) return undefined; // TODO: compute total time for units in training queue return queue.length; }, foundationProgress: function() { if (this._entity.foundationProgress == undefined) return undefined; return this._entity.foundationProgress; }, owner: function() { return this._entity.owner; }, isOwn: function(player) { if (typeof(this._entity.owner) === "undefined") return false; return this._entity.owner === player; }, isFriendly: function(player) { return this.isOwn(player); // TODO: diplomacy }, isEnemy: function(player) { return !this.isOwn(player); // TODO: diplomacy }, resourceSupplyAmount: function() { if(this._entity.resourceSupplyAmount === undefined) return undefined; return this._entity.resourceSupplyAmount; }, resourceSupplyGatherers: function() { if (this._entity.resourceSupplyGatherers !== undefined) return this._entity.resourceSupplyGatherers; return []; }, isFull: function() { if (this._entity.resourceSupplyGatherers !== undefined) return (this.maxGatherers() === this._entity.resourceSupplyGatherers.length); return undefined; }, resourceCarrying: function() { if(this._entity.resourceCarrying === undefined) return undefined; return this._entity.resourceCarrying; }, - garrisoned: function() { return new EntityCollection(this._ai, this._entity.garrisoned); }, + garrisoned: function() { return new m.EntityCollection(this._ai, this._entity.garrisoned); }, canGarrisonInside: function() { return this._entity.garrisoned.length < this.garrisonMax(); }, // TODO: visibility move: function(x, z, queued) { queued = queued || false; - Engine.PostCommand({"type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": queued }); + Engine.PostCommand(PlayerID,{"type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": queued }); return this; }, attackMove: function(x, z, queued) { queued = queued || false; - Engine.PostCommand({"type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "queued": queued }); + Engine.PostCommand(PlayerID,{"type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "queued": queued }); return this; }, // violent, aggressive, defensive, passive, standground setStance: function(stance,queued){ - Engine.PostCommand({"type": "stance", "entities": [this.id()], "name" : stance, "queued": queued }); + Engine.PostCommand(PlayerID,{"type": "stance", "entities": [this.id()], "name" : stance, "queued": queued }); return this; }, // TODO: replace this with the proper "STOP" command stopMoving: function() { if (this.position() !== undefined) - Engine.PostCommand({"type": "walk", "entities": [this.id()], "x": this.position()[0], "z": this.position()[1], "queued": false}); + Engine.PostCommand(PlayerID,{"type": "walk", "entities": [this.id()], "x": this.position()[0], "z": this.position()[1], "queued": false}); }, unload: function(id) { if (!this._template.GarrisonHolder) return undefined; - Engine.PostCommand({"type": "unload", "garrisonHolder": this.id(), "entities": [id]}); + Engine.PostCommand(PlayerID,{"type": "unload", "garrisonHolder": this.id(), "entities": [id]}); return this; }, // Unloads all owned units, don't unload allies unloadAll: function() { if (!this._template.GarrisonHolder) return undefined; - Engine.PostCommand({"type": "unload-all-own", "garrisonHolders": [this.id()]}); + Engine.PostCommand(PlayerID,{"type": "unload-all-own", "garrisonHolders": [this.id()]}); return this; }, garrison: function(target) { - Engine.PostCommand({"type": "garrison", "entities": [this.id()], "target": target.id(),"queued": false}); + Engine.PostCommand(PlayerID,{"type": "garrison", "entities": [this.id()], "target": target.id(),"queued": false}); return this; }, attack: function(unitId) { - Engine.PostCommand({"type": "attack", "entities": [this.id()], "target": unitId, "queued": false}); + Engine.PostCommand(PlayerID,{"type": "attack", "entities": [this.id()], "target": unitId, "queued": false}); return this; }, // Flees from a unit in the opposite direction. flee: function(unitToFleeFrom) { if (this.position() !== undefined && unitToFleeFrom.position() !== undefined) { var FleeDirection = [this.position()[0] - unitToFleeFrom.position()[0],this.position()[1] - unitToFleeFrom.position()[1]]; - var dist = VectorDistance(unitToFleeFrom.position(), this.position() ); + var dist = m.VectorDistance(unitToFleeFrom.position(), this.position() ); FleeDirection[0] = (FleeDirection[0]/dist) * 8; FleeDirection[1] = (FleeDirection[1]/dist) * 8; - Engine.PostCommand({"type": "walk", "entities": [this.id()], "x": this.position()[0] + FleeDirection[0]*5, "z": this.position()[1] + FleeDirection[1]*5, "queued": false}); + Engine.PostCommand(PlayerID,{"type": "walk", "entities": [this.id()], "x": this.position()[0] + FleeDirection[0]*5, "z": this.position()[1] + FleeDirection[1]*5, "queued": false}); } return this; }, gather: function(target, queued) { queued = queued || false; - Engine.PostCommand({"type": "gather", "entities": [this.id()], "target": target.id(), "queued": queued}); + Engine.PostCommand(PlayerID,{"type": "gather", "entities": [this.id()], "target": target.id(), "queued": queued}); return this; }, repair: function(target, queued) { queued = queued || false; - Engine.PostCommand({"type": "repair", "entities": [this.id()], "target": target.id(), "autocontinue": false, "queued": queued}); + Engine.PostCommand(PlayerID,{"type": "repair", "entities": [this.id()], "target": target.id(), "autocontinue": false, "queued": queued}); return this; }, returnResources: function(target, queued) { queued = queued || false; - Engine.PostCommand({"type": "returnresource", "entities": [this.id()], "target": target.id(), "queued": queued}); + Engine.PostCommand(PlayerID,{"type": "returnresource", "entities": [this.id()], "target": target.id(), "queued": queued}); return this; }, destroy: function() { - Engine.PostCommand({"type": "delete-entities", "entities": [this.id()] }); + Engine.PostCommand(PlayerID,{"type": "delete-entities", "entities": [this.id()] }); return this; }, barter: function(buyType, sellType, amount) { - Engine.PostCommand({"type": "barter", "sell" : sellType, "buy" : buyType, "amount" : amount }); + Engine.PostCommand(PlayerID,{"type": "barter", "sell" : sellType, "buy" : buyType, "amount" : amount }); return this; }, train: function(type, count, metadata) { var trainable = this.trainableEntities(); if (!trainable) { error("Called train("+type+", "+count+") on non-training entity "+this); return this; } if (trainable.indexOf(type) === -1) { error("Called train("+type+", "+count+") on entity "+this+" which can't train that"); return this; } - Engine.PostCommand({ + Engine.PostCommand(PlayerID,{ "type": "train", "entities": [this.id()], "template": type, "count": count, "metadata": metadata }); return this; }, construct: function(template, x, z, angle, metadata) { // TODO: verify this unit can construct this, just for internal // sanity-checking and error reporting - - Engine.PostCommand({ + Engine.PostCommand(PlayerID,{ "type": "construct", "entities": [this.id()], "template": template, "x": x, "z": z, "angle": angle, "autorepair": false, "autocontinue": false, "queued": false, "metadata" : metadata // can be undefined }); return this; }, research: function(template) { - Engine.PostCommand({ "type": "research", "entity": this.id(), "template": template }); + Engine.PostCommand(PlayerID,{ "type": "research", "entity": this.id(), "template": template }); return this; }, stopProduction: function(id) { - Engine.PostCommand({ "type": "stop-production", "entity": this.id(), "id": id }); + Engine.PostCommand(PlayerID,{ "type": "stop-production", "entity": this.id(), "id": id }); return this; }, stopAllProduction: function(percentToStopAt) { var queue = this._entity.trainingQueue; if (!queue) return true; // no queue, so technically we stopped all production. for (var i in queue) { if (queue[i].progress < percentToStopAt) - Engine.PostCommand({ "type": "stop-production", "entity": this.id(), "id": queue[i].id }); + Engine.PostCommand(PlayerID,{ "type": "stop-production", "entity": this.id(), "id": queue[i].id }); } return this; } }); +return m; + +}(API3); Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/filters.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/filters.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/filters.js (revision 14441) @@ -1,240 +1,248 @@ -var Filters = { +var API3 = function(m) +{ + +m.Filters = { byType: function(type){ return {"func" : function(ent){ return ent.templateName() === type; }, "dynamicProperties": []}; }, byClass: function(cls){ return {"func" : function(ent){ return ent.hasClass(cls); }, "dynamicProperties": []}; }, byClassesAnd: function(clsList){ return {"func" : function(ent){ var ret = true; for (var i in clsList){ ret = ret && ent.hasClass(clsList[i]); } return ret; }, "dynamicProperties": []}; }, byClassesOr: function(clsList){ return {"func" : function(ent){ var ret = false; for (var i in clsList){ ret = ret || ent.hasClass(clsList[i]); } return ret; }, "dynamicProperties": []}; }, byMetadata: function(player, key, value){ return {"func" : function(ent){ return (ent.getMetadata(player, key) == value); }, "dynamicProperties": ['metadata.' + key]}; }, // can be used for stuffs which won't change once entities are created. byStaticMetadata: function(player, key, value){ return {"func" : function(ent){ return (ent.getMetadata(player, key) == value); }, "dynamicProperties": []}; }, byHasMetadata: function(player, key){ return {"func" : function(ent){ return (ent.getMetadata(player, key) != undefined); }, "dynamicProperties": ['metadata.' + key]}; }, and: function(filter1, filter2){ return {"func": function(ent){ return filter1.func(ent) && filter2.func(ent); }, "dynamicProperties": filter1.dynamicProperties.concat(filter2.dynamicProperties)}; }, or: function(filter1, filter2){ return {"func" : function(ent){ return filter1.func(ent) || filter2.func(ent); }, "dynamicProperties": filter1.dynamicProperties.concat(filter2.dynamicProperties)}; }, not: function(filter){ return {"func": function(ent){ return !filter.func(ent); }, "dynamicProperties": filter.dynamicProperties}; }, byOwner: function(owner){ return {"func" : function(ent){ return (ent.owner() === owner); }, "dynamicProperties": ['owner']}; }, byNotOwner: function(owner){ return {"func" : function(ent){ return (ent.owner() !== owner); }, "dynamicProperties": ['owner']}; }, byOwners: function(owners){ return {"func" : function(ent){ for (var i in owners){ if (ent.owner() == +owners[i]){ return true; } } return false; }, "dynamicProperties": ['owner']}; }, byCanGarrison: function(){ return {"func" : function(ent){ return ent.garrisonMax() > 0; }, "dynamicProperties": []}; }, byTrainingQueue: function(){ return {"func" : function(ent){ return ent.trainingQueue(); }, "dynamicProperties": ['trainingQueue']}; }, byResearchAvailable: function(){ return {"func" : function(ent){ return ent.researchableTechs() !== undefined; }, "dynamicProperties": []}; }, byTargetedEntity: function(targetID){ return {"func": function(ent) { return (ent.unitAIOrderData().length && ent.unitAIOrderData()[0]["target"] && ent.unitAIOrderData()[0]["target"] == targetID); }, "dynamicProperties": ['unitAIOrderData']}; }, byCanAttack: function(saidClass){ return {"func" : function(ent){ return ent.canAttackClass(saidClass); }, "dynamicProperties": []}; }, isGarrisoned: function(){ return {"func" : function(ent){ return ent.position() == -1; // assumes garrisoned }, "dynamicProperties": []}; }, isSoldier: function(){ return {"func" : function(ent){ return Filters.byClassesOr(["CitizenSoldier", "Super"])(ent); }, "dynamicProperties": []}; }, isIdle: function(){ return {"func" : function(ent){ return ent.isIdle(); }, "dynamicProperties": ['idle']}; }, isFoundation: function(){ return {"func": function(ent){ return ent.foundationProgress() !== undefined; }, "dynamicProperties": []}; }, byDistance: function(startPoint, dist){ return {"func": function(ent){ if (ent.position() === undefined){ return false; }else{ - return (SquareVectorDistance(startPoint, ent.position()) < dist*dist); + return (m.SquareVectorDistance(startPoint, ent.position()) < dist*dist); } }, "dynamicProperties": ['position']}; }, // Distance filter with no auto updating, use with care byStaticDistance: function(startPoint, dist){ return {"func": function(ent){ if (!ent.position()){ return false; }else{ - return (SquareVectorDistance(startPoint, ent.position()) < dist*dist); + return (m.SquareVectorDistance(startPoint, ent.position()) < dist*dist); } }, "dynamicProperties": []}; }, byTerritory: function(Map, territoryIndex){ return {"func": function(ent){ if (Map.point(ent.position()) == territoryIndex) { return true; } else { return false; } }, "dynamicProperties": ['position']}; }, isDropsite: function(resourceType){ return {"func": function(ent){ return (ent.resourceDropsiteTypes() && (resourceType === undefined || ent.resourceDropsiteTypes().indexOf(resourceType) !== -1)); }, "dynamicProperties": []}; }, byResource: function(resourceType){ return {"func" : function(ent){ var type = ent.resourceSupplyType(); if (!type) return false; var amount = ent.resourceSupplyMax(); if (!amount) return false; // Skip targets that are too hard to hunt if (ent.isUnhuntable()) return false; // And don't go for the bloody fish! TODO: better accessibility checks if (ent.hasClass("SeaCreature")) { return false; } // Don't go for floating treasures since we won't be able to reach them and it kills the pathfinder. if (ent.templateName() == "other/special_treasure_shipwreck_debris" || ent.templateName() == "other/special_treasure_shipwreck" ){ return false; } if (type.generic == "treasure"){ return (resourceType == type.specific); } else { return (resourceType == type.generic); } }, "dynamicProperties": []}; } }; + +return m; + +}(API3); + Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/map-module.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/map-module.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/map-module.js (revision 14441) @@ -1,294 +1,300 @@ +var API3 = function(m) +{ + /* The map module. * Copied with changes from QuantumState's original for qBot, it's a component for storing 8 bit values. */ -const TERRITORY_PLAYER_MASK = 0x3F; - -function Map(sharedScript, originalMap, actualCopy){ +// The function needs to be named too because of the copyConstructor functionality +m.Map = function Map(sharedScript, originalMap, actualCopy){ // get the map to find out the correct dimensions var gameMap = sharedScript.passabilityMap; this.width = gameMap.width; this.height = gameMap.height; this.length = gameMap.data.length; this.maxVal = 255; if (originalMap && actualCopy){ this.map = new Uint8Array(this.length); for (var i = 0; i < originalMap.length; ++i) this.map[i] = originalMap[i]; } else if (originalMap) { this.map = originalMap; } else { this.map = new Uint8Array(this.length); } this.cellSize = 4; } -Map.prototype.setMaxVal = function(val){ +m.Map.prototype.setMaxVal = function(val){ this.maxVal = val; }; -Map.prototype.gamePosToMapPos = function(p){ +m.Map.prototype.gamePosToMapPos = function(p){ return [Math.floor(p[0]/this.cellSize), Math.floor(p[1]/this.cellSize)]; }; -Map.prototype.point = function(p){ +m.Map.prototype.point = function(p){ var q = this.gamePosToMapPos(p); return this.map[q[0] + this.width * q[1]]; }; -Map.prototype.addInfluence = function(cx, cy, maxDist, strength, type) { +m.Map.prototype.addInfluence = function(cx, cy, maxDist, strength, type) { strength = strength ? +strength : +maxDist; type = type ? type : 'linear'; var x0 = Math.max(0, cx - maxDist); var y0 = Math.max(0, cy - maxDist); var x1 = Math.min(this.width, cx + maxDist); var y1 = Math.min(this.height, cy + maxDist); var maxDist2 = maxDist * maxDist; var str = 0.0; switch (type){ case 'linear': str = +strength / +maxDist; break; case 'quadratic': str = +strength / +maxDist2; break; case 'constant': str = +strength; break; } for ( var y = y0; y < y1; ++y) { for ( var x = x0; x < x1; ++x) { var dx = x - cx; var dy = y - cy; var r2 = dx*dx + dy*dy; if (r2 < maxDist2){ var quant = 0; switch (type){ case 'linear': var r = Math.sqrt(r2); quant = str * (maxDist - r); break; case 'quadratic': quant = str * (maxDist2 - r2); break; case 'constant': quant = str; break; } if (this.map[x + y * this.width] + quant < 0) this.map[x + y * this.width] = 0; else if (this.map[x + y * this.width] + quant > this.maxVal) this.map[x + y * this.width] = this.maxVal; // avoids overflow. else this.map[x + y * this.width] += quant; } } } }; -Map.prototype.multiplyInfluence = function(cx, cy, maxDist, strength, type) { +m.Map.prototype.multiplyInfluence = function(cx, cy, maxDist, strength, type) { strength = strength ? +strength : +maxDist; type = type ? type : 'constant'; var x0 = Math.max(0, cx - maxDist); var y0 = Math.max(0, cy - maxDist); var x1 = Math.min(this.width, cx + maxDist); var y1 = Math.min(this.height, cy + maxDist); var maxDist2 = maxDist * maxDist; var str = 0.0; switch (type){ case 'linear': str = strength / maxDist; break; case 'quadratic': str = strength / maxDist2; break; case 'constant': str = strength; break; } for ( var y = y0; y < y1; ++y) { for ( var x = x0; x < x1; ++x) { var dx = x - cx; var dy = y - cy; var r2 = dx*dx + dy*dy; if (r2 < maxDist2){ var quant = 0; switch (type){ case 'linear': var r = Math.sqrt(r2); quant = str * (maxDist - r); break; case 'quadratic': quant = str * (maxDist2 - r2); break; case 'constant': quant = str; break; } var machin = this.map[x + y * this.width] * quant; if (machin < 0) this.map[x + y * this.width] = 0; else if (machin > this.maxVal) this.map[x + y * this.width] = this.maxVal; else this.map[x + y * this.width] = machin; } } } }; // doesn't check for overflow. -Map.prototype.setInfluence = function(cx, cy, maxDist, value) { +m.Map.prototype.setInfluence = function(cx, cy, maxDist, value) { value = value ? value : 0; var x0 = Math.max(0, cx - maxDist); var y0 = Math.max(0, cy - maxDist); var x1 = Math.min(this.width, cx + maxDist); var y1 = Math.min(this.height, cy + maxDist); var maxDist2 = maxDist * maxDist; for ( var y = y0; y < y1; ++y) { for ( var x = x0; x < x1; ++x) { var dx = x - cx; var dy = y - cy; var r2 = dx*dx + dy*dy; if (r2 < maxDist2){ this.map[x + y * this.width] = value; } } } }; -Map.prototype.sumInfluence = function(cx, cy, radius){ +m.Map.prototype.sumInfluence = function(cx, cy, radius){ var x0 = Math.max(0, cx - radius); var y0 = Math.max(0, cy - radius); var x1 = Math.min(this.width, cx + radius); var y1 = Math.min(this.height, cy + radius); var radius2 = radius * radius; var sum = 0; for ( var y = y0; y < y1; ++y) { for ( var x = x0; x < x1; ++x) { var dx = x - cx; var dy = y - cy; var r2 = dx*dx + dy*dy; if (r2 < radius2){ sum += this.map[x + y * this.width]; } } } return sum; }; /** * Make each cell's 16-bit/8-bit value at least one greater than each of its * neighbours' values. (If the grid is initialised with 0s and 65535s or 255s, the * result of each cell is its Manhattan distance to the nearest 0.) */ -Map.prototype.expandInfluences = function(maximum, map) { +m.Map.prototype.expandInfluences = function(maximum, map) { var grid = this.map; if (map !== undefined) grid = map; if (maximum == undefined) maximum = this.maxVal; var w = this.width; var h = this.height; for ( var y = 0; y < h; ++y) { var min = maximum; for ( var x = 0; x < w; ++x) { var g = grid[x + y * w]; if (g > min) grid[x + y * w] = min; else if (g < min) min = g; ++min; if (min > maximum) min = maximum; } for ( var x = w - 2; x >= 0; --x) { var g = grid[x + y * w]; if (g > min) grid[x + y * w] = min; else if (g < min) min = g; ++min; if (min > maximum) min = maximum; } } for ( var x = 0; x < w; ++x) { var min = maximum; for ( var y = 0; y < h; ++y) { var g = grid[x + y * w]; if (g > min) grid[x + y * w] = min; else if (g < min) min = g; ++min; if (min > maximum) min = maximum; } for ( var y = h - 2; y >= 0; --y) { var g = grid[x + y * w]; if (g > min) grid[x + y * w] = min; else if (g < min) min = g; ++min; if (min > maximum) min = maximum; } } }; -Map.prototype.findBestTile = function(radius, obstructionTiles){ +m.Map.prototype.findBestTile = function(radius, obstructionTiles){ // Find the best non-obstructed tile var bestIdx = 0; var bestVal = -1; for ( var i = 0; i < this.length; ++i) { if (obstructionTiles.map[i] > radius) { var v = this.map[i]; if (v > bestVal) { bestVal = v; bestIdx = i; } } } return [bestIdx, bestVal]; }; // Multiplies current map by the parameter map pixelwise -Map.prototype.multiply = function(map, onlyBetter, divider, maxMultiplier){ +m.Map.prototype.multiply = function(map, onlyBetter, divider, maxMultiplier){ for (var i = 0; i < this.length; ++i){ if (map.map[i]/divider > 1) this.map[i] = Math.min(maxMultiplier*this.map[i], this.map[i] * (map.map[i]/divider)); } }; // add to current map by the parameter map pixelwise -Map.prototype.add = function(map){ +m.Map.prototype.add = function(map){ for (var i = 0; i < this.length; ++i) { if (this.map[i] + map.map[i] < 0) this.map[i] = 0; else if (this.map[i] + map.map[i] > this.maxVal) this.map[i] = this.maxVal; else this.map[i] += map.map[i]; } }; -Map.prototype.dumpIm = function(name, threshold){ +m.Map.prototype.dumpIm = function(name, threshold){ name = name ? name : "default.png"; threshold = threshold ? threshold : this.maxVal; Engine.DumpImage(name, this.map, this.width, this.height, threshold); }; + +return m; + +}(API3); Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/shared.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/shared.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/shared.js (revision 14441) @@ -1,389 +1,396 @@ +var API3 = function(m) +{ + // Shared script handling templates and basic terrain analysis -function SharedScript(settings) +m.SharedScript = function(settings) { if (!settings) return; this._players = settings.players; this._templates = settings.templates; this._derivedTemplates = {}; this._techTemplates = settings.techTemplates; this._entityMetadata = {}; for (var i in this._players) this._entityMetadata[this._players[i]] = {}; // always create for 8 + gaia players, since _players isn't aware of the human. this._techModifications = { 0 : {}, 1 : {}, 2 : {}, 3 : {}, 4 : {}, 5 : {}, 6 : {}, 7 : {}, 8 : {} }; // array of entity collections this._entityCollections = []; // each name is a reference to the actual one. this._entityCollectionsName = {}; this._entityCollectionsByDynProp = {}; this._entityCollectionsUID = 0; // A few notes about these maps. They're updated by checking for "create" and "destroy" events for all resources // TODO: change the map when the resource amounts change for at least stone and metal mines. this.resourceMaps = {}; // Contains maps showing the density of wood, stone and metal this.CCResourceMaps = {}; // Contains maps showing the density of wood, stone and metal, optimized for CC placement. // Resource maps data. // By how much to divide the resource amount for plotting (ie a tree having 200 wood is "4"). this.decreaseFactor = {'wood': 50.0, 'stone': 90.0, 'metal': 90.0, 'food': 40.0}; this.turn = 0; } //Return a simple object (using no classes etc) that will be serialized //into saved games //TODO: that -SharedScript.prototype.Serialize = function() +m.SharedScript.prototype.Serialize = function() { return { "players" : this._players, "templates" : this._templates, "techTp" : this._techTemplates }; }; // Called after the constructor when loading a saved game, with 'data' being // whatever Serialize() returned -SharedScript.prototype.Deserialize = function(data) +m.SharedScript.prototype.Deserialize = function(data) { this._players = data.players; this._templates = data.templates; this._techTemplates = data.techTp; this.isDeserialized = true; }; // Components that will be disabled in foundation entity templates. // (This is a bit yucky and fragile since it's the inverse of // CCmpTemplateManager::CopyFoundationSubset and only includes components // that our EntityTemplate class currently uses.) -var g_FoundationForbiddenComponents = { +m.g_FoundationForbiddenComponents = { "ProductionQueue": 1, "ResourceSupply": 1, "ResourceDropsite": 1, "GarrisonHolder": 1, }; // Components that will be disabled in resource entity templates. // Roughly the inverse of CCmpTemplateManager::CopyResourceSubset. -var g_ResourceForbiddenComponents = { +m.g_ResourceForbiddenComponents = { "Cost": 1, "Decay": 1, "Health": 1, "UnitAI": 1, "UnitMotion": 1, "Vision": 1 }; -SharedScript.prototype.GetTemplate = function(name) +m.SharedScript.prototype.GetTemplate = function(name) { if (this._templates[name]) return this._templates[name]; if (this._derivedTemplates[name]) return this._derivedTemplates[name]; // If this is a foundation template, construct it automatically if (name.indexOf("foundation|") !== -1) { var base = this.GetTemplate(name.substr(11)); var foundation = {}; for (var key in base) - if (!g_FoundationForbiddenComponents[key]) + if (!m.g_FoundationForbiddenComponents[key]) foundation[key] = base[key]; this._derivedTemplates[name] = foundation; return foundation; } else if (name.indexOf("resource|") !== -1) { var base = this.GetTemplate(name.substr(9)); var resource = {}; for (var key in base) - if (!g_ResourceForbiddenComponents[key]) + if (!m.g_ResourceForbiddenComponents[key]) resource[key] = base[key]; this._derivedTemplates[name] = resource; return resource; } error("Tried to retrieve invalid template '"+name+"'"); return null; }; // Initialize the shared component. // We need to now the initial state of the game for this, as we will use it. // This is called right at the end of the map generation. -SharedScript.prototype.init = function(state) { +m.SharedScript.prototype.init = function(state) { this.passabilityClasses = state.passabilityClasses; this.passabilityMap = state.passabilityMap; this.players = this._players; this.playersData = state.players; this.territoryMap = state.territoryMap; this.timeElapsed = state.timeElapsed; for (var o in state.players) this._techModifications[o] = state.players[o].techModifications; this._entities = {}; for (var id in state.entities) - this._entities[id] = new Entity(this, state.entities[id]); + this._entities[id] = new m.Entity(this, state.entities[id]); // entity collection updated on create/destroy event. - this.entities = new EntityCollection(this, this._entities); + this.entities = new m.EntityCollection(this, this._entities); // create the terrain analyzer - this.terrainAnalyzer = new TerrainAnalysis(); + this.terrainAnalyzer = new m.TerrainAnalysis(); this.terrainAnalyzer.init(this, state); - this.accessibility = new Accessibility(); + this.accessibility = new m.Accessibility(); this.accessibility.init(state, this.terrainAnalyzer); // defined in TerrainAnalysis.js this.createResourceMaps(this); this.gameState = {}; for (var i in this._players) { - this.gameState[this._players[i]] = new GameState(); + this.gameState[this._players[i]] = new m.GameState(); this.gameState[this._players[i]].init(this,state,this._players[i]); } }; // General update of the shared script, before each AI's update // applies entity deltas, and each gamestate. -SharedScript.prototype.onUpdate = function(state) +m.SharedScript.prototype.onUpdate = function(state) { if (this.isDeserialized && this.turn !== 0) { this.isDeserialized = false; this.init(state); } else if (this.isDeserialized) return; // deals with updating based on create and destroy messages. this.ApplyEntitiesDelta(state); Engine.ProfileStart("onUpdate"); // those are dynamic and need to be reset as the "state" object moves in memory. this.events = state.events; this.passabilityClasses = state.passabilityClasses; this.passabilityMap = state.passabilityMap; this.playersData = state.players; this.territoryMap = state.territoryMap; this.timeElapsed = state.timeElapsed; for (var o in state.players) this._techModifications[o] = state.players[o].techModifications; for (var i in this.gameState) this.gameState[i].update(this,state); // TODO: merge those two with "ApplyEntitiesDelta" since after all they do the same. this.updateResourceMaps(this, this.events); this.terrainAnalyzer.updateMapWithEvents(this); this.turn++; Engine.ProfileStop(); }; -SharedScript.prototype.ApplyEntitiesDelta = function(state) +m.SharedScript.prototype.ApplyEntitiesDelta = function(state) { Engine.ProfileStart("Shared ApplyEntitiesDelta"); var foundationFinished = {}; for each (var evt in state.events) { if (evt.type == "Create") { if (!state.entities[evt.msg.entity]) { continue; // Sometimes there are things like foundations which get destroyed too fast } - this._entities[evt.msg.entity] = new Entity(this, state.entities[evt.msg.entity]); + this._entities[evt.msg.entity] = new m.Entity(this, state.entities[evt.msg.entity]); this.entities.addEnt(this._entities[evt.msg.entity]); // Update all the entity collections since the create operation affects static properties as well as dynamic for (var entCollection in this._entityCollections) { this._entityCollections[entCollection].updateEnt(this._entities[evt.msg.entity]); } } else if (evt.type == "Destroy") { // A small warning: javascript "delete" does not actually delete, it only removes the reference in this object. // the "deleted" object remains in memory, and any older reference to it will still reference it as if it were not "deleted". // Worse, they might prevent it from being garbage collected, thus making it stay alive and consuming ram needlessly. // So take care, and if you encounter a weird bug with deletion not appearing to work correctly, this is probably why. if (!this._entities[evt.msg.entity]) continue; if (foundationFinished[evt.msg.entity]) evt.msg["SuccessfulFoundation"] = true; // The entity was destroyed but its data may still be useful, so // remember the entity and this AI's metadata concerning it evt.msg.metadata = {}; evt.msg.entityObj = (evt.msg.entityObj || this._entities[evt.msg.entity]); for (var i in this._players) evt.msg.metadata[this._players[i]] = this._entityMetadata[this._players[i]][evt.msg.entity]; for each (var entCol in this._entityCollections) { entCol.removeEnt(this._entities[evt.msg.entity]); } this.entities.removeEnt(this._entities[evt.msg.entity]); delete this._entities[evt.msg.entity]; for (var i in this._players) delete this._entityMetadata[this._players[i]][evt.msg.entity]; } else if (evt.type == "EntityRenamed") { // Switch the metadata for (var i in this._players) { this._entityMetadata[this._players[i]][evt.msg.newentity] = this._entityMetadata[this._players[i]][evt.msg.entity]; this._entityMetadata[this._players[i]][evt.msg.entity] = {}; } } else if (evt.type == "TrainingFinished") { // Apply metadata stored in training queues for each (var ent in evt.msg.entities) { for (var key in evt.msg.metadata) { this.setMetadata(evt.msg.owner, this._entities[ent], key, evt.msg.metadata[key]) } } } else if (evt.type == "ConstructionFinished") { // we can rely on this being before the "Destroy" command as this is the order defined by FOundation.js // we'll move metadata. if (!this._entities[evt.msg.entity]) continue; var ent = this._entities[evt.msg.entity]; var newEnt = this._entities[evt.msg.newentity]; if (this._entityMetadata[ent.owner()] && this._entityMetadata[ent.owner()][evt.msg.entity] !== undefined) for (var key in this._entityMetadata[ent.owner()][evt.msg.entity]) { this.setMetadata(ent.owner(), newEnt, key, this._entityMetadata[ent.owner()][evt.msg.entity][key]) } foundationFinished[evt.msg.entity] = true; } else if (evt.type == "AIMetadata") { if (!this._entities[evt.msg.id]) continue; // might happen in some rare cases of foundations getting destroyed, perhaps. // Apply metadata (here for buildings for example) for (var key in evt.msg.metadata) { this.setMetadata(evt.msg.owner, this._entities[evt.msg.id], key, evt.msg.metadata[key]) } } } for (var id in state.entities) { var changes = state.entities[id]; for (var prop in changes) { this._entities[id]._entity[prop] = changes[prop]; this.updateEntityCollections(prop, this._entities[id]); } } Engine.ProfileStop(); }; -SharedScript.prototype.registerUpdatingEntityCollection = function(entCollection, noPush) +m.SharedScript.prototype.registerUpdatingEntityCollection = function(entCollection, noPush) { if (!noPush) { this._entityCollections.push(entCollection); } entCollection.setUID(this._entityCollectionsUID); for each (var prop in entCollection.dynamicProperties()) { this._entityCollectionsByDynProp[prop] = this._entityCollectionsByDynProp[prop] || []; this._entityCollectionsByDynProp[prop].push(entCollection); } this._entityCollectionsUID++; }; -SharedScript.prototype.removeUpdatingEntityCollection = function(entCollection) +m.SharedScript.prototype.removeUpdatingEntityCollection = function(entCollection) { for (var i in this._entityCollections) { if (this._entityCollections[i].getUID() === entCollection.getUID()) { this._entityCollections.splice(i, 1); } } for each (var prop in entCollection.dynamicProperties()) { for (var i in this._entityCollectionsByDynProp[prop]) { if (this._entityCollectionsByDynProp[prop][i].getUID() === entCollection.getUID()) { this._entityCollectionsByDynProp[prop].splice(i, 1); } } } }; -SharedScript.prototype.updateEntityCollections = function(property, ent) +m.SharedScript.prototype.updateEntityCollections = function(property, ent) { if (this._entityCollectionsByDynProp[property] !== undefined) { for (var entCollectionid in this._entityCollectionsByDynProp[property]) { this._entityCollectionsByDynProp[property][entCollectionid].updateEnt(ent); } } } -SharedScript.prototype.setMetadata = function(player, ent, key, value) +m.SharedScript.prototype.setMetadata = function(player, ent, key, value) { var metadata = this._entityMetadata[player][ent.id()]; if (!metadata) metadata = this._entityMetadata[player][ent.id()] = {}; metadata[key] = value; this.updateEntityCollections('metadata', ent); this.updateEntityCollections('metadata.' + key, ent); }; -SharedScript.prototype.getMetadata = function(player, ent, key) +m.SharedScript.prototype.getMetadata = function(player, ent, key) { var metadata = this._entityMetadata[player][ent.id()]; if (!metadata || !(key in metadata)) return undefined; return metadata[key]; }; -SharedScript.prototype.deleteMetadata = function(player, ent, key) +m.SharedScript.prototype.deleteMetadata = function(player, ent, key) { var metadata = this._entityMetadata[player][ent.id()]; if (!metadata || !(key in metadata)) return true; metadata[key] = undefined; delete metadata[key]; return true; }; -function copyPrototype(descendant, parent) { +m.copyPrototype = function(descendant, parent) { var sConstructor = parent.toString(); var aMatch = sConstructor.match( /\s*function (.*)\(/ ); if ( aMatch != null ) { descendant.prototype[aMatch[1]] = parent; } for (var m in parent.prototype) { descendant.prototype[m] = parent.prototype[m]; } }; +return m; + +}(API3); + Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/terrain-analysis.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/terrain-analysis.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/terrain-analysis.js (revision 14441) @@ -1,782 +1,789 @@ +var API3 = function(m) +{ + /* * TerrainAnalysis, inheriting from the Map Component. * * This creates a suitable passability map for pathfinding units and provides the findClosestPassablePoint() function. * This is part of the Shared Script, and thus should only be used for things that are non-player specific. * This.map is a map of the world, where particular stuffs are pointed with a value * For example, impassable land is 0, water is 200, areas near tree (ie forest grounds) are 41… * This is intended for use with 8 bit maps for reduced memory usage. * Upgraded from QuantumState's original TerrainAnalysis for qBot. * You may notice a lot of the A* star codes differ only by a few things. * It's wanted: each does a very slightly different things * But truly separating optimizes. */ -function TerrainAnalysis() { +m.TerrainAnalysis = function() { this.cellSize = 4; } -copyPrototype(TerrainAnalysis, Map); +m.copyPrototype(m.TerrainAnalysis, m.Map); -TerrainAnalysis.prototype.init = function(sharedScript,rawState) { +m.TerrainAnalysis.prototype.init = function(sharedScript,rawState) { var self = this; var passabilityMap = rawState.passabilityMap; this.width = passabilityMap.width; this.height = passabilityMap.height; // the first two won't change, the third is a reference to a value updated by C++ this.obstructionMaskLand = rawState.passabilityClasses["default"]; this.obstructionMaskWater = rawState.passabilityClasses["ship"]; this.obstructionMask = rawState.passabilityClasses["pathfinderObstruction"]; var obstructionTiles = new Uint8Array(passabilityMap.data.length); /* Generated map legend: 0 is impassable 200 is deep water (ie non-passable by land units) 201 is shallow water (passable by land units and water units) 255 is land (or extremely shallow water where ships can't go). 40 is "tree". The following 41-49 range is "near a tree", with the second number showing how many trees this tile neighbors. 30 is "geological component", such as a mine */ for (var i = 0; i < passabilityMap.data.length; ++i) { // If impassable for land units, set to 0, else to 255. obstructionTiles[i] = (passabilityMap.data[i] & this.obstructionMaskLand) ? 0 : 255; if (!(passabilityMap.data[i] & this.obstructionMaskWater) && obstructionTiles[i] === 0) obstructionTiles[i] = 200; // if navigable and not walkable (ie basic water), set to 200. else if (!(passabilityMap.data[i] & this.obstructionMaskWater) && obstructionTiles[i] === 255) obstructionTiles[i] = 201; // navigable and walkable. } var square = [ [-1,-1], [-1,0], [-1, 1], [0,1], [1,1], [1,0], [1,-1], [0,-1], [0,0] ]; var xx = 0; var yy = 0; var value = 0; var pos = []; var x = 0; var y = 0; var radius = 0; for (var entI in sharedScript._entities) { var ent = sharedScript._entities[entI]; if (ent.hasClass("ForestPlant") === true) { pos = this.gamePosToMapPos(ent.position()); x = pos[0]; y = pos[1]; // unless it's impassable already, mark it as 40. if (obstructionTiles[x + y*this.width] !== 0) obstructionTiles[x + y*this.width] = 40; for (var i in square) { xx = square[i][0]; yy = square[i][1]; if (x+i[0] >= 0 && x+xx < this.width && y+yy >= 0 && y+yy < this.height) { value = obstructionTiles[(x+xx) + (y+yy)*this.width]; if (value === 255) obstructionTiles[(x+xx) + (y+yy)*this.width] = 41; else if (value < 49 && value > 40) obstructionTiles[(x+xx) + (y+yy)*this.width] = value + 1; } } } else if (ent.hasClass("Geology") === true) { radius = Math.floor(ent.obstructionRadius() / 4); pos = this.gamePosToMapPos(ent.position()); x = pos[0]; y = pos[1]; // Unless it's impassable, mark as 30. This takes precedence over trees. obstructionTiles[x + y*this.width] = obstructionTiles[x + y*this.width] === 0 ? 0 : 30; for (var xx = -radius; xx <= radius;xx++) for (var yy = -radius; yy <= radius;yy++) if (x+xx >= 0 && x+xx < this.width && y+yy >= 0 && y+yy < this.height) obstructionTiles[(x+xx) + (y+yy)*this.width] = obstructionTiles[(x+xx) + (y+yy)*this.width] === 0 ? 0 : 30; } } // Okay now we have a pretty good knowledge of the map. this.Map(rawState, obstructionTiles); this.obstructionMaskLand = null; this.obstructionMaskWater = null; this.obstructionMask = null; delete this.obstructionMaskLand; delete this.obstructionMaskWater; delete this.obstructionMask; }; // Returns the (approximately) closest point which is passable by searching in a spiral pattern -TerrainAnalysis.prototype.findClosestPassablePoint = function(startPoint, onLand, limitDistance, quickscope){ +m.TerrainAnalysis.prototype.findClosestPassablePoint = function(startPoint, onLand, limitDistance, quickscope){ var w = this.width; var p = startPoint; var direction = 1; if (p[0] + w*p[1] < 0 || p[0] + w*p[1] >= this.length) { return undefined; } // quickscope if (this.map[p[0] + w*p[1]] === 255) { if (this.countConnected(p[0] + w*p[1], onLand) >= 2) { return p; } } var count = 0; // search in a spiral pattern. We require a value that is actually accessible in this case, ie 255, 201 or 41 if land, 200/201 if water. for (var i = 1; i < w; i++){ for (var j = 0; j < 2; j++){ for (var k = 0; k < i; k++){ p[j] += direction; // if the value is not markedly inaccessible var index = p[0] + w*p[1]; if (this.map[index] !== 0 && this.map[index] !== 90 && this.map[index] !== 120 && this.map[index] !== 30 && this.map[index] !== 40){ if (quickscope || this.countConnected(index, onLand) >= 2){ return p; } } if (limitDistance !== undefined && count > limitDistance){ return undefined; } count++; } } direction *= -1; } return undefined; }; // Returns an estimate of a tile accessibility. It checks neighboring cells over two levels. // returns a count. It's not integer. About 2 should be fairly accessible already. -TerrainAnalysis.prototype.countConnected = function(startIndex, byLand){ +m.TerrainAnalysis.prototype.countConnected = function(startIndex, byLand){ var count = 0.0; var w = this.width; var positions = [[0,1], [0,-1], [1,0], [-1,0], [1,1], [-1,-1], [1,-1], [-1,1], [0,2], [0,-2], [2,0], [-2,0], [2,2], [-2,-2], [2,-2], [-2,2]/*, [1,2], [1,-2], [2,1], [-2,1], [-1,2], [-1,-2], [2,-1], [-2,-1]*/]; for (var i in positions) { var index = startIndex + positions[i][0] + positions[i][1]*w; if (this.map[index] !== 0) { if (byLand) { if (this.map[index] === 201) count++; else if (this.map[index] === 255) count++; else if (this.map[index] === 41) count++; else if (this.map[index] === 42) count += 0.5; else if (this.map[index] === 43) count += 0.3; else if (this.map[index] === 44) count += 0.13; else if (this.map[index] === 45) count += 0.08; else if (this.map[index] === 46) count += 0.05; else if (this.map[index] === 47) count += 0.03; } else { if (this.map[index] === 201) count++; if (this.map[index] === 200) count++; } } } return count; }; // TODO: for now this resets to 255. -TerrainAnalysis.prototype.updateMapWithEvents = function(sharedAI) { +m.TerrainAnalysis.prototype.updateMapWithEvents = function(sharedAI) { var self = this; var events = sharedAI.events; var passabilityMap = sharedAI.passabilityMap; // looking for creation or destruction of entities, and updates the map accordingly. for (var i in events) { var e = events[i]; if (e.type === "Destroy") { if (e.msg.entityObj){ var ent = e.msg.entityObj; if (ent.hasClass("Geology")) { var x = self.gamePosToMapPos(ent.position())[0]; var y = self.gamePosToMapPos(ent.position())[1]; // remove it. Don't really care about surrounding and possible overlappings. var radius = Math.floor(ent.obstructionRadius() / self.cellSize); for (var xx = -radius; xx <= radius;xx++) for (var yy = -radius; yy <= radius;yy++) { if (x+xx >= 0 && x+xx < self.width && y+yy >= 0 && y+yy < self.height && this.map[(x+xx) + (y+yy)*self.width] === 30) { this.map[(x+xx) + (y+yy)*self.width] = 255; } } } else if (ent.hasClass("ForestPlant")){ var x = self.gamePosToMapPos(ent.position())[0]; var y = self.gamePosToMapPos(ent.position())[1]; var nbOfNeigh = 0; for (var xx = -1; xx <= 1;xx++) for (var yy = -1; yy <= 1;yy++) { if (xx == 0 && yy == 0) continue; if (this.map[(x+xx) + (y+yy)*self.width] === 40) nbOfNeigh++; else if (this.map[(x+xx) + (y+yy)*self.width] === 41) { this.map[(x+xx) + (y+yy)*self.width] = 255; } else if (this.map[(x+xx) + (y+yy)*self.width] > 41 && this.map[(x+xx) + (y+yy)*self.width] < 50) this.map[(x+xx) + (y+yy)*self.width] = this.map[(x+xx) + (y+yy)*self.width] - 1; } if (nbOfNeigh > 0) this.map[x + y*self.width] = this.map[x + y*self.width] = 40 + nbOfNeigh; else this.map[x + y*self.width] = this.map[x + y*self.width] = 255; } } } } } /* * Accessibility inherits from TerrainAnalysis * * This can easily and efficiently determine if any two points are connected. * it can also determine if any point is "probably" reachable, assuming the unit can get close enough * for optimizations it's called after the TerrainAnalyser has finished initializing his map * so this can use the land regions already. */ -function Accessibility() { +m.Accessibility = function() { } -copyPrototype(Accessibility, TerrainAnalysis); +m.copyPrototype(m.Accessibility, m.TerrainAnalysis); -Accessibility.prototype.init = function(rawState, terrainAnalyser){ +m.Accessibility.prototype.init = function(rawState, terrainAnalyser){ var self = this; this.Map(rawState, terrainAnalyser.map); this.landPassMap = new Uint8Array(terrainAnalyser.length); this.navalPassMap = new Uint8Array(terrainAnalyser.length); this.regionSize = []; this.regionType = []; // "inaccessible", "land" or "water"; // ID of the region associated with an array of region IDs. this.regionLinks = []; // initialized to 0, it's more optimized to start at 1 (I'm checking that if it's not 0, then it's already aprt of a region, don't touch); // However I actually store all unpassable as region 1 (because if I don't, on some maps the toal nb of region is over 256, and it crashes as the mapis 8bit.) // So start at 2. this.regionID = 2; for (var i = 0; i < this.landPassMap.length; ++i) { if (this.map[i] !== 0) { // any non-painted, non-inacessible area. if (this.landPassMap[i] === 0 && this.floodFill(i,this.regionID,false)) this.regionType[this.regionID++] = "land"; if (this.navalPassMap[i] === 0 && this.floodFill(i,this.regionID,true)) this.regionType[this.regionID++] = "water"; } else if (this.landPassMap[i] === 0) { // any non-painted, inacessible area. this.floodFill(i,1,false); this.floodFill(i,1,true); } } // calculating region links. Regions only touching diagonaly are not linked. // since we're checking all of them, we'll check from the top left to the bottom right var w = this.width; for (var x = 0; x < this.width-1; ++x) { for (var y = 0; y < this.height-1; ++y) { // checking right. var thisLID = this.landPassMap[x+y*w]; var thisNID = this.navalPassMap[x+y*w]; var rightLID = this.landPassMap[x+1+y*w]; var rightNID = this.navalPassMap[x+1+y*w]; var bottomLID = this.landPassMap[x+y*w+w]; var bottomNID = this.navalPassMap[x+y*w+w]; if (thisLID > 1) { if (rightNID > 1) if (this.regionLinks[thisLID].indexOf(rightNID) === -1) this.regionLinks[thisLID].push(rightNID); if (bottomNID > 1) if (this.regionLinks[thisLID].indexOf(bottomNID) === -1) this.regionLinks[thisLID].push(bottomNID); } if (thisNID > 1) { if (rightLID > 1) if (this.regionLinks[thisNID].indexOf(rightLID) === -1) this.regionLinks[thisNID].push(rightLID); if (bottomLID > 1) if (this.regionLinks[thisNID].indexOf(bottomLID) === -1) this.regionLinks[thisNID].push(bottomLID); if (thisLID > 1) if (this.regionLinks[thisNID].indexOf(thisLID) === -1) this.regionLinks[thisNID].push(thisLID); } } } //warn(uneval(this.regionLinks)); //Engine.DumpImage("LandPassMap.png", this.landPassMap, this.width, this.height, 255); //Engine.DumpImage("NavalPassMap.png", this.navalPassMap, this.width, this.height, 255); } -Accessibility.prototype.getAccessValue = function(position, onWater) { +m.Accessibility.prototype.getAccessValue = function(position, onWater) { var gamePos = this.gamePosToMapPos(position); if (onWater === true) return this.navalPassMap[gamePos[0] + this.width*gamePos[1]]; var ret = this.landPassMap[gamePos[0] + this.width*gamePos[1]]; if (ret === 1) { // quick spiral search. var indx = [ [-1,-1],[-1,0],[-1,1],[0,1],[1,1],[1,0],[1,-1],[0,-1]] for (i in indx) { ret = this.landPassMap[gamePos[0]+indx[0] + this.width*(gamePos[1]+indx[0])] if (ret !== undefined && ret !== 1) return ret; } } return ret; }; // Returns true if a point is deemed currently accessible (is not blocked by surrounding trees...) // NB: accessible means that you can reach it from one side, not necessariliy that you can go ON it. -Accessibility.prototype.isAccessible = function(gameState, position, onLand){ +m.Accessibility.prototype.isAccessible = function(gameState, position, onLand){ var gamePos = this.gamePosToMapPos(position); // quick check if (this.countConnected(gamePos[0] + this.width*gamePos[1], onLand) >= 2) { return true; } return false; }; // Return true if you can go from a point to a point without switching means of transport // Hardcore means is also checks for isAccessible at the end (it checks for either water or land though, beware). // This is a blind check and not a pathfinder: for all it knows there is a huge block of trees in the middle. -Accessibility.prototype.pathAvailable = function(gameState, start, end, onWater, hardcore){ +m.Accessibility.prototype.pathAvailable = function(gameState, start, end, onWater, hardcore){ var pstart = this.gamePosToMapPos(start); var istart = pstart[0] + pstart[1]*this.width; var pend = this.gamePosToMapPos(end); var iend = pend[0] + pend[1]*this.width; if (onWater) { if (this.navalPassMap[istart] === this.navalPassMap[iend]) { if (hardcore && this.isAccessible(gameState, end,false)) return true; else if (hardcore) return false; return true; } } else { if (this.landPassMap[istart] === this.landPassMap[iend]) { if (hardcore && this.isAccessible(gameState, end,true)) return true; else if (hardcore) return false; return true; } } return false; }; -Accessibility.prototype.getTrajectTo = function(start, end, noBound) { +m.Accessibility.prototype.getTrajectTo = function(start, end, noBound) { var pstart = this.gamePosToMapPos(start); var istart = pstart[0] + pstart[1]*this.width; var pend = this.gamePosToMapPos(end); var iend = pend[0] + pend[1]*this.width; var onLand = true; if (this.landPassMap[istart] <= 1 && this.navalPassMap[istart] > 1) onLand = false; if (this.landPassMap[istart] <= 1 && this.navalPassMap[istart] <= 1) return false; var endRegion = this.landPassMap[iend]; if (endRegion <= 1 && this.navalPassMap[iend] > 1) endRegion = this.navalPassMap[iend]; else if (endRegion <= 1) return false; if (onLand) var startRegion = this.landPassMap[istart]; else var startRegion = this.navalPassMap[istart]; return this.getTrajectToIndex(startRegion, endRegion, noBound); } // Return a "path" of accessibility indexes from one point to another, including the start and the end indexes (unless specified otherwise) // this can tell you what sea zone you need to have a dock on, for example. // assumes a land unit unless start point is over deep water. // if the path is more complicated than "land->sea->land" (or "sea->land->sea"), it will run A* to try and figure it out // Thus it can handle arbitrarily complicated paths (theoretically). -Accessibility.prototype.getTrajectToIndex = function(istart, iend, noBound){ +m.Accessibility.prototype.getTrajectToIndex = function(istart, iend, noBound){ var startRegion = istart; var currentRegion = istart; var endRegion = iend; // optimizations to avoid needless memory usage // if it's the same, return the path if (startRegion === endRegion) return [startRegion]; else if (this.regionLinks[startRegion].indexOf(endRegion) !== -1) return [startRegion, endRegion]; else { var rgs = this.regionLinks[startRegion]; for (p in rgs) { if (this.regionLinks[rgs[p]].indexOf(endRegion) !== -1) return [startRegion, rgs[p], endRegion]; } } // it appears to be difficult. // computing A* over a graph with all nodes equally good (might want to change this sometimes), currently it returns the shortest path switch-wise. this.openList = []; this.parentSquare = new Uint8Array(this.regionSize.length); this.isOpened = new Boolean(this.regionSize.length); this.gCostArray = new Uint8Array(this.regionSize.length); this.isOpened[currentRegion] = true; this.openList.push(currentRegion); this.gCostArray[currentRegion] = 0; this.parentSquare[currentRegion] = currentRegion; var w = this.width; var h = this.height; //creation of variables used in the loop var found = false; // on to A* while (found === false && this.openList.length !== 0) { var currentDist = 300; var ti = 0; for (var i in this.openList) { var sum = this.gCostArray[this.openList[i]]; if (sum < currentDist) { ti = i; currentRegion = this.openList[i]; currentDist = sum; } } this.openList.splice(ti,1); this.isOpened[currentRegion] = false; // special case, might make it faster (usually oceans connect multiple land masses, sometimes all of them) if (this.regionType[currentRegion] == "water" && endLand) { var idx = this.regionLinks[currentRegion].indexOf(endRegion); if (idx !== -1) { this.parentSquare[endRegion] = currentRegion; this.gCostArray[endRegion] = this.gCostArray[currentRegion] + 1; found = true; break; } } for (var i in this.regionLinks[currentRegion]) { var region = this.regionLinks[currentRegion][i]; if(this.isOpened[region] === undefined) { this.parentSquare[region] = currentRegion; this.gCostArray[region] = this.gCostArray[currentRegion] + 1; this.openList.push(region); this.isOpened[region] = true; if (region === endRegion) { found = true; break; } } else { if (this.gCostArray[region] > 1 + this.gCostArray[currentRegion]) { this.parentSquare[region] = currentRegion; this.gCostArray[region] = 1 + this.gCostArray[currentRegion]; } } } } var path = []; if (found) { currentRegion = endRegion; if (!noBound) path.push(currentRegion); while (this.parentSquare[currentRegion] !== startRegion) { currentRegion = this.parentSquare[currentRegion]; path.push(currentRegion); } if (!noBound) path.push(startRegion); } else { delete this.parentSquare; delete this.isOpened; delete this.gCostArray; return false; } delete this.parentSquare; delete this.isOpened; delete this.gCostArray; return path; }; -Accessibility.prototype.getRegionSize = function(position, onWater){ +m.Accessibility.prototype.getRegionSize = function(position, onWater){ var pos = this.gamePosToMapPos(position); var index = pos[0] + pos[1]*this.width; var ID = (onWater === true) ? this.navalPassMap[index] : this.landPassMap[index]; if (this.regionSize[ID] === undefined) return 0; return this.regionSize[ID]; }; -Accessibility.prototype.getRegionSizei = function(index, onWater) { +m.Accessibility.prototype.getRegionSizei = function(index, onWater) { if (this.regionSize[this.landPassMap[index]] === undefined && (!onWater || this.regionSize[this.navalPassMap[index]] === undefined)) return 0; if (onWater && this.regionSize[this.navalPassMap[index]] > this.regionSize[this.landPassMap[index]]) return this.regionSize[this.navalPassMap[index]]; return this.regionSize[this.landPassMap[index]]; }; // Implementation of a fast flood fill. Reasonably good performances for JS. // TODO: take big zones of impassable trees into account? -Accessibility.prototype.floodFill = function(startIndex, value, onWater) +m.Accessibility.prototype.floodFill = function(startIndex, value, onWater) { this.s = startIndex; if ((!onWater && this.landPassMap[this.s] !== 0) || (onWater && this.navalPassMap[this.s] !== 0) ) { return false; // already painted. } this.floodFor = "land"; if (this.map[this.s] === 0) { this.landPassMap[this.s] = 1; this.navalPassMap[this.s] = 1; return false; } if (onWater === true) { if (this.map[this.s] !== 200 && this.map[this.s] !== 201) { this.navalPassMap[this.s] = 1; // impassable for naval return false; // do nothing } this.floodFor = "water"; } else if (this.map[this.s] === 200) { this.landPassMap[this.s] = 1; // impassable for land return false; } // here we'll be able to start. for (var i = this.regionSize.length; i <= value; ++i) { this.regionLinks.push([]); this.regionSize.push(0); this.regionType.push("inaccessible"); } var w = this.width; var h = this.height; var x = 0; var y = 0; // Get x and y from index var IndexArray = [this.s]; var newIndex = 0; while(IndexArray.length){ newIndex = IndexArray.pop(); y = 0; var loop = false; // vertical iteration do { --y; loop = false; var index = +newIndex + w*y; if (index < 0) break; if (this.floodFor === "land" && this.landPassMap[index] === 0 && this.map[index] !== 0 && this.map[index] !== 200) { loop = true; } else if (this.floodFor === "water" && this.navalPassMap[index] === 0 && (this.map[index] === 200 || this.map[index] === 201)) { loop = true; } else { break; } } while (loop === true) // should actually break ++y; var reachLeft = false; var reachRight = false; loop = true; do { var index = +newIndex + w*y; if (this.floodFor === "land" && this.landPassMap[index] === 0 && this.map[index] !== 0 && this.map[index] !== 200) { this.landPassMap[index] = value; this.regionSize[value]++; } else if (this.floodFor === "water" && this.navalPassMap[index] === 0 && (this.map[index] === 200 || this.map[index] === 201)) { this.navalPassMap[index] = value; this.regionSize[value]++; } else { break; } if (index%w > 0) { if (this.floodFor === "land" && this.landPassMap[index -1] === 0 && this.map[index -1] !== 0 && this.map[index -1] !== 200) { if(!reachLeft) { IndexArray.push(index -1); reachLeft = true; } } else if (this.floodFor === "water" && this.navalPassMap[index -1] === 0 && (this.map[index -1] === 200 || this.map[index -1] === 201)) { if(!reachLeft) { IndexArray.push(index -1); reachLeft = true; } } else if(reachLeft) { reachLeft = false; } } if (index%w < w - 1) { if (this.floodFor === "land" && this.landPassMap[index +1] === 0 && this.map[index +1] !== 0 && this.map[index +1] !== 200) { if(!reachRight) { IndexArray.push(index +1); reachRight = true; } } else if (this.floodFor === "water" && this.navalPassMap[index +1] === 0 && (this.map[index +1] === 200 || this.map[index +1] === 201)) { if(!reachRight) { IndexArray.push(index +1); reachRight = true; } } else if(reachRight) { reachRight = false; } } ++y; } while (index/w < w-1) // should actually break } return true; } // creates a map of resource density -SharedScript.prototype.createResourceMaps = function(sharedScript) { +m.SharedScript.prototype.createResourceMaps = function(sharedScript) { for (var resource in this.decreaseFactor){ // if there is no resourceMap create one with an influence for everything with that resource if (! this.resourceMaps[resource]){ // We're creting them 8-bit. Things could go above 255 if there are really tons of resources // But at that point the precision is not really important anyway. And it saves memory. - this.resourceMaps[resource] = new Map(sharedScript, new Uint8Array(sharedScript.passabilityMap.data.length)); + this.resourceMaps[resource] = new m.Map(sharedScript, new Uint8Array(sharedScript.passabilityMap.data.length)); this.resourceMaps[resource].setMaxVal(255); - this.CCResourceMaps[resource] = new Map(sharedScript, new Uint8Array(sharedScript.passabilityMap.data.length)); + this.CCResourceMaps[resource] = new m.Map(sharedScript, new Uint8Array(sharedScript.passabilityMap.data.length)); this.CCResourceMaps[resource].setMaxVal(255); } } for (var entI in sharedScript._entities) { var ent = sharedScript._entities[entI]; if (ent && ent.position() && ent.resourceSupplyType() && ent.resourceSupplyType().generic !== "treasure") { var resource = ent.resourceSupplyType().generic; var x = Math.floor(ent.position()[0] / 4); var z = Math.floor(ent.position()[1] / 4); var strength = Math.floor(ent.resourceSupplyMax()/this.decreaseFactor[resource]); if (resource === "wood" || resource === "food") { this.CCResourceMaps[resource].addInfluence(x, z, 15, strength/2.0,'constant'); this.resourceMaps[resource].addInfluence(x, z, 9.0, strength,'constant'); this.resourceMaps[resource].addInfluence(x, z, 2, -5,'constant'); } else if (resource === "stone" || resource === "metal") { this.CCResourceMaps[resource].addInfluence(x, z, 30, strength,'constant'); this.resourceMaps[resource].addInfluence(x, z, 12.0, strength/1.5); this.resourceMaps[resource].addInfluence(x, z, 12.0, strength/2.0,'constant'); this.resourceMaps[resource].addInfluence(x, z, 8, -50); } } } }; // TODO: make it regularly update stone+metal mines and their resource levels. // creates and maintains a map of unused resource density // this also takes dropsites into account. // resources that are "part" of a dropsite are not counted. -SharedScript.prototype.updateResourceMaps = function(sharedScript, events) { +m.SharedScript.prototype.updateResourceMaps = function(sharedScript, events) { for (var resource in this.decreaseFactor){ // if there is no resourceMap create one with an influence for everything with that resource if (! this.resourceMaps[resource]){ // We're creting them 8-bit. Things could go above 255 if there are really tons of resources // But at that point the precision is not really important anyway. And it saves memory. - this.resourceMaps[resource] = new Map(sharedScript, new Uint8Array(sharedScript.passabilityMap.data.length)); + this.resourceMaps[resource] = new m.Map(sharedScript, new Uint8Array(sharedScript.passabilityMap.data.length)); this.resourceMaps[resource].setMaxVal(255); - this.CCResourceMaps[resource] = new Map(sharedScript, new Uint8Array(sharedScript.passabilityMap.data.length)); + this.CCResourceMaps[resource] = new m.Map(sharedScript, new Uint8Array(sharedScript.passabilityMap.data.length)); this.CCResourceMaps[resource].setMaxVal(255); } } // Look for destroy events and subtract the entities original influence from the resourceMap // TODO: perhaps do something when dropsites appear/disappear. for (var key in events) { var e = events[key]; if (e.type === "Destroy") { if (e.msg.entityObj){ var ent = e.msg.entityObj; if (ent && ent.position() && ent.resourceSupplyType() && ent.resourceSupplyType().generic !== "treasure") { var resource = ent.resourceSupplyType().generic; var x = Math.floor(ent.position()[0] / 4); var z = Math.floor(ent.position()[1] / 4); var strength = Math.floor(ent.resourceSupplyMax()/this.decreaseFactor[resource]); if (resource === "wood" || resource === "food") { this.resourceMaps[resource].addInfluence(x, z, 2, 5,'constant'); this.resourceMaps[resource].addInfluence(x, z, 9.0, -strength,'constant'); this.CCResourceMaps[resource].addInfluence(x, z, 15, -strength/2.0,'constant'); } else if (resource === "stone" || resource === "metal") { this.resourceMaps[resource].addInfluence(x, z, 8, 50); this.resourceMaps[resource].addInfluence(x, z, 12.0, -strength/1.5); this.resourceMaps[resource].addInfluence(x, z, 12.0, -strength/2.0,'constant'); this.CCResourceMaps[resource].addInfluence(x, z, 30, -strength,'constant'); } } } } else if (e.type === "Create") { if (e.msg.entity){ var ent = sharedScript._entities[e.msg.entity]; if (ent && ent.position() && ent.resourceSupplyType() && ent.resourceSupplyType().generic !== "treasure"){ var resource = ent.resourceSupplyType().generic; var x = Math.floor(ent.position()[0] / 4); var z = Math.floor(ent.position()[1] / 4); var strength = Math.floor(ent.resourceSupplyMax()/this.decreaseFactor[resource]); if (resource === "wood" || resource === "food") { this.CCResourceMaps[resource].addInfluence(x, z, 15, strength/2.0,'constant'); this.resourceMaps[resource].addInfluence(x, z, 9.0, strength,'constant'); this.resourceMaps[resource].addInfluence(x, z, 2, -5,'constant'); } else if (resource === "stone" || resource === "metal") { this.CCResourceMaps[resource].addInfluence(x, z, 30, strength,'constant'); this.resourceMaps[resource].addInfluence(x, z, 12.0, strength/1.5); this.resourceMaps[resource].addInfluence(x, z, 12.0, strength/2.0,'constant'); this.resourceMaps[resource].addInfluence(x, z, 8, -50); } } } } } }; + +return m; + +}(API3); Index: ps/trunk/binaries/data/mods/public/simulation/ai/qbot/config.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/qbot/config.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/qbot/config.js (revision 14441) @@ -1,63 +1,58 @@ -var baseConfig = { - "attack" : { +function Config() { + this.debug = false + + this.attack = { "minAttackSize" : 20, // attackMoveToLocation "maxAttackSize" : 60, // attackMoveToLocation "enemyRatio" : 1.5, // attackMoveToLocation "groupSize" : 10 // military - }, + }; // defence - "defence" : { + this.defence = { "acquireDistance" : 220, "releaseDistance" : 250, "groupRadius" : 20, "groupBreakRadius" : 40, "groupMergeRadius" : 10, "defenderRatio" : 2 - }, + }; // military - "buildings" : { + this.buildings = { "moderate" : { "default" : [ "structures/{civ}_barracks" ] }, "advanced" : { "hele" : [ "structures/{civ}_gymnasion", "structures/{civ}_fortress" ], "athen" : [ "structures/{civ}_gymnasion", "structures/{civ}_fortress" ], "spart" : [ "structures/{civ}_syssiton", "structures/{civ}_fortress" ], "mace" : [ "structures/{civ}_fortress" ], "cart" : [ "structures/{civ}_fortress", "structures/{civ}_embassy_celtic", "structures/{civ}_embassy_iberian", "structures/{civ}_embassy_italiote" ], "celt" : [ "structures/{civ}_kennel", "structures/{civ}_fortress_b", "structures/{civ}_fortress_g" ], "iber" : [ "structures/{civ}_fortress" ], "pers" : [ "structures/{civ}_fortress", "structures/{civ}_stables", "structures/{civ}_apadana" ], "rome" : [ "structures/{civ}_army_camp", "structures/{civ}_fortress" ] }, "fort" : { "default" : [ "structures/{civ}_fortress" ], "celt" : [ "structures/{civ}_fortress_b", "structures/{civ}_fortress_g" ] } - }, + }; // qbot - "priorities" : { // Note these are dynamic, you are only setting the initial values + this.priorities = { // Note these are dynamic, you are only setting the initial values "house" : 500, "citizenSoldier" : 100, "villager" : 100, "economicBuilding" : 30, "field" : 20, "advancedSoldier" : 30, "siege" : 10, "militaryBuilding" : 50, "defenceBuilding" : 17, "civilCentre" : 1000 - }, - - "debug" : false -}; - -var Config = { - "debug": false + }; }; -Config.__proto__ = baseConfig; \ No newline at end of file Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/technology.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/technology.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/technology.js (revision 14441) @@ -1,138 +1,145 @@ +var API3 = function(m) +{ + // Wrapper around a technology template -function Technology(allTemplates, templateName) +m.Technology = function(allTemplates, templateName) { this._templateName = templateName; var template = allTemplates[templateName]; // check if this is one of two paired technologies. this._isPair = template.pair === undefined ? false : true; if (this._isPair) { if (allTemplates[template.pair].top == templateName) this._pairedWith = allTemplates[template.pair].bottom; else this._pairedWith = allTemplates[template.pair].top; } // check if it only defines a pair: this._definesPair = template.top === undefined ? false : true; this._template = template; this._techTemplates = allTemplates; } // returns generic, or specific if civ provided. -Technology.prototype.name = function(civ) +m.Technology.prototype.name = function(civ) { if (civ === undefined) { return this._template.genericName; } else { if (this._template.specificName === undefined || this._template.specificName[civ] === undefined) return undefined; return this._template.specificName[civ]; } }; -Technology.prototype.pairDef = function() +m.Technology.prototype.pairDef = function() { return this._definesPair; }; // in case this defines a pair only, returns the two paired technologies. -Technology.prototype.getPairedTechs = function() +m.Technology.prototype.getPairedTechs = function() { if (!this._definesPair) return undefined; - var techOne = new Technology(this._techTemplates, this._template.top); - var techTwo = new Technology(this._techTemplates, this._template.bottom); + var techOne = new m.Technology(this._techTemplates, this._template.top); + var techTwo = new m.Technology(this._techTemplates, this._template.bottom); return [techOne,techTwo]; }; -Technology.prototype.pair = function() +m.Technology.prototype.pair = function() { if (!this._isPair) return undefined; return this._template.pair; }; -Technology.prototype.pairedWith = function() +m.Technology.prototype.pairedWith = function() { if (!this._isPair) return undefined; return this._pairedWith; }; -Technology.prototype.cost = function() +m.Technology.prototype.cost = function() { if (!this._template.cost) return undefined; return this._template.cost; }; // seconds -Technology.prototype.researchTime = function() +m.Technology.prototype.researchTime = function() { if (!this._template.researchTime) return undefined; return this._template.researchTime; }; -Technology.prototype.requirements = function() +m.Technology.prototype.requirements = function() { if (!this._template.requirements) return undefined; return this._template.requirements; }; -Technology.prototype.autoResearch = function() +m.Technology.prototype.autoResearch = function() { if (!this._template.autoResearch) return undefined; return this._template.autoResearch; }; -Technology.prototype.supersedes = function() +m.Technology.prototype.supersedes = function() { if (!this._template.supersedes) return undefined; return this._template.supersedes; }; -Technology.prototype.modifications = function() +m.Technology.prototype.modifications = function() { if (!this._template.modifications) return undefined; return this._template.modifications; }; -Technology.prototype.affects = function() +m.Technology.prototype.affects = function() { if (!this._template.affects) return undefined; return this._template.affects; }; -Technology.prototype.isAffected = function(classes) +m.Technology.prototype.isAffected = function(classes) { if (!this._template.affects) return false; for (var index in this._template.affects) { var reqClasses = this._template.affects[index].split(" "); var fitting = true; for (var i in reqClasses) { if (classes.indexOf(reqClasses[i]) === -1) { fitting = false; break; } } if (fitting === true) return true; } return false; }; + +return m; + +}(API3); Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/utils.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/utils.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/utils.js (revision 14441) @@ -1,77 +1,84 @@ -function VectorDistance(a, b) +var API3 = function(m) +{ + +m.VectorDistance = function(a, b) { var dx = a[0] - b[0]; var dz = a[1] - b[1]; return Math.sqrt(dx*dx + dz*dz); } -function SquareVectorDistance(a, b) +m.SquareVectorDistance = function(a, b) { var dx = a[0] - b[0]; var dz = a[1] - b[1]; return (dx*dx + dz*dz); } // A is the reference, B must be in "range" of A // this supposes the range is already squared -function inRange(a, b, range)// checks for X distance +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. -function ManhattanDistance(a, b) +m.ManhattanDistance = function(a, b) { var dx = a[0] - b[0]; var dz = a[1] - b[1]; return Math.abs(dx) + Math.abs(dz); } -function AssocArraytoArray(assocArray) { +m.AssocArraytoArray = function(assocArray) { var endArray = []; for (var i in assocArray) endArray.push(assocArray[i]); return endArray; }; -function MemoizeInit(obj) +m.MemoizeInit = function(obj) { obj._memoizeCache = {}; } -function Memoize(funcname, func) +m.Memoize = function(funcname, func) { return function() { var args = funcname + '|' + Array.prototype.join.call(arguments, '|'); if (args in this._memoizeCache) return this._memoizeCache[args]; var ret = func.apply(this, arguments); this._memoizeCache[args] = ret; return ret; }; } -function ShallowClone(obj) +m.ShallowClone = function(obj) { var ret = {}; for (var k in obj) ret[k] = obj[k]; return ret; } // Picks a random element from an array -function PickRandom(list){ +m.PickRandom = function(list){ if (list.length === 0) { return undefined; } else { return list[Math.floor(Math.random()*list.length)]; } } + +return m; + +}(API3); Index: ps/trunk/binaries/data/mods/public/simulation/ai/qbot/defence.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/qbot/defence.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/qbot/defence.js (revision 14441) @@ -1,278 +1,278 @@ -function Defence(){ +function Defence(Config){ this.ACQUIRE_DIST = Config.defence.acquireDistance; this.RELEASE_DIST = Config.defence.releaseDistance; this.GROUP_RADIUS = Config.defence.groupRadius; // units will be added to a group if they are within this radius this.GROUP_BREAK_RADIUS = Config.defence.groupBreakRadius; // units will leave a group if they are outside of this radius this.GROUP_MERGE_RADIUS = Config.defence.groupMergeRadius; // Two groups with centres this far apart will be merged this.DEFENCE_RATIO = Config.defence.defenderRatio; // How many defenders we want per attacker. Need to balance fewer losses vs. lost economy // These are objects with the keys being entity ids and values being the entity objects // NOTE: It is assumed that all attackers have a valid position, the attackers list must be kept up to date so this // property is maintained this.attackers = {}; // Enemy soldiers which are attacking our base this.defenders = {}; // Our soldiers currently being used for defence // A list of groups, enemy soldiers are clumped together in groups. this.groups = []; } Defence.prototype.update = function(gameState, events, militaryManager){ Engine.ProfileStart("Defence Manager"); var enemyTroops = militaryManager.getEnemySoldiers(); this.updateAttackers(gameState, events, enemyTroops); this.updateGroups(); var unassignedDefenders = this.updateDefenders(gameState); this.assignDefenders(gameState, militaryManager, unassignedDefenders); Engine.ProfileStop(); }; Defence.prototype.assignDefenders = function(gameState, militaryManager, unassignedDefenders){ var numAttackers = Object.keys(this.attackers).length; var numDefenders = Object.keys(this.defenders).length; var numUnassignedDefenders = unassignedDefenders.length; var numAssignedDefenders = numDefenders - numUnassignedDefenders; // TODO: this is non optimal, we may have unevenly distributed defenders // Unassign defenders which aren't needed if (numAttackers * this.DEFENCE_RATIO <= numAssignedDefenders){ militaryManager.unassignUnits(unassignedDefenders); var CCs = gameState.getOwnEntities().filter(Filters.byClass("CivCentre")); for (var i in unassignedDefenders){ var pos = this.defenders[unassignedDefenders[i]].position(); // Move back to nearest CC if (pos){ var nearestCCArray = CCs.filterNearest(pos, 1).toEntityArray(); if (nearestCCArray.length > 0){ var movePos = nearestCCArray[0].position(); this.defenders[unassignedDefenders[i]].move(movePos[0], movePos[1]); } } delete this.defenders[unassignedDefenders[i]]; } return; } // Check to see if we need to recruit more defenders if (numAttackers * this.DEFENCE_RATIO > numDefenders){ var numNeeded = Math.ceil(numAttackers * this.DEFENCE_RATIO - numDefenders); var numIdleAvailable = militaryManager.countAvailableUnits(Filters.isIdle()); if (numIdleAvailable > numNeeded){ var newUnits = militaryManager.getAvailableUnits(numNeeded, Filters.isIdle()); for (var i in newUnits){ var ent = gameState.getEntityById(newUnits[i]); } unassignedDefenders = unassignedDefenders.concat(newUnits); }else{ var newUnits = militaryManager.getAvailableUnits(numNeeded); for (var i in newUnits){ var ent = gameState.getEntityById(newUnits[i]); ent.setMetadata("initialPosition", ent.position()); } unassignedDefenders = unassignedDefenders.concat(newUnits); } } // Now distribute the unassigned defenders among the attacking groups. for (var i in unassignedDefenders){ var id = unassignedDefenders[i]; var ent = gameState.getEntityById(id); if (!ent.position()){ debug("Defender with no position! (shouldn't happen)"); debug(ent); continue; } var minDist = Math.min(); var closestGroup = undefined; for (var j in this.groups){ var dist = VectorDistance(this.groups[j].position, ent.position()); if (dist < minDist && this.groups[j].members.length * this.DEFENCE_RATIO > this.groups[j].defenders.length){ minDist = dist; closestGroup = this.groups[j]; } } if (closestGroup !== undefined){ var rand = Math.floor(Math.random()*closestGroup.members.length); ent.attack(closestGroup.members[rand]); this.defenders[id] = ent; closestGroup.defenders.push(id); } } }; Defence.prototype.updateDefenders = function(gameState){ var newDefenders = {}; var unassignedDefenders = []; for (var i in this.groups){ this.removeDestroyed(gameState, this.groups[i].defenders); for (var j in this.groups[i].defenders){ var id = this.groups[i].defenders[j]; newDefenders[id] = this.defenders[id]; var ent = gameState.getEntityById(id); // If the defender is idle then set it to attack another member of the group it is targetting if (ent && ent.isIdle()){ var rand = Math.floor(Math.random()*this.groups[i].members.length); ent.attack(this.groups[i].members[rand]); } } } for (var id in this.defenders){ if (!gameState.getEntityById(id)){ delete this.defenders[id]; } else if (!newDefenders[id]){ unassignedDefenders.push(id); } } return unassignedDefenders; }; // Returns an entity collection of key buildings which should be defended. // Currently just returns civ centres Defence.prototype.getKeyBuildings = function(gameState){ return gameState.getOwnEntities().filter(Filters.byClass("CivCentre")); }; /* * This function puts all attacking enemy troops into this.attackers, the list from the turn before is put into * this.oldAttackers, also any new attackers have their id's listed in this.newAttackers. */ Defence.prototype.updateAttackers = function(gameState, events, enemyTroops){ var self = this; var keyBuildings = this.getKeyBuildings(gameState); this.newAttackers = []; this.oldAttackers = this.attackers; this.attackers = {}; enemyTroops.forEach(function(ent){ if (ent.position()){ var minDist = Math.min(); keyBuildings.forEach(function(building){ if (building.position() && VectorDistance(ent.position(), building.position()) < minDist){ minDist = VectorDistance(ent.position(), building.position()); } }); if (self.oldAttackers[ent.id()]){ if (minDist < self.RELEASE_DIST){ self.attackers[ent.id()] = ent; } }else{ if (minDist < self.ACQUIRE_DIST){ self.attackers[ent.id()] = ent; self.newAttackers.push(ent.id()); } } } }); }; Defence.prototype.removeDestroyed = function(gameState, entList){ for (var i = 0; i < entList.length; i++){ if (!gameState.getEntityById(entList[i])){ entList.splice(i, 1); i--; } } }; Defence.prototype.updateGroups = function(){ // clean up groups by removing members and removing empty groups for (var i = 0; i < this.groups.length; i++){ var group = this.groups[i]; // remove members which are no longer attackers for (var j = 0; j < group.members.length; j++){ if (!this.attackers[group.members[j]]){ group.members.splice(j, 1); j--; } } // recalculate centre of group group.sumPosition = [0,0]; for (var j = 0; j < group.members.length; j++){ group.sumPosition[0] += this.attackers[group.members[j]].position()[0]; group.sumPosition[1] += this.attackers[group.members[j]].position()[1]; } group.position[0] = group.sumPosition[0]/group.members.length; group.position[1] = group.sumPosition[1]/group.members.length; // remove members that are too far away for (var j = 0; j < group.members.length; j++){ if ( VectorDistance(this.attackers[group.members[j]].position(), group.position) > this.GROUP_BREAK_RADIUS){ this.newAttackers.push(group.members[j]); group.sumPosition[0] -= this.attackers[group.members[j]].position()[0]; group.sumPosition[1] -= this.attackers[group.members[j]].position()[1]; group.members.splice(j, 1); j--; } } if (group.members.length === 0){ this.groups.splice(i, 1); i--; } group.position[0] = group.sumPosition[0]/group.members.length; group.position[1] = group.sumPosition[1]/group.members.length; } // add ungrouped attackers to groups for (var j in this.newAttackers){ var ent = this.attackers[this.newAttackers[j]]; var foundGroup = false; for (var i in this.groups){ if (VectorDistance(ent.position(), this.groups[i].position) <= this.GROUP_RADIUS){ this.groups[i].members.push(ent.id()); this.groups[i].sumPosition[0] += ent.position()[0]; this.groups[i].sumPosition[1] += ent.position()[1]; this.groups[i].position[0] = this.groups[i].sumPosition[0]/this.groups[i].members.length; this.groups[i].position[1] = this.groups[i].sumPosition[1]/this.groups[i].members.length; foundGroup = true; break; } } if (!foundGroup){ this.groups.push({"members": [ent.id()], "position": [ent.position()[0], ent.position()[1]], "sumPosition": [ent.position()[0], ent.position()[1]], "defenders": []}); } } // merge groups which are close together for (var i = 0; i < this.groups.length; i++){ for (var j = 0; j < this.groups.length; j++){ if (this.groups[i].members.length < this.groups[j].members.length){ if (VectorDistance(this.groups[i].position, this.groups[j].position) < this.GROUP_MERGE_RADIUS){ this.groups[j].members = this.groups[i].members.concat(this.groups[j].members); this.groups[j].defenders = this.groups[i].defenders.concat(this.groups[j].defenders); this.groups[j].sumPosition[0] += this.groups[i].sumPosition[0]; this.groups[j].sumPosition[1] += this.groups[i].sumPosition[1]; this.groups[j].position[0] = this.groups[j].sumPosition[0]/this.groups[j].members.length; this.groups[j].position[1] = this.groups[j].sumPosition[1]/this.groups[j].members.length; this.groups.splice(i, 1); i--; break; } } } } }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/aegis/aegis.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/aegis/aegis.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/aegis/aegis.js (revision 14441) @@ -1,257 +1,271 @@ + +var AEGIS = (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. -var uniqueIDBOPlans = 0; // training/building/research plans -var uniqueIDBases = 1; // base manager ID. Starts at one because "0" means "no base" on the map -var uniqueIDTPlans = 1; // transport plans. starts at 1 because 0 might be used as none. +m.playerGlobals = []; +m.DebugEnabled = false; -function AegisBot(settings) { - BaseAI.call(this, settings); +m.AegisBot = function AegisBot(settings) { + API3.BaseAI.call(this, settings); - Config.updateDifficulty(settings.difficulty); + this.Config = new m.Config(); + + this.Config.updateDifficulty(settings.difficulty); this.turn = 0; this.playedTurn = 0; - - this.priorities = Config.priorities; + + this.priorities = this.Config.priorities; // this.queues can only be modified by the queue manager or things will go awry. this.queues = {}; for (i in this.priorities) - this.queues[i] = new Queue(); + this.queues[i] = new m.Queue(); - this.queueManager = new QueueManager(this.queues, this.priorities); + this.queueManager = new m.QueueManager(this.Config, this.queues, this.priorities); - this.HQ = new HQ(); + this.HQ = new m.HQ(this.Config); this.firstTime = true; this.savedEvents = []; this.defcon = 5; this.defconChangeTime = -10000000; -} +}; -AegisBot.prototype = new BaseAI(); +m.AegisBot.prototype = new API3.BaseAI(); + +m.AegisBot.prototype.CustomInit = function(gameState, sharedScript) { + + 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. -AegisBot.prototype.CustomInit = function(gameState, sharedScript) { - this.HQ.init(gameState,sharedScript.events,this.queues); - debug ("Initialized with the difficulty " + Config.difficulty); + m.debug ("Initialized with the difficulty " + this.Config.difficulty); - var ents = gameState.getEntities().filter(Filters.byOwner(PlayerID)); + var ents = gameState.getEntities().filter(API3.Filters.byOwner(this.player)); var myKeyEntities = ents.filter(function(ent) { return ent.hasClass("CivCentre"); }); if (myKeyEntities.length == 0){ - myKeyEntities = gameState.getEntities().filter(Filters.byOwner(PlayerID)); + myKeyEntities = gameState.getEntities().filter(API3.Filters.byOwner(this.player)); } - var filter = Filters.byClass("CivCentre"); - var enemyKeyEntities = gameState.getEntities().filter(Filters.not(Filters.byOwner(PlayerID))).filter(filter); + var filter = API3.Filters.byClass("CivCentre"); + var enemyKeyEntities = gameState.getEntities().filter(API3.Filters.not(API3.Filters.byOwner(this.player))).filter(filter); if (enemyKeyEntities.length == 0){ - enemyKeyEntities = gameState.getEntities().filter(Filters.not(Filters.byOwner(PlayerID))); + enemyKeyEntities = gameState.getEntities().filter(API3.Filters.not(API3.Filters.byOwner(this.player))); } this.myIndex = this.accessibility.getAccessValue(myKeyEntities.toEntityArray()[0].position()); - this.pathFinder = new aStarPath(gameState, false, true); + 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" + PlayerID + ".png", this.pathFinder.TotorMap.map, this.pathFinder.TotorMap.width,this.pathFinder.TotorMap.height,255); + //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; this.chooseRandomStrategy(); } -AegisBot.prototype.OnUpdate = function(sharedScript) { +m.AegisBot.prototype.OnUpdate = function(sharedScript) { + if (this.gameFinished){ return; } if (this.events.length > 0 && this.turn !== 0){ this.savedEvents = this.savedEvents.concat(this.events); } // Run the update every n turns, offset depending on player ID to balance the load if ((this.turn + this.player) % 8 == 5) { Engine.ProfileStart("Aegis 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) { - debug ("Assuming this is a water map"); + 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. - if (this.gameState.canResearch(townPhase,true) && this.gameState.getTimeElapsed() > (Config.Economy.townPhase*1000) && this.gameState.getPopulation() > 40 + if (this.gameState.canResearch(townPhase,true) && this.gameState.getTimeElapsed() > (this.Config.Economy.townPhase*1000) && this.gameState.getPopulation() > 40 && this.gameState.findResearchers(townPhase,true).length != 0 && this.queues.majorTech.length() === 0 - && this.gameState.getOwnEntities().filter(Filters.byClass("Village")).length > 5) + && this.gameState.getOwnEntities().filter(API3.Filters.byClass("Village")).length > 5) { this.queueManager.pauseQueue("villager", true); this.queueManager.pauseQueue("citizenSoldier", true); this.queueManager.pauseQueue("house", true); - this.queues.majorTech.addItem(new ResearchPlan(this.gameState, townPhase,0,-1,true)); // we rush the town phase. - debug ("Trying to reach town phase"); + this.queues.majorTech.addItem(new m.ResearchPlan(this.gameState, townPhase,0,-1,true)); // we rush the town phase. + m.debug ("Trying to reach town phase"); } - else if (this.gameState.canResearch(cityPhase,true) && this.gameState.getTimeElapsed() > (Config.Economy.cityPhase*1000) + else if (this.gameState.canResearch(cityPhase,true) && this.gameState.getTimeElapsed() > (this.Config.Economy.cityPhase*1000) && this.gameState.getOwnEntitiesByRole("worker").length > 85 && this.gameState.findResearchers(cityPhase, true).length != 0 && this.queues.majorTech.length() === 0) { - debug ("Trying to reach city phase"); - this.queues.majorTech.addItem(new ResearchPlan(this.gameState, cityPhase)); + m.debug ("Trying to reach city phase"); + this.queues.majorTech.addItem(new m.ResearchPlan(this.gameState, cityPhase)); } // defcon cooldown if (this.defcon < 5 && this.gameState.timeSinceDefconChange() > 20000) { this.defcon++; - debug ("updefconing to " +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.length % 29; for (var i = 0; i < n; i++){ Math.random(); } delete this.savedEvents; this.savedEvents = []; Engine.ProfileStop(); } this.turn++; }; -AegisBot.prototype.chooseRandomStrategy = function() +m.AegisBot.prototype.chooseRandomStrategy = function() { // deactivated for now. this.strategy = "normal"; // rarely and if we can assume it's not a water map. - if (!this.pathInfo.needboat && 0)//Math.random() < 0.2 && Config.difficulty == 2) + if (!this.pathInfo.needboat && 0)//Math.random() < 0.2 && this.Config.difficulty == 2) { this.strategy = "rush"; // going to rush. this.HQ.targetNumWorkers = 0; - Config.Economy.townPhase = 480; - Config.Economy.cityPhase = 900; - Config.Economy.farmsteadStartTime = 600; - Config.Economy.femaleRatio = 0; // raise it since we'll want to rush age 2. + this.Config.Economy.townPhase = 480; + this.Config.Economy.cityPhase = 900; + this.Config.Economy.farmsteadStartTime = 600; + this.Config.Economy.femaleRatio = 0; // raise it since we'll want to rush age 2. } }; -/*AegisBot.prototype.Deserialize = function(data, sharedScript) +/*m.AegisBot.prototype.Deserialize = function(data, sharedScript) { }; // Override the default serializer AegisBot.prototype.Serialize = function() { return {}; };*/ -function debug(output){ - if (Config.debug){ +m.debug = function(output){ + if (m.DebugEnabled){ if (typeof output === "string"){ warn(output); }else{ warn(uneval(output)); } } -} +}; -function copyPrototype(descendant, parent) { +m.copyPrototype = function(descendant, parent) { var sConstructor = parent.toString(); var aMatch = sConstructor.match( /\s*function (.*)\(/ ); if ( aMatch != null ) { descendant.prototype[aMatch[1]] = parent; } for (var m in parent.prototype) { descendant.prototype[m] = parent.prototype[m]; } -} +}; + +return m; +}()); Index: ps/trunk/binaries/data/mods/public/simulation/ai/aegis/attack_plan.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/aegis/attack_plan.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/aegis/attack_plan.js (revision 14441) @@ -1,1240 +1,1247 @@ +var AEGIS = 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 rae 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. */ -function CityAttack(gameState, HQ, uniqueID, targetEnemy, type , targetFinder) { +m.CityAttack = function CityAttack(gameState, HQ, Config, uniqueID, targetEnemy, type , targetFinder) { + this.Config = Config; //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. var enemyCount = {}; for (var i = 1; i <=8; i++) enemyCount[i] = 0; gameState.getEntities().forEach(function(ent) { if (gameState.isEntityEnemy(ent) && ent.owner() !== 0) { enemyCount[ent.owner()]++; } }); var max = 0; for (var i in enemyCount) if (enemyCount[i] > max && +i !== PlayerID) { this.targetPlayer = +i; max = enemyCount[i]; } } if (this.targetPlayer === undefined || this.targetPlayer === -1) { this.failed = true; return false; } - var CCs = gameState.getOwnEntities().filter(Filters.byClass("CivCentre")); + var CCs = gameState.getOwnEntities().filter(API3.Filters.byClass("CivCentre")); if (CCs.length === 0) { this.failed = true; return false; } - debug ("Target (" + PlayerID +") = " +this.targetPlayer); + m.debug ("Target (" + PlayerID +") = " +this.targetPlayer); this.targetFinder = targetFinder || this.defaultTargetFinder; this.type = type || "normal"; this.name = uniqueID; this.healthRecord = []; this.timeOfPlanStart = gameState.getTimeElapsed(); // we get the time at which we decided to start the attack this.maxPreparationTime = 210*1000; // in this case we want to have the attack ready by the 13th minute. Countdown. Minimum 2 minutes. - if (type !== "superSized" && Config.difficulty >= 1) + if (type !== "superSized" && this.Config.difficulty >= 1) this.maxPreparationTime = 780000 - gameState.getTimeElapsed() < 120000 ? 120000 : 780000 - gameState.getTimeElapsed(); this.pausingStart = 0; this.totalPausingTime = 0; this.paused = false; this.onArrivalReaction = "proceedOnTargets"; // 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 = {}; this.unitStat["RangedInfantry"] = { "priority" : 1, "minSize" : 4, "targetSize" : 10, "batchSize" : 5, "classes" : ["Infantry","Ranged"], "interests" : [ ["canGather", 2], ["strength",2], ["cost",1] ], "templates" : [] }; this.unitStat["MeleeInfantry"] = { "priority" : 1, "minSize" : 4, "targetSize" : 10, "batchSize" : 5, "classes" : ["Infantry","Melee"], "interests" : [ ["canGather", 2], ["strength",2], ["cost",1] ], "templates" : [] }; this.unitStat["MeleeCavalry"] = { "priority" : 1, "minSize" : 3, "targetSize" : 8 , "batchSize" : 3, "classes" : ["Cavalry","Melee"], "interests" : [ ["strength",2], ["cost",1] ], "templates" : [] }; this.unitStat["RangedCavalry"] = { "priority" : 1, "minSize" : 3, "targetSize" : 8 , "batchSize" : 3, "classes" : ["Cavalry","Ranged"], "interests" : [ ["strength",2], ["cost",1] ], "templates" : [] }; var priority = 50; if (type === "rush") { // we have 3 minutes to train infantry. delete this.unitStat["RangedInfantry"]; delete this.unitStat["MeleeInfantry"]; delete this.unitStat["MeleeCavalry"]; delete this.unitStat["RangedCavalry"]; this.unitStat["Infantry"] = { "priority" : 1, "minSize" : 10, "targetSize" : 30, "batchSize" : 1, "classes" : ["Infantry"], "interests" : [ ["strength",1], ["cost",1] ], "templates" : [] }; this.maxPreparationTime = 150*1000; priority = 120; } else if (type === "superSized") { // our first attack has started worst case at the 14th minute, we want to attack another time by the 21th minute, so we rock 6.5 minutes this.maxPreparationTime = 480000; // basically we want a mix of citizen soldiers so our barracks have a purpose, and champion units. this.unitStat["RangedInfantry"] = { "priority" : 1, "minSize" : 5, "targetSize" : 20, "batchSize" : 5, "classes" : ["Infantry","Ranged", "CitizenSoldier"], "interests" : [["strength",3], ["cost",1] ], "templates" : [] }; this.unitStat["MeleeInfantry"] = { "priority" : 1, "minSize" : 5, "targetSize" : 20, "batchSize" : 5, "classes" : ["Infantry","Melee", "CitizenSoldier" ], "interests" : [ ["strength",3], ["cost",1] ], "templates" : [] }; this.unitStat["ChampRangedInfantry"] = { "priority" : 1, "minSize" : 5, "targetSize" : 15, "batchSize" : 5, "classes" : ["Infantry","Ranged", "Champion"], "interests" : [["strength",3], ["cost",1] ], "templates" : [] }; this.unitStat["ChampMeleeInfantry"] = { "priority" : 1, "minSize" : 5, "targetSize" : 15, "batchSize" : 5, "classes" : ["Infantry","Melee", "Champion" ], "interests" : [ ["strength",3], ["cost",1] ], "templates" : [] }; this.unitStat["MeleeCavalry"] = { "priority" : 1, "minSize" : 3, "targetSize" : 18, "batchSize" : 3, "classes" : ["Cavalry","Melee", "CitizenSoldier" ], "interests" : [ ["strength",2], ["cost",1] ], "templates" : [] }; this.unitStat["RangedCavalry"] = { "priority" : 1, "minSize" : 3, "targetSize" : 18 , "batchSize" : 3, "classes" : ["Cavalry","Ranged", "CitizenSoldier"], "interests" : [ ["strength",2], ["cost",1] ], "templates" : [] }; this.unitStat["ChampMeleeInfantry"] = { "priority" : 0.8, "minSize" : 3, "targetSize" : 12, "batchSize" : 3, "classes" : ["Infantry","Melee", "Champion" ], "interests" : [ ["strength",3], ["cost",1] ], "templates" : [] }; this.unitStat["ChampMeleeCavalry"] = { "priority" : 0.8, "minSize" : 3, "targetSize" : 12, "batchSize" : 3, "classes" : ["Cavalry","Melee", "Champion" ], "interests" : [ ["strength",2], ["cost",1] ], "templates" : [] }; priority = 70; } // 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); 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 = Filters.and(Filters.byMetadata(PlayerID, "plan",this.name),Filters.byOwner(PlayerID)); + var filter = API3.Filters.and(API3.Filters.byMetadata(PlayerID, "plan",this.name),API3.Filters.byOwner(PlayerID)); this.unitCollection = gameState.getOwnEntities().filter(filter); this.unitCollection.registerUpdates(); this.unitCollection.length; 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 = Filters.and(Filters.byClassesAnd(Unit["classes"]),Filters.and(Filters.byMetadata(PlayerID, "plan",this.name),Filters.byOwner(PlayerID))); + 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.getOwnEntities().filter(filter); this.unit[cat].registerUpdates(); this.unit[cat].length; this.buildOrder.push([0, Unit["classes"], this.unit[cat], Unit, cat]); } /*if (gameState.getTimeElapsed() > 900000) // 15 minutes { this.unitStat.Cavalry.Ranged["minSize"] = 5; this.unitStat.Cavalry.Melee["minSize"] = 5; this.unitStat.Infantry.Ranged["minSize"] = 10; this.unitStat.Infantry.Melee["minSize"] = 10; this.unitStat.Cavalry.Ranged["targetSize"] = 10; this.unitStat.Cavalry.Melee["targetSize"] = 10; this.unitStat.Infantry.Ranged["targetSize"] = 20; this.unitStat.Infantry.Melee["targetSize"] = 20; this.unitStat.Siege["targetSize"] = 5; this.unitStat.Siege["minSize"] = 2; } else { this.maxPreparationTime = 180000; }*/ // todo: REACTIVATE (in all caps) if (type === "harass_raid" && 0 == 1) { this.targetFinder = this.raidingTargetFinder; this.onArrivalReaction = "huntVillagers"; this.type = "harass_raid"; // This is a Cavalry raid against villagers. A Cavalry Swordsman has a bonus against these. Only build these this.maxPreparationTime = 180000; // 3 minutes. if (gameState.playerData.civ === "hele") // hellenes have an ealry Cavalry Swordsman { this.unitCount.Cavalry.Melee = { "subCat" : ["Swordsman"] , "usesSubcategories" : true, "Swordsman" : undefined, "priority" : 1, "currentAmount" : 0, "minimalAmount" : 0, "preferedAmount" : 0 }; this.unitCount.Cavalry.Melee.Swordsman = { "priority" : 1, "currentAmount" : 0, "minimalAmount" : 4, "preferedAmount" : 7, "fallback" : "abort" }; } else { this.unitCount.Cavalry.Melee = { "subCat" : undefined , "usesSubcategories" : false, "priority" : 1, "currentAmount" : 0, "minimalAmount" : 4, "preferedAmount" : 7 }; } this.unitCount.Cavalry.Ranged["minimalAmount"] = 0; this.unitCount.Cavalry.Ranged["preferedAmount"] = 0; this.unitCount.Infantry.Ranged["minimalAmount"] = 0; this.unitCount.Infantry.Ranged["preferedAmount"] = 0; this.unitCount.Infantry.Melee["minimalAmount"] = 0; this.unitCount.Infantry.Melee["preferedAmount"] = 0; this.unitCount.Siege["preferedAmount"] = 0; } this.anyNotMinimal = true; // used for support plans - var myFortresses = gameState.getOwnTrainingFacilities().filter(Filters.byClass("GarrisonFortress")); + 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]; } if (type == 'harass_raid') { this.rallyPoint[0] = (position[0]*3.9 + 0.1 * CCpos[0]) / 4.0; this.rallyPoint[1] = (position[1]*3.9 + 0.1 * CCpos[1]) / 4.0; } } // some variables for during the attack this.position5TurnsAgo = [0,0]; this.lastPosition = [0,0]; this.position = [0,0]; this.threatList = []; // sounds so FBI this.tactics = undefined; this.assignUnits(gameState); - //debug ("Before"); + //m.debug ("Before"); //Engine.DumpHeap(); // get a good path to an estimated target. - this.pathFinder = new aStarPath(gameState,false,false, this.targetPlayer); + 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; - //debug ("after"); + //m.debug ("after"); //Engine.DumpHeap(); return true; }; -CityAttack.prototype.getName = function(){ +m.CityAttack.prototype.getName = function(){ return this.name; }; -CityAttack.prototype.getType = function(){ +m.CityAttack.prototype.getType = function(){ return this.type; }; // Returns true if the attack can be executed at the current time // Basically his checks we have enough units. // We run a count of our units. -CityAttack.prototype.canStart = function(gameState){ +m.CityAttack.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; // TODO: check if our target is valid and a few other stuffs (good moment to attack?) }; -CityAttack.prototype.isStarted = function(){ +m.CityAttack.prototype.isStarted = function(){ if ((this.state !== "unexecuted")) - debug ("Attack plan already started"); + m.debug ("Attack plan already started"); return !(this.state == "unexecuted"); }; -CityAttack.prototype.isPaused = function(){ +m.CityAttack.prototype.isPaused = function(){ return this.paused; }; -CityAttack.prototype.setPaused = function(gameState, boolValue){ +m.CityAttack.prototype.setPaused = function(gameState, boolValue){ if (!this.paused && boolValue === true) { this.pausingStart = gameState.getTimeElapsed(); this.paused = true; - debug ("Pausing attack plan " +this.name); + m.debug ("Pausing attack plan " +this.name); } else if (this.paused && boolValue === false) { this.totalPausingTime += gameState.getTimeElapsed() - this.pausingStart; this.paused = false; - debug ("Unpausing attack plan " +this.name); + m.debug ("Unpausing attack plan " +this.name); } }; -CityAttack.prototype.mustStart = function(gameState){ +m.CityAttack.prototype.mustStart = function(gameState){ if (this.isPaused() || this.path === undefined) return false; var MaxReachedEverywhere = true; for (var unitCat in this.unitStat) { var Unit = this.unitStat[unitCat]; if (this.unit[unitCat].length < Unit["targetSize"]) { MaxReachedEverywhere = false; } } if (MaxReachedEverywhere || (gameState.getPopulationMax() - gameState.getPopulation() < 10 && this.canStart(gameState))) return true; return (this.maxPreparationTime + this.timeOfPlanStart + this.totalPausingTime < gameState.getTimeElapsed()); }; // Adds a build order. If resetQueue is true, this will reset the queue. -CityAttack.prototype.addBuildOrder = function(gameState, name, unitStats, resetQueue) { +m.CityAttack.prototype.addBuildOrder = function(gameState, name, unitStats, resetQueue) { if (!this.isStarted()) { - debug ("Adding a build order for " + name); + m.debug ("Adding a build order for " + name); // 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 = Filters.and(Filters.byClassesAnd(Unit["classes"]),Filters.and(Filters.byMetadata(PlayerID, "plan",this.name),Filters.byOwner(PlayerID))); + 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.getOwnEntities().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(); } } }; // 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. -CityAttack.prototype.updatePreparation = function(gameState, HQ,events) { +m.CityAttack.prototype.updatePreparation = function(gameState, HQ,events) { var self = this; if (this.path == undefined || this.target == undefined || this.path === "toBeContinued") { // find our target if (this.target == undefined) { var targets = this.targetFinder(gameState, HQ); if (targets.length === 0) targets = this.defaultTargetFinder(gameState, HQ); if (targets.length !== 0) { - debug ("Aiming for " + targets); + m.debug ("Aiming for " + targets); // picking a target var maxDist = -1; var index = 0; for (var i in targets._entities) { // we're sure it has a position has TargetFinder already checks that. - var dist = SquareVectorDistance(targets._entities[i].position(), this.rallyPoint); + 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);//,gameState); else if (this.path === "toBeContinued") this.path = this.pathFinder.continuePath();//gameState); 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) { - debug ("This is actually a water map."); + m.debug ("This is actually a water map."); gameState.ai.HQ.waterMap = true; return 0; } - debug ("We need a ship."); + m.debug ("We need a ship."); 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 = Map.createTerritoryMap(gameState); + 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 = Map.createTerritoryMap(gameState); + 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"); // 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(); if ( gameState.ai.playedTurn % 5 == 0) this.AllToRallyPoint(gameState, true); } 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(); if (gameState.ai.playedTurn % 5 == 0) { this.AllToRallyPoint(gameState, true); // TODO: should use this time to let gatherers deposit resources. } Engine.ProfileStop(); return 1; } else if (!this.mustStart(gameState)) { // We still have time left to recruit units and do stuffs. // let's sort by training advancement, ie 'current size / target size' // count the number of queued units too. // substract priority. this.buildOrder.sort(function (a,b) { //}) { var aQueued = gameState.countOwnQueuedEntitiesWithMetadata("special","Plan_"+self.name+"_"+a[4]); aQueued += self.queue.countQueuedUnitsWithMetadata("special","Plan_"+self.name+"_"+a[4]); aQueued += self.queueChamp.countQueuedUnitsWithMetadata("special","Plan_"+self.name+"_"+a[4]); a[0] = (a[2].length + aQueued)/a[3]["targetSize"]; var bQueued = gameState.countOwnQueuedEntitiesWithMetadata("special","Plan_"+self.name+"_"+b[4]); bQueued += self.queue.countQueuedUnitsWithMetadata("special","Plan_"+self.name+"_"+b[4]); bQueued += self.queueChamp.countQueuedUnitsWithMetadata("special","Plan_"+self.name+"_"+b[4]); b[0] = (b[2].length + bQueued)/b[3]["targetSize"]; a[0] -= a[3]["priority"]; b[0] -= b[3]["priority"]; return (a[0]) - (b[0]); }); this.assignUnits(gameState); if (gameState.ai.playedTurn % 5 == 0) { this.AllToRallyPoint(gameState, false); this.unitCollection.setStance("standground"); // make sure units won't disperse out of control } Engine.ProfileStart("Creating units."); // gets the number in training of the same kind as the first one. var specialData = "Plan_"+this.name+"_"+this.buildOrder[0][4]; var inTraining = gameState.countOwnQueuedEntitiesWithMetadata("special",specialData); var queued = this.queue.countQueuedUnitsWithMetadata("special",specialData) + this.queueChamp.countQueuedUnitsWithMetadata("special",specialData) if (queued + inTraining + this.buildOrder[0][2].length <= 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 (this.buildOrder[0][0] < 1 && queue.length() <= 5) { var template = HQ.findBestTrainableSoldier(gameState, this.buildOrder[0][1], this.buildOrder[0][3]["interests"] ); - //debug ("tried " + uneval(this.buildOrder[0][1]) +", and " + template); + //m.debug ("tried " + uneval(this.buildOrder[0][1]) +", and " + template); // 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"]; // TODO: this should be plan dependant. if (gameState.getTimeElapsed() > 1800000) max *= 2; if (gameState.getTemplate(template).hasClass("CitizenSoldier")) - queue.addItem( new TrainingPlan(gameState,template, { "role" : "worker", "plan" : this.name, "special" : specialData, "base" : 1 }, this.buildOrder[0][3]["batchSize"],max ) ); + queue.addItem( new m.TrainingPlan(gameState,template, { "role" : "worker", "plan" : this.name, "special" : specialData, "base" : 1 }, this.buildOrder[0][3]["batchSize"],max ) ); else - queue.addItem( new TrainingPlan(gameState,template, { "role" : "attack", "plan" : this.name, "special" : specialData, "base" : 1 }, this.buildOrder[0][3]["batchSize"],max ) ); + queue.addItem( new m.TrainingPlan(gameState,template, { "role" : "attack", "plan" : this.name, "special" : specialData, "base" : 1 }, this.buildOrder[0][3]["batchSize"],max ) ); } } } /* if (!this.startedPathing && this.path === undefined) { // find our target var targets = this.targetFinder(gameState, HQ); if (targets.length === 0){ targets = this.defaultTargetFinder(gameState, HQ); } if (targets.length) { this.targetPos = undefined; var count = 0; while (!this.targetPos){ var rand = Math.floor((Math.random()*targets.length)); var target = targets.toEntityArray()[rand]; this.targetPos = target.position(); count++; if (count > 1000){ - debug("No target with a valid position found"); + m.debug("No target with a valid position found"); return false; } } this.startedPathing = true; // Start pathfinding using the optimized version, with a minimal sampling of 2 this.pathFinder.getPath(this.rallyPoint,this.targetPos, false, 2, gameState); } } else if (this.startedPathing) { var path = this.pathFinder.continuePath(gameState); if (path !== "toBeContinued") { this.startedPathing = false; this.path = path; - debug("Pathing ended"); + m.debug("Pathing ended"); } } */ Engine.ProfileStop(); Engine.ProfileStop(); // can happen for now if (this.buildOrder.length === 0) { - debug ("Ending plan: no build orders"); + m.debug ("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)) return 2; else return 0; return 0; }; -CityAttack.prototype.assignUnits = function(gameState){ +m.CityAttack.prototype.assignUnits = function(gameState){ var self = this; // 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). var NoRole = gameState.getOwnEntitiesByRole(undefined); if (this.type === "rush") NoRole = gameState.getOwnEntitiesByRole("worker"); NoRole.forEach(function(ent) { if (ent.hasClass("Unit") && ent.attackTypes() !== undefined) { if (ent.hasClasses(["CitizenSoldier", "Infantry"])) ent.setMetadata(PlayerID, "role", "worker"); else ent.setMetadata(PlayerID, "role", "attack"); ent.setMetadata(PlayerID, "plan", self.name); } }); }; // this sends a unit by ID back to the "rally point" -CityAttack.prototype.ToRallyPoint = function(gameState,id) +m.CityAttack.prototype.ToRallyPoint = function(gameState,id) { // Move back to nearest rallypoint gameState.getEntityById(id).move(this.rallyPoint[0],this.rallyPoint[1]); } // this sends all units back to the "rally point" by entity collections. // It doesn't disturb ones that could be currently defending, even if the plan is not (yet) paused. -CityAttack.prototype.AllToRallyPoint = function(gameState, evenWorkers) { +m.CityAttack.prototype.AllToRallyPoint = function(gameState, evenWorkers) { var self = this; if (evenWorkers) { for (var unitCat in this.unit) { this.unit[unitCat].forEach(function (ent) { if (ent.getMetadata(PlayerID, "role") != "defence") { ent.setMetadata(PlayerID,"role", "attack"); ent.move(self.rallyPoint[0],self.rallyPoint[1]); } }); } } else { for (var unitCat in this.unit) { this.unit[unitCat].forEach(function (ent) { if (ent.getMetadata(PlayerID, "role") != "worker" && ent.getMetadata(PlayerID, "role") != "defence") ent.move(self.rallyPoint[0],self.rallyPoint[1]); }); } } } // Default target finder aims for conquest critical targets -CityAttack.prototype.defaultTargetFinder = function(gameState, HQ){ +m.CityAttack.prototype.defaultTargetFinder = function(gameState, HQ){ var targets = undefined; targets = HQ.enemyWatchers[this.targetPlayer].getEnemyBuildings(gameState, "CivCentre",true); if (targets.length == 0) { targets = HQ.enemyWatchers[this.targetPlayer].getEnemyBuildings(gameState, "ConquestCritical"); } // If there's nothing, attack anything else that's less critical if (targets.length == 0) { targets = HQ.enemyWatchers[this.targetPlayer].getEnemyBuildings(gameState, "Town",true); } if (targets.length == 0) { targets = HQ.enemyWatchers[this.targetPlayer].getEnemyBuildings(gameState, "Village",true); } // no buildings, attack anything conquest critical, even units (it's assuming it won't move). if (targets.length == 0) { - targets = gameState.getEnemyEntities().filter(Filters.and( Filters.byOwner(this.targetPlayer),Filters.byClass("ConquestCritical"))); + targets = gameState.getEnemyEntities().filter(API3.Filters.and( API3.Filters.byOwner(this.targetPlayer),API3.Filters.byClass("ConquestCritical"))); } return targets; }; // tupdate -CityAttack.prototype.raidingTargetFinder = function(gameState, HQ, Target){ +m.CityAttack.prototype.raidingTargetFinder = function(gameState, HQ, Target){ var targets = undefined; if (Target == "villager") { // let's aim for any resource dropsite. We assume villagers are in the neighborhood (note: the human player could certainly troll us... small (scouting) TODO here.) targets = gameState.entities.filter(function(ent) { return (ent.hasClass("Structure") && ent.resourceDropsiteTypes() !== undefined && !ent.hasClass("CivCentre") && ent.owner() === this.targetPlayer && ent.position()); }); if (targets.length == 0) { targets = gameState.entities.filter(function(ent) { return (ent.hasClass("CivCentre") && ent.resourceDropsiteTypes() !== undefined && ent.owner() === this.targetPlayer && ent.position()); }); } if (targets.length == 0) { // if we're here, it means they also don't have no CC... So I'll just take any building at this point. targets = gameState.entities.filter(function(ent) { return (ent.hasClass("Structure") && ent.owner() === this.targetPlayer && ent.position()); }); } return targets; } else { return this.defaultTargetFinder(gameState, HQ); } }; // 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 -CityAttack.prototype.StartAttack = function(gameState, HQ){ +m.CityAttack.prototype.StartAttack = function(gameState, HQ){ // 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.freeze(); this.unitCollection.allowQuickIter(); this.unitCollection.move(this.path[0][0][0], this.path[0][0][1]); this.unitCollection.setStance("aggressive"); - this.unitCollection.filter(Filters.byClass("Siege")).setStance("defensive"); + this.unitCollection.filter(API3.Filters.byClass("Siege")).setStance("defensive"); this.state = "walking"; } else { gameState.ai.gameFinished = true; - debug ("I do not have any target. So I'll just assume I won the game."); + 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 -CityAttack.prototype.update = function(gameState, HQ, events){ +m.CityAttack.prototype.update = function(gameState, HQ, events){ var self = this; Engine.ProfileStart("Update Attack"); // we're marching towards the target // Check for attacked units in our band. var bool_attacked = false; // raids don't care about attacks much if (this.unitCollection.length === 0) { Engine.ProfileStop(); return 0; } this.position = this.unitCollection.getCentrePosition(); var IDs = this.unitCollection.toIdArray(); // this actually doesn't do anything right now. if (this.state === "walking") { var attackedNB = 0; var toProcess = {}; var armyToProcess = {}; // 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. for (var key in events) { var e = events[key]; if (e.type === "Attacked" && e.msg) { if (IDs.indexOf(e.msg.target) !== -1) { var attacker = gameState.getEntityById(e.msg.attacker); var ourUnit = gameState.getEntityById(e.msg.target); if (attacker && attacker.position() && attacker.hasClass("Unit") && attacker.owner() != 0 && attacker.owner() != PlayerID) { - var territoryMap = Map.createTerritoryMap(gameState); + var territoryMap = m.createTerritoryMap(gameState); if ( +territoryMap.point(attacker.position()) - 64 === +this.targetPlayer) { attackedNB++; } //if (HQ.enemyWatchers[attacker.owner()]) { //toProcess[attacker.id()] = attacker; //var armyID = HQ.enemyWatchers[attacker.owner()].getArmyFromMember(attacker.id()); //armyToProcess[armyID[0]] = armyID[1]; //} } // if we're being attacked by a building, flee. if (attacker && ourUnit && attacker.hasClass("Structure")) { ourUnit.flee(attacker); } } } } if (attackedNB > 4) { - debug ("Attack Plan " +this.type +" " +this.name +" has arrived to destination."); + 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"; } /* }&& this.type !== "harass_raid"){ // walking toward the target var sumAttackerPos = [0,0]; var numAttackers = 0; // let's check if one of our unit is not under attack, by any chance. for (var key in events){ var e = events[key]; if (e.type === "Attacked" && e.msg){ if (this.unitCollection.toIdArray().indexOf(e.msg.target) !== -1){ var attacker = HeadQuarters.entity(e.msg.attacker); if (attacker && attacker.position()){ sumAttackerPos[0] += attacker.position()[0]; sumAttackerPos[1] += attacker.position()[1]; numAttackers += 1; bool_attacked = true; // todo: differentiate depending on attacker type... If it's a ship, let's not do anythin, a building, depends on the attack type/ if (this.threatList.indexOf(e.msg.attacker) === -1) { var enemySoldiers = HeadQuarters.getEnemySoldiers().toEntityArray(); for (var j in enemySoldiers) { var enemy = enemySoldiers[j]; if (enemy.position() === undefined) // likely garrisoned continue; - if (inRange(enemy.position(), attacker.position(), 1000) && this.threatList.indexOf(enemy.id()) === -1) + if (m.inRange(enemy.position(), attacker.position(), 1000) && this.threatList.indexOf(enemy.id()) === -1) this.threatList.push(enemy.id()); } this.threatList.push(e.msg.attacker); } } } } } if (bool_attacked > 0){ var avgAttackerPos = [sumAttackerPos[0]/numAttackers, sumAttackerPos[1]/numAttackers]; units.move(avgAttackerPos[0], avgAttackerPos[1]); // let's run towards it. this.tactics = new Tactics(gameState,HeadQuarters, this.idList,this.threatList,true); this.state = "attacking_threat"; } }else if (this.state === "attacking_threat"){ this.tactics.eventMetadataCleanup(events,HeadQuarters); var removeList = this.tactics.removeTheirDeads(HeadQuarters); this.tactics.removeMyDeads(HeadQuarters); for (var i in removeList){ this.threatList.splice(this.threatList.indexOf(removeList[i]),1); } if (this.threatList.length <= 0) { this.tactics.disband(HeadQuarters,events); this.tactics = undefined; this.state = "walking"; units.move(this.path[0][0], this.path[0][1]); }else { this.tactics.reassignAttacks(HeadQuarters); } }*/ } 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 (SquareVectorDistance(this.position, this.position5TurnsAgo) < 10 && this.path.length > 0 && gameState.ai.playedTurn % 5 === 0) { + 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(Filters.byClass("Siege")); + var sieges = this.unitCollection.filter(API3.Filters.byClass("Siege")); var farthest = 0; var farthestEnt = -1; sieges.forEach (function (ent) { - if (SquareVectorDistance(ent.position(),self.position) > farthest) + if (API3.SquareVectorDistance(ent.position(),self.position) > farthest) { - farthest = SquareVectorDistance(ent.position(),self.position); + 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 && SquareVectorDistance(this.position, this.lastPosition) < 20 && this.path.length > 0) { + 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(Filters.and(Filters.byOwner(this.targetPlayer), Filters.byClass("StoneWall"))); + 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 && SquareVectorDistance(self.position, ent.position()) < 800) + if (!nexttoWalls && API3.SquareVectorDistance(self.position, ent.position()) < 800) nexttoWalls = true; }); // there are walls but we can attack - if (nexttoWalls && this.unitCollection.filter(Filters.byCanAttack("StoneWall")).length !== 0) + if (nexttoWalls && this.unitCollection.filter(API3.Filters.byCanAttack("StoneWall")).length !== 0) { - debug ("Attack Plan " +this.type +" " +this.name +" has met walls and is not happy."); + m.debug ("Attack Plan " +this.type +" " +this.name +" has met walls and is not happy."); this.state = "arrived"; } else if (nexttoWalls) { // abort plan. - debug ("Attack Plan " +this.type +" " +this.name +" has met walls and gives up."); + 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 (SquareVectorDistance(this.unitCollection.getCentrePosition(), this.targetPos) < 7500 || - SquareVectorDistance(this.unitCollection.getCentrePosition(), this.path[0][0]) < 650) { - if (this.unitCollection.filter(Filters.byClass("Siege")).length !== 0 - && SquareVectorDistance(this.unitCollection.getCentrePosition(), this.targetPos) >= 7500 - && SquareVectorDistance(this.unitCollection.filter(Filters.byClass("Siege")).getCentrePosition(), this.path[0][0]) >= 650) + if (API3.SquareVectorDistance(this.unitCollection.getCentrePosition(), this.targetPos) < 7500 || + API3.SquareVectorDistance(this.unitCollection.getCentrePosition(), this.path[0][0]) < 650) { + if (this.unitCollection.filter(API3.Filters.byClass("Siege")).length !== 0 + && API3.SquareVectorDistance(this.unitCollection.getCentrePosition(), this.targetPos) >= 7500 + && API3.SquareVectorDistance(this.unitCollection.filter(API3.Filters.byClass("Siege")).getCentrePosition(), this.path[0][0]) >= 650) { } else { for (var i = 0; i < this.path.length; ++i) { - debug ("path waypoint " + i + "," + this.path[i][1] + " at " + uneval(this.path[i][0])); + m.debug ("path waypoint " + i + "," + this.path[i][1] + " at " + uneval(this.path[i][0])); } - debug ("position is " + this.unitCollection.getCentrePosition()); + m.debug ("position is " + this.unitCollection.getCentrePosition()); // okay so here basically two cases. The first one is "we need a boat at this point". // the second one is "we need to unload at this point". The third is "normal". 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 { - debug ("Attack Plan " +this.type +" " +this.name +" has arrived to destination."); + 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) { - debug ("Attack Plan " +this.type +" " +this.name +" has arrived to destination."); + 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 TransportPlan(gameState, this.unitCollection.toIdArray(), this.path[0][0], false); + var plan = new m.TransportPlan(gameState, this.unitCollection.toIdArray(), this.path[0][0], false); this.tpPlanID = plan.ID; HQ.navalManager.transportPlans.push(plan); - debug ("Transporting over sea"); + 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 (!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"); }); } else if (this.onArrivalReaction == "huntVillagers") { // let's get any villager and target them with a tactics manager var enemyCitizens = gameState.entities.filter(function(ent) { return (gameState.isEntityEnemy(ent) && ent.hasClass("Support") && ent.owner() !== 0 && ent.position()); }); var targetList = []; enemyCitizens.forEach( function (enemy) { - if (inRange(enemy.position(), units.getCentrePosition(), 2500) && targetList.indexOf(enemy.id()) === -1) + if (m.inRange(enemy.position(), units.getCentrePosition(), 2500) && targetList.indexOf(enemy.id()) === -1) targetList.push(enemy.id()); }); if (targetList.length > 0) { this.tactics = new Tactics(gameState,HeadQuarters, this.idList,targetList); this.state = "huntVillagers"; var arrivedthisTurn = true; } else { this.state = ""; var arrivedthisTurn = true; } } } if (this.state === "") { // Units attacked will target their attacker unless they're siege. Then we take another non-siege unit to attack them. for (var key in events) { var e = events[key]; if (e.type === "Attacked" && e.msg) { if (IDs.indexOf(e.msg.target) !== -1) { var attacker = gameState.getEntityById(e.msg.attacker); var ourUnit = gameState.getEntityById(e.msg.target); if (attacker && attacker.position() && attacker.hasClass("Unit") && attacker.owner() != 0 && attacker.owner() != PlayerID) { if (ourUnit.hasClass("Siege")) { - var collec = this.unitCollection.filterNearest(ourUnit.position(), 8).filter(Filters.not(Filters.byClass("Siege"))).toEntityArray(); + var collec = this.unitCollection.filterNearest(ourUnit.position(), 8).filter(API3.Filters.not(API3.Filters.byClass("Siege"))).toEntityArray(); if (collec.length !== 0) { collec[0].attack(attacker.id()); if (collec.length !== 1) { collec[1].attack(attacker.id()); if (collec.length !== 2) { collec[2].attack(attacker.id()); } } } } else { ourUnit.attack(attacker.id()); } } } } } var enemyUnits = gameState.getGEC("player-" +this.targetPlayer + "-units"); var enemyStructures = gameState.getGEC("player-" +this.targetPlayer + "-structures"); if (this.unitCollUpdateArray === undefined || this.unitCollUpdateArray.length === 0) { this.unitCollUpdateArray = this.unitCollection.toIdArray(); } else { // some stuffs for locality and speed - var territoryMap = Map.createTerritoryMap(gameState); + 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[0]); if (!ent) continue; var orderData = ent.unitAIOrderData(); if (orderData.length !== 0) orderData = orderData[0]; else orderData = undefined; // if the unit is in my territory, make it move. if (territoryMap.point(ent.position()) - 64 === PlayerID) ent.move(this.targetPos[0],this.targetPos[1]); // update it. var needsUpdate = false; if (ent.isIdle()) needsUpdate = true; if (ent.hasClass("Siege") && (!orderData || !orderData["target"] || !gameState.getEntityById(orderData["target"]) || !gameState.getEntityById(orderData["target"]).hasClass("ConquestCritical")) ) needsUpdate = true; else if (!ent.hasClass("Siege") && orderData && orderData["target"] && gameState.getEntityById(orderData["target"]) && gameState.getEntityById(orderData["target"]).hasClass("Structure")) needsUpdate = true; // try to make it attack a unit instead if (timeElapsed - ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime") < 10000) needsUpdate = false; if (needsUpdate === true || arrivedthisTurn) { ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", timeElapsed); var mStruct = enemyStructures.filter(function (enemy) { //}){ if (!enemy.position() || (enemy.hasClass("StoneWall") && ent.canAttackClass("StoneWall"))) { return false; } - if (SquareVectorDistance(enemy.position(),ent.position()) > 3000) { + if (API3.SquareVectorDistance(enemy.position(),ent.position()) > 3000) { return false; } return true; }); var mUnit; if (ent.hasClass("Cavalry") && ent.countersClasses(["Support"])) { mUnit = enemyUnits.filter(function (enemy) { //}){ if (!enemy.position()) { return false; } if (!enemy.hasClass("Support")) return false; - if (SquareVectorDistance(enemy.position(),ent.position()) > 10000) { + if (API3.SquareVectorDistance(enemy.position(),ent.position()) > 10000) { return false; } return true; }); } if (!(ent.hasClass("Cavalry") && ent.countersClasses(["Support"])) || mUnit.length === 0) { mUnit = enemyUnits.filter(function (enemy) { //}){ if (!enemy.position()) { return false; } - if (SquareVectorDistance(enemy.position(),ent.position()) > 10000) { + if (API3.SquareVectorDistance(enemy.position(),ent.position()) > 10000) { return false; } return true; }); } 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")) // we hate gates { isGate = true; vala += 10000; } else if (structa.hasClass("ConquestCritical")) vala += 200; var valb = structb.costSum(); if (structb.hasClass("Gates") && ent.canAttackClass("StoneWall")) // we hate gates { isGate = true; valb += 10000; } else if (structb.hasClass("ConquestCritical")) valb += 200; //warn ("Structure " +structa.genericName() + " is worth " +vala); //warn ("Structure " +structb.genericName() + " is worth " +valb); 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.1); ent.attack(mStruct[+rand].id()); - //debug ("Siege units attacking a structure from " +mStruct[+rand].owner() + " , " +mStruct[+rand].templateName()); + //m.debug ("Siege units attacking a structure from " +mStruct[+rand].owner() + " , " +mStruct[+rand].templateName()); } - } else if (SquareVectorDistance(self.targetPos, ent.position()) > 900 ){ - //debug ("Siege units moving to " + uneval(self.targetPos)); + } else if (API3.SquareVectorDistance(self.targetPos, ent.position()) > 900 ){ + //m.debug ("Siege units moving to " + uneval(self.targetPos)); ent.move(self.targetPos[0],self.targetPos[1]); } } else { if (mUnit.length !== 0) { var rand = Math.floor(Math.random() * mUnit.length*0.99); ent.attack(mUnit[(+rand)].id()); - //debug ("Units attacking a unit from " +mUnit[+rand].owner() + " , " +mUnit[+rand].templateName()); - } else if (SquareVectorDistance(self.targetPos, ent.position()) > 900 ){ - //debug ("Units moving to " + uneval(self.targetPos)); + //m.debug ("Units attacking a unit from " +mUnit[+rand].owner() + " , " +mUnit[+rand].templateName()); + } else if (API3.SquareVectorDistance(self.targetPos, ent.position()) > 900 ){ + //m.debug ("Units moving to " + uneval(self.targetPos)); ent.move(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")) // we hate gates { isGate = true; vala += 10000; } else if (structa.hasClass("ConquestCritical")) vala += 100; var valb = structb.costSum(); if (structb.hasClass("Gates") && ent.canAttackClass("StoneWall")) // we hate gates { 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()); - //debug ("Units attacking a structure from " +mStruct[+rand].owner() + " , " +mStruct[+rand].templateName()); + //m.debug ("Units attacking a structure from " +mStruct[+rand].owner() + " , " +mStruct[+rand].templateName()); } } } } } this.unitCollUpdateArray.splice(0,10); } // updating targets. if (!gameState.getEntityById(this.target.id())) { var targets = this.targetFinder(gameState, HQ); if (targets.length === 0){ targets = this.defaultTargetFinder(gameState, HQ); } if (targets.length) { - debug ("Seems like our target has been destroyed. Switching."); - debug ("Aiming for " + targets); + m.debug ("Seems like our target has been destroyed. Switching."); + m.debug ("Aiming for " + targets); // picking a target this.targetPos = undefined; var count = 0; while (!this.targetPos){ var rand = Math.floor((Math.random()*targets.length)); this.target = targets.toEntityArray()[rand]; this.targetPos = this.target.position(); count++; if (count > 1000){ - debug("No target with a valid position found"); + m.debug("No target with a valid position found"); 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(); } /* if (this.state === "huntVillagers") { this.tactics.eventMetadataCleanup(events,HeadQuarters); this.tactics.removeTheirDeads(HeadQuarters); this.tactics.removeMyDeads(HeadQuarters); if (this.tactics.isBattleOver()) { this.tactics.disband(HeadQuarters,events); this.tactics = undefined; this.state = ""; return 0; // assume over } else this.tactics.reassignAttacks(HeadQuarters); }*/ this.lastPosition = this.position; Engine.ProfileStop(); return this.unitCollection.length; }; -CityAttack.prototype.totalCountUnits = function(gameState){ +m.CityAttack.prototype.totalCountUnits = function(gameState){ var totalcount = 0; for (var i in this.idList) { totalcount++; } return totalcount; }; // reset any units -CityAttack.prototype.Abort = function(gameState){ +m.CityAttack.prototype.Abort = function(gameState){ this.unitCollection.forEach(function(ent) { 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"); }; + +return m; +}(AEGIS); Index: ps/trunk/binaries/data/mods/public/simulation/ai/aegis/base-manager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/aegis/base-manager.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/aegis/base-manager.js (revision 14441) @@ -1,966 +1,974 @@ +var AEGIS = 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…) */ -var BaseManager = function() { +m.BaseManager = function(Config) { + this.Config = Config; this.farmingFields = false; - this.ID = uniqueIDBases++; + this.ID = m.playerGlobals[PlayerID].uniqueIDBases++; // anchor building: seen as the main building of the base. Needs to have territorial influence this.anchor = undefined; // list of IDs of buildings in our base that have a "territory pusher" function. this.territoryBuildings = []; // will tell if we should be considered as a source of X. this.willGather = { "food": 0, "wood": 0, "stone":0, "metal": 0 }; this.isFarming = false; this.isHunting = true; this.constructing = false; // vector for iterating, to check one use the HQ map. this.territoryIndices = []; }; -BaseManager.prototype.init = function(gameState, events, unconstructed){ +m.BaseManager.prototype.init = function(gameState, events, unconstructed){ this.constructing = unconstructed; // entitycollections - this.units = gameState.getOwnEntities().filter(Filters.and(Filters.byClass("Unit"),Filters.byMetadata(PlayerID, "base", this.ID))); - this.buildings = gameState.getOwnEntities().filter(Filters.and(Filters.byClass("Structure"),Filters.byMetadata(PlayerID, "base", this.ID))); + this.units = gameState.getOwnEntities().filter(API3.Filters.and(API3.Filters.byClass("Unit"),API3.Filters.byMetadata(PlayerID, "base", this.ID))); + this.buildings = gameState.getOwnEntities().filter(API3.Filters.and(API3.Filters.byClass("Structure"),API3.Filters.byMetadata(PlayerID, "base", this.ID))); - this.workers = this.units.filter(Filters.byMetadata(PlayerID,"role","worker")); + this.workers = this.units.filter(API3.Filters.byMetadata(PlayerID,"role","worker")); this.workers.allowQuickIter(); this.buildings.allowQuickIter(); this.units.allowQuickIter(); this.units.registerUpdates(); this.buildings.registerUpdates(); this.workers.registerUpdates(); // array of entity IDs, with each being // { "food" : [close entities, semi-close entities, faraway entities, closeAmount, medianAmount, assignedWorkers collection ] … } (one per resource) // note that "median amount" also counts the closeAmount. this.dropsites = { }; // TODO: difficulty levels for this? // smallRadius is the distance necessary to mark a resource as linked to a dropsite. this.smallRadius = { 'food':40*40,'wood':45*45,'stone':40*40,'metal':40*40 }; // medRadius is the maximal distance for a link, albeit one that would still make us want to build a new dropsite. this.medRadius = { 'food':70*70,'wood':70*70,'stone':80*80,'metal':80*80 }; // bigRadius is the distance for a weak link, mainly for optimizing search for resources when a DP is depleted. this.bigRadius = { 'food':70*70,'wood':200*200,'stone':200*200,'metal':200*200 }; }; -BaseManager.prototype.assignEntity = function(unit){ +m.BaseManager.prototype.assignEntity = function(unit){ unit.setMetadata(PlayerID, "base", this.ID); this.units.updateEnt(unit); this.workers.updateEnt(unit); this.buildings.updateEnt(unit); // TODO: immediately assign it some task? if (unit.hasClass("Structure") && unit.hasTerritoryInfluence() && this.territoryBuildings.indexOf(unit.id()) === -1) this.territoryBuildings.push(unit.id()); }; -BaseManager.prototype.setAnchor = function(anchorEntity) { +m.BaseManager.prototype.setAnchor = function(anchorEntity) { if (!anchorEntity.hasClass("Structure") || !anchorEntity.hasTerritoryInfluence()) { warn("Error: Aegis' 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); if (this.territoryBuildings.indexOf(this.anchor.id()) === -1) this.territoryBuildings.push(this.anchor.id()); return true; } // affects the HQ map. -BaseManager.prototype.initTerritory = function(HQ, gameState) { +m.BaseManager.prototype.initTerritory = function(HQ, gameState) { if (!this.anchor) warn ("Error: Aegis tried to initialize the territory of base " + this.ID + " without assigning it an anchor building first"); var radius = Math.round((this.anchor.territoryInfluenceRadius() / 4.0) * 1.25); var LandSize = gameState.sharedScript.accessibility.getRegionSize(this.anchor.position()); this.accessIndex = gameState.sharedScript.accessibility.getAccessValue(this.anchor.position()); if (LandSize < 6500) { // We're on a small land, we'll assign all territories in the vicinity. // there's a slight chance we're on an elongated weird stuff, we'll just pump up a little the radius radius = Math.round(radius*1.2); } var x = Math.round(this.anchor.position()[0]/gameState.cellSize); var y = Math.round(this.anchor.position()[1]/gameState.cellSize); this.territoryIndices = []; var width = gameState.getMap().width; for (var xi = -radius; xi <= radius; ++xi) for (var yi = -radius; yi <= radius; ++yi) if (xi*xi+yi*yi < radius*radius && HQ.basesMap.map[(x+xi) + (y+yi)*width] === 0) { if (this.accessIndex == gameState.sharedScript.accessibility.landPassMap[x+xi + width*(y+yi)]) { this.territoryIndices.push((x+xi) + (y+yi)*width); HQ.basesMap.map[(x+xi) + (y+yi)*width] = this.ID; } } } -BaseManager.prototype.initGatheringFunctions = function(HQ, gameState, specTypes) { +m.BaseManager.prototype.initGatheringFunctions = function(HQ, gameState, specTypes) { // init our gathering functions. var types = ["food","wood","stone","metal"]; if (specTypes !== undefined) type = specTypes; var self = this; var count = 0; for (i in types) { var type = types[i]; // TODO: set us as "X" gatherer - this.buildings.filter(Filters.isDropsite(type)).forEach(function(ent) { self.initializeDropsite(gameState, ent,type) }); + this.buildings.filter(API3.Filters.isDropsite(type)).forEach(function(ent) { self.initializeDropsite(gameState, ent,type) }); if (this.getResourceLevel(gameState, type, "all") > 1000) this.willGather[type] = 1; } if (this.willGather["food"] === 0) { var needFarm = true; // Let's check again for food for (base in HQ.baseManagers) if (HQ.baseManagers[base].willGather["food"] === 1) needFarm = false; if (needFarm) this.willGather["food"] = 1; } - debug ("food" + this.willGather["food"]); - debug (this.willGather["wood"]); - debug (this.willGather["stone"]); - debug (this.willGather["metal"]); + m.debug ("food" + this.willGather["food"]); + m.debug (this.willGather["wood"]); + m.debug (this.willGather["stone"]); + m.debug (this.willGather["metal"]); } -BaseManager.prototype.checkEvents = function (gameState, events, queues) { +m.BaseManager.prototype.checkEvents = function (gameState, events, queues) { for (i in events) { if (events[i].type == "Destroy") { // let's check we haven't lost an important building here. var evt = events[i]; if (evt.msg != undefined && !evt.msg.SuccessfulFoundation && evt.msg.entityObj != undefined && evt.msg.metadata !== undefined && evt.msg.metadata[PlayerID] && evt.msg.metadata[PlayerID]["base"] !== undefined && evt.msg.metadata[PlayerID]["base"] == this.ID) { var ent = evt.msg.entityObj; if (ent.hasTerritoryInfluence()) this.territoryBuildings.splice(this.territoryBuildings.indexOf(ent.id()),1); if (ent.resourceDropsiteTypes()) this.scrapDropsite(gameState, ent); if (evt.msg.metadata[PlayerID]["baseAnchor"] && evt.msg.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(); }); if (ent.hasClass("CivCentre")) { // TODO: might want to tell the queue manager to pause other stuffs if we are the only base. - queues.civilCentre.addItem(new ConstructionPlan(gameState, "structures/{civ}_civil_centre", { "base" : this.ID, "baseAnchor" : true }, 0 , -1,ent.position())); + queues.civilCentre.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_civil_centre", { "base" : this.ID, "baseAnchor" : true }, 0 , -1,ent.position())); } else { // TODO - queues.civilCentre.addItem(new ConstructionPlan(gameState, "structures/{civ}_civil_centre", { "base" : this.ID, "baseAnchor" : true },0,-1,ent.position())); + queues.civilCentre.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_civil_centre", { "base" : this.ID, "baseAnchor" : true },0,-1,ent.position())); } } } } } for (i in events) { if (events[i].type == "ConstructionFinished") { // let's check we haven't lost an important building here. var evt = events[i]; if (evt.msg && evt.msg.newentity) { // TODO: we ought to add new resources or do something. var ent = gameState.getEntityById(evt.msg.newentity); if (ent === undefined) continue; if (ent.getMetadata(PlayerID,"base") == this.ID) { if(ent.hasTerritoryInfluence()) this.territoryBuildings.push(ent.id()); if (ent.resourceDropsiteTypes()) for (ress in ent.resourceDropsiteTypes()) this.initializeDropsite(gameState, ent, ent.resourceDropsiteTypes()[ress]); if (ent.resourceSupplyAmount() && ent.resourceSupplyType()["specific"] == "grain") this.assignResourceToDP(gameState,ent); } } } else if (events[i].type == "Create") { // Checking for resources. var evt = events[i]; if (evt.msg && evt.msg.entity) { var ent = gameState.getEntityById(evt.msg.entity); if (ent === undefined) continue; if (ent.resourceSupplyAmount() && ent.owner() == 0) this.assignResourceToDP(gameState,ent); } } } }; // If no specific dropsite, it'll assign to the closest -BaseManager.prototype.assignResourceToDP = function (gameState, supply, specificDP) { +m.BaseManager.prototype.assignResourceToDP = function (gameState, supply, specificDP) { var type = supply.resourceSupplyType()["generic"]; if (type == "treasure") type = supply.resourceSupplyType()["specific"]; if (!specificDP) { var closest = -1; var dist = Math.min(); for (i in this.dropsites) { var dp = gameState.getEntityById(i); - var distance = SquareVectorDistance(supply.position(), dp.position()); + var distance = API3.SquareVectorDistance(supply.position(), dp.position()); if (distance < dist && distance < this.bigRadius[type]) { closest = dp.id(); dist = distance; } } if (closest !== -1) { supply.setMetadata(PlayerID, "linked-dropsite-close", (dist < this.smallRadius[type]) ); supply.setMetadata(PlayerID, "linked-dropsite-nearby", (dist < this.medRadius[type]) ); supply.setMetadata(PlayerID, "linked-dropsite", closest ); supply.setMetadata(PlayerID, "linked-dropsite-dist", +dist); } } // TODO: ought to recount immediatly. } -BaseManager.prototype.initializeDropsite = function (gameState, ent, type) { +m.BaseManager.prototype.initializeDropsite = function (gameState, ent, type) { var count = 0, farCount = 0; var self = this; var resources = gameState.getResourceSupplies(type); // TODO: if we're initing, we should probably remove them anyway. if (self.dropsites[ent.id()] === undefined || self.dropsites[ent.id()][type] === undefined) { resources.filter( function (supply) { //}){ if (!supply.position() || !ent.position()) return; - var distance = SquareVectorDistance(supply.position(), ent.position()); + var distance = API3.SquareVectorDistance(supply.position(), ent.position()); if (supply.getMetadata(PlayerID, "linked-dropsite") == undefined || supply.getMetadata(PlayerID, "linked-dropsite-dist") > distance) { if (distance < self.bigRadius[type]) { supply.setMetadata(PlayerID, "linked-dropsite-close", (distance < self.smallRadius[type]) ); supply.setMetadata(PlayerID, "linked-dropsite-nearby", (distance < self.medRadius[type]) ); supply.setMetadata(PlayerID, "linked-dropsite", ent.id() ); supply.setMetadata(PlayerID, "linked-dropsite-dist", +distance); if(distance < self.smallRadius[type]) count += supply.resourceSupplyAmount(); if (distance < self.medRadius[type]) farCount += supply.resourceSupplyAmount(); } } }); // This one is both for the nearby and the linked - var filter = Filters.byMetadata(PlayerID, "linked-dropsite", ent.id()); + var filter = API3.Filters.byMetadata(PlayerID, "linked-dropsite", ent.id()); var collection = resources.filter(filter); collection.registerUpdates(); - filter = Filters.byMetadata(PlayerID, "linked-dropsite-close",true); + filter = API3.Filters.byMetadata(PlayerID, "linked-dropsite-close",true); var collection2 = collection.filter(filter); collection2.registerUpdates(); - filter = Filters.byMetadata(PlayerID, "linked-dropsite-nearby",true); + filter = API3.Filters.byMetadata(PlayerID, "linked-dropsite-nearby",true); var collection3 = collection.filter(filter); collection3.registerUpdates(); - filter = Filters.byMetadata(PlayerID, "linked-to-dropsite", ent.id()); + filter = API3.Filters.byMetadata(PlayerID, "linked-to-dropsite", ent.id()); var WkCollection = this.workers.filter(filter); WkCollection.registerUpdates(); if (!self.dropsites[ent.id()]) self.dropsites[ent.id()] = {}; self.dropsites[ent.id()][type] = [collection2,collection3, collection, count, farCount, WkCollection]; // TODO: flag us on the SharedScript "type" map. // TODO: get workers on those resources and do something with them. } - if (Config.debug) + if (m.DebugEnabled) { // Make resources glow wildly if (type == "food") { self.dropsites[ent.id()][type][2].forEach(function(ent){ - Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0.5,0,0]}); + Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0.5,0,0]}); }); self.dropsites[ent.id()][type][1].forEach(function(ent){ - Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [2,0,0]}); + Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [2,0,0]}); }); self.dropsites[ent.id()][type][0].forEach(function(ent){ - Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [10,0,0]}); + Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [10,0,0]}); }); } if (type == "wood") { self.dropsites[ent.id()][type][2].forEach(function(ent){ - Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,0.5,0]}); + Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,0.5,0]}); }); self.dropsites[ent.id()][type][1].forEach(function(ent){ - Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,2,0]}); + Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,2,0]}); }); self.dropsites[ent.id()][type][0].forEach(function(ent){ - Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,10,0]}); + Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,10,0]}); }); } if (type == "stone") { self.dropsites[ent.id()][type][2].forEach(function(ent){ - Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0.5,0.5,0]}); + Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0.5,0.5,0]}); }); self.dropsites[ent.id()][type][1].forEach(function(ent){ - Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [2,2,0]}); + Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [2,2,0]}); }); self.dropsites[ent.id()][type][0].forEach(function(ent){ - Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [10,10,0]}); + Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [10,10,0]}); }); } if (type == "metal") { self.dropsites[ent.id()][type][2].forEach(function(ent){ - Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,0.5,0.5]}); + Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,0.5,0.5]}); }); self.dropsites[ent.id()][type][1].forEach(function(ent){ - Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,2,2]}); + Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,2,2]}); }); self.dropsites[ent.id()][type][0].forEach(function(ent){ - Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,10,10]}); + Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,10,10]}); }); } } }; // completely and "safely" remove a dropsite from our list. // this also removes any linked resource and so on. // TODO: should re-add the resources to another dropsite. -BaseManager.prototype.scrapDropsite = function (gameState, ent) { +m.BaseManager.prototype.scrapDropsite = function (gameState, ent) { if (this.dropsites[ent.id()] === undefined) return true; for (i in this.dropsites[ent.id()]) { var type = i; var dp = this.dropsites[ent.id()][i]; dp[2].forEach(function (supply) { //}){ supply.deleteMetadata(PlayerID,"linked-dropsite-nearby"); supply.deleteMetadata(PlayerID,"linked-dropsite-close"); supply.deleteMetadata(PlayerID,"linked-dropsite"); supply.deleteMetadata(PlayerID,"linked-dropsite-dist"); }); dp[5].forEach(function (worker) { worker.deleteMetadata(PlayerID,"linked-to-dropsite"); // TODO: should probably stop the worker or something. }); dp = [undefined, undefined, undefined, 0, 0, undefined]; delete this.dropsites[ent.id()][i]; } this.dropsites[ent.id()] = undefined; delete this.dropsites[ent.id()]; return true; }; // Returns the position of the best place to build a new dropsite for the specified resource -BaseManager.prototype.findBestDropsiteLocation = function(gameState, 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 territory = Map.createTerritoryMap(gameState); + var territory = m.createTerritoryMap(gameState); - var obstructions = Map.createObstructionMap(gameState,this.accessIndex,storeHousePlate); + var obstructions = m.createObstructionMap(gameState,this.accessIndex,storeHousePlate); obstructions.expandInfluences(); // copy the resource map as initialization. - var friendlyTiles = new Map(gameState.sharedScript, gameState.sharedScript.resourceMaps[resource].map, true); + var friendlyTiles = new API3.Map(gameState.sharedScript, gameState.sharedScript.resourceMaps[resource].map, true); - var DPFoundations = gameState.getOwnFoundations().filter(Filters.byType(gameState.applyCiv("foundation|structures/{civ}_storehouse"))); + var DPFoundations = gameState.getOwnFoundations().filter(API3.Filters.byType(gameState.applyCiv("foundation|structures/{civ}_storehouse"))); // 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]; friendlyTiles.map[j] *= 1.5; // 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 (friendlyTiles.map[j] !== 0 && i !== "food") friendlyTiles.map[j] += gameState.sharedScript.resourceMaps[i].map[j]; for (var i in this.dropsites) { var pos = [j%friendlyTiles.width, Math.floor(j/friendlyTiles.width)]; var dpPos = gameState.getEntityById(i).position(); - if (dpPos && SquareVectorDistance(friendlyTiles.gamePosToMapPos(dpPos), pos) < 250) + if (dpPos && API3.SquareVectorDistance(friendlyTiles.gamePosToMapPos(dpPos), pos) < 250) { friendlyTiles.map[j] = 0; continue; - } else if (dpPos && SquareVectorDistance(friendlyTiles.gamePosToMapPos(dpPos), pos) < 450) + } else if (dpPos && API3.SquareVectorDistance(friendlyTiles.gamePosToMapPos(dpPos), pos) < 450) friendlyTiles.map[j] /= 2; } for (var i in DPFoundations._entities) { var pos = [j%friendlyTiles.width, Math.floor(j/friendlyTiles.width)]; var dpPos = gameState.getEntityById(i).position(); - if (dpPos && SquareVectorDistance(friendlyTiles.gamePosToMapPos(dpPos), pos) < 250) + if (dpPos && API3.SquareVectorDistance(friendlyTiles.gamePosToMapPos(dpPos), pos) < 250) friendlyTiles.map[j] = 0; - else if (dpPos && SquareVectorDistance(friendlyTiles.gamePosToMapPos(dpPos), pos) < 450) + else if (dpPos && API3.SquareVectorDistance(friendlyTiles.gamePosToMapPos(dpPos), pos) < 450) friendlyTiles.map[j] /= 2; } } - if (Config.debug) + if (m.DebugEnabled) friendlyTiles.dumpIm("DP_" + resource + "_" + gameState.getTimeElapsed() + ".png"); var best = friendlyTiles.findBestTile(2, obstructions); // try to find a spot to place a DP. var bestIdx = best[0]; // tell the dropsite builder we haven't found anything satisfactory. if (best[1] < 60) return false; var x = ((bestIdx % friendlyTiles.width) + 0.5) * gameState.cellSize; var z = (Math.floor(bestIdx / friendlyTiles.width) + 0.5) * gameState.cellSize; return [x,z]; }; // update the resource level of a dropsite. -BaseManager.prototype.updateDropsite = function (gameState, ent, type) { +m.BaseManager.prototype.updateDropsite = function (gameState, ent, type) { if (this.dropsites[ent.id()] === undefined || this.dropsites[ent.id()][type] === undefined) return undefined; // should initialize it first. var count = 0, farCount = 0; var resources = gameState.getResourceSupplies(type); this.dropsites[ent.id()][type][1].forEach( function (supply) { //}){ farCount += supply.resourceSupplyAmount(); }); this.dropsites[ent.id()][type][0].forEach( function (supply) { //}){ count += supply.resourceSupplyAmount(); }); this.dropsites[ent.id()][type][3] = count; this.dropsites[ent.id()][type][4] = farCount; return true; }; // Updates dropsites. -BaseManager.prototype.updateDropsites = function (gameState) { +m.BaseManager.prototype.updateDropsites = function (gameState) { // for each dropsite, recalculate for (i in this.dropsites) { for (type in this.dropsites[i]) { this.updateDropsite(gameState,gameState.getEntityById(i),type); } } }; // TODO: ought to be cached or something probably // Returns the number of slots available for workers here. // we're assuming Max - 3 for metal/stone mines, and 20 for any dropsite that has wood. // TODO: for wood might want to count the trees too. // TODO: this returns "future" worker capacity, might want to have a current one. -BaseManager.prototype.getWorkerCapacity = function (gameState, type) { +m.BaseManager.prototype.getWorkerCapacity = function (gameState, type) { var count = 0; if (type == "food") return 1000000; // TODO: perhaps return something sensible here. if (type === "stone" || type === "metal") { for (id in this.dropsites) if (this.dropsites[id][type]) this.dropsites[id][type][1].forEach(function (ent) {// }){ if (ent.resourceSupplyAmount() > 500) count += ent.maxGatherers() - 3; }); } else if (type === "wood") { for (id in this.dropsites) if (this.dropsites[id][type] && (this.dropsites[id][type][4]) > 1000) count += Math.min(15, this.dropsites[id][type][4] / 200); } return count; }; // TODO: ought to be cached or something probably // Returns the amount of resource left -BaseManager.prototype.getResourceLevel = function (gameState, type, searchType, threshold) { +m.BaseManager.prototype.getResourceLevel = function (gameState, type, searchType, threshold) { var count = 0; if (searchType == "all") { // return all resources in the base area. - gameState.getResourceSupplies(type).filter(Filters.byTerritory(gameState.ai.HQ.basesMap, this.ID)).forEach( function (ent) { //}){ + gameState.getResourceSupplies(type).filter(API3.Filters.byTerritory(gameState.ai.HQ.basesMap, this.ID)).forEach( function (ent) { //}){ count += ent.resourceSupplyAmount(); }); return count; } if (searchType == "dropsites") { // for each dropsite, recalculate for (i in this.dropsites) if (this.dropsites[i][type] !== undefined) count += this.dropsites[i][type][4]; return count; } if (searchType == "dropsitesClose") { // for each dropsite, recalculate for (i in this.dropsites) if (this.dropsites[i][type] !== undefined) count += this.dropsites[i][type][3]; return count; } if (searchType == "dropsites-dpcount") { var seuil = 800; if (threshold) seuil = threshold; // for each dropsite, recalculate for (i in this.dropsites) if (this.dropsites[i][type] !== undefined) { if (this.dropsites[i][type][4] > seuil) count++; } return count; } return 0; }; // check our resource levels and react accordingly -BaseManager.prototype.checkResourceLevels = function (gameState,queues) { +m.BaseManager.prototype.checkResourceLevels = function (gameState,queues) { for (type in this.willGather) { if (this.willGather[type] === 0) continue; if (type !== "food" && gameState.playedTurn % 10 === 4 && this.getResourceLevel(gameState,type, "all") < 200) this.willGather[type] = 0; // won't gather at all if (this.willGather[type] === 2) continue; var count = this.getResourceLevel(gameState,type, "dropsites"); if (type == "food") { if (!this.isFarming && count < 1600 && queues.field.length === 0) { // tell the queue manager we'll be trying to build fields shortly. - for (var i = 0; i < Config.Economy.initialFields;++i) + for (var i = 0; i < this.Config.Economy.initialFields;++i) { - var plan = new ConstructionPlan(gameState, "structures/{civ}_field", { "base" : this.ID }); + 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 (!this.isFarming && count < 650) { for (i in queues.field.queue) queues.field.queue[i].isGo = function() { return true; }; // start them this.isFarming = true; } if (this.isFarming) { var numFarms = 0; - this.buildings.filter(Filters.byClass("Field")).forEach(function (field) { + this.buildings.filter(API3.Filters.byClass("Field")).forEach(function (field) { if (field.resourceSupplyAmount() > 400) numFarms++; }); var numFd = gameState.countEntitiesByType(gameState.applyCiv("foundation|structures/{civ}_field"), true); numFarms += numFd; numFarms += queues.field.countQueuedUnits(); // let's see if we need to push new farms. if (numFd < 2) if (numFarms < Math.round(this.gatherersByType(gameState, "food").length / 4.6) || numFarms < Math.round(this.workers.length / 15.0)) - queues.field.addItem(new ConstructionPlan(gameState, "structures/{civ}_field", { "base" : this.ID })); + queues.field.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_field", { "base" : this.ID })); // TODO: refine count to only count my base. } } else if (queues.dropsites.length() === 0 && gameState.countFoundationsWithType(gameState.applyCiv("structures/{civ}_storehouse")) === 0) { var wantedDPs = Math.ceil(this.gatherersByType(gameState, type).length / 12.0); var need = wantedDPs - this.getResourceLevel(gameState,type, "dropsites-dpcount",2000); if (need > 0) { var pos = this.findBestDropsiteLocation(gameState, type); if (!pos) { - debug ("Found no right position for a " + type + " dropsite, going into \"noSpot\" mode"); + m.debug ("Found no right position for a " + type + " dropsite, going into \"noSpot\" mode"); this.willGather[type] = 2; // won't build // TODO: tell the HQ we'll be needing a new base for this resource, or tell it we've ran out of resource Z. } else { - debug ("planning new dropsite for " + type); - queues.dropsites.addItem(new ConstructionPlan(gameState, "structures/{civ}_storehouse",{ "base" : this.ID }, 0, -1, pos)); + m.debug ("planning new dropsite for " + type); + queues.dropsites.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_storehouse",{ "base" : this.ID }, 0, -1, pos)); } } } } }; // let's return the estimated gather rates. -BaseManager.prototype.getGatherRates = function(gameState, currentRates) { +m.BaseManager.prototype.getGatherRates = function(gameState, currentRates) { }; -BaseManager.prototype.assignRolelessUnits = function(gameState) { +m.BaseManager.prototype.assignRolelessUnits = function(gameState) { // TODO: make this cleverer. - var roleless = this.units.filter(Filters.not(Filters.byHasMetadata(PlayerID, "role"))); + 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")) { if (ent.hasClass("Cavalry") && !self.isHunting) return; 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. -BaseManager.prototype.setWorkersIdleByPriority = function(gameState){ +m.BaseManager.prototype.setWorkersIdleByPriority = function(gameState){ var self = this; if (gameState.currentPhase() < 2 && gameState.getTimeElapsed() < 360000) return; // not in the first phase or the first 6 minutes. var types = gameState.ai.queueManager.getAvailableResources(gameState); var bestType = ""; var avgOverdraft = 0; for (i in types.types) avgOverdraft += types[types.types[i]]; avgOverdraft /= 4; for (i in types.types) if (types[types.types[i]] > avgOverdraft + 200 || (types[types.types[i]] > avgOverdraft && avgOverdraft > 200)) if (this.gatherersByType(gameState,types.types[i]).length > 0) { // TODO: perhaps change this? var nb = 2; this.gatherersByType(gameState,types.types[i]).forEach( function (ent) { //}){ if (nb > 0) { - //debug ("Moving " +ent.id() + " from " + types.types[i]); + //m.debug ("Moving " +ent.id() + " from " + types.types[i]); nb--; // TODO: might want to direct assign. ent.stopMoving(); ent.setMetadata(PlayerID, "subrole","idle"); } }); } - //debug (currentRates); + //m.debug (currentRates); }; // TODO: work on this. -BaseManager.prototype.reassignIdleWorkers = function(gameState) { +m.BaseManager.prototype.reassignIdleWorkers = function(gameState) { var self = this; // Search for idle workers, and tell them to gather resources based on demand - var filter = Filters.or(Filters.byMetadata(PlayerID,"subrole","idle"), Filters.not(Filters.byHasMetadata(PlayerID,"subrole"))); + 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); if (idleWorkers.length) { idleWorkers.forEach(function(ent) { // Check that the worker isn't garrisoned if (ent.position() === undefined){ return; } if (ent.hasClass("Worker")) { var types = gameState.ai.HQ.pickMostNeededResources(gameState); - //debug ("assigning " +ent.id() + " to " + types[0]); + //m.debug ("assigning " +ent.id() + " to " + types[0]); ent.setMetadata(PlayerID, "subrole", "gatherer"); ent.setMetadata(PlayerID, "gather-type", types[0]); if (gameState.turnCache["gathererAssignementCache-" + types[0]]) gameState.turnCache["gathererAssignementCache-" + types[0]]++; else gameState.turnCache["gathererAssignementCache-" + types[0]] = 1; // Okay let's now check we can actually remain here for that if (self.willGather[types[0]] !== 1) { // TODO: if fail, we should probably pick the second most needed resource. gameState.ai.HQ.switchWorkerBase(gameState, ent, types[0]); } } else { ent.setMetadata(PlayerID, "subrole", "hunter"); } }); } }; -BaseManager.prototype.workersBySubrole = function(gameState, subrole) { - return gameState.updatingCollection("subrole-" + subrole +"-base-" + this.ID, Filters.byMetadata(PlayerID, "subrole", subrole), this.workers, true); +m.BaseManager.prototype.workersBySubrole = function(gameState, subrole) { + return gameState.updatingCollection("subrole-" + subrole +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "subrole", subrole), this.workers, true); }; -BaseManager.prototype.gatherersByType = function(gameState, type) { - return gameState.updatingCollection("workers-gathering-" + type +"-base-" + this.ID, Filters.byMetadata(PlayerID, "gather-type", type), this.workersBySubrole(gameState, "gatherer")); +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. -BaseManager.prototype.pickBuilders = function(gameState, number) { - var collec = new EntityCollection(gameState.sharedScript); +m.BaseManager.prototype.pickBuilders = function(gameState, number) { + var collec = new API3.EntityCollection(gameState.sharedScript); // TODO: choose better. - var workers = this.workers.filter(Filters.not(Filters.byClass("Cavalry"))).toEntityArray(); + var workers = this.workers.filter(API3.Filters.not(API3.Filters.byClass("Cavalry"))).toEntityArray(); workers.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,"plan") != undefined) vala = -100; if (b.getMetadata(PlayerID,"plan") != undefined) valb = -100; return a < b }); for (var i = 0; i < number; ++i) { workers[i].stopMoving(); workers[i].setMetadata(PlayerID, "subrole","idle"); collec.addEnt(workers[i]); } return collec; } -BaseManager.prototype.assignToFoundations = function(gameState, noRepair) { +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; - var foundations = this.buildings.filter(Filters.and(Filters.isFoundation(),Filters.not(Filters.byClass("Field")))).toEntityArray(); + 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.needsRepair() && ent.getMetadata(PlayerID, "plan") == undefined) { return true; } return false; }).toEntityArray(); // Check if nothing to build if (!foundations.length && !damagedBuildings.length){ return; } - var workers = this.workers.filter(Filters.not(Filters.byClass("Cavalry"))); + 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(Filters.isIdle()); + 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(Filters.and(Filters.isFoundation(), Filters.byMetadata(PlayerID, "baseAnchor", true))).length !== 0) + 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(Filters.byMetadata(PlayerID, "baseAnchor", true)).toEntityArray(); + 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.15); if (this.constructing == true && maxTotalBuilders < 15) maxTotalBuilders = 15; for (var i in foundations) { var target = foundations[i]; // Removed: sometimes the AI would not notice it has empty unbuilt fields //if (target._template.BuildRestrictions.Category === "Field") // continue; // we do not build fields var assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length; - var targetNB = Config.Economy.targetNumBuilders; // TODO: dynamic that. + var targetNB = this.Config.Economy.targetNumBuilders; // TODO: dynamic that. if (target.hasClass("CivCentre") || target.buildTime() > 150 || target.hasClass("House")) targetNB *= 2; 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() && SquareVectorDistance(ent.position(), target.position()) < 10000 && assigned + addedToThis < targetNB) + 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); }); var nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), targetNB - assigned - addedToThis); nearestNonBuilders.forEach(function(ent) { addedWorkers++; addedToThis++; 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 = Map.createTerritoryMap(gameState); + 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 < this.targetNumBuilders/3) { if (builderWorkers.length + addedWorkers < this.targetNumBuilders*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(), this.targetNumBuilders/3 - assigned); nearestNonBuilders.forEach(function(ent) { ent.stopMoving(); addedWorkers++; ent.setMetadata(PlayerID, "subrole", "builder"); ent.setMetadata(PlayerID, "target-foundation", target.id()); }); } } } }; -BaseManager.prototype.update = function(gameState, queues, events) { +m.BaseManager.prototype.update = function(gameState, queues, events) { Engine.ProfileStart("Base update - base " + this.ID); var self = this; this.updateDropsites(gameState); this.checkResourceLevels(gameState, queues); Engine.ProfileStart("Assign builders"); this.assignToFoundations(gameState); Engine.ProfileStop() // if (!this.constructing) // { if (gameState.ai.playedTurn % 2 === 0) this.setWorkersIdleByPriority(gameState); this.assignRolelessUnits(gameState); /*Engine.ProfileStart("Swap Workers"); var gathererGroups = {}; gameState.getOwnEntitiesByRole("worker").forEach(function(ent){ }){ if (ent.hasClass("Cavalry")) return; var key = uneval(ent.resourceGatherRates()); if (!gathererGroups[key]){ gathererGroups[key] = {"food": [], "wood": [], "metal": [], "stone": []}; } if (ent.getMetadata(PlayerID, "gather-type") in gathererGroups[key]){ gathererGroups[key][ent.getMetadata(PlayerID, "gather-type")].push(ent); } }); for (var i in gathererGroups){ for (var j in gathererGroups){ var a = eval(i); var b = eval(j); if (a !== undefined && b !== undefined) if (a["food.grain"]/b["food.grain"] > a["wood.tree"]/b["wood.tree"] && gathererGroups[i]["wood"].length > 0 && gathererGroups[j]["food"].length > 0){ for (var k = 0; k < Math.min(gathererGroups[i]["wood"].length, gathererGroups[j]["food"].length); k++){ gathererGroups[i]["wood"][k].setMetadata(PlayerID, "gather-type", "food"); gathererGroups[j]["food"][k].setMetadata(PlayerID, "gather-type", "wood"); } } } } Engine.ProfileStop();*/ // should probably be last to avoid reallocations of units that would have done stuffs otherwise. Engine.ProfileStart("Assigning Workers"); this.reassignIdleWorkers(gameState); Engine.ProfileStop(); // } // TODO: do this incrementally a la defence.js Engine.ProfileStart("Run Workers"); this.workers.forEach(function(ent) { if (!ent.getMetadata(PlayerID, "worker-object")) - ent.setMetadata(PlayerID, "worker-object", new Worker(ent)); + ent.setMetadata(PlayerID, "worker-object", new m.Worker(ent)); ent.getMetadata(PlayerID, "worker-object").update(self, gameState); }); Engine.ProfileStop(); Engine.ProfileStop(); }; + +return m; + +}(AEGIS); Index: ps/trunk/binaries/data/mods/public/simulation/ai/aegis/config.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/aegis/config.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/aegis/config.js (revision 14441) @@ -1,115 +1,120 @@ -// Baseconfig is the highest difficulty. -var baseConfig = { - "Military" : { +var AEGIS = function(m) +{ + +m.Config = function() { + this.difficulty = 2; // 0 is sandbox, 1 is easy, 2 is medium, 3 is hard, 4 is very hard. + // overriden by the GUI + + this.Military = { "fortressLapseTime" : 540, // Time to wait between building 2 fortresses "defenceBuildingTime" : 600, // Time to wait before building towers or fortresses "attackPlansStartTime" : 0, // time to wait before attacking. Start as soon as possible (first barracks) "techStartTime" : 120, // time to wait before teching. Will only start after town phase so it's irrelevant. "popForBarracks1" : 15, "popForBarracks2" : 95, "timeForBlacksmith" : 900, - }, - "Economy" : { + }; + this.Economy = { "townPhase" : 180, // time to start trying to reach town phase (might be a while after. Still need the requirements + ress ) "cityPhase" : 840, // time to start trying to reach city phase "popForMarket" : 80, "popForFarmstead" : 45, "dockStartTime" : 240, // Time to wait before building the dock "techStartTime" : 0, // time to wait before teching. "targetNumBuilders" : 1.5, // Base number of builders per foundation. "femaleRatio" : 0.4, // percent of females among the workforce. "initialFields" : 2 - }, + }; // Note: attack settings are set directly in attack_plan.js // defence - "Defence" : { + this.Defence = + { "defenceRatio" : 5, // see defence.js for more info. "armyCompactSize" : 700, // squared. Half-diameter of an army. "armyBreakawaySize" : 900 // squared. - }, + }; // military - "buildings" : { + this.buildings = + { "moderate" : { "default" : [ "structures/{civ}_barracks" ] }, "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" ], "maur" : [ "structures/{civ}_elephant_stables"] }, "fort" : { "default" : [ "structures/{civ}_fortress" ], "celt" : [ "structures/{civ}_fortress_b", "structures/{civ}_fortress_g" ] } - }, + }; // qbot - "priorities" : { // Note these are dynamic, you are only setting the initial values + this.priorities = + { // Note these are dynamic, you are only setting the initial values "house" : 350, "villager" : 40, "citizenSoldier" : 60, "ships" : 70, "economicBuilding" : 90, "dropsites" : 120, "field" : 500, "militaryBuilding" : 110, "defenceBuilding" : 70, "majorTech" : 700, "minorTech" : 50, "civilCentre" : 400 - }, - "difficulty" : 2, // 0 is sandbox, 1 is easy, 2 is medium, 3 is hard, 4 is very hard. - "debug" : false + }; }; -var Config = { - "debug": false, - "difficulty" : 2, // overriden by the GUI - updateDifficulty: function(difficulty) +//Config.prototype = new BaseConfig(); + +m.Config.prototype.updateDifficulty = function(difficulty) +{ + this.difficulty = difficulty; + // changing settings based on difficulty. + if (this.difficulty === 1) { - Config.difficulty = difficulty; - // changing settings based on difficulty. - if (Config.difficulty === 1) - { - Config.Military.defenceBuildingTime = 1200; - Config.Military.attackPlansStartTime = 960; - Config.Military.popForBarracks1 = 35; - Config.Military.popForBarracks2 = 150; // shouldn't reach it - Config.Military.popForBlacksmith = 150; // shouldn't reach it - - Config.Economy.cityPhase = 1800; - Config.Economy.popForMarket = 80; - Config.Economy.techStartTime = 600; - Config.Economy.femaleRatio = 0.6; - Config.Economy.initialFields = 1; - // Config.Economy.targetNumWorkers will be set by AI scripts. - } - else if (Config.difficulty === 0) - { - Config.Military.defenceBuildingTime = 450; - Config.Military.attackPlansStartTime = 9600000; // never - Config.Military.popForBarracks1 = 60; - Config.Military.popForBarracks2 = 150; // shouldn't reach it - Config.Military.popForBlacksmith = 150; // shouldn't reach it - - Config.Economy.cityPhase = 240000; - Config.Economy.popForMarket = 200; - Config.Economy.techStartTime = 1800; - Config.Economy.femaleRatio = 0.2; - Config.Economy.initialFields = 1; - // Config.Economy.targetNumWorkers will be set by AI scripts. - } + this.Military.defenceBuildingTime = 1200; + this.Military.attackPlansStartTime = 960; + 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.techStartTime = 600; + this.Economy.femaleRatio = 0.6; + this.Economy.initialFields = 1; + // Config.Economy.targetNumWorkers will be set by AI scripts. + } + else if (this.difficulty === 0) + { + this.Military.defenceBuildingTime = 450; + this.Military.attackPlansStartTime = 9600000; // never + 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.techStartTime = 1800; + this.Economy.femaleRatio = 0.2; + this.Economy.initialFields = 1; + // Config.Economy.targetNumWorkers will be set by AI scripts. } }; -Config.__proto__ = baseConfig; +return m; +}(AEGIS); Index: ps/trunk/binaries/data/mods/public/simulation/ai/aegis/data.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/aegis/data.json (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/aegis/data.json (revision 14441) @@ -1,6 +1,7 @@ { "name": "Aegis Bot", "description": "Wraitii's improvement of qBot. It is more reliable and generally a better player. Note that it doesn't support saved games yet, and there may be other bugs. Please report issues to Wildfire Games (see the link in the main menu).", + "moduleName" : "AEGIS", "constructor": "AegisBot", "useShared": true } Index: ps/trunk/binaries/data/mods/public/simulation/ai/aegis/defence.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/aegis/defence.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/aegis/defence.js (revision 14441) @@ -1,843 +1,849 @@ +var AEGIS = function(m) +{ + // directly imported from Marilyn, with slight modifications to work with qBot. -function Defence(){ +m.Defence = function(Config){ this.defenceRatio = Config.Defence.defenceRatio;// How many defenders we want per attacker. Need to balance fewer losses vs. lost economy // note: the choice should be a no-brainer most of the time: better deflect the attack. // This is also sometimes forcebly overcome by the defense manager. this.armyCompactSize = Config.Defence.armyCompactSize; // a bit more than 40 wide in diameter this.armyBreakawaySize = Config.Defence.armyBreakawaySize; // a bit more than 45 wide in diameter this.totalAttackNb = 0; // used for attack IDs this.attacks = []; this.toKill = []; // keeps a list of targeted enemy at instant T this.enemyArmy = {}; // array of players, storing for each an array of armies. this.attackerCache = {}; this.listOfEnemies = {}; this.listedEnemyCollection = null; // entity collection of this.listOfEnemies // Some Stats this.nbAttackers = 0; this.nbDefenders = 0; // Caching variables this.totalArmyNB = 0; this.enemyUnits = {}; this.enemyArmyLoop = {}; // boolean 0/1 that's for optimization this.attackerCacheLoopIndicator = 0; // this is a list of units to kill. They should be gaia animals, or lonely units. Works the same as listOfEnemies, ie an entityColelction which I'll have to cleanup this.listOfWantedUnits = {}; this.WantedUnitsAttacker = {}; // same as attackerCache. this.defenders = null; this.idleDefs = null; } // DO NOTE: the Defence manager, when it calls for Defence, makes the military manager go into "Defence mode"... This makes it not update any plan that's started or not. // This allows the Defence manager to take units from the plans for Defence. // Defcon levels // 5: no danger whatsoever detected // 4: a few enemy units are being dealt with, but nothing too dangerous. // 3: A reasonnably sized enemy army is being dealt with, but it should not be a problem. // 2: A big enemy army is in the base, but we are not outnumbered // 1: Huge army in the base, outnumbering us. -Defence.prototype.update = function(gameState, events, HQ){ +m.Defence.prototype.update = function(gameState, events, HQ){ Engine.ProfileStart("Defence Manager"); // a litlle cache-ing if (!this.idleDefs) { - var filter = Filters.and(Filters.byMetadata(PlayerID, "role", "defence"), Filters.isIdle()); + var filter = API3.Filters.and(API3.Filters.byMetadata(PlayerID, "role", "defence"), API3.Filters.isIdle()); this.idleDefs = gameState.getOwnEntities().filter(filter); this.idleDefs.registerUpdates(); } if (!this.defenders) { - var filter = Filters.byMetadata(PlayerID, "role", "defence"); + var filter = API3.Filters.byMetadata(PlayerID, "role", "defence"); this.defenders = gameState.getOwnEntities().filter(filter); this.defenders.registerUpdates(); } /*if (!this.listedEnemyCollection) { - var filter = Filters.byMetadata(PlayerID, "listed-enemy", true); + var filter = API3.Filters.byMetadata(PlayerID, "listed-enemy", true); this.listedEnemyCollection = gameState.getEnemyEntities().filter(filter); this.listedEnemyCollection.registerUpdates(); } - this.myBuildings = gameState.getOwnEntities().filter(Filters.byClass("Structure")).toEntityArray(); - this.myUnits = gameState.getOwnEntities().filter(Filters.byClass("Unit")); + this.myBuildings = gameState.getOwnEntities().filter(API3.Filters.byClass("Structure")).toEntityArray(); + this.myUnits = gameState.getOwnEntities().filter(API3.Filters.byClass("Unit")); */ - var filter = Filters.and(Filters.byClassesOr(["CitizenSoldier", "Hero", "Champion", "Siege"]), Filters.byOwner(PlayerID)); + var filter = API3.Filters.and(API3.Filters.byClassesOr(["CitizenSoldier", "Hero", "Champion", "Siege"]), API3.Filters.byOwner(PlayerID)); this.myUnits = gameState.updatingGlobalCollection("player-" +PlayerID + "-soldiers", filter); - filter = Filters.and(Filters.byClass("Structure"), Filters.byOwner(PlayerID)); + filter = API3.Filters.and(API3.Filters.byClass("Structure"), API3.Filters.byOwner(PlayerID)); this.myBuildings = gameState.updatingGlobalCollection("player-" +PlayerID + "-structures", filter); - this.territoryMap = Map.createTerritoryMap(gameState); // used by many func + this.territoryMap = m.createTerritoryMap(gameState); // used by many func // First step: we deal with enemy armies, those are the highest priority. this.defendFromEnemies(gameState, events, HQ); // second step: we loop through messages, and sort things as needed (dangerous buildings, attack by animals, ships, lone units, whatever). // TODO : a lot. this.MessageProcess(gameState,events,HQ); this.DealWithWantedUnits(gameState,events,HQ); /* var self = this; // putting unneeded units at rest this.idleDefs.forEach(function(ent) { if (ent.getMetadata(PlayerID, "formerrole")) ent.setMetadata(PlayerID, "role", ent.getMetadata(PlayerID, "formerrole") ); else ent.setMetadata(PlayerID, "role", "worker"); ent.setMetadata(PlayerID, "subrole", undefined); self.nbDefenders--; });*/ Engine.ProfileStop(); return; }; /* // returns armies that are still seen as dangerous (in the LOS of any of my buildings for now) Defence.prototype.reevaluateDangerousArmies = function(gameState, armies) { var stillDangerousArmies = {}; for (var i in armies) { var pos = armies[i].getCentrePosition(); if (pos === undefined) if (+this.territoryMap.point(pos) - 64 === +PlayerID) { stillDangerousArmies[i] = armies[i]; continue; } for (var o in this.myBuildings) { // if the armies out of my buildings LOS (with a little more, because we're cheating right now and big armies could go undetected) - if (inRange(pos, this.myBuildings[o].position(),this.myBuildings[o].visionRange()*this.myBuildings[o].visionRange() + 2500)) { + if (m.inRange(pos, this.myBuildings[o].position(),this.myBuildings[o].visionRange()*this.myBuildings[o].visionRange() + 2500)) { stillDangerousArmies[i] = armies[i]; break; } } } return stillDangerousArmies; } // returns armies we now see as dangerous, ie in my territory Defence.prototype.evaluateArmies = function(gameState, armies) { var DangerousArmies = {}; for (var i in armies) { if (armies[i].getCentrePosition() && +this.territoryMap.point(armies[i].getCentrePosition()) - 64 === +PlayerID) { DangerousArmies[i] = armies[i]; } } return DangerousArmies; }*/ // Incorporates an entity in an army. If no army fits, it creates a new one around this one. // an army is basically an entity collection. -Defence.prototype.armify = function(gameState, entity, HQ, minNBForArmy) { +m.Defence.prototype.armify = function(gameState, entity, HQ, minNBForArmy) { if (entity.position() === undefined) return; if (this.enemyArmy[entity.owner()] === undefined) { this.enemyArmy[entity.owner()] = {}; } else { for (var armyIndex in this.enemyArmy[entity.owner()]) { var army = this.enemyArmy[entity.owner()][armyIndex]; if (army.getCentrePosition() === undefined) { } else { - if (SquareVectorDistance(army.getCentrePosition(), entity.position()) < this.armyCompactSize) + if (API3.SquareVectorDistance(army.getCentrePosition(), entity.position()) < this.armyCompactSize) { entity.setMetadata(PlayerID, "inArmy", armyIndex); army.addEnt(entity); return; } } } } if (HQ) { var self = this; - var close = HQ.enemyWatchers[entity.owner()].enemySoldiers.filter(Filters.byDistance(entity.position(), self.armyCompactSize)); + var close = HQ.enemyWatchers[entity.owner()].enemySoldiers.filter(API3.Filters.byDistance(entity.position(), self.armyCompactSize)); if (!minNBForArmy || close.length >= minNBForArmy) { // if we're here, we need to create an army for it, and freeze it to make sure no unit will be added automatically - var newArmy = new EntityCollection(gameState.sharedScript, {}, [Filters.byOwner(entity.owner())]); + var newArmy = new API3.EntityCollection(gameState.sharedScript, {}, [API3.Filters.byOwner(entity.owner())]); newArmy.addEnt(entity); newArmy.freeze(); newArmy.registerUpdates(); entity.setMetadata(PlayerID, "inArmy", this.totalArmyNB); this.enemyArmy[entity.owner()][this.totalArmyNB] = newArmy; close.forEach(function (ent) { //}){ if (ent.position() !== undefined && self.reevaluateEntity(gameState, ent)) { ent.setMetadata(PlayerID, "inArmy", self.totalArmyNB); self.enemyArmy[ent.owner()][self.totalArmyNB].addEnt(ent); } }); this.totalArmyNB++; } } else { // if we're here, we need to create an army for it, and freeze it to make sure no unit will be added automatically - var newArmy = new EntityCollection(gameState.sharedScript, {}, [Filters.byOwner(entity.owner())]); + var newArmy = new API3.EntityCollection(gameState.sharedScript, {}, [API3.Filters.byOwner(entity.owner())]); newArmy.addEnt(entity); newArmy.freeze(); newArmy.registerUpdates(); entity.setMetadata(PlayerID, "inArmy", this.totalArmyNB); this.enemyArmy[entity.owner()][this.totalArmyNB] = newArmy; this.totalArmyNB++; } return; } // Returns if a unit should be seen as dangerous or not. -Defence.prototype.evaluateRawEntity = function(gameState, entity) { +m.Defence.prototype.evaluateRawEntity = function(gameState, entity) { if (entity.position && +this.territoryMap.point(entity.position) - 64 === +PlayerID && entity._template.Attack !== undefined) return true; return false; } -Defence.prototype.evaluateEntity = function(gameState, entity) { +m.Defence.prototype.evaluateEntity = function(gameState, entity) { if (!entity.position()) return false; if (this.territoryMap.point(entity.position()) - 64 === entity.owner() || entity.attackTypes() === undefined) return false; for (var i in this.myBuildings._entities) { if (!this.myBuildings._entities[i].hasClass("ConquestCritical")) continue; - if (SquareVectorDistance(this.myBuildings._entities[i].position(), entity.position()) < 6000) + if (API3.SquareVectorDistance(this.myBuildings._entities[i].position(), entity.position()) < 6000) return true; } return false; } // returns false if the unit is in its territory -Defence.prototype.reevaluateEntity = function(gameState, entity) { +m.Defence.prototype.reevaluateEntity = function(gameState, entity) { if ( (entity.position() && +this.territoryMap.point(entity.position()) - 64 === +entity.owner()) || entity.attackTypes() === undefined) return false; return true; } // This deals with incoming enemy armies, setting the defcon if needed. It will take new soldiers, and assign them to attack // TODO: still is still pretty dumb, it could use improvements. -Defence.prototype.defendFromEnemies = function(gameState, events, HQ) { +m.Defence.prototype.defendFromEnemies = function(gameState, events, HQ) { var self = this; // New, faster system will loop for enemy soldiers, and also females on occasions ( TODO ) // if a dangerous unit is found, it will check for neighbors and make them into an "army", an entityCollection // > updated against owner, for the day when I throw healers in the deal. // armies are checked against each other now and then to see if they should be merged, and units in armies are checked to see if they should be taken away from the army. // We keep a list of idle defenders. For any new attacker, we'll check if we have any idle defender available, and if not, we assign available units. // At the end of each turn, if we still have idle defenders, we either assign them to neighboring units, or we release them. var nbOfAttackers = 0; // actually new attackers. var newEnemies = []; // clean up using events. for each(var evt in events) { if (evt.type == "Destroy") { if (this.listOfEnemies[evt.msg.entity] !== undefined) { if (this.attackerCache[evt.msg.entity] !== undefined) { this.attackerCache[evt.msg.entity].forEach(function(ent) { ent.stopMoving(); }); delete self.attackerCache[evt.msg.entity]; } delete this.listOfEnemies[evt.msg.entity]; this.nbAttackers--; } else if (evt.msg.entityObj && evt.msg.entityObj.owner() === PlayerID && evt.msg.metadata[PlayerID] && evt.msg.metadata[PlayerID]["role"] && evt.msg.metadata[PlayerID]["role"] === "defence") { // lost a brave man there. this.nbDefenders--; } } } // Optimizations: this will slowly iterate over all units (saved at an instant T) and all armies. // It'll add new units if they are now dangerous and were not before // It'll also deal with cleanup of armies. // When it's finished it'll start over. for (var enemyID in this.enemyArmy) { //this.enemyUnits[enemyID] = HQ.enemyWatchers[enemyID].getAllEnemySoldiers(); if (this.enemyUnits[enemyID] === undefined || this.enemyUnits[enemyID].length === 0) { this.enemyUnits[enemyID] = HQ.enemyWatchers[enemyID].enemySoldiers.toEntityArray(); } else { // we have some units still to check in this array. Check 15 (TODO: DIFFLEVEL) // Note: given the way memory works, if the entity has been recently deleted, its reference may still exist. // and this.enemyUnits[enemyID][0] may still point to that reference, "reviving" the unit. // So we've got to make sure it's not supposed to be dead. for (var check = 0; check < 20; check++) { if (this.enemyUnits[enemyID].length > 0 && gameState.getEntityById(this.enemyUnits[enemyID][0].id()) !== undefined) { if (this.enemyUnits[enemyID][0].getMetadata(PlayerID, "inArmy") !== undefined) { this.enemyUnits[enemyID].splice(0,1); } else { var dangerous = this.evaluateEntity(gameState, this.enemyUnits[enemyID][0]); if (dangerous) this.armify(gameState, this.enemyUnits[enemyID][0], HQ,2); this.enemyUnits[enemyID].splice(0,1); } } else if (this.enemyUnits[enemyID].length > 0 && gameState.getEntityById(this.enemyUnits[enemyID][0].id()) === undefined) { this.enemyUnits[enemyID].splice(0,1); } } } // okay then we'll check one of the armies // creating the array to iterate over. if (this.enemyArmyLoop[enemyID] === undefined || this.enemyArmyLoop[enemyID].length === 0) { this.enemyArmyLoop[enemyID] = []; for (var i in this.enemyArmy[enemyID]) this.enemyArmyLoop[enemyID].push([this.enemyArmy[enemyID][i],i]); } // and now we check the last known army. if (this.enemyArmyLoop[enemyID].length !== 0) { var army = this.enemyArmyLoop[enemyID][0][0]; var position = army.getCentrePosition(); if (!position) { var index = this.enemyArmyLoop[enemyID][0][1]; delete this.enemyArmy[enemyID][index]; this.enemyArmyLoop[enemyID].splice(0,1); } else { army.forEach(function (ent) { //}){ // check if the unit is a breakaway - if (ent.position() && SquareVectorDistance(position, ent.position()) > self.armyBreakawaySize) + if (ent.position() && API3.SquareVectorDistance(position, ent.position()) > self.armyBreakawaySize) { ent.setMetadata(PlayerID, "inArmy", undefined); army.removeEnt(ent); if (self.evaluateEntity(gameState,ent)) self.armify(gameState,ent); } else { // check if we have registered that unit already. if (self.listOfEnemies[ent.id()] === undefined) { - self.listOfEnemies[ent.id()] = new EntityCollection(gameState.sharedScript, {}, [Filters.byOwner(ent.owner())]); + self.listOfEnemies[ent.id()] = new API3.EntityCollection(gameState.sharedScript, {}, [API3.Filters.byOwner(ent.owner())]); self.listOfEnemies[ent.id()].freeze(); self.listOfEnemies[ent.id()].addEnt(ent); self.listOfEnemies[ent.id()].registerUpdates(); - self.attackerCache[ent.id()] = self.myUnits.filter(Filters.byTargetedEntity(ent.id())); + self.attackerCache[ent.id()] = self.myUnits.filter(API3.Filters.byTargetedEntity(ent.id())); self.attackerCache[ent.id()].registerUpdates(); nbOfAttackers++; self.nbAttackers++; newEnemies.push(ent); } else if (self.attackerCache[ent.id()] === undefined || self.attackerCache[ent.id()].length == 0) { nbOfAttackers++; newEnemies.push(ent); } else if (!self.reevaluateEntity(gameState,ent)) { ent.setMetadata(PlayerID, "inArmy", undefined); army.removeEnt(ent); if (self.attackerCache[ent.id()] !== undefined) { self.attackerCache[ent.id()].forEach(function(ent) { ent.stopMoving(); }); delete self.attackerCache[ent.id()]; delete self.listOfEnemies[ent.id()]; self.nbAttackers--; } } } }); // TODO: check if the army itself is not dangerous anymore. this.enemyArmyLoop[enemyID].splice(0,1); } } // okay so now the army update is done. } // Reordering attack because the pathfinder is for now not dynamically updated for (var o in this.attackerCache) { if ((this.attackerCacheLoopIndicator + o) % 2 === 0) { this.attackerCache[o].forEach(function (ent) { var attackPos = gameState.getEntityById(+o).position() if (attackPos) ent.attackMove(attackPos[0],attackPos[1]); ent.setStance("aggressive"); }); } } this.attackerCacheLoopIndicator++; this.attackerCacheLoopIndicator = this.attackerCacheLoopIndicator % 2; if (this.nbAttackers === 0 && this.nbDefenders === 0) { // Release all our units - this.myUnits.filter(Filters.byMetadata(PlayerID, "role","defence")).forEach(function (defender) { //}){ + this.myUnits.filter(API3.Filters.byMetadata(PlayerID, "role","defence")).forEach(function (defender) { //}){ defender.stopMoving(); if (defender.getMetadata(PlayerID, "formerrole")) defender.setMetadata(PlayerID, "role", defender.getMetadata(PlayerID, "formerrole") ); else defender.setMetadata(PlayerID, "role", "worker"); defender.setMetadata(PlayerID, "subrole", undefined); self.nbDefenders--; }); HQ.ungarrisonAll(gameState); HQ.unpauseAllPlans(gameState); return; } else if (this.nbAttackers === 0 && this.nbDefenders !== 0) { // Release all our units - this.myUnits.filter(Filters.byMetadata(PlayerID, "role","defence")).forEach(function (defender) { //}){ + this.myUnits.filter(API3.Filters.byMetadata(PlayerID, "role","defence")).forEach(function (defender) { //}){ defender.stopMoving(); if (defender.getMetadata(PlayerID, "formerrole")) defender.setMetadata(PlayerID, "role", defender.getMetadata(PlayerID, "formerrole") ); else defender.setMetadata(PlayerID, "role", "worker"); defender.setMetadata(PlayerID, "subrole", undefined); self.nbDefenders--; }); HQ.ungarrisonAll(gameState); HQ.unpauseAllPlans(gameState); return; } if ( (this.nbDefenders < 4 && this.nbAttackers >= 5) || this.nbDefenders === 0) { HQ.ungarrisonAll(gameState); } - //debug ("total number of attackers:"+ this.nbAttackers); - //debug ("total number of defenders:"+ this.nbDefenders); + //m.debug ("total number of attackers:"+ this.nbAttackers); + //m.debug ("total number of defenders:"+ this.nbDefenders); // If I'm here, I have a list of enemy units, and a list of my units attacking it (in absolute terms, I could use a list of any unit attacking it). // now I'll list my idle defenders, then my idle soldiers that could defend. // and then I'll assign my units. // and then rock on. if (this.nbAttackers > 15){ gameState.setDefcon(3); } else if (this.nbAttackers > 5){ gameState.setDefcon(4); } // we're having too many. Release those that attack units already dealt with, or idle ones. - if (this.myUnits.filter(Filters.byMetadata(PlayerID, "role","defence")).length > nbOfAttackers*this.defenceRatio*1.2) { - this.myUnits.filter(Filters.byMetadata(PlayerID, "role","defence")).forEach(function (defender) { //}){ + if (this.myUnits.filter(API3.Filters.byMetadata(PlayerID, "role","defence")).length > nbOfAttackers*this.defenceRatio*1.2) { + this.myUnits.filter(API3.Filters.byMetadata(PlayerID, "role","defence")).forEach(function (defender) { //}){ if ( defender.isIdle() || (defender.unitAIOrderData() && defender.unitAIOrderData()["target"])) { if ( defender.isIdle() || (self.attackerCache[defender.unitAIOrderData()["target"]] && self.attackerCache[defender.unitAIOrderData()["target"]].length > 3)) { // okay release me. defender.stopMoving(); if (defender.getMetadata(PlayerID, "formerrole")) defender.setMetadata(PlayerID, "role", defender.getMetadata(PlayerID, "formerrole") ); else defender.setMetadata(PlayerID, "role", "worker"); defender.setMetadata(PlayerID, "subrole", undefined); self.nbDefenders--; } } }); } - var nonDefenders = this.myUnits.filter(Filters.or(Filters.not(Filters.byMetadata(PlayerID, "role","defence")),Filters.isIdle())); - nonDefenders = nonDefenders.filter(Filters.not(Filters.byClass("Female"))); - nonDefenders = nonDefenders.filter(Filters.not(Filters.byMetadata(PlayerID, "subrole","attacking"))); + var nonDefenders = this.myUnits.filter(API3.Filters.or(API3.Filters.not(API3.Filters.byMetadata(PlayerID, "role","defence")),API3.Filters.isIdle())); + nonDefenders = nonDefenders.filter(API3.Filters.not(API3.Filters.byClass("Female"))); + nonDefenders = nonDefenders.filter(API3.Filters.not(API3.Filters.byMetadata(PlayerID, "subrole","attacking"))); var defenceRatio = this.defenceRatio; if (newEnemies.length + this.nbAttackers > (this.nbDefenders + nonDefenders.length) * 0.8 && this.nbAttackers > 9) gameState.setDefcon(2); if (newEnemies.length + this.nbAttackers > (this.nbDefenders + nonDefenders.length) * 1.5 && this.nbAttackers > 5) gameState.setDefcon(1); - //debug ("newEnemies.length "+ newEnemies.length); - //debug ("nonDefenders.length "+ nonDefenders.length); + //m.debug ("newEnemies.length "+ newEnemies.length); + //m.debug ("nonDefenders.length "+ nonDefenders.length); if (gameState.defcon() > 3) HQ.unpauseAllPlans(gameState); if ( (nonDefenders.length + this.nbDefenders > newEnemies.length + this.nbAttackers) || this.nbDefenders + nonDefenders.length < 4) { - var buildings = gameState.getOwnEntities().filter(Filters.byCanGarrison()).toEntityArray(); + var buildings = gameState.getOwnEntities().filter(API3.Filters.byCanGarrison()).toEntityArray(); buildings.forEach( function (struct) { if (struct.garrisoned() && struct.garrisoned().length) struct.unloadAll(); }); }; if (newEnemies.length === 0) return; /* if (gameState.defcon() < 2 && (this.nbAttackers-this.nbDefenders) > 15) { HQ.pauseAllPlans(gameState); } else if (gameState.defcon() < 3 && this.nbDefenders === 0 && newEnemies.length === 0) { HQ.ungarrisonAll(gameState); }*/ // A little sorting to target sieges/champions first. newEnemies.sort (function (a,b) { var vala = 1; var valb = 1; if (a.hasClass("Siege")) vala = 10; else if (a.hasClass("Champion") || a.hasClass("Hero")) vala = 5; if (b.hasClass("Siege")) valb = 10; else if (b.hasClass("Champion") || b.hasClass("Hero")) valb = 5; return valb - vala; }); // For each enemy, we'll pick two units. for each (var enemy in newEnemies) { if (nonDefenders.length === 0 || self.nbDefenders >= self.nbAttackers * 1.8) break; // garrisoned. if (enemy.position() === undefined) continue; var assigned = self.attackerCache[enemy.id()].length; var defRatio = defenceRatio; if (enemy.hasClass("Siege")) defRatio *= 1.2; if (assigned >= defRatio) return; // We'll sort through our units that can legitimately attack. var data = []; for (var id in nonDefenders._entities) { var ent = nonDefenders._entities[id]; if (ent.position()) - data.push([id, ent, SquareVectorDistance(enemy.position(), ent.position())]); + data.push([id, ent, API3.SquareVectorDistance(enemy.position(), ent.position())]); } // refine the defenders we want. Since it's the distance squared, it has the effect // of tending to always prefer closer units, though this refinement should change it slighty. data.sort(function (a, b) { var vala = a[2]; var valb = b[2]; // don't defend with siege units unless enemy is also a siege unit. if (a[1].hasClass("Siege") && !enemy.hasClass("Siege")) vala *= 9; if (b[1].hasClass("Siege") && !enemy.hasClass("Siege")) valb *= 9; // If it's a siege unit, We basically ignore units that only deal pierce damage. if (enemy.hasClass("Siege") && a[1].attackStrengths("Melee") !== undefined) vala /= (a[1].attackStrengths("Melee")["hack"] + a[1].attackStrengths("Melee")["crush"]); if (enemy.hasClass("Siege") && b[1].attackStrengths("Melee") !== undefined) valb /= (b[1].attackStrengths("Melee")["hack"] + b[1].attackStrengths("Melee")["crush"]); // If it's a counter, it's better. if (a[1].countersClasses(b[1].classes())) vala *= 0.1; // quite low but remember it's squared distance. if (b[1].countersClasses(a[1].classes())) valb *= 0.1; // If the unit is idle, we prefer. ALso if attack plan. if ((a[1].isIdle() || a[1].getMetadata(PlayerID, "plan") !== undefined) && !a[1].hasClass("Siege")) vala *= 0.15; if ((b[1].isIdle() || b[1].getMetadata(PlayerID, "plan") !== undefined) && !b[1].hasClass("Siege")) valb *= 0.15; return (vala - valb); }); var ret = {}; for each (var val in data.slice(0, Math.min(nonDefenders._length, defRatio - assigned))) ret[val[0]] = val[1]; - var defs = new EntityCollection(nonDefenders._ai, ret); + var defs = new API3.EntityCollection(nonDefenders._ai, ret); // successfully sorted defs.forEach(function (defender) { //}){ if (defender.getMetadata(PlayerID, "plan") != undefined && (gameState.defcon() < 4 || defender.getMetadata(PlayerID,"subrole") == "walking")) HQ.pausePlan(gameState, defender.getMetadata(PlayerID, "plan")); - //debug ("Against " +enemy.id() + " Assigning " + defender.id()); + //m.debug ("Against " +enemy.id() + " Assigning " + defender.id()); if (defender.getMetadata(PlayerID, "role") == "worker" || defender.getMetadata(PlayerID, "role") == "attack") defender.setMetadata(PlayerID, "formerrole", defender.getMetadata(PlayerID, "role")); defender.setMetadata(PlayerID, "role","defence"); defender.setMetadata(PlayerID, "subrole","defending"); var attackPos = enemy.position(); if (attackPos) defender.attackMove(attackPos[0],attackPos[1]); defender.setStance("aggressive"); defender._entity.idle = false; // hack to prevent a bug as informations aren't updated during a turn nonDefenders.updateEnt(defender); assigned++; self.nbDefenders++; }); /*if (gameState.defcon() <= 3) { // let's try to garrison neighboring females. - var buildings = gameState.getOwnEntities().filter(Filters.byCanGarrison()).toEntityArray(); - var females = gameState.getOwnEntities().filter(Filters.byClass("Support")); + var buildings = gameState.getOwnEntities().filter(API3.Filters.byCanGarrison()).toEntityArray(); + var females = gameState.getOwnEntities().filter(API3.Filters.byClass("Support")); var cache = {}; var garrisoned = false; females.forEach( function (ent) { //}){ garrisoned = false; if (ent.position()) { - if (SquareVectorDistance(ent.position(), enemy.position()) < 3500) + if (API3.SquareVectorDistance(ent.position(), enemy.position()) < 3500) { 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) { garrisoned = true; ent.garrison(struct); cache[struct.id()]++; break; } } if (!garrisoned) { ent.flee(enemy); ent.setMetadata(PlayerID,"fleeing", gameState.getTimeElapsed()); } } } }); }*/ } return; } // this processes the attackmessages // So that a unit that gets attacked will not be completely dumb. // warning: huge levels of indentation coming. -Defence.prototype.MessageProcess = function(gameState,events, HQ) { +m.Defence.prototype.MessageProcess = function(gameState,events, HQ) { var self = this; for (var key in events){ var e = events[key]; if (e.type === "Attacked" && e.msg){ if (gameState.isEntityOwn(gameState.getEntityById(e.msg.target))) { var attacker = gameState.getEntityById(e.msg.attacker); var ourUnit = gameState.getEntityById(e.msg.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) { // note: our unit can already by dead by now... We'll then have to rely on the enemy to react. // if we're not on enemy territory var territory = +this.territoryMap.point(attacker.position()) - 64; // we do not consider units that are defenders, and we do not consider units that are part of an attacking attack plan // (attacking attacking plans are dealing with threats on their own). if (ourUnit !== undefined && (ourUnit.getMetadata(PlayerID, "role") == "defence" || ourUnit.getMetadata(PlayerID, "role") == "attack")) continue; // let's check for animals if (attacker.owner() == 0) { // if our unit is still alive, we make it react // in this case we attack. if (ourUnit !== undefined) { if (ourUnit.hasClass("Unit") && !ourUnit.hasClass("Support")) ourUnit.attack(e.msg.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()] = new API3.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()); + var filter = API3.Filters.byTargetedEntity(attacker.id()); this.WantedUnitsAttacker[attacker.id()] = this.myUnits.filter(filter); this.WantedUnitsAttacker[attacker.id()].registerUpdates(); } } // preliminary check: we do not count attacked military units (for sanity for now, TODO). else if ( (territory != attacker.owner() && ourUnit.hasClass("Support")) || (!ourUnit.hasClass("Support") && territory == PlayerID)) { // Also 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)); + var close = gameState.getResourceSupplies("wood").filter(API3.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 "Defence" mode. if (!attacker.hasClass("Female") && !attacker.hasClass("Ship")) { // 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)); + var close = HQ.enemyWatchers[attacker.owner()].enemySoldiers.filter(API3.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 (SquareVectorDistance(position, ent.position()) < self.armyCompactSize) + 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. } } // Defencemanager will deal with them in the next turn. } if (ourUnit && 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 buildings = gameState.getOwnEntities().filter(API3.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.msg.attacker); } } } } } } } } } }; // nice sets of closing brackets, isn't it? // At most, this will put defcon to 4 -Defence.prototype.DealWithWantedUnits = function(gameState, events, HQ) { +m.Defence.prototype.DealWithWantedUnits = function(gameState, events, HQ) { //if (gameState.defcon() < 3) // return; var self = this; var nbOfAttackers = 0; var nbOfDealtWith = 0; // clean up before adding new units (slight speeding up, since new units can't already be dead) for (var i in this.listOfWantedUnits) { if (this.listOfWantedUnits[i].length === 0 || this.listOfEnemies[i] !== undefined) { // unit died/was converted/is already dealt with as part of an army delete this.WantedUnitsAttacker[i]; delete this.listOfWantedUnits[i]; } else { nbOfAttackers++; if (this.WantedUnitsAttacker[i].length > 0) nbOfDealtWith++; } } // note: we can deal with units the way we want because anyway, the Army Defender has already done its task. // If there are still idle defenders here, it's because they aren't needed. // I can also call other units: they're not needed. // Note however that if the defcon level is too high, this won't do anything because it's low priority. // this also won't take units from attack managers if (nbOfAttackers === 0) return; // at most, we'll deal with 3 enemies at once. if (nbOfDealtWith >= 3) return; // dynamic properties are not updated nearly fast enough here so a little caching var addedto = {}; // we send 3 units to each target just to be sure. TODO refine. // we do not use plan units this.idleDefs.forEach(function(ent) { if (nbOfDealtWith < 3 && nbOfAttackers > 0 && ent.getMetadata(PlayerID, "plan") == undefined) for (var o in self.listOfWantedUnits) { if ( (addedto[o] == undefined && self.WantedUnitsAttacker[o].length < 3) || (addedto[o] && self.WantedUnitsAttacker[o].length + addedto[o] < 3)) { if (self.WantedUnitsAttacker[o].length === 0) nbOfDealtWith++; ent.setMetadata(PlayerID, "formerrole", ent.getMetadata(PlayerID, "role")); ent.setMetadata(PlayerID, "role","defence"); ent.setMetadata(PlayerID, "subrole", "defending"); var attackPos = gameState.getEntityById(+o).position(); if (attackPos) ent.attackMove(attackPos[0],attackPos[1]); // TODO: should probably be an else here, unless it really can't happen ent.setStance("aggressive"); if (addedto[o]) addedto[o]++; else addedto[o] = 1; break; } if (self.WantedUnitsAttacker[o].length == 3) nbOfAttackers--; // we hav eenough units, mark this one as being OKAY } }); // still some undealt with attackers, recruit citizen soldiers if (nbOfAttackers > 0 && nbOfDealtWith < 2) { gameState.setDefcon(4); var newSoldiers = gameState.getOwnEntitiesByRole("worker"); newSoldiers.forEach(function(ent) { // If we're not female, we attack if (ent.hasClass("CitizenSoldier")) if (nbOfDealtWith < 3 && nbOfAttackers > 0) for (var o in self.listOfWantedUnits) { if ( (addedto[o] == undefined && self.WantedUnitsAttacker[o].length < 3) || (addedto[o] && self.WantedUnitsAttacker[o].length + addedto[o] < 3)) { if (self.WantedUnitsAttacker[o].length === 0) nbOfDealtWith++; ent.setMetadata(PlayerID, "formerrole", ent.getMetadata(PlayerID, "role")); ent.setMetadata(PlayerID, "role","defence"); ent.setMetadata(PlayerID, "subrole", "defending"); var attackPos = gameState.getEntityById(+o).position(); if (attackPos) ent.attackMove(attackPos[0],attackPos[1]); ent.setStance("aggressive"); if (addedto[o]) addedto[o]++; else addedto[o] = 1; break; } if (self.WantedUnitsAttacker[o].length == 3) nbOfAttackers--; // we hav eenough units, mark this one as being OKAY } }); } return; } + +return m; +}(AEGIS); Index: ps/trunk/binaries/data/mods/public/simulation/ai/aegis/enemy-watcher.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/aegis/enemy-watcher.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/aegis/enemy-watcher.js (revision 14441) @@ -1,215 +1,222 @@ +var AEGIS = function(m) +{ + /* * A class that keeps track of enemy buildings, units, and pretty much anything I can think of (still a LOT TODO here) * Only watches one enemy, you'll need one per enemy. * Note: pretty much unused in the current version. */ -var enemyWatcher = function(gameState, playerToWatch) { +m.enemyWatcher = function(gameState, playerToWatch) { this.watched = playerToWatch; // using global entity collections, shared by any AI that knows the name of this. - var filter = Filters.and(Filters.byClass("Structure"), Filters.byOwner(this.watched)); + var filter = API3.Filters.and(API3.Filters.byClass("Structure"), API3.Filters.byOwner(this.watched)); this.enemyBuildings = gameState.updatingGlobalCollection("player-" +this.watched + "-structures", filter); - filter = Filters.and(Filters.byClass("Unit"), Filters.byOwner(this.watched)); + filter = API3.Filters.and(API3.Filters.byClass("Unit"), API3.Filters.byOwner(this.watched)); this.enemyUnits = gameState.updatingGlobalCollection("player-" +this.watched + "-units", filter); - filter = Filters.and(Filters.byClass("Worker"), Filters.byOwner(this.watched)); + filter = API3.Filters.and(API3.Filters.byClass("Worker"), API3.Filters.byOwner(this.watched)); this.enemyCivilians = gameState.updatingGlobalCollection("player-" +this.watched + "-civilians", filter); - filter = Filters.and(Filters.byClassesOr(["CitizenSoldier", "Hero", "Champion", "Siege"]), Filters.byOwner(this.watched)); + filter = API3.Filters.and(API3.Filters.byClassesOr(["CitizenSoldier", "Hero", "Champion", "Siege"]), API3.Filters.byOwner(this.watched)); this.enemySoldiers = gameState.updatingGlobalCollection("player-" +this.watched + "-soldiers", filter); - filter = Filters.and(Filters.byClass("Worker"), Filters.byOwner(this.watched)); + filter = API3.Filters.and(API3.Filters.byClass("Worker"), API3.Filters.byOwner(this.watched)); this.enemyCivilians = gameState.getEnemyEntities().filter(filter); this.enemyCivilians.registerUpdates(); - filter = Filters.and(Filters.byClassesOr(["CitizenSoldier", "Hero", "Champion", "Siege"]), Filters.byOwner(this.watched)); + filter = API3.Filters.and(API3.Filters.byClassesOr(["CitizenSoldier", "Hero", "Champion", "Siege"]), API3.Filters.byOwner(this.watched)); this.enemySoldiers = gameState.getEnemyEntities().filter(filter); this.enemySoldiers.registerUpdates(); // entity collections too. this.armies = {}; this.enemyBuildingClass = {}; this.totalNBofArmies = 0; // this is an array of integers, refering to "this.armies[ XX ]" this.dangerousArmies = []; }; -enemyWatcher.prototype.getAllEnemySoldiers = function() { +m.enemyWatcher.prototype.getAllEnemySoldiers = function() { return this.enemySoldiers; }; -enemyWatcher.prototype.getAllEnemyBuildings = function() { +m.enemyWatcher.prototype.getAllEnemyBuildings = function() { return this.enemyBuildings; }; -enemyWatcher.prototype.getEnemyBuildings = function(gameState, specialClass, OneTime) { - var filter = Filters.byClass(specialClass); +m.enemyWatcher.prototype.getEnemyBuildings = function(gameState, specialClass, OneTime) { + var filter = API3.Filters.byClass(specialClass); if (OneTime && gameState.getGEC("player-" +this.watched + "-structures-" +specialClass)) return gameState.getGEC("player-" +this.watched + "-structures-" +specialClass); else if (OneTime) return this.enemyBuildings.filter(filter); return gameState.updatingGlobalCollection("player-" +this.watched + "-structures-" +specialClass, filter, gameState.getGEC("player-" +this.watched + "-structures")); }; -enemyWatcher.prototype.getDangerousArmies = function() { +m.enemyWatcher.prototype.getDangerousArmies = function() { var toreturn = {}; for (var i in this.dangerousArmies) toreturn[this.dangerousArmies[i]] = this.armies[this.dangerousArmies[i]]; return toreturn; }; -enemyWatcher.prototype.getSafeArmies = function() { +m.enemyWatcher.prototype.getSafeArmies = function() { var toreturn = {}; for (var i in this.armies) if (this.dangerousArmies.indexOf(i) == -1) toreturn[i] = this.armies[i]; return toreturn; }; -enemyWatcher.prototype.resetDangerousArmies = function() { +m.enemyWatcher.prototype.resetDangerousArmies = function() { this.dangerousArmies = []; }; -enemyWatcher.prototype.setAsDangerous = function(armyID) { +m.enemyWatcher.prototype.setAsDangerous = function(armyID) { if (this.dangerousArmies.indexOf(armyID) === -1) this.dangerousArmies.push(armyID); }; -enemyWatcher.prototype.isDangerous = function(armyID) { +m.enemyWatcher.prototype.isDangerous = function(armyID) { if (this.dangerousArmies.indexOf(armyID) === -1) return false; return true; }; // returns [id, army] -enemyWatcher.prototype.getArmyFromMember = function(memberID) { +m.enemyWatcher.prototype.getArmyFromMember = function(memberID) { for (var i in this.armies) { if (this.armies[i].toIdArray().indexOf(memberID) !== -1) return [i,this.armies[i]]; } return undefined; }; -enemyWatcher.prototype.isPartOfDangerousArmy = function(memberID) { +m.enemyWatcher.prototype.isPartOfDangerousArmy = function(memberID) { var armyID = this.getArmyFromMember(memberID)[0]; if (this.isDangerous(armyID)) return true; return false; }; -enemyWatcher.prototype.cleanDebug = function() { +m.enemyWatcher.prototype.cleanDebug = function() { for (var armyID in this.armies) { var army = this.armies[armyID]; - debug ("Army " +armyID); - debug (army.length +" members, centered around " +army.getCentrePosition()); + m.debug ("Army " +armyID); + m.debug (army.length +" members, centered around " +army.getCentrePosition()); } } // this will monitor any unmonitored soldier. -enemyWatcher.prototype.detectArmies = function(gameState){ +m.enemyWatcher.prototype.detectArmies = function(gameState){ //this.cleanDebug(); var self = this; if (gameState.ai.playedTurn % 4 === 0) { Engine.ProfileStart("Looking for new soldiers"); // let's loop through unmonitored enemy soldiers this.unmonitoredEnemySoldiers.forEach( function (enemy) { //}){ if (enemy.position() === undefined) return; // this was an unmonitored unit, we do not know any army associated with it. We assign it a new army (we'll merge later if needed) enemy.setMetadata(PlayerID, "monitored","true"); var armyID = gameState.player + "" + self.totalNBofArmies; self.totalNBofArmies++, enemy.setMetadata(PlayerID, "EnemyWatcherArmy",armyID); - var filter = Filters.byMetadata(PlayerID, "EnemyWatcherArmy",armyID); + var filter = API3.Filters.byMetadata(PlayerID, "EnemyWatcherArmy",armyID); var army = self.enemySoldiers.filter(filter); self.armies[armyID] = army; self.armies[armyID].registerUpdates(); self.armies[armyID].length; }); Engine.ProfileStop(); } else if (gameState.ai.playedTurn % 16 === 3) { Engine.ProfileStart("Merging"); this.mergeArmies(); // calls "scrap empty armies" Engine.ProfileStop(); } else if (gameState.ai.playedTurn % 16 === 7) { Engine.ProfileStart("Splitting"); this.splitArmies(gameState); Engine.ProfileStop(); } }; // this will merge any two army who are too close together. The distance for "army" is fairly big. // note: this doesn't actually merge two entity collections... It simply changes the unit metadatas, and will clear the empty entity collection -enemyWatcher.prototype.mergeArmies = function(){ +m.enemyWatcher.prototype.mergeArmies = function(){ for (var army in this.armies) { var firstArmy = this.armies[army]; if (firstArmy.length !== 0) { var firstAPos = firstArmy.getApproximatePosition(4); for (var otherArmy in this.armies) { if (otherArmy !== army && this.armies[otherArmy].length !== 0) { var secondArmy = this.armies[otherArmy]; // we're not self merging, so we check if the two armies are close together - if (inRange(firstAPos,secondArmy.getApproximatePosition(4), 4000 ) ) { + if (m.inRange(firstAPos,secondArmy.getApproximatePosition(4), 4000 ) ) { // okay so we merge the two together // if the other one was dangerous and we weren't, we're now. if (this.dangerousArmies.indexOf(otherArmy) !== -1 && this.dangerousArmies.indexOf(army) === -1) this.dangerousArmies.push(army); secondArmy.forEach( function(ent) { ent.setMetadata(PlayerID, "EnemyWatcherArmy",army); }); } } } } } this.ScrapEmptyArmies(); }; -enemyWatcher.prototype.ScrapEmptyArmies = function(){ +m.enemyWatcher.prototype.ScrapEmptyArmies = function(){ var removelist = []; for (var army in this.armies) { if (this.armies[army].length === 0) { removelist.push(army); // if the army was dangerous, we remove it from the list if (this.dangerousArmies.indexOf(army) !== -1) this.dangerousArmies.splice(this.dangerousArmies.indexOf(army),1); } } for each (var toRemove in removelist) { delete this.armies[toRemove]; } }; // splits any unit too far from the centerposition -enemyWatcher.prototype.splitArmies = function(gameState){ +m.enemyWatcher.prototype.splitArmies = function(gameState){ var self = this; - var map = Map.createTerritoryMap(gameState); + var map = m.createTerritoryMap(gameState); for (var armyID in this.armies) { var army = this.armies[armyID]; var centre = army.getApproximatePosition(4); if (map.getOwner(centre) === gameState.player) continue; army.forEach( function (enemy) { if (enemy.position() === undefined) return; - if (!inRange(enemy.position(),centre, 3500) ) { + if (!m.inRange(enemy.position(),centre, 3500) ) { var newArmyID = gameState.player + "" + self.totalNBofArmies; if (self.dangerousArmies.indexOf(armyID) !== -1) self.dangerousArmies.push(newArmyID); self.totalNBofArmies++, enemy.setMetadata(PlayerID, "EnemyWatcherArmy",newArmyID); - var filter = Filters.byMetadata(PlayerID, "EnemyWatcherArmy",newArmyID); + var filter = API3.Filters.byMetadata(PlayerID, "EnemyWatcherArmy",newArmyID); var newArmy = self.enemySoldiers.filter(filter); self.armies[newArmyID] = newArmy; self.armies[newArmyID].registerUpdates(); self.armies[newArmyID].length; } }); } }; + + +return m; +}(AEGIS); Index: ps/trunk/binaries/data/mods/public/simulation/ai/aegis/entity-extend.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/aegis/entity-extend.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/aegis/entity-extend.js (revision 14441) @@ -1,63 +1,69 @@ +var AEGIS = function(m) +{ + // returns some sort of DPS * health factor. If you specify a class, it'll use the modifiers against that class too. -function getMaxStrength(ent, againstClass) +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; +}(AEGIS); Index: ps/trunk/binaries/data/mods/public/simulation/ai/aegis/entitycollection-extend.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/aegis/entitycollection-extend.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/aegis/entitycollection-extend.js (revision 14441) @@ -1,10 +1,16 @@ -function EntityCollectionFromIds(gameState, idList){ +var AEGIS = 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 EntityCollection(gameState.sharedScript, ents); + return new API3.EntityCollection(gameState.sharedScript, ents); } + +return m; +}(AEGIS); Index: ps/trunk/binaries/data/mods/public/simulation/ai/aegis/headquarters.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/aegis/headquarters.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/aegis/headquarters.js (revision 14441) @@ -1,1257 +1,1269 @@ +var AEGIS = function(m) +{ /* Headquarters * Deal with high level logic for the AI. Most of the interesting stuff gets done here. * Some tasks: -defining RESS needs -BO decisions. > training workers > building stuff (though we'll send that to bases) > researching -picking strategy (specific manager?) -diplomacy (specific manager?) -planning attacks -picking new CC locations. */ -var HQ = function() { - this.targetNumBuilders = Config.Economy.targetNumBuilders; // number of workers we want building stuff +m.HQ = function(Config) { - this.dockStartTime = Config.Economy.dockStartTime * 1000; - this.techStartTime = Config.Economy.techStartTime * 1000; + this.Config = Config; + this.targetNumBuilders = this.Config.Economy.targetNumBuilders; // number of workers we want building stuff + + this.dockStartTime = this.Config.Economy.dockStartTime * 1000; + this.techStartTime = this.Config.Economy.techStartTime * 1000; this.dockFailed = false; // sanity check this.waterMap = false; // set by the aegis.js file. // tell if we can't gather from a resource type for sanity checks. this.outOf = { "food" : false, "wood" : false, "stone" : false, "metal" : false }; this.baseManagers = {}; // this means we'll have about a big third of women, and thus we can maximize resource gathering rates. - this.femaleRatio = Config.Economy.femaleRatio; + this.femaleRatio = this.Config.Economy.femaleRatio; this.fortressStartTime = 0; - this.fortressLapseTime = Config.Military.fortressLapseTime * 1000; - this.defenceBuildingTime = Config.Military.defenceBuildingTime * 1000; - this.attackPlansStartTime = Config.Military.attackPlansStartTime * 1000; - this.defenceManager = new Defence(); + this.fortressLapseTime = this.Config.Military.fortressLapseTime * 1000; + this.defenceBuildingTime = this.Config.Military.defenceBuildingTime * 1000; + this.attackPlansStartTime = this.Config.Military.attackPlansStartTime * 1000; + this.defenceManager = new m.Defence(this.Config); - this.navalManager = new NavalManager(); + this.navalManager = new m.NavalManager(); this.TotalAttackNumber = 0; this.upcomingAttacks = { "CityAttack" : [] }; this.startedAttacks = { "CityAttack" : [] }; }; // More initialisation for stuff that needs the gameState -HQ.prototype.init = function(gameState, events, queues){ +m.HQ.prototype.init = function(gameState, events, queues){ // initialize base map. Each pixel is a base ID, or 0 if none - this.basesMap = new Map(gameState.sharedScript, new Uint8Array(gameState.getMap().data.length)); + this.basesMap = new API3.Map(gameState.sharedScript, new Uint8Array(gameState.getMap().data.length)); this.basesMap.setMaxVal(255); - if (Config.Economy.targetNumWorkers) - this.targetNumWorkers = Config.Economy.targetNumWorkers; + if (this.Config.Economy.targetNumWorkers) + this.targetNumWorkers = this.Config.Economy.targetNumWorkers; else if (this.targetNumWorkers === undefined) - this.targetNumWorkers = Math.max(Math.floor(gameState.getPopulationMax()*(0.2 + Math.min(+(Config.difficulty)*0.125,0.3))), 1); + this.targetNumWorkers = Math.max(Math.floor(gameState.getPopulationMax()*(0.2 + Math.min(+(this.Config.difficulty)*0.125,0.3))), 1); // Let's get our initial situation here. // TODO: improve on this. // TODO: aknowledge bases, assign workers already. - var ents = gameState.getEntities().filter(Filters.byOwner(PlayerID)); + var ents = gameState.getEntities().filter(API3.Filters.byOwner(PlayerID)); var workersNB = 0; var hasScout = false; var treasureAmount = { 'food': 0, 'wood': 0, 'stone': 0, 'metal': 0 }; var hasCC = false; - if (ents.filter(Filters.byClass("CivCentre")).length > 0) + if (ents.filter(API3.Filters.byClass("CivCentre")).length > 0) hasCC = true; - workersNB = ents.filter(Filters.byClass("Worker")).length; - if (ents.filter(Filters.byClass("Cavalry")).length > 0) + workersNB = ents.filter(API3.Filters.byClass("Worker")).length; + if (ents.filter(API3.Filters.byClass("Cavalry")).length > 0) hasScout = true; // tODO: take multiple CCs into account. if (hasCC) { - var CC = ents.filter(Filters.byClass("CivCentre")).toEntityArray()[0]; + var CC = ents.filter(API3.Filters.byClass("CivCentre")).toEntityArray()[0]; for (i in treasureAmount) gameState.getResourceSupplies(i).forEach( function (ent) { - if (ent.resourceSupplyType().generic === "treasure" && SquareVectorDistance(ent.position(), CC.position()) < 5000) + if (ent.resourceSupplyType().generic === "treasure" && API3.SquareVectorDistance(ent.position(), CC.position()) < 5000) treasureAmount[i] += ent.resourceSupplyMax(); }); - this.baseManagers[1] = new BaseManager(); + this.baseManagers[1] = new m.BaseManager(this.Config); this.baseManagers[1].init(gameState, events); this.baseManagers[1].setAnchor(CC); this.baseManagers[1].initTerritory(this, gameState); this.baseManagers[1].initGatheringFunctions(this, gameState); - if (Config.debug) + if (m.DebugEnabled) this.basesMap.dumpIm("basesMap.png"); var self = this; ents.forEach( function (ent) { //}){ self.baseManagers[1].assignEntity(ent); }); } // we now have enough data to decide on a few things. // TODO: here would be where we pick our initial strategy. // immediatly build a wood dropsite if possible. if (this.baseManagers[1]) { if (gameState.ai.queueManager.getAvailableResources(gameState)["wood"] >= 250) { var pos = this.baseManagers[1].findBestDropsiteLocation(gameState, "wood"); if (pos) { - queues.dropsites.addItem(new ConstructionPlan(gameState, "structures/{civ}_storehouse",{ "base" : 1 }, 0, -1, pos)); - queues.minorTech.addItem(new ResearchPlan(gameState, "gather_capacity_wheelbarrow")); + queues.dropsites.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_storehouse",{ "base" : 1 }, 0, -1, pos)); + queues.minorTech.addItem(new m.ResearchPlan(gameState, "gather_capacity_wheelbarrow")); } } } - var map = new Map(gameState.sharedScript, gameState.sharedScript.CCResourceMaps["wood"].map); - if (Config.debug) + var map = new API3.Map(gameState.sharedScript, gameState.sharedScript.CCResourceMaps["wood"].map); + if (m.DebugEnabled) map.dumpIm("map_CC_Wood.png"); //this.reassignIdleWorkers(gameState); this.navalManager.init(gameState, events, queues); // TODO: change that. var civ = gameState.playerData.civ; // load units and buildings from the config files - if (civ in Config.buildings.moderate){ - this.bModerate = Config.buildings.moderate[civ]; + if (civ in this.Config.buildings.moderate){ + this.bModerate = this.Config.buildings.moderate[civ]; }else{ - this.bModerate = Config.buildings.moderate['default']; + this.bModerate = this.Config.buildings.moderate['default']; } - if (civ in Config.buildings.advanced){ - this.bAdvanced = Config.buildings.advanced[civ]; + if (civ in this.Config.buildings.advanced){ + this.bAdvanced = this.Config.buildings.advanced[civ]; }else{ - this.bAdvanced = Config.buildings.advanced['default']; + this.bAdvanced = this.Config.buildings.advanced['default']; } - if (civ in Config.buildings.fort){ - this.bFort = Config.buildings.fort[civ]; + if (civ in this.Config.buildings.fort){ + this.bFort = this.Config.buildings.fort[civ]; }else{ - this.bFort = Config.buildings.fort['default']; + this.bFort = this.Config.buildings.fort['default']; } for (var i in this.bAdvanced){ this.bAdvanced[i] = gameState.applyCiv(this.bAdvanced[i]); } for (var i in this.bFort){ this.bFort[i] = gameState.applyCiv(this.bFort[i]); } // TODO: figure out how to make this generic for (var i in this.attackManagers){ this.availableAttacks[i] = new this.attackManagers[i](gameState, this); } var enemies = gameState.getEnemyEntities(); - var filter = Filters.byClassesOr(["CitizenSoldier", "Champion", "Hero", "Siege"]); + var filter = API3.Filters.byClassesOr(["CitizenSoldier", "Champion", "Hero", "Siege"]); this.enemySoldiers = enemies.filter(filter); // TODO: cope with diplomacy changes this.enemySoldiers.registerUpdates(); // each enemy watchers keeps a list of entity collections about the enemy it watches // It also keeps track of enemy armies, merging/splitting as needed // TODO: remove those. this.enemyWatchers = {}; this.ennWatcherIndex = []; for (var i = 1; i <= 8; i++) if (PlayerID != i && gameState.isPlayerEnemy(i)) { - this.enemyWatchers[i] = new enemyWatcher(gameState, i); + this.enemyWatchers[i] = new m.enemyWatcher(gameState, i); this.ennWatcherIndex.push(i); this.defenceManager.enemyArmy[i] = []; } }; -HQ.prototype.checkEvents = function (gameState, events, queues) { +m.HQ.prototype.checkEvents = function (gameState, events, queues) { for (i in events) { if (events[i].type == "Destroy") { // TODO: probably check stuffs like a base destruction. } else if (events[i].type == "Create") { var evt = events[i]; // Let's check if we have a building set to create a new base. if (evt.msg && evt.msg.entity) { var ent = gameState.getEntityById(evt.msg.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 = uniqueIDBases; - this.baseManagers[bID] = new BaseManager(); + var bID = m.playerGlobals[PlayerID].uniqueIDBases; + this.baseManagers[bID] = new m.BaseManager(this.Config); this.baseManagers[bID].init(gameState, events, true); this.baseManagers[bID].setAnchor(ent); this.baseManagers[bID].initTerritory(this, gameState); // Let's get a few units out there to build this. // TODO: select the best base, or use multiple bases. var builders = this.bulkPickWorkers(gameState, bID, 10); builders.forEach(function (worker) { worker.setMetadata(PlayerID, "base", bID); worker.setMetadata(PlayerID, "subrole", "builder"); worker.setMetadata(PlayerID, "target-foundation", ent.id()); }); } } } else if (events[i].type == "ConstructionFinished") { var evt = events[i]; // Let's check if we have a building set to create a new base. // TODO: move to the base manager. if (evt.msg && evt.msg.newentity) { var ent = gameState.getEntityById(evt.msg.newentity); if (ent === undefined) continue; // happens when this message is right before a "Destroy" one for the same entity. if (ent.isOwn(PlayerID) && ent.getMetadata(PlayerID, "baseAnchor") == true) { var base = ent.getMetadata(PlayerID, "base"); if (this.baseManagers[base].constructing) { this.baseManagers[base].constructing = false; this.baseManagers[base].initGatheringFunctions(this, gameState); } } } } } }; // okay, so here we'll create both females and male workers. // We'll try to keep close to the "ratio" defined atop. // Choice of citizen soldier is a bit messy. // Before having 100 workers it focuses on speed, cost, and won't choose units that cost stone/metal // After 100 it just picks the strongest; // TODO: This should probably be changed to favor a more mixed approach for better defense. // (or even to adapt based on estimated enemy strategy). // TODO: this should probably set which base it wants them in. -HQ.prototype.trainMoreWorkers = function(gameState, queues) { +m.HQ.prototype.trainMoreWorkers = function(gameState, queues) { // Count the workers in the world and in progress var numFemales = gameState.countEntitiesAndQueuedByType(gameState.applyCiv("units/{civ}_support_female_citizen")); numFemales += queues.villager.countQueuedUnitsWithClass("Support"); // counting the workers that aren't part of a plan var numWorkers = 0; gameState.getOwnEntities().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 numQueued = queues.villager.countQueuedUnits() + queues.citizenSoldier.countQueuedUnits(); 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 && (queues.villager.length() + queues.citizenSoldier.length()) < 120 && numInTraining < 15) { var template = gameState.applyCiv("units/{civ}_support_female_citizen"); var size = Math.min(5, Math.ceil(numTotal / 10)); if (numFemales/numTotal > this.femaleRatio && (numTotal > 20 || (this.fastStart && numTotal > 10))) { if (numTotal < 100) 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"); if (gameState.currentPhase() === 1) size = 2; } if (numFemales/numTotal > this.femaleRatio * 1.3) queues.villager.paused = true; else if ((numFemales/numTotal < this.femaleRatio * 1.1) || gameState.ai.queueManager.getAvailableResources(gameState)["food"] > 250) queues.villager.paused = false; // TODO: perhaps assign them a default resource and check the base according to that. // base "0" means "auto" if (template === gameState.applyCiv("units/{civ}_support_female_citizen")) - queues.villager.addItem(new TrainingPlan(gameState, template, { "role" : "worker", "base" : 0 }, size, 0, -1, size )); + queues.villager.addItem(new m.TrainingPlan(gameState, template, { "role" : "worker", "base" : 0 }, size, 0, -1, size )); else - queues.citizenSoldier.addItem(new TrainingPlan(gameState, template, { "role" : "worker", "base" : 0 }, size, 0, -1, size)); + queues.citizenSoldier.addItem(new m.TrainingPlan(gameState, template, { "role" : "worker", "base" : 0 }, size, 0, -1, size)); } }; // picks the best template based on parameters and classes -HQ.prototype.findBestTrainableUnit = function(gameState, classes, parameters) { +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 += getMaxStrength(a[1]) * param[1]; - bTopParam += getMaxStrength(b[1]) * param[1]; + aTopParam += m.getMaxStrength(a[1]) * param[1]; + bTopParam += m.getMaxStrength(b[1]) * 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]; } } return -(aTopParam/(aDivParam+1)) + (bTopParam/(bDivParam+1)); }); return units[0][0]; }; // picks the best template based on parameters and classes -HQ.prototype.findBestTrainableSoldier = function(gameState, classes, parameters) { +m.HQ.prototype.findBestTrainableSoldier = 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 += getMaxStrength(a[1]) * param[1]; - bTopParam += getMaxStrength(b[1]) * param[1]; + aTopParam += m.getMaxStrength(a[1]) * param[1]; + bTopParam += m.getMaxStrength(b[1]) * param[1]; } if (param[0] == "siegeStrength") { - aTopParam += getMaxStrength(a[1], "Structure") * param[1]; - bTopParam += getMaxStrength(b[1], "Structure") * param[1]; + 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]; } 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. -HQ.prototype.tryResearchTechs = function(gameState, queues) { +m.HQ.prototype.tryResearchTechs = function(gameState, queues) { if (queues.minorTech.length() === 0) { var possibilities = gameState.findAvailableTech(); if (possibilities.length === 0) return; // randomly pick one. No worries about pairs in that case. var p = Math.floor((Math.random()*possibilities.length)); - queues.minorTech.addItem(new ResearchPlan(gameState, possibilities[p][0])); + queues.minorTech.addItem(new m.ResearchPlan(gameState, possibilities[p][0])); } } // We're given a worker and a resource type // We'll assign the worker for the best base for that resource type. // TODO: improve choice alogrithm -HQ.prototype.switchWorkerBase = function(gameState, worker, type) { +m.HQ.prototype.switchWorkerBase = function(gameState, worker, type) { var bestBase = 0; + for (var i in this.baseManagers) { if (this.baseManagers[i].willGather[type] >= 1) { if (this.baseManagers[i].accessIndex === this.baseManagers[worker.getMetadata(PlayerID,"base")].accessIndex || this.navalManager.canReach(gameState, this.baseManagers[i].accessIndex, this.baseManagers[worker.getMetadata(PlayerID,"base")].accessIndex)) { bestBase = i; break; } } } if (bestBase && bestBase !== worker.getMetadata(PlayerID,"base")) { worker.setMetadata(PlayerID,"base",bestBase); return true; } else { return false; } }; // returns an entity collection of workers through BaseManager.pickBuilders // TODO: better the choice algo. // TODO: also can't get over multiple bases right now. -HQ.prototype.bulkPickWorkers = function(gameState, newBaseID, number) { +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 = AssocArraytoArray(this.baseManagers).sort(function (a,b) { + var baseBest = m.AssocArraytoArray(this.baseManagers).sort(function (a,b) { if (a.accessIndex === accessIndex && b.accessIndex !== accessIndex) return -1; else if (b.accessIndex === accessIndex && a.accessIndex !== accessIndex) return 1; return 0; }); for (i in baseBest) { if (baseBest[i].workers.length > number) { return baseBest[i].pickBuilders(gameState,number); } } return false; } // returns the current gather rate // This is not per-se exact, it performs a few adjustments ad-hoc to account for travel distance, stuffs like that. -HQ.prototype.GetCurrentGatherRates = function(gameState) { +m.HQ.prototype.GetCurrentGatherRates = function(gameState) { var self = this; var currentRates = {}; for (var type in this.wantedRates) currentRates[type] = 0; for (i in this.baseManagers) this.baseManagers[i].getGatherRates(gameState, currentRates); return currentRates; }; // Pick the resource which most needs another worker -HQ.prototype.pickMostNeededResources = function(gameState) { +m.HQ.prototype.pickMostNeededResources = function(gameState) { var self = this; this.wantedRates = gameState.ai.queueManager.wantedGatherRates(gameState); var currentRates = {}; for (var type in this.wantedRates) currentRates[type] = 0; for (i in this.baseManagers) { var base = this.baseManagers[i]; for (var type in this.wantedRates) { if (gameState.turnCache["gathererAssignementCache-" + type]) currentRates[type] += gameState.turnCache["gathererAssignementCache-" + type]; base.gatherersByType(gameState,type).forEach (function (ent) { //}){ var worker = ent.getMetadata(PlayerID, "worker-object"); if (worker) currentRates[type] += worker.getGatherRate(gameState); }); } } // let's get our ideal number. var types = Object.keys(this.wantedRates); types.sort(function(a, b) { var va = (Math.max(0,self.wantedRates[a] - currentRates[a]))/ (currentRates[a]+1); var vb = (Math.max(0,self.wantedRates[b] - currentRates[b]))/ (currentRates[b]+1); // If they happen to be equal (generally this means "0" aka no need), make it equitable. 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. -HQ.prototype.buildNewCC= function(gameState, queues) { +m.HQ.prototype.buildNewCC= function(gameState, queues) { var numCCs = gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_civil_centre")); 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 ConstructionPlan(gameState, "structures/{civ}_civil_centre")); + queues.civilCentre.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_civil_centre")); } return (gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_civil_centre"), true) == 0 && gameState.currentPhase() > 1); }; // Returns the best position to build a new Civil Centre // Whose primary function would be to reach new resources of type "resource". -HQ.prototype.findBestEcoCCLocation = function(gameState, resource){ +m.HQ.prototype.findBestEcoCCLocation = function(gameState, resource){ var CCPlate = gameState.getTemplate("structures/{civ}_civil_centre"); // This builds a map. The procedure is fairly simple. It adds the resource maps // (which are dynamically updated and are made so that they will facilitate DP placement) // Then checks for a good spot in the territory. If none, and town/city phase, checks outside // The AI will currently not build a CC if it wouldn't connect with an existing CC. - var territory = Map.createTerritoryMap(gameState); + var territory = m.createTerritoryMap(gameState); - var obstructions = Map.createObstructionMap(gameState, 0); + var obstructions = m.createObstructionMap(gameState, 0); obstructions.expandInfluences(); // copy the resource map as initialization. - var friendlyTiles = new Map(gameState.sharedScript, gameState.sharedScript.CCResourceMaps[resource].map, true); + var friendlyTiles = new API3.Map(gameState.sharedScript, gameState.sharedScript.CCResourceMaps[resource].map, true); friendlyTiles.setMaxVal(255); - var ents = gameState.getOwnEntities().filter(Filters.byClass("CivCentre")).toEntityArray(); - var eEnts = gameState.getEnemyEntities().filter(Filters.byClass("CivCentre")).toEntityArray(); + var ents = gameState.getOwnEntities().filter(API3.Filters.byClass("CivCentre")).toEntityArray(); + var eEnts = gameState.getEnemyEntities().filter(API3.Filters.byClass("CivCentre")).toEntityArray(); var dps = gameState.getOwnDropsites().toEntityArray(); for (var j = 0; j < friendlyTiles.length; ++j) { // We check for our other CCs: the distance must not be too big. Anything bigger will result in scrapping. // This ensures territorial continuity. // TODO: maybe whenever I get around to implement multi-base support (details below, requires being part of the team. If you're not, ask wraitii directly by PM). // (see www.wildfiregames.com/forum/index.php?showtopic=16702&#entry255631 ) // TODO: figure out what I was trying to say above. var canBuild = true; var canBuild2 = false; var pos = [j%friendlyTiles.width+0.5, Math.floor(j/friendlyTiles.width)+0.5]; for (var i in ents) { var entPos = ents[i].position(); entPos = [entPos[0]/4.0,entPos[1]/4.0]; - var dist = SquareVectorDistance(entPos, pos); + var dist = API3.SquareVectorDistance(entPos, pos); if (dist < 2120) { canBuild = false; continue; } else if (dist < 8000 || this.waterMap) canBuild2 = true; } // checking for bases. if (this.basesMap.map[j] !== 0) canBuild = false; if (!canBuild2) canBuild = false; if (canBuild) { // Checking for enemy CCs for (var i in eEnts) { var entPos = eEnts[i].position(); entPos = [entPos[0]/4.0,entPos[1]/4.0]; // 7100 works well as a limit. - if (SquareVectorDistance(entPos, pos) < 2500) + if (API3.SquareVectorDistance(entPos, pos) < 2500) { canBuild = false; continue; } } } if (!canBuild) { friendlyTiles.map[j] = 0; continue; } for (var i in dps) { var dpPos = dps[i].position(); if (dpPos === undefined) { // Probably a mauryan elephant, skip continue; } dpPos = [dpPos[0]/4.0,dpPos[1]/4.0]; - if (SquareVectorDistance(dpPos, pos) < 100) + if (API3.SquareVectorDistance(dpPos, pos) < 100) { friendlyTiles.map[j] = 0; continue; - } else if (SquareVectorDistance(dpPos, pos) < 400) + } else if (API3.SquareVectorDistance(dpPos, pos) < 400) friendlyTiles.map[j] /= 2; } friendlyTiles.map[j] *= 1.5; for (var i in gameState.sharedScript.CCResourceMaps) if (friendlyTiles.map[j] !== 0 && i !== "food") { var val = friendlyTiles.map[j] + gameState.sharedScript.CCResourceMaps[i].map[j]; if (val < 255) friendlyTiles.map[j] = val; else friendlyTiles.map[j] = 255; } } var best = friendlyTiles.findBestTile(6, obstructions); var bestIdx = best[0]; - if (Config.debug) + if (m.DebugEnabled) { friendlyTiles.map[bestIdx] = 270; friendlyTiles.dumpIm("cc_placement_base_" + gameState.getTimeElapsed() + "_" + resource + "_" + best[1] + ".png",301); //obstructions.dumpIm("cc_placement_base_" + gameState.getTimeElapsed() + "_" + resource + "_" + best[1] + "_obs.png", 20); } // not good enough. if (best[1] < 60) return false; var x = ((bestIdx % friendlyTiles.width) + 0.5) * gameState.cellSize; var z = (Math.floor(bestIdx / friendlyTiles.width) + 0.5) * gameState.cellSize; - debug ("Best for value " + best[1] + " at " + uneval([x,z])); + m.debug ("Best for value " + best[1] + " at " + uneval([x,z])); return [x,z]; }; -HQ.prototype.buildTemple = function(gameState, queues){ +m.HQ.prototype.buildTemple = function(gameState, queues){ if (gameState.currentPhase() >= 2 ) { if (queues.economicBuilding.countQueuedUnits() === 0 && gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_temple")) === 0){ - queues.economicBuilding.addItem(new ConstructionPlan(gameState, "structures/{civ}_temple", { "base" : 1 })); + queues.economicBuilding.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_temple", { "base" : 1 })); } } }; -HQ.prototype.buildMarket = function(gameState, queues){ - if (gameState.getPopulation() > Config.Economy.popForMarket && gameState.currentPhase() >= 2 ) { +m.HQ.prototype.buildMarket = function(gameState, queues){ + if (gameState.getPopulation() > this.Config.Economy.popForMarket && gameState.currentPhase() >= 2 ) { if (queues.economicBuilding.countQueuedUnitsWithClass("BarterMarket") === 0 && gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_market")) === 0){ //only ever build one storehouse/CC/market at a time - queues.economicBuilding.addItem(new ConstructionPlan(gameState, "structures/{civ}_market", { "base" : 1 })); + queues.economicBuilding.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_market", { "base" : 1 })); } } }; // Build a farmstead to go to town phase faster and prepare for research. Only really active on higher diff mode. -HQ.prototype.buildFarmstead = function(gameState, queues){ - if (gameState.getPopulation() > Config.Economy.popForFarmstead) { +m.HQ.prototype.buildFarmstead = function(gameState, queues){ + if (gameState.getPopulation() > this.Config.Economy.popForFarmstead) { // achtung: "DropsiteFood" does not refer to CCs. if (queues.economicBuilding.countQueuedUnitsWithClass("DropsiteFood") === 0 && gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_farmstead")) === 0){ //only ever build one storehouse/CC/market at a time - queues.economicBuilding.addItem(new ConstructionPlan(gameState, "structures/{civ}_farmstead", { "base" : 1 })); + queues.economicBuilding.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_farmstead", { "base" : 1 })); } } }; // TODO: generic this, probably per-base -HQ.prototype.buildDock = function(gameState, queues){ +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")) === 0) { var tp = "" if (gameState.civ() == "cart" && gameState.currentPhase() > 1) tp = "structures/{civ}_super_dock"; else if (gameState.civ() !== "cart") tp = "structures/{civ}_dock"; if (tp !== "") { var remaining = this.navalManager.getUnconnectedSeas(gameState, this.baseManagers[1].accessIndex); - queues.economicBuilding.addItem(new ConstructionPlan(gameState, tp, { "base" : 1, "sea" : remaining[0] })); + queues.economicBuilding.addItem(new m.ConstructionPlan(gameState, tp, { "base" : 1, "sea" : remaining[0] })); } } } }; // if Aegis has resources it doesn't need, it'll try to barter it for resources it needs // once per turn because the info doesn't update between a turn and I don't want to fix it. // Not sure how efficient it is but it seems to be sane, at least. -HQ.prototype.tryBartering = function(gameState){ +m.HQ.prototype.tryBartering = function(gameState){ var done = false; if (gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_market"), true) >= 1) { var needs = gameState.ai.queueManager.futureNeeds(gameState); var ress = gameState.ai.queueManager.getAvailableResources(gameState); for (var sell in needs) { for (var buy in needs) { if (!done && buy != sell && needs[sell] <= 0 && ress[sell] > 400) { // if we don't need it and have a buffer if ( (ress[buy] < 400) || needs[buy] > 0) { // if we need that other resource/ have too little of it var markets = gameState.getOwnEntitiesByType(gameState.applyCiv("structures/{civ}_market"), true).toEntityArray(); markets[0].barter(buy,sell,100); - //debug ("bartered " +sell +" for " + buy + ", value 100"); + //m.debug ("bartered " +sell +" for " + buy + ", value 100"); done = true; } } } } } }; // build more houses if needed. // kinda ugly, lots of special cases to both build enough houses but not tooo many… -HQ.prototype.buildMoreHouses = function(gameState,queues) { +m.HQ.prototype.buildMoreHouses = function(gameState,queues) { if (gameState.getPopulationLimit() < gameState.getPopulationMax()) { var numPlanned = queues.house.length(); if (numPlanned < 3 || (numPlanned < 5 && gameState.getPopulation() > 80)) { - var plan = new ConstructionPlan(gameState, "structures/{civ}_house", { "base" : 1 }); + var plan = new m.ConstructionPlan(gameState, "structures/{civ}_house", { "base" : 1 }); + // make the difficulty available to the isGo function without having to pass it as argument + var difficulty = this.Config.difficulty; // change the starting condition to "less than 15 slots left". plan.isGo = function (gameState) { var HouseNb = gameState.countEntitiesByType(gameState.applyCiv("foundation|structures/{civ}_house"), true); var freeSlots = 0; if (gameState.civ() == "gaul" || gameState.civ() == "brit" || gameState.civ() == "iber") freeSlots = gameState.getPopulationLimit() + HouseNb*5 - gameState.getPopulation(); else freeSlots = gameState.getPopulationLimit() + HouseNb*10 - gameState.getPopulation(); - if (gameState.getPopulation() > 55 && Config.difficulty > 1) + if (gameState.getPopulation() > 55 && difficulty > 1) return (freeSlots <= 21); - else if (gameState.getPopulation() >= 20 && Config.difficulty !== 0) + else if (gameState.getPopulation() >= 20 && difficulty !== 0) return (freeSlots <= 16); else return (freeSlots <= 10); } queues.house.addItem(plan); } } }; // checks if we have bases for all resource types (bar food for now) or if we need to expand. -HQ.prototype.checkBasesRessLevel = function(gameState,queues) { +m.HQ.prototype.checkBasesRessLevel = function(gameState,queues) { if (gameState.currentPhase() === 1 && !gameState.isResearching(gameState.townPhase())) return; var count = { "wood" : 0, "stone" : 0, "metal" : 0 } var capacity = { "wood" : 0, "stone" : 0, "metal" : 0 } var need = { "wood" : true, "stone" : true, "metal" : true }; var posss = []; + for (i in this.baseManagers) { var base = this.baseManagers[i]; for (type in count) { - if (base.getResourceLevel(gameState, type, "all") > 1500*Math.max(Config.difficulty,2)) + if (base.getResourceLevel(gameState, type, "all") > 1500*Math.max(this.Config.difficulty,2)) count[type]++; capacity[type] += base.getWorkerCapacity(gameState, type); if (base.willGather[type] !== 2) need[type] = false; } } for (type in count) { if (count[type] === 0 || need[type] - || capacity[type] < gameState.getOwnEntities().filter(Filters.and(Filters.byMetadata(PlayerID, "subrole", "gatherer"), Filters.byMetadata(PlayerID, "gather-type", type))).length * 1.05) + || capacity[type] < gameState.getOwnEntities().filter(API3.Filters.and(API3.Filters.byMetadata(PlayerID, "subrole", "gatherer"), API3.Filters.byMetadata(PlayerID, "gather-type", type))).length * 1.05) { // plan a new base. if (gameState.countFoundationsWithType(gameState.applyCiv("structures/{civ}_civil_centre")) === 0 && queues.civilCentre.length() === 0) { if (this.outOf[type] && gameState.ai.playedTurn % 10 !== 0) continue; var pos = this.findBestEcoCCLocation(gameState, type); if (!pos) { // Okay so we'll set us as out of this. this.outOf[type] = true; } else { // base "-1" means new base. - queues.civilCentre.addItem(new ConstructionPlan(gameState, "structures/{civ}_civil_centre",{ "base" : -1 }, 0, -1, pos)); + queues.civilCentre.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_civil_centre",{ "base" : -1 }, 0, -1, pos)); } } } } }; // Deals with building fortresses and towers. // Currently build towers next to every useful dropsites. // TODO: Fortresses are placed randomly atm. -HQ.prototype.buildDefences = function(gameState, queues){ +m.HQ.prototype.buildDefences = function(gameState, queues){ - var workersNumber = gameState.getOwnEntitiesByRole("worker").filter(Filters.not(Filters.byHasMetadata(PlayerID,"plan"))).length; + var workersNumber = gameState.getOwnEntitiesByRole("worker").filter(API3.Filters.not(API3.Filters.byHasMetadata(PlayerID,"plan"))).length; if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv('structures/{civ}_defense_tower')) + queues.defenceBuilding.length() < gameState.getEntityLimits()["DefenseTower"] && queues.defenceBuilding.length() < 4 && gameState.currentPhase() > 1) { for (i in this.baseManagers) { for (j in this.baseManagers[i].dropsites) { var amnts = this.baseManagers[i].dropsites[j]; var dpEnt = gameState.getEntityById(j); if (dpEnt !== undefined && dpEnt.getMetadata(PlayerID, "defenseTower") !== true) if (amnts["wood"] || amnts["metal"] || amnts["stone"]) { var position = dpEnt.position(); if (position) { - queues.defenceBuilding.addItem(new ConstructionPlan(gameState, 'structures/{civ}_defense_tower', { "base" : i }, 0 , -1, position)); + queues.defenceBuilding.addItem(new m.ConstructionPlan(gameState, 'structures/{civ}_defense_tower', { "base" : i }, 0 , -1, position)); } dpEnt.setMetadata(PlayerID, "defenseTower", true); } } } } var numFortresses = 0; for (var i in this.bFort){ numFortresses += gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bFort[i])); } if (queues.defenceBuilding.length() < 1 && (gameState.currentPhase() > 2 || gameState.isResearching("phase_city_generic"))) { if (workersNumber >= 80 && gameState.getTimeElapsed() > numFortresses * this.fortressLapseTime + this.fortressStartTime) { if (!this.fortressStartTime) this.fortressStartTime = gameState.getTimeElapsed(); - queues.defenceBuilding.addItem(new ConstructionPlan(gameState, this.bFort[0], { "base" : 1 })); - debug ("Building a fortress"); + queues.defenceBuilding.addItem(new m.ConstructionPlan(gameState, this.bFort[0], { "base" : 1 })); + m.debug ("Building a fortress"); } } if (gameState.countEntitiesByType(gameState.applyCiv(this.bFort[i]), true) >= 1) { // let's add a siege building plan to the current attack plan if there is none currently. if (this.upcomingAttacks["CityAttack"].length !== 0) { var attack = this.upcomingAttacks["CityAttack"][0]; if (!attack.unitStat["Siege"]) { // no minsize as we don't want the plan to fail at the last minute though. var stat = { "priority" : 1.1, "minSize" : 0, "targetSize" : 4, "batchSize" : 2, "classes" : ["Siege"], "interests" : [ ["siegeStrength", 3], ["cost",1] ] ,"templates" : [] }; if (gameState.civ() == "cart" || gameState.civ() == "maur") stat["classes"] = ["Elephant"]; attack.addBuildOrder(gameState, "Siege", stat, true); } } } }; -HQ.prototype.buildBlacksmith = function(gameState, queues){ - if (gameState.getTimeElapsed() > Config.Military.timeForBlacksmith*1000) { +m.HQ.prototype.buildBlacksmith = function(gameState, queues){ + if (gameState.getTimeElapsed() > this.Config.Military.timeForBlacksmith*1000) { if (queues.militaryBuilding.length() === 0 && gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_blacksmith")) === 0) { var tp = gameState.getTemplate(gameState.applyCiv("structures/{civ}_blacksmith")); if (tp.available(gameState)) - queues.militaryBuilding.addItem(new ConstructionPlan(gameState, "structures/{civ}_blacksmith", { "base" : 1 })); + queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_blacksmith", { "base" : 1 })); } } }; // Deals with constructing military buildings (barracks, stables…) // They are mostly defined by Config.js. This is unreliable since changes could be done easily. // TODO: We need to determine these dynamically. Also doesn't build fortresses since the above function does that. // TODO: building placement is bad. Choice of buildings is also fairly dumb. -HQ.prototype.constructTrainingBuildings = function(gameState, queues) { +m.HQ.prototype.constructTrainingBuildings = function(gameState, queues) { Engine.ProfileStart("Build buildings"); - var workersNumber = gameState.getOwnEntitiesByRole("worker").filter(Filters.not(Filters.byHasMetadata(PlayerID, "plan"))).length; + var workersNumber = gameState.getOwnEntitiesByRole("worker").filter(API3.Filters.not(API3.Filters.byHasMetadata(PlayerID, "plan"))).length; - if (workersNumber > Config.Military.popForBarracks1) { + if (workersNumber > this.Config.Military.popForBarracks1) { if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bModerate[0])) + queues.militaryBuilding.length() < 1) { - debug ("Trying to build barracks"); - queues.militaryBuilding.addItem(new ConstructionPlan(gameState, this.bModerate[0], { "base" : 1 })); + m.debug ("Trying to build barracks"); + queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, this.bModerate[0], { "base" : 1 })); } } - if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bModerate[0])) < 2 && workersNumber > Config.Military.popForBarracks2) + if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bModerate[0])) < 2 && workersNumber > this.Config.Military.popForBarracks2) if (queues.militaryBuilding.length() < 1) - queues.militaryBuilding.addItem(new ConstructionPlan(gameState, this.bModerate[0], { "base" : 1 })); + queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, this.bModerate[0], { "base" : 1 })); if (gameState.countEntitiesByType(gameState.applyCiv(this.bModerate[0]), true) === 2 && gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bModerate[0])) < 3 && workersNumber > 125) if (queues.militaryBuilding.length() < 1) { - queues.militaryBuilding.addItem(new ConstructionPlan(gameState, this.bModerate[0], { "base" : 1 })); + queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, this.bModerate[0], { "base" : 1 })); if (gameState.civ() == "gaul" || gameState.civ() == "brit" || gameState.civ() == "iber") { - queues.militaryBuilding.addItem(new ConstructionPlan(gameState, this.bModerate[0], { "base" : 1 })); - queues.militaryBuilding.addItem(new ConstructionPlan(gameState, this.bModerate[0], { "base" : 1 })); + queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, this.bModerate[0], { "base" : 1 })); + queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, this.bModerate[0], { "base" : 1 })); } } //build advanced military buildings - if (workersNumber >= Config.Military.popForBarracks2 - 15 && gameState.currentPhase() > 2){ + 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.countFoundationsWithType(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])) < 1){ - queues.militaryBuilding.addItem(new ConstructionPlan(gameState, this.bAdvanced[i], { "base" : 1 })); + queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, this.bAdvanced[i], { "base" : 1 })); } } } } if (gameState.civ() !== "gaul" && gameState.civ() !== "brit" && gameState.civ() !== "iber" && workersNumber > 130 && gameState.currentPhase() > 2) { var Const = 0; for (var i in this.bAdvanced) Const += gameState.countEntitiesByType(gameState.applyCiv(this.bAdvanced[i]), true); if (inConst == 1) { var i = Math.floor(Math.random() * this.bAdvanced.length); if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bAdvanced[i])) < 1){ - queues.militaryBuilding.addItem(new ConstructionPlan(gameState, this.bAdvanced[i], { "base" : 1 })); - queues.militaryBuilding.addItem(new ConstructionPlan(gameState, this.bAdvanced[i], { "base" : 1 })); + queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, this.bAdvanced[i], { "base" : 1 })); + queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, this.bAdvanced[i], { "base" : 1 })); } } } Engine.ProfileStop(); }; // TODO: use pop(). Currently unused as this is too gameable. -HQ.prototype.garrisonAllFemales = function(gameState) { - var buildings = gameState.getOwnEntities().filter(Filters.byCanGarrison()).toEntityArray(); - var females = gameState.getOwnEntities().filter(Filters.byClass("Support")); +m.HQ.prototype.garrisonAllFemales = function(gameState) { + var buildings = gameState.getOwnEntities().filter(API3.Filters.byCanGarrison()).toEntityArray(); + var females = gameState.getOwnEntities().filter(API3.Filters.byClass("Support")); var cache = {}; females.forEach( function (ent) { for (var i in buildings) { if (ent.position()) { var struct = buildings[i]; if (!cache[struct.id()]) cache[struct.id()] = 0; if (struct.garrisoned() && struct.garrisonMax() - struct.garrisoned().length - cache[struct.id()] > 0) { ent.garrison(struct); cache[struct.id()]++; break; } } } }); this.hasGarrisonedFemales = true; }; -HQ.prototype.ungarrisonAll = function(gameState) { +m.HQ.prototype.ungarrisonAll = function(gameState) { this.hasGarrisonedFemales = false; - var buildings = gameState.getOwnEntities().filter(Filters.and(Filters.byClass("Structure"),Filters.byCanGarrison())).toEntityArray(); + var buildings = gameState.getOwnEntities().filter(API3.Filters.and(API3.Filters.byClass("Structure"),API3.Filters.byCanGarrison())).toEntityArray(); buildings.forEach( function (struct) { if (struct.garrisoned() && struct.garrisoned().length) struct.unloadAll(); }); }; -HQ.prototype.pausePlan = function(gameState, planName) { +m.HQ.prototype.pausePlan = function(gameState, planName) { for (var attackType in this.upcomingAttacks) { for (var i in this.upcomingAttacks[attackType]) { var attack = this.upcomingAttacks[attackType][i]; if (attack.getName() == planName) attack.setPaused(gameState, true); } } for (var attackType in this.startedAttacks) { for (var i in this.startedAttacks[attackType]) { var attack = this.startedAttacks[attackType][i]; if (attack.getName() == planName) attack.setPaused(gameState, true); } } } -HQ.prototype.unpausePlan = function(gameState, planName) { +m.HQ.prototype.unpausePlan = function(gameState, planName) { for (var attackType in this.upcomingAttacks) { for (var i in this.upcomingAttacks[attackType]) { var attack = this.upcomingAttacks[attackType][i]; if (attack.getName() == planName) attack.setPaused(gameState, false); } } for (var attackType in this.startedAttacks) { for (var i in this.startedAttacks[attackType]) { var attack = this.startedAttacks[attackType][i]; if (attack.getName() == planName) attack.setPaused(gameState, false); } } } -HQ.prototype.pauseAllPlans = function(gameState) { +m.HQ.prototype.pauseAllPlans = function(gameState) { for (var attackType in this.upcomingAttacks) { for (var i in this.upcomingAttacks[attackType]) { var attack = this.upcomingAttacks[attackType][i]; attack.setPaused(gameState, true); } } for (var attackType in this.startedAttacks) { for (var i in this.startedAttacks[attackType]) { var attack = this.startedAttacks[attackType][i]; attack.setPaused(gameState, true); } } } -HQ.prototype.unpauseAllPlans = function(gameState) { +m.HQ.prototype.unpauseAllPlans = function(gameState) { for (var attackType in this.upcomingAttacks) { for (var i in this.upcomingAttacks[attackType]) { var attack = this.upcomingAttacks[attackType][i]; attack.setPaused(gameState, false); } } for (var attackType in this.startedAttacks) { for (var i in this.startedAttacks[attackType]) { var attack = this.startedAttacks[attackType][i]; attack.setPaused(gameState, false); } } } // Some functions are run every turn // Others once in a while -HQ.prototype.update = function(gameState, queues, events) { +m.HQ.prototype.update = function(gameState, queues, events) { Engine.ProfileStart("Headquarters update"); this.checkEvents(gameState,events,queues); //this.buildMoreHouses(gameState); //Engine.ProfileStart("Train workers and build farms, houses. Research techs."); this.trainMoreWorkers(gameState, queues); // sandbox doesn't expand. - if (Config.difficulty !== 0) + if (this.Config.difficulty !== 0) this.checkBasesRessLevel(gameState, queues); this.buildMoreHouses(gameState,queues); if (gameState.getTimeElapsed() > this.techStartTime && gameState.currentPhase() > 2) this.tryResearchTechs(gameState,queues); - if (Config.difficulty > 1) + if (this.Config.difficulty > 1) this.tryBartering(gameState); this.buildFarmstead(gameState, queues); this.buildMarket(gameState, queues); // Deactivated: the temple had no useful purpose for the AI now. //if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_market")) === 1) // this.buildTemple(gameState, queues); this.buildDock(gameState, queues); // not if not a water map. Engine.ProfileStart("Constructing military buildings and building defences"); this.constructTrainingBuildings(gameState, queues); this.buildBlacksmith(gameState, queues); if(gameState.getTimeElapsed() > this.defenceBuildingTime) this.buildDefences(gameState, queues); Engine.ProfileStop(); for (i in this.baseManagers) { this.baseManagers[i].checkEvents(gameState, events, queues) - if ( ( (+i + gameState.ai.playedTurn) % (uniqueIDBases - 1)) === 0) + if ( ( (+i + gameState.ai.playedTurn) % (m.playerGlobals[PlayerID].uniqueIDBases - 1)) === 0) this.baseManagers[i].update(gameState, queues, events); } this.navalManager.update(gameState, queues, events); this.defenceManager.update(gameState, events, this); Engine.ProfileStart("Looping through attack plans"); // TODO: bump this into a function. // TODO: implement some form of check before starting a new attack plans. Sometimes it is not the priority. if (1) { for (var attackType in this.upcomingAttacks) { for (var i = 0;i < this.upcomingAttacks[attackType].length; ++i) { var attack = this.upcomingAttacks[attackType][i]; // okay so we'll get the support plan if (!attack.isStarted()) { var updateStep = attack.updatePreparation(gameState, this,events); // now we're gonna check if the preparation time is over if (updateStep === 1 || attack.isPaused() ) { // just chillin' } else if (updateStep === 0 || updateStep === 3) { - debug ("Military Manager: " +attack.getType() +" plan " +attack.getName() +" aborted."); + m.debug ("Military Manager: " +attack.getType() +" plan " +attack.getName() +" aborted."); if (updateStep === 3) { this.attackPlansEncounteredWater = true; - debug("No attack path found. Aborting."); + m.debug("No attack path found. Aborting."); } attack.Abort(gameState, this); this.upcomingAttacks[attackType].splice(i--,1); } else if (updateStep === 2) { var chatText = "I am launching an attack against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; if (Math.random() < 0.2) chatText = "Attacking " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; else if (Math.random() < 0.3) chatText = "I have sent an army against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; else if (Math.random() < 0.3) chatText = "I'm starting an attack against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; gameState.ai.chatTeam(chatText); - debug ("Military Manager: Starting " +attack.getType() +" plan " +attack.getName()); + m.debug ("Military Manager: Starting " +attack.getType() +" plan " +attack.getName()); attack.StartAttack(gameState,this); this.startedAttacks[attackType].push(attack); this.upcomingAttacks[attackType].splice(i--,1); } } else { var chatText = "I am launching an attack against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; if (Math.random() < 0.2) chatText = "Attacking " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; else if (Math.random() < 0.3) chatText = "I have sent an army against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; else if (Math.random() < 0.3) chatText = "I'm starting an attack against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; gameState.ai.chatTeam(chatText); - debug ("Military Manager: Starting " +attack.getType() +" plan " +attack.getName()); + m.debug ("Military Manager: Starting " +attack.getType() +" plan " +attack.getName()); this.startedAttacks[attackType].push(attack); this.upcomingAttacks[attackType].splice(i--,1); } } } } for (var attackType in this.startedAttacks) { for (var i = 0; i < this.startedAttacks[attackType].length; ++i) { var attack = this.startedAttacks[attackType][i]; // okay so then we'll update the attack. if (!attack.isPaused()) { var remaining = attack.update(gameState,this,events); if (remaining == 0 || remaining == undefined) { - debug ("Military Manager: " +attack.getType() +" plan " +attack.getName() +" is now finished."); + m.debug ("Military Manager: " +attack.getType() +" plan " +attack.getName() +" is now finished."); attack.Abort(gameState); this.startedAttacks[attackType].splice(i--,1); } } } } // TODO: remove the limitation to attacks when on water maps. // Note: these indications of "rush" are currently unused. if (gameState.ai.strategy === "rush" && this.startedAttacks["CityAttack"].length !== 0) { // and then we revert. gameState.ai.strategy = "normal"; - Config.Economy.femaleRatio = 0.4; + this.Config.Economy.femaleRatio = 0.4; gameState.ai.modules.economy.targetNumWorkers = Math.max(Math.floor(gameState.getPopulationMax()*0.55), 1); } else if (gameState.ai.strategy === "rush" && this.upcomingAttacks["CityAttack"].length === 0) { - Lalala = new CityAttack(gameState, this,this.TotalAttackNumber, -1, "rush") + Lalala = new m.CityAttack(gameState, this, this.Config, this.TotalAttackNumber, -1, "rush") this.TotalAttackNumber++; this.upcomingAttacks["CityAttack"].push(Lalala); - debug ("Starting a little something"); + m.debug ("Starting a little something"); } else if (gameState.ai.strategy !== "rush" && !this.waterMap) { // creating plans after updating because an aborted plan might be reused in that case. if (gameState.countEntitiesByType(gameState.applyCiv(this.bModerate[0]), true) >= 1 && !this.attackPlansEncounteredWater && gameState.getTimeElapsed() > this.attackPlansStartTime && gameState.currentPhase() > 1) { if (gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_dock"), true) === 0 && this.waterMap) { // wait till we get a dock. } else { // basically only the first plan, really. if (this.upcomingAttacks["CityAttack"].length == 0 && gameState.getTimeElapsed() < 12*60000) { - var Lalala = new CityAttack(gameState, this,this.TotalAttackNumber, -1); + var Lalala = new m.CityAttack(gameState, this, this.Config, this.TotalAttackNumber, -1); if (Lalala.failed) { this.attackPlansEncounteredWater = true; // hack } else { - debug ("Military Manager: Creating the plan " +this.TotalAttackNumber); + m.debug ("Military Manager: Creating the plan " +this.TotalAttackNumber); this.TotalAttackNumber++; this.upcomingAttacks["CityAttack"].push(Lalala); } - } else if (this.upcomingAttacks["CityAttack"].length == 0 && Config.difficulty !== 0) { - var Lalala = new CityAttack(gameState, this,this.TotalAttackNumber, -1, "superSized"); + } else if (this.upcomingAttacks["CityAttack"].length == 0 && this.Config.difficulty !== 0) { + var Lalala = new m.CityAttack(gameState, this, this.Config, this.TotalAttackNumber, -1, "superSized"); if (Lalala.failed) { this.attackPlansEncounteredWater = true; // hack } else { - debug ("Military Manager: Creating the super sized plan " +this.TotalAttackNumber); + m.debug ("Military Manager: Creating the super sized plan " +this.TotalAttackNumber); this.TotalAttackNumber++; this.upcomingAttacks["CityAttack"].push(Lalala); } } } } } /* // very old relic. This should be reimplemented someday so the code stays here. if (this.HarassRaiding && this.preparingRaidNumber + this.startedRaidNumber < 1 && gameState.getTimeElapsed() < 780000) { - var Lalala = new CityAttack(gameState, this,this.totalStartedAttackNumber, -1, "harass_raid"); + var Lalala = new m.CityAttack(gameState, this,this.totalStartedAttackNumber, -1, "harass_raid"); if (!Lalala.createSupportPlans(gameState, this, )) { - debug ("Military Manager: harrassing plan not a valid option"); + m.debug ("Military Manager: harrassing plan not a valid option"); this.HarassRaiding = false; } else { - debug ("Military Manager: Creating the harass raid plan " +this.totalStartedAttackNumber); + m.debug ("Military Manager: Creating the harass raid plan " +this.totalStartedAttackNumber); this.totalStartedAttackNumber++; this.preparingRaidNumber++; this.currentAttacks.push(Lalala); } } */ Engine.ProfileStop(); /* Engine.ProfileStop(); Engine.ProfileStart("Build new Dropsites"); this.buildDropsites(gameState, queues); Engine.ProfileStop(); - if (Config.difficulty !== 0) + if (this.Config.difficulty !== 0) this.tryBartering(gameState); this.buildFarmstead(gameState, queues); this.buildMarket(gameState, queues); // Deactivated: the temple had no useful purpose for the AI now. //if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_market")) === 1) // this.buildTemple(gameState, queues); this.buildDock(gameState, queues); // not if not a water map. */ Engine.ProfileStop(); // Heaquarters update }; + +return m; + +}(AEGIS); Index: ps/trunk/binaries/data/mods/public/simulation/ai/aegis/map-module.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/aegis/map-module.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/aegis/map-module.js (revision 14441) @@ -1,142 +1,148 @@ +var AEGIS = function(m) +{ + // other map functions +m.TERRITORY_PLAYER_MASK = 0x3F; -Map.createObstructionMap = function(gameState, accessIndex, template){ +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] & TERRITORY_PLAYER_MASK); + 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] & TERRITORY_PLAYER_MASK); + 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 Map(gameState.sharedScript, obstructionTiles); + 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.getOwnEntities().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; }; - -Map.createTerritoryMap = function(gameState) { +m.createTerritoryMap = function(gameState) { var map = gameState.ai.territoryMap; - var ret = new Map(gameState.sharedScript, map.data); + var ret = new API3.Map(gameState.sharedScript, map.data); ret.getOwner = function(p) { - return this.point(p) & TERRITORY_PLAYER_MASK; + return this.point(p) & m.TERRITORY_PLAYER_MASK; } ret.getOwnerIndex = function(p) { - return this.map[p] & TERRITORY_PLAYER_MASK; + return this.map[p] & m.TERRITORY_PLAYER_MASK; } return ret; }; + +return m; +}(AEGIS); Index: ps/trunk/binaries/data/mods/public/simulation/ai/aegis/naval-manager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/aegis/naval-manager.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/aegis/naval-manager.js (revision 14441) @@ -1,288 +1,294 @@ +var AEGIS = function(m) +{ + /* Naval Manager Will deal with anything ships. -Basically trade over water (with fleets and goals commissioned by the economy manager) -Defence 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. */ -var NavalManager = function() { +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 -NavalManager.prototype.init = function(gameState, events, queues) { +m.NavalManager.prototype.init = function(gameState, events, queues) { // finished docks - this.docks = gameState.getOwnEntities().filter(Filters.and(Filters.byClass("Dock"), Filters.not(Filters.isFoundation()))); + this.docks = gameState.getOwnEntities().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(Filters.byClass("Ship")); + 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(Filters.byCanGarrison()); - this.warships = this.ships.filter(Filters.byClass("Warship")); + 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 EntityCollection(gameState.sharedScript)); - this.seaTpShips.push(new EntityCollection(gameState.sharedScript)); - this.seaWarships.push(new EntityCollection(gameState.sharedScript)); + 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(Filters.byStaticMetadata(PlayerID, "sea", i)); + var collec = this.ships.filter(API3.Filters.byStaticMetadata(PlayerID, "sea", i)); collec.registerUpdates(); this.seaShips.push(collec); - collec = this.tpShips.filter(Filters.byStaticMetadata(PlayerID, "sea", i)); + collec = this.tpShips.filter(API3.Filters.byStaticMetadata(PlayerID, "sea", i)); collec.registerUpdates(); this.seaTpShips.push(collec); - var collec = this.warships.filter(Filters.byStaticMetadata(PlayerID, "sea", i)); + 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([]); } }; -NavalManager.prototype.getUnconnectedSeas = function (gameState, region) { +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. -NavalManager.prototype.canReach = function (gameState, regionA, regionB) { +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) { - debug ("cannot reach because of " + path[i+1]); + m.debug ("cannot reach because of " + path[i+1]); return false; // we wn't be able to board on that sea } } return true; }; -NavalManager.prototype.checkEvents = function (gameState, queues, events) { +m.NavalManager.prototype.checkEvents = function (gameState, queues, events) { for (i in events) { if (events[i].type == "Destroy") { // TODO: probably check stuffs like a base destruction. } else if (events[i].type == "ConstructionFinished") { var evt = events[i]; if (evt.msg && evt.msg.newentity) { var entity = gameState.getEntityById(evt.msg.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); } } } } }; -NavalManager.prototype.addPlan = function(plan) { +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. -NavalManager.prototype.askForTransport = function(entity, startPos, endPos) { +m.NavalManager.prototype.askForTransport = function(entity, startPos, endPos) { this.askedPlans.push([entity, startPos, endPos]); }; // creates aforementionned plans -NavalManager.prototype.createPlans = function(gameState) { +m.NavalManager.prototype.createPlans = function(gameState) { var startID = {}; for (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 TransportPlan(gameState, startID[i][k].units, startID[i][k].dest, false) + var tpPlan = new m.TransportPlan(gameState, startID[i][k].units, startID[i][k].dest, false) this.transportPlans.push (tpPlan); } }; // TODO: work on this. -NavalManager.prototype.maintainFleet = function(gameState, queues, events) { +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 TrainingPlan(gameState, "units/{civ}_ship_bireme", { "sea" : i }, 1, 0, -1, 1 )); + queues.ships.addItem(new m.TrainingPlan(gameState, "units/{civ}_ship_bireme", { "sea" : i }, 1, 0, -1, 1 )); } } }; // bumps up the number of ships we want if we need more. -NavalManager.prototype.checkLevels = function(gameState, queues) { +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 -NavalManager.prototype.assignToPlans = function(gameState, queues, events) { +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 (ship in this.seaTpShips[zone]._entities) { if (!ship.getMetadata(PlayerID, "tpplan")) { - debug ("Assigning ship " + ship.id() + " to plan" + plan.ID); + m.debug ("Assigning ship " + ship.id() + " to plan" + plan.ID); plan.assignShip(gameState, ship); return true; } } } } } return false; }; -NavalManager.prototype.checkActivePlan = function(ID) { +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 -NavalManager.prototype.update = function(gameState, queues, events) { +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; +}(AEGIS); Index: ps/trunk/binaries/data/mods/public/simulation/ai/aegis/plan-transport.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/aegis/plan-transport.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/aegis/plan-transport.js (revision 14441) @@ -1,472 +1,478 @@ +var AEGIS = 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? -var TransportPlan = function(gameState, units, destination, allAtOnce, escortSize, onlyIfOK) { +m.TransportPlan = function(gameState, units, destination, allAtOnce, escortSize, onlyIfOK) { var self = this; - this.ID = uniqueIDTPlans++; + this.ID = m.playerGlobals[PlayerID].uniqueIDTPlans++; var unitsID = []; if (units.length !== undefined) unitsID = units; else unitsID = [units]; - this.units = EntityCollectionFromIds(gameState, unitsID); + 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(); - debug ("Starting a new plan with ID " + this.ID + " to " + destination); - debug ("units are " + uneval (units)); + 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 -TransportPlan.prototype.countFreeSlots = function(onlyTrulyFree) +m.TransportPlan.prototype.countFreeSlots = function(onlyTrulyFree) { var slots = 0; this.transportShips.forEach(function (ent) { //}){ slots += ent.garrisonMax(); if (onlyTrulyFree) slots -= ent.garrisoned().length; }); } -TransportPlan.prototype.assignShip = function(gameState, ship) +m.TransportPlan.prototype.assignShip = function(gameState, ship) { ship.setMetadata(PlayerID,"tpplan", this.ID); } -TransportPlan.prototype.releaseAll = function(gameState) +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) }); } -TransportPlan.prototype.releaseAllShips = function(gameState) +m.TransportPlan.prototype.releaseAllShips = function(gameState) { this.ships.forEach(function (ent) { ent.setMetadata(PlayerID,"tpplan", undefined) }); } -TransportPlan.prototype.needTpShips = function() +m.TransportPlan.prototype.needTpShips = function() { if ((this.allAtOnce && this.countFreeSlots() >= this.units.length) || this.transportShips.length > 0) return false; return true; } -TransportPlan.prototype.needEscortShips = function() +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 -TransportPlan.prototype.neededShipsZone = function() +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) */ -TransportPlan.prototype.carryOn = function(gameState, navalManager) +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]; - debug ("plan " + this.ID + " from " + this.startIndex); + 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]; - debug ("Plan " + this.ID + " over seazone " + this.seaZone); + 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 Map(gameState.sharedScript); + var obstructions = new API3.Map(gameState.sharedScript); // wanted map. - var friendlyTiles = new Map(gameState.sharedScript); + 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]; - debug ("Plan " + this.ID + " new boarding spot is " + this.boardingSpot); + 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; - debug ("Boarding"); + 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 (SquareVectorDistance(this.ships.getCentrePosition(),this.boardingSpot) > 1600) + 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 (SquareVectorDistance(this.units.getCentrePosition(),this.boardingSpot) > 1600) + 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 (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 Map(gameState.sharedScript); + var obstructions = new API3.Map(gameState.sharedScript); // wanted map. - var friendlyTiles = new Map(gameState.sharedScript); + 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 { - debug ("too short at " +uneval(this.path)); + 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 (SquareVectorDistance(this.ships.getCentrePosition(),this.unboardingSpot) > 400) + 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 (SquareVectorDistance(this.ships.getCentrePosition(),this.unboardingSpot) > 400) + 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) { - debug ("plan " + this.ID + " going back for more"); + m.debug ("plan " + this.ID + " going back for more"); // basically reset. delete this.boardingSpot; delete this.unboardingSpot; this.state = "unstarted"; this.releaseAllShips(); return true; } - debug ("plan " + this.ID + " is finished"); + m.debug ("plan " + this.ID + " is finished"); return false; } } return true; } + +return m; +}(AEGIS); Index: ps/trunk/binaries/data/mods/public/simulation/ai/aegis/queue-manager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/aegis/queue-manager.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/aegis/queue-manager.js (revision 14441) @@ -1,556 +1,569 @@ +var AEGIS = 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. -var QueueManager = function(queues, priorities) { +m.QueueManager = function(Config, queues, priorities) { + this.Config = Config; this.queues = queues; this.priorities = priorities; this.account = {}; this.accounts = {}; // the sorting would need to be updated on priority change but there is currently none. var self = this; this.queueArrays = []; for (var p in this.queues) { this.account[p] = 0; - this.accounts[p] = new Resources(); + 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 = []; }; -QueueManager.prototype.getAvailableResources = function(gameState, noAccounts) { +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; }; -QueueManager.prototype.getTotalAccountedResources = function(gameState) { - var resources = new 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; }; -QueueManager.prototype.currentNeeds = function(gameState) { - var needs = new Resources(); +m.QueueManager.prototype.currentNeeds = function(gameState) { + var needs = new API3.Resources(); // get out current resources, not removing accounts. var current = this.getAvailableResources(gameState, true); //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.getNext().isGo(gameState)) needs.add(queue.getNext().getCost()); else if (queue.length() > 0 && !queue.getNext().isGo(gameState)) { var cost = queue.getNext().getCost(); cost.multiply(0.5); needs.add(cost); } if (queue.length() > 1 && queue.queue[1].isGo(gameState)) needs.add(queue.queue[1].getCost()); } return { "food" : Math.max(25 + needs.food - current.food, 0), "wood" : Math.max(needs.wood - current.wood, 0), "stone" : Math.max(needs.stone - current.stone, 0), "metal" : Math.max(needs.metal - current.metal, 0) }; }; -QueueManager.prototype.futureNeeds = function(gameState) { - var needs = new Resources(); +m.QueueManager.prototype.futureNeeds = function(gameState) { + var needs = new API3.Resources(); // get out current resources, not removing accounts. var current = this.getAvailableResources(gameState, true); //queueArrays because it's faster. for (var i in this.queueArrays) { var name = this.queueArrays[i][0]; var queue = this.queueArrays[i][1]; for (var j = 0; j < queue.length(); ++j) { var costs = queue.queue[j].getCost(); if (!queue.queue[j].isGo(gameState)) costs.multiply(0.5); needs.add(costs); } } return { "food" : Math.max(25 + needs.food - current.food, 10), "wood" : Math.max(needs.wood - current.wood, 10), "stone" : Math.max(needs.stone - current.stone, 0), "metal" : Math.max(needs.metal - current.metal, 0) }; }; // calculate the gather rates we'd want to be able to use all elements in our queues -QueueManager.prototype.wantedGatherRates = function(gameState) { +m.QueueManager.prototype.wantedGatherRates = function(gameState) { var rates = { "food" : 0, "wood" : 0, "stone" : 0, "metal" : 0 }; var qTime = 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]; for (var j = 0; j < queue.length(); ++j) { var elem = queue.queue[j]; var cost = elem.getCost(); if (qTime < elem.startTime) qTime = elem.startTime; if (!elem.isGo(gameState)) { // assume 2 minutes. // TODO work on this. for (type in qCosts) qCosts[type] += cost[type]; qTime += 120000; break; // disregard other stuffs. } if (!elem.endTime) { // estimate time based on priority + cost + nb // TODO: work on this. for (type in qCosts) + { qCosts[type] += (cost[type] + Math.min(cost[type],this.priorities[name])); + } qTime += 30000; } else { // TODO: work on this. for (type in qCosts) qCosts[type] += (cost[type] + Math.min(cost[type],this.priorities[name])); // TODO: refine based on % completed. qTime += (elem.endTime-elem.startTime); } } for (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; }; -/*QueueManager.prototype.logNeeds = function(gameState) { +/*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 (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 (j in array) { log (j + ";"); for (var i = 0; i < this.totor.length; ++i) { log (this.totor[i][j] + ";"); } } log(); for (j in array) { log (j + ";"); for (var i = 0; i < this.totor.length; ++i) { log (this.currentGathR[i][j] + ";"); } } log(); for (j in array) { log (j + ";"); for (var i = 0; i < this.totor.length; ++i) { log (this.currentGathRWanted[i].indexOf(j) + ";"); } } log(); for (j in array) { log (j + ";"); for (var i = 0; i < this.totor.length; ++i) { log (this.ressLev[i][j] + ";"); } } } }; */ -QueueManager.prototype.printQueues = function(gameState){ - debug("QUEUES"); +m.QueueManager.prototype.printQueues = function(gameState){ + m.debug("QUEUES"); for (var i in this.queues){ var qStr = ""; var q = this.queues[i]; if (q.queue.length > 0) - debug((i + ":")); + m.debug((i + ":")); for (var j in q.queue){ qStr = " " + q.queue[j].type + " "; if (q.queue[j].number) qStr += "x" + q.queue[j].number; - debug (qStr); + m.debug (qStr); } } - debug ("Accounts"); + m.debug ("Accounts"); for (var p in this.accounts) { - debug(p + ": " + uneval(this.accounts[p])); + m.debug(p + ": " + uneval(this.accounts[p])); } - debug("Needed Resources:" + uneval(this.futureNeeds(gameState,false))); - debug ("Wanted Gather Rates:" + uneval(this.wantedGatherRates(gameState))); - debug ("Current Resources:" + uneval(gameState.getResources())); - debug ("Available Resources:" + uneval(this.getAvailableResources(gameState))); + m.debug("Needed Resources:" + uneval(this.futureNeeds(gameState,false))); + m.debug ("Wanted Gather Rates:" + uneval(this.wantedGatherRates(gameState))); + m.debug ("Current Resources:" + uneval(gameState.getResources())); + m.debug ("Available Resources:" + uneval(this.getAvailableResources(gameState))); }; // nice readable HTML version. -QueueManager.prototype.HTMLprintQueues = function(gameState){ - if (!Config.debug) +m.QueueManager.prototype.HTMLprintQueues = function(gameState){ + if (!m.DebugEnabled) return; log(" Aegis Queue Manager "); for (var i in this.queues){ log (""); var q = this.queues[i]; var str = ""); for (var j in q.queue) { if (q.queue[j].isGo(gameState)) log (""); } log (""); } log ("
Aegis Build Order
" + i +"
"; for each (k in this.accounts[i].types) if(k != "population") { str += this.accounts[i][k] + k.substr(0,1).toUpperCase() ; if (k != "metal") str += " / "; } log(str + "
"); else log (""); var qStr = ""; qStr += q.queue[j].type; if (q.queue[j].number) qStr += "x" + q.queue[j].number; log (qStr); log ("
"); /*log ("

Accounts

"); for (var p in this.accounts) { log("

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

"); }*/ log ("

Needed Resources:" + uneval(this.futureNeeds(gameState,false)) + "

"); log ("

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

"); log ("

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

"); log ("

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

"); log(""); }; -QueueManager.prototype.clear = function(){ +m.QueueManager.prototype.clear = function(){ this.curItemQueue = []; for (var i in this.queues) this.queues[i].empty(); }; -QueueManager.prototype.update = function(gameState) { +m.QueueManager.prototype.update = function(gameState) { var self = this; for (var i in this.priorities){ if (!(this.priorities[i] > 0)){ this.priorities[i] = 1; // TODO: make the Queue Manager not die when priorities are zero. warn("QueueManager received bad priorities, please report this error: " + uneval(this.priorities)); } } Engine.ProfileStart("Queue Manager"); // Let's assign resources to plans that need'em var availableRes = this.getAvailableResources(gameState); for (var ress in availableRes) { if (availableRes[ress] > 0 && ress != "population") { 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 (/2). // -queues accounts are capped at "resources for the first + 80% 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); if (this.queues[j].length() > 0 && this.accounts[j][ress] < queueCost[ress] && !this.queues[j].paused) { // check that we're not too forward in this resource compared to others. /*var maxp = this.accounts[j][ress] / (queueCost[ress]+1); var tooFull = false; for (tempRess in availableRes) if (tempRess !== ress && queueCost[tempRess] > 0 && (this.accounts[j][tempRess] / (queueCost[tempRess]+1)) - maxp < -0.2) tooFull = true; if (tooFull) continue;*/ // 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 = tempPrio[j]/totalPriority * availableRes[ress]; var maxAdd = Math.floor(Math.min(maxNeed[j], toAdd)); this.accounts[j][ress] += maxAdd; } }/* else if (ress != "population" && gameState.ai.playedTurn % 5 === 0) { // okay here we haev no resource available. We'll try to shift resources to complete plans if possible. // So basically if 2 queues have resources, and one is higher priority, and it needs resources // We'll shift from the lower priority to the higher if we can complete it. var queues = []; for (var j in this.queues) { if (this.queues[j].length() && this.queues[j].getNext().isGo(gameState) && this.accounts[j][ress] > 0) queues.push(j); } if (queues.length > 1) { // we'll work from the bottom to the top. ie lowest priority will try to give to highest priority. queues.sort(function (a,b) { return (self.priorities[a] < self.priorities[b]); }); var under = 0, over = queues.length - 1; while (under !== over) { var cost = this.queues[queues[over]].getNext().getCost()[ress]; var totalCost = this.queues[queues[over]].maxAccountWanted(gameState)[ress]; if (this.accounts[queues[over]] >= cost) { --over; // check the next one. continue; } // need some discrepancy in priorities if (this.priorities[queues[under]] < this.priorities[queues[over]] - 20) { if (this.accounts[queues[under]] + this.accounts[queues[over]] >= cost) { var amnt = cost - this.accounts[queues[over]]; this.accounts[queues[under]] -= amnt; this.accounts[queues[over]] += amnt; --over; - debug ("Shifting " + amnt + " from " + queues[under] + " to " +queues[over]); + m.debug ("Shifting " + amnt + " from " + queues[under] + " to " +queues[over]); continue; } else { ++under; continue; } } else { break; } } // okaaaay. } }*/ } Engine.ProfileStart("Pick items from queues"); - //debug ("start"); - //debug (uneval(this.accounts)); + //m.debug ("start"); + //m.debug (uneval(this.accounts)); // 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 Resources(); + 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); } } } else if (queue.length() === 0) { this.accounts[name].reset(); } } - //debug (uneval(this.accounts)); + //m.debug (uneval(this.accounts)); Engine.ProfileStop(); if (gameState.ai.playedTurn % 30 === 0) this.HTMLprintQueues(gameState); Engine.ProfileStop(); }; -QueueManager.prototype.pauseQueue = function(queue, scrapAccounts) { +m.QueueManager.prototype.pauseQueue = function(queue, scrapAccounts) { if (this.queues[queue]) { this.queues[queue].paused = true; if (scrapAccounts) this.accounts[queue].reset(); } } -QueueManager.prototype.unpauseQueue = function(queue) { +m.QueueManager.prototype.unpauseQueue = function(queue) { if (this.queues[queue]) this.queues[queue].paused = false; } -QueueManager.prototype.pauseAll = function(scrapAccounts, but) { +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; } } -QueueManager.prototype.unpauseAll = function(but) { +m.QueueManager.prototype.unpauseAll = function(but) { for (var p in this.queues) if (p != but) this.queues[p].paused = false; } -QueueManager.prototype.addQueue = function(queueName, priority) { +m.QueueManager.prototype.addQueue = function(queueName, priority) { if (this.queues[queueName] == undefined) { - this.queues[queueName] = new Queue(); + this.queues[queueName] = new m.Queue(); this.priorities[queueName] = priority; this.account[queueName] = 0; - this.accounts[queueName] = new Resources(); + 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]]) }); } } -QueueManager.prototype.removeQueue = function(queueName) { +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.account[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]]) }); } } -QueueManager.prototype.changePriority = function(queueName, newPriority) { +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) { + 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; +}(AEGIS); Index: ps/trunk/binaries/data/mods/public/simulation/ai/aegis/queue.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/aegis/queue.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/aegis/queue.js (revision 14441) @@ -1,104 +1,110 @@ +var AEGIS = function(m) +{ + /* * Holds a list of wanted items to train or construct */ -var Queue = function() { +m.Queue = function() { this.queue = []; this.paused = false; }; -Queue.prototype.empty = function() { +m.Queue.prototype.empty = function() { this.queue = []; }; -Queue.prototype.addItem = function(plan) { +m.Queue.prototype.addItem = function(plan) { 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); }; -Queue.prototype.getNext = function() { +m.Queue.prototype.getNext = function() { if (this.queue.length > 0) { return this.queue[0]; } else { return null; } }; -Queue.prototype.startNext = function(gameState) { +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 100% of the cost of the first element and 80% of that of the second -Queue.prototype.maxAccountWanted = function(gameState) { - var cost = new Resources(); +m.Queue.prototype.maxAccountWanted = function(gameState) { + 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)) { var costs = this.queue[1].getCost(); costs.multiply(0.8); cost.add(costs); } return cost; }; -Queue.prototype.queueCost = function(){ - var cost = new Resources(); +m.Queue.prototype.queueCost = function(){ + var cost = new API3.Resources(); for (var key in this.queue){ cost.add(this.queue[key].getCost()); } return cost; }; -Queue.prototype.length = function() { +m.Queue.prototype.length = function() { return this.queue.length; }; -Queue.prototype.countQueuedUnits = function(){ +m.Queue.prototype.countQueuedUnits = function(){ var count = 0; for (var i in this.queue){ count += this.queue[i].number; } return count; }; -Queue.prototype.countQueuedUnitsWithClass = function(classe){ +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; }; -Queue.prototype.countQueuedUnitsWithMetadata = function(data,value){ +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; }; -Queue.prototype.countAllByType = function(t){ +m.Queue.prototype.countAllByType = function(t){ var count = 0; for (var i = 0; i < this.queue.length; i++){ if (this.queue[i].type === t){ count += this.queue[i].number; } } return count; }; + +return m; +}(AEGIS); Index: ps/trunk/binaries/data/mods/public/simulation/ai/aegis/queueplan-building.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/aegis/queueplan-building.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/aegis/queueplan-building.js (revision 14441) @@ -1,231 +1,238 @@ -var ConstructionPlan = function(gameState, type, metadata, startTime, expectedTime, position) { +var AEGIS = function(m) +{ + +m.ConstructionPlan = function(gameState, type, metadata, startTime, expectedTime, position) { this.type = gameState.applyCiv(type); this.position = position; this.metadata = metadata; - this.ID = uniqueIDBOPlans++; + this.ID = m.playerGlobals[PlayerID].uniqueIDBOPlans++; this.template = gameState.getTemplate(this.type); if (!this.template) { return false; } this.category = "building"; - this.cost = new Resources(this.template.cost()); + this.cost = new API3.Resources(this.template.cost()); this.number = 1; // The number of buildings to build if (!startTime) this.startTime = 0; else this.startTime = startTime; if (!expectedTime) this.expectedTime = -1; else this.expectedTime = expectedTime; return true; }; // return true if we willstart amassing resource for this plan -ConstructionPlan.prototype.isGo = function(gameState) { +m.ConstructionPlan.prototype.isGo = function(gameState) { return (gameState.getTimeElapsed() > this.startTime); }; // checks other than resource ones. // TODO: change this. -ConstructionPlan.prototype.canStart = function(gameState) { +m.ConstructionPlan.prototype.canStart = function(gameState) { if (gameState.buildingsBuilt > 0) 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); }; -ConstructionPlan.prototype.start = function(gameState) { +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; - debug("No room to place " + this.type); + m.debug("No room to place " + this.type); return; } if (this.template.hasClass("Naval")) - debug (pos); + m.debug (pos); gameState.buildingsBuilt++; 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 builders[0].construct(this.type, pos.x, pos.z, pos.angle, this.metadata); }; -ConstructionPlan.prototype.getCost = function() { - var costs = new Resources(); +m.ConstructionPlan.prototype.getCost = function() { + var costs = new API3.Resources(); costs.add(this.cost); return costs; }; -ConstructionPlan.prototype.findGoodPosition = function(gameState) { +m.ConstructionPlan.prototype.findGoodPosition = function(gameState) { var template = gameState.getTemplate(this.type); var cellSize = gameState.cellSize; // size of each tile // First, find all tiles that are far enough away from obstructions: - var obstructionMap = Map.createObstructionMap(gameState,0, template); + var obstructionMap = m.createObstructionMap(gameState,0, template); //obstructionMap.dumpIm(template.buildCategory() + "_obstructions_pre.png"); if (template.buildCategory() !== "Dock") obstructionMap.expandInfluences(); //obstructionMap.dumpIm(template.buildCategory() + "_obstructions.png"); // Compute each tile's closeness to friendly structures: - var friendlyTiles = new Map(gameState.sharedScript); + var friendlyTiles = new API3.Map(gameState.sharedScript); var alreadyHasHouses = false; // If a position was specified then place the building as close to it as possible if (this.position) { 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 gameState.getOwnEntities().forEach(function(ent) { if (ent.hasClass("Structure")) { var infl = 32; if (ent.hasClass("CivCentre")) infl *= 4; var pos = ent.position(); var x = Math.round(pos[0] / cellSize); var z = Math.round(pos[1] / cellSize); if (ent.buildCategory() == "Wall") { // no real blockers, but can't build where they are friendlyTiles.addInfluence(x, z, 2,-1000); return; } if (template._template.BuildRestrictions.Category === "Field"){ if (ent.resourceDropsiteTypes() && ent.resourceDropsiteTypes().indexOf("food") !== -1){ if (ent.hasClass("CivCentre")) friendlyTiles.addInfluence(x, z, infl/4, infl); else friendlyTiles.addInfluence(x, z, infl, infl); } }else{ if (template.genericName() == "House" && ent.genericName() == "House") { friendlyTiles.addInfluence(x, z, 15.0,20,'linear'); // houses are close to other houses alreadyHasHouses = true; } else if (template.hasClass("GarrisonFortress") && ent.genericName() == "House") { friendlyTiles.addInfluence(x, z, 30, -50); } else if (template.genericName() == "House") { friendlyTiles.addInfluence(x, z, Math.ceil(infl/4.0),-infl/2.0); // houses are farther away from other buildings but houses } else if (template.hasClass("GarrisonFortress")) { friendlyTiles.addInfluence(x, z, 20, 10); friendlyTiles.addInfluence(x, z, 10, -40, 'linear'); } else if (ent.genericName() != "House") // houses have no influence on other buildings { friendlyTiles.addInfluence(x, z, infl); //avoid building too close to each other if possible. friendlyTiles.addInfluence(x, z, 5, -5, 'linear'); } // If this is not a field add a negative influence near the CivCentre because we want to leave this // area for fields. if (ent.hasClass("CivCentre") && template.genericName() != "House"){ friendlyTiles.addInfluence(x, z, Math.floor(infl/8), Math.floor(-infl/2)); } else if (ent.hasClass("CivCentre")) { friendlyTiles.addInfluence(x, z, infl/3.0, infl + 1); friendlyTiles.addInfluence(x, z, Math.ceil(infl/5.0), -(infl/2.0), 'linear'); } } } }); if (this.metadata && this.metadata.base !== undefined) for (var base in gameState.ai.HQ.baseManagers) if (base != this.metadata.base) for (var j in gameState.ai.HQ.baseManagers[base].territoryIndices) friendlyTiles.map[gameState.ai.HQ.baseManagers[base].territoryIndices[j]] = 0; } //friendlyTiles.dumpIm(template.buildCategory() + "_" +gameState.getTimeElapsed() + ".png", 200); // 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("GarrisonFortress")) radius = Math.floor(template.obstructionRadius() / cellSize) + 2; 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); // further contract cause walls // Note: I'm currently destroying them so that doesn't matter. //if (gameState.playerData.civ == "iber") // radius *= 0.95; // Find the best non-obstructed if (template.genericName() == "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 === -1) { return false; } //friendlyTiles.setInfluence((bestIdx % friendlyTiles.width), Math.floor(bestIdx / friendlyTiles.width), 1, 200); //friendlyTiles.dumpIm(template.buildCategory() + "_" +gameState.getTimeElapsed() + ".png", 200); var x = ((bestIdx % friendlyTiles.width) + 0.5) * cellSize; var z = (Math.floor(bestIdx / friendlyTiles.width) + 0.5) * cellSize; // default angle var angle = 3*Math.PI/4; return { "x" : x, "z" : z, "angle" : angle }; }; + + +return m; +}(AEGIS); Index: ps/trunk/binaries/data/mods/public/simulation/ai/aegis/queueplan-research.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/aegis/queueplan-research.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/aegis/queueplan-research.js (revision 14441) @@ -1,74 +1,80 @@ -var ResearchPlan = function(gameState, type, startTime, expectedTime, rush) { +var AEGIS = function(m) +{ + +m.ResearchPlan = function(gameState, type, startTime, expectedTime, rush) { this.type = type; - this.ID = uniqueIDBOPlans++; + this.ID = m.playerGlobals[PlayerID].uniqueIDBOPlans++; this.template = gameState.getTemplate(this.type); if (!this.template || this.template.researchTime === undefined) { return false; } this.category = "technology"; - this.cost = new Resources(this.template.cost(),0); + this.cost = new API3.Resources(this.template.cost(),0); this.number = 1; // Obligatory for compatibility if (!startTime) this.startTime = 0; else this.startTime = startTime; if (!expectedTime) this.expectedTime = -1; else this.expectedTime = expectedTime; if (rush) this.rush = true; else this.rush = false; return true; }; // return true if we willstart amassing resource for this plan -ResearchPlan.prototype.isGo = function(gameState) { +m.ResearchPlan.prototype.isGo = function(gameState) { return (gameState.getTimeElapsed() > this.startTime); }; -ResearchPlan.prototype.canStart = function(gameState) { +m.ResearchPlan.prototype.canStart = function(gameState) { // also checks canResearch return (gameState.findResearchers(this.type).length !== 0); }; -ResearchPlan.prototype.start = function(gameState) { +m.ResearchPlan.prototype.start = function(gameState) { var self = this; // TODO: this is special cased for "rush" technologies, ie the town phase // which currently is a 100% focus. gameState.ai.queueManager.unpauseAll(); - //debug ("Starting the research plan for " + this.type); + //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); } }; -ResearchPlan.prototype.getCost = function(){ - var costs = new Resources(); +m.ResearchPlan.prototype.getCost = function(){ + var costs = new API3.Resources(); costs.add(this.cost); return costs; }; + +return m; +}(AEGIS); Index: ps/trunk/binaries/data/mods/public/simulation/ai/aegis/queueplan-training.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/aegis/queueplan-training.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/aegis/queueplan-training.js (revision 14441) @@ -1,86 +1,93 @@ -var TrainingPlan = function(gameState, type, metadata, number, startTime, expectedTime, maxMerge) { +var AEGIS = function(m) +{ + +m.TrainingPlan = function(gameState, type, metadata, number, startTime, expectedTime, maxMerge) { this.type = gameState.applyCiv(type); this.metadata = metadata; - this.ID = uniqueIDBOPlans++; + this.ID = m.playerGlobals[PlayerID].uniqueIDBOPlans++; this.template = gameState.getTemplate(this.type); if (!this.template) return false; this.category = "unit"; - this.cost = new Resources(this.template.cost(), this.template._template.Cost.Population); + this.cost = new API3.Resources(this.template.cost(), this.template._template.Cost.Population); if (!number) this.number = 1; else this.number = number; if (!maxMerge) this.maxMerge = 5; else this.maxMerge = maxMerge; if (!startTime) this.startTime = 0; else this.startTime = startTime; if (!expectedTime) this.expectedTime = -1; else this.expectedTime = expectedTime; return true; }; // return true if we willstart amassing resource for this plan -TrainingPlan.prototype.isGo = function(gameState) { +m.TrainingPlan.prototype.isGo = function(gameState) { return (gameState.getTimeElapsed() > this.startTime); }; -TrainingPlan.prototype.canStart = function(gameState) { +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); }; -TrainingPlan.prototype.start = function(gameState) { +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); } }; -TrainingPlan.prototype.getCost = function(){ - var multCost = new Resources(); +m.TrainingPlan.prototype.getCost = function(){ + var multCost = new API3.Resources(); multCost.add(this.cost); multCost.multiply(this.number); return multCost; }; -TrainingPlan.prototype.addItem = function(amount){ +m.TrainingPlan.prototype.addItem = function(amount){ if (amount === undefined) amount = 1; this.number += amount; }; + + +return m; +}(AEGIS); Index: ps/trunk/binaries/data/mods/public/simulation/ai/aegis/template-manager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/aegis/template-manager.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/aegis/template-manager.js (revision 14441) @@ -1,116 +1,122 @@ +var AEGIS = function(m) +{ + /* * Used to know which templates I have, which templates I know I can train, things like that. * Mostly unused. */ -var TemplateManager = function(gameState) { +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); }; -TemplateManager.prototype.AcknowledgeTemplates = function(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); } }); } -TemplateManager.prototype.getBuildableSubtemplates = function(gameState) +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); } } } } } -TemplateManager.prototype.getTrainableSubtemplates = function(gameState) +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); } } } } } -TemplateManager.prototype.getTemplateCounters = function(gameState) +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 -TemplateManager.prototype.getCountersToClasses = function(gameState,classes,templateName) +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; +}(AEGIS); Index: ps/trunk/binaries/data/mods/public/simulation/ai/aegis/timer.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/aegis/timer.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/aegis/timer.js (revision 14441) @@ -1,106 +1,112 @@ +var AEGIS = function(m) +{ + //The Timer class // The instance of this class is created in the qBot object under the name 'timer' //The methods that are available to call from this instance are: //timer.setTimer : Creates a new timer with the given interval (miliseconds). // Optionally set dalay or a limited repeat value. //timer.checkTimer : Gives true if called at the time of the interval. //timer.clearTimer : Deletes the timer permanently. No way to get the same timer back. //timer.activateTimer : Sets the status of a deactivated timer to active. //timer.deactivateTimer : Deactivates a timer. Deactivated timers will never give true. // Currently totally unused, iirc. //-EmjeR-// Timer class // -var Timer = function() { +m.Timer = function() { ///Private array. var alarmList = []; ///Private methods function num_alarms() { return alarmList.length; }; function get_alarm(id) { return alarmList[id]; }; function add_alarm(index, alarm) { alarmList[index] = alarm; }; function delete_alarm(id) { // Set the array element to undefined delete alarmList[id]; }; ///Privileged methods // Add an new alarm to the list this.setTimer = function(gameState, interval, delay, repeat) { delay = delay || 0; repeat = repeat || -1; var index = num_alarms(); //Add a new alarm to the list add_alarm(index, new alarm(gameState, index, interval, delay, repeat)); return index; }; // Check if a alarm has reached its interval. this.checkTimer = function(gameState,id) { var alarm = get_alarm(id); if (alarm === undefined) return false; if (!alarm.active) return false; var time = gameState.getTimeElapsed(); var alarmState = false; // If repeat forever (repeat is -1). Or if the alarm has rung less times than repeat. if (alarm.repeat < 0 || alarm.counter < alarm.repeat) { var time_diffrence = time - alarm.start_time - alarm.delay - alarm.interval * alarm.counter; if (time_diffrence > alarm.interval) { alarmState = true; alarm.counter++; } } // Check if the alarm has rung 'alarm.repeat' times if so, delete the alarm. if (alarm.counter >= alarm.repeat && alarm.repeat != -1) { this.clearTimer(id); } return alarmState; }; // Remove an alarm from the list. this.clearTimer = function(id) { delete_alarm(id); }; // Activate a deactivated alarm. this.activateTimer = function(id) { var alarm = get_alarm(id); alarm.active = true; }; // Deactivate an active alarm but don't delete it. this.deactivateTimer = function(id) { var alarm = get_alarm(id); alarm.active = false; }; }; //-EmjeR-// Alarm class // -function alarm(gameState, id, interval, delay, repeat) { +m.alarm = function(gameState, id, interval, delay, repeat) { this.id = id; this.interval = interval; this.delay = delay; this.repeat = repeat; this.start_time = gameState.getTimeElapsed(); this.active = true; this.counter = 0; }; + +return m; +}(AEGIS); Index: ps/trunk/binaries/data/mods/public/simulation/ai/aegis/utils-extend.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/aegis/utils-extend.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/aegis/utils-extend.js (revision 14441) @@ -1,26 +1,32 @@ -function AssocArraytoArray(assocArray) { +var AEGIS = 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 -function inRange(a, b, range)// checks for X distance +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. -function ManhattanDistance(a, b) +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; +}(AEGIS); Index: ps/trunk/binaries/data/mods/public/simulation/ai/aegis/worker.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/aegis/worker.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/aegis/worker.js (revision 14441) @@ -1,638 +1,646 @@ +var AEGIS = function(m) +{ + /** * This class makes a worker do as instructed by the economy manager */ -var Worker = function(ent) { +m.Worker = function(ent) { this.ent = ent; this.maxApproachTime = 45000; this.unsatisfactoryResource = false; // if true we'll reguarly check if we can't have better now. this.baseID = 0; }; -Worker.prototype.update = function(baseManager, gameState) { +m.Worker.prototype.update = function(baseManager, gameState) { 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, let's not start gathering, heh? // TODO: remove this when we're hunting? if (this.ent.unitAIState().split(".")[1] === "COMBAT" || this.ent.getMetadata(PlayerID, "role") === "transport") { return; } 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. if (!this.ent.resourceCarrying() || this.ent.resourceCarrying().length === 0 || this.ent.resourceCarrying()[0].type === this.ent.getMetadata(PlayerID, "gather-type")){ Engine.ProfileStart("Start Gathering"); this.unsatisfactoryResource = false; this.startGathering(baseManager,gameState); Engine.ProfileStop(); this.startApproachingResourceTime = gameState.getTimeElapsed(); } else { // Should deposit resources Engine.ProfileStart("Return Resources"); if (!this.returnResources(gameState)) { // no dropsite, abandon cargo. // if we were ordered to gather something else, try that. if (this.ent.resourceCarrying()[0].type !== this.ent.getMetadata(PlayerID, "gather-type")) this.startGathering(baseManager,gameState); else { // okay so we haven't found a proper dropsite for the resource we're supposed to gather // so let's get idle and the base manager will reassign us, hopefully well. this.ent.setMetadata(PlayerID, "gather-type",undefined); this.ent.setMetadata(PlayerID, "subrole", "idle"); this.ent.stopMoving(); } } Engine.ProfileStop(); } - // debug: show the resource we're gathering from + // m.debug: show the resource we're gathering from //Engine.PostCommand({"type": "set-shading-color", "entities": [this.ent.id()], "rgb": [10,0,0]}); } else if (this.ent.unitAIState().split(".")[1] === "GATHER") { // check for transport. if (gameState.ai.playedTurn % 5 === 0) { if (this.ent.unitAIOrderData().length && this.ent.unitAIState().split(".")[2] === "APPROACHING" && this.ent.unitAIOrderData()[0]["target"]) { var ress = gameState.getEntityById(this.ent.unitAIOrderData()[0]["target"]); if (ress !== undefined) { var index = gameState.ai.accessibility.getAccessValue(ress.position()); var mIndex = gameState.ai.accessibility.getAccessValue(this.ent.position()); if (index !== mIndex && index !== 1) { //gameState.ai.HQ.navalManager.askForTransport(this.ent.id(), this.ent.position(), ress.position()); } } } } /* if (gameState.getTimeElapsed() - this.startApproachingResourceTime > this.maxApproachTime) { if (this.ent.unitAIOrderData().length && this.ent.unitAIState().split(".")[1] === "GATHER" && this.ent.unitAIOrderData()[0]["target"]) { var ent = gameState.getEntityById(this.ent.unitAIOrderData()[0]["target"]); - debug ("here " + this.startApproachingResourceAmount + "," + ent.resourceSupplyAmount()); + m.debug ("here " + this.startApproachingResourceAmount + "," + ent.resourceSupplyAmount()); if (ent && this.startApproachingResourceAmount == ent.resourceSupplyAmount() && this.startEnt == ent.id()) { - debug (ent.toString() + " is inaccessible"); + m.debug (ent.toString() + " is inaccessible"); ent.setMetadata(PlayerID, "inaccessible", true); this.ent.flee(ent); this.ent.setMetadata(PlayerID, "subrole", "idle"); } } }*/ // we're gathering. Let's check that it's not a resource we'd rather not gather from. if ((this.ent.id() + gameState.ai.playedTurn) % 6 === 0 && this.checkUnsatisfactoryResource(gameState)) { Engine.ProfileStart("Start Gathering"); this.startGathering(baseManager,gameState); Engine.ProfileStop(); } // TODO: reimplement the "reaching time" check. /*if (gameState.getTimeElapsed() - this.startApproachingResourceTime > this.maxApproachTime) { if (this.gatheringFrom) { var ent = gameState.getEntityById(this.gatheringFrom); if ((ent && ent.resourceSupplyAmount() == ent.resourceSupplyMax())) { // if someone gathers from it, it's only that the pathfinder sucks. - debug (ent.toString() + " is inaccessible"); + m.debug (ent.toString() + " is inaccessible"); ent.setMetadata(PlayerID, "inaccessible", true); this.ent.flee(ent); this.ent.setMetadata(PlayerID, "subrole", "idle"); this.gatheringFrom = undefined; } } }*/ } else if (this.ent.unitAIState().split(".")[1] === "COMBAT") { /*if (gameState.getTimeElapsed() - this.startApproachingResourceTime > this.maxApproachTime) { var ent = gameState.getEntityById(this.ent.unitAIOrderData()[0].target); if (ent && !ent.isHurt()) { // if someone gathers from it, it's only that the pathfinder sucks. - debug (ent.toString() + " is inaccessible from Combat"); + m.debug (ent.toString() + " is inaccessible from Combat"); ent.setMetadata(PlayerID, "inaccessible", true); this.ent.flee(ent); this.ent.setMetadata(PlayerID, "subrole", "idle"); this.gatheringFrom = undefined; } }*/ } } else if(subrole === "builder") { // check for transport. if (gameState.ai.playedTurn % 5 === 0) { if (this.ent.unitAIOrderData().length && this.ent.unitAIState().split(".")[2] === "APPROACHING" && this.ent.unitAIOrderData()[0]["target"]) { var ress = gameState.getEntityById(this.ent.unitAIOrderData()[0]["target"]); if (ress !== undefined) { var index = gameState.ai.accessibility.getAccessValue(ress.position()); var mIndex = gameState.ai.accessibility.getAccessValue(this.ent.position()); if (index !== mIndex && index !== 1) { //gameState.ai.HQ.navalManager.askForTransport(this.ent.id(), this.ent.position(), ress.position()); } } } } if (this.ent.unitAIState().split(".")[1] !== "REPAIR") { var target = gameState.getEntityById(this.ent.getMetadata(PlayerID, "target-foundation")); // okay so apparently we aren't working. // Unless we've been explicitely told to keep our role, make us idle. if (!target || target.foundationProgress() === undefined && target.needsRepair() == false) { if (!this.ent.getMetadata(PlayerID, "keepSubrole")) this.ent.setMetadata(PlayerID, "subrole", "idle"); } else this.ent.repair(target); } this.startApproachingResourceTime = gameState.getTimeElapsed(); //Engine.PostCommand({"type": "set-shading-color", "entities": [this.ent.id()], "rgb": [0,10,0]}); // TODO: we should maybe decide on our own to build other buildings, not rely on the assigntofoundation stuff. } else if(subrole === "hunter") { if (this.ent.isIdle()){ Engine.ProfileStart("Start Hunting"); this.startHunting(gameState, baseManager); Engine.ProfileStop(); } } }; // check if our current resource is unsatisfactory // this can happen in two ways: // -either we were on an unsatisfactory resource last time we started gathering (this.unsatisfactoryResource) // -Or we auto-moved to a bad resource thanks to the great UnitAI. -Worker.prototype.checkUnsatisfactoryResource = function(gameState) { +m.Worker.prototype.checkUnsatisfactoryResource = function(gameState) { if (this.unsatisfactoryResource) return true; if (this.ent.unitAIOrderData().length && this.ent.unitAIState().split(".")[1] === "GATHER" && this.ent.unitAIState().split(".")[2] === "GATHERING" && this.ent.unitAIOrderData()[0]["target"]) { var ress = gameState.getEntityById(this.ent.unitAIOrderData()[0]["target"]); if (!ress || !ress.getMetadata(PlayerID,"linked-dropsite") || !ress.getMetadata(PlayerID,"linked-dropsite-nearby") || gameState.ai.accessibility.getAccessValue(ress.position()) === -1) return true; } return false; }; -Worker.prototype.startGathering = function(baseManager, gameState) { +m.Worker.prototype.startGathering = function(baseManager, gameState) { + var resource = this.ent.getMetadata(PlayerID, "gather-type"); var ent = this.ent; var self = this; if (!ent.position()){ // TODO: work out what to do when entity has no position return; } // TODO: this is not necessarily optimal. // find closest dropsite which has nearby resources of the correct type var minDropsiteDist = Math.min(); // set to infinity initially var nearestResources = undefined; var nearestDropsite = undefined; // first step: count how many dropsites we have that have enough resources "close" to them. // TODO: this is a huge part of multi-base support. Count only those in the same base as the worker. var number = 0; - var ourDropsites = EntityCollectionFromIds(gameState,Object.keys(baseManager.dropsites)); + var ourDropsites = m.EntityCollectionFromIds(gameState,Object.keys(baseManager.dropsites)); if (ourDropsites.length === 0) { - debug ("We do not have a dropsite for " + resource + ", aborting"); + m.debug ("We do not have a dropsite for " + resource + ", aborting"); return; } var maxPerDP = 20; if (resource === "food") maxPerDP = 200; ourDropsites.forEach(function (dropsite) { if (baseManager.dropsites[dropsite.id()][resource] && baseManager.dropsites[dropsite.id()][resource][4] > 1000 && baseManager.dropsites[dropsite.id()][resource][5].length < maxPerDP) number++; }); // Allright second step, if there are any such dropsites, we pick the closest. // we pick one with a lot of resource, or we pick the only one available (if it's high enough, otherwise we'll see with "far" below). if (number > 0) { ourDropsites.forEach(function (dropsite) { //}){ if (baseManager.dropsites[dropsite.id()][resource] === undefined) return; if (dropsite.position() && (baseManager.dropsites[dropsite.id()][resource][4] > 1000 || (number === 1 && baseManager.dropsites[dropsite.id()][resource][4] > 200) ) && baseManager.dropsites[dropsite.id()][resource][5].length < maxPerDP) { - var dist = SquareVectorDistance(ent.position(), dropsite.position()); + var dist = API3.SquareVectorDistance(ent.position(), dropsite.position()); if (dist < minDropsiteDist){ minDropsiteDist = dist; nearestResources = baseManager.dropsites[dropsite.id()][resource][1]; nearestDropsite = dropsite; } } }); } + // we've found no fitting dropsites close enough from us. // So'll try with far away. if (!nearestResources || nearestResources.length === 0) { ourDropsites.forEach(function (dropsite) { //}){ if (baseManager.dropsites[dropsite.id()][resource] === undefined) return; if (dropsite.position() && baseManager.dropsites[dropsite.id()][resource][4] > 400 && baseManager.dropsites[dropsite.id()][resource][5].length < maxPerDP) { - var dist = SquareVectorDistance(ent.position(), dropsite.position()); + var dist = API3.SquareVectorDistance(ent.position(), dropsite.position()); if (dist < minDropsiteDist){ minDropsiteDist = dist; nearestResources = baseManager.dropsites[dropsite.id()][resource][1]; nearestDropsite = dropsite; } } }); } - + if (!nearestResources || nearestResources.length === 0){ if (resource === "food") if (this.buildAnyField(gameState)) return; if (this.unsatisfactoryResource == true) return; // we were already not satisfied, we're still not, change not. if (gameState.ai.HQ.switchWorkerBase(gameState, this.ent, resource)) return; - //debug ("No fitting dropsite for " + resource + " found, iterating the map."); + //m.debug ("No fitting dropsite for " + resource + " found, iterating the map."); nearestResources = gameState.getResourceSupplies(resource); this.unsatisfactoryResource = true; // TODO: should try setting up dropsites. } else { this.unsatisfactoryResource = false; } if (nearestResources.length === 0){ if (resource === "food") { if (this.buildAnyField(gameState)) return; if (gameState.ai.HQ.switchWorkerBase(gameState, this.ent, resource)) return; - debug("No " + resource + " found! (1)"); + m.debug("No " + resource + " found! (1)"); } else { if (gameState.ai.HQ.switchWorkerBase(gameState, this.ent, resource)) return; - debug("No " + resource + " found! (1)"); + m.debug("No " + resource + " found! (1)"); } return; } - //debug("Found " + nearestResources.length + "spots for " + resource); + //m.debug("Found " + nearestResources.length + "spots for " + resource); /*if (!nearestDropsite) { - debug ("No dropsite for " +resource); + m.debug ("No dropsite for " +resource); return; }*/ var supplies = []; var nearestSupplyDist = Math.min(); var nearestSupply = undefined; // filter resources // TODo: add a bonus for resources with a lot of resources left, perhaps, to spread gathering? nearestResources.forEach(function(supply) { //}){ // sanity check, perhaps sheep could be garrisoned? if (!supply.position()) { - //debug ("noposition"); + //m.debug ("noposition"); return; } if (supply.getMetadata(PlayerID, "inaccessible") === true) { - //debug ("inaccessible"); + //m.debug ("inaccessible"); return; } if (supply.isFull() === true || (gameState.turnCache["ressGathererNB"] && gameState.turnCache["ressGathererNB"][supply.id()] && gameState.turnCache["ressGathererNB"][supply.id()] + supply.resourceSupplyGatherers().length >= supply.maxGatherers)) return; // Don't gather enemy farms or farms from another base if ((!supply.isOwn(PlayerID) && supply.owner() !== 0) || (supply.isOwn(PlayerID) && supply.getMetadata(PlayerID,"base") !== self.baseID)) { - //debug ("enemy"); + //m.debug ("enemy"); return; } // quickscope accessbility check. if (!gameState.ai.accessibility.pathAvailable(gameState, ent.position(), supply.position())) { - //debug ("nopath"); + //m.debug ("nopath"); return; } // some simple check for chickens: if they're in a square that's inaccessible, we won't gather from them. if (supply.footprintRadius() < 1) { - var fakeMap = new Map(gameState.sharedScript,gameState.getMap().data); + 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; } } // measure the distance to the resource (largely irrelevant) - var dist = SquareVectorDistance(supply.position(), ent.position()); + var dist = API3.SquareVectorDistance(supply.position(), ent.position()); if (dist > 4900 && supply.hasClass("Animal")) return; // Add on a factor for the nearest dropsite if one exists if (nearestDropsite !== undefined ){ - dist += 4*SquareVectorDistance(supply.position(), nearestDropsite.position()); + dist += 4*API3.SquareVectorDistance(supply.position(), nearestDropsite.position()); dist /= 5.0; } - var territoryOwner = Map.createTerritoryMap(gameState).getOwner(supply.position()); + var territoryOwner = m.createTerritoryMap(gameState).getOwner(supply.position()); if (territoryOwner != PlayerID && territoryOwner != 0) { dist *= 5.0; //return; } else if (dist < 40000 && supply.resourceSupplyType().generic == "treasure"){ // go for treasures if they're not in enemy territory dist /= 1000; } if (dist < nearestSupplyDist) { nearestSupplyDist = dist; nearestSupply = supply; } }); if (nearestSupply) { var pos = nearestSupply.position(); // find a fitting dropsites in case we haven't already. if (!nearestDropsite) { ourDropsites.forEach(function (dropsite){ //}){ if (dropsite.position()){ - var dist = SquareVectorDistance(pos, dropsite.position()); + var dist = API3.SquareVectorDistance(pos, dropsite.position()); if (dist < minDropsiteDist){ minDropsiteDist = dist; nearestDropsite = dropsite; } } }); if (!nearestDropsite) { - debug ("No dropsite for " +resource); + m.debug ("No dropsite for " +resource); return; } } // if the resource is far away, try to build a farm instead. var tried = false; - if (resource === "food" && SquareVectorDistance(pos,this.ent.position()) > 22500) + if (resource === "food" && API3.SquareVectorDistance(pos,this.ent.position()) > 22500) { tried = this.buildAnyField(gameState); - if (!tried && SquareVectorDistance(pos,this.ent.position()) > 62500) { + if (!tried && API3.SquareVectorDistance(pos,this.ent.position()) > 62500) { // TODO: ought to change behavior here. return; // wait. a farm should appear. } } if (!tried) { if (!gameState.turnCache["ressGathererNB"]) { gameState.turnCache["ressGathererNB"] = {}; gameState.turnCache["ressGathererNB"][nearestSupply.id()] = 1; } else if (!gameState.turnCache["ressGathererNB"][nearestSupply.id()]) gameState.turnCache["ressGathererNB"][nearestSupply.id()] = 1; else gameState.turnCache["ressGathererNB"][nearestSupply.id()]++; - this.maxApproachTime = Math.max(30000, VectorDistance(pos,this.ent.position()) * 5000); + this.maxApproachTime = Math.max(30000, API3.VectorDistance(pos,this.ent.position()) * 5000); this.startApproachingResourceAmount = ent.resourceSupplyAmount(); this.startEnt = ent.id(); ent.gather(nearestSupply); ent.setMetadata(PlayerID, "target-foundation", undefined); } } else { if (resource === "food" && this.buildAnyField(gameState)) return; if (gameState.ai.HQ.switchWorkerBase(gameState, this.ent, resource)) return; if (resource !== "food") - debug("No " + resource + " found! (2)"); + m.debug("No " + resource + " found! (2)"); // If we had a fitting closest dropsite with a lot of resources, mark it as not good. It means it's probably full. Then retry. // it'll be resetted next time it's counted anyway. if (nearestDropsite && nearestDropsite.getMetadata(PlayerID, "resource-quantity-" +resource)+nearestDropsite.getMetadata(PlayerID, "resource-quantity-far-" +resource) > 400) { nearestDropsite.setMetadata(PlayerID, "resource-quantity-" +resource, 0); nearestDropsite.setMetadata(PlayerID, "resource-quantity-far-" +resource, 0); this.startGathering(baseManager, gameState); } } }; // Makes the worker deposit the currently carried resources at the closest dropsite -Worker.prototype.returnResources = function(gameState){ +m.Worker.prototype.returnResources = function(gameState){ if (!this.ent.resourceCarrying() || this.ent.resourceCarrying().length === 0){ return true; // assume we're OK. } var resource = this.ent.resourceCarrying()[0].type; var self = this; if (!this.ent.position()){ // TODO: work out what to do when entity has no position return true; } var closestDropsite = undefined; var dist = Math.min(); gameState.getOwnDropsites(resource).forEach(function(dropsite){ if (dropsite.position()){ - var d = SquareVectorDistance(self.ent.position(), dropsite.position()); + var d = API3.SquareVectorDistance(self.ent.position(), dropsite.position()); if (d < dist){ dist = d; closestDropsite = dropsite; } } }); if (!closestDropsite){ - debug("No dropsite found to deposit " + resource); + m.debug("No dropsite found to deposit " + resource); return false; } this.ent.returnResources(closestDropsite); return true; }; -Worker.prototype.startHunting = function(gameState, baseManager){ +m.Worker.prototype.startHunting = function(gameState, baseManager){ var ent = this.ent; if (!ent.position() || !baseManager.isHunting) return; // So here we're doing it basic. We check what we can hunt, we hunt it. No fancies. var resources = gameState.getResourceSupplies("food"); if (resources.length === 0){ - debug("No food found to hunt!"); + m.debug("No food found to hunt!"); return; } var supplies = []; var nearestSupplyDist = Math.min(); var nearestSupply = undefined; resources.forEach(function(supply) { //}){ if (!supply.position()) return; if (supply.getMetadata(PlayerID, "inaccessible") === true) return; if (supply.isFull() === true) return; if (!supply.hasClass("Animal")) return; // measure the distance to the resource - var dist = SquareVectorDistance(supply.position(), ent.position()); + var dist = API3.SquareVectorDistance(supply.position(), ent.position()); - var territoryOwner = Map.createTerritoryMap(gameState).getOwner(supply.position()); + var territoryOwner = m.createTerritoryMap(gameState).getOwner(supply.position()); if (territoryOwner != PlayerID && territoryOwner != 0) { dist *= 3.0; } // quickscope accessbility check if (!gameState.ai.accessibility.pathAvailable(gameState, ent.position(), supply.position(),false, true)) return; if (dist < nearestSupplyDist) { nearestSupplyDist = dist; nearestSupply = supply; } }); if (nearestSupply) { var pos = nearestSupply.position(); var nearestDropsite = 0; var minDropsiteDist = 1000000; // find a fitting dropsites in case we haven't already. gameState.getOwnDropsites("food").forEach(function (dropsite){ //}){ if (dropsite.position()){ - var dist = SquareVectorDistance(pos, dropsite.position()); + var dist = API3.SquareVectorDistance(pos, dropsite.position()); if (dist < minDropsiteDist){ minDropsiteDist = dist; nearestDropsite = dropsite; } } }); if (!nearestDropsite) { baseManager.isHunting = false; ent.setMetadata(PlayerID, "role", undefined); - debug ("No dropsite for hunting food"); + m.debug ("No dropsite for hunting food"); return; } if (minDropsiteDist > 35000) { baseManager.isHunting = false; ent.setMetadata(PlayerID, "role", undefined); } else { ent.gather(nearestSupply); ent.setMetadata(PlayerID, "target-foundation", undefined); } } else { baseManager.isHunting = false; ent.setMetadata(PlayerID, "role", undefined); - debug("No food found for hunting! (2)"); + m.debug("No food found for hunting! (2)"); } }; -Worker.prototype.getResourceType = function(type){ +m.Worker.prototype.getResourceType = function(type){ if (!type || !type.generic){ return undefined; } if (type.generic === "treasure"){ return type.specific; }else{ return type.generic; } }; -Worker.prototype.getGatherRate = function(gameState) { +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; - //debug (+rates[tstring] + " for " + tstring + " for " + this.ent._templateName); + //m.debug (+rates[tstring] + " for " + tstring + " for " + this.ent._templateName); if (rates[tstring]) return rates[tstring]; return 0; } return 0; }; -Worker.prototype.buildAnyField = function(gameState){ +m.Worker.prototype.buildAnyField = function(gameState){ var self = this; var okay = false; - var foundations = gameState.getOwnFoundations().filter(Filters.byMetadata(PlayerID,"base",this.baseID)); + var foundations = gameState.getOwnFoundations().filter(API3.Filters.byMetadata(PlayerID,"base",this.baseID)); foundations.filterNearest(this.ent.position(), foundations.length); foundations.forEach(function (found) { if (found._template.BuildRestrictions.Category === "Field" && !okay) { self.ent.repair(found); okay = true; return; } }); if (!okay) { var foundations = gameState.getOwnFoundations(); foundations.filterNearest(this.ent.position(), foundations.length); foundations.forEach(function (found) { if (found._template.BuildRestrictions.Category === "Field" && !okay) { self.ent.repair(found); self.ent.setMetadata(PlayerID,"base", found.getMetadata(PlayerID,"base")); okay = true; return; } }); } return okay; }; + +return m; +}(AEGIS); Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v2/base.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v2/base.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v2/base.js (revision 14441) @@ -1,308 +1,311 @@ +var PlayerID = -1; + function BaseAI(settings) { if (!settings) return; // Make some properties non-enumerable, so they won't be serialised Object.defineProperty(this, "_player", {value: settings.player, enumerable: false}); Object.defineProperty(this, "_templates", {value: settings.templates, enumerable: false}); Object.defineProperty(this, "_derivedTemplates", {value: {}, enumerable: false}); this._entityMetadata = {}; this._entityCollections = []; this._entityCollectionsByDynProp = {}; this._entityCollectionsUID = 0; this.turn = 0; } //Return a simple object (using no classes etc) that will be serialized //into saved games BaseAI.prototype.Serialize = function() { var rawEntities = {}; for (var id in this._entities) { rawEntities[id] = this._entities[id]._entity; } return { _rawEntities: rawEntities, _entityMetadata: this._entityMetadata, }; // TODO: ought to get the AI script subclass to serialize its own state }; //Called after the constructor when loading a saved game, with 'data' being //whatever Serialize() returned BaseAI.prototype.Deserialize = function(data) { var rawEntities = data._rawEntities; this._entityMetadata = data._entityMetadata; this._entities = {} for (var id in rawEntities) { this._entities[id] = new Entity(this, rawEntities[id]); } // TODO: ought to get the AI script subclass to deserialize its own state }; // Components that will be disabled in foundation entity templates. // (This is a bit yucky and fragile since it's the inverse of // CCmpTemplateManager::CopyFoundationSubset and only includes components // that our EntityTemplate class currently uses.) var g_FoundationForbiddenComponents = { "ProductionQueue": 1, "ResourceSupply": 1, "ResourceDropsite": 1, "GarrisonHolder": 1, }; // Components that will be disabled in resource entity templates. // Roughly the inverse of CCmpTemplateManager::CopyResourceSubset. var g_ResourceForbiddenComponents = { "Cost": 1, "Decay": 1, "Health": 1, "UnitAI": 1, "UnitMotion": 1, "Vision": 1 }; BaseAI.prototype.GetTemplate = function(name) { if (this._templates[name]) return this._templates[name]; if (this._derivedTemplates[name]) return this._derivedTemplates[name]; // If this is a foundation template, construct it automatically if (name.indexOf("foundation|") !== -1) { var base = this.GetTemplate(name.substr(11)); var foundation = {}; for (var key in base) if (!g_FoundationForbiddenComponents[key]) foundation[key] = base[key]; this._derivedTemplates[name] = foundation; return foundation; } else if (name.indexOf("resource|") !== -1) { var base = this.GetTemplate(name.substr(9)); var resource = {}; for (var key in base) if (!g_ResourceForbiddenComponents[key]) resource[key] = base[key]; this._derivedTemplates[name] = resource; return resource; } error("Tried to retrieve invalid template '"+name+"'"); return null; }; -BaseAI.prototype.HandleMessage = function(state) +BaseAI.prototype.HandleMessage = function(state, playerID) { + PlayerID = playerID; if (!this._entities) { // Do a (shallow) clone of all the initial entity properties (in order // to copy into our own script context and minimise cross-context // weirdness) this._entities = {}; for (var id in state.entities) { this._entities[id] = new Entity(this, state.entities[id]); } } else { this.ApplyEntitiesDelta(state); } Engine.ProfileStart("HandleMessage setup"); this.entities = new EntityCollection(this, this._entities); this.events = state.events; this.passabilityClasses = state.passabilityClasses; this.passabilityMap = state.passabilityMap; this.player = this._player; this.playerData = state.players[this._player]; this.templates = this._templates; this.territoryMap = state.territoryMap; this.timeElapsed = state.timeElapsed; Engine.ProfileStop(); this.OnUpdate(); // Clean up temporary properties, so they don't disturb the serializer delete this.entities; delete this.events; delete this.passabilityClasses; delete this.passabilityMap; delete this.player; delete this.playerData; delete this.templates; delete this.territoryMap; delete this.timeElapsed; }; BaseAI.prototype.ApplyEntitiesDelta = function(state) { Engine.ProfileStart("ApplyEntitiesDelta"); for each (var evt in state.events) { if (evt.type == "Create") { if (! state.entities[evt.msg.entity]) { continue; // Sometimes there are things like foundations which get destroyed too fast } this._entities[evt.msg.entity] = new Entity(this, state.entities[evt.msg.entity]); // Update all the entity collections since the create operation affects static properties as well as dynamic for each (var entCollection in this._entityCollections) { entCollection.updateEnt(this._entities[evt.msg.entity]); } } else if (evt.type == "Destroy") { if (!this._entities[evt.msg.entity]) { continue; } // The entity was destroyed but its data may still be useful, so // remember the entity and this AI's metadata concerning it evt.msg.metadata = (evt.msg.metadata || []); evt.msg.entityObj = (evt.msg.entityObj || this._entities[evt.msg.entity]); evt.msg.metadata[this._player] = this._entityMetadata[evt.msg.entity]; for each (var entCol in this._entityCollections) { entCol.removeEnt(this._entities[evt.msg.entity]); } delete this._entities[evt.msg.entity]; delete this._entityMetadata[evt.msg.entity]; } else if (evt.type == "TrainingFinished") { // Apply metadata stored in training queues, but only if they // look like they were added by us if (evt.msg.owner === this._player) { for each (var ent in evt.msg.entities) { for (var key in evt.msg.metadata) { this.setMetadata(this._entities[ent], key, evt.msg.metadata[key]) } } } } } for (var id in state.entities) { var changes = state.entities[id]; for (var prop in changes) { this._entities[id]._entity[prop] = changes[prop]; this.updateEntityCollections(prop, this._entities[id]); } } Engine.ProfileStop(); }; BaseAI.prototype.OnUpdate = function() { // AIs override this function // They should do at least this.turn++; }; BaseAI.prototype.chat = function(message) { - Engine.PostCommand({"type": "chat", "message": message}); + Engine.PostCommand(PlayerID, {"type": "chat", "message": message}); }; BaseAI.prototype.registerUpdatingEntityCollection = function(entCollection) { entCollection.setUID(this._entityCollectionsUID); this._entityCollections.push(entCollection); for each (var prop in entCollection.dynamicProperties()) { this._entityCollectionsByDynProp[prop] = this._entityCollectionsByDynProp[prop] || []; this._entityCollectionsByDynProp[prop].push(entCollection); } this._entityCollectionsUID++; }; BaseAI.prototype.removeUpdatingEntityCollection = function(entCollection) { for (var i in this._entityCollections) { if (this._entityCollections[i].getUID() === entCollection.getUID()) { this._entityCollections.splice(i, 1); } } for each (var prop in entCollection.dynamicProperties()) { for (var i in this._entityCollectionsByDynProp[prop]) { if (this._entityCollectionsByDynProp[prop][i].getUID() === entCollection.getUID()) { this._entityCollectionsByDynProp[prop].splice(i, 1); } } } }; BaseAI.prototype.updateEntityCollections = function(property, ent) { if (this._entityCollectionsByDynProp[property] !== undefined) { for each (var entCollection in this._entityCollectionsByDynProp[property]) { entCollection.updateEnt(ent); } } } BaseAI.prototype.setMetadata = function(ent, key, value) { var metadata = this._entityMetadata[ent.id()]; if (!metadata) metadata = this._entityMetadata[ent.id()] = {}; metadata[key] = value; this.updateEntityCollections('metadata', ent); this.updateEntityCollections('metadata.' + key, ent); } BaseAI.prototype.getMetadata = function(ent, key) { var metadata = this._entityMetadata[ent.id()]; if (!metadata || !(key in metadata)) return undefined; return metadata[key]; } Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v2/entity.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v2/entity.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v2/entity.js (revision 14441) @@ -1,454 +1,454 @@ var EntityTemplate = Class({ _init: function(template) { this._template = template; }, rank: function() { if (!this._template.Identity) return undefined; return this._template.Identity.Rank; }, classes: function() { if (!this._template.Identity || !this._template.Identity.Classes || !this._template.Identity.Classes._string) return undefined; return this._template.Identity.Classes._string.split(/\s+/); }, hasClass: function(name) { var classes = this.classes(); return (classes && classes.indexOf(name) != -1); }, civ: function() { if (!this._template.Identity) return undefined; return this._template.Identity.Civ; }, cost: function() { if (!this._template.Cost) return undefined; var ret = {}; for (var type in this._template.Cost.Resources) ret[type] = +this._template.Cost.Resources[type]; return ret; }, /** * Returns the radius of a circle surrounding this entity's * obstruction shape, or undefined if no obstruction. */ obstructionRadius: function() { if (!this._template.Obstruction) return undefined; if (this._template.Obstruction.Static) { var w = +this._template.Obstruction.Static["@width"]; var h = +this._template.Obstruction.Static["@depth"]; return Math.sqrt(w*w + h*h) / 2; } if (this._template.Obstruction.Unit) return +this._template.Obstruction.Unit["@radius"]; return 0; // this should never happen }, maxHitpoints: function() { if (this._template.Health !== undefined) return this._template.Health.Max; return 0; }, isHealable: function() { if (this._template.Health !== undefined) return this._template.Health.Unhealable !== "true"; return false; }, isRepairable: function() { if (this._template.Health !== undefined) return this._template.Health.Repairable === "true"; return false; }, armourStrengths: function() { if (!this._template.Armour) return undefined; return { hack: +this._template.Armour.Hack, pierce: +this._template.Armour.Pierce, crush: +this._template.Armour.Crush }; }, attackTypes: function() { if (!this._template.Attack) return undefined; var ret = []; for (var type in this._template.Attack) ret.push(type); return ret; }, attackRange: function(type) { if (!this._template.Attack || !this._template.Attack[type]) return undefined; return { max: +this._template.Attack[type].MaxRange, min: +(this._template.Attack[type].MinRange || 0) }; }, attackStrengths: function(type) { if (!this._template.Attack || !this._template.Attack[type]) return undefined; return { hack: +(this._template.Attack[type].Hack || 0), pierce: +(this._template.Attack[type].Pierce || 0), crush: +(this._template.Attack[type].Crush || 0) }; }, attackTimes: function(type) { if (!this._template.Attack || !this._template.Attack[type]) return undefined; return { prepare: +(this._template.Attack[type].PrepareTime || 0), repeat: +(this._template.Attack[type].RepeatTime || 1000) }; }, buildableEntities: function() { if (!this._template.Builder) return undefined; if (!this._template.Builder.Entities._string) return []; var civ = this.civ(); var templates = this._template.Builder.Entities._string.replace(/\{civ\}/g, civ).split(/\s+/); return templates; // TODO: map to Entity? }, trainableEntities: function() { if (!this._template.ProductionQueue || !this._template.ProductionQueue.Entities || !this._template.ProductionQueue.Entities._string) return undefined; var civ = this.civ(); var templates = this._template.ProductionQueue.Entities._string.replace(/\{civ\}/g, civ).split(/\s+/); return templates; }, resourceSupplyType: function() { if (!this._template.ResourceSupply) return undefined; var [type, subtype] = this._template.ResourceSupply.Type.split('.'); return { "generic": type, "specific": subtype }; }, resourceSupplyMax: function() { if (!this._template.ResourceSupply) return undefined; return +this._template.ResourceSupply.Amount; }, maxGatherers: function() { if (this._template.ResourceSupply !== undefined) return this._template.ResourceSupply.MaxGatherers; return 0; }, resourceGatherRates: function() { if (!this._template.ResourceGatherer) return undefined; var ret = {}; for (var r in this._template.ResourceGatherer.Rates) ret[r] = this._template.ResourceGatherer.Rates[r] * this._template.ResourceGatherer.BaseSpeed; return ret; }, resourceDropsiteTypes: function() { if (!this._template.ResourceDropsite) return undefined; return this._template.ResourceDropsite.Types.split(/\s+/); }, garrisonableClasses: function() { if (!this._template.GarrisonHolder || !this._template.GarrisonHolder.List._string) return undefined; return this._template.GarrisonHolder.List._string.split(/\s+/); }, /** * Returns whether this is an animal that is too difficult to hunt. * (Currently this includes all non-domestic animals.) */ isUnhuntable: function() { if (!this._template.UnitAI || !this._template.UnitAI.NaturalBehaviour) return false; // Ideally other animals should be huntable, but e.g. skittish animals // must be hunted by ranged units, and some animals may be too tough. return (this._template.UnitAI.NaturalBehaviour != "domestic"); }, buildCategory: function() { if (!this._template.BuildRestrictions || !this._template.BuildRestrictions.Category) return undefined; return this._template.BuildRestrictions.Category; }, buildDistance: function() { if (!this._template.BuildRestrictions || !this._template.BuildRestrictions.Distance) return undefined; return this._template.BuildRestrictions.Distance; }, buildPlacementType: function() { if (!this._template.BuildRestrictions || !this._template.BuildRestrictions.PlacementType) return undefined; return this._template.BuildRestrictions.PlacementType; }, buildTerritories: function() { if (!this._template.BuildRestrictions || !this._template.BuildRestrictions.Territory) return undefined; return this._template.BuildRestrictions.Territory.split(/\s+/); }, hasBuildTerritory: function(territory) { var territories = this.buildTerritories(); return (territories && territories.indexOf(territory) != -1); }, visionRange: function() { if (!this._template.Vision) return undefined; return this._template.Vision.Range; } }); var Entity = Class({ _super: EntityTemplate, _init: function(baseAI, entity) { this._super.call(this, baseAI.GetTemplate(entity.template)); this._ai = baseAI; this._templateName = entity.template; this._entity = entity; }, toString: function() { return "[Entity " + this.id() + " " + this.templateName() + "]"; }, id: function() { return this._entity.id; }, templateName: function() { return this._templateName; }, /** * Returns extra data that the AI scripts have associated with this entity, * for arbitrary local annotations. * (This data is not shared with any other AI scripts.) */ getMetadata: function(key) { return this._ai.getMetadata(this, key); }, /** * Sets extra data to be associated with this entity. */ setMetadata: function(key, value) { this._ai.setMetadata(this, key, value); }, deleteMetadata: function() { delete this._ai._entityMetadata[this.id()]; }, position: function() { return this._entity.position; }, isIdle: function() { if (typeof this._entity.idle === "undefined") return undefined; return this._entity.idle; }, unitAIState: function() { return this._entity.unitAIState; }, unitAIOrderData: function() { return this._entity.unitAIOrderData; }, hitpoints: function() { if (this._entity.hitpoints !== undefined) return this._entity.hitpoints; return undefined; }, isHurt: function() { return this.hitpoints() < this.maxHitpoints(); }, needsHeal: function() { return this.isHurt() && this.isHealable(); }, needsRepair: function() { return this.isHurt() && this.isRepairable(); }, /** * Returns the current training queue state, of the form * [ { "id": 0, "template": "...", "count": 1, "progress": 0.5, "metadata": ... }, ... ] */ trainingQueue: function() { var queue = this._entity.trainingQueue; return queue; }, trainingQueueTime: function() { var queue = this._entity.trainingQueue; if (!queue) return undefined; // TODO: compute total time for units in training queue return queue.length; }, foundationProgress: function() { if (typeof this._entity.foundationProgress === "undefined") return undefined; return this._entity.foundationProgress; }, owner: function() { return this._entity.owner; }, isOwn: function() { if (typeof this._entity.owner === "undefined") return false; return this._entity.owner === this._ai._player; }, isFriendly: function() { return this.isOwn(); // TODO: diplomacy }, isEnemy: function() { return !this.isOwn(); // TODO: diplomacy }, resourceSupplyAmount: function() { if(this._entity.resourceSupplyAmount === undefined) return undefined; return this._entity.resourceSupplyAmount; }, resourceSupplyGatherers: function() { if (this._entity.resourceSupplyGatherers !== undefined) return this._entity.resourceSupplyGatherers; return []; }, isFull: function() { if (this._entity.resourceSupplyGatherers !== undefined) return (this.maxGatherers() === this._entity.resourceSupplyGatherers.length); return undefined; }, resourceCarrying: function() { if(this._entity.resourceCarrying === undefined) return undefined; return this._entity.resourceCarrying; }, garrisoned: function() { return new EntityCollection(this._ai, this._entity.garrisoned); }, // TODO: visibility move: function(x, z, queued) { queued = queued || false; - Engine.PostCommand({"type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": queued}); + Engine.PostCommand(PlayerID, {"type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": queued}); return this; }, garrison: function(target) { - Engine.PostCommand({"type": "garrison", "entities": [this.id()], "target": target.id(),"queued": false}); + Engine.PostCommand(PlayerID, {"type": "garrison", "entities": [this.id()], "target": target.id(),"queued": false}); return this; }, attack: function(unitId) { - Engine.PostCommand({"type": "attack", "entities": [this.id()], "target": unitId, "queued": false}); + Engine.PostCommand(PlayerID, {"type": "attack", "entities": [this.id()], "target": unitId, "queued": false}); return this; }, gather: function(target, queued) { queued = queued || false; - Engine.PostCommand({"type": "gather", "entities": [this.id()], "target": target.id(), "queued": queued}); + Engine.PostCommand(PlayerID, {"type": "gather", "entities": [this.id()], "target": target.id(), "queued": queued}); return this; }, repair: function(target, queued) { queued = queued || false; - Engine.PostCommand({"type": "repair", "entities": [this.id()], "target": target.id(), "autocontinue": false, "queued": queued}); + Engine.PostCommand(PlayerID, {"type": "repair", "entities": [this.id()], "target": target.id(), "autocontinue": false, "queued": queued}); return this; }, returnResources: function(target, queued) { queued = queued || false; - Engine.PostCommand({"type": "returnresource", "entities": [this.id()], "target": target.id(), "queued": queued}); + Engine.PostCommand(PlayerID, {"type": "returnresource", "entities": [this.id()], "target": target.id(), "queued": queued}); return this; }, destroy: function() { - Engine.PostCommand({"type": "delete-entities", "entities": [this.id()]}); + Engine.PostCommand(PlayerID, {"type": "delete-entities", "entities": [this.id()]}); return this; }, train: function(type, count, metadata) { var trainable = this.trainableEntities(); if (!trainable) { error("Called train("+type+", "+count+") on non-training entity "+this); return this; } if (trainable.indexOf(type) === -1) { error("Called train("+type+", "+count+") on entity "+this+" which can't train that"); return this; } - Engine.PostCommand({ + Engine.PostCommand(PlayerID, { "type": "train", "entities": [this.id()], "template": type, "count": count, "metadata": metadata }); return this; }, construct: function(template, x, z, angle) { // TODO: verify this unit can construct this, just for internal // sanity-checking and error reporting - Engine.PostCommand({ + Engine.PostCommand(PlayerID, { "type": "construct", "entities": [this.id()], "template": template, "x": x, "z": z, "angle": angle, "autorepair": false, "autocontinue": false, "queued": false }); return this; }, }); Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v2/entitycollection.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v2/entitycollection.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v2/entitycollection.js (revision 14441) @@ -1,200 +1,200 @@ function EntityCollection(baseAI, entities, filters) { this._ai = baseAI; this._entities = entities; this._filters = filters || []; // Compute length lazily on demand, since it can be // expensive for large collections this._length = undefined; Object.defineProperty(this, "length", { get: function () { if (this._length === undefined) { this._length = 0; for (var id in entities) ++this._length; } return this._length; } }); } EntityCollection.prototype.toIdArray = function() { var ret = []; for (var id in this._entities) ret.push(+id); return ret; }; EntityCollection.prototype.toEntityArray = function() { var ret = []; for each (var ent in this._entities) ret.push(ent); return ret; }; EntityCollection.prototype.toString = function() { return "[EntityCollection " + this.toEntityArray().join(" ") + "]"; }; /** * Returns the (at most) n entities nearest to targetPos. */ EntityCollection.prototype.filterNearest = function(targetPos, n) { // Compute the distance of each entity var data = []; // [ [id, ent, distance], ... ] for (var id in this._entities) { var ent = this._entities[id]; if (ent.position()) data.push([id, ent, VectorDistance(targetPos, ent.position())]); } // Sort by increasing distance data.sort(function (a, b) { return (a[2] - b[2]); }); // Extract the first n var ret = {}; for each (var val in data.slice(0, n)) ret[val[0]] = val[1]; return new EntityCollection(this._ai, ret); }; EntityCollection.prototype.filter = function(filter, thisp) { if (typeof(filter) == "function") filter = {"func": filter, "dynamicProperties": []}; var ret = {}; for (var id in this._entities) { var ent = this._entities[id]; if (filter.func.call(thisp, ent, id, this)) ret[id] = ent; } return new EntityCollection(this._ai, ret, this._filters.concat([filter])); }; EntityCollection.prototype.filter_raw = function(callback, thisp) { var ret = {}; for (var id in this._entities) { var ent = this._entities[id]; var val = this._entities[id]._entity; if (callback.call(thisp, val, id, this)) ret[id] = ent; } return new EntityCollection(this._ai, ret); }; EntityCollection.prototype.forEach = function(callback, thisp) { for (var id in this._entities) { var ent = this._entities[id]; callback.call(thisp, ent, id, this); } return this; }; EntityCollection.prototype.move = function(x, z, queued) { queued = queued || false; - Engine.PostCommand({"type": "walk", "entities": this.toIdArray(), "x": x, "z": z, "queued": queued}); + Engine.PostCommand(PlayerID, {"type": "walk", "entities": this.toIdArray(), "x": x, "z": z, "queued": queued}); return this; }; EntityCollection.prototype.destroy = function() { - Engine.PostCommand({"type": "delete-entities", "entities": this.toIdArray()}); + Engine.PostCommand(PlayerID, {"type": "delete-entities", "entities": this.toIdArray()}); return this; }; // Removes an entity from the collection, returns true if the entity was a member, false otherwise EntityCollection.prototype.removeEnt = function(ent) { if (this._entities[ent.id()]) { // Checking length may initialize it, so do it before deleting. if (this.length !== undefined) this._length--; delete this._entities[ent.id()]; return true; } else { return false; } }; // Adds an entity to the collection, returns true if the entity was not member, false otherwise EntityCollection.prototype.addEnt = function(ent) { if (this._entities[ent.id()]) { return false; } else { // Checking length may initialize it, so do it before adding. if (this.length !== undefined) this._length++; this._entities[ent.id()] = ent; return true; } }; // Checks the entity against the filters, and adds or removes it appropriately, returns true if the // entity collection was modified. EntityCollection.prototype.updateEnt = function(ent) { var passesFilters = true; for each (var filter in this._filters) { passesFilters = passesFilters && filter.func(ent); } if (passesFilters) { return this.addEnt(ent); } else { return this.removeEnt(ent); } }; EntityCollection.prototype.registerUpdates = function() { this._ai.registerUpdatingEntityCollection(this); }; EntityCollection.prototype.dynamicProperties = function() { var ret = []; for each (var filter in this._filters) { ret = ret.concat(filter.dynamicProperties); } return ret; }; EntityCollection.prototype.setUID = function(id) { this._UID = id; }; EntityCollection.prototype.getUID = function() { return this._UID; }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/class.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/class.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/class.js (revision 14441) @@ -1,36 +1,42 @@ +var API3 = function(m) +{ /** * Provides a nicer syntax for defining classes, * with support for OO-style inheritance. */ -function Class(data) +m.Class = function(data) { var ctor; if (data._init) ctor = data._init; else ctor = function() { }; if (data._super) { ctor.prototype = { "__proto__": data._super.prototype }; } for (var key in data) { ctor.prototype[key] = data[key]; } return ctor; } /* Test inheritance: var A = Class({foo:1, bar:10}); print((new A).foo+" "+(new A).bar+"\n"); var B = Class({foo:2, bar:20}); print((new A).foo+" "+(new A).bar+"\n"); print((new B).foo+" "+(new B).bar+"\n"); var C = Class({_super:A, foo:3}); print((new A).foo+" "+(new A).bar+"\n"); print((new B).foo+" "+(new B).bar+"\n"); print((new C).foo+" "+(new C).bar+"\n"); //*/ + +return m; + +}(API3); Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/entitycollection.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/entitycollection.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/entitycollection.js (revision 14441) @@ -1,385 +1,392 @@ -function EntityCollection(sharedAI, entities, filters) +var API3 = function(m) +{ + +m.EntityCollection = function(sharedAI, entities, filters) { this._ai = sharedAI; this._entities = entities || {}; this._filters = filters || []; this._quickIter = false; // will make the entity collection store an array (not associative) of entities used when calling "foreach". // probably should not be usde for very dynamic entity collections. // Compute length lazily on demand, since it can be // expensive for large collections this._length = undefined; Object.defineProperty(this, "length", { get: function () { if (this._length === undefined) { this._length = 0; for (var id in entities) ++this._length; } return this._length; } }); this.frozen = false; -} +}; // If an entitycollection is frozen, it will never automatically add a unit. // But can remove one. // this makes it easy to create entity collection that will auto-remove dead units // but never add new ones. -EntityCollection.prototype.freeze = function() +m.EntityCollection.prototype.freeze = function() { this.frozen = true; }; -EntityCollection.prototype.defreeze = function() +m.EntityCollection.prototype.defreeze = function() { this.frozen = false; }; -EntityCollection.prototype.allowQuickIter = function() +m.EntityCollection.prototype.allowQuickIter = function() { this._quickIter = true; this._entitiesArray = []; for each (var ent in this._entities) this._entitiesArray.push(ent); }; -EntityCollection.prototype.toIdArray = function() +m.EntityCollection.prototype.toIdArray = function() { var ret = []; for (var id in this._entities) ret.push(+id); return ret; }; -EntityCollection.prototype.toEntityArray = function() +m.EntityCollection.prototype.toEntityArray = function() { if (this._quickIter === true) return this._entitiesArray; var ret = []; for each (var ent in this._entities) ret.push(ent); return ret; }; -EntityCollection.prototype.toString = function() +m.EntityCollection.prototype.toString = function() { return "[EntityCollection " + this.toEntityArray().join(" ") + "]"; }; /** * Returns the (at most) n entities nearest to targetPos. */ -EntityCollection.prototype.filterNearest = function(targetPos, n) +m.EntityCollection.prototype.filterNearest = function(targetPos, n) { // Compute the distance of each entity var data = []; // [id, ent, distance] if (this._quickIter === true) { for (var i in this._entitiesArray) { var ent = this._entitiesArray[i]; if (ent.position() !== -1) - data.push([ent.id(), ent, SquareVectorDistance(targetPos, ent.position())]); + data.push([ent.id(), ent, m.SquareVectorDistance(targetPos, ent.position())]); } } else { for (var id in this._entities) { var ent = this._entities[id]; if (ent.position() !== -1) - data.push([id, ent, SquareVectorDistance(targetPos, ent.position())]); + data.push([id, ent, m.SquareVectorDistance(targetPos, ent.position())]); } } // Sort by increasing distance data.sort(function (a, b) { return (a[2] - b[2]); }); // Extract the first n var ret = {}; var length = Math.min(n, entData.length); for (var i = 0; i < length; ++i) ret[data[i][0]] = data[i][1]; - return new EntityCollection(this._ai, ret); + return new m.EntityCollection(this._ai, ret); }; -EntityCollection.prototype.filter = function(filter, thisp) +m.EntityCollection.prototype.filter = function(filter, thisp) { if (typeof(filter) == "function") filter = {"func": filter, "dynamicProperties": []}; var ret = {}; if (this._quickIter === true) { for (var i in this._entitiesArray) { var ent = this._entitiesArray[i]; var id = ent.id(); if (filter.func.call(thisp, ent, id, this)) ret[id] = ent; } } else { for (var id in this._entities) { var ent = this._entities[id]; if (filter.func.call(thisp, ent, id, this)) ret[id] = ent; } } - return new EntityCollection(this._ai, ret, this._filters.concat([filter])); + return new m.EntityCollection(this._ai, ret, this._filters.concat([filter])); }; -EntityCollection.prototype.filter_raw = function(callback, thisp) +m.EntityCollection.prototype.filter_raw = function(callback, thisp) { var ret = {}; for (var id in this._entities) { var ent = this._entities[id]; var val = this._entities[id]._entity; if (callback.call(thisp, val, id, this)) ret[id] = ent; } - return new EntityCollection(this._ai, ret); + return new m.EntityCollection(this._ai, ret); }; -EntityCollection.prototype.forEach = function(callback) +m.EntityCollection.prototype.forEach = function(callback) { if (this._quickIter === true) { for (var id in this._entitiesArray) { callback(this._entitiesArray[id]); } return this; } for (var id in this._entities) { callback(this._entities[id]); } return this; }; -EntityCollection.prototype.filterNearest = function(targetPos, n) +m.EntityCollection.prototype.filterNearest = function(targetPos, n) { // Compute the distance of each entity var data = []; // [ [id, ent, distance], ... ] for (var id in this._entities) { var ent = this._entities[id]; if (ent.position()) - data.push([id, ent, SquareVectorDistance(targetPos, ent.position())]); + data.push([id, ent, m.SquareVectorDistance(targetPos, ent.position())]); } // Sort by increasing distance data.sort(function (a, b) { return (a[2] - b[2]); }); if (n === undefined) n = this._length; // Extract the first n var ret = {}; for each (var val in data.slice(0, n)) ret[val[0]] = val[1]; - return new EntityCollection(this._ai, ret); + return new m.EntityCollection(this._ai, ret); }; -EntityCollection.prototype.move = function(x, z, queued) +m.EntityCollection.prototype.move = function(x, z, queued) { queued = queued || false; - Engine.PostCommand({"type": "walk", "entities": this.toIdArray(), "x": x, "z": z, "queued": queued}); + Engine.PostCommand(PlayerID,{"type": "walk", "entities": this.toIdArray(), "x": x, "z": z, "queued": queued}); return this; }; -EntityCollection.prototype.attackMove = function(x, z, queued) +m.EntityCollection.prototype.attackMove = function(x, z, queued) { queued = queued || false; Engine.PostCommand({"type": "attack-walk", "entities": this.toIdArray(), "x": x, "z": z, "queued": queued}); return this; }; -EntityCollection.prototype.moveIndiv = function(x, z, queued) +m.EntityCollection.prototype.moveIndiv = function(x, z, queued) { queued = queued || false; for (var id in this._entities) { // The following try {} finally {} block is a workaround for OOS problems in multiplayer games with AI players (check ticket #2000). // It disables JIT compiling of this loop. Don't remove it unless you are sure that the underlying issue has been resolved! // TODO: Check this again after the SpiderMonkey upgrade. try {} finally {} - Engine.PostCommand({"type": "walk", "entities": [this._entities[id].id()], "x": x, "z": z, "queued": queued}); + Engine.PostCommand(PlayerID,{"type": "walk", "entities": [this._entities[id].id()], "x": x, "z": z, "queued": queued}); } return this; }; -EntityCollection.prototype.destroy = function() +m.EntityCollection.prototype.destroy = function() { - Engine.PostCommand({"type": "delete-entities", "entities": this.toIdArray()}); + Engine.PostCommand(PlayerID,{"type": "delete-entities", "entities": this.toIdArray()}); return this; }; -EntityCollection.prototype.attack = function(unit) +m.EntityCollection.prototype.attack = function(unit) { var unitId; if (typeof(unit) === "Entity") { unitId = unit.id(); } else { unitId = unit; } - Engine.PostCommand({"type": "attack", "entities": this.toIdArray(), "target": unitId, "queued": false}); + Engine.PostCommand(PlayerID,{"type": "attack", "entities": this.toIdArray(), "target": unitId, "queued": false}); return this; }; // violent, aggressive, defensive, passive, standground -EntityCollection.prototype.setStance = function(stance) +m.EntityCollection.prototype.setStance = function(stance) { - Engine.PostCommand({"type": "stance", "entities": this.toIdArray(), "name" : stance, "queued": false}); + Engine.PostCommand(PlayerID,{"type": "stance", "entities": this.toIdArray(), "name" : stance, "queued": false}); return this; }; // Returns the average position of all units -EntityCollection.prototype.getCentrePosition = function() +m.EntityCollection.prototype.getCentrePosition = function() { var sumPos = [0, 0]; var count = 0; this.forEach(function(ent) { if (ent.position()) { sumPos[0] += ent.position()[0]; sumPos[1] += ent.position()[1]; count ++; } }); if (count === 0) { return undefined; } else { return [sumPos[0]/count, sumPos[1]/count]; } }; // returns the average position from the sample first units. // This might be faster for huge collections, but there's // always a risk that it'll be unprecise. -EntityCollection.prototype.getApproximatePosition = function(sample) +m.EntityCollection.prototype.getApproximatePosition = function(sample) { var sumPos = [0, 0]; var i = 0; for (var id in this._entities) { var ent = this._entities[id]; if (ent.position()) { sumPos[0] += ent.position()[0]; sumPos[1] += ent.position()[1]; i++; } if (i === sample) break; } if (sample === 0) { return undefined; } else { return [sumPos[0]/i, sumPos[1]/i]; } }; // Removes an entity from the collection, returns true if the entity was a member, false otherwise -EntityCollection.prototype.removeEnt = function(ent) +m.EntityCollection.prototype.removeEnt = function(ent) { if (this._entities[ent.id()]) { // Checking length may initialize it, so do it before deleting. if (this.length !== undefined) this._length--; if (this._quickIter === true) this._entitiesArray.splice(this._entitiesArray.indexOf(ent),1); delete this._entities[ent.id()]; return true; } else { return false; } }; // Adds an entity to the collection, returns true if the entity was not member, false otherwise -EntityCollection.prototype.addEnt = function(ent) +m.EntityCollection.prototype.addEnt = function(ent) { if (this._entities[ent.id()]) { return false; } else { // Checking length may initialize it, so do it before adding. if (this.length !== undefined) this._length++; this._entities[ent.id()] = ent; if (this._quickIter === true) this._entitiesArray.push(ent); return true; } }; // Checks the entity against the filters, and adds or removes it appropriately, returns true if the // entity collection was modified. // Force can add a unit despite a freezing. // If an entitycollection is frozen, it will never automatically add a unit. // But can remove one. -EntityCollection.prototype.updateEnt = function(ent, force) +m.EntityCollection.prototype.updateEnt = function(ent, force) { var passesFilters = true; for each (var filter in this._filters) { passesFilters = passesFilters && filter.func(ent); } if (passesFilters) { if (!force && this.frozen) return false; return this.addEnt(ent); } else { return this.removeEnt(ent); } }; -EntityCollection.prototype.registerUpdates = function(noPush) +m.EntityCollection.prototype.registerUpdates = function(noPush) { this._ai.registerUpdatingEntityCollection(this,noPush); }; -EntityCollection.prototype.unregister = function() +m.EntityCollection.prototype.unregister = function() { this._ai.removeUpdatingEntityCollection(this); }; -EntityCollection.prototype.dynamicProperties = function() +m.EntityCollection.prototype.dynamicProperties = function() { var ret = []; for each (var filter in this._filters) { ret = ret.concat(filter.dynamicProperties); } return ret; }; -EntityCollection.prototype.setUID = function(id) +m.EntityCollection.prototype.setUID = function(id) { this._UID = id; }; -EntityCollection.prototype.getUID = function() +m.EntityCollection.prototype.getUID = function() { return this._UID; }; + +return m; + +}(API3); Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/gamestate.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/gamestate.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/gamestate.js (revision 14441) @@ -1,640 +1,648 @@ +var API3 = function(m) +{ + + /** * Provides an API for the rest of the AI scripts to query the world state at a * higher level than the raw data. */ -var GameState = function() { +m.GameState = function() { this.ai = null; // must be updated by the AIs. this.cellSize = 4.0; // Size of each map tile this.buildingsBuilt = 0; this.turnCache = {}; }; -GameState.prototype.init = function(SharedScript, state, player) { +m.GameState.prototype.init = function(SharedScript, state, player) { this.sharedScript = SharedScript; this.EntCollecNames = SharedScript._entityCollectionsName; this.EntCollec = SharedScript._entityCollections; this.timeElapsed = SharedScript.timeElapsed; this.templates = SharedScript._templates; this.techTemplates = SharedScript._techTemplates; this.entities = SharedScript.entities; this.player = player; this.playerData = this.sharedScript.playersData[this.player]; this.techModifications = SharedScript._techModifications[this.player]; }; -GameState.prototype.update = function(SharedScript, state) { +m.GameState.prototype.update = function(SharedScript, state) { this.sharedScript = SharedScript; this.EntCollecNames = SharedScript._entityCollectionsName; this.EntCollec = SharedScript._entityCollections; this.timeElapsed = SharedScript.timeElapsed; this.templates = SharedScript._templates; this.techTemplates = SharedScript._techTemplates; this._entities = SharedScript._entities; this.entities = SharedScript.entities; this.playerData = SharedScript.playersData[this.player]; this.techModifications = SharedScript._techModifications[this.player]; this.buildingsBuilt = 0; this.turnCache = {}; }; -GameState.prototype.updatingCollection = function(id, filter, collection, allowQuick){ +m.GameState.prototype.updatingCollection = function(id, filter, collection, allowQuick){ // automatically add the player ID id = this.player + "-" + id; if (!this.EntCollecNames[id]){ if (collection !== undefined) this.EntCollecNames[id] = collection.filter(filter); else { this.EntCollecNames[id] = this.entities.filter(filter); } if (allowQuick) this.EntCollecNames[id].allowQuickIter(); this.EntCollecNames[id].registerUpdates(); // warn ("New Collection named " +id); } return this.EntCollecNames[id]; }; -GameState.prototype.destroyCollection = function(id){ +m.GameState.prototype.destroyCollection = function(id){ // automatically add the player ID id = this.player + "-" + id; if (this.EntCollecNames[id] !== undefined){ this.sharedScript.removeUpdatingEntityCollection(this.EntCollecNames[id]); delete this.EntCollecNames[id]; } }; -GameState.prototype.getEC = function(id){ +m.GameState.prototype.getEC = function(id){ // automatically add the player ID id = this.player + "-" + id; if (this.EntCollecNames[id] !== undefined) return this.EntCollecNames[id]; return undefined; }; -GameState.prototype.updatingGlobalCollection = function(id, filter, collection, allowQuick) { +m.GameState.prototype.updatingGlobalCollection = function(id, filter, collection, allowQuick) { if (!this.EntCollecNames[id]){ if (collection !== undefined) this.EntCollecNames[id] = collection.filter(filter); else this.EntCollecNames[id] = this.entities.filter(filter); if (allowQuick) this.EntCollecNames[id].allowQuickIter(); this.EntCollecNames[id].registerUpdates(); //warn ("New Global Collection named " +id); } return this.EntCollecNames[id]; }; -GameState.prototype.destroyGlobalCollection = function(id) +m.GameState.prototype.destroyGlobalCollection = function(id) { if (this.EntCollecNames[id] !== undefined){ this.sharedScript.removeUpdatingEntityCollection(this.EntCollecNames[id]); delete this.EntCollecNames[id]; } }; -GameState.prototype.getGEC = function(id) +m.GameState.prototype.getGEC = function(id) { if (this.EntCollecNames[id] !== undefined) return this.EntCollecNames[id]; return undefined; }; -GameState.prototype.currentPhase = function() +m.GameState.prototype.currentPhase = function() { if (this.isResearched("phase_city")) return 3; if (this.isResearched("phase_town")) return 2; if (this.isResearched("phase_village")) return 1; return 0; }; -GameState.prototype.townPhase = function() +m.GameState.prototype.townPhase = function() { if (this.playerData.civ == "athen") return "phase_town_athen"; return "phase_town_generic"; }; -GameState.prototype.cityPhase = function() +m.GameState.prototype.cityPhase = function() { return "phase_city_generic"; }; -GameState.prototype.isResearched = function(template) +m.GameState.prototype.isResearched = function(template) { return this.playerData.researchedTechs[template] !== undefined; }; // true if started or queued -GameState.prototype.isResearching = function(template) +m.GameState.prototype.isResearching = function(template) { return (this.playerData.researchStarted[template] !== undefined || this.playerData.researchQueued[template] !== undefined); }; // this is an absolute check that doesn't check if we have a building to research from. -GameState.prototype.canResearch = function(techTemplateName, noRequirementCheck) +m.GameState.prototype.canResearch = function(techTemplateName, noRequirementCheck) { var template = this.getTemplate(techTemplateName); if (!template) return false; // researching or already researched: NOO. if (this.playerData.researchQueued[techTemplateName] || this.playerData.researchStarted[techTemplateName] || this.playerData.researchedTechs[techTemplateName]) return false; if (noRequirementCheck === true) return true; // not already researched, check if we can. // basically a copy of the function in technologyManager since we can't use it. // Checks the requirements for a technology to see if it can be researched at the current time // The technology which this technology supersedes is required if (template.supersedes() && !this.playerData.researchedTechs[template.supersedes()]) return false; // if this is a pair, we must check that the paire tech is not being researched if (template.pair()) { var other = template.pairedWith(); if (this.playerData.researchQueued[other] || this.playerData.researchStarted[other] || this.playerData.researchedTechs[other]) return false; } return this.checkTechRequirements(template.requirements()); } // Private function for checking a set of requirements is met // basically copies TechnologyManager's -GameState.prototype.checkTechRequirements = function (reqs) +m.GameState.prototype.checkTechRequirements = function (reqs) { // If there are no requirements then all requirements are met if (!reqs) return true; if (reqs.tech) { return (this.playerData.researchedTechs[reqs.tech] !== undefined && this.playerData.researchedTechs[reqs.tech]); } else if (reqs.all) { for (var i = 0; i < reqs.all.length; i++) { if (!this.checkTechRequirements(reqs.all[i])) return false; } return true; } else if (reqs.any) { for (var i = 0; i < reqs.any.length; i++) { if (this.checkTechRequirements(reqs.any[i])) return true; } return false; } else if (reqs.class) { if (reqs.numberOfTypes) { if (this.playerData.typeCountsByClass[reqs.class]) return (reqs.numberOfTypes <= Object.keys(this.playerData.typeCountsByClass[reqs.class]).length); else return false; } else if (reqs.number) { if (this.playerData.classCounts[reqs.class]) return (reqs.number <= this.playerData.classCounts[reqs.class]); else return false; } } else if (reqs.civ) { if (this.playerData.civ == reqs.civ) return true; else return false; } // The technologies requirements are not a recognised format error("Bad requirements " + uneval(reqs)); return false; }; -GameState.prototype.getTimeElapsed = function() +m.GameState.prototype.getTimeElapsed = function() { return this.timeElapsed; }; -GameState.prototype.getTemplate = function(type) +m.GameState.prototype.getTemplate = function(type) { if (this.techTemplates[type] !== undefined) - return new Technology(this.techTemplates, type); + return new m.Technology(this.techTemplates, type); if (!this.templates[type]) return null; - return new EntityTemplate(this.templates[type], this.techModifications); + return new m.EntityTemplate(this.templates[type], this.techModifications); }; -GameState.prototype.applyCiv = function(str) { +m.GameState.prototype.applyCiv = function(str) { return str.replace(/\{civ\}/g, this.playerData.civ); }; -GameState.prototype.civ = function() { +m.GameState.prototype.civ = function() { return this.playerData.civ; }; /** * @returns {Resources} */ -GameState.prototype.getResources = function() { - return new Resources(this.playerData.resourceCounts); +m.GameState.prototype.getResources = function() { + return new m.Resources(this.playerData.resourceCounts); }; -GameState.prototype.getMap = function() { +m.GameState.prototype.getMap = function() { return this.sharedScript.passabilityMap; }; -GameState.prototype.getPopulation = function() { +m.GameState.prototype.getPopulation = function() { return this.playerData.popCount; }; -GameState.prototype.getPopulationLimit = function() { +m.GameState.prototype.getPopulationLimit = function() { return this.playerData.popLimit; }; -GameState.prototype.getPopulationMax = function() { +m.GameState.prototype.getPopulationMax = function() { return this.playerData.popMax; }; -GameState.prototype.getPassabilityClassMask = function(name) { +m.GameState.prototype.getPassabilityClassMask = function(name) { if (!(name in this.sharedScript.passabilityClasses)){ error("Tried to use invalid passability class name '" + name + "'"); } return this.sharedScript.passabilityClasses[name]; }; -GameState.prototype.getPlayerID = function() { +m.GameState.prototype.getPlayerID = function() { return this.player; }; -GameState.prototype.isPlayerAlly = function(id) { +m.GameState.prototype.isPlayerAlly = function(id) { return this.playerData.isAlly[id]; }; -GameState.prototype.isPlayerEnemy = function(id) { +m.GameState.prototype.isPlayerEnemy = function(id) { return this.playerData.isEnemy[id]; }; -GameState.prototype.getEnemies = function(){ +m.GameState.prototype.getEnemies = function(){ var ret = []; for (var i in this.playerData.isEnemy){ if (this.playerData.isEnemy[i]){ ret.push(i); } } return ret; }; -GameState.prototype.isEntityAlly = function(ent) { +m.GameState.prototype.isEntityAlly = function(ent) { if (ent && ent.owner && (typeof ent.owner) === "function"){ return this.playerData.isAlly[ent.owner()]; } else if (ent && ent.owner){ return this.playerData.isAlly[ent.owner]; } return false; }; -GameState.prototype.isEntityEnemy = function(ent) { +m.GameState.prototype.isEntityEnemy = function(ent) { if (ent && ent.owner && (typeof ent.owner) === "function"){ return this.playerData.isEnemy[ent.owner()]; } else if (ent && ent.owner){ return this.playerData.isEnemy[ent.owner]; } return false; }; -GameState.prototype.isEntityOwn = function(ent) { +m.GameState.prototype.isEntityOwn = function(ent) { if (ent && ent.owner && (typeof ent.owner) === "function"){ return ent.owner() == this.player; } else if (ent && ent.owner){ return ent.owner == this.player; } return false; }; -GameState.prototype.getOwnEntities = function() { - return this.updatingCollection("own-entities", Filters.byOwner(this.player)); +m.GameState.prototype.getOwnEntities = function() { + return this.updatingCollection("own-entities", m.Filters.byOwner(this.player)); }; -GameState.prototype.getEnemyEntities = function() { +m.GameState.prototype.getEnemyEntities = function() { var diplomacyChange = false; var enemies = this.getEnemies(); if (this.enemies){ if (this.enemies.length != enemies.length){ diplomacyChange = true; }else{ for (var i = 0; i < enemies.length; i++){ if (enemies[i] !== this.enemies[i]){ diplomacyChange = true; } } } } if (diplomacyChange || !this.enemies){ - return this.updatingCollection("enemy-entities", Filters.byOwners(enemies)); + return this.updatingCollection("enemy-entities", m.Filters.byOwners(enemies)); this.enemies = enemies; } return this.getEC("enemy-entities"); }; -GameState.prototype.getEntities = function() { +m.GameState.prototype.getEntities = function() { return this.entities; }; -GameState.prototype.getEntityById = function(id){ +m.GameState.prototype.getEntityById = function(id){ if (this.entities._entities[id]) { return this.entities._entities[id]; }else{ //debug("Entity " + id + " requested does not exist"); } return undefined; }; -GameState.prototype.getOwnEntitiesByMetadata = function(key, value, maintain){ +m.GameState.prototype.getOwnEntitiesByMetadata = function(key, value, maintain){ if (maintain === true) - return this.updatingCollection(key + "-" + value, Filters.byMetadata(this.player, key, value),this.getOwnEntities()); - return this.getOwnEntities().filter(Filters.byMetadata(this.player, key, value)); + return this.updatingCollection(key + "-" + value, m.Filters.byMetadata(this.player, key, value),this.getOwnEntities()); + return this.getOwnEntities().filter(m.Filters.byMetadata(this.player, key, value)); }; -GameState.prototype.getOwnEntitiesByRole = function(role){ +m.GameState.prototype.getOwnEntitiesByRole = function(role){ return this.getOwnEntitiesByMetadata("role", role, true); }; -GameState.prototype.getOwnTrainingFacilities = function(){ - return this.updatingCollection("own-training-facilities", Filters.byTrainingQueue(), this.getOwnEntities(), true); +m.GameState.prototype.getOwnTrainingFacilities = function(){ + return this.updatingCollection("own-training-facilities", m.Filters.byTrainingQueue(), this.getOwnEntities(), true); }; -GameState.prototype.getOwnResearchFacilities = function(){ - return this.updatingCollection("own-research-facilities", Filters.byResearchAvailable(), this.getOwnEntities(), true); +m.GameState.prototype.getOwnResearchFacilities = function(){ + return this.updatingCollection("own-research-facilities", m.Filters.byResearchAvailable(), this.getOwnEntities(), true); }; -GameState.prototype.getOwnEntitiesByType = function(type, maintain){ - var filter = Filters.byType(type); +m.GameState.prototype.getOwnEntitiesByType = function(type, maintain){ + var filter = m.Filters.byType(type); if (maintain === true) return this.updatingCollection("own-by-type-" + type, filter, this.getOwnEntities()); return this.getOwnEntities().filter(filter); }; -GameState.prototype.countEntitiesByType = function(type, maintain) { +m.GameState.prototype.countEntitiesByType = function(type, maintain) { return this.getOwnEntitiesByType(type, maintain).length; }; -GameState.prototype.countEntitiesAndQueuedByType = function(type) { +m.GameState.prototype.countEntitiesAndQueuedByType = function(type) { var count = this.countEntitiesByType(type, true); // Count building foundations if (this.getTemplate(type).hasClass("Structure") === true) count += this.countEntitiesByType("foundation|" + type, true); else if (this.getTemplate(type).resourceSupplyType() !== undefined) // animal resources count += this.countEntitiesByType("resource|" + type, true); else { // Count entities in building production queues // TODO: maybe this fails for corrals. this.getOwnTrainingFacilities().forEach(function(ent){ ent.trainingQueue().forEach(function(item) { if (item.unitTemplate == type){ count += item.count; } }); }); } return count; }; -GameState.prototype.countFoundationsWithType = function(type) { +m.GameState.prototype.countFoundationsWithType = function(type) { var foundationType = "foundation|" + type; var count = 0; this.getOwnEntities().forEach(function(ent) { var t = ent.templateName(); if (t == foundationType) ++count; }); return count; }; -GameState.prototype.countOwnEntitiesByRole = function(role) { +m.GameState.prototype.countOwnEntitiesByRole = function(role) { return this.getOwnEntitiesByRole(role).length; }; -GameState.prototype.countOwnEntitiesAndQueuedWithRole = function(role) { +m.GameState.prototype.countOwnEntitiesAndQueuedWithRole = function(role) { var count = this.countOwnEntitiesByRole(role); // Count entities in building production queues this.getOwnTrainingFacilities().forEach(function(ent) { ent.trainingQueue().forEach(function(item) { if (item.metadata && item.metadata.role == role) count += item.count; }); }); return count; }; -GameState.prototype.countOwnQueuedEntitiesWithMetadata = function(data, value) { +m.GameState.prototype.countOwnQueuedEntitiesWithMetadata = function(data, value) { // Count entities in building production queues var count = 0; this.getOwnTrainingFacilities().forEach(function(ent) { ent.trainingQueue().forEach(function(item) { if (item.metadata && item.metadata[data] && item.metadata[data] == value) count += item.count; }); }); return count; }; /** * Find buildings that are capable of training the given unit type, and aren't * already too busy. */ -GameState.prototype.findTrainers = function(template) { +m.GameState.prototype.findTrainers = function(template) { var maxQueueLength = 3; // avoid tying up resources in giant training queues return this.getOwnTrainingFacilities().filter(function(ent) { var trainable = ent.trainableEntities(); if (!trainable || trainable.indexOf(template) == -1) return false; var queue = ent.trainingQueue(); if (queue) { if (queue.length >= maxQueueLength) return false; } return true; }); }; /** * Find units that are capable of constructing the given building type. */ -GameState.prototype.findBuilders = function(template) { +m.GameState.prototype.findBuilders = function(template) { return this.getOwnEntities().filter(function(ent) { var buildable = ent.buildableEntities(); if (!buildable || buildable.indexOf(template) == -1) return false; return true; }); }; /** * Find buildings that are capable of researching the given tech, and aren't * already too busy. */ -GameState.prototype.findResearchers = function(templateName, noRequirementCheck) { +m.GameState.prototype.findResearchers = function(templateName, noRequirementCheck) { // let's check we can research the tech. if (!this.canResearch(templateName, noRequirementCheck)) return []; var template = this.getTemplate(templateName); var self = this; return this.getOwnResearchFacilities().filter(function(ent) { //}){ var techs = ent.researchableTechs(); for (var i in techs) { var thisTemp = self.getTemplate(techs[i]); if (thisTemp.pairDef()) { var pairedTechs = thisTemp.getPairedTechs(); if (pairedTechs[0]._templateName == templateName || pairedTechs[1]._templateName == templateName) return true; } else { if (techs[i] == templateName) return true; } } return false; }); }; -GameState.prototype.getOwnFoundations = function() { - return this.updatingCollection("ownFoundations", Filters.isFoundation(), this.getOwnEntities()); +m.GameState.prototype.getOwnFoundations = function() { + return this.updatingCollection("ownFoundations", m.Filters.isFoundation(), this.getOwnEntities()); }; -GameState.prototype.getOwnDropsites = function(resource){ +m.GameState.prototype.getOwnDropsites = function(resource){ if (resource !== undefined) - return this.updatingCollection("dropsite-own-" + resource, Filters.isDropsite(resource), this.getOwnEntities(), true); - return this.updatingCollection("dropsite-own", Filters.isDropsite(), this.getOwnEntities(), true); + return this.updatingCollection("dropsite-own-" + resource, m.Filters.isDropsite(resource), this.getOwnEntities(), true); + return this.updatingCollection("dropsite-own", m.Filters.isDropsite(), this.getOwnEntities(), true); }; -GameState.prototype.getResourceSupplies = function(resource){ - return this.updatingGlobalCollection("resource-" + resource, Filters.byResource(resource), this.getEntities(), true); +m.GameState.prototype.getResourceSupplies = function(resource){ + return this.updatingGlobalCollection("resource-" + resource, m.Filters.byResource(resource), this.getEntities(), true); }; -GameState.prototype.getEntityLimits = function() { +m.GameState.prototype.getEntityLimits = function() { return this.playerData.entityLimits; }; -GameState.prototype.getEntityCounts = function() { +m.GameState.prototype.getEntityCounts = function() { return this.playerData.entityCounts; }; // Checks whether the maximum number of buildings have been cnstructed for a certain catergory -GameState.prototype.isEntityLimitReached = function(category) { +m.GameState.prototype.isEntityLimitReached = function(category) { if(this.playerData.entityLimits[category] === undefined || this.playerData.entityCounts[category] === undefined) return false; return (this.playerData.entityCounts[category] >= this.playerData.entityLimits[category]); }; -GameState.prototype.findTrainableUnits = function(classes){ +m.GameState.prototype.findTrainableUnits = function(classes){ var allTrainable = []; this.getOwnEntities().forEach(function(ent) { var trainable = ent.trainableEntities(); if (ent.hasClass("Structure")) for (var i in trainable){ if (allTrainable.indexOf(trainable[i]) === -1){ allTrainable.push(trainable[i]); } } }); var ret = []; for (var i in allTrainable) { var template = this.getTemplate(allTrainable[i]); var okay = true; for (var o in classes) if (!template.hasClass(classes[o])) okay = false; if (template.hasClass("Hero")) // disabling heroes for now okay = false; if (okay) ret.push( [allTrainable[i], template] ); } return ret; }; // Return all techs which can currently be researched // Does not factor cost. // If there are pairs, both techs are returned. -GameState.prototype.findAvailableTech = function() { +m.GameState.prototype.findAvailableTech = function() { var allResearchable = []; this.getOwnEntities().forEach(function(ent) { var searchable = ent.researchableTechs(); for (var i in searchable) { if (allResearchable.indexOf(searchable[i]) === -1) { allResearchable.push(searchable[i]); } } }); var ret = []; for (var i in allResearchable) { var template = this.getTemplate(allResearchable[i]); if (template.pairDef()) { var techs = template.getPairedTechs(); if (this.canResearch(techs[0]._templateName)) ret.push([techs[0]._templateName, techs[0]] ); if (this.canResearch(techs[1]._templateName)) ret.push([techs[1]._templateName, techs[1]] ); } else { if (this.canResearch(allResearchable[i]) && template._templateName != this.townPhase() && template._templateName != this.cityPhase()) ret.push( [allResearchable[i], template] ); } } return ret; }; // defcon utilities -GameState.prototype.timeSinceDefconChange = function() { +m.GameState.prototype.timeSinceDefconChange = function() { return this.getTimeElapsed()-this.ai.defconChangeTime; }; -GameState.prototype.setDefcon = function(level,force) { +m.GameState.prototype.setDefcon = function(level,force) { if (this.ai.defcon >= level || force) { this.ai.defcon = level; this.ai.defconChangeTime = this.getTimeElapsed(); } }; -GameState.prototype.defcon = function() { +m.GameState.prototype.defcon = function() { return this.ai.defcon; }; +return m; + +}(API3); + Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/resources.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/resources.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/resources.js (revision 14441) @@ -1,74 +1,81 @@ -function Resources(amounts, population) { +var API3 = function(m) +{ + +m.Resources = function(amounts, population) { if (amounts === undefined) { amounts = { food : 0, wood : 0, stone : 0, metal : 0 }; } for ( var tKey in this.types) { var t = this.types[tKey]; this[t] = amounts[t] || 0; } if (population > 0) { this.population = parseInt(population); } else { this.population = 0; } } -Resources.prototype.types = [ "food", "wood", "stone", "metal" ]; +m.Resources.prototype.types = [ "food", "wood", "stone", "metal" ]; -Resources.prototype.reset = function() { +m.Resources.prototype.reset = function() { for ( var tKey in this.types) { var t = this.types[tKey]; this[t] = 0; } this.population = 0; }; -Resources.prototype.canAfford = function(that) { +m.Resources.prototype.canAfford = function(that) { for ( var tKey in this.types) { var t = this.types[tKey]; if (this[t] < that[t]) { return false; } } return true; }; -Resources.prototype.add = function(that) { +m.Resources.prototype.add = function(that) { for ( var tKey in this.types) { var t = this.types[tKey]; this[t] += that[t]; } this.population += that.population; }; -Resources.prototype.subtract = function(that) { +m.Resources.prototype.subtract = function(that) { for ( var tKey in this.types) { var t = this.types[tKey]; this[t] -= that[t]; } this.population += that.population; }; -Resources.prototype.multiply = function(n) { +m.Resources.prototype.multiply = function(n) { for ( var tKey in this.types) { var t = this.types[tKey]; this[t] *= n; } this.population *= n; }; -Resources.prototype.toInt = function() { +m.Resources.prototype.toInt = function() { var sum = 0; for ( var tKey in this.types) { var t = this.types[tKey]; sum += this[t]; } sum += this.population * 50; // based on typical unit costs return sum; }; + +return m; + +}(API3); Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/terrain-analysis-pathfinder.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/terrain-analysis-pathfinder.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api-v3/terrain-analysis-pathfinder.js (revision 14441) @@ -1,327 +1,334 @@ +var API3 = function(m) +{ + // An implementation of A* as a pathfinder. // It's oversamplable, and has a specific "distance from block" // variable to avoid narrow passages if wanted. // It can carry a calculation over multiple turns. // It can work over water, or land. // Note: while theoretically possible, when this goes from land to water // It will return the path to the "boarding" point and // a new path will need to be created. // this should only be called by an AI player after setting gamestate.ai // The initializer creates an expanded influence map for checking. // It's not extraordinarily slow, but it might be. -function aStarPath(gameState, onWater, disregardEntities, targetTerritory) { +m.aStarPath = function(gameState, onWater, disregardEntities, targetTerritory) { var self = this; // get the terrain analyzer map as a reference. this.Map(gameState.ai, gameState.ai.terrainAnalyzer.map); // get the accessibility as a reference this.accessibility = gameState.ai.accessibility; this.terrainAnalyzer = gameState.ai.terrainAnalyzer; if (onWater) { this.waterPathfinder = true; } else this.waterPathfinder = false; var pathObstruction = gameState.sharedScript.passabilityClasses["pathfinderObstruction"]; var passMap = gameState.sharedScript.passabilityMap; var terrMap = gameState.sharedScript.territoryMap; this.widthMap = new Uint8Array(this.map.length); for (var i = 0; i < this.map.length; ++i) { if (this.map[i] === 0) this.widthMap[i] = 0; else if (!disregardEntities && (((terrMap.data[i] & 0x3F) !== gameState.ai.player && (terrMap.data[i] & 0x3F) !== 0 && (terrMap.data[i] & 0x3F) !== targetTerritory) || (passMap.data[i] & pathObstruction))) this.widthMap[i] = 1; // we try to avoid enemy territory and pathfinder obstructions. else if (!disregardEntities && this.map[i] === 30) this.widthMap[i] = 0; else if (!disregardEntities && this.map[i] === 40) this.widthMap[i] = 0; else if (!disregardEntities && this.map[i] === 41) this.widthMap[i] = 2; else if (!disregardEntities && this.map[i] === 42) this.widthMap[i] = 1; else if (!onWater && this.map[i] === 201) this.widthMap[i] = 1; else this.widthMap[i] = 255; } this.expandInfluences(255,this.widthMap); } -copyPrototype(aStarPath, Map); +m.copyPrototype(m.aStarPath, m.Map); // marks some points of the map as impassable. This can be used to create different paths, or to avoid going through some areas. -aStarPath.prototype.markImpassableArea = function(cx, cy, Distance) { +m.aStarPath.prototype.markImpassableArea = function(cx, cy, Distance) { [cx,cy] = this.gamePosToMapPos([cx,cy]); var x0 = Math.max(0, cx - Distance); var y0 = Math.max(0, cy - Distance); var x1 = Math.min(this.width, cx + Distance); var y1 = Math.min(this.height, cy + Distance); var maxDist2 = Distance * Distance; for ( var y = y0; y < y1; ++y) { for ( var x = x0; x < x1; ++x) { var dx = x - cx; var dy = y - cy; var r2 = dx*dx + dy*dy; if (r2 < maxDist2){ this.widthMap[x + y * this.width] = 0; } } } }; // sending gamestate creates a map // (you run the risk of "jumping" over obstacles or weird behavior. -aStarPath.prototype.getPath = function(start, end, Sampling, preferredWidth, iterationLimit, gamestate) +m.aStarPath.prototype.getPath = function(start, end, Sampling, preferredWidth, iterationLimit, gamestate) { this.Sampling = Sampling >= 1 ? Sampling : 1; this.minWidth = 1; this.preferredWidth = (preferredWidth !== undefined && preferredWidth >= this.Sampling) ? preferredWidth : this.Sampling; if (start[0] < 0 || this.gamePosToMapPos(start)[0] >= this.width || start[1] < 0 || this.gamePosToMapPos(start)[1] >= this.height) return undefined; var s = this.terrainAnalyzer.findClosestPassablePoint(this.gamePosToMapPos(start), !this.waterPathfinder,500,false); var e = this.terrainAnalyzer.findClosestPassablePoint(this.gamePosToMapPos(end), !this.waterPathfinder,500,true); var w = this.width; if (!s || !e) { return undefined; } if (gamestate !== undefined) { - this.TotorMap = new Map(gamestate); + this.TotorMap = new m.Map(gamestate); this.TotorMap.addInfluence(s[0],s[1],1,200,'constant'); this.TotorMap.addInfluence(e[0],e[1],1,200,'constant'); } this.iterationLimit = 9000000000; if (iterationLimit !== undefined) this.iterationLimit = iterationLimit; this.s = s[0] + w*s[1]; this.e = e[0] + w*e[1]; if (this.waterPathfinder && this.map[this.s] !== 200 && this.map[this.s] !== 201) { debug ("Trying a path over water, but we are on land, aborting"); return undefined; } else if (!this.waterPathfinder && this.map[this.s] === 200) { debug ("Trying a path over land, but we are over water, aborting"); return undefined; } this.onWater = this.waterPathfinder; this.pathChangesTransport = false; // We are going to create a map, it's going to take memory. To avoid OOM errors, GC before we do so. //Engine.ForceGC(); this.openList = []; this.parentSquare = new Uint32Array(this.map.length); this.isOpened = new Boolean(this.map.length); this.fCostArray = new Uint32Array(this.map.length); this.gCostArray = new Uint32Array(this.map.length); this.currentSquare = this.s; this.totalIteration = 0; this.isOpened[this.s] = true; this.openList.push(this.s); - this.fCostArray[this.s] = SquareVectorDistance([this.s%w, Math.floor(this.s/w)], [this.e%w, Math.floor(this.e/w)]); + this.fCostArray[this.s] = m.SquareVectorDistance([this.s%w, Math.floor(this.s/w)], [this.e%w, Math.floor(this.e/w)]); this.gCostArray[this.s] = 0; this.parentSquare[this.s] = this.s; return this.continuePath(gamestate); } // in case it's not over yet, this can carry on the calculation of a path over multiple turn until it's over -aStarPath.prototype.continuePath = function(gamestate) +m.aStarPath.prototype.continuePath = function(gamestate) { var w = this.width; var h = this.height; var positions = [[0,1], [0,-1], [1,0], [-1,0], [1,1], [-1,-1], [1,-1], [-1,1]]; var cost = [100,100,100,100,150,150,150,150]; //creation of variables used in the loop var found = false; var shortcut = false; var infinity = Math.min(); var currentDist = infinity; var e = this.e; var s = this.s; var iteration = 0; var target = [this.e%w, Math.floor(this.e/w)]; var changes = {}; var tIndex = 0; // on to A* while (found === false && this.openList.length !== 0 && iteration < this.iterationLimit) { currentDist = infinity; if (shortcut === true) { this.currentSquare = this.openList.shift(); } else { for (var i in this.openList) { var sum = this.fCostArray[this.openList[i]] + this.gCostArray[this.openList[i]]; if (sum < currentDist) { this.currentSquare = this.openList[i]; tIndex = i; currentDist = sum; } } this.openList.splice(tIndex,1); } if (!this.onWater && this.map[this.currentSquare] === 200) { this.onWater = true; } else if (this.onWater && (this.map[this.currentSquare] !== 200 && this.map[this.currentSquare] !== 201)) { this.onWater = false; } shortcut = false; this.isOpened[this.currentSquare] = false; if (gamestate !== undefined) this.TotorMap.addInfluence(this.currentSquare % w, Math.floor(this.currentSquare / w),1,40,'constant'); for (var i in positions) { var index = 0 + this.currentSquare +positions[i][0]*this.Sampling +w*this.Sampling*positions[i][1]; if (this.widthMap[index] >= this.minWidth || (this.onWater && this.map[index] > 0 && this.map[index] !== 200 && this.map[index] !== 201) || (!this.onWater && this.map[this.index] === 200)) { if(this.isOpened[index] === undefined) { this.parentSquare[index] = this.currentSquare; - this.fCostArray[index] = SquareVectorDistance([index%w, Math.floor(index/w)], target);// * cost[i]; + this.fCostArray[index] = m.SquareVectorDistance([index%w, Math.floor(index/w)], target);// * cost[i]; this.gCostArray[index] = this.gCostArray[this.currentSquare] + cost[i] * this.Sampling;// - this.map[index]; if (!this.onWater && this.map[index] === 200) { this.gCostArray[index] += 10000; } else if (this.onWater && this.map[index] !== 200) { this.gCostArray[index] += 10000; } if (this.widthMap[index] < this.preferredWidth) this.gCostArray[index] += 200 * (this.preferredWidth-this.widthMap[index]); if (this.map[index] === 200 || (this.map[index] === 201 && this.onWater)) this.gCostArray[index] += 1000; if (this.openList[0] !== undefined && this.fCostArray[this.openList[0]] + this.gCostArray[this.openList[0]] > this.fCostArray[index] + this.gCostArray[index]) { this.openList.unshift(index); shortcut = true; } else { this.openList.push(index); } this.isOpened[index] = true; - if (SquareVectorDistance( [index%w, Math.floor(index/w)] , target) <= this.Sampling*this.Sampling-1) { + if (m.SquareVectorDistance( [index%w, Math.floor(index/w)] , target) <= this.Sampling*this.Sampling-1) { if (this.e != index) this.parentSquare[this.e] = index; found = true; break; } } else { var addCost = 0; if (!this.onWater && this.map[index] === 200) { addCost += 10000; } else if (this.onWater && this.map[index] !== 200) { addCost += 10000; } if (this.widthMap[index] < this.preferredWidth) addCost += 200 * (this.preferredWidth-this.widthMap[index]); if (this.map[index] === 200 || (this.map[index] === 201 && this.onWater)) addCost += 1000; // already on the Open or closed list if (this.gCostArray[index] > cost[i] * this.Sampling + addCost + this.gCostArray[this.currentSquare]) { this.parentSquare[index] = this.currentSquare; this.gCostArray[index] = cost[i] * this.Sampling + addCost + this.gCostArray[this.currentSquare]; } } } } iteration++; } this.totalIteration += iteration; if (iteration === this.iterationLimit && found === false && this.openList.length !== 0) { // we've got to assume that we stopped because we reached the upper limit of iterations return "toBeContinued"; } //debug (this.totalIteration); var paths = []; if (found) { this.currentSquare = e; var lastPosx = 0; var lastPosy = 0; while (this.parentSquare[this.currentSquare] !== s) { this.currentSquare = this.parentSquare[this.currentSquare]; if (!this.onWater && this.map[this.currentSquare] === 200) { //debug ("We must cross water, going " +this.currentSquare + " from parent " + this.parentSquare[this.currentSquare]); this.pathChangesTransport = true; changes[this.currentSquare] = true; this.onWater = true; } else if (this.onWater && (this.map[this.currentSquare] !== 200 && this.map[this.currentSquare] !== 201)) { //debug ("We must cross to the ground, going " +this.currentSquare + " from parent " + this.parentSquare[this.currentSquare]); this.pathChangesTransport = true; changes[this.currentSquare] = true; this.onWater = false; } if (gamestate !== undefined && changes[this.currentSquare]) this.TotorMap.addInfluence(this.currentSquare % w, Math.floor(this.currentSquare / w),2,200,'constant'); if (gamestate !== undefined) this.TotorMap.addInfluence(this.currentSquare % w, Math.floor(this.currentSquare / w),1,50,'constant'); - if (SquareVectorDistance([lastPosx,lastPosy],[this.currentSquare % w, Math.floor(this.currentSquare / w)]) > 300 || changes[this.currentSquare]) + if (m.SquareVectorDistance([lastPosx,lastPosy],[this.currentSquare % w, Math.floor(this.currentSquare / w)]) > 300 || changes[this.currentSquare]) { lastPosx = (this.currentSquare % w); lastPosy = Math.floor(this.currentSquare / w); paths.push([ [lastPosx*this.cellSize,lastPosy*this.cellSize], changes[this.currentSquare] ]); if (gamestate !== undefined) this.TotorMap.addInfluence(this.currentSquare % w, Math.floor(this.currentSquare / w),1,50 + paths.length,'constant'); } } } else { // we have not found a path. // what do we do then? } if (gamestate !== undefined) this.TotorMap.dumpIm("Path From " +s +" to " +e +".png",255); delete this.parentSquare; delete this.isOpened; delete this.fCostArray; delete this.gCostArray; // the return, if defined is [ [path, each waypoint being [position, mustchangeTransport] ], is there any transport change, ] if (paths.length > 0) { return [paths, this.pathChangesTransport]; } else { return undefined; } } + +return m; + +}(API3); Index: ps/trunk/binaries/data/mods/public/simulation/ai/qbot/attackMoveToLocation.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/qbot/attackMoveToLocation.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/qbot/attackMoveToLocation.js (revision 14441) @@ -1,238 +1,240 @@ -function AttackMoveToLocation(gameState, militaryManager, minAttackSize, maxAttackSize, targetFinder){ +function AttackMoveToLocation(gameState, Config, militaryManager, minAttackSize, maxAttackSize, targetFinder){ + + this.Config = Config; this.minAttackSize = minAttackSize || Config.attack.minAttackSize; this.maxAttackSize = maxAttackSize || Config.attack.maxAttackSize; this.idList=[]; this.previousTime = 0; this.state = "unexecuted"; this.targetFinder = targetFinder || this.defaultTargetFinder; this.healthRecord = []; }; // Returns true if the attack can be executed at the current time AttackMoveToLocation.prototype.canExecute = function(gameState, militaryManager){ var enemyStrength = militaryManager.measureEnemyStrength(gameState); var enemyCount = militaryManager.measureEnemyCount(gameState); // We require our army to be >= this strength - var targetStrength = enemyStrength * Config.attack.enemyRatio; + var targetStrength = enemyStrength * this.Config.attack.enemyRatio; var availableCount = militaryManager.countAvailableUnits(); var availableStrength = militaryManager.measureAvailableStrength(); debug("Troops needed for attack: " + this.minAttackSize + " Have: " + availableCount); debug("Troops strength for attack: " + targetStrength + " Have: " + availableStrength); return ((availableStrength >= targetStrength && availableCount >= this.minAttackSize) || availableCount >= this.maxAttackSize); }; // Default target finder aims for conquest critical targets AttackMoveToLocation.prototype.defaultTargetFinder = function(gameState, militaryManager){ // Find the critical enemy buildings we could attack var targets = militaryManager.getEnemyBuildings(gameState,"ConquestCritical"); // If there are no critical structures, attack anything else that's critical if (targets.length == 0) { targets = gameState.entities.filter(function(ent) { return (gameState.isEntityEnemy(ent) && ent.hasClass("ConquestCritical") && ent.owner() !== 0 && ent.position()); }); } // If there's nothing, attack anything else that's less critical if (targets.length == 0) { targets = militaryManager.getEnemyBuildings(gameState,"Town"); } if (targets.length == 0) { targets = militaryManager.getEnemyBuildings(gameState,"Village"); } return targets; }; // Executes the attack plan, after this is executed the update function will be run every turn AttackMoveToLocation.prototype.execute = function(gameState, militaryManager){ var availableCount = militaryManager.countAvailableUnits(); var numWanted = Math.min(availableCount, this.maxAttackSize); this.idList = militaryManager.getAvailableUnits(numWanted); var pending = EntityCollectionFromIds(gameState, this.idList); var targets = this.targetFinder(gameState, militaryManager); if (targets.length === 0){ targets = this.defaultTargetFinder(gameState, militaryManager); } // If we have a target, move to it if (targets.length) { // Add an attack role so the economic manager doesn't try and use them pending.forEach(function(ent) { ent.setMetadata("role", "attack"); }); var curPos = pending.getCentrePosition(); // pick a random target from the list this.targetPos = undefined; var count = 0; while (!this.targetPos) { var rand = Math.floor((Math.random()*targets.length)); var target = targets.toEntityArray()[rand]; this.targetPos = target.position(); count++; if (count > 1000) { warn("No target with a valid position found"); return; } } // Find possible distinct paths to the enemy var pathFinder = new PathFinder(gameState); var pathsToEnemy = pathFinder.getPaths(curPos, this.targetPos); if (!pathsToEnemy || !pathsToEnemy[0] || pathsToEnemy[0][0] === undefined || pathsToEnemy[0][1] === undefined) { pathsToEnemy = [[this.targetPos]]; } var randPath = Math.floor(Math.random() * pathsToEnemy.length); this.path = pathsToEnemy[randPath]; pending.move(this.path[0][0], this.path[0][1]); } else if (targets.length == 0 ) { gameState.ai.gameFinished = true; return; } this.state = "walking"; }; // Runs every turn after the attack is executed // This removes idle units from the attack AttackMoveToLocation.prototype.update = function(gameState, militaryManager, events){ if (!this.targetPos){ for (var idKey in this.idList){ var id = this.idList[idKey]; militaryManager.unassignUnit(id); } this.idList = []; } // keep the list of units in good order by pruning ids with no corresponding entities (i.e. dead units) var removeList = []; var totalHealth = 0; for (var idKey in this.idList){ var id = this.idList[idKey]; var ent = militaryManager.entity(id); if (ent === undefined){ removeList.push(id); }else{ if (ent.hitpoints()){ totalHealth += ent.hitpoints(); } } } for (var i in removeList){ this.idList.splice(this.idList.indexOf(removeList[i]),1); } var units = EntityCollectionFromIds(gameState, this.idList); if (!this.path || this.path.length === 0){ var idleCount = 0; var self = this; units.forEach(function(ent){ if (ent.isIdle()){ if (ent.position() && VectorDistance(ent.position(), self.targetPos) > 30){ ent.move(self.targetPos[0], self.targetPos[1]); }else{ militaryManager.unassignUnit(ent.id()); } } }); return; } var deltaHealth = 0; var deltaTime = 1; var time = gameState.getTimeElapsed(); this.healthRecord.push([totalHealth, time]); if (this.healthRecord.length > 1){ for (var i = this.healthRecord.length - 1; i >= 0; i--){ deltaHealth = totalHealth - this.healthRecord[i][0]; deltaTime = time - this.healthRecord[i][1]; if (this.healthRecord[i][1] < time - 5*1000){ break; } } } var numUnits = this.idList.length; if (numUnits < 1) return; var damageRate = -deltaHealth / deltaTime * 1000; var centrePos = units.getCentrePosition(); if (! centrePos) return; var idleCount = 0; // Looks for idle units away from the formations centre for (var idKey in this.idList){ var id = this.idList[idKey]; var ent = militaryManager.entity(id); if (ent.isIdle()){ if (ent.position() && VectorDistance(ent.position(), centrePos) > 20){ var dist = VectorDistance(ent.position(), centrePos); var vector = [centrePos[0] - ent.position()[0], centrePos[1] - ent.position()[1]]; vector[0] *= 10/dist; vector[1] *= 10/dist; ent.move(centrePos[0] + vector[0], centrePos[1] + vector[1]); }else{ idleCount++; } } } if ((damageRate / Math.sqrt(numUnits)) > 2){ if (this.state === "walking"){ var sumAttackerPos = [0,0]; var numAttackers = 0; for (var key in events){ var e = events[key]; //{type:"Attacked", msg:{attacker:736, target:1133, type:"Melee"}} if (e.type === "Attacked" && e.msg){ if (this.idList.indexOf(e.msg.target) !== -1){ var attacker = militaryManager.entity(e.msg.attacker); if (attacker && attacker.position()){ sumAttackerPos[0] += attacker.position()[0]; sumAttackerPos[1] += attacker.position()[1]; numAttackers += 1; } } } } if (numAttackers > 0){ var avgAttackerPos = [sumAttackerPos[0]/numAttackers, sumAttackerPos[1]/numAttackers]; // Stop moving units.move(centrePos[0], centrePos[1]); this.state = "attacking"; } } }else{ if (this.state === "attacking"){ units.move(this.path[0][0], this.path[0][1]); this.state = "walking"; } } if (this.state === "walking"){ if (VectorDistance(centrePos, this.path[0]) < 20 || idleCount/numUnits > 0.8){ this.path.shift(); if (this.path.length > 0){ units.move(this.path[0][0], this.path[0][1]); } } } this.previousTime = time; this.previousHealth = totalHealth; }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/qbot/entitycollection-extend.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/qbot/entitycollection-extend.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/qbot/entitycollection-extend.js (revision 14441) @@ -1,40 +1,40 @@ EntityCollection.prototype.attack = function(unit) { var unitId; if (typeOf(unit) === "Entity"){ unitId = unit.id(); }else{ unitId = unit; } - Engine.PostCommand({"type": "attack", "entities": this.toIdArray(), "target": unitId, "queued": false}); + Engine.PostCommand(PlayerID, {"type": "attack", "entities": this.toIdArray(), "target": unitId, "queued": false}); return this; }; function EntityCollectionFromIds(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 EntityCollection(gameState.ai, ents); } EntityCollection.prototype.getCentrePosition = function(){ var sumPos = [0, 0]; var count = 0; this.forEach(function(ent){ if (ent.position()){ sumPos[0] += ent.position()[0]; sumPos[1] += ent.position()[1]; count ++; } }); if (count === 0){ return undefined; }else{ return [sumPos[0]/count, sumPos[1]/count]; } -}; \ No newline at end of file +}; Index: ps/trunk/source/simulation2/components/CCmpAIManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpAIManager.cpp (revision 14440) +++ ps/trunk/source/simulation2/components/CCmpAIManager.cpp (revision 14441) @@ -1,1143 +1,1083 @@ /* Copyright (C) 2013 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "simulation2/system/Component.h" #include "ICmpAIManager.h" #include "simulation2/MessageTypes.h" #include "graphics/Terrain.h" #include "lib/timer.h" #include "lib/tex/tex.h" #include "lib/allocators/shared_ptr.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Util.h" #include "simulation2/components/ICmpAIInterface.h" #include "simulation2/components/ICmpCommandQueue.h" #include "simulation2/components/ICmpObstructionManager.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/components/ICmpTemplateManager.h" #include "simulation2/components/ICmpTechnologyTemplateManager.h" #include "simulation2/components/ICmpTerritoryManager.h" #include "simulation2/helpers/Grid.h" #include "simulation2/serialization/DebugSerializer.h" #include "simulation2/serialization/StdDeserializer.h" #include "simulation2/serialization/StdSerializer.h" #include "simulation2/serialization/SerializeTemplates.h" /** * @file * Player AI interface. * AI is primarily scripted, and the CCmpAIManager component defined here * takes care of managing all the scripts. * * To avoid slow AI scripts causing jerky rendering, they are run in a background * thread (maintained by CAIWorker) so that it's okay if they take a whole simulation * turn before returning their results (though preferably they shouldn't use nearly * that much CPU). * * CCmpAIManager grabs the world state after each turn (making use of AIInterface.js * and AIProxy.js to decide what data to include) then passes it to CAIWorker. * The AI scripts will then run asynchronously and return a list of commands to execute. * Any attempts to read the command list (including indirectly via serialization) * will block until it's actually completed, so the rest of the engine should avoid * reading it for as long as possible. * * JS values are passed between the game and AI threads using ScriptInterface::StructuredClone. * * TODO: actually the thread isn't implemented yet, because performance hasn't been * sufficiently problematic to justify the complexity yet, but the CAIWorker interface * is designed to hopefully support threading when we want it. */ /** * Implements worker thread for CCmpAIManager. */ class CAIWorker { private: class CAIPlayer { NONCOPYABLE(CAIPlayer); public: CAIPlayer(CAIWorker& worker, const std::wstring& aiName, player_id_t player, uint8_t difficulty, - const shared_ptr& runtime, boost::rand48& rng) : - m_Worker(worker), m_AIName(aiName), m_Player(player), m_Difficulty(difficulty), m_ScriptInterface("Engine", "AI", runtime) + shared_ptr scriptInterface) : + m_Worker(worker), m_AIName(aiName), m_Player(player), m_Difficulty(difficulty), m_ScriptInterface(scriptInterface) { - m_ScriptInterface.SetCallbackData(static_cast (this)); - - m_ScriptInterface.ReplaceNondeterministicRNG(rng); - m_ScriptInterface.LoadGlobalScripts(); - - m_ScriptInterface.RegisterFunction("IncludeModule"); - m_ScriptInterface.RegisterFunction("DumpHeap"); - m_ScriptInterface.RegisterFunction("ForceGC"); - m_ScriptInterface.RegisterFunction("PostCommand"); - - m_ScriptInterface.RegisterFunction, u32, u32, u32, CAIPlayer::DumpImage>("DumpImage"); - - m_ScriptInterface.RegisterFunction("RegisterSerializablePrototype"); } ~CAIPlayer() { // Clean up rooted objects before destroying their script context m_Obj = CScriptValRooted(); m_Commands.clear(); } - static void IncludeModule(void* cbdata, std::wstring name) - { - CAIPlayer* self = static_cast (cbdata); - - self->LoadScripts(name); - } - static void DumpHeap(void* cbdata) - { - CAIPlayer* self = static_cast (cbdata); - - //std::cout << JS_GetGCParameter(self->m_ScriptInterface.GetRuntime(), JSGC_BYTES) << std::endl; - self->m_ScriptInterface.DumpHeap(); - } - static void ForceGC(void* cbdata) - { - CAIPlayer* self = static_cast (cbdata); - - JS_GC(self->m_ScriptInterface.GetContext()); - } - static void PostCommand(void* cbdata, CScriptValRooted cmd) - { - CAIPlayer* self = static_cast (cbdata); - - self->m_Commands.push_back(self->m_ScriptInterface.WriteStructuredClone(cmd.get())); - } - - /** - * Debug function for AI scripts to dump 2D array data (e.g. terrain tile weights). - * TODO: check if this needs to be here too. - */ - static void DumpImage(void* UNUSED(cbdata), std::wstring name, std::vector data, u32 w, u32 h, u32 max) - { - // TODO: this is totally not threadsafe. - - VfsPath filename = L"screenshots/aidump/" + name; - - if (data.size() != w*h) - { - debug_warn(L"DumpImage: data size doesn't match w*h"); - return; - } - - if (max == 0) - { - debug_warn(L"DumpImage: max must not be 0"); - return; - } - - const size_t bpp = 8; - int flags = TEX_BOTTOM_UP|TEX_GREY; - - const size_t img_size = w * h * bpp/8; - const size_t hdr_size = tex_hdr_size(filename); - shared_ptr buf; - AllocateAligned(buf, hdr_size+img_size, maxSectorSize); - Tex t; - if (tex_wrap(w, h, bpp, flags, buf, hdr_size, &t) < 0) - return; - - u8* img = buf.get() + hdr_size; - for (size_t i = 0; i < data.size(); ++i) - img[i] = (u8)((data[i] * 255) / max); - - tex_write(&t, filename); - tex_free(&t); - } - - static void RegisterSerializablePrototype(void* cbdata, std::wstring name, CScriptVal proto) - { - CAIPlayer* self = static_cast (cbdata); - // Add our player number to avoid name conflicts with other prototypes - // TODO: it would be better if serializable prototypes were stored in ScriptInterfaces - // and then each serializer would access those matching its own context, but that's - // not possible with AIs sharing data across contexts - std::wstringstream protoID; - protoID << self->m_Player << L"." << name.c_str(); - self->m_Worker.RegisterSerializablePrototype(protoID.str(), proto); - } - - bool LoadScripts(const std::wstring& moduleName) - { - // Ignore modules that are already loaded - if (m_LoadedModules.find(moduleName) != m_LoadedModules.end()) - return true; - - // Mark this as loaded, to prevent it recursively loading itself - m_LoadedModules.insert(moduleName); - - // Load and execute *.js - VfsPaths pathnames; - vfs::GetPathnames(g_VFS, L"simulation/ai/" + moduleName + L"/", L"*.js", pathnames); - for (VfsPaths::iterator it = pathnames.begin(); it != pathnames.end(); ++it) - { - if (!m_ScriptInterface.LoadGlobalScriptFile(*it)) - { - LOGERROR(L"Failed to load script %ls", it->string().c_str()); - return false; - } - } - - return true; - } - bool Initialise(bool callConstructor) { - if (!LoadScripts(m_AIName)) + // LoadScripts will only load each script once even though we call it for each player + if (!m_Worker.LoadScripts(m_AIName)) return false; OsPath path = L"simulation/ai/" + m_AIName + L"/data.json"; CScriptValRooted metadata = m_Worker.LoadMetadata(path); if (metadata.uninitialised()) { LOGERROR(L"Failed to create AI player: can't find %ls", path.string().c_str()); return false; } // Get the constructor name from the metadata + // If the AI doesn't use modules, we look for the constructor in the global object + // TODO: All AIs should use modules. Remove the condition if this requirement is met. + std::string moduleName; std::string constructor; - if (!m_ScriptInterface.GetProperty(metadata.get(), "constructor", constructor)) + CScriptVal objectWithConstructor; // object that should contain the constructor function + CScriptVal ctor; + if (!m_ScriptInterface->HasProperty(metadata.get(), "moduleName")) + { + objectWithConstructor = m_ScriptInterface->GetGlobalObject(); + } + else + { + m_ScriptInterface->GetProperty(metadata.get(), "moduleName", moduleName); + if(!m_ScriptInterface->GetProperty(m_ScriptInterface->GetGlobalObject(), moduleName.c_str(), objectWithConstructor) || objectWithConstructor.undefined()) + { + LOGERROR(L"Failed to create AI player: %ls: can't find the module that should contain the constructor: '%hs'", path.string().c_str(), moduleName.c_str()); + return false; + } + } + if (!m_ScriptInterface->GetProperty(metadata.get(), "constructor", constructor)) { LOGERROR(L"Failed to create AI player: %ls: missing 'constructor'", path.string().c_str()); return false; } // Get the constructor function from the loaded scripts - CScriptVal ctor; - if (!m_ScriptInterface.GetProperty(m_ScriptInterface.GetGlobalObject(), constructor.c_str(), ctor) + if (!m_ScriptInterface->GetProperty(objectWithConstructor.get(), constructor.c_str(), ctor) || ctor.undefined()) { LOGERROR(L"Failed to create AI player: %ls: can't find constructor '%hs'", path.string().c_str(), constructor.c_str()); return false; } - m_ScriptInterface.GetProperty(metadata.get(), "useShared", m_UseSharedComponent); + m_ScriptInterface->GetProperty(metadata.get(), "useShared", m_UseSharedComponent); CScriptVal obj; if (callConstructor) { // Set up the data to pass as the constructor argument CScriptVal settings; - m_ScriptInterface.Eval(L"({})", settings); - m_ScriptInterface.SetProperty(settings.get(), "player", m_Player, false); - m_ScriptInterface.SetProperty(settings.get(), "difficulty", m_Difficulty, false); + m_ScriptInterface->Eval(L"({})", settings); + m_ScriptInterface->SetProperty(settings.get(), "player", m_Player, false); + m_ScriptInterface->SetProperty(settings.get(), "difficulty", m_Difficulty, false); ENSURE(m_Worker.m_HasLoadedEntityTemplates); - m_ScriptInterface.SetProperty(settings.get(), "templates", m_Worker.m_EntityTemplates, false); + m_ScriptInterface->SetProperty(settings.get(), "templates", m_Worker.m_EntityTemplates, false); - obj = m_ScriptInterface.CallConstructor(ctor.get(), settings.get()); + obj = m_ScriptInterface->CallConstructor(ctor.get(), settings.get()); } else { // For deserialization, we want to create the object with the correct prototype // but don't want to actually run the constructor again // XXX: actually we don't currently use this path for deserialization - maybe delete it? - obj = m_ScriptInterface.NewObjectFromConstructor(ctor.get()); + obj = m_ScriptInterface->NewObjectFromConstructor(ctor.get()); } if (obj.undefined()) { LOGERROR(L"Failed to create AI player: %ls: error calling constructor '%hs'", path.string().c_str(), constructor.c_str()); return false; } - m_Obj = CScriptValRooted(m_ScriptInterface.GetContext(), obj); + m_Obj = CScriptValRooted(m_ScriptInterface->GetContext(), obj); return true; } - void Run(CScriptVal state) + void Run(CScriptVal state, int playerID) { m_Commands.clear(); - m_ScriptInterface.CallFunctionVoid(m_Obj.get(), "HandleMessage", state); + m_ScriptInterface->CallFunctionVoid(m_Obj.get(), "HandleMessage", state, playerID); } // overloaded with a sharedAI part. // javascript can handle both natively on the same function. - void Run(CScriptVal state, CScriptValRooted SharedAI) + void Run(CScriptVal state, int playerID, CScriptValRooted SharedAI) { m_Commands.clear(); - m_ScriptInterface.CallFunctionVoid(m_Obj.get(), "HandleMessage", state, SharedAI); + m_ScriptInterface->CallFunctionVoid(m_Obj.get(), "HandleMessage", state, playerID, SharedAI); } void InitAI(CScriptVal state, CScriptValRooted SharedAI) { m_Commands.clear(); - m_ScriptInterface.CallFunctionVoid(m_Obj.get(), "Init", state, SharedAI); + m_ScriptInterface->CallFunctionVoid(m_Obj.get(), "Init", state, m_Player, SharedAI); } CAIWorker& m_Worker; std::wstring m_AIName; player_id_t m_Player; uint8_t m_Difficulty; bool m_UseSharedComponent; - ScriptInterface m_ScriptInterface; + shared_ptr m_ScriptInterface; CScriptValRooted m_Obj; std::vector > m_Commands; - std::set m_LoadedModules; }; public: struct SCommandSets { player_id_t player; std::vector > commands; }; CAIWorker() : // TODO: Passing a 32 MB argument to CreateRuntime() is a temporary fix // to prevent frequent AI out-of-memory crashes. The argument should be // removed as soon whenever the new pathfinder is committed // And the AIs can stop relying on their own little hands. m_ScriptRuntime(ScriptInterface::CreateRuntime(33554432)), - m_ScriptInterface("Engine", "AI", m_ScriptRuntime), + m_ScriptInterface(new ScriptInterface("Engine", "AI", m_ScriptRuntime)), m_TurnNum(0), m_CommandsComputed(true), m_HasLoadedEntityTemplates(false), m_HasSharedComponent(false) { // TODO: ought to seed the RNG (in a network-synchronised way) before we use it - m_ScriptInterface.ReplaceNondeterministicRNG(m_RNG); - m_ScriptInterface.LoadGlobalScripts(); + m_ScriptInterface->ReplaceNondeterministicRNG(m_RNG); + m_ScriptInterface->LoadGlobalScripts(); - m_ScriptInterface.SetCallbackData(NULL); + m_ScriptInterface->SetCallbackData(static_cast (this)); - m_ScriptInterface.RegisterFunction("PostCommand"); - m_ScriptInterface.RegisterFunction("DumpHeap"); - m_ScriptInterface.RegisterFunction("ForceGC"); + m_ScriptInterface->RegisterFunction("PostCommand"); + m_ScriptInterface->RegisterFunction("IncludeModule"); + m_ScriptInterface->RegisterFunction("DumpHeap"); + m_ScriptInterface->RegisterFunction("ForceGC"); - m_ScriptInterface.RegisterFunction, u32, u32, u32, CAIWorker::DumpImage>("DumpImage"); + m_ScriptInterface->RegisterFunction, u32, u32, u32, CAIWorker::DumpImage>("DumpImage"); } ~CAIWorker() { // Clear rooted script values before destructing the script interface m_EntityTemplates = CScriptValRooted(); m_PlayerMetadata.clear(); m_Players.clear(); m_GameState.reset(); m_PassabilityMapVal = CScriptValRooted(); m_TerritoryMapVal = CScriptValRooted(); } + + bool LoadScripts(const std::wstring& moduleName) + { + // Ignore modules that are already loaded + if (m_LoadedModules.find(moduleName) != m_LoadedModules.end()) + return true; + + // Mark this as loaded, to prevent it recursively loading itself + m_LoadedModules.insert(moduleName); + + // Load and execute *.js + VfsPaths pathnames; + vfs::GetPathnames(g_VFS, L"simulation/ai/" + moduleName + L"/", L"*.js", pathnames); + for (VfsPaths::iterator it = pathnames.begin(); it != pathnames.end(); ++it) + { + if (!m_ScriptInterface->LoadGlobalScriptFile(*it)) + { + LOGERROR(L"Failed to load script %ls", it->string().c_str()); + return false; + } + } + + return true; + } + + static void IncludeModule(void* cbdata, std::wstring name) + { + CAIWorker* self = static_cast (cbdata); + self->LoadScripts(name); + } - // This is called by AIs if they use the v3 API. - // If the AIs originate the call, cbdata is not NULL. - // If the shared component does, it is, so it must not be taken into account. - static void PostCommand(void* cbdata, CScriptValRooted cmd) + static void PostCommand(void* cbdata, int playerid, CScriptValRooted cmd) { - if (cbdata == NULL) { - debug_warn(L"Warning: the shared component has tried to push an engine command. Ignoring."); - return; + CAIWorker* self = static_cast (cbdata); + self->PostCommand(playerid, cmd); + } + + void PostCommand(int playerid, CScriptValRooted cmd) + { + for (size_t i=0; im_Player == playerid) + { + m_Players[i]->m_Commands.push_back(m_ScriptInterface->WriteStructuredClone(cmd.get())); + return; + } } - CAIPlayer* self = static_cast (cbdata); - self->m_Commands.push_back(self->m_ScriptInterface.WriteStructuredClone(cmd.get())); + + LOGERROR(L"Invalid playerid in PostCommand!"); } // The next two ought to be implmeneted someday but for now as it returns "null" it can't static void DumpHeap(void* cbdata) { if (cbdata == NULL) { debug_warn(L"Warning: the shared component has asked for DumpHeap. Ignoring."); return; } CAIWorker* self = static_cast (cbdata); - self->m_ScriptInterface.DumpHeap(); + self->m_ScriptInterface->DumpHeap(); } static void ForceGC(void* cbdata) { if (cbdata == NULL) { debug_warn(L"Warning: the shared component has asked for ForceGC. Ignoring."); return; } CAIWorker* self = static_cast (cbdata); PROFILE3("AI compute GC"); - JS_GC(self->m_ScriptInterface.GetContext()); + JS_GC(self->m_ScriptInterface->GetContext()); } /** * Debug function for AI scripts to dump 2D array data (e.g. terrain tile weights). */ static void DumpImage(void* UNUSED(cbdata), std::wstring name, std::vector data, u32 w, u32 h, u32 max) { // TODO: this is totally not threadsafe. VfsPath filename = L"screenshots/aidump/" + name; if (data.size() != w*h) { debug_warn(L"DumpImage: data size doesn't match w*h"); return; } if (max == 0) { debug_warn(L"DumpImage: max must not be 0"); return; } const size_t bpp = 8; int flags = TEX_BOTTOM_UP|TEX_GREY; const size_t img_size = w * h * bpp/8; const size_t hdr_size = tex_hdr_size(filename); shared_ptr buf; AllocateAligned(buf, hdr_size+img_size, maxSectorSize); Tex t; if (tex_wrap(w, h, bpp, flags, buf, hdr_size, &t) < 0) return; u8* img = buf.get() + hdr_size; for (size_t i = 0; i < data.size(); ++i) img[i] = (u8)((data[i] * 255) / max); tex_write(&t, filename); tex_free(&t); } bool TryLoadSharedComponent(bool hasTechs) { // we don't need to load it. if (!m_HasSharedComponent) return false; // reset the value so it can be used to determine if we actually initialized it. m_HasSharedComponent = false; - - VfsPaths sharedPathnames; - // Check for "shared" module. - vfs::GetPathnames(g_VFS, L"simulation/ai/common-api-v3/", L"*.js", sharedPathnames); - for (VfsPaths::iterator it = sharedPathnames.begin(); it != sharedPathnames.end(); ++it) - { - if (!m_ScriptInterface.LoadGlobalScriptFile(*it)) - { - LOGERROR(L"Failed to load shared script %ls", it->string().c_str()); - return false; - } + + if (LoadScripts(L"common-api-v3")) m_HasSharedComponent = true; - } - if (!m_HasSharedComponent) + else return false; - + // mainly here for the error messages OsPath path = L"simulation/ai/common-api-v2/"; - // Constructor name is SharedScript + // Constructor name is SharedScript, it's in the module API3 + // TODO: Hardcoding this is bad, we need a smarter way. + CScriptVal AIModule; CScriptVal ctor; - if (!m_ScriptInterface.GetProperty(m_ScriptInterface.GetGlobalObject(), "SharedScript", ctor) + if (!m_ScriptInterface->GetProperty(m_ScriptInterface->GetGlobalObject(), "API3", AIModule) || AIModule.undefined()) + { + LOGERROR(L"Failed to create shared AI component: %ls: can't find module '%hs'", path.string().c_str(), "API3"); + return false; + } + + if (!m_ScriptInterface->GetProperty(AIModule.get(), "SharedScript", ctor) || ctor.undefined()) { LOGERROR(L"Failed to create shared AI component: %ls: can't find constructor '%hs'", path.string().c_str(), "SharedScript"); return false; } // Set up the data to pass as the constructor argument CScriptVal settings; - m_ScriptInterface.Eval(L"({})", settings); + m_ScriptInterface->Eval(L"({})", settings); CScriptVal playersID; - m_ScriptInterface.Eval(L"({})", playersID); + m_ScriptInterface->Eval(L"({})", playersID); for (size_t i = 0; i < m_Players.size(); ++i) { - jsval val = m_ScriptInterface.ToJSVal(m_ScriptInterface.GetContext(), m_Players[i]->m_Player); - m_ScriptInterface.SetPropertyInt(playersID.get(), i, CScriptVal(val), true); + jsval val = m_ScriptInterface->ToJSVal(m_ScriptInterface->GetContext(), m_Players[i]->m_Player); + m_ScriptInterface->SetPropertyInt(playersID.get(), i, CScriptVal(val), true); } - m_ScriptInterface.SetProperty(settings.get(), "players", playersID); + m_ScriptInterface->SetProperty(settings.get(), "players", playersID); ENSURE(m_HasLoadedEntityTemplates); - m_ScriptInterface.SetProperty(settings.get(), "templates", m_EntityTemplates, false); + m_ScriptInterface->SetProperty(settings.get(), "templates", m_EntityTemplates, false); if (hasTechs) { - m_ScriptInterface.SetProperty(settings.get(), "techTemplates", m_TechTemplates, false); + m_ScriptInterface->SetProperty(settings.get(), "techTemplates", m_TechTemplates, false); } else { // won't get the tech templates directly. CScriptVal fakeTech; - m_ScriptInterface.Eval("({})", fakeTech); - m_ScriptInterface.SetProperty(settings.get(), "techTemplates", fakeTech, false); + m_ScriptInterface->Eval("({})", fakeTech); + m_ScriptInterface->SetProperty(settings.get(), "techTemplates", fakeTech, false); } - m_SharedAIObj = CScriptValRooted(m_ScriptInterface.GetContext(),m_ScriptInterface.CallConstructor(ctor.get(), settings.get())); + m_SharedAIObj = CScriptValRooted(m_ScriptInterface->GetContext(),m_ScriptInterface->CallConstructor(ctor.get(), settings.get())); if (m_SharedAIObj.undefined()) { LOGERROR(L"Failed to create shared AI component: %ls: error calling constructor '%hs'", path.string().c_str(), "SharedScript"); return false; } return true; } bool AddPlayer(const std::wstring& aiName, player_id_t player, uint8_t difficulty, bool callConstructor) { - shared_ptr ai(new CAIPlayer(*this, aiName, player, difficulty, m_ScriptRuntime, m_RNG)); + shared_ptr ai(new CAIPlayer(*this, aiName, player, difficulty, m_ScriptInterface)); if (!ai->Initialise(callConstructor)) return false; // this will be set to true if we need to load the shared Component. if (!m_HasSharedComponent) m_HasSharedComponent = ai->m_UseSharedComponent; - m_ScriptInterface.MaybeGC(); + m_ScriptInterface->MaybeGC(); m_Players.push_back(ai); return true; } bool RunGamestateInit(const shared_ptr& gameState, const Grid& passabilityMap, const Grid& territoryMap) { // this will be run last by InitGame.Js, passing the full game representation. // For now it will run for the shared Component. // This is NOT run during deserialization. - CScriptVal state = m_ScriptInterface.ReadStructuredClone(gameState); - JSContext* cx = m_ScriptInterface.GetContext(); + CScriptVal state = m_ScriptInterface->ReadStructuredClone(gameState); + JSContext* cx = m_ScriptInterface->GetContext(); m_PassabilityMapVal = CScriptValRooted(cx, ScriptInterface::ToJSVal(cx, passabilityMap)); m_TerritoryMapVal = CScriptValRooted(cx, ScriptInterface::ToJSVal(cx, territoryMap)); if (m_HasSharedComponent) { - m_ScriptInterface.SetProperty(state.get(), "passabilityMap", m_PassabilityMapVal, true); - m_ScriptInterface.SetProperty(state.get(), "territoryMap", m_TerritoryMapVal, true); + m_ScriptInterface->SetProperty(state.get(), "passabilityMap", m_PassabilityMapVal, true); + m_ScriptInterface->SetProperty(state.get(), "territoryMap", m_TerritoryMapVal, true); - m_ScriptInterface.CallFunctionVoid(m_SharedAIObj.get(), "init", state); - m_ScriptInterface.MaybeGC(); + m_ScriptInterface->CallFunctionVoid(m_SharedAIObj.get(), "init", state); + m_ScriptInterface->MaybeGC(); for (size_t i = 0; i < m_Players.size(); ++i) { if (m_HasSharedComponent && m_Players[i]->m_UseSharedComponent) m_Players[i]->InitAI(state,m_SharedAIObj); } } return true; } void StartComputation(const shared_ptr& gameState, const Grid& passabilityMap, const Grid& territoryMap, bool territoryMapDirty) { ENSURE(m_CommandsComputed); m_GameState = gameState; if (passabilityMap.m_DirtyID != m_PassabilityMap.m_DirtyID) { m_PassabilityMap = passabilityMap; - JSContext* cx = m_ScriptInterface.GetContext(); + JSContext* cx = m_ScriptInterface->GetContext(); m_PassabilityMapVal = CScriptValRooted(cx, ScriptInterface::ToJSVal(cx, m_PassabilityMap)); } if (territoryMapDirty) { m_TerritoryMap = territoryMap; - JSContext* cx = m_ScriptInterface.GetContext(); + JSContext* cx = m_ScriptInterface->GetContext(); m_TerritoryMapVal = CScriptValRooted(cx, ScriptInterface::ToJSVal(cx, m_TerritoryMap)); } m_CommandsComputed = false; } void WaitToFinishComputation() { if (!m_CommandsComputed) { PerformComputation(); m_CommandsComputed = true; } } void GetCommands(std::vector& commands) { WaitToFinishComputation(); commands.clear(); commands.resize(m_Players.size()); for (size_t i = 0; i < m_Players.size(); ++i) { commands[i].player = m_Players[i]->m_Player; commands[i].commands = m_Players[i]->m_Commands; } } void RegisterTechTemplates(const shared_ptr& techTemplates) { - JSContext* cx = m_ScriptInterface.GetContext(); - m_TechTemplates = CScriptValRooted(cx, m_ScriptInterface.ReadStructuredClone(techTemplates)); + JSContext* cx = m_ScriptInterface->GetContext(); + m_TechTemplates = CScriptValRooted(cx, m_ScriptInterface->ReadStructuredClone(techTemplates)); } void LoadEntityTemplates(const std::vector >& templates) { m_HasLoadedEntityTemplates = true; - m_ScriptInterface.Eval("({})", m_EntityTemplates); + m_ScriptInterface->Eval("({})", m_EntityTemplates); for (size_t i = 0; i < templates.size(); ++i) { - jsval val = templates[i].second->ToJSVal(m_ScriptInterface.GetContext(), false); - m_ScriptInterface.SetProperty(m_EntityTemplates.get(), templates[i].first.c_str(), CScriptVal(val), true); + jsval val = templates[i].second->ToJSVal(m_ScriptInterface->GetContext(), false); + m_ScriptInterface->SetProperty(m_EntityTemplates.get(), templates[i].first.c_str(), CScriptVal(val), true); } // Since the template data is shared between AI players, freeze it // to stop any of them changing it and confusing the other players - m_ScriptInterface.FreezeObject(m_EntityTemplates.get(), true); + m_ScriptInterface->FreezeObject(m_EntityTemplates.get(), true); } void Serialize(std::ostream& stream, bool isDebug) { WaitToFinishComputation(); if (isDebug) { - CDebugSerializer serializer(m_ScriptInterface, stream); + CDebugSerializer serializer(*m_ScriptInterface, stream); serializer.Indent(4); SerializeState(serializer); } else { - CStdSerializer serializer(m_ScriptInterface, stream); + CStdSerializer serializer(*m_ScriptInterface, stream); // TODO: see comment in Deserialize() serializer.SetSerializablePrototypes(m_SerializablePrototypes); SerializeState(serializer); } } void SerializeState(ISerializer& serializer) { std::stringstream rngStream; rngStream << m_RNG; serializer.StringASCII("rng", rngStream.str(), 0, 32); serializer.NumberU32_Unbounded("turn", m_TurnNum); serializer.NumberU32_Unbounded("num ais", (u32)m_Players.size()); serializer.Bool("useSharedScript", m_HasSharedComponent); if (m_HasSharedComponent) { CScriptVal sharedData; - if (!m_ScriptInterface.CallFunction(m_SharedAIObj.get(), "Serialize", sharedData)) + if (!m_ScriptInterface->CallFunction(m_SharedAIObj.get(), "Serialize", sharedData)) LOGERROR(L"AI shared script Serialize call failed"); serializer.ScriptVal("sharedData", sharedData); } for (size_t i = 0; i < m_Players.size(); ++i) { serializer.String("name", m_Players[i]->m_AIName, 1, 256); serializer.NumberI32_Unbounded("player", m_Players[i]->m_Player); serializer.NumberU8_Unbounded("difficulty", m_Players[i]->m_Difficulty); serializer.NumberU32_Unbounded("num commands", (u32)m_Players[i]->m_Commands.size()); for (size_t j = 0; j < m_Players[i]->m_Commands.size(); ++j) { - CScriptVal val = m_ScriptInterface.ReadStructuredClone(m_Players[i]->m_Commands[j]); + CScriptVal val = m_ScriptInterface->ReadStructuredClone(m_Players[i]->m_Commands[j]); serializer.ScriptVal("command", val); } - bool hasCustomSerialize = m_ScriptInterface.HasProperty(m_Players[i]->m_Obj.get(), "Serialize"); + bool hasCustomSerialize = m_ScriptInterface->HasProperty(m_Players[i]->m_Obj.get(), "Serialize"); if (hasCustomSerialize) { CScriptVal scriptData; - if (!m_ScriptInterface.CallFunction(m_Players[i]->m_Obj.get(), "Serialize", scriptData)) + if (!m_ScriptInterface->CallFunction(m_Players[i]->m_Obj.get(), "Serialize", scriptData)) LOGERROR(L"AI script Serialize call failed"); serializer.ScriptVal("data", scriptData); } else { serializer.ScriptVal("data", m_Players[i]->m_Obj.get()); } } } void Deserialize(std::istream& stream) { ENSURE(m_CommandsComputed); // deserializing while we're still actively computing would be bad - CStdDeserializer deserializer(m_ScriptInterface, stream); + CStdDeserializer deserializer(*m_ScriptInterface, stream); m_PlayerMetadata.clear(); m_Players.clear(); std::string rngString; std::stringstream rngStream; deserializer.StringASCII("rng", rngString, 0, 32); rngStream << rngString; rngStream >> m_RNG; deserializer.NumberU32_Unbounded("turn", m_TurnNum); uint32_t numAis; deserializer.NumberU32_Unbounded("num ais", numAis); deserializer.Bool("useSharedScript", m_HasSharedComponent); TryLoadSharedComponent(false); if (m_HasSharedComponent) { CScriptVal sharedData; deserializer.ScriptVal("sharedData", sharedData); - if (!m_ScriptInterface.CallFunctionVoid(m_SharedAIObj.get(), "Deserialize", sharedData)) + if (!m_ScriptInterface->CallFunctionVoid(m_SharedAIObj.get(), "Deserialize", sharedData)) LOGERROR(L"AI shared script Deserialize call failed"); } for (size_t i = 0; i < numAis; ++i) { std::wstring name; player_id_t player; uint8_t difficulty; deserializer.String("name", name, 1, 256); deserializer.NumberI32_Unbounded("player", player); deserializer.NumberU8_Unbounded("difficulty",difficulty); if (!AddPlayer(name, player, difficulty, true)) throw PSERROR_Deserialize_ScriptError(); uint32_t numCommands; deserializer.NumberU32_Unbounded("num commands", numCommands); m_Players.back()->m_Commands.reserve(numCommands); for (size_t j = 0; j < numCommands; ++j) { CScriptVal val; deserializer.ScriptVal("command", val); - m_Players.back()->m_Commands.push_back(m_ScriptInterface.WriteStructuredClone(val.get())); + m_Players.back()->m_Commands.push_back(m_ScriptInterface->WriteStructuredClone(val.get())); } // TODO: this is yucky but necessary while the AIs are sharing data between contexts; // ideally a new (de)serializer instance would be created for each player // so they would have a single, consistent script context to use and serializable // prototypes could be stored in their ScriptInterface deserializer.SetSerializablePrototypes(m_DeserializablePrototypes); - bool hasCustomDeserialize = m_ScriptInterface.HasProperty(m_Players.back()->m_Obj.get(), "Deserialize"); + bool hasCustomDeserialize = m_ScriptInterface->HasProperty(m_Players.back()->m_Obj.get(), "Deserialize"); if (hasCustomDeserialize) { CScriptVal scriptData; deserializer.ScriptVal("data", scriptData); if (m_Players[i]->m_UseSharedComponent) { - if (!m_ScriptInterface.CallFunctionVoid(m_Players.back()->m_Obj.get(), "Deserialize", scriptData, m_SharedAIObj)) + if (!m_ScriptInterface->CallFunctionVoid(m_Players.back()->m_Obj.get(), "Deserialize", scriptData, m_SharedAIObj)) LOGERROR(L"AI script Deserialize call failed"); } - else if (!m_ScriptInterface.CallFunctionVoid(m_Players.back()->m_Obj.get(), "Deserialize", scriptData)) + else if (!m_ScriptInterface->CallFunctionVoid(m_Players.back()->m_Obj.get(), "Deserialize", scriptData)) { LOGERROR(L"AI script deserialize() call failed"); } } else { deserializer.ScriptVal("data", m_Players.back()->m_Obj); } } } int getPlayerSize() { return m_Players.size(); } void RegisterSerializablePrototype(std::wstring name, CScriptVal proto) { // Require unique prototype and name (for reverse lookup) // TODO: this is yucky - see comment in Deserialize() JSObject* obj = JSVAL_TO_OBJECT(proto.get()); std::pair::iterator, bool> ret1 = m_SerializablePrototypes.insert(std::make_pair(obj, name)); std::pair::iterator, bool> ret2 = m_DeserializablePrototypes.insert(std::make_pair(name, obj)); if (!ret1.second || !ret2.second) LOGERROR(L"RegisterSerializablePrototype called with same prototype multiple times: p=%p n='%ls'", obj, name.c_str()); } private: CScriptValRooted LoadMetadata(const VfsPath& path) { if (m_PlayerMetadata.find(path) == m_PlayerMetadata.end()) { // Load and cache the AI player metadata - m_PlayerMetadata[path] = m_ScriptInterface.ReadJSONFile(path); + m_PlayerMetadata[path] = m_ScriptInterface->ReadJSONFile(path); } return m_PlayerMetadata[path]; } void PerformComputation() { if (m_Players.size() == 0) { // Run the GC every so often. // (This isn't particularly necessary, but it makes profiling clearer // since it avoids random GC delays while running other scripts) if (m_TurnNum++ % 50 == 0) { PROFILE3("AI compute GC"); - m_ScriptInterface.MaybeGC(); + m_ScriptInterface->MaybeGC(); } return; } // Deserialize the game state, to pass to the AI's HandleMessage CScriptVal state; { PROFILE3("AI compute read state"); - state = m_ScriptInterface.ReadStructuredClone(m_GameState); - m_ScriptInterface.SetProperty(state.get(), "passabilityMap", m_PassabilityMapVal, true); - m_ScriptInterface.SetProperty(state.get(), "territoryMap", m_TerritoryMapVal, true); + state = m_ScriptInterface->ReadStructuredClone(m_GameState); + m_ScriptInterface->SetProperty(state.get(), "passabilityMap", m_PassabilityMapVal, true); + m_ScriptInterface->SetProperty(state.get(), "territoryMap", m_TerritoryMapVal, true); } // It would be nice to do - // m_ScriptInterface.FreezeObject(state.get(), true); + // m_ScriptInterface->FreezeObject(state.get(), true); // to prevent AI scripts accidentally modifying the state and // affecting other AI scripts they share it with. But the performance // cost is far too high, so we won't do that. // If there is a shared component, run it if (m_HasSharedComponent) { PROFILE3("AI run shared component"); - m_ScriptInterface.CallFunctionVoid(m_SharedAIObj.get(), "onUpdate", state); + m_ScriptInterface->CallFunctionVoid(m_SharedAIObj.get(), "onUpdate", state); } for (size_t i = 0; i < m_Players.size(); ++i) { PROFILE3("AI script"); PROFILE2_ATTR("player: %d", m_Players[i]->m_Player); PROFILE2_ATTR("script: %ls", m_Players[i]->m_AIName.c_str()); + if (m_HasSharedComponent && m_Players[i]->m_UseSharedComponent) - m_Players[i]->Run(state,m_SharedAIObj); + m_Players[i]->Run(state, m_Players[i]->m_Player, m_SharedAIObj); else - m_Players[i]->Run(state); + m_Players[i]->Run(state, m_Players[i]->m_Player); } // Run GC if we are about to overflow - if (JS_GetGCParameter(m_ScriptInterface.GetRuntime(), JSGC_BYTES) > 33000000) + if (JS_GetGCParameter(m_ScriptInterface->GetRuntime(), JSGC_BYTES) > 33000000) { PROFILE3("AI compute GC"); - JS_GC(m_ScriptInterface.GetContext()); + JS_GC(m_ScriptInterface->GetContext()); } // Run the GC every so often. // (This isn't particularly necessary, but it makes profiling clearer // since it avoids random GC delays while running other scripts) /*if (m_TurnNum++ % 20 == 0) { PROFILE3("AI compute GC"); - m_ScriptInterface.MaybeGC(); + m_ScriptInterface->MaybeGC(); }*/ } shared_ptr m_ScriptRuntime; - ScriptInterface m_ScriptInterface; + shared_ptr m_ScriptInterface; boost::rand48 m_RNG; u32 m_TurnNum; CScriptValRooted m_EntityTemplates; bool m_HasLoadedEntityTemplates; CScriptValRooted m_TechTemplates; std::map m_PlayerMetadata; std::vector > m_Players; // use shared_ptr just to avoid copying bool m_HasSharedComponent; CScriptValRooted m_SharedAIObj; std::vector m_Commands; + std::set m_LoadedModules; + shared_ptr m_GameState; Grid m_PassabilityMap; CScriptValRooted m_PassabilityMapVal; Grid m_TerritoryMap; CScriptValRooted m_TerritoryMapVal; bool m_CommandsComputed; std::map m_SerializablePrototypes; std::map m_DeserializablePrototypes; }; /** * Implementation of ICmpAIManager. */ class CCmpAIManager : public ICmpAIManager { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeToMessageType(MT_ProgressiveLoad); } DEFAULT_COMPONENT_ALLOCATOR(AIManager) static std::string GetSchema() { return ""; } virtual void Init(const CParamNode& UNUSED(paramNode)) { m_TerritoriesDirtyID = 0; m_JustDeserialized = false; StartLoadEntityTemplates(); } virtual void Deinit() { } virtual void Serialize(ISerializer& serialize) { // Because the AI worker uses its own ScriptInterface, we can't use the // ISerializer (which was initialised with the simulation ScriptInterface) // directly. So we'll just grab the ISerializer's stream and write to it // with an independent serializer. m_Worker.Serialize(serialize.GetStream(), serialize.IsDebug()); } virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) { Init(paramNode); ForceLoadEntityTemplates(); m_Worker.Deserialize(deserialize.GetStream()); m_JustDeserialized = true; } virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_ProgressiveLoad: { const CMessageProgressiveLoad& msgData = static_cast (msg); *msgData.total += (int)m_TemplateNames.size(); if (*msgData.progressed) break; if (ContinueLoadEntityTemplates()) *msgData.progressed = true; *msgData.progress += (int)m_TemplateLoadedIdx; break; } } } virtual void AddPlayer(std::wstring id, player_id_t player, uint8_t difficulty) { m_Worker.AddPlayer(id, player, difficulty, true); // AI players can cheat and see through FoW/SoD, since that greatly simplifies // their implementation. // (TODO: maybe cleverer AIs should be able to optionally retain FoW/SoD) CmpPtr cmpRangeManager(GetSystemEntity()); if (cmpRangeManager) cmpRangeManager->SetLosRevealAll(player, true); } virtual void TryLoadSharedComponent() { ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); // load the technology templates CmpPtr cmpTechTemplateManager(GetSystemEntity()); ENSURE(cmpTechTemplateManager); // Get the game state from AIInterface CScriptVal techTemplates = cmpTechTemplateManager->GetAllTechs(); m_Worker.RegisterTechTemplates(scriptInterface.WriteStructuredClone(techTemplates.get())); m_Worker.TryLoadSharedComponent(true); } virtual void RunGamestateInit() { ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); CmpPtr cmpAIInterface(GetSystemEntity()); ENSURE(cmpAIInterface); // Get the game state from AIInterface // We flush events from the initialization so we get a clean state now. CScriptVal state = cmpAIInterface->GetFullRepresentation(true); // Get the passability data Grid dummyGrid; const Grid* passabilityMap = &dummyGrid; CmpPtr cmpPathfinder(GetSystemEntity()); if (cmpPathfinder) passabilityMap = &cmpPathfinder->GetPassabilityGrid(); // Get the territory data // Since getting the territory grid can trigger a recalculation, we check NeedUpdate first Grid dummyGrid2; const Grid* territoryMap = &dummyGrid2; CmpPtr cmpTerritoryManager(GetSystemEntity()); if (cmpTerritoryManager && cmpTerritoryManager->NeedUpdate(&m_TerritoriesDirtyID)) { territoryMap = &cmpTerritoryManager->GetTerritoryGrid(); } LoadPathfinderClasses(state); m_Worker.RunGamestateInit(scriptInterface.WriteStructuredClone(state.get()), *passabilityMap, *territoryMap); } virtual void StartComputation() { PROFILE("AI setup"); ForceLoadEntityTemplates(); ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); if (m_Worker.getPlayerSize() == 0) return; CmpPtr cmpAIInterface(GetSystemEntity()); ENSURE(cmpAIInterface); // Get the game state from AIInterface CScriptVal state; if (m_JustDeserialized) state = cmpAIInterface->GetFullRepresentation(true); else state = cmpAIInterface->GetRepresentation(); // Get the passability data Grid dummyGrid; const Grid* passabilityMap = &dummyGrid; CmpPtr cmpPathfinder(GetSystemEntity()); if (cmpPathfinder) passabilityMap = &cmpPathfinder->GetPassabilityGrid(); // Get the territory data // Since getting the territory grid can trigger a recalculation, we check NeedUpdate first bool territoryMapDirty = false; Grid dummyGrid2; const Grid* territoryMap = &dummyGrid2; CmpPtr cmpTerritoryManager(GetSystemEntity()); if (cmpTerritoryManager && cmpTerritoryManager->NeedUpdate(&m_TerritoriesDirtyID)) { territoryMap = &cmpTerritoryManager->GetTerritoryGrid(); territoryMapDirty = true; } LoadPathfinderClasses(state); m_Worker.StartComputation(scriptInterface.WriteStructuredClone(state.get()), *passabilityMap, *territoryMap, territoryMapDirty); m_JustDeserialized = false; } virtual void PushCommands() { ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); std::vector commands; m_Worker.GetCommands(commands); CmpPtr cmpCommandQueue(GetSystemEntity()); if (!cmpCommandQueue) return; for (size_t i = 0; i < commands.size(); ++i) { for (size_t j = 0; j < commands[i].commands.size(); ++j) { cmpCommandQueue->PushLocalCommand(commands[i].player, scriptInterface.ReadStructuredClone(commands[i].commands[j])); } } } private: std::vector m_TemplateNames; size_t m_TemplateLoadedIdx; std::vector > m_Templates; size_t m_TerritoriesDirtyID; bool m_JustDeserialized; void StartLoadEntityTemplates() { CmpPtr cmpTemplateManager(GetSystemEntity()); ENSURE(cmpTemplateManager); m_TemplateNames = cmpTemplateManager->FindAllTemplates(false); m_TemplateLoadedIdx = 0; m_Templates.reserve(m_TemplateNames.size()); } // Tries to load the next entity template. Returns true if we did some work. bool ContinueLoadEntityTemplates() { if (m_TemplateLoadedIdx >= m_TemplateNames.size()) return false; CmpPtr cmpTemplateManager(GetSystemEntity()); ENSURE(cmpTemplateManager); const CParamNode* node = cmpTemplateManager->GetTemplateWithoutValidation(m_TemplateNames[m_TemplateLoadedIdx]); if (node) m_Templates.push_back(std::make_pair(m_TemplateNames[m_TemplateLoadedIdx], node)); m_TemplateLoadedIdx++; // If this was the last template, send the data to the worker if (m_TemplateLoadedIdx == m_TemplateNames.size()) m_Worker.LoadEntityTemplates(m_Templates); return true; } void ForceLoadEntityTemplates() { while (ContinueLoadEntityTemplates()) { } } void LoadPathfinderClasses(CScriptVal state) { CmpPtr cmpPathfinder(GetSystemEntity()); if (!cmpPathfinder) return; ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); CScriptVal classesVal; scriptInterface.Eval("({ pathfinderObstruction: 1, foundationObstruction: 2 })", classesVal); std::map classes = cmpPathfinder->GetPassabilityClasses(); for (std::map::iterator it = classes.begin(); it != classes.end(); ++it) scriptInterface.SetProperty(classesVal.get(), it->first.c_str(), it->second, true); scriptInterface.SetProperty(state.get(), "passabilityClasses", classesVal, true); } CAIWorker m_Worker; }; REGISTER_COMPONENT_TYPE(AIManager) Index: ps/trunk/binaries/data/mods/public/simulation/ai/qbot/economy.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/qbot/economy.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/qbot/economy.js (revision 14441) @@ -1,543 +1,543 @@ var EconomyManager = function() { this.targetNumBuilders = 5; // number of workers we want building stuff this.targetNumFields = 3; this.resourceMaps = {}; // Contains maps showing the density of wood, stone and metal this.setCount = 0; //stops villagers being reassigned to other resources too frequently, count a set number of //turns before trying to reassign them. this.dropsiteNumbers = {wood: 2, stone: 1, metal: 1}; }; // More initialisation for stuff that needs the gameState EconomyManager.prototype.init = function(gameState){ this.targetNumWorkers = Math.max(Math.floor(gameState.getPopulationMax()/3), 1); }; EconomyManager.prototype.trainMoreWorkers = function(gameState, queues) { // Count the workers in the world and in progress var numWorkers = gameState.countEntitiesAndQueuedByType(gameState.applyCiv("units/{civ}_support_female_citizen")); numWorkers += queues.villager.countTotalQueuedUnits(); // If we have too few, train more if (numWorkers < this.targetNumWorkers) { for ( var i = 0; i < this.targetNumWorkers - numWorkers; i++) { queues.villager.addItem(new UnitTrainingPlan(gameState, "units/{civ}_support_female_citizen", { "role" : "worker" })); } } }; // Pick the resource which most needs another worker EconomyManager.prototype.pickMostNeededResources = function(gameState) { var self = this; // Find what resource type we're most in need of if (!gameState.turnCache["gather-weights-calculated"]){ this.gatherWeights = gameState.ai.queueManager.futureNeeds(gameState); gameState.turnCache["gather-weights-calculated"] = true; } var numGatherers = {}; for ( var type in this.gatherWeights){ numGatherers[type] = gameState.updatingCollection("workers-gathering-" + type, Filters.byMetadata("gather-type", type), gameState.getOwnEntitiesByRole("worker")).length; } var types = Object.keys(this.gatherWeights); types.sort(function(a, b) { // Prefer fewer gatherers (divided by weight) var va = numGatherers[a] / (self.gatherWeights[a]+1); var vb = numGatherers[b] / (self.gatherWeights[b]+1); return va-vb; }); return types; }; EconomyManager.prototype.reassignRolelessUnits = function(gameState) { //TODO: Move this out of the economic section var roleless = gameState.getOwnEntitiesByRole(undefined); roleless.forEach(function(ent) { if (ent.hasClass("Worker")){ ent.setMetadata("role", "worker"); }else if(ent.hasClass("CitizenSoldier") || ent.hasClass("Champion")){ ent.setMetadata("role", "soldier"); }else{ ent.setMetadata("role", "unknown"); } }); }; // If the numbers of workers on the resources is unbalanced then set some of workers to idle so // they can be reassigned by reassignIdleWorkers. EconomyManager.prototype.setWorkersIdleByPriority = function(gameState){ this.gatherWeights = gameState.ai.queueManager.futureNeeds(gameState); var numGatherers = {}; var totalGatherers = 0; var totalWeight = 0; for ( var type in this.gatherWeights){ numGatherers[type] = 0; totalWeight += this.gatherWeights[type]; } gameState.getOwnEntitiesByRole("worker").forEach(function(ent) { if (ent.getMetadata("subrole") === "gatherer"){ numGatherers[ent.getMetadata("gather-type")] += 1; totalGatherers += 1; } }); for ( var type in this.gatherWeights){ var allocation = Math.floor(totalGatherers * (this.gatherWeights[type]/totalWeight)); if (allocation < numGatherers[type]){ var numToTake = numGatherers[type] - allocation; gameState.getOwnEntitiesByRole("worker").forEach(function(ent) { if (ent.getMetadata("subrole") === "gatherer" && ent.getMetadata("gather-type") === type && numToTake > 0){ ent.setMetadata("subrole", "idle"); numToTake -= 1; } }); } } }; EconomyManager.prototype.reassignIdleWorkers = function(gameState) { var self = this; // Search for idle workers, and tell them to gather resources based on demand var filter = Filters.or(Filters.isIdle(), Filters.byMetadata("subrole", "idle")); var idleWorkers = gameState.updatingCollection("idle-workers", filter, gameState.getOwnEntitiesByRole("worker")); if (idleWorkers.length) { var resourceSupplies; idleWorkers.forEach(function(ent) { // Check that the worker isn't garrisoned if (ent.position() === undefined){ return; } var types = self.pickMostNeededResources(gameState); ent.setMetadata("subrole", "gatherer"); ent.setMetadata("gather-type", types[0]); }); } }; EconomyManager.prototype.workersBySubrole = function(gameState, subrole) { var workers = gameState.getOwnEntitiesByRole("worker"); return gameState.updatingCollection("subrole-" + subrole, Filters.byMetadata("subrole", subrole), workers); }; EconomyManager.prototype.assignToFoundations = function(gameState) { // If we have some foundations, and we don't have enough // builder-workers, // try reassigning some other workers who are nearby var foundations = gameState.getOwnFoundations(); // Check if nothing to build if (!foundations.length){ return; } var workers = gameState.getOwnEntitiesByRole("worker"); var builderWorkers = this.workersBySubrole(gameState, "builder"); // Check if enough builders var extraNeeded = this.targetNumBuilders - builderWorkers.length; if (extraNeeded <= 0){ return; } // Pick non-builders who are closest to the first foundation, // and tell them to start building it var target = foundations.toEntityArray()[0]; var nonBuilderWorkers = workers.filter(function(ent) { // check position so garrisoned units aren't tasked return (ent.getMetadata("subrole") !== "builder" && ent.position() !== undefined); }); var nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), extraNeeded); // Order each builder individually, not as a formation nearestNonBuilders.forEach(function(ent) { ent.setMetadata("subrole", "builder"); ent.setMetadata("target-foundation", target); }); }; EconomyManager.prototype.buildMoreFields = function(gameState, queues) { // give time for treasures to be gathered if (gameState.getTimeElapsed() < 30 * 1000) return; var numFood = 0; gameState.updatingCollection("active-dropsite-food", Filters.byMetadata("active-dropsite-food", true), gameState.getOwnDropsites("food")).forEach(function (dropsite){ numFood += dropsite.getMetadata("nearby-resources-food").length; }); numFood += gameState.countFoundationsWithType(gameState.applyCiv("structures/{civ}_field")); numFood += queues.field.totalLength(); for ( var i = numFood; i < this.targetNumFields; i++) { queues.field.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_field")); } }; // If all the CC's are destroyed then build a new one EconomyManager.prototype.buildNewCC= function(gameState, queues) { var numCCs = gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_civil_centre")); numCCs += queues.civilCentre.totalLength(); for ( var i = numCCs; i < 1; i++) { queues.civilCentre.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_civil_centre")); } }; //creates and maintains a map of tree density EconomyManager.prototype.updateResourceMaps = function(gameState, events){ // The weight of the influence function is amountOfResource/decreaseFactor var decreaseFactor = {'wood': 15, 'stone': 100, 'metal': 100, 'food': 20}; // This is the maximum radius of the influence var radius = {'wood':13, 'stone': 10, 'metal': 10, 'food': 10}; var self = this; for (var resource in radius){ // if there is no resourceMap create one with an influence for everything with that resource if (! this.resourceMaps[resource]){ this.resourceMaps[resource] = new Map(gameState); var supplies = gameState.getResourceSupplies(resource); supplies.forEach(function(ent){ if (!ent.position()){ return; } var x = Math.round(ent.position()[0] / gameState.cellSize); var z = Math.round(ent.position()[1] / gameState.cellSize); var strength = Math.round(ent.resourceSupplyMax()/decreaseFactor[resource]); self.resourceMaps[resource].addInfluence(x, z, radius[resource], strength); }); } // TODO: fix for treasure and move out of loop // Look for destroy events and subtract the entities original influence from the resourceMap for (var i in events) { var e = events[i]; if (e.type === "Destroy") { if (e.msg.entityObj){ var ent = e.msg.entityObj; if (ent && ent.position() && ent.resourceSupplyType() && ent.resourceSupplyType().generic === resource){ var x = Math.round(ent.position()[0] / gameState.cellSize); var z = Math.round(ent.position()[1] / gameState.cellSize); var strength = Math.round(ent.resourceSupplyMax()/decreaseFactor[resource]); this.resourceMaps[resource].addInfluence(x, z, radius[resource], -strength); } } }else if (e.type === "Create") { if (e.msg.entityObj){ var ent = e.msg.entityObj; if (ent && ent.position() && ent.resourceSupplyType() && ent.resourceSupplyType().generic === resource){ var x = Math.round(ent.position()[0] / gameState.cellSize); var z = Math.round(ent.position()[1] / gameState.cellSize); var strength = Math.round(ent.resourceSupplyMax()/decreaseFactor[resource]); this.resourceMaps[resource].addInfluence(x, z, radius[resource], strength); } } } } } //this.resourceMaps['wood'].dumpIm("tree_density.png"); }; // Returns the position of the best place to build a new dropsite for the specified resource EconomyManager.prototype.getBestResourceBuildSpot = function(gameState, resource){ // A map which gives a positive weight for all CCs and adds a negative weight near all dropsites var friendlyTiles = new Map(gameState); gameState.getOwnEntities().forEach(function(ent) { // We want to build near a CC of ours if (ent.hasClass("CivCentre")){ var infl = 200; var pos = ent.position(); var x = Math.round(pos[0] / gameState.cellSize); var z = Math.round(pos[1] / gameState.cellSize); friendlyTiles.addInfluence(x, z, infl, 0.1 * infl); friendlyTiles.addInfluence(x, z, infl/2, 0.1 * infl); } // We don't want multiple dropsites at one spot so add a negative for all dropsites if (ent.resourceDropsiteTypes() && ent.resourceDropsiteTypes().indexOf(resource) !== -1){ var infl = 20; var pos = ent.position(); var x = Math.round(pos[0] / gameState.cellSize); var z = Math.round(pos[1] / gameState.cellSize); friendlyTiles.addInfluence(x, z, infl, -50, 'quadratic'); } }); // Multiply by tree density to get a combination of the two maps friendlyTiles.multiply(this.resourceMaps[resource]); //friendlyTiles.dumpIm(resource + "_density_fade.png", 10000); var obstructions = Map.createObstructionMap(gameState); obstructions.expandInfluences(); var bestIdx = friendlyTiles.findBestTile(4, obstructions)[0]; // Convert from 1d map pixel coordinates to game engine coordinates var x = ((bestIdx % friendlyTiles.width) + 0.5) * gameState.cellSize; var z = (Math.floor(bestIdx / friendlyTiles.width) + 0.5) * gameState.cellSize; return [x,z]; }; EconomyManager.prototype.updateResourceConcentrations = function(gameState){ var self = this; var resources = ["food", "wood", "stone", "metal"]; for (var key in resources){ var resource = resources[key]; gameState.getOwnEntities().forEach(function(ent) { if (ent.resourceDropsiteTypes() && ent.resourceDropsiteTypes().indexOf(resource) !== -1){ var radius = 14; var pos = ent.position(); var x = Math.round(pos[0] / gameState.cellSize); var z = Math.round(pos[1] / gameState.cellSize); var quantity = self.resourceMaps[resource].sumInfluence(x, z, radius); ent.setMetadata("resourceQuantity_" + resource, quantity); } }); } }; // Stores lists of nearby resources EconomyManager.prototype.updateNearbyResources = function(gameState){ var self = this; var resources = ["food", "wood", "stone", "metal"]; var resourceSupplies; var radius = 100; for (var key in resources){ var resource = resources[key]; gameState.getOwnDropsites(resource).forEach(function(ent) { if (ent.getMetadata("nearby-resources-" + resource) === undefined){ var filterPos = Filters.byStaticDistance(ent.position(), radius); var collection = gameState.getResourceSupplies(resource).filter(filterPos); collection.registerUpdates(); ent.setMetadata("nearby-resources-" + resource, collection); ent.setMetadata("active-dropsite-" + resource, true); } if (ent.getMetadata("nearby-resources-" + resource).length === 0){ ent.setMetadata("active-dropsite-" + resource, false); }else{ ent.setMetadata("active-dropsite-" + resource, true); } /* // Make resources glow wildly if (resource == "food"){ ent.getMetadata("nearby-resources-" + resource).forEach(function(ent){ - Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [10,0,0]}); + Engine.PostCommand(PlayerID, {"type": "set-shading-color", "entities": [ent.id()], "rgb": [10,0,0]}); }); } if (resource == "wood"){ ent.getMetadata("nearby-resources-" + resource).forEach(function(ent){ - Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,10,0]}); + Engine.PostCommand(PlayerID, {"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,10,0]}); }); } if (resource == "metal"){ ent.getMetadata("nearby-resources-" + resource).forEach(function(ent){ - Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,0,10]}); + Engine.PostCommand(PlayerID, {"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,0,10]}); }); }*/ }); } }; //return the number of resource dropsites with an acceptable amount of the resource nearby EconomyManager.prototype.checkResourceConcentrations = function(gameState, resource){ //TODO: make these values adaptive var requiredInfluence = {wood: 16000, stone: 300, metal: 300}; var count = 0; gameState.getOwnEntities().forEach(function(ent) { if (ent.resourceDropsiteTypes() && ent.resourceDropsiteTypes().indexOf(resource) !== -1){ var quantity = ent.getMetadata("resourceQuantity_" + resource); if (quantity >= requiredInfluence[resource]){ count ++; } } }); return count; }; EconomyManager.prototype.buildMarket = function(gameState, queues){ if (gameState.getTimeElapsed() > 600 * 1000){ if (queues.economicBuilding.totalLength() === 0 && gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_market")) === 0){ //only ever build one storehouse/CC/market at a time queues.economicBuilding.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_market")); } } }; EconomyManager.prototype.buildDropsites = function(gameState, queues){ if (queues.economicBuilding.totalLength() === 0 && gameState.countFoundationsWithType(gameState.applyCiv("structures/{civ}_storehouse")) === 0 && gameState.countFoundationsWithType(gameState.applyCiv("structures/{civ}_civil_centre")) === 0){ //only ever build one storehouse/CC/market at a time if (gameState.getTimeElapsed() > 30 * 1000){ for (var resource in this.dropsiteNumbers){ if (this.checkResourceConcentrations(gameState, resource) < this.dropsiteNumbers[resource]){ var spot = this.getBestResourceBuildSpot(gameState, resource); var myCivCentres = gameState.getOwnEntities().filter(function(ent) { if (!ent.hasClass("CivCentre") || ent.position() === undefined){ return false; } var dx = (spot[0]-ent.position()[0]); var dy = (spot[1]-ent.position()[1]); var dist2 = dx*dx + dy*dy; return (ent.hasClass("CivCentre") && dist2 < 180*180); }); if (myCivCentres.length === 0){ queues.economicBuilding.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_civil_centre", spot)); }else{ queues.economicBuilding.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_storehouse", spot)); } break; } } } } }; EconomyManager.prototype.update = function(gameState, queues, events) { Engine.ProfileStart("economy update"); this.reassignRolelessUnits(gameState); this.buildNewCC(gameState,queues); Engine.ProfileStart("Train workers and build farms"); this.trainMoreWorkers(gameState, queues); this.buildMoreFields(gameState, queues); Engine.ProfileStop(); //Later in the game we want to build stuff faster. if (gameState.countEntitiesByType(gameState.applyCiv("units/{civ}_support_female_citizen")) > this.targetNumWorkers * 0.5) { this.targetNumBuilders = 10; }else{ this.targetNumBuilders = 5; } if (gameState.countEntitiesByType(gameState.applyCiv("units/{civ}_support_female_citizen")) > this.targetNumWorkers * 0.8) { this.dropsiteNumbers = {wood: 3, stone: 2, metal: 2}; }else{ this.dropsiteNumbers = {wood: 2, stone: 1, metal: 1}; } Engine.ProfileStart("Update Resource Maps and Concentrations"); this.updateResourceMaps(gameState, events); this.updateResourceConcentrations(gameState); this.updateNearbyResources(gameState); Engine.ProfileStop(); Engine.ProfileStart("Build new Dropsites"); this.buildDropsites(gameState, queues); Engine.ProfileStop(); this.buildMarket(gameState, queues); // TODO: implement a timer based system for this this.setCount += 1; if (this.setCount >= 20){ this.setWorkersIdleByPriority(gameState); this.setCount = 0; } Engine.ProfileStart("Reassign Idle Workers"); this.reassignIdleWorkers(gameState); Engine.ProfileStop(); Engine.ProfileStart("Swap Workers"); var gathererGroups = {}; gameState.getOwnEntitiesByRole("worker").forEach(function(ent){ var key = uneval(ent.resourceGatherRates()); if (!gathererGroups[key]){ gathererGroups[key] = {"food": [], "wood": [], "metal": [], "stone": []}; } if (ent.getMetadata("gather-type") in gathererGroups[key]){ gathererGroups[key][ent.getMetadata("gather-type")].push(ent); } }); for (var i in gathererGroups){ for (var j in gathererGroups){ var a = eval(i); var b = eval(j); if (a["food.grain"]/b["food.grain"] > a["wood.tree"]/b["wood.tree"] && gathererGroups[i]["wood"].length > 0 && gathererGroups[j]["food"].length > 0){ for (var k = 0; k < Math.min(gathererGroups[i]["wood"].length, gathererGroups[j]["food"].length); k++){ gathererGroups[i]["wood"][k].setMetadata("gather-type", "food"); gathererGroups[j]["food"][k].setMetadata("gather-type", "wood"); } } } } Engine.ProfileStop(); Engine.ProfileStart("Assign builders"); this.assignToFoundations(gameState); Engine.ProfileStop(); Engine.ProfileStart("Run Workers"); gameState.getOwnEntitiesByRole("worker").forEach(function(ent){ if (!ent.getMetadata("worker-object")){ ent.setMetadata("worker-object", new Worker(ent)); } ent.getMetadata("worker-object").update(gameState); }); // Gatherer count updates for non-workers var filter = Filters.and(Filters.not(Filters.byMetadata("worker-object", undefined)), Filters.not(Filters.byMetadata("role", "worker"))); gameState.updatingCollection("reassigned-workers", filter, gameState.getOwnEntities()).forEach(function(ent){ ent.getMetadata("worker-object").updateGathererCounts(gameState); }); // Gatherer count updates for destroyed units for (var i in events) { var e = events[i]; if (e.type === "Destroy") { if (e.msg.metadata && e.msg.metadata[gameState.getPlayerID()] && e.msg.metadata[gameState.getPlayerID()]["worker-object"]){ e.msg.metadata[gameState.getPlayerID()]["worker-object"].updateGathererCounts(gameState, true); } } } Engine.ProfileStop(); Engine.ProfileStop(); }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/qbot/qbot.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/qbot/qbot.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/qbot/qbot.js (revision 14441) @@ -1,180 +1,184 @@ +var g_debugEnabled = false; function QBotAI(settings) { BaseAI.call(this, settings); this.turn = 0; + this.Config = new Config(); + this.modules = { "economy": new EconomyManager(), - "military": new MilitaryAttackManager(), + "military": new MilitaryAttackManager(this.Config), "housing": new HousingManager() }; + // this.queues cannot be modified past initialisation or queue-manager will break this.queues = { house : new Queue(), citizenSoldier : new Queue(), villager : new Queue(), economicBuilding : new Queue(), field : new Queue(), advancedSoldier : new Queue(), siege : new Queue(), militaryBuilding : new Queue(), defenceBuilding : new Queue(), civilCentre: new Queue() }; this.productionQueues = []; - this.priorities = Config.priorities; + this.priorities = this.Config.priorities; this.queueManager = new QueueManager(this.queues, this.priorities); this.firstTime = true; this.savedEvents = []; } QBotAI.prototype = new BaseAI(); //Some modules need the gameState to fully initialise QBotAI.prototype.runInit = function(gameState){ if (this.firstTime){ for (var i in this.modules){ if (this.modules[i].init){ this.modules[i].init(gameState); } } this.timer = new Timer(); this.firstTime = false; var myKeyEntities = gameState.getOwnEntities().filter(function(ent) { return ent.hasClass("CivCentre"); }); if (myKeyEntities.length == 0){ myKeyEntities = gameState.getOwnEntities(); } var filter = Filters.byClass("CivCentre"); var enemyKeyEntities = gameState.getEnemyEntities().filter(filter); if (enemyKeyEntities.length == 0){ enemyKeyEntities = gameState.getEnemyEntities(); } this.accessibility = new Accessibility(gameState, myKeyEntities.toEntityArray()[0].position()); if (enemyKeyEntities.length == 0) return; var pathFinder = new PathFinder(gameState); this.pathsToMe = pathFinder.getPaths(enemyKeyEntities.toEntityArray()[0].position(), myKeyEntities.toEntityArray()[0].position(), 'entryPoints'); } }; QBotAI.prototype.OnUpdate = function() { if (this.gameFinished){ return; } if (this.events.length > 0){ this.savedEvents = this.savedEvents.concat(this.events); } // Run the update every n turns, offset depending on player ID to balance // the load if ((this.turn + this.player) % 10 == 0) { Engine.ProfileStart("qBot"); var gameState = new GameState(this); if (gameState.getOwnEntities().length === 0){ Engine.ProfileStop(); return; // With no entities to control the AI cannot do anything } this.runInit(gameState); for (var i in this.modules){ this.modules[i].update(gameState, this.queues, this.savedEvents); } this.updateDynamicPriorities(gameState, this.queues); this.queueManager.update(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.length % 29; for (var i = 0; i < n; i++){ Math.random(); } delete this.savedEvents; this.savedEvents = []; Engine.ProfileStop(); } this.turn++; }; QBotAI.prototype.updateDynamicPriorities = function(gameState, queues){ // Dynamically change priorities Engine.ProfileStart("Change Priorities"); var females = gameState.countEntitiesByType(gameState.applyCiv("units/{civ}_support_female_citizen")); var femalesTarget = this.modules["economy"].targetNumWorkers; var enemyStrength = this.modules["military"].measureEnemyStrength(gameState); var availableStrength = this.modules["military"].measureAvailableStrength(); var additionalPriority = (enemyStrength - availableStrength) * 5; additionalPriority = Math.min(Math.max(additionalPriority, -50), 220); var advancedProportion = (availableStrength / 40) * (females/femalesTarget); advancedProportion = Math.min(advancedProportion, 0.7); this.priorities.citizenSoldier = (1-advancedProportion) * (150 + additionalPriority) + 1; this.priorities.advancedSoldier = advancedProportion * (150 + additionalPriority) + 1; if (females/femalesTarget > 0.7){ this.priorities.defenceBuilding = 70; } Engine.ProfileStop(); }; // TODO: Remove override when the whole AI state is serialised QBotAI.prototype.Deserialize = function(data) { BaseAI.prototype.Deserialize.call(this, data); }; // Override the default serializer QBotAI.prototype.Serialize = function() { var ret = BaseAI.prototype.Serialize.call(this); ret._entityMetadata = {}; return ret; }; function debug(output){ - if (Config.debug){ + if (g_debugEnabled){ if (typeof output === "string"){ warn(output); }else{ warn(uneval(output)); } } } function copyPrototype(descendant, parent) { var sConstructor = parent.toString(); var aMatch = sConstructor.match( /\s*function (.*)\(/ ); if ( aMatch != null ) { descendant.prototype[aMatch[1]] = parent; } for (var m in parent.prototype) { descendant.prototype[m] = parent.prototype[m]; } } Index: ps/trunk/binaries/data/mods/public/simulation/ai/qbot/military.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/qbot/military.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/qbot/military.js (revision 14441) @@ -1,459 +1,461 @@ /* * Military strategy: * * Try training an attack squad of a specified size * * When it's the appropriate size, send it to attack the enemy * * Repeat forever * */ -var MilitaryAttackManager = function() { +var MilitaryAttackManager = function(Config) { + + this.Config = Config // these use the structure soldiers[unitId] = true|false to register the units this.attackManagers = [AttackMoveToLocation]; this.availableAttacks = []; this.currentAttacks = []; // Counts how many attacks we have sent at the enemy. this.attackCount = 0; this.lastAttackTime = 0; - this.defenceManager = new Defence(); + this.defenceManager = new Defence(Config); }; MilitaryAttackManager.prototype.init = function(gameState) { var civ = gameState.playerData.civ; // load units and buildings from the config files - if (civ in Config.buildings.moderate){ - this.bModerate = Config.buildings.moderate[civ]; + if (civ in this.Config.buildings.moderate){ + this.bModerate = this.Config.buildings.moderate[civ]; }else{ - this.bModerate = Config.buildings.moderate['default']; + this.bModerate = this.Config.buildings.moderate['default']; } - if (civ in Config.buildings.advanced){ - this.bAdvanced = Config.buildings.advanced[civ]; + if (civ in this.Config.buildings.advanced){ + this.bAdvanced = this.Config.buildings.advanced[civ]; }else{ - this.bAdvanced = Config.buildings.advanced['default']; + this.bAdvanced = this.Config.buildings.advanced['default']; } - if (civ in Config.buildings.fort){ - this.bFort = Config.buildings.fort[civ]; + if (civ in this.Config.buildings.fort){ + this.bFort = this.Config.buildings.fort[civ]; }else{ - this.bFort = Config.buildings.fort['default']; + this.bFort = this.Config.buildings.fort['default']; } for (var i in this.bAdvanced){ this.bAdvanced[i] = gameState.applyCiv(this.bAdvanced[i]); } for (var i in this.bFort){ this.bFort[i] = gameState.applyCiv(this.bFort[i]); } this.getEconomicTargets = function(gameState, militaryManager){ return militaryManager.getEnemyBuildings(gameState, "Economic"); }; // TODO: figure out how to make this generic for (var i in this.attackManagers){ - this.availableAttacks[i] = new this.attackManagers[i](gameState, this); + this.availableAttacks[i] = new this.attackManagers[i](gameState, this.Config, this); } var enemies = gameState.getEnemyEntities(); var filter = Filters.byClassesOr(["CitizenSoldier", "Champion", "Hero", "Siege"]); this.enemySoldiers = enemies.filter(filter); // TODO: cope with diplomacy changes this.enemySoldiers.registerUpdates(); }; /** * @param (GameState) gameState * @param (string) soldierTypes * @returns array of soldiers for which training buildings exist */ MilitaryAttackManager.prototype.findTrainableUnits = function(gameState, soldierType){ var allTrainable = []; gameState.getOwnEntities().forEach(function(ent) { var trainable = ent.trainableEntities(); for (var i in trainable){ if (allTrainable.indexOf(trainable[i]) === -1){ allTrainable.push(trainable[i]); } } }); var ret = []; for (var i in allTrainable){ var template = gameState.getTemplate(allTrainable[i]); if (soldierType == this.getSoldierType(template)){ ret.push(allTrainable[i]); } } return ret; }; // Returns the type of a soldier, either citizenSoldier, advanced or siege MilitaryAttackManager.prototype.getSoldierType = function(ent){ if (ent.hasClass("Hero")){ return undefined; } if (ent.hasClass("CitizenSoldier") && !ent.hasClass("Cavalry")){ return "citizenSoldier"; }else if (ent.hasClass("Champion") || ent.hasClass("CitizenSoldier")){ return "advanced"; }else if (ent.hasClass("Siege")){ return "siege"; }else{ return undefined; } }; /** * Returns the unit type we should begin training. (Currently this is whatever * we have least of.) */ MilitaryAttackManager.prototype.findBestNewUnit = function(gameState, queue, soldierType) { var units = this.findTrainableUnits(gameState, soldierType); // Count each type var types = []; for ( var tKey in units) { var t = units[tKey]; types.push([t, gameState.countEntitiesAndQueuedByType(gameState.applyCiv(t)) + queue.countAllByType(gameState.applyCiv(t)) ]); } // Sort by increasing count types.sort(function(a, b) { return a[1] - b[1]; }); if (types.length === 0){ return false; } return types[0][0]; }; MilitaryAttackManager.prototype.registerSoldiers = function(gameState) { var soldiers = gameState.getOwnEntitiesByRole("soldier"); var self = this; soldiers.forEach(function(ent) { ent.setMetadata("role", "military"); ent.setMetadata("military", "unassigned"); }); }; // return count of enemy buildings for a given building class MilitaryAttackManager.prototype.getEnemyBuildings = function(gameState,cls) { var targets = gameState.entities.filter(function(ent) { return (gameState.isEntityEnemy(ent) && ent.hasClass("Structure") && ent.hasClass(cls) && ent.owner() !== 0 && ent.position()); }); return targets; }; // return n available units and makes these units unavailable MilitaryAttackManager.prototype.getAvailableUnits = function(n, filter) { var ret = []; var count = 0; var units = undefined; if (filter){ units = this.getUnassignedUnits().filter(filter); }else{ units = this.getUnassignedUnits(); } units.forEach(function(ent){ ret.push(ent.id()); ent.setMetadata("military", "assigned"); ent.setMetadata("role", "military"); count++; if (count >= n) { return; } }); return ret; }; // Takes a single unit id, and marks it unassigned MilitaryAttackManager.prototype.unassignUnit = function(unit){ this.entity(unit).setMetadata("military", "unassigned"); }; // Takes an array of unit id's and marks all of them unassigned MilitaryAttackManager.prototype.unassignUnits = function(units){ for (var i in units){ this.unassignUnit(units[i]); } }; MilitaryAttackManager.prototype.getUnassignedUnits = function(){ return this.gameState.getOwnEntitiesByMetadata("military", "unassigned"); }; MilitaryAttackManager.prototype.countAvailableUnits = function(filter){ var count = 0; if (filter){ return this.getUnassignedUnits().filter(filter).length; }else{ return this.getUnassignedUnits().length; } }; // Takes an entity id and returns an entity object or undefined if there is no entity with that id // Also sends a debug message warning if the id has no entity MilitaryAttackManager.prototype.entity = function(id) { return this.gameState.getEntityById(id); }; // Returns the military strength of unit MilitaryAttackManager.prototype.getUnitStrength = function(ent){ var strength = 0.0; var attackTypes = ent.attackTypes(); var armourStrength = ent.armourStrengths(); var hp = 2 * ent.hitpoints() / (160 + 1*ent.maxHitpoints()); //100 = typical number of hitpoints for (var typeKey in attackTypes) { var type = attackTypes[typeKey]; 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]); 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; }; // Returns the strength of the available units of ai army MilitaryAttackManager.prototype.measureAvailableStrength = function(){ var strength = 0.0; var self = this; this.getUnassignedUnits(this.gameState).forEach(function(ent){ strength += self.getUnitStrength(ent); }); return strength; }; MilitaryAttackManager.prototype.getEnemySoldiers = function(){ return this.enemySoldiers; }; // Returns the number of units in the largest enemy army MilitaryAttackManager.prototype.measureEnemyCount = function(gameState){ // Measure enemy units var isEnemy = gameState.playerData.isEnemy; var enemyCount = []; var maxCount = 0; for ( var i = 1; i < isEnemy.length; i++) { enemyCount[i] = 0; } // Loop through the enemy soldiers and add one to the count for that soldiers player's count this.enemySoldiers.forEach(function(ent) { enemyCount[ent.owner()]++; if (enemyCount[ent.owner()] > maxCount) { maxCount = enemyCount[ent.owner()]; } }); return maxCount; }; // Returns the strength of the largest enemy army MilitaryAttackManager.prototype.measureEnemyStrength = function(gameState){ // Measure enemy strength var isEnemy = gameState.playerData.isEnemy; var enemyStrength = []; var maxStrength = 0; var self = this; for ( var i = 1; i < isEnemy.length; i++) { enemyStrength[i] = 0; } // Loop through the enemy soldiers and add the strength to that soldiers player's total strength this.enemySoldiers.forEach(function(ent) { enemyStrength[ent.owner()] += self.getUnitStrength(ent); if (enemyStrength[ent.owner()] > maxStrength) { maxStrength = enemyStrength[ent.owner()]; } }); return maxStrength; }; // Adds towers to the defenceBuilding queue MilitaryAttackManager.prototype.buildDefences = function(gameState, queues){ if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv('structures/{civ}_defense_tower')) + queues.defenceBuilding.totalLength() < gameState.getEntityLimits()["DefenseTower"]) { gameState.getOwnEntities().forEach(function(dropsiteEnt) { if (dropsiteEnt.resourceDropsiteTypes() && dropsiteEnt.getMetadata("defenseTower") !== true){ var position = dropsiteEnt.position(); if (position){ queues.defenceBuilding.addItem(new BuildingConstructionPlan(gameState, 'structures/{civ}_defense_tower', position)); } dropsiteEnt.setMetadata("defenseTower", true); } }); } var numFortresses = 0; for (var i in this.bFort){ numFortresses += gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bFort[i])); } if (numFortresses + queues.defenceBuilding.totalLength() < gameState.getEntityLimits()["Fortress"]) { if (gameState.countEntitiesByType(gameState.applyCiv("units/{civ}_support_female_citizen")) > gameState.ai.modules["economy"].targetNumWorkers * 0.5){ if (gameState.getTimeElapsed() > 350 * 1000 * numFortresses){ if (gameState.ai.pathsToMe && gameState.ai.pathsToMe.length > 0){ var position = gameState.ai.pathsToMe.shift(); // TODO: pick a fort randomly from the list. queues.defenceBuilding.addItem(new BuildingConstructionPlan(gameState, this.bFort[0], position)); }else{ queues.defenceBuilding.addItem(new BuildingConstructionPlan(gameState, this.bFort[0])); } } } } }; MilitaryAttackManager.prototype.constructTrainingBuildings = function(gameState, queues) { // Build more military buildings // TODO: make military building better Engine.ProfileStart("Build buildings"); if (gameState.countEntitiesByType(gameState.applyCiv("units/{civ}_support_female_citizen")) > 30) { if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bModerate[0])) + queues.militaryBuilding.totalLength() < 1) { queues.militaryBuilding.addItem(new BuildingConstructionPlan(gameState, this.bModerate[0])); } } //build advanced military buildings if (gameState.countEntitiesByType(gameState.applyCiv("units/{civ}_support_female_citizen")) > gameState.ai.modules["economy"].targetNumWorkers * 0.7){ if (queues.militaryBuilding.totalLength() === 0){ for (var i in this.bAdvanced){ if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bAdvanced[i])) < 1){ queues.militaryBuilding.addItem(new BuildingConstructionPlan(gameState, this.bAdvanced[i])); } } } } Engine.ProfileStop(); }; MilitaryAttackManager.prototype.trainMilitaryUnits = function(gameState, queues){ Engine.ProfileStart("Train Units"); // Continually try training new units, in batches of 5 if (queues.citizenSoldier.length() < 6) { var newUnit = this.findBestNewUnit(gameState, queues.citizenSoldier, "citizenSoldier"); if (newUnit){ queues.citizenSoldier.addItem(new UnitTrainingPlan(gameState, newUnit, { "role" : "soldier" }, 5)); } } if (queues.advancedSoldier.length() < 2) { var newUnit = this.findBestNewUnit(gameState, queues.advancedSoldier, "advanced"); if (newUnit){ queues.advancedSoldier.addItem(new UnitTrainingPlan(gameState, newUnit, { "role" : "soldier" }, 5)); } } if (queues.siege.length() < 4) { var newUnit = this.findBestNewUnit(gameState, queues.siege, "siege"); if (newUnit){ queues.siege.addItem(new UnitTrainingPlan(gameState, newUnit, { "role" : "soldier" }, 2)); } } Engine.ProfileStop(); }; MilitaryAttackManager.prototype.update = function(gameState, queues, events) { var self = this; Engine.ProfileStart("military update"); this.gameState = gameState; this.registerSoldiers(gameState); this.trainMilitaryUnits(gameState, queues); this.constructTrainingBuildings(gameState, queues); this.buildDefences(gameState, queues); this.defenceManager.update(gameState, events, this); Engine.ProfileStart("Plan new attacks"); // Look for attack plans which can be executed, only do this once every minute for (var i = 0; i < this.availableAttacks.length; i++){ if (this.availableAttacks[i].canExecute(gameState, this)){ this.availableAttacks[i].execute(gameState, this); this.currentAttacks.push(this.availableAttacks[i]); //debug("Attacking!"); } - this.availableAttacks.splice(i, 1, new this.attackManagers[i](gameState, this)); + this.availableAttacks.splice(i, 1, new this.attackManagers[i](gameState, this.Config, this)); } Engine.ProfileStop(); Engine.ProfileStart("Update attacks"); // Keep current attacks updated for (var i in this.currentAttacks){ this.currentAttacks[i].update(gameState, this, events); } Engine.ProfileStop(); Engine.ProfileStart("Use idle military as workers"); // Set unassigned to be workers TODO: fix this so it doesn't scan all units every time this.getUnassignedUnits(gameState).forEach(function(ent){ if (self.getSoldierType(ent) === "citizenSoldier"){ ent.setMetadata("role", "worker"); } }); Engine.ProfileStop(); Engine.ProfileStop(); }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/qbot/entity-extend.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/qbot/entity-extend.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/qbot/entity-extend.js (revision 14441) @@ -1,14 +1,14 @@ Entity.prototype.deleteMetadata = function(id) { delete this._ai._entityMetadata[this.id()]; }; Entity.prototype.garrison = function(target) { - Engine.PostCommand({"type": "garrison", "entities": [this.id()], "target": target.id(),"queued": false}); + Engine.PostCommand(PlayerID, {"type": "garrison", "entities": [this.id()], "target": target.id(),"queued": false}); return this; }; Entity.prototype.attack = function(unitId) { - Engine.PostCommand({"type": "attack", "entities": [this.id()], "target": unitId, "queued": false}); + Engine.PostCommand(PlayerID, {"type": "attack", "entities": [this.id()], "target": unitId, "queued": false}); return this; -}; \ No newline at end of file +}; Index: ps/trunk/binaries/data/mods/public/simulation/ai/qbot/worker.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/qbot/worker.js (revision 14440) +++ ps/trunk/binaries/data/mods/public/simulation/ai/qbot/worker.js (revision 14441) @@ -1,251 +1,251 @@ /** * This class makes a worker do as instructed by the economy manager */ var Worker = function(ent) { this.ent = ent; this.approachCount = 0; }; Worker.prototype.update = function(gameState) { var subrole = this.ent.getMetadata("subrole"); if (!this.ent.position()){ // If the worker has no position then no work can be done return; } if (subrole === "gatherer"){ if (!(this.ent.unitAIState().split(".")[1] === "GATHER" && this.ent.unitAIOrderData()[0].type && this.getResourceType(this.ent.unitAIOrderData()[0].type) === this.ent.getMetadata("gather-type")) && !(this.ent.unitAIState().split(".")[1] === "RETURNRESOURCE")){ // TODO: handle combat for hunting animals if (!this.ent.resourceCarrying() || this.ent.resourceCarrying().length === 0 || this.ent.resourceCarrying()[0].type === this.ent.getMetadata("gather-type")){ Engine.ProfileStart("Start Gathering"); this.startGathering(gameState); Engine.ProfileStop(); } else if (this.ent.unitAIState().split(".")[1] !== "RETURNRESOURCE") { // Should deposit resources Engine.ProfileStart("Return Resources"); this.returnResources(gameState); Engine.ProfileStop(); } this.startApproachingResourceTime = gameState.getTimeElapsed(); - //Engine.PostCommand({"type": "set-shading-color", "entities": [this.ent.id()], "rgb": [10,0,0]}); + //Engine.PostCommand(PlayerID, {"type": "set-shading-color", "entities": [this.ent.id()], "rgb": [10,0,0]}); }else{ // If we haven't reached the resource in 2 minutes twice in a row and none of the resource has been // gathered then mark it as inaccessible. if (gameState.getTimeElapsed() - this.startApproachingResourceTime > 120000){ if (this.gatheringFrom){ var ent = gameState.getEntityById(this.gatheringFrom); if (ent && ent.resourceSupplyAmount() == ent.resourceSupplyMax()){ if (this.approachCount > 0){ ent.setMetadata("inaccessible", true); this.ent.setMetadata("subrole", "idle"); } this.approachCount++; }else{ this.approachCount = 0; } this.startApproachingResourceTime = gameState.getTimeElapsed(); } } } }else if(subrole === "builder"){ if (this.ent.unitAIState().split(".")[1] !== "REPAIR"){ var target = this.ent.getMetadata("target-foundation"); this.ent.repair(target); } - //Engine.PostCommand({"type": "set-shading-color", "entities": [this.ent.id()], "rgb": [0,10,0]}); + //Engine.PostCommand(PlayerID, {"type": "set-shading-color", "entities": [this.ent.id()], "rgb": [0,10,0]}); } Engine.ProfileStart("Update Gatherer Counts"); this.updateGathererCounts(gameState); Engine.ProfileStop(); }; Worker.prototype.updateGathererCounts = function(gameState, dead){ // update gatherer counts for the resources if (this.ent.unitAIState().split(".")[2] === "GATHERING" && !dead){ if (this.gatheringFrom !== this.ent.unitAIOrderData()[0].target){ if (this.gatheringFrom){ var ent = gameState.getEntityById(this.gatheringFrom); if (ent){ ent.setMetadata("gatherer-count", ent.getMetadata("gatherer-count") - 1); this.markFull(ent); } } this.gatheringFrom = this.ent.unitAIOrderData()[0].target; if (this.gatheringFrom){ var ent = gameState.getEntityById(this.gatheringFrom); if (ent){ ent.setMetadata("gatherer-count", (ent.getMetadata("gatherer-count") || 0) + 1); this.markFull(ent); } } } }else{ if (this.gatheringFrom){ var ent = gameState.getEntityById(this.gatheringFrom); if (ent){ ent.setMetadata("gatherer-count", ent.getMetadata("gatherer-count") - 1); this.markFull(ent); } this.gatheringFrom = undefined; } } }; Worker.prototype.markFull = function(ent){ var maxCounts = {"food": 20, "wood": 5, "metal": 20, "stone": 20, "treasure": 1}; if (ent.resourceSupplyType() && ent.getMetadata("gatherer-count") >= maxCounts[ent.resourceSupplyType().generic]){ if (!ent.getMetadata("full")){ ent.setMetadata("full", true); } }else{ if (ent.getMetadata("full")){ ent.setMetadata("full", false); } } }; Worker.prototype.startGathering = function(gameState){ var resource = this.ent.getMetadata("gather-type"); var ent = this.ent; if (!ent.position()){ // TODO: work out what to do when entity has no position return; } // find closest dropsite which has nearby resources of the correct type var minDropsiteDist = Math.min(); // set to infinity initially var nearestResources = undefined; var nearestDropsite = undefined; gameState.updatingCollection("active-dropsite-" + resource, Filters.byMetadata("active-dropsite-" + resource, true), gameState.getOwnDropsites(resource)).forEach(function (dropsite){ if (dropsite.position()){ var dist = VectorDistance(ent.position(), dropsite.position()); if (dist < minDropsiteDist){ minDropsiteDist = dist; nearestResources = dropsite.getMetadata("nearby-resources-" + resource); nearestDropsite = dropsite; } } }); if (!nearestResources || nearestResources.length === 0){ nearestResources = gameState.getResourceSupplies(resource); gameState.getOwnDropsites(resource).forEach(function (dropsite){ if (dropsite.position()){ var dist = VectorDistance(ent.position(), dropsite.position()); if (dist < minDropsiteDist){ minDropsiteDist = dist; nearestDropsite = dropsite; } } }); } if (nearestResources.length === 0){ debug("No " + resource + " found! (1)"); return; } var supplies = []; var nearestSupplyDist = Math.min(); var nearestSupply = undefined; nearestResources.forEach(function(supply) { // TODO: handle enemy territories if (!supply.position()){ return; } if (supply.isFull() === true) { return; } // measure the distance to the resource var dist = VectorDistance(supply.position(), ent.position()); // Add on a factor for the nearest dropsite if one exists if (nearestDropsite){ dist += 5 * VectorDistance(supply.position(), nearestDropsite.position()); } // Go for treasure as a priority if (dist < 1000 && supply.resourceSupplyType().generic == "treasure"){ dist /= 1000; } if (dist < nearestSupplyDist){ nearestSupplyDist = dist; nearestSupply = supply; } }); if (nearestSupply) { var pos = nearestSupply.position(); var territoryOwner = gameState.getTerritoryMap().getOwner(pos); if (!gameState.ai.accessibility.isAccessible(pos) || (territoryOwner != gameState.getPlayerID() && territoryOwner != 0)){ nearestSupply.setMetadata("inaccessible", true); }else{ ent.gather(nearestSupply); } }else{ debug("No " + resource + " found! (2)"); } }; // Makes the worker deposit the currently carried resources at the closest dropsite Worker.prototype.returnResources = function(gameState){ if (!this.ent.resourceCarrying() || this.ent.resourceCarrying().length === 0){ return; } var resource = this.ent.resourceCarrying()[0].type; var self = this; if (!this.ent.position()){ // TODO: work out what to do when entity has no position return; } var closestDropsite = undefined; var dist = Math.min(); gameState.getOwnDropsites(resource).forEach(function(dropsite){ if (dropsite.position()){ var d = VectorDistance(self.ent.position(), dropsite.position()); if (d < dist){ dist = d; closestDropsite = dropsite; } } }); if (!closestDropsite){ debug("No dropsite found for " + resource); return; } this.ent.returnResources(closestDropsite); }; Worker.prototype.getResourceType = function(type){ if (!type || !type.generic){ return undefined; } if (type.generic === "treasure"){ return type.specific; }else{ return type.generic; } -}; \ No newline at end of file +};