Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js (revision 20408) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js (revision 20409) @@ -1,1008 +1,1007 @@ var API3 = function(m) { // defines a template. // It's completely raw data, except it's slightly cleverer now and then. m.Template = m.Class({ _init: function(sharedAI, templateName, template) { this._templateName = templateName; this._template = template; // save a reference to the template tech modifications if (!sharedAI._templatesModifications[this._templateName]) sharedAI._templatesModifications[this._templateName] = {}; this._templateModif = sharedAI._templatesModifications[this._templateName]; this._tpCache = new Map(); }, // helper function to return a template value, optionally adjusting for tech. // TODO: there's no support for "_string" values here. get: function(string) { let value = this._template; if (this._entityModif && this._entityModif.has(string)) return this._entityModif.get(string); else if (this._templateModif) { let owner = this._entity ? this._entity.owner : PlayerID; if (this._templateModif[owner] && this._templateModif[owner].has(string)) return this._templateModif[owner].get(string); } if (!this._tpCache.has(string)) { let args = string.split("/"); for (let arg of args) { if (value[arg]) value = value[arg]; else { value = undefined; break; } } this._tpCache.set(string, value); } return this._tpCache.get(string); }, genericName: function() { return this.get("Identity/GenericName"); }, rank: function() { return this.get("Identity/Rank"); }, classes: function() { let template = this.get("Identity"); if (!template) return undefined; return GetIdentityClasses(template); }, requiredTech: function() { return this.get("Identity/RequiredTechnology"); }, available: function(gameState) { let techRequired = this.requiredTech(); if (!techRequired) return true; return gameState.isResearched(techRequired); }, // specifically phase: function() { let techRequired = this.requiredTech(); if (!techRequired) return 0; if (techRequired === "phase_village") return 1; if (techRequired === "phase_town") return 2; if (techRequired === "phase_city") return 3; if (techRequired.startsWith("phase_")) return 4; return 0; }, hasClass: function(name) { if (!this._classes) this._classes = this.classes(); let classes = this._classes; return classes && classes.indexOf(name) !== -1; }, hasClasses: function(array) { if (!this._classes) this._classes = this.classes(); let classes = this._classes; if (!classes) return false; for (let cls of array) if (classes.indexOf(cls) === -1) return false; return true; }, civ: function() { return this.get("Identity/Civ"); }, "cost": function(productionQueue) { if (!this.get("Cost")) return undefined; let ret = {}; for (let type in this.get("Cost/Resources")) ret[type] = +this.get("Cost/Resources/" + type); return ret; }, "costSum": function(productionQueue) { let cost = this.cost(productionQueue); if (!cost) return undefined; let ret = 0; for (let type in cost) ret += cost[type]; return ret; }, "techCostMultiplier": function(type) { return +(this.get("ProductionQueue/TechCostMultiplier/"+type) || 1); }, /** * Returns { "max": max, "min": min } or undefined if no obstruction. * max: radius of the outer circle surrounding this entity's obstruction shape * min: radius of the inner circle */ "obstructionRadius": function() { if (!this.get("Obstruction")) return undefined; if (this.get("Obstruction/Static")) { let w = +this.get("Obstruction/Static/@width"); let h = +this.get("Obstruction/Static/@depth"); return { "max": Math.sqrt(w*w + h*h) / 2, "min": Math.min(h, w) / 2 }; } if (this.get("Obstruction/Unit")) { let r = +this.get("Obstruction/Unit/@radius"); return { "max": r, "min": r }; } let right = this.get("Obstruction/Obstructions/Right"); let left = this.get("Obstruction/Obstructions/Left"); if (left && right) { let w = +right["@x"] + +right["@width"]/2 - +left["@x"] + +left["@width"]/2; let h = Math.max(+right["@z"] + +right["@depth"]/2, +left["@z"] + +left["@depth"]/2) - Math.min(+right["@z"] - +right["@depth"]/2, +left["@z"] - +left["@depth"]/2); return { "max": Math.sqrt(w*w + h*h) / 2, "min": Math.min(h, w) / 2 }; } return { "max": 0, "min": 0 }; // Units have currently no obstructions }, /** * Returns the radius of a circle surrounding this entity's footprint. */ "footprintRadius": function() { if (!this.get("Footprint")) return undefined; if (this.get("Footprint/Square")) { let w = +this.get("Footprint/Square/@width"); let h = +this.get("Footprint/Square/@depth"); return Math.sqrt(w*w + h*h) / 2; } if (this.get("Footprint/Circle")) return +this.get("Footprint/Circle/@radius"); return 0; // this should never happen }, maxHitpoints: function() { return +(this.get("Health/Max") || 0); }, isHealable: function() { if (this.get("Health") !== undefined) return this.get("Health/Unhealable") !== "true"; return false; }, isRepairable: function() { return this.get("Repairable") !== undefined; }, getPopulationBonus: function() { return +this.get("Cost/PopulationBonus"); }, "armourStrengths": function() { if (!this.get("Armour")) return undefined; return { "Hack": +this.get("Armour/Hack"), "Pierce": +this.get("Armour/Pierce"), "Crush": +this.get("Armour/Crush") }; }, attackTypes: function() { if (!this.get("Attack")) return undefined; let ret = []; for (let type in this.get("Attack")) ret.push(type); return ret; }, attackRange: function(type) { if (!this.get("Attack/" + type +"")) return undefined; return { max: +this.get("Attack/" + type +"/MaxRange"), min: +(this.get("Attack/" + type +"/MinRange") || 0) }; }, "attackStrengths": function(type) { if (!this.get("Attack/" + type +"")) return undefined; return { "Hack": +(this.get("Attack/" + type + "/Hack") || 0), "Pierce": +(this.get("Attack/" + type + "/Pierce") || 0), "Crush": +(this.get("Attack/" + type + "/Crush") || 0) }; }, captureStrength: function() { if (!this.get("Attack/Capture")) return undefined; return +this.get("Attack/Capture/Value") || 0; }, attackTimes: function(type) { if (!this.get("Attack/" + type +"")) return undefined; return { prepare: +(this.get("Attack/" + type + "/PrepareTime") || 0), repeat: +(this.get("Attack/" + type + "/RepeatTime") || 1000) }; }, // returns the classes this templates counters: // Return type is [ [-neededClasses- , multiplier], … ]. getCounteredClasses: function() { if (!this.get("Attack")) return undefined; let Classes = []; for (let type in this.get("Attack")) { let bonuses = this.get("Attack/" + type + "/Bonuses"); if (!bonuses) continue; for (let b in bonuses) { let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes"); if (bonusClasses) Classes.push([bonusClasses.split(" "), +this.get("Attack/" + type +"/Bonuses" + b +"/Multiplier")]); } } return Classes; }, // returns true if the entity counters those classes. // TODO: refine using the multiplier countersClasses: function(classes) { if (!this.get("Attack")) return false; let mcounter = []; for (let type in this.get("Attack")) { let bonuses = this.get("Attack/" + type + "/Bonuses"); if (!bonuses) continue; for (let b in bonuses) { let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes"); if (bonusClasses) mcounter.concat(bonusClasses.split(" ")); } } for (let 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.get("Attack/" + type +"")) return undefined; if (this.get("Attack/" + type + "/Bonuses")) { for (let b in this.get("Attack/" + type + "/Bonuses")) { let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes"); if (!bonusClasses) continue; for (let bcl of bonusClasses.split(" ")) if (bcl === againstClass) return +this.get("Attack/" + type + "/Bonuses/" + b + "/Multiplier"); } } return 1; }, - "buildableEntities": function() { + "buildableEntities": function(civ = this.civ()) { let templates = this.get("Builder/Entities/_string"); if (!templates) return []; - let civ = this.civ(); return templates.replace(/\{civ\}/g, civ).split(/\s+/); }, "trainableEntities": function(civ) { let templates = this.get("ProductionQueue/Entities/_string"); if (!templates) return undefined; if (civ) templates = templates.replace(/\{civ\}/g, civ); return templates.split(/\s+/); }, "researchableTechs": function(civ) { let templates = this.get("ProductionQueue/Technologies/_string"); if (!templates) return undefined; if (civ) templates = templates.replace(/\{civ\}/g, civ); return templates.split(/\s+/); }, "resourceSupplyType": function() { if (!this.get("ResourceSupply")) return undefined; let [type, subtype] = this.get("ResourceSupply/Type").split('.'); return { "generic": type, "specific": subtype }; }, // will return either "food", "wood", "stone", "metal" and not treasure. "getResourceType": function() { if (!this.get("ResourceSupply")) return undefined; let [type, subtype] = this.get("ResourceSupply/Type").split('.'); if (type == "treasure") return subtype; return type; }, "getDiminishingReturns": function() { return +(this.get("ResourceSupply/DiminishingReturns") || 1); }, "resourceSupplyMax": function() { return +this.get("ResourceSupply/Amount"); }, "maxGatherers": function() { return +(this.get("ResourceSupply/MaxGatherers") || 0); }, resourceGatherRates: function() { if (!this.get("ResourceGatherer")) return undefined; let ret = {}; let baseSpeed = +this.get("ResourceGatherer/BaseSpeed"); for (let r in this.get("ResourceGatherer/Rates")) ret[r] = +this.get("ResourceGatherer/Rates/" + r) * baseSpeed; return ret; }, resourceDropsiteTypes: function() { if (!this.get("ResourceDropsite")) return undefined; let types = this.get("ResourceDropsite/Types"); return types ? types.split(/\s+/) : []; }, garrisonableClasses: function() { return this.get("GarrisonHolder/List/_string"); }, garrisonMax: function() { return this.get("GarrisonHolder/Max"); }, garrisonEjectHealth: function() { return +this.get("GarrisonHolder/EjectHealth"); }, getDefaultArrow: function() { return +this.get("BuildingAI/DefaultArrowCount"); }, getArrowMultiplier: function() { return +this.get("BuildingAI/GarrisonArrowMultiplier"); }, getGarrisonArrowClasses: function() { if (!this.get("BuildingAI")) return undefined; return this.get("BuildingAI/GarrisonArrowClasses").split(/\s+/); }, buffHeal: function() { return +this.get("GarrisonHolder/BuffHeal"); }, promotion: function() { return this.get("Promotion/Entity"); }, /** * Returns whether this is an animal that is too difficult to hunt. * (Any non domestic currently.) */ isHuntable: function() { if(!this.get("ResourceSupply/KillBeforeGather")) return false; // special case: rabbits too difficult to hunt for such a small food amount let specificName = this.get("Identity/SpecificName"); if (specificName && specificName === "Rabbit") return false; // do not hunt retaliating animals (animals without UnitAI are dead animals) let behaviour = this.get("UnitAI/NaturalBehaviour"); return !this.get("UnitAI") || !(behaviour === "violent" || behaviour === "aggressive" || behaviour === "defensive"); }, walkSpeed: function() { return +this.get("UnitMotion/WalkSpeed"); }, trainingCategory: function() { return this.get("TrainingRestrictions/Category"); }, buildCategory: function() { return this.get("BuildRestrictions/Category"); }, "buildTime": function(productionQueue) { let time = +this.get("Cost/BuildTime"); if (productionQueue) time *= productionQueue.techCostMultiplier("time"); return time; }, "buildDistance": function() { let distance = this.get("BuildRestrictions/Distance"); if (!distance) return undefined; let ret = {}; for (let key in distance) ret[key] = this.get("BuildRestrictions/Distance/" + key); return ret; }, buildPlacementType: function() { return this.get("BuildRestrictions/PlacementType"); }, buildTerritories: function() { if (!this.get("BuildRestrictions") || !this.get("BuildRestrictions/Territory")) return undefined; return this.get("BuildRestrictions/Territory").split(/\s+/); }, hasBuildTerritory: function(territory) { let territories = this.buildTerritories(); return territories && territories.indexOf(territory) !== -1; }, hasTerritoryInfluence: function() { return this.get("TerritoryInfluence") !== undefined; }, hasDefensiveFire: function() { if (!this.get("Attack/Ranged")) return false; return this.getDefaultArrow() || this.getArrowMultiplier(); }, territoryInfluenceRadius: function() { if (this.get("TerritoryInfluence") !== undefined) return +this.get("TerritoryInfluence/Radius"); return -1; }, territoryInfluenceWeight: function() { if (this.get("TerritoryInfluence") !== undefined) return +this.get("TerritoryInfluence/Weight"); return -1; }, territoryDecayRate: function() { return +(this.get("TerritoryDecay/DecayRate") || 0); }, defaultRegenRate: function() { return +(this.get("Capturable/RegenRate") || 0); }, garrisonRegenRate: function() { return +(this.get("Capturable/GarrisonRegenRate") || 0); }, visionRange: function() { return +this.get("Vision/Range"); }, gainMultiplier: function() { return +this.get("Trader/GainMultiplier"); } }); // defines an entity, with a super Template. // also redefines several of the template functions where the only change is applying aura and tech modifications. m.Entity = m.Class({ _super: m.Template, _init: function(sharedAI, entity) { this._super.call(this, sharedAI, entity.template, sharedAI.GetTemplate(entity.template)); this._entity = entity; this._ai = sharedAI; // save a reference to the template tech modifications if (!sharedAI._templatesModifications[this._templateName]) sharedAI._templatesModifications[this._templateName] = {}; this._templateModif = sharedAI._templatesModifications[this._templateName]; // save a reference to the entity tech/aura modifications if (!sharedAI._entitiesModifications.has(entity.id)) sharedAI._entitiesModifications.set(entity.id, new Map()); this._entityModif = sharedAI._entitiesModifications.get(entity.id); }, 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 should not be 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; }, angle: function() { return this._entity.angle; }, isIdle: function() { if (typeof this._entity.idle === "undefined") return undefined; return this._entity.idle; }, "getStance": function() { return this._entity.stance !== undefined ? this._entity.stance : undefined; }, unitAIState: function() { return this._entity.unitAIState !== undefined ? this._entity.unitAIState : undefined; }, unitAIOrderData: function() { return this._entity.unitAIOrderData !== undefined ? this._entity.unitAIOrderData : undefined; }, hitpoints: function() { return this._entity.hitpoints !== undefined ? this._entity.hitpoints : 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(); }, decaying: function() { return this._entity.decaying !== undefined ? this._entity.decaying : undefined; }, capturePoints: function() {return this._entity.capturePoints !== undefined ? this._entity.capturePoints : undefined; }, "isSharedDropsite": function() { return this._entity.sharedDropsite === true; }, /** * Returns the current training queue state, of the form * [ { "id": 0, "template": "...", "count": 1, "progress": 0.5, "metadata": ... }, ... ] */ trainingQueue: function() { let queue = this._entity.trainingQueue; return queue; }, trainingQueueTime: function() { let queue = this._entity.trainingQueue; if (!queue) return undefined; let time = 0; for (let item of queue) time += item.timeRemaining; return time/1000; }, foundationProgress: function() { if (this._entity.foundationProgress === undefined) return undefined; return this._entity.foundationProgress; }, getBuilders: function() { if (this._entity.foundationProgress === undefined) return undefined; if (this._entity.foundationBuilders === undefined) return []; return this._entity.foundationBuilders; }, getBuildersNb: function() { if (this._entity.foundationProgress === undefined) return undefined; if (this._entity.foundationBuilders === undefined) return 0; return this._entity.foundationBuilders.length; }, owner: function() { return this._entity.owner; }, isOwn: function(player) { if (typeof this._entity.owner === "undefined") return false; return this._entity.owner === player; }, resourceSupplyAmount: function() { if (this._entity.resourceSupplyAmount === undefined) return undefined; return this._entity.resourceSupplyAmount; }, resourceSupplyNumGatherers: function() { if (this._entity.resourceSupplyNumGatherers !== undefined) return this._entity.resourceSupplyNumGatherers; return undefined; }, isFull: function() { if (this._entity.resourceSupplyNumGatherers !== undefined) return this.maxGatherers() === this._entity.resourceSupplyNumGatherers; return undefined; }, resourceCarrying: function() { if (this._entity.resourceCarrying === undefined) return undefined; return this._entity.resourceCarrying; }, currentGatherRate: function() { // returns the gather rate for the current target if applicable. if (!this.get("ResourceGatherer")) return undefined; if (this.unitAIOrderData().length && (this.unitAIState().split(".")[1] === "GATHER" || this.unitAIState().split(".")[1] === "RETURNRESOURCE")) { let res; // this is an abuse of "_ai" but it works. if (this.unitAIState().split(".")[1] === "GATHER" && this.unitAIOrderData()[0].target !== undefined) res = this._ai._entities.get(this.unitAIOrderData()[0].target); else if (this.unitAIOrderData()[1] !== undefined && this.unitAIOrderData()[1].target !== undefined) res = this._ai._entities.get(this.unitAIOrderData()[1].target); if (!res) return 0; let type = res.resourceSupplyType(); if (!type) return 0; if (type.generic === "treasure") return 1000; let tstring = type.generic + "." + type.specific; let rate = +this.get("ResourceGatherer/BaseSpeed"); rate *= +this.get("ResourceGatherer/Rates/" +tstring); if (rate) return rate; return 0; } return undefined; }, isBuilder: function() { return this.get("Builder") !== undefined; }, isGatherer: function() { return this.get("ResourceGatherer") !== undefined; }, canGather: function(type) { let gatherRates = this.get("ResourceGatherer/Rates"); if (!gatherRates) return false; for (let r in gatherRates) if (r.split('.')[0] === type) return true; return false; }, isGarrisonHolder: function() { return this.get("GarrisonHolder") !== undefined; }, garrisoned: function() { return this._entity.garrisoned; }, canGarrisonInside: function() { return this._entity.garrisoned.length < this.garrisonMax(); }, /** * returns true if the entity can attack (including capture) the given class. */ "canAttackClass": function(aClass) { if (!this.get("Attack")) return false; for (let type in this.get("Attack")) { if (type === "Slaughter") continue; let restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string"); if (!restrictedClasses || !MatchesClassList([aClass], restrictedClasses)) return true; } return false; }, /** * returns true if the entity can capture the given target entity * if no target is given, returns true if the entity has the Capture attack */ "canCapture": function(target) { if (!this.get("Attack/Capture")) return false; if (!target) return true; if (!target.get("Capturable")) return false; let restrictedClasses = this.get("Attack/Capture/RestrictedClasses/_string"); return !restrictedClasses || !MatchesClassList(target.classes(), restrictedClasses); }, "isCapturable": function() { return this.get("Capturable") !== undefined; }, "canGuard": function() { return this.get("UnitAI/CanGuard") === "true"; }, "canGarrison": function() { return "Garrisonable" in this._template; }, move: function(x, z, queued = false) { Engine.PostCommand(PlayerID,{"type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": queued }); return this; }, moveToRange: function(x, z, min, max, queued = false) { Engine.PostCommand(PlayerID,{"type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued }); return this; }, attackMove: function(x, z, targetClasses, queued = false) { Engine.PostCommand(PlayerID,{"type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "queued": queued }); return this; }, // violent, aggressive, defensive, passive, standground setStance: function(stance, queued = false) { if (this.getStance() === undefined) return undefined; Engine.PostCommand(PlayerID,{"type": "stance", "entities": [this.id()], "name" : stance, "queued": queued }); return this; }, stopMoving: function() { Engine.PostCommand(PlayerID,{"type": "stop", "entities": [this.id()], "queued": false}); }, unload: function(id) { if (!this.get("GarrisonHolder")) return undefined; Engine.PostCommand(PlayerID,{"type": "unload", "garrisonHolder": this.id(), "entities": [id]}); return this; }, // Unloads all owned units, don't unload allies unloadAll: function() { if (!this.get("GarrisonHolder")) return undefined; Engine.PostCommand(PlayerID,{"type": "unload-all-by-owner", "garrisonHolders": [this.id()]}); return this; }, garrison: function(target, queued = false) { Engine.PostCommand(PlayerID,{"type": "garrison", "entities": [this.id()], "target": target.id(),"queued": queued}); return this; }, attack: function(unitId, allowCapture = true, queued = false) { Engine.PostCommand(PlayerID,{"type": "attack", "entities": [this.id()], "target": unitId, "allowCapture": allowCapture, "queued": queued}); return this; }, // moveApart from a point in the opposite direction with a distance dist moveApart: function(point, dist) { if (this.position() !== undefined) { let direction = [this.position()[0] - point[0], this.position()[1] - point[1]]; let norm = m.VectorDistance(point, this.position()); if (norm === 0) direction = [1, 0]; else { direction[0] /= norm; direction[1] /= norm; } Engine.PostCommand(PlayerID,{"type": "walk", "entities": [this.id()], "x": this.position()[0] + direction[0]*dist, "z": this.position()[1] + direction[1]*dist, "queued": false}); } return this; }, // Flees from a unit in the opposite direction. flee: function(unitToFleeFrom) { if (this.position() !== undefined && unitToFleeFrom.position() !== undefined) { let FleeDirection = [this.position()[0] - unitToFleeFrom.position()[0], this.position()[1] - unitToFleeFrom.position()[1]]; let dist = m.VectorDistance(unitToFleeFrom.position(), this.position() ); FleeDirection[0] = 40 * FleeDirection[0]/dist; FleeDirection[1] = 40 * FleeDirection[1]/dist; Engine.PostCommand(PlayerID,{"type": "walk", "entities": [this.id()], "x": this.position()[0] + FleeDirection[0], "z": this.position()[1] + FleeDirection[1], "queued": false}); } return this; }, gather: function(target, queued = false) { Engine.PostCommand(PlayerID,{"type": "gather", "entities": [this.id()], "target": target.id(), "queued": queued}); return this; }, repair: function(target, autocontinue = false, queued = false) { Engine.PostCommand(PlayerID,{"type": "repair", "entities": [this.id()], "target": target.id(), "autocontinue": autocontinue, "queued": queued}); return this; }, returnResources: function(target, queued = false) { Engine.PostCommand(PlayerID,{"type": "returnresource", "entities": [this.id()], "target": target.id(), "queued": queued}); return this; }, destroy: function() { Engine.PostCommand(PlayerID,{"type": "delete-entities", "entities": [this.id()] }); return this; }, barter: function(buyType, sellType, amount) { Engine.PostCommand(PlayerID,{"type": "barter", "sell" : sellType, "buy" : buyType, "amount" : amount }); return this; }, tradeRoute: function(target, source) { Engine.PostCommand(PlayerID,{"type": "setup-trade-route", "entities": [this.id()], "target": target.id(), "source": source.id(), "route": undefined, "queued": false }); return this; }, setRallyPoint: function(target, command) { let data = {"command": command, "target": target.id()}; Engine.PostCommand(PlayerID, {"type": "set-rallypoint", "entities": [this.id()], "x": target.position()[0], "z": target.position()[1], "data": data}); return this; }, unsetRallyPoint: function() { Engine.PostCommand(PlayerID, {"type": "unset-rallypoint", "entities": [this.id()]}); return this; }, train: function(civ, type, count, metadata, promotedTypes) { let trainable = this.trainableEntities(civ); 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(PlayerID,{ "type": "train", "entities": [this.id()], "template": type, "count": count, "metadata": metadata, "promoted": promotedTypes }); 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(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(PlayerID,{ "type": "research", "entity": this.id(), "template": template }); return this; }, stopProduction: function(id) { Engine.PostCommand(PlayerID,{ "type": "stop-production", "entity": this.id(), "id": id }); return this; }, stopAllProduction: function(percentToStopAt) { let queue = this._entity.trainingQueue; if (!queue) return true; // no queue, so technically we stopped all production. for (let item of queue) if (item.progress < percentToStopAt) Engine.PostCommand(PlayerID,{ "type": "stop-production", "entity": this.id(), "id": item.id }); return this; }, "guard": function(target, queued = false) { Engine.PostCommand(PlayerID, { "type": "guard", "entities": [this.id()], "target": target.id(), "queued": queued }); return this; }, "removeGuard": function() { Engine.PostCommand(PlayerID, { "type": "remove-guard", "entities": [this.id()] }); return this; } }); return m; }(API3); Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/gamestate.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/gamestate.js (revision 20408) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/gamestate.js (revision 20409) @@ -1,951 +1,924 @@ 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. */ m.GameState = function() { this.ai = null; // must be updated by the AIs. }; m.GameState.prototype.init = function(SharedScript, state, player) { this.sharedScript = SharedScript; this.EntCollecNames = SharedScript._entityCollectionsName; this.timeElapsed = SharedScript.timeElapsed; this.circularMap = SharedScript.circularMap; this.templates = SharedScript._templates; this.techTemplates = SharedScript._techTemplates; this.entities = SharedScript.entities; this.player = player; this.playerData = SharedScript.playersData[this.player]; this.gameType = SharedScript.gameType; this.alliedVictory = SharedScript.alliedVictory; this.ceasefireActive = SharedScript.ceasefireActive; // get the list of possible phases for this civ: // we assume all of them are researchable from the civil centre this.phases = [{ name: "phase_village" }, { name: "phase_town" }, { name: "phase_city" }]; let cctemplate = this.getTemplate(this.applyCiv("structures/{civ}_civil_centre")); if (!cctemplate) return; let civ = this.getPlayerCiv(); let techs = cctemplate.researchableTechs(civ); for (let phase of this.phases) { phase.requirements = []; let k = techs.indexOf(phase.name); if (k !== -1) { let reqs = DeriveTechnologyRequirements(this.getTemplate(techs[k])._template, civ); if (reqs) { phase.requirements = reqs; continue; } } for (let tech of techs) { let template = this.getTemplate(tech)._template; if (template.replaces && template.replaces.indexOf(phase.name) != -1) { let reqs = DeriveTechnologyRequirements(template, civ); if (reqs) { phase.name = tech; phase.requirements = reqs; break; } } } } // Then check if this mod has an additionnal phase for (let tech of techs) { let template = this.getTemplate(tech)._template; if (!template.supersedes || template.supersedes != this.phases[2].name) continue; let reqs = DeriveTechnologyRequirements(template, civ); if (reqs) this.phases.push({ "name": tech, "requirements": reqs }); break; } }; m.GameState.prototype.update = function(SharedScript) { this.timeElapsed = SharedScript.timeElapsed; this.playerData = SharedScript.playersData[this.player]; this.ceasefireActive = SharedScript.ceasefireActive; }; m.GameState.prototype.updatingCollection = function(id, filter, parentCollection) { let gid = "player-" + this.player + "-" + id; // automatically add the player ID return this.updatingGlobalCollection(gid, filter, parentCollection); }; m.GameState.prototype.destroyCollection = function(id) { let gid = "player-" + this.player + "-" + id; // automatically add the player ID this.destroyGlobalCollection(gid); }; m.GameState.prototype.updatingGlobalCollection = function(gid, filter, parentCollection) { if (this.EntCollecNames.has(gid)) return this.EntCollecNames.get(gid); let collection = parentCollection ? parentCollection.filter(filter) : this.entities.filter(filter); collection.registerUpdates(); this.EntCollecNames.set(gid, collection); return collection; }; m.GameState.prototype.destroyGlobalCollection = function(gid) { if (!this.EntCollecNames.has(gid)) return; this.sharedScript.removeUpdatingEntityCollection(this.EntCollecNames.get(gid)); this.EntCollecNames.delete(gid); }; /** * Reset the entities collections which depend on diplomacy */ m.GameState.prototype.resetOnDiplomacyChanged = function() { for (let name of this.EntCollecNames.keys()) if (name.startsWith("player-" + this.player + "-diplo")) this.destroyGlobalCollection(name); }; m.GameState.prototype.getTimeElapsed = function() { return this.timeElapsed; }; m.GameState.prototype.getBarterPrices = function() { return this.playerData.barterPrices; }; m.GameState.prototype.getGameType = function() { return this.gameType; }; m.GameState.prototype.getAlliedVictory = function() { return this.alliedVictory; }; m.GameState.prototype.isCeasefireActive = function() { return this.ceasefireActive; }; m.GameState.prototype.getTemplate = function(type) { if (this.techTemplates[type] !== undefined) return new m.Technology(this.techTemplates, type); if (!this.templates[type]) return null; return new m.Template(this.sharedScript, type, this.templates[type]); }; /** Return the template of the structure built from this foundation */ m.GameState.prototype.getBuiltTemplate = function(foundationName) { if (!foundationName.startsWith("foundation|")) { warn("Foundation " + foundationName + " not recognised as a foundation."); return null; } return this.getTemplate(foundationName.substr(11)); }; m.GameState.prototype.applyCiv = function(str) { return str.replace(/\{civ\}/g, this.playerData.civ); }; m.GameState.prototype.getPlayerCiv = function(player) { return player !== undefined ? this.sharedScript.playersData[player].civ : this.playerData.civ; }; m.GameState.prototype.currentPhase = function() { for (let i = this.phases.length; i > 0; --i) if (this.isResearched(this.phases[i-1].name)) return i; return 0; }; m.GameState.prototype.getNumberOfPhases = function() { return this.phases.length; }; m.GameState.prototype.getPhaseName = function(i) { return this.phases[i-1] ? this.phases[i-1].name : undefined; }; m.GameState.prototype.getPhaseEntityRequirements = function(i) { let entityReqs = []; for (let requirement of this.phases[i-1].requirements) { if (!requirement.entities) continue; for (let entity of requirement.entities) if (entity.check == "count") entityReqs.push({ "class": entity.class, "count": entity.number }); } return entityReqs; }; m.GameState.prototype.isResearched = function(template) { return this.playerData.researchedTechs[template] !== undefined; }; /** true if started or queued */ m.GameState.prototype.isResearching = function(template) { return this.playerData.researchStarted[template] !== undefined || this.playerData.researchQueued[template] !== undefined; }; /** this is an "in-absolute" check that doesn't check if we have a building to research from. */ m.GameState.prototype.canResearch = function(techTemplateName, noRequirementCheck) { if (this.playerData.disabledTechnologies[techTemplateName]) return false; let 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) return true; // if this is a pair, we must check that the pair tech is not being researched if (template.pair()) { let other = template.pairedWith(); if (this.playerData.researchQueued[other] || this.playerData.researchStarted[other] || this.playerData.researchedTechs[other]) return false; } return this.checkTechRequirements(template.requirements(this.playerData.civ)); }; /** * Private function for checking a set of requirements is met. * Basically copies TechnologyManager, but compares against * variables only available within the AI */ m.GameState.prototype.checkTechRequirements = function(reqs) { if (!reqs) return false; if (!reqs.length) return true; function doesEntitySpecPass(entity) { switch (entity.check) { case "count": if (!this.playerData.classCounts[entity.class] || this.playerData.classCounts[entity.class] < entity.number) return false; break; case "variants": if (!this.playerData.typeCountsByClass[entity.class] || Object.keys(this.playerData.typeCountsByClass[entity.class]).length < entity.number) return false; break; } return true; } return reqs.some(req => { return Object.keys(req).every(type => { switch (type) { case "techs": return req[type].every(tech => !!this.playerData.researchedTechs[tech]); case "entities": return req[type].every(doesEntitySpecPass, this); } return false; }); }); }; m.GameState.prototype.getPassabilityMap = function() { return this.sharedScript.passabilityMap; }; m.GameState.prototype.getPassabilityClassMask = function(name) { if (!this.sharedScript.passabilityClasses[name]) error("Tried to use invalid passability class name '" + name + "'"); return this.sharedScript.passabilityClasses[name]; }; m.GameState.prototype.getResources = function() { return new m.Resources(this.playerData.resourceCounts); }; m.GameState.prototype.getPopulation = function() { return this.playerData.popCount; }; m.GameState.prototype.getPopulationLimit = function() { return this.playerData.popLimit; }; m.GameState.prototype.getPopulationMax = function() { return this.playerData.popMax; }; m.GameState.prototype.getPlayerID = function() { return this.player; }; m.GameState.prototype.hasAllies = function() { for (let i in this.playerData.isAlly) if (this.playerData.isAlly[i] && +i !== this.player && this.sharedScript.playersData[i].state !== "defeated") return true; return false; }; m.GameState.prototype.hasEnemies = function() { for (let i in this.playerData.isEnemy) if (this.playerData.isEnemy[i] && +i !== 0 && this.sharedScript.playersData[i].state !== "defeated") return true; return false; }; m.GameState.prototype.hasNeutrals = function() { for (let i in this.playerData.isNeutral) if (this.playerData.isNeutral[i] && this.sharedScript.playersData[i].state !== "defeated") return true; return false; }; m.GameState.prototype.isPlayerNeutral = function(id) { return this.playerData.isNeutral[id]; }; m.GameState.prototype.isPlayerAlly = function(id) { return this.playerData.isAlly[id]; }; m.GameState.prototype.isPlayerMutualAlly = function(id) { return this.playerData.isMutualAlly[id]; }; m.GameState.prototype.isPlayerEnemy = function(id) { return this.playerData.isEnemy[id]; }; m.GameState.prototype.getEnemies = function() { let ret = []; for (let i in this.playerData.isEnemy) if (this.playerData.isEnemy[i]) ret.push(+i); return ret; }; m.GameState.prototype.getNeutrals = function() { let ret = []; for (let i in this.playerData.isNeutral) if (this.playerData.isNeutral[i]) ret.push(+i); return ret; }; m.GameState.prototype.getAllies = function() { let ret = []; for (let i in this.playerData.isAlly) if (this.playerData.isAlly[i]) ret.push(+i); return ret; }; m.GameState.prototype.getExclusiveAllies = function() { // Player is not included let ret = []; for (let i in this.playerData.isAlly) if (this.playerData.isAlly[i] && +i !== this.player) ret.push(+i); return ret; }; m.GameState.prototype.getMutualAllies = function() { let ret = []; for (let i in this.playerData.isMutualAlly) if (this.playerData.isMutualAlly[i] && this.sharedScript.playersData[i].isMutualAlly[this.player]) ret.push(+i); return ret; }; m.GameState.prototype.isEntityAlly = function(ent) { if (!ent) return false; return this.playerData.isAlly[ent.owner()]; }; m.GameState.prototype.isEntityExclusiveAlly = function(ent) { if (!ent) return false; return this.playerData.isAlly[ent.owner()] && ent.owner() !== this.player; }; m.GameState.prototype.isEntityEnemy = function(ent) { if (!ent) return false; return this.playerData.isEnemy[ent.owner()]; }; m.GameState.prototype.isEntityOwn = function(ent) { if (!ent) return false; return ent.owner() === this.player; }; m.GameState.prototype.getEntityById = function(id) { if (this.entities._entities.has(+id)) return this.entities._entities.get(+id); return undefined; }; m.GameState.prototype.getEntities = function(id) { if (id === undefined) return this.entities; return this.updatingGlobalCollection("player-" + id + "-entities", m.Filters.byOwner(id)); }; m.GameState.prototype.getStructures = function() { return this.updatingGlobalCollection("structures", m.Filters.byClass("Structure"), this.entities); }; m.GameState.prototype.getOwnEntities = function() { return this.updatingGlobalCollection("player-" + this.player + "-entities", m.Filters.byOwner(this.player)); }; m.GameState.prototype.getOwnStructures = function() { return this.updatingGlobalCollection("player-" + this.player + "-structures", m.Filters.byClass("Structure"), this.getOwnEntities()); }; m.GameState.prototype.getOwnUnits = function() { return this.updatingGlobalCollection("player-" + this.player + "-units", m.Filters.byClass("Unit"), this.getOwnEntities()); }; m.GameState.prototype.getAllyEntities = function() { return this.entities.filter(m.Filters.byOwners(this.getAllies())); }; m.GameState.prototype.getExclusiveAllyEntities = function() { return this.entities.filter(m.Filters.byOwners(this.getExclusiveAllies())); }; m.GameState.prototype.getAllyStructures = function() { return this.updatingCollection("diplo-ally-structures", m.Filters.byOwners(this.getAllies()), this.getStructures()); }; m.GameState.prototype.getNeutralStructures = function() { return this.getStructures().filter(m.Filters.byOwners(this.getNeutrals())); }; m.GameState.prototype.getEnemyEntities = function() { return this.entities.filter(m.Filters.byOwners(this.getEnemies())); }; m.GameState.prototype.getEnemyStructures = function(enemyID) { if (enemyID === undefined) return this.updatingCollection("diplo-enemy-structures", m.Filters.byOwners(this.getEnemies()), this.getStructures()); return this.updatingGlobalCollection("player-" + enemyID + "-structures", m.Filters.byOwner(enemyID), this.getStructures()); }; m.GameState.prototype.getEnemyUnits = function(enemyID) { if (enemyID === undefined) return this.getEnemyEntities().filter(m.Filters.byClass("Unit")); return this.updatingGlobalCollection("player-" + enemyID + "-units", m.Filters.byClass("Unit"), this.getEntities(enemyID)); }; /** if maintain is true, this will be stored. Otherwise it's one-shot. */ m.GameState.prototype.getOwnEntitiesByMetadata = function(key, value, maintain) { if (maintain === true) 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)); }; m.GameState.prototype.getOwnEntitiesByRole = function(role, maintain) { return this.getOwnEntitiesByMetadata("role", role, maintain); }; m.GameState.prototype.getOwnEntitiesByType = function(type, maintain) { let filter = m.Filters.byType(type); if (maintain === true) return this.updatingCollection("type-" + type, filter, this.getOwnEntities()); return this.getOwnEntities().filter(filter); }; m.GameState.prototype.getOwnEntitiesByClass = function(cls, maintain) { let filter = m.Filters.byClass(cls); if (maintain) return this.updatingCollection("class-" + cls, filter, this.getOwnEntities()); return this.getOwnEntities().filter(filter); }; m.GameState.prototype.getOwnFoundationsByClass = function(cls, maintain) { let filter = m.Filters.byClass(cls); if (maintain) return this.updatingCollection("foundations-class-" + cls, filter, this.getOwnFoundations()); return this.getOwnFoundations().filter(filter); }; m.GameState.prototype.getOwnTrainingFacilities = function() { return this.updatingGlobalCollection("player-" + this.player + "-training-facilities", m.Filters.byTrainingQueue(), this.getOwnEntities()); }; m.GameState.prototype.getOwnResearchFacilities = function() { return this.updatingGlobalCollection("player-" + this.player + "-research-facilities", m.Filters.byResearchAvailable(this.playerData.civ), this.getOwnEntities()); }; m.GameState.prototype.countEntitiesByType = function(type, maintain) { return this.getOwnEntitiesByType(type, maintain).length; }; m.GameState.prototype.countEntitiesAndQueuedByType = function(type, maintain) { let template = this.getTemplate(type); if (!template) return 0; let count = this.countEntitiesByType(type, maintain); // Count building foundations if (template.hasClass("Structure") === true) count += this.countFoundationsByType(type, true); else if (template.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) { for (let item of ent.trainingQueue()) if (item.unitTemplate == type) count += item.count; }); } return count; }; m.GameState.prototype.countFoundationsByType = function(type, maintain) { let foundationType = "foundation|" + type; if (maintain === true) return this.updatingCollection("foundation-type-" + type, m.Filters.byType(foundationType), this.getOwnFoundations()).length; let count = 0; this.getOwnStructures().forEach(function(ent) { if (ent.templateName() == foundationType) ++count; }); return count; }; m.GameState.prototype.countOwnEntitiesByRole = function(role) { return this.getOwnEntitiesByRole(role, "true").length; }; m.GameState.prototype.countOwnEntitiesAndQueuedWithRole = function(role) { let count = this.countOwnEntitiesByRole(role); // Count entities in building production queues this.getOwnTrainingFacilities().forEach(function(ent) { for (let item of ent.trainingQueue()) if (item.metadata && item.metadata.role && item.metadata.role == role) count += item.count; }); return count; }; m.GameState.prototype.countOwnQueuedEntitiesWithMetadata = function(data, value) { // Count entities in building production queues let count = 0; this.getOwnTrainingFacilities().forEach(function(ent) { for (let item of ent.trainingQueue()) if (item.metadata && item.metadata[data] && item.metadata[data] == value) count += item.count; }); return count; }; m.GameState.prototype.getOwnFoundations = function() { return this.updatingGlobalCollection("player-" + this.player + "-foundations", m.Filters.isFoundation(), this.getOwnStructures()); }; m.GameState.prototype.getOwnDropsites = function(resource) { if (resource) return this.updatingCollection("ownDropsite-" + resource, m.Filters.isDropsite(resource), this.getOwnEntities()); return this.updatingCollection("ownDropsite-all", m.Filters.isDropsite(), this.getOwnEntities()); }; m.GameState.prototype.getAnyDropsites = function(resource) { if (resource) return this.updatingGlobalCollection("anyDropsite-" + resource, m.Filters.isDropsite(resource), this.getEntities()); return this.updatingGlobalCollection("anyDropsite-all", m.Filters.isDropsite(), this.getEntities()); }; m.GameState.prototype.getResourceSupplies = function(resource) { return this.updatingGlobalCollection("resource-" + resource, m.Filters.byResource(resource), this.getEntities()); }; m.GameState.prototype.getHuntableSupplies = function() { return this.updatingGlobalCollection("resource-hunt", m.Filters.isHuntable(), this.getEntities()); }; m.GameState.prototype.getFishableSupplies = function() { return this.updatingGlobalCollection("resource-fish", m.Filters.isFishable(), this.getEntities()); }; /** This returns only units from buildings. */ m.GameState.prototype.findTrainableUnits = function(classes, anticlasses) { let allTrainable = []; let civ = this.playerData.civ; this.getOwnTrainingFacilities().forEach(function(ent) { let trainable = ent.trainableEntities(civ); if (!trainable) return; for (let unit of trainable) if (allTrainable.indexOf(unit) === -1) allTrainable.push(unit); }); let ret = []; let limits = this.getEntityLimits(); let current = this.getEntityCounts(); for (let trainable of allTrainable) { if (this.isTemplateDisabled(trainable)) continue; let template = this.getTemplate(trainable); if (!template || !template.available(this)) continue; if (classes.some(c => !template.hasClass(c))) continue; if (anticlasses.some(c => template.hasClass(c))) continue; let category = template.trainingCategory(); if (category && limits[category] && current[category] >= limits[category]) continue; ret.push( [trainable, template] ); } return ret; }; /** * Return all techs which can currently be researched * Does not factor cost. * If there are pairs, both techs are returned. */ m.GameState.prototype.findAvailableTech = function() { let allResearchable = []; let civ = this.playerData.civ; for (let ent of this.getOwnEntities().values()) { let searchable = ent.researchableTechs(civ); if (!searchable) continue; for (let tech of searchable) if (!this.playerData.disabledTechnologies[tech] && allResearchable.indexOf(tech) === -1) allResearchable.push(tech); } let ret = []; for (let tech of allResearchable) { let template = this.getTemplate(tech); if (template.pairDef()) { let 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(tech)) { // Phases are treated separately if (this.phases.every(phase => template._templateName != phase.name)) ret.push( [tech, template] ); } } return ret; }; /** * Return true if we have a building able to train that template */ m.GameState.prototype.hasTrainer = function(template) { let civ = this.playerData.civ; for (let ent of this.getOwnTrainingFacilities().values()) { let trainable = ent.trainableEntities(civ); if (trainable && trainable.indexOf(template) !== -1) return true; } return false; }; /** * Find buildings able to train that template. */ m.GameState.prototype.findTrainers = function(template) { let civ = this.playerData.civ; return this.getOwnTrainingFacilities().filter(function(ent) { let trainable = ent.trainableEntities(civ); return trainable && trainable.indexOf(template) !== -1; }); }; /** * Get any unit that is capable of constructing the given building type. */ m.GameState.prototype.findBuilder = function(template) { for (let ent of this.getOwnUnits().values()) { let buildable = ent.buildableEntities(); if (buildable && buildable.indexOf(template) !== -1) return ent; } return undefined; }; /** Return true if one of our buildings is capable of researching the given tech */ m.GameState.prototype.hasResearchers = function(templateName, noRequirementCheck) { // let's check we can research the tech. if (!this.canResearch(templateName, noRequirementCheck)) return false; let civ = this.playerData.civ; for (let ent of this.getOwnResearchFacilities().values()) { let techs = ent.researchableTechs(civ); for (let tech of techs) { let temp = this.getTemplate(tech); if (temp.pairDef()) { let pairedTechs = temp.getPairedTechs(); if (pairedTechs[0]._templateName == templateName || pairedTechs[1]._templateName == templateName) return true; } else if (tech == templateName) return true; } } return false; }; /** Find buildings that are capable of researching the given tech */ m.GameState.prototype.findResearchers = function(templateName, noRequirementCheck) { // let's check we can research the tech. if (!this.canResearch(templateName, noRequirementCheck)) return undefined; let self = this; let civ = this.playerData.civ; return this.getOwnResearchFacilities().filter(function(ent) { let techs = ent.researchableTechs(civ); for (let tech of techs) { let thisTemp = self.getTemplate(tech); if (thisTemp.pairDef()) { let pairedTechs = thisTemp.getPairedTechs(); if (pairedTechs[0]._templateName == templateName || pairedTechs[1]._templateName == templateName) return true; } else if (tech == templateName) return true; } return false; }); }; -/** - * Get any buildable structure with a given class - * TODO when several available, choose the best one - */ -m.GameState.prototype.findStructureWithClass = function(classes) -{ - let entTemplates = new Set(); - for (let ent of this.getOwnUnits().values()) - { - if (entTemplates.has(ent.templateName())) - continue; - let buildables = ent.buildableEntities(); - for (let buildable of buildables) - { - if (this.isTemplateDisabled(buildable)) - continue; - let template = this.getTemplate(buildable); - if (!template || !template.available(this)) - continue; - if (MatchesClassList(template.classes(), classes)) - return buildable; - } - entTemplates.add(ent.templateName()); - } - return undefined; -}; - m.GameState.prototype.getEntityLimits = function() { return this.playerData.entityLimits; }; m.GameState.prototype.getEntityCounts = function() { return this.playerData.entityCounts; }; m.GameState.prototype.isTemplateAvailable = function(templateName) { return this.templates[templateName] && !this.isTemplateDisabled(templateName); }; m.GameState.prototype.isTemplateDisabled = function(templateName) { if (!this.playerData.disabledTemplates[templateName]) return false; return this.playerData.disabledTemplates[templateName]; }; /** Checks whether the maximum number of buildings have been constructed for a certain catergory */ 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]; }; m.GameState.prototype.getTraderTemplatesGains = function() { let shipMechantTemplateName = this.applyCiv("units/{civ}_ship_merchant"); let supportTraderTemplateName = this.applyCiv("units/{civ}_support_trader"); let shipMerchantTemplate = !this.isTemplateDisabled(shipMechantTemplateName) && this.getTemplate(shipMechantTemplateName); let supportTraderTemplate = !this.isTemplateDisabled(supportTraderTemplateName) && this.getTemplate(supportTraderTemplateName); let norm = TradeGainNormalization(this.sharedScript.mapSize); let ret = {}; if (supportTraderTemplate) ret.landGainMultiplier = norm * supportTraderTemplate.gainMultiplier(); if (shipMerchantTemplate) ret.navalGainMultiplier = norm * shipMerchantTemplate.gainMultiplier(); return ret; }; return m; }(API3); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/buildManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/buildManager.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/buildManager.js (revision 20409) @@ -0,0 +1,171 @@ +var PETRA = function(m) +{ + +/** + * One task of this manager is to cache the list of structures we have builders for, + * to avoid having to loop on all entities each time. + * It also takes care of the structures we can't currently build and should not try to build endlessly. + */ + +m.BuildManager = function() +{ + // List of buildings we have builders for, with number of possible builders. + this.builderCounters = new Map(); + // List of buildings we can't currently build (because no room, no builder or whatever), + // with time we should wait before trying again to build it. + this.unbuildables = new Map(); +}; + +/** Initialization at start of game */ +m.BuildManager.prototype.init = function(gameState) +{ + let civ = gameState.getPlayerCiv(); + for (let ent of gameState.getOwnUnits().values()) + this.incrementBuilderCounters(civ, ent, 1); +}; + +m.BuildManager.prototype.incrementBuilderCounters = function(civ, ent, increment) +{ + for (let buildable of ent.buildableEntities(civ)) + { + if (this.builderCounters.has(buildable)) + { + let count = this.builderCounters.get(buildable) + increment; + if (count < 0) + { + API3.warning(" Petra error in incrementBuilderCounters for " + buildable + " with count < 0"); + continue; + } + this.builderCounters.set(buildable, count); + } + else if (increment > 0) + this.builderCounters.set(buildable, increment); + else + API3.warning(" Petra error in incrementBuilderCounters for " + buildable + " not yet set"); + } +}; + +/** Update the builders counters */ +m.BuildManager.prototype.checkEvents = function(gameState, events) +{ + this.elapsedTime = gameState.ai.elapsedTime; + let civ = gameState.getPlayerCiv(); + + for (let evt of events.Create) + { + let ent = gameState.getEntityById(evt.entity); + if (ent && ent.isOwn(PlayerID) && ent.hasClass("Unit")) + this.incrementBuilderCounters(civ, ent, 1); + } + + for (let evt of events.Destroy) + { + if (!evt.entityObj) + continue; + let ent = evt.entityObj; + if (ent && ent.isOwn(PlayerID) && ent.hasClass("Unit")) + this.incrementBuilderCounters(civ, ent, -1); + } + + for (let evt of events.OwnershipChanged) // capture events + { + let increment; + if (evt.from == PlayerID) + increment = -1; + else if (evt.to == PlayerID) + increment = 1; + else + continue; + let ent = gameState.getEntityById(evt.entity); + if (ent && ent.hasClass("Unit")) + this.incrementBuilderCounters(civ, ent, increment); + } +}; + + +/** + * Get the first buildable structure with a given class + * TODO when several available, choose the best one + */ +m.BuildManager.prototype.findStructureWithClass = function(gameState, classes) +{ + for (let [templateName, count] of this.builderCounters) + { + if (count == 0 || gameState.isTemplateDisabled(templateName)) + continue; + let template = gameState.getTemplate(templateName); + if (!template || !template.available(gameState)) + continue; + if (MatchesClassList(template.classes(), classes)) + return templateName; + } + return undefined; +}; + +m.BuildManager.prototype.hasBuilder = function(template) +{ + let numBuilders = this.builderCounters.get(template); + return numBuilders && numBuilders > 0; +}; + +m.BuildManager.prototype.isUnbuildable = function(gameState, template) +{ + return this.unbuildables.has(template) && this.unbuildables.get(template).time > gameState.ai.elapsedTime; +}; + +m.BuildManager.prototype.setBuildable = function(template) +{ + if (this.unbuildables.has(template)) + this.unbuildables.delete(template); +}; + +/** Time is the duration in second that we will wait before checking again if it is buildable */ +m.BuildManager.prototype.setUnbuildable = function(gameState, template, time = 180, reason = "room") +{ + if (!this.unbuildables.has(template)) + this.unbuildables.set(template, { "reason": reason, "time": gameState.ai.elapsedTime + time }); + else + { + let unbuildable = this.unbuildables.get(template); + if (unbuildable.time < gameState.ai.elapsedTime + time) + { + unbuildable.reason = reason; + unbuildable.time = gameState.ai.elapsedTime + time; + } + } +}; + +/** Return the number of unbuildables due to missing room */ +m.BuildManager.prototype.numberMissingRoom = function(gameState) +{ + let num = 0; + for (let unbuildable of this.unbuildables.values()) + if (unbuildable.reason == "room" && unbuildable.time > gameState.ai.elapsedTime) + ++num; + return num; +}; + +/** Reset the unbuildables due to missing room */ +m.BuildManager.prototype.resetMissingRoom = function(gameState) +{ + for (let [key, unbuildable] of this.unbuildables) + if (unbuildable.reason == "room") + this.unbuildables.delete(key); +}; + +m.BuildManager.prototype.Serialize = function() +{ + return { + "builderCounters": this.builderCounters, + "unbuildables": this.unbuildables + }; +}; + +m.BuildManager.prototype.Deserialize = function(data) +{ + for (let key in data) + this[key] = data[key]; +}; + +return m; +}(PETRA); Property changes on: ps/trunk/binaries/data/mods/public/simulation/ai/petra/buildManager.js ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js (revision 20408) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js (revision 20409) @@ -1,2596 +1,2575 @@ var PETRA = function(m) { /** * Headquarters * Deal with high level logic for the AI. Most of the interesting stuff gets done here. * Some tasks: * -defining RESS needs * -BO decisions. * > training workers * > building stuff (though we'll send that to bases) * -picking strategy (specific manager?) * -diplomacy -> diplomacyManager * -planning attacks -> attackManager * -picking new CC locations. */ m.HQ = function(Config) { this.Config = Config; this.phasing = 0; // existing values: 0 means no, i > 0 means phasing towards phase i // Cache the rates. this.turnCache = {}; // Some resources objects (will be filled in init) this.wantedRates = {}; this.currentRates = {}; this.lastFailedGather = {}; // workers configuration this.targetNumWorkers = this.Config.Economy.targetNumWorkers; this.supportRatio = this.Config.Economy.supportRatio; - this.stopBuilding = new Map(); // list of buildings to stop (temporarily) production because no room - this.fortStartTime = 180; // sentry defense towers, will start at fortStartTime + towerLapseTime this.towerStartTime = 0; // stone defense towers, will start as soon as available this.towerLapseTime = this.Config.Military.towerLapseTime; this.fortressStartTime = 0; // will start as soon as available this.fortressLapseTime = this.Config.Military.fortressLapseTime; this.extraTowers = Math.round(Math.min(this.Config.difficulty, 3) * this.Config.personality.defensive); this.extraFortresses = Math.round(Math.max(Math.min(this.Config.difficulty - 1, 2), 0) * this.Config.personality.defensive); this.baseManagers = []; this.attackManager = new m.AttackManager(this.Config); + this.buildManager = new m.BuildManager(); this.defenseManager = new m.DefenseManager(this.Config); this.tradeManager = new m.TradeManager(this.Config); this.navalManager = new m.NavalManager(this.Config); this.researchManager = new m.ResearchManager(this.Config); this.diplomacyManager = new m.DiplomacyManager(this.Config); this.garrisonManager = new m.GarrisonManager(this.Config); this.gameTypeManager = new m.GameTypeManager(this.Config); this.capturableTargets = new Map(); this.capturableTargetsTime = 0; }; /** More initialisation for stuff that needs the gameState */ m.HQ.prototype.init = function(gameState, queues) { this.territoryMap = m.createTerritoryMap(gameState); // initialize base map. Each pixel is a base ID, or 0 if not or not accessible this.basesMap = new API3.Map(gameState.sharedScript, "territory"); // create borderMap: flag cells on the border of the map // then this map will be completed with our frontier in updateTerritories this.borderMap = m.createBorderMap(gameState); // list of allowed regions this.landRegions = {}; // try to determine if we have a water map this.navalMap = false; this.navalRegions = {}; for (let res of gameState.sharedScript.resourceInfo.codes) { this.wantedRates[res] = 0; this.currentRates[res] = 0; } this.treasures = gameState.getEntities().filter(function (ent) { let type = ent.resourceSupplyType(); return type && type.generic === "treasure"; }); this.treasures.registerUpdates(); this.currentPhase = gameState.currentPhase(); this.decayingStructures = new Set(); }; /** * initialization needed after deserialization (only called when deserialization) */ m.HQ.prototype.postinit = function(gameState) { // Rebuild the base maps from the territory indices of each base this.basesMap = new API3.Map(gameState.sharedScript, "territory"); for (let base of this.baseManagers) for (let j of base.territoryIndices) this.basesMap.map[j] = base.ID; for (let ent of gameState.getOwnEntities().values()) { if (!ent.resourceDropsiteTypes() || ent.hasClass("Elephant")) continue; // Entities which have been built or have changed ownership after the last AI turn have no base. // they will be dealt with in the next checkEvents let baseID = ent.getMetadata(PlayerID, "base"); if (baseID === undefined) continue; let base = this.getBaseByID(baseID); base.assignResourceToDropsite(gameState, ent); } this.updateTerritories(gameState); }; /** * returns the sea index linking regions 1 and region 2 (supposed to be different land region) * otherwise return undefined * for the moment, only the case land-sea-land is supported */ m.HQ.prototype.getSeaBetweenIndices = function (gameState, index1, index2) { let path = gameState.ai.accessibility.getTrajectToIndex(index1, index2); if (path && path.length == 3 && gameState.ai.accessibility.regionType[path[1]] === "water") return path[1]; if (this.Config.debug > 1) { API3.warn("bad path from " + index1 + " to " + index2 + " ??? " + uneval(path)); API3.warn(" regionLinks start " + uneval(gameState.ai.accessibility.regionLinks[index1])); API3.warn(" regionLinks end " + uneval(gameState.ai.accessibility.regionLinks[index2])); } return undefined; }; m.HQ.prototype.checkEvents = function (gameState, events, queues) { + this.buildManager.checkEvents(gameState, events); + if (events.TerritoriesChanged.length || events.DiplomacyChanged.length) this.updateTerritories(gameState); for (let evt of events.DiplomacyChanged) { if (evt.player !== PlayerID && evt.otherPlayer !== PlayerID) continue; // Reset the entities collections which depend on diplomacy gameState.resetOnDiplomacyChanged(); break; } for (let evt of events.Create) { // Let's check if we have a valuable foundation needing builders quickly // (normal foundations are taken care in baseManager.assignToFoundations) let ent = gameState.getEntityById(evt.entity); if (!ent || !ent.isOwn(PlayerID) || ent.foundationProgress() === undefined) continue; if (ent.getMetadata(PlayerID, "base") == -1) { // Okay so let's try to create a new base around this. let newbase = new m.BaseManager(gameState, this.Config); newbase.init(gameState, "unconstructed"); newbase.setAnchor(gameState, ent); this.baseManagers.push(newbase); // Let's get a few units from other bases there to build this. let builders = this.bulkPickWorkers(gameState, newbase, 10); if (builders !== false) { builders.forEach(function (worker) { worker.setMetadata(PlayerID, "base", newbase.ID); worker.setMetadata(PlayerID, "subrole", "builder"); worker.setMetadata(PlayerID, "target-foundation", ent.id()); }); } } } for (let evt of events.ConstructionFinished) { // Let's check if we have a building set to create a new base. // TODO: move to the base manager. if (evt.newentity) { if (evt.newentity === evt.entity) // repaired building continue; let ent = gameState.getEntityById(evt.newentity); if (!ent || !ent.isOwn(PlayerID)) continue; if (ent.getMetadata(PlayerID, "baseAnchor") === true) { let base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); if (base.constructing) base.constructing = false; base.anchor = ent; base.anchorId = evt.newentity; base.buildings.updateEnt(ent); if (base.ID === this.baseManagers[1].ID) { // this is our first base, let us configure our starting resources this.configFirstBase(gameState); } else { // let us hope this new base will fix our possible resource shortage this.saveResources = undefined; this.saveSpace = undefined; } } } } for (let evt of events.OwnershipChanged) // capture events { if (evt.to !== PlayerID) continue; let ent = gameState.getEntityById(evt.entity); if (!ent) continue; if (ent.position()) ent.setMetadata(PlayerID, "access", gameState.ai.accessibility.getAccessValue(ent.position())); if (ent.hasClass("Unit")) { m.getBestBase(gameState, ent).assignEntity(gameState, ent); ent.setMetadata(PlayerID, "role", undefined); ent.setMetadata(PlayerID, "subrole", undefined); ent.setMetadata(PlayerID, "plan", undefined); ent.setMetadata(PlayerID, "PartOfArmy", undefined); if (ent.hasClass("Trader")) { ent.setMetadata(PlayerID, "role", "trader"); ent.setMetadata(PlayerID, "route", undefined); } if (ent.hasClass("Worker")) { ent.setMetadata(PlayerID, "role", "worker"); ent.setMetadata(PlayerID, "subrole", "idle"); } if (ent.hasClass("Ship")) ent.setMetadata(PlayerID, "sea", gameState.ai.accessibility.getAccessValue(ent.position(), true)); if (!ent.hasClass("Support") && !ent.hasClass("Ship") && ent.attackTypes() !== undefined) ent.setMetadata(PlayerID, "plan", -1); continue; } if (ent.hasClass("CivCentre")) // build a new base around it { let newbase = new m.BaseManager(gameState, this.Config); if (ent.foundationProgress() !== undefined) newbase.init(gameState, "unconstructed"); else newbase.init(gameState, "captured"); newbase.setAnchor(gameState, ent); this.baseManagers.push(newbase); newbase.assignEntity(gameState, ent); } else { // TODO should be reassigned later if a better base is captured m.getBestBase(gameState, ent).assignEntity(gameState, ent); if (ent.decaying()) { if (ent.isGarrisonHolder() && this.garrisonManager.addDecayingStructure(gameState, evt.entity, true)) continue; if (!this.decayingStructures.has(evt.entity)) this.decayingStructures.add(evt.entity); } } } // deal with the different rally points of training units: the rally point is set when the training starts // for the time being, only autogarrison is used for (let evt of events.TrainingStarted) { let ent = gameState.getEntityById(evt.entity); if (!ent || !ent.isOwn(PlayerID)) continue; if (!ent._entity.trainingQueue || !ent._entity.trainingQueue.length) continue; let metadata = ent._entity.trainingQueue[0].metadata; if (metadata && metadata.garrisonType) ent.setRallyPoint(ent, "garrison"); // trained units will autogarrison else ent.unsetRallyPoint(); } for (let evt of events.TrainingFinished) { for (let entId of evt.entities) { let ent = gameState.getEntityById(entId); if (!ent || !ent.isOwn(PlayerID)) continue; if (!ent.position()) { // we are autogarrisoned, check that the holder is registered in the garrisonManager let holderId = ent.unitAIOrderData()[0].target; let holder = gameState.getEntityById(holderId); if (holder) this.garrisonManager.registerHolder(gameState, holder); } else if (ent.getMetadata(PlayerID, "garrisonType")) { // we were supposed to be autogarrisoned, but this has failed (may-be full) ent.setMetadata(PlayerID, "garrisonType", undefined); } // Check if this unit is no more needed in its attack plan // (happen when the training ends after the attack is started or aborted) let plan = ent.getMetadata(PlayerID, "plan"); if (plan !== undefined && plan >= 0) { let attack = this.attackManager.getPlan(plan); if (!attack || attack.state !== "unexecuted") ent.setMetadata(PlayerID, "plan", -1); } // Assign it immediately to something useful to do if (ent.getMetadata(PlayerID, "role") === "worker") { let base; if (ent.getMetadata(PlayerID, "base") === undefined) { base = m.getBestBase(gameState, ent); base.assignEntity(gameState, ent); } else base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); base.reassignIdleWorkers(gameState, [ent]); base.workerObject.update(gameState, ent); } else if (ent.resourceSupplyType() && ent.position()) { let type = ent.resourceSupplyType(); if (!type.generic) continue; let dropsites = gameState.getOwnDropsites(type.generic); let pos = ent.position(); let access = gameState.ai.accessibility.getAccessValue(pos); let distmin = Math.min(); let goal; for (let dropsite of dropsites.values()) { if (!dropsite.position() || dropsite.getMetadata(PlayerID, "access") !== access) continue; let dist = API3.SquareVectorDistance(pos, dropsite.position()); if (dist > distmin) continue; distmin = dist; goal = dropsite.position(); } if (goal) ent.moveToRange(goal[0], goal[1]); } } } for (let evt of events.TerritoryDecayChanged) { let ent = gameState.getEntityById(evt.entity); if (!ent || !ent.isOwn(PlayerID) || ent.foundationProgress() !== undefined) continue; if (evt.to) { if (ent.isGarrisonHolder() && this.garrisonManager.addDecayingStructure(gameState, evt.entity)) continue; if (!this.decayingStructures.has(evt.entity)) this.decayingStructures.add(evt.entity); } else if (ent.isGarrisonHolder()) this.garrisonManager.removeDecayingStructure(evt.entity); } // Then deals with decaying structures: destroy them if being lost to enemy (except in easier difficulties) if (this.Config.difficulty < 2) return; for (let entId of this.decayingStructures) { let ent = gameState.getEntityById(entId); if (ent && ent.decaying() && ent.isOwn(PlayerID)) { let capture = ent.capturePoints(); if (!capture) continue; let captureRatio = capture[PlayerID] / capture.reduce((a, b) => a + b); if (captureRatio < 0.50) continue; let decayToGaia = true; for (let i = 1; i < capture.length; ++i) { if (gameState.isPlayerAlly(i) || !capture[i]) continue; decayToGaia = false; break; } if (decayToGaia) continue; let ratioMax = 0.70 + randFloat(0., 0.1); for (let evt of events.Attacked) { if (ent.id() != evt.target) continue; ratioMax = 0.85 + randFloat(0., 0.1); break; } if (captureRatio > ratioMax) continue; ent.destroy(); } this.decayingStructures.delete(entId); } }; /** Ensure that all requirements are met when phasing up*/ m.HQ.prototype.checkPhaseRequirements = function(gameState, queues) { if (gameState.getNumberOfPhases() == this.currentPhase) return; let requirements = gameState.getPhaseEntityRequirements(this.currentPhase + 1); let plan; let queue; for (let entityReq of requirements) { // Village requirements are met elsewhere by constructing more houses if (entityReq.class === "Village" || entityReq.class === "NotField") continue; if (gameState.getOwnEntitiesByClass(entityReq.class, true).length >= entityReq.count) continue; switch (entityReq.class) { case "Town": if (!queues.economicBuilding.hasQueuedUnits() && !queues.militaryBuilding.hasQueuedUnits() && !queues.defenseBuilding.hasQueuedUnits()) { if (!gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities() && this.canBuild(gameState, "structures/{civ}_market")) { plan = new m.ConstructionPlan(gameState, "structures/{civ}_market"); queue = "economicBuilding"; break; } if (!gameState.getOwnEntitiesByClass("Temple", true).hasEntities() && this.canBuild(gameState, "structures/{civ}_temple")) { plan = new m.ConstructionPlan(gameState, "structures/{civ}_temple"); queue = "economicBuilding"; break; } if (!gameState.getOwnEntitiesByClass("Blacksmith", true).hasEntities() && this.canBuild(gameState, "structures/{civ}_blacksmith")) { plan = new m.ConstructionPlan(gameState, "structures/{civ}_blacksmith"); queue = "militaryBuilding"; break; } if (this.canBuild(gameState, "structures/{civ}_defense_tower")) { plan = new m.ConstructionPlan(gameState, "structures/{civ}_defense_tower"); queue = "defenseBuilding"; break; } } break; default: // All classes not dealt with inside vanilla game. // We put them for the time being on the economic queue, except if wonder queue = entityReq.class === "Wonder" ? "wonder" : "economicBuilding"; if (!queues[queue].hasQueuedUnits()) { - let structure = gameState.findStructureWithClass([entityReq.class]); + let structure = this.buildManager.findStructureWithClass(gameState, [entityReq.class]); if (structure && this.canBuild(gameState, structure)) plan = new m.ConstructionPlan(gameState, structure); } } if (plan) { if (queue == "wonder") { gameState.ai.queueManager.changePriority("majorTech", 400); plan.queueToReset = "majorTech"; } else { gameState.ai.queueManager.changePriority(queue, 1000); plan.queueToReset = queue; } queues[queue].addPlan(plan); return; } } }; /** Called by any "phase" research plan once it's started */ m.HQ.prototype.OnPhaseUp = function(gameState, phase) { }; /** This code trains citizen workers, trying to keep close to a ratio of worker/soldiers */ m.HQ.prototype.trainMoreWorkers = function(gameState, queues) { // default template let requirementsDef = [ ["costsResource", 1, "food"] ]; let classesDef = ["Support", "Worker"]; let templateDef = this.findBestTrainableUnit(gameState, classesDef, requirementsDef); // counting the workers that aren't part of a plan let numberOfWorkers = 0; // all workers let numberOfSupports = 0; // only support workers (i.e. non fighting) gameState.getOwnUnits().forEach (function (ent) { if (ent.getMetadata(PlayerID, "role") === "worker" && ent.getMetadata(PlayerID, "plan") === undefined) { ++numberOfWorkers; if (ent.hasClass("Support")) ++numberOfSupports; } }); let numberInTraining = 0; gameState.getOwnTrainingFacilities().forEach(function(ent) { for (let item of ent.trainingQueue()) { numberInTraining += item.count; if (item.metadata && item.metadata.role && item.metadata.role === "worker" && item.metadata.plan === undefined) { numberOfWorkers += item.count; if (item.metadata.support) numberOfSupports += item.count; } } }); // Anticipate the optimal batch size when this queue will start // and adapt the batch size of the first and second queued workers to the present population // to ease a possible recovery if our population was drastically reduced by an attack // (need to go up to second queued as it is accounted in queueManager) let size = numberOfWorkers < 12 ? 1 : Math.min(5, Math.ceil(numberOfWorkers / 10)); if (queues.villager.plans[0]) { queues.villager.plans[0].number = Math.min(queues.villager.plans[0].number, size); if (queues.villager.plans[1]) queues.villager.plans[1].number = Math.min(queues.villager.plans[1].number, size); } if (queues.citizenSoldier.plans[0]) { queues.citizenSoldier.plans[0].number = Math.min(queues.citizenSoldier.plans[0].number, size); if (queues.citizenSoldier.plans[1]) queues.citizenSoldier.plans[1].number = Math.min(queues.citizenSoldier.plans[1].number, size); } let numberOfQueuedSupports = queues.villager.countQueuedUnits(); let numberOfQueuedSoldiers = queues.citizenSoldier.countQueuedUnits(); let numberQueued = numberOfQueuedSupports + numberOfQueuedSoldiers; let numberTotal = numberOfWorkers + numberQueued; if (this.saveResources && numberTotal > this.Config.Economy.popPhase2 + 10) return; if (numberTotal > this.targetNumWorkers || (numberTotal >= this.Config.Economy.popPhase2 && this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2)))) return; if (numberQueued > 50 || (numberOfQueuedSupports > 20 && numberOfQueuedSoldiers > 20) || numberInTraining > 15) return; // Choose whether we want soldiers or support units. let supportRatio = this.supportRatio; if (!gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}_field"))) supportRatio = Math.min(this.supportRatio, 0.1); else if (this.attackManager.upcomingAttacks.Rush.length) supportRatio = Math.min(this.supportRatio, 0.2); let supportMax = supportRatio * this.targetNumWorkers; let supportNum = supportMax * (1 - Math.exp(-numberTotal/supportMax)); let template; if (numberOfSupports + numberOfQueuedSupports > supportNum) { let requirements; if (numberTotal < 45) requirements = [ ["speed", 0.5], ["costsResource", 0.5, "stone"], ["costsResource", 0.5, "metal"] ]; else requirements = [ ["strength", 1] ]; let classes = ["CitizenSoldier", "Infantry"]; // We want at least 33% ranged and 33% melee classes.push(pickRandom(["Ranged", "Melee", "Infantry"])); template = this.findBestTrainableUnit(gameState, classes, requirements); } // If the template variable is empty, the default unit (Support unit) will be used // base "0" means automatic choice of base if (!template && templateDef) queues.villager.addPlan(new m.TrainingPlan(gameState, templateDef, { "role": "worker", "base": 0, "support": true }, size, size)); else if (template) queues.citizenSoldier.addPlan(new m.TrainingPlan(gameState, template, { "role": "worker", "base": 0 }, size, size)); }; /** picks the best template based on parameters and classes */ m.HQ.prototype.findBestTrainableUnit = function(gameState, classes, requirements) { let units; if (classes.indexOf("Hero") !== -1) units = gameState.findTrainableUnits(classes, []); else if (classes.indexOf("Siege") !== -1) // We do not want siege tower as AI does not know how to use it units = gameState.findTrainableUnits(classes, ["SiegeTower"]); else // We do not want hero when not explicitely specified units = gameState.findTrainableUnits(classes, ["Hero"]); if (units.length === 0) return undefined; let parameters = requirements.slice(); let remainingResources = this.getTotalResourceLevel(gameState); // resources (estimation) still gatherable in our territory let availableResources = gameState.ai.queueManager.getAvailableResources(gameState); // available (gathered) resources for (let type in remainingResources) { if (availableResources[type] > 800) continue; if (remainingResources[type] > 800) continue; let costsResource = remainingResources[type] > 400 ? 0.6 : 0.2; let toAdd = true; for (let param of parameters) { if (param[0] !== "costsResource" || param[2] !== type) continue; param[1] = Math.min( param[1], costsResource ); toAdd = false; break; } if (toAdd) parameters.push( [ "costsResource", costsResource, type ] ); } units.sort(function(a, b) { let aCost = 1 + a[1].costSum(); let bCost = 1 + b[1].costSum(); let aValue = 0.1; let bValue = 0.1; for (let param of parameters) { if (param[0] == "strength") { aValue += m.getMaxStrength(a[1]) * param[1]; bValue += m.getMaxStrength(b[1]) * param[1]; } else if (param[0] == "siegeStrength") { aValue += m.getMaxStrength(a[1], "Structure") * param[1]; bValue += m.getMaxStrength(b[1], "Structure") * param[1]; } else if (param[0] == "speed") { aValue += a[1].walkSpeed() * param[1]; bValue += b[1].walkSpeed() * param[1]; } else if (param[0] == "costsResource") { // requires a third parameter which is the resource if (a[1].cost()[param[2]]) aValue *= param[1]; if (b[1].cost()[param[2]]) bValue *= param[1]; } else if (param[0] == "canGather") { // checking against wood, could be anything else really. if (a[1].resourceGatherRates() && a[1].resourceGatherRates()["wood.tree"]) aValue *= param[1]; if (b[1].resourceGatherRates() && b[1].resourceGatherRates()["wood.tree"]) bValue *= param[1]; } else API3.warn(" trainMoreUnits avec non prevu " + uneval(param)); } return -aValue/aCost + bValue/bCost; }); return units[0][0]; }; /** * returns an entity collection of workers through BaseManager.pickBuilders * TODO: when same accessIndex, sort by distance */ m.HQ.prototype.bulkPickWorkers = function(gameState, baseRef, number) { let accessIndex = baseRef.accessIndex; if (!accessIndex) return false; // sorting bases by whether they are on the same accessindex or not. let baseBest = this.baseManagers.slice().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; }); let needed = number; let workers = new API3.EntityCollection(gameState.sharedScript); for (let base of baseBest) { if (base.ID === baseRef.ID) continue; base.pickBuilders(gameState, workers, needed); if (workers.length < number) needed = number - workers.length; else break; } if (!workers.length) return false; return workers; }; m.HQ.prototype.getTotalResourceLevel = function(gameState) { let total = {}; for (let res of gameState.sharedScript.resourceInfo.codes) total[res] = 0; for (let base of this.baseManagers) for (let res in total) total[res] += base.getResourceLevel(gameState, res); return total; }; /** * returns the current gather rate * This is not per-se exact, it performs a few adjustments ad-hoc to account for travel distance, stuffs like that. */ m.HQ.prototype.GetCurrentGatherRates = function(gameState) { if (!this.turnCache.gatherRates) { for (let res in this.currentRates) this.currentRates[res] = 0.5 * this.GetTCResGatherer(res); for (let base of this.baseManagers) base.getGatherRates(gameState, this.currentRates); for (let res in this.currentRates) { if (this.currentRates[res] < 0) { if (this.Config.debug > 0) API3.warn("Petra: current rate for " + res + " < 0 with " + this.GetTCResGatherer(res) + " moved gatherers"); this.currentRates[res] = 0; } } this.turnCache.gatherRates = true; } return this.currentRates; }; /** * Pick the resource which most needs another worker * How this works: * We get the rates we would want to have to be able to deal with our plans * We get our current rates * We compare; we pick the one where the discrepancy is highest. * Need to balance long-term needs and possible short-term needs. */ m.HQ.prototype.pickMostNeededResources = function(gameState) { this.wantedRates = gameState.ai.queueManager.wantedGatherRates(gameState); let currentRates = this.GetCurrentGatherRates(gameState); let needed = []; for (let res in this.wantedRates) needed.push({ "type": res, "wanted": this.wantedRates[res], "current": currentRates[res] }); needed.sort((a, b) => { let va = Math.max(0, a.wanted - a.current) / (a.current + 1); let vb = Math.max(0, b.wanted - b.current) / (b.current + 1); // If they happen to be equal (generally this means "0" aka no need), make it fair. if (va === vb) return a.current - b.current; return vb - va; }); return needed; }; /** * Returns the best position to build a new Civil Centre * Whose primary function would be to reach new resources of type "resource". */ m.HQ.prototype.findEconomicCCLocation = function(gameState, template, resource, proximity, fromStrategic) { // 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 look for a good spot. Engine.ProfileStart("findEconomicCCLocation"); // obstruction map let obstructions = m.createObstructionMap(gameState, 0, template); let halfSize = 0; if (template.get("Footprint/Square")) halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; else if (template.get("Footprint/Circle")) halfSize = +template.get("Footprint/Circle/@radius"); let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); let dpEnts = gameState.getOwnDropsites().filter(API3.Filters.not(API3.Filters.byClassesOr(["CivCentre", "Elephant"]))); let ccList = []; for (let cc of ccEnts.values()) ccList.push({"pos": cc.position(), "ally": gameState.isPlayerAlly(cc.owner())}); let dpList = []; for (let dp of dpEnts.values()) dpList.push({"pos": dp.position()}); let bestIdx; let bestVal; let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); let scale = 250 * 250; let proxyAccess; let nbShips = this.navalManager.transportShips.length; if (proximity) // this is our first base { // if our first base, ensure room around radius = Math.ceil((template.obstructionRadius().max + 8) / obstructions.cellSize); // scale is the typical scale at which we want to find a location for our first base // look for bigger scale if we start from a ship (access < 2) or from a small island let cellArea = gameState.getPassabilityMap().cellSize * gameState.getPassabilityMap().cellSize; proxyAccess = gameState.ai.accessibility.getAccessValue(proximity); if (proxyAccess < 2 || cellArea*gameState.ai.accessibility.regionSize[proxyAccess] < 24000) scale = 400 * 400; } let width = this.territoryMap.width; let cellSize = this.territoryMap.cellSize; for (let j = 0; j < this.territoryMap.length; ++j) { if (this.territoryMap.getOwnerIndex(j) !== 0) continue; // with enough room around to build the cc let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) continue; // we require that it is accessible let index = gameState.ai.accessibility.landPassMap[i]; if (!this.landRegions[index]) continue; if (proxyAccess && nbShips === 0 && proxyAccess !== index) continue; let norm = 0.5; // TODO adjust it, knowing that we will sum 5 maps // checking distance to other cc let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; if (proximity) // this is our first cc, let's do it near our units norm /= 1 + API3.SquareVectorDistance(proximity, pos) / scale; else { let minDist = Math.min(); for (let cc of ccList) { let dist = API3.SquareVectorDistance(cc.pos, pos); if (dist < 14000) // Reject if too near from any cc { norm = 0; break; } if (!cc.ally) continue; if (dist < 40000) // Reject if too near from an allied cc { norm = 0; break; } if (dist < 62000) // Disfavor if quite near an allied cc norm *= 0.5; if (dist < minDist) minDist = dist; } if (norm === 0) continue; if (minDist > 170000 && !this.navalMap) // Reject if too far from any allied cc (not connected) continue; if (minDist > 130000) // Disfavor if quite far from any allied cc { if (this.navalMap) { if (minDist > 250000) norm *= 0.5; else norm *= 0.8; } else norm *= 0.5; } for (let dp of dpList) { let dist = API3.SquareVectorDistance(dp.pos, pos); if (dist < 3600) { norm = 0; break; } else if (dist < 6400) norm *= 0.5; } if (norm === 0) continue; } if (this.borderMap.map[j] & m.fullBorder_Mask) // disfavor the borders of the map norm *= 0.5; let val = 2*gameState.sharedScript.ccResourceMaps[resource].map[j]; for (let res in gameState.sharedScript.resourceMaps) if (res !== "food") val += gameState.sharedScript.ccResourceMaps[res].map[j]; val *= norm; if (bestVal !== undefined && val < bestVal) continue; if (this.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = val; bestIdx = i; } Engine.ProfileStop(); if (bestVal === undefined) return false; let cut = 60; if (fromStrategic || proximity) // be less restrictive cut = 30; if (this.Config.debug > 1) API3.warn("we have found a base for " + resource + " with best (cut=" + cut + ") = " + bestVal); // not good enough. if (bestVal < cut) return false; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; // Define a minimal number of wanted ships in the seas reaching this new base let indexIdx = gameState.ai.accessibility.landPassMap[bestIdx]; for (let base of this.baseManagers) { if (!base.anchor || base.accessIndex === indexIdx) continue; let sea = this.getSeaBetweenIndices(gameState, base.accessIndex, indexIdx); if (sea !== undefined) this.navalManager.setMinimalTransportShips(gameState, sea, 1); } return [x, z]; }; /** * Returns the best position to build a new Civil Centre * Whose primary function would be to assure territorial continuity with our allies */ m.HQ.prototype.findStrategicCCLocation = function(gameState, template) { // This builds a map. The procedure is fairly simple. // We minimize the Sum((dist-300)**2) where the sum is on the three nearest allied CC // with the constraints that all CC have dist > 200 and at least one have dist < 400 // This needs at least 2 CC. Otherwise, go back to economic CC. let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); let ccList = []; let numAllyCC = 0; for (let cc of ccEnts.values()) { let ally = gameState.isPlayerAlly(cc.owner()); ccList.push({"pos": cc.position(), "ally": ally}); if (ally) ++numAllyCC; } if (numAllyCC < 2) return this.findEconomicCCLocation(gameState, template, "wood", undefined, true); Engine.ProfileStart("findStrategicCCLocation"); // obstruction map let obstructions = m.createObstructionMap(gameState, 0, template); let halfSize = 0; if (template.get("Footprint/Square")) halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; else if (template.get("Footprint/Circle")) halfSize = +template.get("Footprint/Circle/@radius"); let bestIdx; let bestVal; let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); let width = this.territoryMap.width; let cellSize = this.territoryMap.cellSize; let currentVal, delta; let distcc0, distcc1, distcc2; for (let j = 0; j < this.territoryMap.length; ++j) { if (this.territoryMap.getOwnerIndex(j) !== 0) continue; // with enough room around to build the cc let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) continue; // we require that it is accessible let index = gameState.ai.accessibility.landPassMap[i]; if (!this.landRegions[index]) continue; // checking distances to other cc let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; let minDist = Math.min(); distcc0 = undefined; for (let cc of ccList) { let dist = API3.SquareVectorDistance(cc.pos, pos); if (dist < 14000) // Reject if too near from any cc { minDist = 0; break; } if (!cc.ally) continue; if (dist < 62000) // Reject if quite near from ally cc { minDist = 0; break; } if (dist < minDist) minDist = dist; if (!distcc0 || dist < distcc0) { distcc2 = distcc1; distcc1 = distcc0; distcc0 = dist; } else if (!distcc1 || dist < distcc1) { distcc2 = distcc1; distcc1 = dist; } else if (!distcc2 || dist < distcc2) distcc2 = dist; } if (minDist < 1 || minDist > 170000 && !this.navalMap) continue; delta = Math.sqrt(distcc0) - 300; // favor a distance of 300 currentVal = delta*delta; delta = Math.sqrt(distcc1) - 300; currentVal += delta*delta; if (distcc2) { delta = Math.sqrt(distcc2) - 300; currentVal += delta*delta; } // disfavor border of the map if (this.borderMap.map[j] & m.fullBorder_Mask) currentVal += 10000; if (bestVal !== undefined && currentVal > bestVal) continue; if (this.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = currentVal; bestIdx = i; } if (this.Config.debug > 1) API3.warn("We've found a strategic base with bestVal = " + bestVal); Engine.ProfileStop(); if (bestVal === undefined) return undefined; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; // Define a minimal number of wanted ships in the seas reaching this new base let indexIdx = gameState.ai.accessibility.landPassMap[bestIdx]; for (let base of this.baseManagers) { if (!base.anchor || base.accessIndex === indexIdx) continue; let sea = this.getSeaBetweenIndices(gameState, base.accessIndex, indexIdx); if (sea !== undefined) this.navalManager.setMinimalTransportShips(gameState, sea, 1); } return [x, z]; }; /** * Returns the best position to build a new market: if the allies already have a market, build it as far as possible * from it, although not in our border to be able to defend it easily. If no allied market, our second market will * follow the same logic. * To do so, we suppose that the gain/distance is an increasing function of distance and look for the max distance * for performance reasons. */ m.HQ.prototype.findMarketLocation = function(gameState, template) { let markets = gameState.updatingCollection("diplo-ExclusiveAllyMarkets", API3.Filters.byClass("Market"), gameState.getExclusiveAllyEntities()).toEntityArray(); if (!markets.length) markets = gameState.updatingCollection("OwnMarkets", API3.Filters.byClass("Market"), gameState.getOwnStructures()).toEntityArray(); if (!markets.length) // this is the first market. For the time being, place it arbitrarily by the ConstructionPlan return [-1, -1, -1, 0]; // obstruction map let obstructions = m.createObstructionMap(gameState, 0, template); let halfSize = 0; if (template.get("Footprint/Square")) halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; else if (template.get("Footprint/Circle")) halfSize = +template.get("Footprint/Circle/@radius"); let bestIdx; let bestJdx; let bestVal; let bestDistSq; let bestGainMult; let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); let isNavalMarket = template.hasClass("NavalMarket"); let width = this.territoryMap.width; let cellSize = this.territoryMap.cellSize; let traderTemplatesGains = gameState.getTraderTemplatesGains(); for (let j = 0; j < this.territoryMap.length; ++j) { // do not try on the narrow border of our territory if (this.borderMap.map[j] & m.narrowFrontier_Mask) continue; if (this.basesMap.map[j] === 0) // only in our territory continue; // with enough room around to build the market let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) continue; let index = gameState.ai.accessibility.landPassMap[i]; if (!this.landRegions[index]) continue; let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; // checking distances to other markets let maxVal = 0; let maxDistSq; let maxGainMult; let gainMultiplier; for (let market of markets) { if (isNavalMarket && market.hasClass("NavalMarket")) { if (m.getSeaAccess(gameState, market) !== gameState.ai.accessibility.getAccessValue(pos, true)) continue; gainMultiplier = traderTemplatesGains.navalGainMultiplier; } else if (m.getLandAccess(gameState, market) === index && !m.isLineInsideEnemyTerritory(gameState, market.position(), pos)) gainMultiplier = traderTemplatesGains.landGainMultiplier; else continue; if (!gainMultiplier) continue; let distSq = API3.SquareVectorDistance(market.position(), pos); if (gainMultiplier * distSq > maxVal) { maxVal = gainMultiplier * distSq; maxDistSq = distSq; maxGainMult = gainMultiplier; } } if (maxVal === 0) continue; if (bestVal !== undefined && maxVal < bestVal) continue; if (this.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = maxVal; bestDistSq = maxDistSq; bestGainMult = maxGainMult; bestIdx = i; bestJdx = j; } if (this.Config.debug > 1) API3.warn("We found a market position with bestVal = " + bestVal); if (bestVal === undefined) // no constraints. For the time being, place it arbitrarily by the ConstructionPlan return [-1, -1, -1, 0]; let expectedGain = Math.round(bestGainMult * TradeGain(bestDistSq, gameState.sharedScript.mapSize)); if (this.Config.debug > 1) API3.warn("this would give a trading gain of " + expectedGain); // do not keep it if gain is too small, except if this is our first BarterMarket if (expectedGain < this.tradeManager.minimalGain || expectedGain < 8 && (!template.hasClass("BarterMarket") || gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities())) return false; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; return [x, z, this.basesMap.map[bestJdx], expectedGain]; }; /** * Returns the best position to build defensive buildings (fortress and towers) * Whose primary function is to defend our borders */ m.HQ.prototype.findDefensiveLocation = function(gameState, template) { // We take the point in our territory which is the nearest to any enemy cc // but requiring a minimal distance with our other defensive structures // and not in range of any enemy defensive structure to avoid building under fire. let ownStructures = gameState.getOwnStructures().filter(API3.Filters.byClassesOr(["Fortress", "Tower"])).toEntityArray(); let enemyStructures = gameState.getEnemyStructures().filter(API3.Filters.not(API3.Filters.byOwner(0))). filter(API3.Filters.byClassesOr(["CivCentre", "Fortress", "Tower"])); if (!enemyStructures.hasEntities()) // we may be in cease fire mode, build defense against neutrals { enemyStructures = gameState.getNeutralStructures().filter(API3.Filters.not(API3.Filters.byOwner(0))). filter(API3.Filters.byClassesOr(["CivCentre", "Fortress", "Tower"])); if (!enemyStructures.hasEntities() && !gameState.getAlliedVictory()) enemyStructures = gameState.getAllyStructures().filter(API3.Filters.not(API3.Filters.byOwner(PlayerID))). filter(API3.Filters.byClassesOr(["CivCentre", "Fortress", "Tower"])); if (!enemyStructures.hasEntities()) return undefined; } enemyStructures = enemyStructures.toEntityArray(); let wonderMode = gameState.getGameType() === "wonder"; let wonderDistmin; let wonders; if (wonderMode) { wonders = gameState.getOwnStructures().filter(API3.Filters.byClass("Wonder")).toEntityArray(); wonderMode = wonders.length !== 0; if (wonderMode) wonderDistmin = (50 + wonders[0].footprintRadius()) * (50 + wonders[0].footprintRadius()); } // obstruction map let obstructions = m.createObstructionMap(gameState, 0, template); let halfSize = 0; if (template.get("Footprint/Square")) halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; else if (template.get("Footprint/Circle")) halfSize = +template.get("Footprint/Circle/@radius"); let bestIdx; let bestJdx; let bestVal; let width = this.territoryMap.width; let cellSize = this.territoryMap.cellSize; let isTower = template.hasClass("Tower"); let isFortress = template.hasClass("Fortress"); let radius; if (isFortress) radius = Math.floor((template.obstructionRadius().max + 8) / obstructions.cellSize); else radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); for (let j = 0; j < this.territoryMap.length; ++j) { if (!wonderMode) { // do not try if well inside or outside territory if (!(this.borderMap.map[j] & m.fullFrontier_Mask)) continue; if (this.borderMap.map[j] & m.largeFrontier_Mask && isTower) continue; } if (this.basesMap.map[j] === 0) // inaccessible cell continue; // with enough room around to build the cc let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) continue; let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; // checking distances to other structures let minDist = Math.min(); let dista = 0; if (wonderMode) { dista = API3.SquareVectorDistance(wonders[0].position(), pos); if (dista < wonderDistmin) continue; dista *= 200; // empirical factor (TODO should depend on map size) to stay near the wonder } for (let str of enemyStructures) { if (str.foundationProgress() !== undefined) continue; let strPos = str.position(); if (!strPos) continue; let dist = API3.SquareVectorDistance(strPos, pos); if (dist < 6400) // TODO check on true attack range instead of this 80*80 { minDist = -1; break; } if (str.hasClass("CivCentre") && dist + dista < minDist) minDist = dist + dista; } if (minDist < 0) continue; let cutDist = 900; // 30*30 TODO maybe increase it for (let str of ownStructures) { let strPos = str.position(); if (!strPos) continue; if (API3.SquareVectorDistance(strPos, pos) < cutDist) { minDist = -1; break; } } if (minDist < 0 || minDist === Math.min()) continue; if (bestVal !== undefined && minDist > bestVal) continue; if (this.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = minDist; bestIdx = i; bestJdx = j; } if (bestVal === undefined) return undefined; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; return [x, z, this.basesMap.map[bestJdx]]; }; m.HQ.prototype.buildTemple = function(gameState, queues) { // at least one market (which have the same queue) should be build before any temple if (queues.economicBuilding.hasQueuedUnits() || gameState.getOwnEntitiesByClass("Temple", true).hasEntities() || !gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities()) return; // Try to build a temple earlier if in regicide to recruit healer guards if (this.currentPhase < 3 && gameState.getGameType() !== "regicide") return; if (!this.canBuild(gameState, "structures/{civ}_temple")) return; queues.economicBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_temple")); }; m.HQ.prototype.buildMarket = function(gameState, queues) { if (gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities() || !this.canBuild(gameState, "structures/{civ}_market")) return; if (queues.economicBuilding.hasQueuedUnitsWithClass("BarterMarket")) { if (!this.navalMap && !queues.economicBuilding.paused) { // Put available resources in this market when not a naval map let queueManager = gameState.ai.queueManager; let cost = queues.economicBuilding.plans[0].getCost(); queueManager.setAccounts(gameState, cost, "economicBuilding"); if (!queueManager.canAfford("economicBuilding", cost)) { for (let q in queueManager.queues) { if (q === "economicBuilding") continue; queueManager.transferAccounts(cost, q, "economicBuilding"); if (queueManager.canAfford("economicBuilding", cost)) break; } } } return; } if (gameState.getPopulation() < this.Config.Economy.popForMarket) return; gameState.ai.queueManager.changePriority("economicBuilding", 3*this.Config.priorities.economicBuilding); let plan = new m.ConstructionPlan(gameState, "structures/{civ}_market"); plan.queueToReset = "economicBuilding"; queues.economicBuilding.addPlan(plan); }; /** Build a farmstead */ m.HQ.prototype.buildFarmstead = function(gameState, queues) { // Only build one farmstead for the time being ("DropsiteFood" does not refer to CCs) if (gameState.getOwnEntitiesByClass("Farmstead", true).hasEntities()) return; // Wait to have at least one dropsite and house before the farmstead if (!gameState.getOwnEntitiesByClass("Storehouse", true).hasEntities()) return; if (!gameState.getOwnEntitiesByClass("House", true).hasEntities()) return; if (queues.economicBuilding.hasQueuedUnitsWithClass("DropsiteFood")) return; if (!this.canBuild(gameState, "structures/{civ}_farmstead")) return; queues.economicBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_farmstead")); }; /** * Try to build a wonder when required * force = true when called from the gameTypeManager in case of Wonder mode */ m.HQ.prototype.buildWonder = function(gameState, queues, force = false) { if (queues.wonder && queues.wonder.hasQueuedUnits() || gameState.getOwnEntitiesByClass("Wonder", true).hasEntities() || !this.canBuild(gameState, "structures/{civ}_wonder")) return; if (!force) { let templateName = gameState.applyCiv("structures/{civ}_wonder"); if (gameState.isTemplateDisabled(templateName)) return; let template = gameState.getTemplate(templateName); if (!template) return; // Check that we have enough resources to start thinking to build a wonder let cost = template.cost(); let resources = gameState.getResources(); let highLevel = 0; let lowLevel = 0; for (let res in cost) { if (resources[res] && resources[res] > 0.7 * cost[res]) ++highLevel; else if (!resources[res] || resources[res] < 0.3 * cost[res]) ++lowLevel; } if (highLevel == 0 || lowLevel > 1) return; } queues.wonder.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_wonder")); }; /** Build a corral, and train animals there */ m.HQ.prototype.manageCorral = function(gameState, queues) { if (queues.corral.hasQueuedUnits()) return; let nCorral = gameState.getOwnEntitiesByClass("Corral", true).length; if (!nCorral || !gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}_field")) && nCorral < this.currentPhase && gameState.getPopulation() > 30*nCorral) { if (this.canBuild(gameState, "structures/{civ}_corral")) { queues.corral.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_corral")); return; } if (!nCorral) return; } // And train some animals for (let corral of gameState.getOwnEntitiesByClass("Corral", true).values()) { if (corral.foundationProgress() !== undefined) continue; let trainables = corral.trainableEntities(); for (let trainable of trainables) { if (gameState.isTemplateDisabled(trainable)) continue; let template = gameState.getTemplate(trainable); if (!template || !template.isHuntable()) continue; let count = gameState.countEntitiesByType(trainable, true); for (let item of corral.trainingQueue()) count += item.count; if (count > nCorral) continue; queues.corral.addPlan(new m.TrainingPlan(gameState, trainable, { "trainer": corral.id() })); return; } } }; /** * build more houses if needed. * kinda ugly, lots of special cases to both build enough houses but not tooo many… */ m.HQ.prototype.buildMoreHouses = function(gameState, queues) { if (!gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}_house")) || gameState.getPopulationMax() <= gameState.getPopulationLimit()) return; let numPlanned = queues.house.length(); if (numPlanned < 3 || numPlanned < 5 && gameState.getPopulation() > 80) { let plan = new m.ConstructionPlan(gameState, "structures/{civ}_house"); // change the starting condition according to the situation. plan.goRequirement = "houseNeeded"; queues.house.addPlan(plan); } if (numPlanned > 0 && this.phasing && gameState.getPhaseEntityRequirements(this.phasing).length) { let houseTemplateName = gameState.applyCiv("structures/{civ}_house"); let houseTemplate = gameState.getTemplate(houseTemplateName); let needed = 0; for (let entityReq of gameState.getPhaseEntityRequirements(this.phasing)) { if (!houseTemplate.hasClass(entityReq.class)) continue; let count = gameState.getOwnStructures().filter(API3.Filters.byClass(entityReq.class)).length; - if (count < entityReq.count && this.stopBuilding.has(houseTemplateName)) + if (count < entityReq.count && this.buildManager.isUnbuildable(gameState, houseTemplateName)) { if (this.Config.debug > 1) API3.warn("no room to place a house ... try to be less restrictive"); - this.stopBuilding.delete(houseTemplateName); + this.buildManager.setBuildable(houseTemplateName); this.requireHouses = true; } needed = Math.max(needed, entityReq.count - count); } let houseQueue = queues.house.plans; for (let i = 0; i < numPlanned; ++i) if (houseQueue[i].isGo(gameState)) --needed; else if (needed > 0) { houseQueue[i].goRequirement = undefined; --needed; } } if (this.requireHouses) { let houseTemplate = gameState.getTemplate(gameState.applyCiv("structures/{civ}_house")); if (!this.phasing || gameState.getPhaseEntityRequirements(this.phasing).every(req => !houseTemplate.hasClass(req.class) || gameState.getOwnStructures().filter(API3.Filters.byClass(req.class)).length >= req.count)) this.requireHouses = undefined; } // When population limit too tight // - if no room to build, try to improve with technology // - otherwise increase temporarily the priority of houses let house = gameState.applyCiv("structures/{civ}_house"); let HouseNb = gameState.getOwnFoundations().filter(API3.Filters.byClass("House")).length; let popBonus = gameState.getTemplate(house).getPopulationBonus(); let freeSlots = gameState.getPopulationLimit() + HouseNb*popBonus - gameState.getPopulation(); let priority; if (freeSlots < 5) { - if (this.stopBuilding.has(house)) + if (this.buildManager.isUnbuildable(gameState, house)) { - if (this.stopBuilding.get(house) > gameState.ai.elapsedTime) - { - if (this.Config.debug > 1) - API3.warn("no room to place a house ... try to improve with technology"); - this.researchManager.researchPopulationBonus(gameState, queues); - } - else - { - this.stopBuilding.delete(house); - priority = 2*this.Config.priorities.house; - } + if (this.Config.debug > 1) + API3.warn("no room to place a house ... try to improve with technology"); + this.researchManager.researchPopulationBonus(gameState, queues); } else priority = 2*this.Config.priorities.house; } else priority = this.Config.priorities.house; + if (priority && priority != gameState.ai.queueManager.getPriority("house")) gameState.ai.queueManager.changePriority("house", priority); }; /** Checks the status of the territory expansion. If no new economic bases created, build some strategic ones. */ m.HQ.prototype.checkBaseExpansion = function(gameState, queues) { if (queues.civilCentre.hasQueuedUnits()) return; // First build one cc if all have been destroyed let activeBases = this.numActiveBase(); if (activeBases === 0) { this.buildFirstBase(gameState); return; } // Then expand if we have not enough room available for buildings - let nstopped = 0; - for (let stopTime of this.stopBuilding.values()) + if (this.buildManager.numberMissingRoom(gameState) > 1) { - if (stopTime === Infinity || stopTime < gameState.ai.elapsedTime || ++nstopped < 2) - continue; if (this.Config.debug > 2) - API3.warn("try to build a new base because not enough room to build " + uneval(this.stopBuilding)); + API3.warn("try to build a new base because not enough room to build "); this.buildNewBase(gameState, queues); return; } // If we've already planned to phase up, wait a bit before trying to expand if (this.phasing) return; // Finally expand if we have lots of units (threshold depending on the aggressivity value) let numUnits = gameState.getOwnUnits().length; let numvar = 10 * (1 - this.Config.personality.aggressive); if (numUnits > activeBases * (65 + numvar + (10 + numvar)*(activeBases-1)) || this.saveResources && numUnits > 50) { if (this.Config.debug > 2) API3.warn("try to build a new base because of population " + numUnits + " for " + activeBases + " CCs"); this.buildNewBase(gameState, queues); } }; m.HQ.prototype.buildNewBase = function(gameState, queues, resource) { if (this.numActiveBase() > 0 && this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2))) return false; if (gameState.getOwnFoundations().filter(API3.Filters.byClass("CivCentre")).hasEntities() || queues.civilCentre.hasQueuedUnits()) return false; let template; // We require at least one of this civ civCentre as they may allow specific units or techs let hasOwnCC = false; for (let ent of gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")).values()) { if (ent.owner() !== PlayerID || ent.templateName() !== gameState.applyCiv("structures/{civ}_civil_centre")) continue; hasOwnCC = true; break; } if (hasOwnCC && this.canBuild(gameState, "structures/{civ}_military_colony")) template = "structures/{civ}_military_colony"; else if (this.canBuild(gameState, "structures/{civ}_civil_centre")) template = "structures/{civ}_civil_centre"; else if (!hasOwnCC && this.canBuild(gameState, "structures/{civ}_military_colony")) template = "structures/{civ}_military_colony"; else return false; // base "-1" means new base. if (this.Config.debug > 1) API3.warn("new base " + gameState.applyCiv(template) + " planned with resource " + resource); queues.civilCentre.addPlan(new m.ConstructionPlan(gameState, template, { "base": -1, "resource": resource })); return true; }; /** Deals with building fortresses and towers along our border with enemies. */ m.HQ.prototype.buildDefenses = function(gameState, queues) { if (this.saveResources && !this.canBarter || queues.defenseBuilding.hasQueuedUnits()) return; if (!this.saveResources && (this.currentPhase > 2 || gameState.isResearching(gameState.getPhaseName(3)))) { // try to build fortresses if (this.canBuild(gameState, "structures/{civ}_fortress")) { let numFortresses = gameState.getOwnEntitiesByClass("Fortress", true).length; if ((!numFortresses || gameState.ai.elapsedTime > (1 + 0.10*numFortresses)*this.fortressLapseTime + this.fortressStartTime) && numFortresses < this.numActiveBase() + 1 + this.extraFortresses && numFortresses < Math.floor(gameState.getPopulation() / 25) && gameState.getOwnFoundationsByClass("Fortress").length < 2) { this.fortressStartTime = gameState.ai.elapsedTime; if (!numFortresses) gameState.ai.queueManager.changePriority("defenseBuilding", 2*this.Config.priorities.defenseBuilding); let plan = new m.ConstructionPlan(gameState, "structures/{civ}_fortress"); plan.queueToReset = "defenseBuilding"; queues.defenseBuilding.addPlan(plan); return; } } } if (this.Config.Military.numSentryTowers && this.currentPhase < 2 && this.canBuild(gameState, "structures/{civ}_sentry_tower")) { let numTowers = gameState.getOwnEntitiesByClass("Tower", true).length; // we count all towers, including wall towers let towerLapseTime = this.saveResource ? (1 + 0.5*numTowers) * this.towerLapseTime : this.towerLapseTime; if (numTowers < this.Config.Military.numSentryTowers && gameState.ai.elapsedTime > towerLapseTime + this.fortStartTime) { this.fortStartTime = gameState.ai.elapsedTime; queues.defenseBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_sentry_tower")); } return; } if (this.currentPhase < 2 || !this.canBuild(gameState, "structures/{civ}_defense_tower")) return; let numTowers = gameState.getOwnEntitiesByClass("DefenseTower", true).filter(API3.Filters.not(API3.Filters.byClass("SentryTower"))).length; let towerLapseTime = this.saveResource ? (1 + numTowers) * this.towerLapseTime : this.towerLapseTime; if ((!numTowers || gameState.ai.elapsedTime > (1 + 0.1*numTowers)*towerLapseTime + this.towerStartTime) && numTowers < 2 * this.numActiveBase() + 3 + this.extraTowers && numTowers < Math.floor(gameState.getPopulation() / 8) && gameState.getOwnFoundationsByClass("DefenseTower").length < 3) { this.towerStartTime = gameState.ai.elapsedTime; if (numTowers > 2 * this.numActiveBase() + 3) gameState.ai.queueManager.changePriority("defenseBuilding", Math.round(0.7*this.Config.priorities.defenseBuilding)); let plan = new m.ConstructionPlan(gameState, "structures/{civ}_defense_tower"); plan.queueToReset = "defenseBuilding"; queues.defenseBuilding.addPlan(plan); } }; m.HQ.prototype.buildBlacksmith = function(gameState, queues) { if (gameState.getPopulation() < this.Config.Military.popForBlacksmith || queues.militaryBuilding.hasQueuedUnits() || gameState.getOwnEntitiesByClass("Blacksmith", true).length) return; // build a market before the blacksmith if (!gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities()) return; if (this.canBuild(gameState, "structures/{civ}_blacksmith")) queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_blacksmith")); }; /** * Deals with constructing military buildings (barracks, stables…) * They are mostly defined by Config.js. This is unreliable since changes could be done easily. */ m.HQ.prototype.constructTrainingBuildings = function(gameState, queues) { if (this.saveResources && !this.canBarter || queues.militaryBuilding.hasQueuedUnits()) return; let numBarracks = this.canBuild(gameState, "structures/{civ}_barracks") ? gameState.getOwnEntitiesByClass("Barracks", true).length : -1; let numStables = this.canBuild(gameState, "structures/{civ}_stables") ? gameState.getOwnEntitiesByClass("Stables", true).length : -1; if (this.saveResources && numBarracks != 0) return; if (gameState.getPopulation() > this.Config.Military.popForBarracks1 || this.phasing == 2 && gameState.getOwnStructures().filter(API3.Filters.byClass("Village")).length < 5) { // first barracks and stables. if (numBarracks == 0) { gameState.ai.queueManager.changePriority("militaryBuilding", 2*this.Config.priorities.militaryBuilding); let plan = new m.ConstructionPlan(gameState, "structures/{civ}_barracks", { "militaryBase": true }); plan.queueToReset = "militaryBuilding"; queues.militaryBuilding.addPlan(plan); return; } if (numStables == 0) { queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_stables", { "militaryBase": true })); return; } // Second barracks and stables if (numBarracks == 1 && gameState.getPopulation() > this.Config.Military.popForBarracks2) { queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_barracks", { "militaryBase": true })); return; } if (numStables == 1 && gameState.getPopulation() > this.Config.Military.popForBarracks2) { queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_stables", { "militaryBase": true })); return; } // Then 3rd barracks/stables if needed if (numBarracks == 2 && numStables == -1 && gameState.getPopulation() > this.Config.Military.popForBarracks2 + 30) { queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_barracks", { "militaryBase": true })); return; } if (numBarracks == -1 && numStables == 2 && gameState.getPopulation() > this.Config.Military.popForBarracks2 + 30) { queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_stables", { "militaryBase": true })); return; } } if (this.saveResources) return; if (this.currentPhase < 3) return; if (this.canBuild(gameState, "structures/{civ}_elephant_stables") && !gameState.getOwnEntitiesByClass("ElephantStables", true).hasEntities()) { queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_elephant_stables", { "militaryBase": true })); return; } if (this.canBuild(gameState, "structures/{civ}_siege_workshop") && !gameState.getOwnEntitiesByClass("Workshop", true).hasEntities()) { queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_siege_workshop", { "militaryBase": true })); return; } if (gameState.getPopulation() < 80 || !this.bAdvanced.length) return; //build advanced military buildings let nAdvanced = 0; for (let advanced of this.bAdvanced) nAdvanced += gameState.countEntitiesAndQueuedByType(advanced, true); if (!nAdvanced || nAdvanced < this.bAdvanced.length && gameState.getPopulation() > 110) { for (let advanced of this.bAdvanced) { if (gameState.countEntitiesAndQueuedByType(advanced, true) > 0 || !this.canBuild(gameState, advanced)) continue; let template = gameState.getTemplate(advanced); if (!template) continue; if (template.hasDefensiveFire() || template.trainableEntities()) queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, advanced, { "militaryBase": true })); else // not a military building, but still use this queue queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, advanced)); return; } } }; /** * Find base nearest to ennemies for military buildings. */ m.HQ.prototype.findBestBaseForMilitary = function(gameState) { let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")).toEntityArray(); let bestBase; let enemyFound = false; let distMin = Math.min(); for (let cce of ccEnts) { if (gameState.isPlayerAlly(cce.owner())) continue; if (enemyFound && !gameState.isPlayerEnemy(cce.owner())) continue; let access = m.getLandAccess(gameState, cce); let isEnemy = gameState.isPlayerEnemy(cce.owner()); for (let cc of ccEnts) { if (cc.owner() != PlayerID) continue; if (m.getLandAccess(gameState, cc) != access) continue; let dist = API3.SquareVectorDistance(cc.position(), cce.position()); if (!enemyFound && isEnemy) enemyFound = true; else if (dist > distMin) continue; bestBase = cc.getMetadata(PlayerID, "base"); distMin = dist; } } return bestBase; }; /** * train with highest priority ranged infantry in the nearest civil centre from a given set of positions * and garrison them there for defense */ m.HQ.prototype.trainEmergencyUnits = function(gameState, positions) { if (gameState.ai.queues.emergency.hasQueuedUnits()) return false; let civ = gameState.getPlayerCiv(); // find nearest base anchor let distcut = 20000; let nearestAnchor; let distmin; for (let pos of positions) { let access = gameState.ai.accessibility.getAccessValue(pos); // check nearest base anchor for (let base of this.baseManagers) { if (!base.anchor || !base.anchor.position()) continue; if (base.anchor.getMetadata(PlayerID, "access") !== access) continue; if (!base.anchor.trainableEntities(civ)) // base still in construction continue; let queue = base.anchor._entity.trainingQueue; if (queue) { let time = 0; for (let item of queue) if (item.progress > 0 || item.metadata && item.metadata.garrisonType) time += item.timeRemaining; if (time/1000 > 5) continue; } let dist = API3.SquareVectorDistance(base.anchor.position(), pos); if (nearestAnchor && dist > distmin) continue; distmin = dist; nearestAnchor = base.anchor; } } if (!nearestAnchor || distmin > distcut) return false; // We will choose randomly ranged and melee units, except when garrisonHolder is full // in which case we prefer melee units let numGarrisoned = this.garrisonManager.numberOfGarrisonedUnits(nearestAnchor); if (nearestAnchor._entity.trainingQueue) { for (let item of nearestAnchor._entity.trainingQueue) { if (item.metadata && item.metadata.garrisonType) numGarrisoned += item.count; else if (!item.progress && (!item.metadata || !item.metadata.trainer)) nearestAnchor.stopProduction(item.id); } } let autogarrison = numGarrisoned < nearestAnchor.garrisonMax() && nearestAnchor.hitpoints() > nearestAnchor.garrisonEjectHealth() * nearestAnchor.maxHitpoints(); let rangedWanted = randBool() && autogarrison; let total = gameState.getResources(); let templateFound; let trainables = nearestAnchor.trainableEntities(civ); let garrisonArrowClasses = nearestAnchor.getGarrisonArrowClasses(); for (let trainable of trainables) { if (gameState.isTemplateDisabled(trainable)) continue; let template = gameState.getTemplate(trainable); if (!template || !template.hasClass("Infantry") || !template.hasClass("CitizenSoldier")) continue; if (autogarrison && !MatchesClassList(template.classes(), garrisonArrowClasses)) continue; if (!total.canAfford(new API3.Resources(template.cost()))) continue; templateFound = [trainable, template]; if (template.hasClass("Ranged") === rangedWanted) break; } if (!templateFound) return false; // Check first if we can afford it without touching the other accounts // and if not, take some of other accounted resources // TODO sort the queues to be substracted let queueManager = gameState.ai.queueManager; let cost = new API3.Resources(templateFound[1].cost()); queueManager.setAccounts(gameState, cost, "emergency"); if (!queueManager.canAfford("emergency", cost)) { for (let q in queueManager.queues) { if (q === "emergency") continue; queueManager.transferAccounts(cost, q, "emergency"); if (queueManager.canAfford("emergency", cost)) break; } } let metadata = { "role": "worker", "base": nearestAnchor.getMetadata(PlayerID, "base"), "plan": -1, "trainer": nearestAnchor.id() }; if (autogarrison) metadata.garrisonType = "protection"; gameState.ai.queues.emergency.addPlan(new m.TrainingPlan(gameState, templateFound[0], metadata, 1, 1)); return true; }; m.HQ.prototype.canBuild = function(gameState, structure, debug = false) { let type = gameState.applyCiv(structure); - // available room to build it - if (this.stopBuilding.has(type)) - { - if (this.stopBuilding.get(type) > gameState.ai.elapsedTime) - return false; - this.stopBuilding.delete(type); - } + if (this.buildManager.isUnbuildable(gameState, type)) + return false; if (gameState.isTemplateDisabled(type)) { - this.stopBuilding.set(type, Infinity); + this.buildManager.setUnbuildable(gameState, type, Infinity, "disabled"); return false; } let template = gameState.getTemplate(type); if (!template) - this.stopBuilding.set(type, Infinity); - if (!template || !template.available(gameState)) + { + this.buildManager.setUnbuildable(gameState, type, Infinity, "notemplate"); return false; + } + + if (!template.available(gameState)) + { + this.buildManager.setUnbuildable(gameState, type, 30, "tech"); + return false; + } - if (!gameState.findBuilder(type)) + if (!this.buildManager.hasBuilder(type)) { - this.stopBuilding.set(type, gameState.ai.elapsedTime + 120); + this.buildManager.setUnbuildable(gameState, type, 120, "nobuilder"); return false; } if (this.numActiveBase() < 1) { // if no base, check that we can build outside our territory let buildTerritories = template.buildTerritories(); if (buildTerritories && (!buildTerritories.length || buildTerritories.length === 1 && buildTerritories[0] === "own")) { - this.stopBuilding.set(type, gameState.ai.elapsedTime + 180); + this.buildManager.setUnbuildable(gameState, type, 180, "room"); return false; } } // build limits let limits = gameState.getEntityLimits(); let category = template.buildCategory(); if (category && limits[category] !== undefined && gameState.getEntityCounts()[category] >= limits[category]) { - this.stopBuilding.set(type, gameState.ai.elapsedTime + 60); + this.buildManager.setUnbuildable(gameState, type, 90, "limit"); return false; } return true; }; -m.HQ.prototype.stopBuild = function(gameState, structure, time=180) -{ - let type = gameState.applyCiv(structure); - if (this.stopBuilding.has(type)) - this.stopBuilding.set(type, Math.max(this.stopBuilding.get(type), gameState.ai.elapsedTime + time)); - else - this.stopBuilding.set(type, gameState.ai.elapsedTime + time); -}; - -m.HQ.prototype.restartBuild = function(gameState, structure) -{ - let type = gameState.applyCiv(structure); - if (this.stopBuilding.has(type)) - this.stopBuilding.delete(type); -}; - m.HQ.prototype.updateTerritories = function(gameState) { const around = [ [-0.7,0.7], [0,1], [0.7,0.7], [1,0], [0.7,-0.7], [0,-1], [-0.7,-0.7], [-1,0] ]; let alliedVictory = gameState.getAlliedVictory(); let passabilityMap = gameState.getPassabilityMap(); let width = this.territoryMap.width; let cellSize = this.territoryMap.cellSize; let insideSmall = Math.round(45 / cellSize); let insideLarge = Math.round(80 / cellSize); // should be about the range of towers let expansion = 0; for (let j = 0; j < this.territoryMap.length; ++j) { if (this.borderMap.map[j] & m.outside_Mask) continue; if (this.borderMap.map[j] & m.fullFrontier_Mask) this.borderMap.map[j] &= ~m.fullFrontier_Mask; // reset the frontier if (this.territoryMap.getOwnerIndex(j) != PlayerID) { // If this tile was already accounted, remove it if (this.basesMap.map[j] === 0) continue; let base = this.getBaseByID(this.basesMap.map[j]); let index = base.territoryIndices.indexOf(j); if (index == -1) { API3.warn(" problem in headquarters::updateTerritories for base " + this.basesMap.map[j]); continue; } base.territoryIndices.splice(index, 1); this.basesMap.map[j] = 0; } else { // Update the frontier let ix = j%width; let iz = Math.floor(j/width); let onFrontier = false; for (let a of around) { let jx = ix + Math.round(insideSmall*a[0]); if (jx < 0 || jx >= width) continue; let jz = iz + Math.round(insideSmall*a[1]); if (jz < 0 || jz >= width) continue; if (this.borderMap.map[jx+width*jz] & m.outside_Mask) continue; let territoryOwner = this.territoryMap.getOwnerIndex(jx+width*jz); if (territoryOwner !== PlayerID && !(alliedVictory && gameState.isPlayerAlly(territoryOwner))) { this.borderMap.map[j] |= m.narrowFrontier_Mask; break; } jx = ix + Math.round(insideLarge*a[0]); if (jx < 0 || jx >= width) continue; jz = iz + Math.round(insideLarge*a[1]); if (jz < 0 || jz >= width) continue; if (this.borderMap.map[jx+width*jz] & m.outside_Mask) continue; territoryOwner = this.territoryMap.getOwnerIndex(jx+width*jz); if (territoryOwner !== PlayerID && !(alliedVictory && gameState.isPlayerAlly(territoryOwner))) onFrontier = true; } if (onFrontier && !(this.borderMap.map[j] & m.narrowFrontier_Mask)) this.borderMap.map[j] |= m.largeFrontier_Mask; // If this tile was not already accounted, add it if (this.basesMap.map[j] !== 0) continue; let landPassable = false; let ind = API3.getMapIndices(j, this.territoryMap, passabilityMap); let access; for (let k of ind) { if (!this.landRegions[gameState.ai.accessibility.landPassMap[k]]) continue; landPassable = true; access = gameState.ai.accessibility.landPassMap[k]; break; } if (!landPassable) continue; let distmin = Math.min(); let baseID; let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; for (let base of this.baseManagers) { if (!base.anchor || !base.anchor.position()) continue; if (base.accessIndex != access) continue; let dist = API3.SquareVectorDistance(base.anchor.position(), pos); if (dist >= distmin) continue; distmin = dist; baseID = base.ID; } if (!baseID) continue; this.getBaseByID(baseID).territoryIndices.push(j); this.basesMap.map[j] = baseID; expansion++; } } if (!expansion) return; // We've increased our territory, so we may have some new room to build - for (let [type, stopTime] of this.stopBuilding) - if (stopTime !== Infinity) - this.stopBuilding.delete(type); + this.buildManager.resetMissingRoom(gameState); // And if sufficient expansion, check if building a new market would improve our present trade routes let cellArea = this.territoryMap.cellSize * this.territoryMap.cellSize; if (expansion * cellArea > 960) this.tradeManager.routeProspection = true; }; /** * returns the base corresponding to baseID */ m.HQ.prototype.getBaseByID = function(baseID) { for (let base of this.baseManagers) if (base.ID === baseID) return base; API3.warn("Petra error: no base found with ID " + baseID); return undefined; }; /** * returns the number of active (i.e. with one cc) bases */ m.HQ.prototype.numActiveBase = function() { if (!this.turnCache.activeBase) { let num = 0; for (let base of this.baseManagers) if (base.anchor) ++num; this.turnCache.activeBase = num; } return this.turnCache.activeBase; }; m.HQ.prototype.resetActiveBase = function() { this.turnCache.activeBase = undefined; }; /** * Count gatherers returning resources in the number of gatherers of resourceSupplies * to prevent the AI always reaffecting idle workers to these resourceSupplies (specially in naval maps). */ m.HQ.prototype.assignGatherers = function() { for (let base of this.baseManagers) { for (let worker of base.workers.values()) { if (worker.unitAIState().split(".")[1] !== "RETURNRESOURCE") continue; let orders = worker.unitAIOrderData(); if (orders.length < 2 || !orders[1].target || orders[1].target !== worker.getMetadata(PlayerID, "supply")) continue; this.AddTCGatherer(orders[1].target); } } }; m.HQ.prototype.isDangerousLocation = function(gameState, pos, radius) { return this.isNearInvadingArmy(pos) || this.isUnderEnemyFire(gameState, pos, radius); }; /** Check that the chosen position is not too near from an invading army */ m.HQ.prototype.isNearInvadingArmy = function(pos) { for (let army of this.defenseManager.armies) if (army.foePosition && API3.SquareVectorDistance(army.foePosition, pos) < 12000) return true; return false; }; m.HQ.prototype.isUnderEnemyFire = function(gameState, pos, radius = 0) { if (!this.turnCache.firingStructures) this.turnCache.firingStructures = gameState.updatingCollection("diplo-FiringStructures", API3.Filters.hasDefensiveFire(), gameState.getEnemyStructures()); for (let ent of this.turnCache.firingStructures.values()) { let range = radius + ent.attackRange("Ranged").max; if (API3.SquareVectorDistance(ent.position(), pos) < range*range) return true; } return false; }; /** Compute the capture strength of all units attacking a capturable target */ m.HQ.prototype.updateCaptureStrength = function(gameState) { this.capturableTargets.clear(); for (let ent of gameState.getOwnUnits().values()) { if (!ent.canCapture()) continue; let state = ent.unitAIState(); if (!state || !state.split(".")[1] || state.split(".")[1] !== "COMBAT") continue; let orderData = ent.unitAIOrderData(); if (!orderData || !orderData.length || !orderData[0].target) continue; let targetId = orderData[0].target; let target = gameState.getEntityById(targetId); if (!target || !target.isCapturable() || !ent.canCapture(target)) continue; if (!this.capturableTargets.has(targetId)) this.capturableTargets.set(targetId, { "strength": ent.captureStrength() * m.getAttackBonus(ent, target, "Capture"), "ents": new Set([ent.id()]) }); else { let capturableTarget = this.capturableTargets.get(target.id()); capturableTarget.strength += ent.captureStrength() * m.getAttackBonus(ent, target, "Capture"); capturableTarget.ents.add(ent.id()); } } for (let [targetId, capturableTarget] of this.capturableTargets) { let target = gameState.getEntityById(targetId); let allowCapture; for (let entId of capturableTarget.ents) { let ent = gameState.getEntityById(entId); if (allowCapture === undefined) allowCapture = m.allowCapture(gameState, ent, target); let orderData = ent.unitAIOrderData(); if (!orderData || !orderData.length || !orderData[0].attackType) continue; if ((orderData[0].attackType === "Capture") !== allowCapture) ent.attack(targetId, allowCapture); } } this.capturableTargetsTime = gameState.ai.elapsedTime; }; /** Some functions that register that we assigned a gatherer to a resource this turn */ /** add a gatherer to the turn cache for this supply. */ m.HQ.prototype.AddTCGatherer = function(supplyID) { if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID] !== undefined) ++this.turnCache.resourceGatherer[supplyID]; else { if (!this.turnCache.resourceGatherer) this.turnCache.resourceGatherer = {}; this.turnCache.resourceGatherer[supplyID] = 1; } }; /** remove a gatherer to the turn cache for this supply. */ m.HQ.prototype.RemoveTCGatherer = function(supplyID) { if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID]) --this.turnCache.resourceGatherer[supplyID]; else { if (!this.turnCache.resourceGatherer) this.turnCache.resourceGatherer = {}; this.turnCache.resourceGatherer[supplyID] = -1; } }; m.HQ.prototype.GetTCGatherer = function(supplyID) { if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID]) return this.turnCache.resourceGatherer[supplyID]; return 0; }; /** The next two are to register that we assigned a gatherer to a resource this turn. */ m.HQ.prototype.AddTCResGatherer = function(resource) { if (this.turnCache["resourceGatherer-" + resource]) ++this.turnCache["resourceGatherer-" + resource]; else this.turnCache["resourceGatherer-" + resource] = 1; this.turnCache.gatherRates = false; }; m.HQ.prototype.GetTCResGatherer = function(resource) { if (this.turnCache["resourceGatherer-" + resource]) return this.turnCache["resourceGatherer-" + resource]; return 0; }; /** * Check if a structure in blinking territory should/can be defended (currently if it has some attacking armies around) */ m.HQ.prototype.isDefendable = function(ent) { if (!this.turnCache.numAround) this.turnCache.numAround = {}; if (this.turnCache.numAround[ent.id()] === undefined) this.turnCache.numAround[ent.id()] = this.attackManager.numAttackingUnitsAround(ent.position(), 130); return +this.turnCache.numAround[ent.id()] > 8; }; /** * Some functions are run every turn * Others once in a while */ m.HQ.prototype.update = function(gameState, queues, events) { Engine.ProfileStart("Headquarters update"); this.turnCache = {}; this.territoryMap = m.createTerritoryMap(gameState); this.canBarter = gameState.getOwnEntitiesByClass("BarterMarket", true).filter(API3.Filters.isBuilt()).hasEntities(); // TODO find a better way to update if (this.currentPhase != gameState.currentPhase()) { if (this.Config.debug > 0) API3.warn(" civ " + gameState.getPlayerCiv() + " has phasedUp from " + this.currentPhase + " to " + gameState.currentPhase() + " at time " + gameState.ai.elapsedTime + " phasing " + this.phasing); this.currentPhase = gameState.currentPhase(); // In principle, this.phasing should be already reset to 0 when starting the research // but this does not work in case of an autoResearch tech if (this.phasing) this.phasing = 0; } /* if (this.Config.debug > 1) { gameState.getOwnUnits().forEach (function (ent) { if (!ent.position()) return; m.dumpEntity(ent); }); } */ this.checkEvents(gameState, events, queues); if (this.phasing) this.checkPhaseRequirements(gameState, queues); else this.researchManager.checkPhase(gameState, queues); if (this.numActiveBase() > 0) { if (gameState.ai.playedTurn % 4 == 0) this.trainMoreWorkers(gameState, queues); if (gameState.ai.playedTurn % 4 == 1) this.buildMoreHouses(gameState,queues); if ((!this.saveResources || this.canBarter) && gameState.ai.playedTurn % 4 == 2) this.buildFarmstead(gameState, queues); if (this.needCorral && gameState.ai.playedTurn % 4 == 3) this.manageCorral(gameState, queues); if (!queues.minorTech.hasQueuedUnits() && gameState.ai.playedTurn % 5 == 1) this.researchManager.update(gameState, queues); } if (this.numActiveBase() < 1 || this.canExpand && gameState.ai.playedTurn % 10 == 7 && this.currentPhase > 1) this.checkBaseExpansion(gameState, queues); if (this.currentPhase > 1) { if (!this.canBarter) this.buildMarket(gameState, queues); if (!this.saveResources) { this.buildBlacksmith(gameState, queues); this.buildTemple(gameState, queues); } if (gameState.ai.playedTurn % 30 === 0 && gameState.getPopulation() > 0.9 * gameState.getPopulationMax()) this.buildWonder(gameState, queues, false); } this.tradeManager.update(gameState, events, queues); this.garrisonManager.update(gameState, events); this.defenseManager.update(gameState, events); if (gameState.ai.playedTurn % 3 == 0) this.constructTrainingBuildings(gameState, queues); if (this.Config.difficulty > 0) this.buildDefenses(gameState, queues); this.assignGatherers(); for (let i = 0; i < this.baseManagers.length; ++i) { this.baseManagers[i].checkEvents(gameState, events, queues); if ((i + gameState.ai.playedTurn)%this.baseManagers.length === 0) this.baseManagers[i].update(gameState, queues, events); } this.navalManager.update(gameState, queues, events); if (this.Config.difficulty > 0 && (this.numActiveBase() > 0 || !this.canBuildUnits)) this.attackManager.update(gameState, queues, events); this.diplomacyManager.update(gameState, events); this.gameTypeManager.update(gameState, events, queues); // We update the capture strength at the end as it can change attack orders if (gameState.ai.elapsedTime - this.capturableTargetsTime > 3) this.updateCaptureStrength(gameState); Engine.ProfileStop(); }; m.HQ.prototype.Serialize = function() { let properties = { "phasing": this.phasing, "wantedRates": this.wantedRates, "currentRates": this.currentRates, "lastFailedGather": this.lastFailedGather, "supportRatio": this.supportRatio, "targetNumWorkers": this.targetNumWorkers, - "stopBuilding": this.stopBuilding, "fortStartTime": this.fortStartTime, "towerStartTime": this.towerStartTime, "fortressStartTime": this.fortressStartTime, "bAdvanced": this.bAdvanced, "saveResources": this.saveResources, "saveSpace": this.saveSpace, "needCorral": this.needCorral, "needFarm": this.needFarm, "needFish": this.needFish, "canExpand": this.canExpand, "canBuildUnits": this.canBuildUnits, "navalMap": this.navalMap, "landRegions": this.landRegions, "navalRegions": this.navalRegions, "decayingStructures": this.decayingStructures, "capturableTargets": this.capturableTargets, "capturableTargetsTime": this.capturableTargetsTime }; let baseManagers = []; for (let base of this.baseManagers) baseManagers.push(base.Serialize()); if (this.Config.debug == -100) { API3.warn(" HQ serialization ---------------------"); API3.warn(" properties " + uneval(properties)); API3.warn(" baseManagers " + uneval(baseManagers)); API3.warn(" attackManager " + uneval(this.attackManager.Serialize())); + API3.warn(" buildManager " + uneval(this.buildManager.Serialize())); API3.warn(" defenseManager " + uneval(this.defenseManager.Serialize())); API3.warn(" tradeManager " + uneval(this.tradeManager.Serialize())); API3.warn(" navalManager " + uneval(this.navalManager.Serialize())); API3.warn(" researchManager " + uneval(this.researchManager.Serialize())); API3.warn(" diplomacyManager " + uneval(this.diplomacyManager.Serialize())); API3.warn(" garrisonManager " + uneval(this.garrisonManager.Serialize())); API3.warn(" gameTypeManager " + uneval(this.gameTypeManager.Serialize())); } return { "properties": properties, "baseManagers": baseManagers, "attackManager": this.attackManager.Serialize(), + "buildManager": this.buildManager.Serialize(), "defenseManager": this.defenseManager.Serialize(), "tradeManager": this.tradeManager.Serialize(), "navalManager": this.navalManager.Serialize(), "researchManager": this.researchManager.Serialize(), "diplomacyManager": this.diplomacyManager.Serialize(), "garrisonManager": this.garrisonManager.Serialize(), "gameTypeManager": this.gameTypeManager.Serialize(), }; }; m.HQ.prototype.Deserialize = function(gameState, data) { for (let key in data.properties) this[key] = data.properties[key]; this.baseManagers = []; for (let base of data.baseManagers) { // the first call to deserialize set the ID base needed by entitycollections let newbase = new m.BaseManager(gameState, this.Config); newbase.Deserialize(gameState, base); newbase.init(gameState); newbase.Deserialize(gameState, base); this.baseManagers.push(newbase); } this.navalManager = new m.NavalManager(this.Config); this.navalManager.init(gameState, true); this.navalManager.Deserialize(gameState, data.navalManager); this.attackManager = new m.AttackManager(this.Config); this.attackManager.Deserialize(gameState, data.attackManager); this.attackManager.init(gameState); this.attackManager.Deserialize(gameState, data.attackManager); + this.buildManager = new m.BuildManager(); + this.buildManager.Deserialize(gameState, data.buildManager); + this.defenseManager = new m.DefenseManager(this.Config); this.defenseManager.Deserialize(gameState, data.defenseManager); this.tradeManager = new m.TradeManager(this.Config); this.tradeManager.init(gameState); this.tradeManager.Deserialize(gameState, data.tradeManager); this.researchManager = new m.ResearchManager(this.Config); this.researchManager.Deserialize(data.researchManager); this.diplomacyManager = new m.DiplomacyManager(this.Config); this.diplomacyManager.Deserialize(data.diplomacyManager); this.garrisonManager = new m.GarrisonManager(this.Config); this.garrisonManager.Deserialize(data.garrisonManager); this.gameTypeManager = new m.GameTypeManager(this.Config); this.gameTypeManager.Deserialize(data.gameTypeManager); }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js (revision 20408) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js (revision 20409) @@ -1,812 +1,812 @@ var PETRA = function(m) { /** * Defines a construction plan, ie a building. * We'll try to fing a good position if non has been provided */ m.ConstructionPlan = function(gameState, type, metadata, position) { if (!m.QueuePlan.call(this, gameState, type, metadata)) return false; this.position = position ? position : 0; this.category = "building"; return true; }; m.ConstructionPlan.prototype = Object.create(m.QueuePlan.prototype); m.ConstructionPlan.prototype.canStart = function(gameState) { if (gameState.ai.HQ.turnCache.buildingBuilt) // do not start another building if already one this turn return false; if (!this.isGo(gameState)) return false; if (this.template.requiredTech() && !gameState.isResearched(this.template.requiredTech())) return false; - return gameState.findBuilder(this.type) !== undefined; + return gameState.ai.HQ.buildManager.hasBuilder(this.type); }; m.ConstructionPlan.prototype.start = function(gameState) { Engine.ProfileStart("Building construction start"); // 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 // at least one unit that can start the foundation let builder = gameState.findBuilder(this.type); let pos = this.findGoodPosition(gameState); if (!pos) { - gameState.ai.HQ.stopBuild(gameState, this.type); + gameState.ai.HQ.buildManager.setUnbuildable(gameState, this.type); Engine.ProfileStop(); return; } if (this.metadata && this.metadata.expectedGain) { // Check if this market is still worth building (others may have been built making it useless) let tradeManager = gameState.ai.HQ.tradeManager; tradeManager.checkRoutes(gameState); if (!tradeManager.isNewMarketWorth(this.metadata.expectedGain)) { Engine.ProfileStop(); return; } } gameState.ai.HQ.turnCache.buildingBuilt = true; if (this.metadata === undefined) this.metadata = { "base": pos.base }; else if (this.metadata.base === undefined) this.metadata.base = pos.base; if (pos.access) this.metadata.access = pos.access; // needed for Docks whose position is on water else this.metadata.access = gameState.ai.accessibility.getAccessValue([pos.x, pos.z]); if (this.template.buildPlacementType() === "shore") { // adjust a bit the position if needed let cosa = Math.cos(pos.angle); let sina = Math.sin(pos.angle); let shiftMax = gameState.ai.HQ.territoryMap.cellSize; for (let shift = 0; shift <= shiftMax; shift += 2) { builder.construct(this.type, pos.x-shift*sina, pos.z-shift*cosa, pos.angle, this.metadata); if (shift > 0) builder.construct(this.type, pos.x+shift*sina, pos.z+shift*cosa, pos.angle, this.metadata); } } else if (pos.xx === undefined || pos.x == pos.xx && pos.z == pos.zz) builder.construct(this.type, pos.x, pos.z, pos.angle, this.metadata); else // try with the lowest, move towards us unless we're same { for (let step = 0; step <= 1; step += 0.2) builder.construct(this.type, step*pos.x + (1-step)*pos.xx, step*pos.z + (1-step)*pos.zz, pos.angle, this.metadata); } this.onStart(gameState); Engine.ProfileStop(); if (this.metadata && this.metadata.proximity) gameState.ai.HQ.navalManager.createTransportIfNeeded(gameState, this.metadata.proximity, [pos.x, pos.z], this.metadata.access); }; m.ConstructionPlan.prototype.findGoodPosition = function(gameState) { let template = this.template; if (template.buildPlacementType() === "shore") return this.findDockPosition(gameState); let HQ = gameState.ai.HQ; if (template.hasClass("Storehouse") && this.metadata.base) { // recompute the best dropsite location in case some conditions have changed let base = HQ.getBaseByID(this.metadata.base); let type = this.metadata.type ? this.metadata.type : "wood"; let newpos = base.findBestDropsiteLocation(gameState, type); if (newpos && newpos.quality > 0) { let pos = newpos.pos; return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": this.metadata.base }; } } if (!this.position) { if (template.hasClass("CivCentre")) { let pos; if (this.metadata && this.metadata.resource) { let proximity = this.metadata.proximity ? this.metadata.proximity : undefined; pos = HQ.findEconomicCCLocation(gameState, template, this.metadata.resource, proximity); } else pos = HQ.findStrategicCCLocation(gameState, template); if (pos) return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": 0 }; return false; } else if (template.hasClass("DefenseTower") || template.hasClass("Fortress") || template.hasClass("ArmyCamp")) { let pos = HQ.findDefensiveLocation(gameState, template); if (pos) return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": pos[2] }; // if this fortress is our first one, just try the standard placement if (!template.hasClass("Fortress") || gameState.getOwnEntitiesByClass("Fortress", true).hasEntities()) return false; } else if (template.hasClass("Market")) // Docks (i.e. NavalMarket) are done before { let pos = HQ.findMarketLocation(gameState, template); if (pos && pos[2] > 0) { if (!this.metadata) this.metadata = {}; this.metadata.expectedGain = pos[3]; return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": pos[2] }; } else if (!pos) return false; } } // Compute each tile's closeness to friendly structures: let placement = new API3.Map(gameState.sharedScript, "territory"); let cellSize = placement.cellSize; // size of each tile let alreadyHasHouses = false; if (this.position) // If a position was specified then place the building as close to it as possible { let x = Math.floor(this.position[0] / cellSize); let z = Math.floor(this.position[1] / cellSize); placement.addInfluence(x, z, 255); } else // No position was specified so try and find a sensible place to build { // give a small > 0 level as the result of addInfluence is constrained to be > 0 // if we really need houses (i.e. Phasing without enough village building), do not apply these constraints if (this.metadata && this.metadata.base !== undefined) { let base = this.metadata.base; for (let j = 0; j < placement.map.length; ++j) if (HQ.basesMap.map[j] == base) placement.map[j] = 45; } else { for (let j = 0; j < placement.map.length; ++j) if (HQ.basesMap.map[j] != 0) placement.map[j] = 45; } if (!HQ.requireHouses || !template.hasClass("House")) { gameState.getOwnStructures().forEach(function(ent) { let pos = ent.position(); let x = Math.round(pos[0] / cellSize); let z = Math.round(pos[1] / cellSize); let struct = ent.foundationProgress() === undefined ? ent : gameState.getBuiltTemplate(ent.templateName()); if (struct.resourceDropsiteTypes() && struct.resourceDropsiteTypes().indexOf("food") !== -1) { if (template.hasClass("Field") || template.hasClass("Corral")) placement.addInfluence(x, z, 80/cellSize, 50); else // If this is not a field add a negative influence because we want to leave this area for fields placement.addInfluence(x, z, 80/cellSize, -20); } else if (template.hasClass("House")) { if (ent.hasClass("House")) { placement.addInfluence(x, z, 60/cellSize, 40); // houses are close to other houses alreadyHasHouses = true; } else if (!ent.hasClass("StoneWall") || ent.hasClass("Gates")) placement.addInfluence(x, z, 60/cellSize, -40); // and further away from other stuffs } else if (template.hasClass("Farmstead") && (!ent.hasClass("Field") && !ent.hasClass("Corral") && (!ent.hasClass("StoneWall") || ent.hasClass("Gates")))) placement.addInfluence(x, z, 100/cellSize, -25); // move farmsteads away to make room (StoneWall test needed for iber) else if (template.hasClass("GarrisonFortress") && ent.hasClass("House")) placement.addInfluence(x, z, 120/cellSize, -50); else if (template.hasClass("Military")) placement.addInfluence(x, z, 40/cellSize, -40); else if (template.genericName() === "Rotary Mill" && ent.hasClass("Field")) placement.addInfluence(x, z, 60/cellSize, 40); }); } if (template.hasClass("Farmstead")) { for (let j = 0; j < placement.map.length; ++j) { let value = placement.map[j] - gameState.sharedScript.resourceMaps.wood.map[j]/3; placement.map[j] = value >= 0 ? value : 0; if (HQ.borderMap.map[j] & m.fullBorder_Mask) placement.map[j] /= 2; // we need space around farmstead, so disfavor map border } } } // Requires to be inside our territory, and inside our base territory if required // and if our first market, put it on border if possible to maximize distance with next market let favorBorder = template.hasClass("BarterMarket"); let disfavorBorder = gameState.currentPhase() > 1 && !template.hasDefensiveFire(); let militaryBase = this.metadata && this.metadata.militaryBase ? HQ.findBestBaseForMilitary(gameState) : undefined; if (this.metadata && this.metadata.base !== undefined) { let base = this.metadata.base; for (let j = 0; j < placement.map.length; ++j) { if (HQ.basesMap.map[j] != base) placement.map[j] = 0; else if (placement.map[j] > 0) { if (favorBorder && HQ.borderMap.map[j] & m.border_Mask) placement.map[j] += 50; else if (disfavorBorder && !(HQ.borderMap.map[j] & m.fullBorder_Mask)) placement.map[j] += 10; let x = (j % placement.width + 0.5) * cellSize; let z = (Math.floor(j / placement.width) + 0.5) * cellSize; if (HQ.isNearInvadingArmy([x, z])) placement.map[j] = 0; } } } else { for (let j = 0; j < placement.map.length; ++j) { if (HQ.basesMap.map[j] == 0) placement.map[j] = 0; else if (placement.map[j] > 0) { if (favorBorder && HQ.borderMap.map[j] & m.border_Mask) placement.map[j] += 50; else if (disfavorBorder && !(HQ.borderMap.map[j] & m.fullBorder_Mask)) placement.map[j] += 10; let x = (j % placement.width + 0.5) * cellSize; let z = (Math.floor(j / placement.width) + 0.5) * cellSize; if (HQ.isNearInvadingArmy([x, z])) placement.map[j] = 0; else if (militaryBase && HQ.basesMap.map[j] == militaryBase) placement.map[j] += 200; } } } // Find the best non-obstructed: // 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 let obstructions = m.createObstructionMap(gameState, 0, template); //obstructions.dumpIm(template.buildPlacementType() + "_obstructions.png"); let radius = 0; if (template.hasClass("Fortress") || this.type === gameState.applyCiv("structures/{civ}_siege_workshop") || this.type === gameState.applyCiv("structures/{civ}_elephant_stables")) radius = Math.floor((template.obstructionRadius().max + 12) / obstructions.cellSize); else if (template.resourceDropsiteTypes() === undefined && !template.hasClass("House") && !template.hasClass("Field")) radius = Math.ceil((template.obstructionRadius().max + 4) / obstructions.cellSize); else radius = Math.ceil((template.obstructionRadius().max + 0.5) / obstructions.cellSize); let bestTile; if (template.hasClass("House") && !alreadyHasHouses) { // try to get some space to place several houses first bestTile = placement.findBestTile(3*radius, obstructions); if (!bestTile.val) bestTile = undefined; } if (!bestTile) bestTile = placement.findBestTile(radius, obstructions); if (!bestTile.val) return false; let bestIdx = bestTile.idx; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; let territorypos = placement.gamePosToMapPos([x, z]); let territoryIndex = territorypos[0] + territorypos[1]*placement.width; // default angle = 3*Math.PI/4; return { "x": x, "z": z, "angle": 3*Math.PI/4, "base": HQ.basesMap.map[territoryIndex] }; }; /** * Placement of buildings with Dock build category * metadata.proximity is defined when first dock without any territory */ m.ConstructionPlan.prototype.findDockPosition = function(gameState) { let template = this.template; let territoryMap = gameState.ai.HQ.territoryMap; let obstructions = m.createObstructionMap(gameState, 0, template); //obstructions.dumpIm(template.buildPlacementType() + "_obstructions.png"); let bestIdx; let bestJdx; let bestAngle; let bestLand; let bestWater; let bestVal = -1; let navalPassMap = gameState.ai.accessibility.navalPassMap; let width = gameState.ai.HQ.territoryMap.width; let cellSize = gameState.ai.HQ.territoryMap.cellSize; let nbShips = gameState.ai.HQ.navalManager.transportShips.length; let wantedLand = this.metadata && this.metadata.land ? this.metadata.land : null; let wantedSea = this.metadata && this.metadata.sea ? this.metadata.sea : null; let proxyAccess = this.metadata && this.metadata.proximity ? gameState.ai.accessibility.getAccessValue(this.metadata.proximity) : null; if (nbShips === 0 && proxyAccess && proxyAccess > 1) { wantedLand = {}; wantedLand[proxyAccess] = true; } let dropsiteTypes = template.resourceDropsiteTypes(); let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); let halfSize = 0; // used for dock angle let halfDepth = 0; // used by checkPlacement let halfWidth = 0; // used by checkPlacement if (template.get("Footprint/Square")) { halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; halfDepth = +template.get("Footprint/Square/@depth") / 2; halfWidth = +template.get("Footprint/Square/@width") / 2; } else if (template.get("Footprint/Circle")) { halfSize = +template.get("Footprint/Circle/@radius"); halfDepth = halfSize; halfWidth = halfSize; } // res is a measure of the amount of resources around, and maxRes is the max value taken into account // water is a measure of the water space around, and maxWater is the max value that can be returned by checkDockPlacement const maxRes = 10; const maxWater = 16; for (let j = 0; j < territoryMap.length; ++j) { if (!this.isDockLocation(gameState, j, halfDepth, wantedLand, wantedSea)) continue; let dist; if (!proxyAccess) { // if not in our (or allied) territory, we do not want it too far to be able to defend it dist = this.getFrontierProximity(gameState, j); if (dist > 4) continue; } let i = territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) continue; if (wantedSea && navalPassMap[i] !== wantedSea) continue; let res = dropsiteTypes ? Math.min(maxRes, this.getResourcesAround(gameState, dropsiteTypes, j, 80)) : maxRes; let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; if (proxyAccess) { // if proximity is given, we look for the nearest point dist = API3.SquareVectorDistance(this.metadata.proximity, pos); dist = Math.sqrt(dist) + 20 * (maxRes - res); } else dist += 0.6 * (maxRes - res); // Add a penalty if on the map border as ship movement will be difficult if (gameState.ai.HQ.borderMap.map[j] & m.fullBorder_Mask) dist += 2; // do a pre-selection, supposing we will have the best possible water if (bestIdx !== undefined && dist > bestVal + maxWater) continue; let x = (i % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(i / obstructions.width) + 0.5) * obstructions.cellSize; let angle = this.getDockAngle(gameState, x, z, halfSize); if (angle === false) continue; let ret = this.checkDockPlacement(gameState, x, z, halfDepth, halfWidth, angle); if (!ret || !gameState.ai.HQ.landRegions[ret.land]) continue; // final selection now that the checkDockPlacement water is known if (bestIdx !== undefined && dist + maxWater - ret.water > bestVal) continue; if (this.metadata.proximity && gameState.ai.accessibility.regionSize[ret.land] < 4000) continue; if (gameState.ai.HQ.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = dist + maxWater - ret.water; bestIdx = i; bestJdx = j; bestAngle = angle; bestLand = ret.land; bestWater = ret.water; } if (bestVal < 0) return false; // if no good place with enough water around and still in first phase, wait for expansion at the next phase if (!this.metadata.proximity && bestWater < 10 && gameState.currentPhase() == 1) return false; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; // Assign this dock to a base let baseIndex = gameState.ai.HQ.basesMap.map[bestJdx]; if (!baseIndex) { for (let base of gameState.ai.HQ.baseManagers) { if (!base.anchor || !base.anchor.position()) continue; if (base.accessIndex !== bestLand) continue; baseIndex = base.ID; break; } if (!baseIndex) { if (gameState.ai.HQ.numActiveBase() > 0) API3.warn("Petra: dock constructed without base index " + baseIndex); else baseIndex = gameState.ai.HQ.baseManagers[0].ID; } } return { "x": x, "z": z, "angle": bestAngle, "base": baseIndex, "access": bestLand }; }; /** Algorithm taken from the function GetDockAngle in simulation/helpers/Commands.js */ m.ConstructionPlan.prototype.getDockAngle = function(gameState, x, z, size) { let pos = gameState.ai.accessibility.gamePosToMapPos([x, z]); let k = pos[0] + pos[1]*gameState.ai.accessibility.width; let seaRef = gameState.ai.accessibility.navalPassMap[k]; if (seaRef < 2) return false; const numPoints = 16; for (let dist = 0; dist < 4; ++dist) { let waterPoints = []; for (let i = 0; i < numPoints; ++i) { let angle = 2 * Math.PI * i / numPoints; pos = [x - (1+dist)*size*Math.sin(angle), z + (1+dist)*size*Math.cos(angle)]; pos = gameState.ai.accessibility.gamePosToMapPos(pos); if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) continue; let j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.navalPassMap[j] === seaRef) waterPoints.push(i); } let length = waterPoints.length; if (!length) continue; let consec = []; for (let i = 0; i < length; ++i) { let count = 0; for (let j = 0; j < length-1; ++j) { if ((waterPoints[(i + j) % length]+1) % numPoints == waterPoints[(i + j + 1) % length]) ++count; else break; } consec[i] = count; } let start = 0; let count = 0; for (let c in consec) { if (consec[c] > count) { start = c; count = consec[c]; } } // If we've found a shoreline, stop searching if (count != numPoints-1) return -((waterPoints[start] + consec[start]/2) % numPoints)/numPoints*2*Math.PI; } return false; }; /** * Algorithm taken from checkPlacement in simulation/components/BuildRestriction.js * to determine the special dock requirements * returns {"land": land index for this dock, "water": amount of water around this spot} */ m.ConstructionPlan.prototype.checkDockPlacement = function(gameState, x, z, halfDepth, halfWidth, angle) { let sz = halfDepth * Math.sin(angle); let cz = halfDepth * Math.cos(angle); // center back position let pos = gameState.ai.accessibility.gamePosToMapPos([x - sz, z - cz]); let j = pos[0] + pos[1]*gameState.ai.accessibility.width; let land = gameState.ai.accessibility.landPassMap[j]; if (land < 2) return null; // center front position pos = gameState.ai.accessibility.gamePosToMapPos([x + sz, z + cz]); j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) return null; // additional constraints compared to BuildRestriction.js to assure we have enough place to build let sw = halfWidth * Math.cos(angle) * 3 / 4; let cw = halfWidth * Math.sin(angle) * 3 / 4; pos = gameState.ai.accessibility.gamePosToMapPos([x - sz + sw, z - cz - cw]); j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] != land) return null; pos = gameState.ai.accessibility.gamePosToMapPos([x - sz - sw, z - cz + cw]); j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] != land) return null; let water = 0; let sp = 15 * Math.sin(angle); let cp = 15 * Math.cos(angle); for (let i = 1; i < 5; ++i) { pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*(sp+sw), z + cz + i*(cp-cw)]); if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) break; j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) break; pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*sp, z + cz + i*cp]); if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) break; j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) break; pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*(sp-sw), z + cz + i*(cp+cw)]); if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) break; j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) break; water += 4; } return {"land": land, "water": water}; }; /** * fast check if we can build a dock: returns false if nearest land is farther than the dock dimension * if the (object) wantedLand is given, this nearest land should have one of these accessibility * if wantedSea is given, this tile should be inside this sea */ const around = [ [ 1.0, 0.0], [ 0.87, 0.50], [ 0.50, 0.87], [ 0.0, 1.0], [-0.50, 0.87], [-0.87, 0.50], [-1.0, 0.0], [-0.87,-0.50], [-0.50,-0.87], [ 0.0,-1.0], [ 0.50,-0.87], [ 0.87,-0.50] ]; m.ConstructionPlan.prototype.isDockLocation = function(gameState, j, dimension, wantedLand, wantedSea) { let width = gameState.ai.HQ.territoryMap.width; let cellSize = gameState.ai.HQ.territoryMap.cellSize; let dist = dimension + 2*cellSize; let x = (j%width + 0.5) * cellSize; let z = (Math.floor(j/width) + 0.5) * cellSize; for (let a of around) { let pos = gameState.ai.accessibility.gamePosToMapPos([x + dist*a[0], z + dist*a[1]]); if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width) continue; if (pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) continue; let k = pos[0] + pos[1]*gameState.ai.accessibility.width; let landPass = gameState.ai.accessibility.landPassMap[k]; if (landPass < 2 || wantedLand && !wantedLand[landPass]) continue; pos = gameState.ai.accessibility.gamePosToMapPos([x - dist*a[0], z - dist*a[1]]); if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width) continue; if (pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) continue; k = pos[0] + pos[1]*gameState.ai.accessibility.width; if (wantedSea && gameState.ai.accessibility.navalPassMap[k] !== wantedSea) continue; else if (!wantedSea && gameState.ai.accessibility.navalPassMap[k] < 2) continue; return true; } return false; }; /** * return a measure of the proximity to our frontier (including our allies) * 0=inside, 1=less than 24m, 2= less than 48m, 3= less than 72m, 4=less than 96m, 5=above 96m */ m.ConstructionPlan.prototype.getFrontierProximity = function(gameState, j) { let alliedVictory = gameState.getAlliedVictory(); let territoryMap = gameState.ai.HQ.territoryMap; let territoryOwner = territoryMap.getOwnerIndex(j); if (territoryOwner === PlayerID || alliedVictory && gameState.isPlayerAlly(territoryOwner)) return 0; let borderMap = gameState.ai.HQ.borderMap; let width = territoryMap.width; let step = Math.round(24 / territoryMap.cellSize); let ix = j % width; let iz = Math.floor(j / width); let best = 5; for (let a of around) { for (let i = 1; i < 5; ++i) { let jx = ix + Math.round(i*step*a[0]); if (jx < 0 || jx >= width) continue; let jz = iz + Math.round(i*step*a[1]); if (jz < 0 || jz >= width) continue; if (borderMap.map[jx+width*jz] & m.outside_Mask) continue; territoryOwner = territoryMap.getOwnerIndex(jx+width*jz); if (alliedVictory && gameState.isPlayerAlly(territoryOwner) || territoryOwner === PlayerID) { best = Math.min(best, i); break; } } if (best === 1) break; } return best; }; /** * get the sum of the resources (except food) around, inside a given radius * resources have a weight (1 if dist=0 and 0 if dist=size) doubled for wood */ m.ConstructionPlan.prototype.getResourcesAround = function(gameState, types, i, radius) { let resourceMaps = gameState.sharedScript.resourceMaps; let w = resourceMaps.wood.width; let cellSize = resourceMaps.wood.cellSize; let size = Math.floor(radius / cellSize); let ix = i % w; let iy = Math.floor(i / w); let total = 0; let nbcell = 0; for (let k of types) { if (k === "food" || !resourceMaps[k]) continue; let weigh0 = k === "wood" ? 2 : 1; for (let dy = 0; dy <= size; ++dy) { let dxmax = size - dy; let ky = iy + dy; if (ky >= 0 && ky < w) { for (let dx = -dxmax; dx <= dxmax; ++dx) { let kx = ix + dx; if (kx < 0 || kx >= w) continue; let ddx = dx > 0 ? dx : -dx; let weight = weigh0 * (dxmax - ddx) / size; total += weight * resourceMaps[k].map[kx + w * ky]; nbcell += weight; } } if (dy === 0) continue; ky = iy - dy; if (ky >= 0 && ky < w) { for (let dx = -dxmax; dx <= dxmax; ++dx) { let kx = ix + dx; if (kx < 0 || kx >= w) continue; let ddx = dx > 0 ? dx : -dx; let weight = weigh0 * (dxmax - ddx) / size; total += weight * resourceMaps[k].map[kx + w * ky]; nbcell += weight; } } } } return nbcell ? total / nbcell : 0; }; m.ConstructionPlan.prototype.isGo = function(gameState) { if (this.goRequirement && this.goRequirement === "houseNeeded") { if (!gameState.ai.HQ.canBuild(gameState, "structures/{civ}_house")) return false; if (gameState.getPopulationMax() <= gameState.getPopulationLimit()) return false; let freeSlots = gameState.getPopulationLimit() - gameState.getPopulation(); for (let ent of gameState.getOwnFoundations().values()) { let template = gameState.getBuiltTemplate(ent.templateName()); if (template) freeSlots += template.getPopulationBonus(); } if (gameState.ai.HQ.saveResources) return freeSlots <= 10; if (gameState.getPopulation() > 55) return freeSlots <= 21; if (gameState.getPopulation() > 30) return freeSlots <= 15; return freeSlots <= 10; } return true; }; m.ConstructionPlan.prototype.onStart = function(gameState) { if (this.queueToReset) gameState.ai.queueManager.changePriority(this.queueToReset, gameState.ai.Config.priorities[this.queueToReset]); }; m.ConstructionPlan.prototype.Serialize = function() { return { "category": this.category, "type": this.type, "ID": this.ID, "metadata": this.metadata, "cost": this.cost.Serialize(), "number": this.number, "position": this.position, "goRequirement": this.goRequirement || undefined, "queueToReset": this.queueToReset || undefined }; }; m.ConstructionPlan.prototype.Deserialize = function(gameState, data) { for (let key in data) this[key] = data[key]; let cost = new API3.Resources(); cost.Deserialize(data.cost); this.cost = cost; }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js (revision 20408) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js (revision 20409) @@ -1,539 +1,540 @@ var PETRA = function(m) { /** * determines the strategy to adopt when starting a new game, depending on the initial conditions */ m.HQ.prototype.gameAnalysis = function(gameState) { // Analysis of the terrain and the different access regions if (!this.regionAnalysis(gameState)) return; this.attackManager.init(gameState); + this.buildManager.init(gameState); this.navalManager.init(gameState); this.tradeManager.init(gameState); this.diplomacyManager.init(gameState); // Make a list of buildable structures from the config file this.structureAnalysis(gameState); // Let's get our initial situation here. let nobase = new m.BaseManager(gameState, this.Config); nobase.init(gameState); nobase.accessIndex = 0; this.baseManagers.push(nobase); // baseManagers[0] will deal with unit/structure without base let ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")); for (let cc of ccEnts.values()) { let newbase = new m.BaseManager(gameState, this.Config); newbase.init(gameState); newbase.setAnchor(gameState, cc); this.baseManagers.push(newbase); } this.updateTerritories(gameState); // Assign entities and resources in the different bases this.assignStartingEntities(gameState); // Sandbox difficulty should not try to expand this.canExpand = this.Config.difficulty != 0; // If no base yet, check if we can construct one. If not, dispatch our units to possible tasks/attacks this.canBuildUnits = true; if (!gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).hasEntities()) { let template = gameState.applyCiv("structures/{civ}_civil_centre"); if (!gameState.isTemplateAvailable(template) || !gameState.getTemplate(template).available(gameState)) { if (this.Config.debug > 1) API3.warn(" this AI is unable to produce any units"); this.canBuildUnits = false; this.dispatchUnits(gameState); } else this.buildFirstBase(gameState); } // configure our first base strategy if (this.baseManagers.length > 1) this.configFirstBase(gameState); }; /** * Assign the starting entities to the different bases */ m.HQ.prototype.assignStartingEntities = function(gameState) { for (let ent of gameState.getOwnEntities().values()) { // do not affect merchant ship immediately to trade as they may-be useful for transport if (ent.hasClass("Trader") && !ent.hasClass("Ship")) this.tradeManager.assignTrader(ent); let pos = ent.position(); if (!pos) { // TODO should support recursive garrisoning. Make a warning for now if (ent.isGarrisonHolder() && ent.garrisoned().length) API3.warn("Petra warning: support for garrisoned units inside garrisoned holders not yet implemented"); continue; } // make sure we have not rejected small regions with units (TODO should probably also check with other non-gaia units) let gamepos = gameState.ai.accessibility.gamePosToMapPos(pos); let index = gamepos[0] + gamepos[1]*gameState.ai.accessibility.width; let land = gameState.ai.accessibility.landPassMap[index]; if (land > 1 && !this.landRegions[land]) this.landRegions[land] = true; let sea = gameState.ai.accessibility.navalPassMap[index]; if (sea > 1 && !this.navalRegions[sea]) this.navalRegions[sea] = true; // if garrisoned units inside, ungarrison them except if a ship in which case we will make a transport // when a construction will start (see createTransportIfNeeded) if (ent.isGarrisonHolder() && ent.garrisoned().length && !ent.hasClass("Ship")) for (let id of ent.garrisoned()) ent.unload(id); ent.setMetadata(PlayerID, "access", gameState.ai.accessibility.getAccessValue(pos)); let bestbase; let territorypos = this.territoryMap.gamePosToMapPos(pos); let territoryIndex = territorypos[0] + territorypos[1]*this.territoryMap.width; for (let i = 1; i < this.baseManagers.length; ++i) { let base = this.baseManagers[i]; if (base.territoryIndices.indexOf(territoryIndex) === -1) continue; base.assignEntity(gameState, ent); bestbase = base; break; } if (!bestbase) // entity outside our territory { bestbase = m.getBestBase(gameState, ent); bestbase.assignEntity(gameState, ent); } // now assign entities garrisoned inside this entity if (ent.isGarrisonHolder() && ent.garrisoned().length) for (let id of ent.garrisoned()) bestbase.assignEntity(gameState, gameState.getEntityById(id)); // and find something useful to do if we already have a base if (pos && bestbase.ID !== this.baseManagers[0].ID) { bestbase.assignRolelessUnits(gameState, [ent]); if (ent.getMetadata(PlayerID, "role") === "worker") { bestbase.reassignIdleWorkers(gameState, [ent]); bestbase.workerObject.update(gameState, ent); } } } }; /** * determine the main land Index (or water index if none) * as well as the list of allowed (land andf water) regions */ m.HQ.prototype.regionAnalysis = function(gameState) { let accessibility = gameState.ai.accessibility; let landIndex; let seaIndex; let ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")); for (let cc of ccEnts.values()) { let land = accessibility.getAccessValue(cc.position()); if (land > 1) { landIndex = land; break; } } if (!landIndex) { let civ = gameState.getPlayerCiv(); for (let ent of gameState.getOwnEntities().values()) { if (!ent.position() || !ent.hasClass("Unit") && !ent.trainableEntities(civ)) continue; let land = accessibility.getAccessValue(ent.position()); if (land > 1) { landIndex = land; break; } let sea = accessibility.getAccessValue(ent.position(), true); if (!seaIndex && sea > 1) seaIndex = sea; } } if (!landIndex && !seaIndex) { API3.warn("Petra error: it does not know how to interpret this map"); return false; } let passabilityMap = gameState.getPassabilityMap(); let totalSize = passabilityMap.width * passabilityMap.width; let minLandSize = Math.floor(0.1*totalSize); let minWaterSize = Math.floor(0.2*totalSize); let cellArea = passabilityMap.cellSize * passabilityMap.cellSize; for (let i = 0; i < accessibility.regionSize.length; ++i) { if (landIndex && i == landIndex) this.landRegions[i] = true; else if (accessibility.regionType[i] === "land" && cellArea*accessibility.regionSize[i] > 320) { if (landIndex) { let sea = this.getSeaBetweenIndices(gameState, landIndex, i); if (sea && (accessibility.regionSize[i] > minLandSize || accessibility.regionSize[sea] > minWaterSize)) { this.navalMap = true; this.landRegions[i] = true; this.navalRegions[sea] = true; } } else { let traject = accessibility.getTrajectToIndex(seaIndex, i); if (traject && traject.length === 2) { this.navalMap = true; this.landRegions[i] = true; this.navalRegions[seaIndex] = true; } } } else if (accessibility.regionType[i] === "water" && accessibility.regionSize[i] > minWaterSize) { this.navalMap = true; this.navalRegions[i] = true; } else if (accessibility.regionType[i] === "water" && cellArea*accessibility.regionSize[i] > 3600) this.navalRegions[i] = true; } if (this.Config.debug < 3) return true; for (let region in this.landRegions) API3.warn(" >>> zone " + region + " taille " + cellArea*gameState.ai.accessibility.regionSize[region]); API3.warn(" navalMap " + this.navalMap); API3.warn(" landRegions " + uneval(this.landRegions)); API3.warn(" navalRegions " + uneval(this.navalRegions)); return true; }; /** * load units and buildings from the config files * TODO: change that to something dynamic */ m.HQ.prototype.structureAnalysis = function(gameState) { let civref = gameState.playerData.civ; let civ = civref in this.Config.buildings.advanced ? civref : 'default'; this.bAdvanced = []; for (let advanced of this.Config.buildings.advanced[civ]) if (gameState.isTemplateAvailable(gameState.applyCiv(advanced))) this.bAdvanced.push(gameState.applyCiv(advanced)); }; /** * build our first base * if not enough resource, try first to do a dock */ m.HQ.prototype.buildFirstBase = function(gameState) { if (gameState.ai.queues.civilCentre.hasQueuedUnits()) return; let templateName = gameState.applyCiv("structures/{civ}_civil_centre"); if (gameState.isTemplateDisabled(templateName)) return; let template = gameState.getTemplate(templateName); if (!template) return; let total = gameState.getResources(); let goal = "civil_centre"; if (!total.canAfford(new API3.Resources(template.cost())) && !this.navalManager.docks.filter(API3.Filters.byClass("Dock")).hasEntities()) { // not enough resource to build a cc, try with a dock to accumulate resources if none yet if (gameState.ai.queues.dock.hasQueuedUnits()) return; templateName = gameState.applyCiv("structures/{civ}_dock"); if (gameState.isTemplateDisabled(templateName)) return; template = gameState.getTemplate(templateName); if (!template || !total.canAfford(new API3.Resources(template.cost()))) return; goal = "dock"; } if (!this.canBuild(gameState, templateName)) return; // We first choose as startingPoint the point where we have the more units let startingPoint = []; for (let ent of gameState.getOwnUnits().values()) { if (!ent.hasClass("Worker") && !(ent.hasClass("Support") && ent.hasClass("Elephant"))) continue; if (ent.hasClass("Cavalry")) continue; let pos = ent.position(); if (!pos) { let holder = m.getHolder(gameState, ent); if (!holder || !holder.position()) continue; pos = holder.position(); } let gamepos = gameState.ai.accessibility.gamePosToMapPos(pos); let index = gamepos[0] + gamepos[1]*gameState.ai.accessibility.width; let land = gameState.ai.accessibility.landPassMap[index]; let sea = gameState.ai.accessibility.navalPassMap[index]; let found = false; for (let point of startingPoint) { if (land !== point.land || sea !== point.sea) continue; if (API3.SquareVectorDistance(point.pos, pos) > 2500) continue; point.weight += 1; found = true; break; } if (!found) startingPoint.push({"pos": pos, "land": land, "sea": sea, "weight": 1}); } if (!startingPoint.length) return; let imax = 0; for (let i = 1; i < startingPoint.length; ++i) if (startingPoint[i].weight > startingPoint[imax].weight) imax = i; if (goal === "dock") { let sea = startingPoint[imax].sea > 1 ? startingPoint[imax].sea : undefined; gameState.ai.queues.dock.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_dock", { "sea": sea, "proximity": startingPoint[imax].pos })); } else gameState.ai.queues.civilCentre.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_civil_centre", { "base": -1, "resource": "wood", "proximity": startingPoint[imax].pos })); }; /** * set strategy if game without construction: * - if one of our allies has a cc, affect a small fraction of our army for his defense, the rest will attack * - otherwise all units will attack */ m.HQ.prototype.dispatchUnits = function(gameState) { let allycc = gameState.getExclusiveAllyEntities().filter(API3.Filters.byClass("CivCentre")).toEntityArray(); if (allycc.length) { if (this.Config.debug > 1) API3.warn(" We have allied cc " + allycc.length + " and " + gameState.getOwnUnits().length + " units "); let units = gameState.getOwnUnits(); let num = Math.max(Math.min(Math.round(0.08*(1+this.Config.personality.cooperative)*units.length), 20), 5); let num1 = Math.floor(num / 2); let num2 = num1; // first pass to affect ranged infantry units.filter(API3.Filters.byClassesAnd(["Infantry", "Ranged"])).forEach(function (ent) { if (!num || !num1) return; if (ent.getMetadata(PlayerID, "allied")) return; let access = gameState.ai.accessibility.getAccessValue(ent.position()); for (let cc of allycc) { if (!cc.position()) continue; if (gameState.ai.accessibility.getAccessValue(cc.position()) != access) continue; --num; --num1; ent.setMetadata(PlayerID, "allied", true); let range = 1.5 * cc.footprintRadius(); ent.moveToRange(cc.position()[0], cc.position()[1], range, range); break; } }); // second pass to affect melee infantry units.filter(API3.Filters.byClassesAnd(["Infantry", "Melee"])).forEach(function (ent) { if (!num || !num2) return; if (ent.getMetadata(PlayerID, "allied")) return; let access = gameState.ai.accessibility.getAccessValue(ent.position()); for (let cc of allycc) { if (!cc.position()) continue; if (gameState.ai.accessibility.getAccessValue(cc.position()) != access) continue; --num; --num2; ent.setMetadata(PlayerID, "allied", true); let range = 1.5 * cc.footprintRadius(); ent.moveToRange(cc.position()[0], cc.position()[1], range, range); break; } }); // and now complete the affectation, including all support units units.forEach(function (ent) { if (!num && !ent.hasClass("Support")) return; if (ent.getMetadata(PlayerID, "allied")) return; let access = gameState.ai.accessibility.getAccessValue(ent.position()); for (let cc of allycc) { if (!cc.position()) continue; if (gameState.ai.accessibility.getAccessValue(cc.position()) != access) continue; if (!ent.hasClass("Support")) --num; ent.setMetadata(PlayerID, "allied", true); let range = 1.5 * cc.footprintRadius(); ent.moveToRange(cc.position()[0], cc.position()[1], range, range); break; } }); } }; /** * configure our first base expansion * - if on a small island, favor fishing * - count the available wood resource, and allow rushes only if enough (we should otherwise favor expansion) */ m.HQ.prototype.configFirstBase = function(gameState) { if (this.baseManagers.length < 2) return; let startingSize = 0; let startingLand = []; for (let region in this.landRegions) { for (let base of this.baseManagers) { if (!base.anchor || base.accessIndex != +region) continue; startingSize += gameState.ai.accessibility.regionSize[region]; startingLand.push(base.accessIndex); break; } } let cell = gameState.getPassabilityMap().cellSize; startingSize = startingSize * cell * cell; if (this.Config.debug > 1) API3.warn("starting size " + startingSize + "(cut at 24000 for fish pushing)"); if (startingSize < 24000) { this.saveSpace = true; this.Config.Economy.popForDock = Math.min(this.Config.Economy.popForDock, 16); let num = Math.max(this.Config.Economy.targetNumFishers, 2); for (let land of startingLand) { for (let sea of gameState.ai.accessibility.regionLinks[land]) if (gameState.ai.HQ.navalRegions[sea]) this.navalManager.updateFishingBoats(sea, num); } } // - count the available wood resource, and react accordingly let startingFood = gameState.getResources().food; let check = {}; for (let proxim of ["nearby", "medium", "faraway"]) { for (let base of this.baseManagers) { for (let supply of base.dropsiteSupplies.food[proxim]) { if (check[supply.id]) // avoid double counting as same resource can appear several time continue; check[supply.id] = true; startingFood += supply.ent.resourceSupplyAmount(); } } } if (startingFood < 800) { if (startingSize < 24000) { this.needFish = true; this.Config.Economy.popForDock = 1; } else this.needFarm = true; } // - count the available wood resource, and allow rushes only if enough (we should otherwise favor expansion) let startingWood = gameState.getResources().wood; check = {}; for (let proxim of ["nearby", "medium", "faraway"]) { for (let base of this.baseManagers) { for (let supply of base.dropsiteSupplies.wood[proxim]) { if (check[supply.id]) // avoid double counting as same resource can appear several time continue; check[supply.id] = true; startingWood += supply.ent.resourceSupplyAmount(); } } } if (this.Config.debug > 1) API3.warn("startingWood: " + startingWood + " (cut at 8500 for no rush and 6000 for saveResources)"); if (startingWood < 6000) { this.saveResources = true; this.Config.Economy.popPhase2 = Math.floor(0.75 * this.Config.Economy.popPhase2); // Switch to town phase sooner to be able to expand if (startingWood < 2000 && this.needFarm) { this.needCorral = true; this.needFarm = false; } } if (startingWood > 8500 && this.canBuildUnits) { let allowed = Math.ceil((startingWood - 8500) / 3000); this.attackManager.setRushes(allowed); } // immediatly build a wood dropsite if possible. let template = gameState.applyCiv("structures/{civ}_storehouse"); if (!gameState.getOwnEntitiesByClass("Storehouse", true).hasEntities() && this.canBuild(gameState, template)) { let newDP = this.baseManagers[1].findBestDropsiteLocation(gameState, "wood"); if (newDP.quality > 40) { // if we start with enough workers, put our available resources in this first dropsite // same thing if our pop exceed the allowed one, as we will need several houses let numWorkers = gameState.getOwnUnits().filter(API3.Filters.byClass("Worker")).length; if (numWorkers > 12 && newDP.quality > 60 || gameState.getPopulation() > gameState.getPopulationLimit() + 20) { let cost = new API3.Resources(gameState.getTemplate(template).cost()); gameState.ai.queueManager.setAccounts(gameState, cost, "dropsites"); } gameState.ai.queues.dropsites.addPlan(new m.ConstructionPlan(gameState, template, { "base": this.baseManagers[1].ID }, newDP.pos)); } } // and build immediately a corral if needed if (this.needCorral) { template = gameState.applyCiv("structures/{civ}_corral"); if (!gameState.getOwnEntitiesByClass("Corral", true).hasEntities() && this.canBuild(gameState, template)) gameState.ai.queues.corral.addPlan(new m.ConstructionPlan(gameState, template, { "base": this.baseManagers[1].ID })); } }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/tradeManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/tradeManager.js (revision 20408) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/tradeManager.js (revision 20409) @@ -1,687 +1,686 @@ var PETRA = function(m) { /** * Manage the trade */ m.TradeManager = function(Config) { this.Config = Config; this.tradeRoute = undefined; this.potentialTradeRoute = undefined; this.routeProspection = false; this.targetNumTraders = this.Config.Economy.targetNumTraders; this.minimalGain = 3; this.warnedAllies = {}; }; m.TradeManager.prototype.init = function(gameState) { this.traders = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "role", "trader")); this.traders.registerUpdates(); }; m.TradeManager.prototype.hasTradeRoute = function() { return this.tradeRoute !== undefined; }; m.TradeManager.prototype.assignTrader = function(ent) { ent.setMetadata(PlayerID, "role", "trader"); this.traders.updateEnt(ent); }; m.TradeManager.prototype.trainMoreTraders = function(gameState, queues) { if (!this.hasTradeRoute() || queues.trader.hasQueuedUnits()) return; let numTraders = this.traders.length; let numSeaTraders = this.traders.filter(API3.Filters.byClass("Ship")).length; let numLandTraders = numTraders - numSeaTraders; // add traders already in training gameState.getOwnTrainingFacilities().forEach(function(ent) { for (let item of ent.trainingQueue()) { if (!item.metadata || !item.metadata.role || item.metadata.role !== "trader") continue; numTraders += item.count; if (item.metadata.sea !== undefined) numSeaTraders += item.count; else numLandTraders += item.count; } }); if (numTraders >= this.targetNumTraders && (!this.tradeRoute.sea && numLandTraders >= Math.floor(this.targetNumTraders/2) || this.tradeRoute.sea && numSeaTraders >= Math.floor(this.targetNumTraders/2))) return; let template; let metadata = { "role": "trader" }; if (this.tradeRoute.sea) { // if we have some merchand ships affected to transport, try first to reaffect them // May-be, there were produced at an early stage when no other ship were available // and the naval manager will train now more appropriate ships. let already = false; let shipToSwitch; gameState.ai.HQ.navalManager.seaTransportShips[this.tradeRoute.sea].forEach(function(ship) { if (already || !ship.hasClass("Trader")) return; if (ship.getMetadata(PlayerID, "role") === "switchToTrader") { already = true; return; } shipToSwitch = ship; }); if (already) return; if (shipToSwitch) { if (shipToSwitch.getMetadata(PlayerID, "transporter") === undefined) shipToSwitch.setMetadata(PlayerID, "role", "trader"); else shipToSwitch.setMetadata(PlayerID, "role", "switchToTrader"); return; } template = gameState.applyCiv("units/{civ}_ship_merchant"); metadata.sea = this.tradeRoute.sea; } else { template = gameState.applyCiv("units/{civ}_support_trader"); if (!this.tradeRoute.source.hasClass("NavalMarket")) metadata.base = this.tradeRoute.source.getMetadata(PlayerID, "base"); else metadata.base = this.tradeRoute.target.getMetadata(PlayerID, "base"); } if (!gameState.getTemplate(template)) { if (this.Config.debug > 0) API3.warn("Petra error: trying to train " + template + " for civ " + gameState.getPlayerCiv() + " but no template found."); return; } queues.trader.addPlan(new m.TrainingPlan(gameState, template, metadata, 1, 1)); }; m.TradeManager.prototype.updateTrader = function(gameState, ent) { if (ent.hasClass("Ship") && gameState.ai.playedTurn % 5 === 0 && !ent.unitAIState().startsWith("INDIVIDUAL.GATHER") && m.gatherTreasure(gameState, ent, true)) return; if (!this.hasTradeRoute() || !ent.isIdle() || !ent.position()) return; if (ent.getMetadata(PlayerID, "transport") !== undefined) return; // TODO if the trader is idle and has workOrders, restore them to avoid losing the current gain Engine.ProfileStart("Trade Manager"); let access = ent.hasClass("Ship") ? ent.getMetadata(PlayerID, "sea") : gameState.ai.accessibility.getAccessValue(ent.position()); let route = this.checkRoutes(gameState, access); if (!route) { // TODO try to garrison land trader inside merchant ship when only sea routes available if (this.Config.debug > 0) API3.warn(" no available route for " + ent.genericName() + " " + ent.id()); Engine.ProfileStop(); return; } let nearerSource = true; if (API3.SquareVectorDistance(route.target.position(), ent.position()) < API3.SquareVectorDistance(route.source.position(), ent.position())) nearerSource = false; if (!ent.hasClass("Ship") && route.land !== access) { if (nearerSource) gameState.ai.HQ.navalManager.requireTransport(gameState, ent, access, route.land, route.source.position()); else gameState.ai.HQ.navalManager.requireTransport(gameState, ent, access, route.land, route.target.position()); Engine.ProfileStop(); return; } if (nearerSource) ent.tradeRoute(route.target, route.source); else ent.tradeRoute(route.source, route.target); ent.setMetadata(PlayerID, "route", this.routeEntToId(route)); Engine.ProfileStop(); }; m.TradeManager.prototype.setTradingGoods = function(gameState) { let tradingGoods = {}; for (let res in gameState.ai.HQ.wantedRates) tradingGoods[res] = 0; // first, try to anticipate future needs let stocks = gameState.ai.HQ.getTotalResourceLevel(gameState); let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState); let remaining = 100; let targetNum = this.Config.Economy.targetNumTraders; for (let res in stocks) { if (res === "food") continue; let wantedRate = gameState.ai.HQ.wantedRates[res]; if (stocks[res] < 200) { tradingGoods[res] = wantedRate > 0 ? 20 : 10; targetNum += Math.min(5, 3 + Math.ceil(wantedRate/30)); } else if (stocks[res] < 500) { tradingGoods[res] = wantedRate > 0 ? 15 : 10; targetNum += 2; } else if (stocks[res] < 1000) { tradingGoods[res] = 10; targetNum += 1; } remaining -= tradingGoods[res]; } this.targetNumTraders = Math.round(this.Config.popScaling * targetNum); // then add what is needed now let mainNeed = Math.floor(remaining * 70 / 100); let nextNeed = remaining - mainNeed; tradingGoods[mostNeeded[0].type] += mainNeed; if (mostNeeded[1].wanted > 0) tradingGoods[mostNeeded[1].type] += nextNeed; else tradingGoods[mostNeeded[0].type] += nextNeed; Engine.PostCommand(PlayerID, {"type": "set-trading-goods", "tradingGoods": tradingGoods}); if (this.Config.debug > 2) API3.warn(" trading goods set to " + uneval(tradingGoods)); }; /** * Try to barter unneeded resources for needed resources. * only once per turn because the info is not updated within a turn */ m.TradeManager.prototype.performBarter = function(gameState) { let barterers = gameState.getOwnEntitiesByClass("BarterMarket", true).filter(API3.Filters.isBuilt()).toEntityArray(); if (barterers.length === 0) return false; // Available resources after account substraction let available = gameState.ai.queueManager.getAvailableResources(gameState); let needs = gameState.ai.queueManager.currentNeeds(gameState); let rates = gameState.ai.HQ.GetCurrentGatherRates(gameState); let barterPrices = gameState.getBarterPrices(); // calculates conversion rates let getBarterRate = (prices, buy, sell) => Math.round(100 * prices.sell[sell] / prices.buy[buy]); // loop through each missing resource checking if we could barter and help finishing a queue quickly. for (let buy of needs.types) { if (needs[buy] === 0 || needs[buy] < rates[buy]*30) // check if our rate allows to gather it fast enough continue; // pick the best resource to barter. let bestToSell; let bestRate = 0; for (let sell of needs.types) { if (sell === buy) continue; if (needs[sell] > 0 || available[sell] < 500) // do not sell if we need it or do not have enough buffer continue; let barterRateMin; if (sell === "food") { barterRateMin = 30; if (available[sell] > 40000) barterRateMin = 0; else if (available[sell] > 15000) barterRateMin = 5; else if (available[sell] > 1000) barterRateMin = 10; } else { barterRateMin = 70; if (available[sell] > 1000) barterRateMin = 50; if (buy === "food") barterRateMin += 20; } let barterRate = getBarterRate(barterPrices, buy, sell); if (barterRate > bestRate && barterRate > barterRateMin) { bestRate = barterRate; bestToSell = sell; } } if (bestToSell !== undefined) { barterers[0].barter(buy, bestToSell, 100); if (this.Config.debug > 2) API3.warn("Necessity bartering: sold " + bestToSell +" for " + buy + " >> need sell " + needs[bestToSell] + " need buy " + needs[buy] + " rate buy " + rates[buy] + " available sell " + available[bestToSell] + " available buy " + available[buy] + " barterRate " + bestRate); return true; } } // now do contingency bartering, selling food to buy finite resources (and annoy our ennemies by increasing prices) if (available.food < 1000 || needs.food > 0) return false; let bestToBuy; let bestChoice = 0; for (let buy of needs.types) { if (buy === "food") continue; let barterRateMin = 80; if (available[buy] < 5000 && available.food > 5000) barterRateMin -= 20 - Math.floor(available[buy]/250); let barterRate = getBarterRate(barterPrices, buy, "food"); if (barterRate < barterRateMin) continue; let choice = barterRate / (100 + available[buy]); if (choice > bestChoice) { bestChoice = choice; bestToBuy = buy; } } if (bestToBuy !== undefined) { barterers[0].barter(bestToBuy, "food", 100); if (this.Config.debug > 2) API3.warn("Contingency bartering: sold food for " + bestToBuy + " available sell " + available.food + " available buy " + available[bestToBuy] + " barterRate " + getBarterRate(barterPrices, bestToBuy, "food")); return true; } return false; }; m.TradeManager.prototype.checkEvents = function(gameState, events) { // check if one market from a traderoute is renamed, change the route accordingly for (let evt of events.EntityRenamed) { let ent = gameState.getEntityById(evt.newentity); if (!ent || !ent.hasClass("Market")) continue; for (let trader of this.traders.values()) { let route = trader.getMetadata(PlayerID, "route"); if (!route) continue; if (route.source === evt.entity) route.source = evt.newentity; else if (route.target === evt.entity) route.target = evt.newentity; else continue; trader.setMetadata(PlayerID, "route", route); } } // if one market (or market-foundation) is destroyed, we should look for a better route for (let evt of events.Destroy) { if (!evt.entityObj) continue; let ent = evt.entityObj; if (!ent || !ent.hasClass("Market") || !gameState.isPlayerAlly(ent.owner())) continue; - this.routeProspection = true; - gameState.ai.HQ.restartBuild(gameState, "structures/{civ}_market"); - gameState.ai.HQ.restartBuild(gameState, "structures/{civ}_dock"); + this.activateProspection(gameState); return true; } // same thing if one market is built for (let evt of events.Create) { let ent = gameState.getEntityById(evt.entity); if (!ent || ent.foundationProgress() !== undefined || !ent.hasClass("Market") || !gameState.isPlayerAlly(ent.owner())) continue; - this.routeProspection = true; - gameState.ai.HQ.restartBuild(gameState, "structures/{civ}_market"); - gameState.ai.HQ.restartBuild(gameState, "structures/{civ}_dock"); + this.activateProspection(gameState); return true; } // and same thing for captured markets for (let evt of events.OwnershipChanged) { if (!gameState.isPlayerAlly(evt.from) && !gameState.isPlayerAlly(evt.to)) continue; let ent = gameState.getEntityById(evt.entity); if (!ent || ent.foundationProgress() !== undefined || !ent.hasClass("Market")) continue; - this.routeProspection = true; - gameState.ai.HQ.restartBuild(gameState, "structures/{civ}_market"); - gameState.ai.HQ.restartBuild(gameState, "structures/{civ}_dock"); + this.activateProspection(gameState); return true; } // or if diplomacy changed if (events.DiplomacyChanged.length) { - this.routeProspection = true; - gameState.ai.HQ.restartBuild(gameState, "structures/{civ}_market"); - gameState.ai.HQ.restartBuild(gameState, "structures/{civ}_dock"); + this.activateProspection(gameState); return true; } return false; }; +m.TradeManager.prototype.activateProspection = function(gameState) +{ + this.routeProspection = true; + gameState.ai.HQ.buildManager.setBuildable(gameState.applyCiv("structures/{civ}_market")); + gameState.ai.HQ.buildManager.setBuildable(gameState.applyCiv("structures/{civ}_dock")); +}; + /** * fills the best trade route in this.tradeRoute and the best potential route in this.potentialTradeRoute * If an index is given, it returns the best route with this index or the best land route if index is a land index */ m.TradeManager.prototype.checkRoutes = function(gameState, accessIndex) { let market1 = gameState.updatingCollection("OwnMarkets", API3.Filters.byClass("Market"), gameState.getOwnStructures()); let market2 = gameState.updatingCollection("diplo-ExclusiveAllyMarkets", API3.Filters.byClass("Market"), gameState.getExclusiveAllyEntities()); if (market1.length + market2.length < 2) // We have to wait ... markets will be built soon { this.tradeRoute = undefined; this.potentialTradeRoute = undefined; return false; } let onlyOurs = !market2.hasEntities(); if (onlyOurs) market2 = market1; let candidate = { "gain": 0 }; let potential = { "gain": 0 }; let bestIndex = { "gain": 0 }; let bestLand = { "gain": 0 }; let mapSize = gameState.sharedScript.mapSize; let traderTemplatesGains = gameState.getTraderTemplatesGains(); for (let m1 of market1.values()) { if (!m1.position()) continue; let access1 = m.getLandAccess(gameState, m1); let sea1 = m1.hasClass("NavalMarket") ? m.getSeaAccess(gameState, m1) : undefined; for (let m2 of market2.values()) { if (onlyOurs && m1.id() >= m2.id()) continue; if (!m2.position()) continue; let access2 = m.getLandAccess(gameState, m2); let sea2 = m2.hasClass("NavalMarket") ? m.getSeaAccess(gameState, m2) : undefined; let land = access1 == access2 ? access1 : undefined; let sea = sea1 && sea1 == sea2 ? sea1 : undefined; if (!land && !sea) continue; if (land && m.isLineInsideEnemyTerritory(gameState, m1.position(), m2.position())) continue; let gainMultiplier; if (land && traderTemplatesGains.landGainMultiplier) gainMultiplier = traderTemplatesGains.landGainMultiplier; else if (sea && traderTemplatesGains.navalGainMultiplier) gainMultiplier = traderTemplatesGains.navalGainMultiplier; else continue; let gain = Math.round(gainMultiplier * TradeGain(API3.SquareVectorDistance(m1.position(), m2.position()), mapSize)); if (gain < this.minimalGain) continue; if (m1.foundationProgress() === undefined && m2.foundationProgress() === undefined) { if (accessIndex) { if (gameState.ai.accessibility.regionType[accessIndex] === "water" && sea === accessIndex) { if (gain < bestIndex.gain) continue; bestIndex = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea }; } else if (gameState.ai.accessibility.regionType[accessIndex] === "land" && land === accessIndex) { if (gain < bestIndex.gain) continue; bestIndex = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea }; } else if (gameState.ai.accessibility.regionType[accessIndex] === "land") { if (gain < bestLand.gain) continue; bestLand = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea }; } } if (gain < candidate.gain) continue; candidate = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea }; } if (gain < potential.gain) continue; potential = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea }; } } if (potential.gain < 1) this.potentialTradeRoute = undefined; else this.potentialTradeRoute = potential; if (candidate.gain < 1) { if (this.Config.debug > 2) API3.warn("no better trade route possible"); this.tradeRoute = undefined; return false; } if (this.Config.debug > 1 && this.tradeRoute) { if (candidate.gain > this.tradeRoute.gain) API3.warn("one better trade route set with gain " + candidate.gain + " instead of " + this.tradeRoute.gain); } else if (this.Config.debug > 1) API3.warn("one trade route set with gain " + candidate.gain); this.tradeRoute = candidate; if (this.Config.chat) { let owner = this.tradeRoute.source.owner(); if (owner === PlayerID) owner = this.tradeRoute.target.owner(); if (owner !== PlayerID && !this.warnedAllies[owner]) { // Warn an ally that we have a trade route with him m.chatNewTradeRoute(gameState, owner); this.warnedAllies[owner] = true; } } if (accessIndex) { if (bestIndex.gain > 0) return bestIndex; else if (gameState.ai.accessibility.regionType[accessIndex] === "land" && bestLand.gain > 0) return bestLand; return false; } return true; }; /** Called when a market was built or destroyed, and checks if trader orders should be changed */ m.TradeManager.prototype.checkTrader = function(gameState, ent) { let presentRoute = ent.getMetadata(PlayerID, "route"); if (!presentRoute) return; if (!ent.position()) { // This trader is garrisoned, we will decide later (when ungarrisoning) what to do ent.setMetadata(PlayerID, "route", undefined); return; } let access = ent.hasClass("Ship") ? ent.getMetadata(PlayerID, "sea") : gameState.ai.accessibility.getAccessValue(ent.position()); let possibleRoute = this.checkRoutes(gameState, access); // Warning: presentRoute is from metadata, so contains entity ids if (!possibleRoute || possibleRoute.source.id() != presentRoute.source && possibleRoute.source.id() != presentRoute.target || possibleRoute.target.id() != presentRoute.source && possibleRoute.target.id() != presentRoute.target) { // Trader will be assigned in updateTrader ent.setMetadata(PlayerID, "route", undefined); if (!possibleRoute && !ent.hasClass("Ship")) { let closestBase = m.getBestBase(gameState, ent, true); if (closestBase.accessIndex == access) { let closestBasePos = closestBase.anchor.position(); ent.moveToRange(closestBasePos[0], closestBasePos[1], 0, 15); return; } } ent.stopMoving(); } }; m.TradeManager.prototype.prospectForNewMarket = function(gameState, queues) { if (queues.economicBuilding.hasQueuedUnitsWithClass("Market") || queues.dock.hasQueuedUnitsWithClass("Market")) return; if (!gameState.ai.HQ.canBuild(gameState, "structures/{civ}_market")) return; if (!gameState.updatingCollection("OwnMarkets", API3.Filters.byClass("Market"), gameState.getOwnStructures()).hasEntities() && !gameState.updatingCollection("diplo-ExclusiveAllyMarkets", API3.Filters.byClass("Market"), gameState.getExclusiveAllyEntities()).hasEntities()) return; let template = gameState.getTemplate(gameState.applyCiv("structures/{civ}_market")); if (!template) return; this.checkRoutes(gameState); let marketPos = gameState.ai.HQ.findMarketLocation(gameState, template); if (!marketPos || marketPos[3] === 0) // marketPos[3] is the expected gain { // no position found - gameState.ai.HQ.stopBuild(gameState, "structures/{civ}_market"); + gameState.ai.HQ.buildManager.setUnbuildable(gameState, gameState.applyCiv("structures/{civ}_market")); return; } this.routeProspection = false; if (!this.isNewMarketWorth(marketPos[3])) return; // position found, but not enough gain compared to our present route if (this.Config.debug > 1) { if (this.potentialTradeRoute) API3.warn("turn " + gameState.ai.playedTurn + "we could have a new route with gain " + marketPos[3] + " instead of the present " + this.potentialTradeRoute.gain); else API3.warn("turn " + gameState.ai.playedTurn + "we could have a first route with gain " + marketPos[3]); } if (!this.tradeRoute) gameState.ai.queueManager.changePriority("economicBuilding", 2*this.Config.priorities.economicBuilding); let plan = new m.ConstructionPlan(gameState, "structures/{civ}_market"); if (!this.tradeRoute) plan.queueToReset = "economicBuilding"; queues.economicBuilding.addPlan(plan); }; m.TradeManager.prototype.isNewMarketWorth = function(expectedGain) { if (this.potentialTradeRoute && expectedGain < 2*this.potentialTradeRoute.gain && expectedGain < this.potentialTradeRoute.gain + 20) return false; return true; }; m.TradeManager.prototype.update = function(gameState, events, queues) { if (gameState.ai.HQ.canBarter) this.performBarter(gameState); if (this.Config.difficulty <= 1) return; if (this.routeProspection) this.prospectForNewMarket(gameState, queues); if (this.checkEvents(gameState, events)) // true if one market was built or destroyed { this.traders.forEach(ent => { this.checkTrader(gameState, ent); }); this.checkRoutes(gameState); } if (this.tradeRoute) { this.traders.forEach(ent => { this.updateTrader(gameState, ent); }); if (gameState.ai.playedTurn % 5 === 0) this.trainMoreTraders(gameState, queues); if (gameState.ai.playedTurn % 20 === 0 && this.traders.length >= 2) gameState.ai.HQ.researchManager.researchTradeBonus(gameState, queues); if (gameState.ai.playedTurn % 60 === 0) this.setTradingGoods(gameState); } }; m.TradeManager.prototype.routeEntToId = function(route) { if (!route) return route; let ret = {}; for (let key in route) ret[key] = key == "source" || key == "target" ? route[key].id() : route[key]; return ret; }; m.TradeManager.prototype.routeIdToEnt = function(gameState, route) { if (!route) return route; let ret = {}; for (let key in route) ret[key] = key == "source" || key == "target" ? gameState.getEntityById(route[key]) : route[key]; return ret; }; m.TradeManager.prototype.Serialize = function() { return { "tradeRoute": this.routeEntToId(this.tradeRoute), "potentialTradeRoute": this.routeEntToId(this.potentialTradeRoute), "routeProspection": this.routeProspection, "targetNumTraders": this.targetNumTraders, "warnedAllies": this.warnedAllies }; }; m.TradeManager.prototype.Deserialize = function(gameState, data) { for (let key in data) { if (key == "tradeRoute" || key == "potentialTradeRoute") this[key] = this.routeIdToEnt(gameState, data[key]); else this[key] = data[key]; } }; return m; }(PETRA);