Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/map-module.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/map-module.js (revision 19512) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/map-module.js (revision 19513) @@ -1,460 +1,460 @@ var API3 = function(m) { /** * The map module. * Copied with changes from QuantumState's original for qBot, it's a component for storing 8 bit values. */ /** The function needs to be named too because of the copyConstructor functionality */ m.Map = function Map(sharedScript, type, originalMap, actualCopy) { // get the correct dimensions according to the map type - let map = (type === "territory" || type === "resource") ? sharedScript.territoryMap : sharedScript.passabilityMap; + let map = type === "territory" || type === "resource" ? sharedScript.territoryMap : sharedScript.passabilityMap; this.width = map.width; this.height = map.height; this.cellSize = map.cellSize; this.length = this.width * this.height; this.maxVal = 255; // sanity check if (originalMap && originalMap.length !== this.length) warn("AI map size incompatibility with type " + type + ": original " + originalMap.length + " new " + this.length); if (originalMap && actualCopy) { this.map = new Uint8Array(this.length); for (let i = 0; i < this.length; ++i) this.map[i] = originalMap[i]; } else if (originalMap) this.map = originalMap; else this.map = new Uint8Array(this.length); }; m.Map.prototype.setMaxVal = function(val) { this.maxVal = val; }; m.Map.prototype.gamePosToMapPos = function(p) { return [Math.floor(p[0]/this.cellSize), Math.floor(p[1]/this.cellSize)]; }; m.Map.prototype.point = function(p) { let q = this.gamePosToMapPos(p); q[0] = q[0] >= this.width ? this.width-1 : (q[0] < 0 ? 0 : q[0]); q[1] = q[1] >= this.width ? this.width-1 : (q[1] < 0 ? 0 : q[1]); return this.map[q[0] + this.width * q[1]]; }; m.Map.prototype.addInfluence = function(cx, cy, maxDist, strength, type = "linear") { strength = strength ? strength : maxDist; let x0 = Math.floor(Math.max(0, cx - maxDist)); let y0 = Math.floor(Math.max(0, cy - maxDist)); let x1 = Math.floor(Math.min(this.width-1, cx + maxDist)); let y1 = Math.floor(Math.min(this.height-1, cy + maxDist)); let maxDist2 = maxDist * maxDist; // code duplicating for speed if (type === "linear") { let str = strength / maxDist; for (let y = y0; y < y1; ++y) { let dy = y - cy; for (let x = x0; x < x1; ++x) { let dx = x - cx; let r2 = dx*dx + dy*dy; if (r2 >= maxDist2) continue; let quant = str * (maxDist - Math.sqrt(r2)); let w = x + y * this.width; if (this.map[w] + quant < 0) this.map[w] = 0; else if (this.map[w] + quant > this.maxVal) this.map[w] = this.maxVal; // avoids overflow. else this.map[w] += quant; } } } else if (type === "quadratic") { let str = strength / maxDist2; for (let y = y0; y < y1; ++y) { let dy = y - cy; for (let x = x0; x < x1; ++x) { let dx = x - cx; let r2 = dx*dx + dy*dy; if (r2 >= maxDist2) continue; let quant = str * (maxDist2 - r2); let w = x + y * this.width; if (this.map[w] + quant < 0) this.map[w] = 0; else if (this.map[w] + quant > this.maxVal) this.map[w] = this.maxVal; // avoids overflow. else this.map[w] += quant; } } } else { for (let y = y0; y < y1; ++y) { let dy = y - cy; for (let x = x0; x < x1; ++x) { let dx = x - cx; let r2 = dx*dx + dy*dy; if (r2 >= maxDist2) continue; let w = x + y * this.width; if (this.map[w] + strength < 0) this.map[w] = 0; else if (this.map[w] + strength > this.maxVal) this.map[w] = this.maxVal; // avoids overflow. else this.map[w] += strength; } } } }; m.Map.prototype.multiplyInfluence = function(cx, cy, maxDist, strength, type = "constant") { strength = strength ? +strength : +maxDist; let x0 = Math.max(0, cx - maxDist); let y0 = Math.max(0, cy - maxDist); let x1 = Math.min(this.width, cx + maxDist); let y1 = Math.min(this.height, cy + maxDist); let maxDist2 = maxDist * maxDist; if (type === "linear") { let str = strength / maxDist; for (let y = y0; y < y1; ++y) { for (let x = x0; x < x1; ++x) { let dx = x - cx; let dy = y - cy; let r2 = dx*dx + dy*dy; if (r2 >= maxDist2) continue; let quant = str * (maxDist - Math.sqrt(r2)) * this.map[x + y * this.width]; if (quant < 0) this.map[x + y * this.width] = 0; else if (quant > this.maxVal) this.map[x + y * this.width] = this.maxVal; else this.map[x + y * this.width] = quant; } } } else if (type === "quadratic") { let str = strength / maxDist2; for (let y = y0; y < y1; ++y) { for (let x = x0; x < x1; ++x) { let dx = x - cx; let dy = y - cy; let r2 = dx*dx + dy*dy; if (r2 >= maxDist2) continue; let quant = str * (maxDist2 - r2) * this.map[x + y * this.width]; if (quant < 0) this.map[x + y * this.width] = 0; else if (quant > this.maxVal) this.map[x + y * this.width] = this.maxVal; else this.map[x + y * this.width] = quant; } } } else { for (let y = y0; y < y1; ++y) { for (let x = x0; x < x1; ++x) { let dx = x - cx; let dy = y - cy; let r2 = dx*dx + dy*dy; if (r2 >= maxDist2) continue; let quant = this.map[x + y * this.width] * strength; if (quant < 0) this.map[x + y * this.width] = 0; else if (quant > this.maxVal) this.map[x + y * this.width] = this.maxVal; else this.map[x + y * this.width] = quant; } } } }; /** doesn't check for overflow. */ m.Map.prototype.setInfluence = function(cx, cy, maxDist, value = 0) { let x0 = Math.max(0, cx - maxDist); let y0 = Math.max(0, cy - maxDist); let x1 = Math.min(this.width, cx + maxDist); let y1 = Math.min(this.height, cy + maxDist); let maxDist2 = maxDist * maxDist; for (let y = y0; y < y1; ++y) { for (let x = x0; x < x1; ++x) { let dx = x - cx; let dy = y - cy; if (dx*dx + dy*dy < maxDist2) this.map[x + y * this.width] = value; } } }; m.Map.prototype.sumInfluence = function(cx, cy, radius) { let x0 = Math.max(0, cx - radius); let y0 = Math.max(0, cy - radius); let x1 = Math.min(this.width, cx + radius); let y1 = Math.min(this.height, cy + radius); let radius2 = radius * radius; let sum = 0; for (let y = y0; y < y1; ++y) { for (let x = x0; x < x1; ++x) { let dx = x - cx; let dy = y - cy; if (dx*dx + dy*dy < radius2) sum += this.map[x + y * this.width]; } } return sum; }; /** * Make each cell's 16-bit/8-bit value at least one greater than each of its * neighbours' values. (If the grid is initialised with 0s and 65535s or 255s, the * result of each cell is its Manhattan distance to the nearest 0.) */ m.Map.prototype.expandInfluences = function(maximum = this.maxVal, grid = this.map) { let w = this.width; let h = this.height; for (let y = 0; y < h; ++y) { let min = maximum; let x0 = y * w; for (let x = 0; x < w; ++x) { let g = grid[x + x0]; if (g > min) grid[x + x0] = min; else if (g < min) min = g; ++min; if (min > maximum) min = maximum; } for (let x = w - 2; x >= 0; --x) { let g = grid[x + x0]; if (g > min) grid[x + x0] = min; else if (g < min) min = g; ++min; if (min > maximum) min = maximum; } } for (let x = 0; x < w; ++x) { let min = maximum; for (let y = 0; y < h; ++y) { let g = grid[x + y * w]; if (g > min) grid[x + y * w] = min; else if (g < min) min = g; ++min; if (min > maximum) min = maximum; } for (let y = h - 2; y >= 0; --y) { let g = grid[x + y * w]; if (g > min) grid[x + y * w] = min; else if (g < min) min = g; ++min; if (min > maximum) min = maximum; } } }; /** Multiplies current map by the parameter map pixelwise */ m.Map.prototype.multiply = function(map, onlyBetter, divider, maxMultiplier) { for (let i = 0; i < this.length; ++i) if (map.map[i]/divider > 1) this.map[i] = Math.min(maxMultiplier*this.map[i], this.map[i] * (map.map[i]/divider)); }; /** add to current map by the parameter map pixelwise */ m.Map.prototype.add = function(map) { for (let i = 0; i < this.length; ++i) { if (this.map[i] + map.map[i] < 0) this.map[i] = 0; else if (this.map[i] + map.map[i] > this.maxVal) this.map[i] = this.maxVal; else this.map[i] += map.map[i]; } }; /** Find the best non-obstructed tile */ m.Map.prototype.findBestTile = function(radius, obstruction) { let bestIdx; let bestVal = -1; for (let j = 0; j < this.length; ++j) { if (this.map[j] <= bestVal) continue; let i = this.getNonObstructedTile(j, radius, obstruction); if (i < 0) continue; bestVal = this.map[j]; bestIdx = i; } return [bestIdx, bestVal]; }; /** return any non obstructed (small) tile inside the (big) tile i from obstruction map */ m.Map.prototype.getNonObstructedTile = function(i, radius, obstruction) { let ratio = this.cellSize / obstruction.cellSize; let ix = (i % this.width) * ratio; let iy = Math.floor(i / this.width) * ratio; let w = obstruction.width; for (let kx = ix; kx < ix + ratio; ++kx) { if (kx < radius || kx >= w - radius) continue; for (let ky = iy; ky < iy + ratio; ++ky) { if (ky < radius || ky >= w - radius) continue; if (obstruction.isObstructedTile(kx, ky, radius)) continue; return kx + ky*w; } } return -1; }; /** return true if the area centered on tile kx-ky and with radius is obstructed */ m.Map.prototype.isObstructedTile = function(kx, ky, radius) { let w = this.width; if (kx < radius || kx >= w - radius || ky < radius || ky >= w - radius || this.map[kx+ky*w] === 0) return true; for (let dy = 0; dy <= radius; ++dy) { let dxmax = dy === 0 ? radius : Math.ceil(Math.sqrt(radius*radius - (dy-0.5)*(dy-0.5))); let xp = kx + (ky + dy)*w; let xm = kx + (ky - dy)*w; for (let dx = -dxmax; dx <= dxmax; ++dx) if (this.map[xp + dx] === 0 || this.map[xm + dx] === 0) return true; } return false; }; /** * returns the nearest obstructed point * TODO check that the landpassmap index is the same */ m.Map.prototype.findNearestObstructed = function(k, radius) { let w = this.width; let ix = k % w; let iy = Math.floor(k / w); let n = this.cellSize > 8 ? 1 : Math.floor(8 / this.cellSize); for (let i = 1; i <= n; ++i) { let kx = ix - i; let ky = iy + i; for (let j = 1; j <= 8*i; ++j) { if (this.isObstructedTile(kx, ky, radius)) { let akx = Math.abs(kx-ix); let aky = Math.abs(ky-iy); if (akx >= aky) { if (kx > ix) --kx; else ++kx; } if (aky >= akx) { if (ky > iy) --ky; else ++ky; } return kx + w*ky; } if (j < 2*i+1) ++kx; else if (j < 4*i+1) --ky; else if (j < 6*i+1) --kx; else ++ky; } } return -1; }; m.Map.prototype.dumpIm = function(name = "default.png", threshold = this.maxVal) { Engine.DumpImage(name, this.map, this.width, this.height, threshold); }; return m; }(API3); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackManager.js (revision 19512) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackManager.js (revision 19513) @@ -1,567 +1,567 @@ var PETRA = function(m) { /** Attack Manager */ m.AttackManager = function(Config) { this.Config = Config; this.totalNumber = 0; this.attackNumber = 0; this.rushNumber = 0; this.raidNumber = 0; this.upcomingAttacks = { "Rush": [], "Raid": [], "Attack": [], "HugeAttack": [] }; this.startedAttacks = { "Rush": [], "Raid": [], "Attack": [], "HugeAttack": [] }; this.debugTime = 0; this.maxRushes = 0; this.rushSize = []; this.currentEnemyPlayer = undefined; // enemy player we are currently targeting this.defeated = {}; }; /** More initialisation for stuff that needs the gameState */ m.AttackManager.prototype.init = function(gameState) { this.outOfPlan = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "plan", -1)); this.outOfPlan.registerUpdates(); }; m.AttackManager.prototype.setRushes = function(allowed) { if (this.Config.personality.aggressive > 0.8 && allowed > 2) { this.maxRushes = 3; this.rushSize = [ 16, 20, 24 ]; } else if (this.Config.personality.aggressive > 0.6 && allowed > 1) { this.maxRushes = 2; this.rushSize = [ 18, 22 ]; } else if (this.Config.personality.aggressive > 0.3 && allowed > 0) { this.maxRushes = 1; this.rushSize = [ 20 ]; } }; m.AttackManager.prototype.checkEvents = function(gameState, events) { for (let evt of events.PlayerDefeated) this.defeated[evt.playerId] = true; let answer = "decline"; let other; let targetPlayer; for (let evt of events.AttackRequest) { if (evt.source === PlayerID || !gameState.isPlayerAlly(evt.source) || !gameState.isPlayerEnemy(evt.player)) continue; targetPlayer = evt.player; let available = 0; for (let attackType in this.upcomingAttacks) { for (let attack of this.upcomingAttacks[attackType]) { if (attack.state === "completing") { if (attack.targetPlayer === targetPlayer) available += attack.unitCollection.length; else if (attack.targetPlayer !== undefined && attack.targetPlayer !== targetPlayer) other = attack.targetPlayer; continue; } attack.targetPlayer = targetPlayer; if (attack.unitCollection.length > 2) available += attack.unitCollection.length; } } if (available > 12) // launch the attack immediately { for (let attackType in this.upcomingAttacks) { for (let attack of this.upcomingAttacks[attackType]) { if (attack.state === "completing" || attack.targetPlayer !== targetPlayer || attack.unitCollection.length < 3) continue; attack.forceStart(); attack.requested = true; } } answer = "join"; } else if (other !== undefined) answer = "other"; break; // take only the first attack request into account } if (targetPlayer !== undefined) m.chatAnswerRequestAttack(gameState, targetPlayer, answer, other); }; /** * Some functions are run every turn * Others once in a while */ m.AttackManager.prototype.update = function(gameState, queues, events) { if (this.Config.debug > 2 && gameState.ai.elapsedTime > this.debugTime + 60) { this.debugTime = gameState.ai.elapsedTime; API3.warn(" upcoming attacks ================="); for (let attackType in this.upcomingAttacks) for (let attack of this.upcomingAttacks[attackType]) API3.warn(" plan " + attack.name + " type " + attackType + " state " + attack.state + " units " + attack.unitCollection.length); API3.warn(" started attacks =================="); for (let attackType in this.startedAttacks) for (let attack of this.startedAttacks[attackType]) API3.warn(" plan " + attack.name + " type " + attackType + " state " + attack.state + " units " + attack.unitCollection.length); API3.warn(" =================================="); } this.checkEvents(gameState, events); let unexecutedAttacks = {"Rush": 0, "Raid": 0, "Attack": 0, "HugeAttack": 0}; for (let attackType in this.upcomingAttacks) { for (let i = 0; i < this.upcomingAttacks[attackType].length; ++i) { let attack = this.upcomingAttacks[attackType][i]; attack.checkEvents(gameState, events); if (attack.isStarted()) API3.warn("Petra problem in attackManager: attack in preparation has already started ???"); let updateStep = attack.updatePreparation(gameState); // now we're gonna check if the preparation time is over if (updateStep === 1 || attack.isPaused() ) { // just chillin' if (attack.state === "unexecuted") ++unexecutedAttacks[attackType]; } else if (updateStep === 0) { if (this.Config.debug > 1) API3.warn("Attack Manager: " + attack.getType() + " plan " + attack.getName() + " aborted."); attack.Abort(gameState); this.upcomingAttacks[attackType].splice(i--,1); } else if (updateStep === 2) { if (attack.StartAttack(gameState)) { if (this.Config.debug > 1) API3.warn("Attack Manager: Starting " + attack.getType() + " plan " + attack.getName()); if (this.Config.chat) m.chatLaunchAttack(gameState, attack.targetPlayer, attack.getType()); this.startedAttacks[attackType].push(attack); } else attack.Abort(gameState); this.upcomingAttacks[attackType].splice(i--,1); } } } for (let attackType in this.startedAttacks) { for (let i = 0; i < this.startedAttacks[attackType].length; ++i) { let attack = this.startedAttacks[attackType][i]; attack.checkEvents(gameState, events); // okay so then we'll update the attack. if (attack.isPaused()) continue; let remaining = attack.update(gameState, events); if (!remaining) { if (this.Config.debug > 1) API3.warn("Military Manager: " + attack.getType() + " plan " + attack.getName() + " is finished with remaining " + remaining); attack.Abort(gameState); this.startedAttacks[attackType].splice(i--,1); } } } // creating plans after updating because an aborted plan might be reused in that case. let barracksNb = gameState.getOwnEntitiesByClass("Barracks", true).filter(API3.Filters.isBuilt()).length; if (this.rushNumber < this.maxRushes && barracksNb >= 1) { if (unexecutedAttacks.Rush === 0) { // we have a barracks and we want to rush, rush. let data = { "targetSize": this.rushSize[this.rushNumber] }; let attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, "Rush", data); if (!attackPlan.failed) { if (this.Config.debug > 1) API3.warn("Headquarters: Rushing plan " + this.totalNumber + " with maxRushes " + this.maxRushes); this.totalNumber++; attackPlan.init(gameState); this.upcomingAttacks.Rush.push(attackPlan); } this.rushNumber++; } } else if (unexecutedAttacks.Attack === 0 && unexecutedAttacks.HugeAttack === 0 && this.startedAttacks.Attack.length + this.startedAttacks.HugeAttack.length < Math.min(2, 1 + Math.round(gameState.getPopulationMax()/100))) { if ((barracksNb >= 1 && (gameState.currentPhase() > 1 || gameState.isResearching(gameState.townPhase()))) || !gameState.ai.HQ.baseManagers[1]) // if we have no base ... nothing else to do than attack { - let type = (this.attackNumber < 2 || this.startedAttacks.HugeAttack.length > 0) ? "Attack" : "HugeAttack"; + let type = this.attackNumber < 2 || this.startedAttacks.HugeAttack.length > 0 ? "Attack" : "HugeAttack"; let attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, type); if (attackPlan.failed) this.attackPlansEncounteredWater = true; // hack else { if (this.Config.debug > 1) API3.warn("Military Manager: Creating the plan " + type + " " + this.totalNumber); this.totalNumber++; attackPlan.init(gameState); this.upcomingAttacks[type].push(attackPlan); } this.attackNumber++; } } if (unexecutedAttacks.Raid === 0 && gameState.ai.HQ.defenseManager.targetList.length) { let target; for (let targetId of gameState.ai.HQ.defenseManager.targetList) { target = gameState.getEntityById(targetId); if (!target) continue; if (gameState.isPlayerEnemy(target.owner())) break; target = undefined; } if (target) { // prepare a raid against this target let data = { "target": target }; let attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, "Raid", data); if (!attackPlan.failed) { if (this.Config.debug > 1) API3.warn("Headquarters: Raiding plan " + this.totalNumber); this.totalNumber++; attackPlan.init(gameState); this.upcomingAttacks.Raid.push(attackPlan); } this.raidNumber++; } } }; m.AttackManager.prototype.getPlan = function(planName) { for (let attackType in this.upcomingAttacks) { for (let attack of this.upcomingAttacks[attackType]) if (attack.getName() == planName) return attack; } for (let attackType in this.startedAttacks) { for (let attack of this.startedAttacks[attackType]) if (attack.getName() == planName) return attack; } return undefined; }; m.AttackManager.prototype.pausePlan = function(planName) { let attack = this.getPlan(planName); if (attack) attack.setPaused(true); }; m.AttackManager.prototype.unpausePlan = function(planName) { let attack = this.getPlan(planName); if (attack) attack.setPaused(false); }; m.AttackManager.prototype.pauseAllPlans = function() { for (let attackType in this.upcomingAttacks) for (let attack of this.upcomingAttacks[attackType]) attack.setPaused(true); for (let attackType in this.startedAttacks) for (let attack of this.startedAttacks[attackType]) attack.setPaused(true); }; m.AttackManager.prototype.unpauseAllPlans = function() { for (let attackType in this.upcomingAttacks) for (let attack of this.upcomingAttacks[attackType]) attack.setPaused(false); for (let attackType in this.startedAttacks) for (let attack of this.startedAttacks[attackType]) attack.setPaused(false); }; m.AttackManager.prototype.getAttackInPreparation = function(type) { if (!this.upcomingAttacks[type].length) return undefined; return this.upcomingAttacks[type][0]; }; /** * determine which player should be attacked: when called when starting the attack, * attack.targetPlayer is undefined and in that case, we keep track of the chosen target * for future attacks. */ m.AttackManager.prototype.getEnemyPlayer = function(gameState, attack) { let enemyPlayer; // first check if there is a preferred enemy based on our victory conditions if (gameState.getGameType() === "wonder") { let moreAdvanced; let enemyWonder; let wonders = gameState.getEnemyStructures().filter(API3.Filters.byClass("Wonder")); for (let wonder of wonders.values()) { if (wonder.owner() === 0) continue; let progress = wonder.foundationProgress(); if (progress === undefined) { enemyWonder = wonder; break; } if (enemyWonder && moreAdvanced > progress) continue; enemyWonder = wonder; moreAdvanced = progress; } if (enemyWonder) { enemyPlayer = enemyWonder.owner(); if (attack.targetPlayer === undefined) this.currentEnemyPlayer = enemyPlayer; return enemyPlayer; } } else if (gameState.getGameType() === "capture_the_relic") { // Target the player with the most relics let maxRelicsOwned = 0; // TODO: target gaia relics for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) { if (!gameState.isPlayerEnemy(i) || this.defeated[i]) continue; let relicsCount = gameState.getEnemyUnits(i).filter(API3.Filters.byClass("Relic")).length; if (relicsCount <= maxRelicsOwned) continue; maxRelicsOwned = relicsCount; enemyPlayer = i; } if (enemyPlayer) { if (attack.targetPlayer === undefined) this.currentEnemyPlayer = enemyPlayer; return enemyPlayer; } } let veto = {}; for (let i in this.defeated) veto[i] = true; // No rush if enemy too well defended (i.e. iberians) if (attack.type === "Rush") { for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) { if (!gameState.isPlayerEnemy(i) || veto[i]) continue; if (this.defeated[i]) continue; let enemyDefense = 0; for (let ent of gameState.getEnemyStructures(i).values()) if (ent.hasClass("Tower") || ent.hasClass("Fortress")) enemyDefense++; if (enemyDefense > 6) veto[i] = true; } } // then if not a huge attack, continue attacking our previous target as long as it has some entities, // otherwise target the most accessible one if (attack.type !== "HugeAttack") { if (attack.targetPlayer === undefined && this.currentEnemyPlayer !== undefined && !this.defeated[this.currentEnemyPlayer] && gameState.isPlayerEnemy(this.currentEnemyPlayer) && gameState.getEntities(this.currentEnemyPlayer).hasEntities()) return this.currentEnemyPlayer; let distmin; let ccmin; let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); for (let ourcc of ccEnts.values()) { if (ourcc.owner() !== PlayerID) continue; let ourPos = ourcc.position(); let access = gameState.ai.accessibility.getAccessValue(ourPos); for (let enemycc of ccEnts.values()) { if (veto[enemycc.owner()]) continue; if (!gameState.isPlayerEnemy(enemycc.owner())) continue; let enemyPos = enemycc.position(); if (access !== gameState.ai.accessibility.getAccessValue(enemyPos)) continue; let dist = API3.SquareVectorDistance(ourPos, enemyPos); if (distmin && dist > distmin) continue; ccmin = enemycc; distmin = dist; } } if (ccmin) { enemyPlayer = ccmin.owner(); if (attack.targetPlayer === undefined) this.currentEnemyPlayer = enemyPlayer; return enemyPlayer; } } // then let's target our strongest enemy (basically counting enemies units) // with priority to enemies with civ center let max = 0; for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) { if (veto[i]) continue; if (!gameState.isPlayerEnemy(i)) continue; let enemyCount = 0; let enemyCivCentre = false; for (let ent of gameState.getEntities(i).values()) { enemyCount++; if (ent.hasClass("CivCentre")) enemyCivCentre = true; } if (enemyCivCentre) enemyCount += 500; if (enemyCount < max) continue; max = enemyCount; enemyPlayer = i; } if (attack.targetPlayer === undefined) this.currentEnemyPlayer = enemyPlayer; return enemyPlayer; }; /** f.e. if we have changed diplomacy with another player. */ m.AttackManager.prototype.cancelAttacksAgainstPlayer = function(player) { for (let attackType in this.upcomingAttacks) for (let attack of this.upcomingAttacks[attackType]) if (attack.targetPlayer === player) attack.targetPlayer = undefined; for (let attackType in this.startedAttacks) for (let i = 0; i < this.startedAttacks[attackType].length; ++i) { let attack = this.startedAttacks[attackType][i]; if (attack.targetPlayer === player) { attack.Abort(gameState); this.startedAttacks[attackType].splice(i--, 1); } } }; m.AttackManager.prototype.Serialize = function() { let properties = { "totalNumber": this.totalNumber, "attackNumber": this.attackNumber, "rushNumber": this.rushNumber, "raidNumber": this.raidNumber, "debugTime": this.debugTime, "maxRushes": this.maxRushes, "rushSize": this.rushSize, "currentEnemyPlayer": this.currentEnemyPlayer, "defeated": this.defeated }; let upcomingAttacks = {}; for (let key in this.upcomingAttacks) { upcomingAttacks[key] = []; for (let attack of this.upcomingAttacks[key]) upcomingAttacks[key].push(attack.Serialize()); } let startedAttacks = {}; for (let key in this.startedAttacks) { startedAttacks[key] = []; for (let attack of this.startedAttacks[key]) startedAttacks[key].push(attack.Serialize()); } return { "properties": properties, "upcomingAttacks": upcomingAttacks, "startedAttacks": startedAttacks }; }; m.AttackManager.prototype.Deserialize = function(gameState, data) { for (let key in data.properties) this[key] = data.properties[key]; this.upcomingAttacks = {}; for (let key in data.upcomingAttacks) { this.upcomingAttacks[key] = []; for (let dataAttack of data.upcomingAttacks[key]) { let attack = new m.AttackPlan(gameState, this.Config, dataAttack.properties.name); attack.Deserialize(gameState, dataAttack); attack.init(gameState); this.upcomingAttacks[key].push(attack); } } this.startedAttacks = {}; for (let key in data.startedAttacks) { this.startedAttacks[key] = []; for (let dataAttack of data.startedAttacks[key]) { let attack = new m.AttackPlan(gameState, this.Config, dataAttack.properties.name); attack.Deserialize(gameState, dataAttack); attack.init(gameState); this.startedAttacks[key].push(attack); } } }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js (revision 19512) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js (revision 19513) @@ -1,1996 +1,1996 @@ var PETRA = function(m) { /** * This is an attack plan: * It deals with everything in an attack, from picking a target to picking a path to it * To making sure units are built, and pushing elements to the queue manager otherwise * It also handles the actual attack, though much work is needed on that. */ m.AttackPlan = function(gameState, Config, uniqueID, type, data) { this.Config = Config; this.name = uniqueID; this.type = type || "Attack"; this.state = "unexecuted"; if (data && data.target) { this.target = data.target; this.targetPos = this.target.position(); this.targetPlayer = this.target.owner(); } else { this.target = undefined; this.targetPos = undefined; this.targetPlayer = undefined; } // get a starting rallyPoint ... will be improved later let rallyPoint; let rallyAccess; let allAccesses = {}; for (let base of gameState.ai.HQ.baseManagers) { if (!base.anchor || !base.anchor.position()) continue; let access = gameState.ai.accessibility.getAccessValue(base.anchor.position()); if (!rallyPoint) { rallyPoint = base.anchor.position(); rallyAccess = access; } if (!allAccesses[access]) allAccesses[access] = base.anchor.position(); } if (!rallyPoint) // no base ? take the position of any of our entities { for (let ent of gameState.getOwnEntities().values()) { if (!ent.position()) continue; let access = gameState.ai.accessibility.getAccessValue(ent.position()); rallyPoint = ent.position(); rallyAccess = access; allAccesses[access] = rallyPoint; break; } if (!rallyPoint) { this.failed = true; return false; } } this.rallyPoint = rallyPoint; this.overseas = 0; if (gameState.ai.HQ.navalMap) { for (let structure of gameState.getEnemyStructures().values()) { if (!structure.position()) continue; let access = gameState.ai.accessibility.getAccessValue(structure.position()); if (access in allAccesses) { this.overseas = 0; this.rallyPoint = allAccesses[access]; break; } else if (!this.overseas) { let sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyAccess, access); if (!sea) continue; this.overseas = sea; gameState.ai.HQ.navalManager.setMinimalTransportShips(gameState, sea, 1); } } } this.paused = false; this.maxCompletingTime = 0; // priority of the queues we'll create. let priority = 70; // unitStat priority is relative. If all are 0, the only relevant criteria is "currentsize/targetsize". // if not, this is a "bonus". The higher the priority, the faster this unit will get built. // Should really be clamped to [0.1-1.5] (assuming 1 is default/the norm) // Eg: if all are priority 1, and the siege is 0.5, the siege units will get built // only once every other category is at least 50% of its target size. // note: siege build order is currently added by the military manager if a fortress is there. this.unitStat = {}; // neededShips is the minimal number of ships which should be available for transport if (type === "Rush") { priority = 250; this.unitStat.Infantry = { "priority": 1, "minSize": 10, "targetSize": 20, "batchSize": 2, "classes": ["Infantry"], "interests": [ ["strength",1], ["cost",1], ["costsResource", 0.5, "stone"], ["costsResource", 0.6, "metal"] ] }; this.unitStat.Cavalry = { "priority": 1, "minSize": 2, "targetSize": 4, "batchSize": 2, "classes": ["Cavalry", "CitizenSoldier"], "interests": [ ["strength",1], ["cost",1] ] }; if (data && data.targetSize) this.unitStat.Infantry.targetSize = data.targetSize; this.neededShips = 1; } else if (type === "Raid") { priority = 150; this.unitStat.Cavalry = { "priority": 1, "minSize": 3, "targetSize": 4, "batchSize": 2, "classes": ["Cavalry", "CitizenSoldier"], "interests": [ ["strength",1], ["cost",1] ] }; this.neededShips = 1; } else if (type === "HugeAttack") { priority = 90; // basically we want a mix of citizen soldiers so our barracks have a purpose, and champion units. this.unitStat.RangedInfantry = { "priority": 0.7, "minSize": 5, "targetSize": 20, "batchSize": 5, "classes": ["Infantry", "Ranged", "CitizenSoldier"], "interests": [["strength",3], ["cost",1] ] }; this.unitStat.MeleeInfantry = { "priority": 0.7, "minSize": 5, "targetSize": 20, "batchSize": 5, "classes": ["Infantry", "Melee", "CitizenSoldier"], "interests": [ ["strength",3], ["cost",1] ] }; this.unitStat.ChampRangedInfantry = { "priority": 1, "minSize": 3, "targetSize": 18, "batchSize": 3, "classes": ["Infantry", "Ranged", "Champion"], "interests": [["strength",3], ["cost",1] ] }; this.unitStat.ChampMeleeInfantry = { "priority": 1, "minSize": 3, "targetSize": 18, "batchSize": 3, "classes": ["Infantry", "Melee", "Champion"], "interests": [ ["strength",3], ["cost",1] ] }; this.unitStat.RangedCavalry = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["Cavalry", "Ranged", "CitizenSoldier"], "interests": [ ["strength",2], ["cost",1] ] }; this.unitStat.MeleeCavalry = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["Cavalry", "Melee", "CitizenSoldier"], "interests": [ ["strength",2], ["cost",1] ] }; this.unitStat.ChampRangedCavalry = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["Cavalry", "Ranged", "Champion"], "interests": [ ["strength",3], ["cost",1] ] }; this.unitStat.ChampMeleeCavalry = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["Cavalry", "Melee", "Champion"], "interests": [ ["strength",2], ["cost",1] ] }; this.unitStat.Hero = { "priority": 1, "minSize": 0, "targetSize": 1, "batchSize": 1, "classes": ["Hero"], "interests": [ ["strength",2], ["cost",1] ] }; this.neededShips = 5; } else { priority = 70; this.unitStat.RangedInfantry = { "priority": 1, "minSize": 6, "targetSize": 16, "batchSize": 3, "classes": ["Infantry","Ranged"], "interests": [ ["canGather", 1], ["strength",1.6], ["cost",1.5], ["costsResource", 0.3, "stone"], ["costsResource", 0.3, "metal"] ] }; this.unitStat.MeleeInfantry = { "priority": 1, "minSize": 6, "targetSize": 16, "batchSize": 3, "classes": ["Infantry","Melee"], "interests": [ ["canGather", 1], ["strength",1.6], ["cost",1.5], ["costsResource", 0.3, "stone"], ["costsResource", 0.3, "metal"] ] }; this.unitStat.Cavalry = { "priority": 1, "minSize": 2, "targetSize": 6, "batchSize": 2, "classes": ["Cavalry", "CitizenSoldier"], "interests": [ ["strength",1], ["cost",1] ] }; this.neededShips = 3; } // Put some randomness on the attack size let variation = randFloat(0.8, 1.2); // and lower priority and smaller sizes for easier difficulty levels if (this.Config.difficulty < 2) { priority *= 0.6; variation *= 0.6; } else if (this.Config.difficulty < 3) { priority *= 0.8; variation *= 0.8; } for (let cat in this.unitStat) { this.unitStat[cat].targetSize = Math.round(variation * this.unitStat[cat].targetSize); this.unitStat[cat].minSize = Math.min(this.unitStat[cat].minSize, this.unitStat[cat].targetSize); } // change the sizes according to max population this.neededShips = Math.ceil(this.Config.popScaling * this.neededShips); for (let cat in this.unitStat) { this.unitStat[cat].targetSize = Math.round(this.Config.popScaling * this.unitStat[cat].targetSize); this.unitStat[cat].minSize = Math.floor(this.Config.popScaling * this.unitStat[cat].minSize); } // TODO: there should probably be one queue per type of training building gameState.ai.queueManager.addQueue("plan_" + this.name, priority); gameState.ai.queueManager.addQueue("plan_" + this.name +"_champ", priority+1); gameState.ai.queueManager.addQueue("plan_" + this.name +"_siege", priority); // each array is [ratio, [associated classes], associated EntityColl, associated unitStat, name ] this.buildOrder = []; this.canBuildUnits = gameState.ai.HQ.canBuildUnits; // some variables used during the attack this.position5TurnsAgo = [0,0]; this.lastPosition = [0,0]; this.position = [0,0]; this.isBlocked = false; // true when this attack faces walls return true; }; m.AttackPlan.prototype.init = function(gameState) { this.queue = gameState.ai.queues["plan_" + this.name]; this.queueChamp = gameState.ai.queues["plan_" + this.name +"_champ"]; this.queueSiege = gameState.ai.queues["plan_" + this.name +"_siege"]; this.unitCollection = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "plan", this.name)); this.unitCollection.registerUpdates(); this.unit = {}; // defining the entity collections. Will look for units I own, that are part of this plan. // Also defining the buildOrders. for (let cat in this.unitStat) { let Unit = this.unitStat[cat]; this.unit[cat] = this.unitCollection.filter(API3.Filters.byClassesAnd(Unit.classes)); this.unit[cat].registerUpdates(); if (this.canBuildUnits) this.buildOrder.push([0, Unit.classes, this.unit[cat], Unit, cat]); } }; m.AttackPlan.prototype.getName = function() { return this.name; }; m.AttackPlan.prototype.getType = function() { return this.type; }; m.AttackPlan.prototype.isStarted = function() { return this.state !== "unexecuted" && this.state !== "completing"; }; m.AttackPlan.prototype.isPaused = function() { return this.paused; }; m.AttackPlan.prototype.setPaused = function(boolValue) { this.paused = boolValue; }; /** * Returns true if the attack can be executed at the current time * Basically it checks we have enough units. */ m.AttackPlan.prototype.canStart = function() { if (!this.canBuildUnits) return true; for (let unitCat in this.unitStat) if (this.unit[unitCat].length < this.unitStat[unitCat].minSize) return false; return true; }; m.AttackPlan.prototype.mustStart = function() { if (this.isPaused()) return false; if (!this.canBuildUnits) return this.unitCollection.hasEntities(); let MaxReachedEverywhere = true; let MinReachedEverywhere = true; for (let unitCat in this.unitStat) { let Unit = this.unitStat[unitCat]; if (this.unit[unitCat].length < Unit.targetSize) MaxReachedEverywhere = false; if (this.unit[unitCat].length < Unit.minSize) { MinReachedEverywhere = false; break; } } if (MaxReachedEverywhere) return true; if (MinReachedEverywhere) { if (this.type === "Raid" && this.target && this.target.foundationProgress() && this.target.foundationProgress() > 60) return true; } return false; }; m.AttackPlan.prototype.forceStart = function() { for (let unitCat in this.unitStat) { let Unit = this.unitStat[unitCat]; Unit.targetSize = 0; Unit.minSize = 0; } }; /** Adds a build order. If resetQueue is true, this will reset the queue. */ m.AttackPlan.prototype.addBuildOrder = function(gameState, name, unitStats, resetQueue) { if (!this.isStarted()) { // no minsize as we don't want the plan to fail at the last minute though. this.unitStat[name] = unitStats; let Unit = this.unitStat[name]; this.unit[name] = this.unitCollection.filter(API3.Filters.byClassesAnd(Unit.classes)); this.unit[name].registerUpdates(); this.buildOrder.push([0, Unit.classes, this.unit[name], Unit, name]); if (resetQueue) { this.queue.empty(); this.queueChamp.empty(); this.queueSiege.empty(); } } }; m.AttackPlan.prototype.addSiegeUnits = function(gameState) { if (this.unitStat.Siege || this.state !== "unexecuted") return false; // no minsize as we don't want the plan to fail at the last minute though. let stat = { "priority": 1, "minSize": 0, "targetSize": 4, "batchSize": 2, "classes": ["Siege"], "interests": [ ["siegeStrength", 3], ["cost",1] ] }; if (gameState.getPlayerCiv() === "maur") stat.classes = ["Elephant", "Champion"]; if (this.Config.difficulty < 2) stat.targetSize = 1; else if (this.Config.difficulty < 3) stat.targetSize = 2; stat.targetSize = Math.round(this.Config.popScaling * stat.targetSize); this.addBuildOrder(gameState, "Siege", stat, true); return true; }; /** Three returns possible: 1 is "keep going", 0 is "failed plan", 2 is "start". */ m.AttackPlan.prototype.updatePreparation = function(gameState) { // the completing step is used to return resources and regroup the units // so we check that we have no more forced order before starting the attack if (this.state === "completing") { // if our target was destroyed, go back to "unexecuted" state if (this.targetPlayer === undefined || !this.target || !gameState.getEntityById(this.target.id())) { this.state = "unexecuted"; this.target = undefined; } else { // check that all units have finished with their transport if needed if (this.waitingForTransport()) return 1; // bloqued units which cannot finish their order should not stop the attack if (gameState.ai.elapsedTime < this.maxCompletingTime && this.hasForceOrder()) return 1; return 2; } } if (this.Config.debug > 3 && gameState.ai.playedTurn % 50 === 0) this.debugAttack(); // if we need a transport, wait for some transport ships if (this.overseas && !gameState.ai.HQ.navalManager.seaTransportShips[this.overseas].length) return 1; this.assignUnits(gameState); if (this.type !== "Raid" && gameState.ai.HQ.attackManager.getAttackInPreparation("Raid") !== undefined) this.reassignCavUnit(gameState); // reassign some cav (if any) to fasten raid preparations // special case: if we've reached max pop, and we can start the plan, start it. if (gameState.getPopulationMax() - gameState.getPopulation() < 5) { let lengthMin = 16; if (gameState.getPopulationMax() < 300) lengthMin -= Math.floor(8 * (300 - gameState.getPopulationMax()) / 300); if (this.canStart() || this.unitCollection.length > lengthMin) { this.queue.empty(); this.queueChamp.empty(); this.queueSiege.empty(); } else // Abort the plan so that its units will be reassigned to other plans. { if (this.Config.debug > 1) { let am = gameState.ai.HQ.attackManager; API3.warn(" attacks upcoming: raid " + am.upcomingAttacks.Raid.length + " rush " + am.upcomingAttacks.Rush.length + " attack " + am.upcomingAttacks.Attack.length + " huge " + am.upcomingAttacks.HugeAttack.length); API3.warn(" attacks started: raid " + am.startedAttacks.Raid.length + " rush " + am.startedAttacks.Rush.length + " attack " + am.startedAttacks.Attack.length + " huge " + am.startedAttacks.HugeAttack.length); } return 0; } } else if (this.mustStart() && gameState.countOwnQueuedEntitiesWithMetadata("plan", +this.name) > 0) { // keep on while the units finish being trained, then we'll start this.queue.empty(); this.queueChamp.empty(); this.queueSiege.empty(); return 1; } else if (!this.mustStart()) { if (this.canBuildUnits) { // We still have time left to recruit units and do stuffs. if (!this.unitStat.Siege) { let numSiegeBuilder = 0; let playerCiv = gameState.getPlayerCiv(); if (playerCiv !== "mace" && playerCiv !== "maur") numSiegeBuilder += gameState.getOwnEntitiesByClass("Fortress", true).filter(API3.Filters.isBuilt()).length; if (playerCiv === "mace" || playerCiv === "maur" || playerCiv === "rome") numSiegeBuilder += gameState.countEntitiesByType(gameState.ai.HQ.bAdvanced[0], true); if (numSiegeBuilder > 0) this.addSiegeUnits(gameState); } this.trainMoreUnits(gameState); // may happen if we have no more training facilities and build orders are canceled if (!this.buildOrder.length) return 0; // will abort the plan } return 1; } // if we're here, it means we must start this.state = "completing"; if (!this.chooseTarget(gameState)) return 0; if (!this.overseas) this.getPathToTarget(gameState); if (this.type === "Raid") this.maxCompletingTime = gameState.ai.elapsedTime + 20; else { if (this.type === "Rush") this.maxCompletingTime = gameState.ai.elapsedTime + 40; else this.maxCompletingTime = gameState.ai.elapsedTime + 60; // warn our allies so that they can help if possible if (!this.requested) Engine.PostCommand(PlayerID, {"type": "attack-request", "source": PlayerID, "player": this.targetPlayer}); } let rallyPoint = this.rallyPoint; let rallyIndex = gameState.ai.accessibility.getAccessValue(rallyPoint); for (let ent of this.unitCollection.values()) { // For the time being, if occupied in a transport, remove the unit from this plan TODO improve that if (ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined) { ent.setMetadata(PlayerID, "plan", -1); continue; } ent.setMetadata(PlayerID, "role", "attack"); ent.setMetadata(PlayerID, "subrole", "completing"); let queued = false; if (ent.resourceCarrying() && ent.resourceCarrying().length) queued = m.returnResources(gameState, ent); let index = gameState.ai.accessibility.getAccessValue(ent.position()); if (index === rallyIndex) ent.moveToRange(rallyPoint[0], rallyPoint[1], 0, 15, queued); else gameState.ai.HQ.navalManager.requireTransport(gameState, ent, index, rallyIndex, rallyPoint); } // reset all queued units let plan = this.name; gameState.ai.queueManager.removeQueue("plan_" + plan); gameState.ai.queueManager.removeQueue("plan_" + plan + "_champ"); gameState.ai.queueManager.removeQueue("plan_" + plan + "_siege"); return 1; }; m.AttackPlan.prototype.trainMoreUnits = function(gameState) { // let's sort by training advancement, ie 'current size / target size' // count the number of queued units too. // substract priority. for (let i = 0; i < this.buildOrder.length; ++i) { let special = "Plan_" + this.name + "_" + this.buildOrder[i][4]; let aQueued = gameState.countOwnQueuedEntitiesWithMetadata("special", special); aQueued += this.queue.countQueuedUnitsWithMetadata("special", special); aQueued += this.queueChamp.countQueuedUnitsWithMetadata("special", special); aQueued += this.queueSiege.countQueuedUnitsWithMetadata("special", special); this.buildOrder[i][0] = this.buildOrder[i][2].length + aQueued; } this.buildOrder.sort(function (a,b) { let va = a[0]/a[3].targetSize - a[3].priority; if (a[0] >= a[3].targetSize) va += 1000; let vb = b[0]/b[3].targetSize - b[3].priority; if (b[0] >= b[3].targetSize) vb += 1000; return va - vb; }); if (this.Config.debug > 1 && gameState.ai.playedTurn%50 === 0) { API3.warn("===================================="); API3.warn("======== build order for plan " + this.name); for (let order of this.buildOrder) { let specialData = "Plan_"+this.name+"_"+order[4]; let inTraining = gameState.countOwnQueuedEntitiesWithMetadata("special", specialData); let queue1 = this.queue.countQueuedUnitsWithMetadata("special", specialData); let queue2 = this.queueChamp.countQueuedUnitsWithMetadata("special", specialData); let queue3 = this.queueSiege.countQueuedUnitsWithMetadata("special", specialData); API3.warn(" >>> " + order[4] + " done " + order[2].length + " training " + inTraining + " queue " + queue1 + " champ " + queue2 + " siege " + queue3 + " >> need " + order[3].targetSize); } API3.warn("===================================="); } let firstOrder = this.buildOrder[0]; if (firstOrder[0] < firstOrder[3].targetSize) { // find the actual queue we want let queue = this.queue; if (firstOrder[3].classes.indexOf("Siege") !== -1 || (gameState.getPlayerCiv() == "maur" && firstOrder[3].classes.indexOf("Elephant") !== -1 && firstOrder[3].classes.indexOf("Champion") !== -1)) queue = this.queueSiege; else if (firstOrder[3].classes.indexOf("Hero") !== -1) queue = this.queueSiege; else if (firstOrder[3].classes.indexOf("Champion") !== -1) queue = this.queueChamp; if (queue.length() <= 5) { let template = gameState.ai.HQ.findBestTrainableUnit(gameState, firstOrder[1], firstOrder[3].interests); // HACK (TODO replace) : if we have no trainable template... Then we'll simply remove the buildOrder, // effectively removing the unit from the plan. if (template === undefined) { if (this.Config.debug > 1) API3.warn("attack no template found " + firstOrder[1]); delete this.unitStat[firstOrder[4]]; // deleting the associated unitstat. this.buildOrder.splice(0,1); } else { if (this.Config.debug > 2) API3.warn("attack template " + template + " added for plan " + this.name); let max = firstOrder[3].batchSize; let specialData = "Plan_" + this.name + "_" + firstOrder[4]; let data = { "plan": this.name, "special": specialData, "base": 0 }; data.role = gameState.getTemplate(template).hasClass("CitizenSoldier") ? "worker" : "attack"; let trainingPlan = new m.TrainingPlan(gameState, template, data, max, max); if (trainingPlan.template) queue.addPlan(trainingPlan); else if (this.Config.debug > 1) API3.warn("training plan canceled because no template for " + template + " build1 " + uneval(firstOrder[1]) + " build3 " + uneval(firstOrder[3].interests)); } } } }; m.AttackPlan.prototype.assignUnits = function(gameState) { let plan = this.name; let added = false; // If we can not build units, assign all available except those affected to allied defense to the current attack if (!this.canBuildUnits) { for (let ent of gameState.getOwnUnits().values()) { if (ent.getMetadata(PlayerID, "allied") || !this.isAvailableUnit(gameState, ent)) continue; ent.setMetadata(PlayerID, "plan", plan); this.unitCollection.updateEnt(ent); added = true; } return added; } if (this.type === "Raid") { // Raid are fast cavalry attack: assign all cav except some for hunting let num = 0; for (let ent of gameState.getOwnUnits().values()) { if (!ent.hasClass("Cavalry") || !this.isAvailableUnit(gameState, ent)) continue; if (num++ < 2) continue; ent.setMetadata(PlayerID, "plan", plan); this.unitCollection.updateEnt(ent); added = true; } return added; } // Assign all units without specific role for (let ent of gameState.getOwnEntitiesByRole(undefined, true).values()) { if (!ent.hasClass("Unit") || !this.isAvailableUnit(gameState, ent)) continue; if (ent.hasClass("Ship") || ent.hasClass("Support") || ent.attackTypes() === undefined) continue; ent.setMetadata(PlayerID, "plan", plan); this.unitCollection.updateEnt(ent); added = true; } // Add units previously in a plan, but which left it because needed for defense or attack finished for (let ent of gameState.ai.HQ.attackManager.outOfPlan.values()) { if (!this.isAvailableUnit(gameState, ent)) continue; ent.setMetadata(PlayerID, "plan", plan); this.unitCollection.updateEnt(ent); added = true; } // Finally add also some workers, // If Rush, assign all kind of workers, keeping only a minimum number of defenders // Otherwise, assign only some idle workers if too much of them let num = 0; let numbase = {}; let keep = this.type === "Rush" ? Math.round(this.Config.popScaling * (12 + 4*this.Config.personality.defensive)) : 6; for (let ent of gameState.getOwnEntitiesByRole("worker", true).values()) { if (!ent.hasClass("CitizenSoldier") || !this.isAvailableUnit(gameState, ent)) continue; let baseID = ent.getMetadata(PlayerID, "base"); if (baseID) numbase[baseID] = numbase[baseID] ? ++numbase[baseID] : 1; else { API3.warn("Petra problem ent without base "); m.dumpEntity(ent); continue; } if (this.type !== "Rush" && ent.getMetadata(PlayerID, "subrole") !== "idle") continue; if (num++ < keep || numbase[baseID] < 5) continue; ent.setMetadata(PlayerID, "plan", plan); this.unitCollection.updateEnt(ent); added = true; } return added; }; m.AttackPlan.prototype.isAvailableUnit = function(gameState, ent) { if (!ent.position()) return false; if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") !== -1 || ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined) return false; if (gameState.ai.HQ.gameTypeManager.criticalEnts.has(ent.id()) && (this.overseas || ent.healthLevel() < 0.8)) return false; return true; }; /** Reassign one (at each turn) Cav unit to fasten raid preparation. */ m.AttackPlan.prototype.reassignCavUnit = function(gameState) { let found; for (let ent of this.unitCollection.values()) { if (!ent.position() || ent.getMetadata(PlayerID, "transport") !== undefined) continue; if (!ent.hasClass("Cavalry") || !ent.hasClass("CitizenSoldier")) continue; found = ent; break; } if (!found) return; let raid = gameState.ai.HQ.attackManager.getAttackInPreparation("Raid"); found.setMetadata(PlayerID, "plan", raid.name); this.unitCollection.updateEnt(found); raid.unitCollection.updateEnt(found); }; m.AttackPlan.prototype.chooseTarget = function(gameState) { if (this.targetPlayer === undefined) { this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); if (this.targetPlayer === undefined) return false; } this.target = this.getNearestTarget(gameState, this.rallyPoint); if (!this.target) { // may-be all our previous enemey target (if not recomputed here) have been destroyed ? this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); if (this.targetPlayer !== undefined) this.target = this.getNearestTarget(gameState, this.rallyPoint); if (!this.target) return false; } this.targetPos = this.target.position(); // redefine a new rally point for this target if we have a base on the same land // find a new one on the pseudo-nearest base (dist weighted by the size of the island) let targetIndex = gameState.ai.accessibility.getAccessValue(this.targetPos); let rallyIndex = gameState.ai.accessibility.getAccessValue(this.rallyPoint); if (targetIndex !== rallyIndex) { let distminSame = Math.min(); let rallySame; let distminDiff = Math.min(); let rallyDiff; for (let base of gameState.ai.HQ.baseManagers) { let anchor = base.anchor; if (!anchor || !anchor.position()) continue; let dist = API3.SquareVectorDistance(anchor.position(), this.targetPos); if (base.accessIndex === targetIndex) { if (dist >= distminSame) continue; distminSame = dist; rallySame = anchor.position(); } else { dist = dist / Math.sqrt(gameState.ai.accessibility.regionSize[base.accessIndex]); if (dist >= distminDiff) continue; distminDiff = dist; rallyDiff = anchor.position(); } } if (rallySame) { this.rallyPoint = rallySame; this.overseas = 0; } else if (rallyDiff) { rallyIndex = gameState.ai.accessibility.getAccessValue(rallyDiff); this.rallyPoint = rallyDiff; this.overseas = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyIndex, targetIndex); if (this.overseas) gameState.ai.HQ.navalManager.setMinimalTransportShips(gameState, this.overseas, this.neededShips); else return false; } } else if (this.overseas) this.overseas = 0; return true; }; /** * sameLand true means that we look for a target for which we do not need to take a transport */ m.AttackPlan.prototype.getNearestTarget = function(gameState, position, sameLand) { this.isBlocked = false; let targets; if (this.type === "Raid") targets = this.raidTargetFinder(gameState); else if (this.type === "Rush" || this.type === "Attack") targets = this.rushTargetFinder(gameState, this.targetPlayer); else targets = this.defaultTargetFinder(gameState, this.targetPlayer); if (!targets.hasEntities()) return undefined; let land = gameState.ai.accessibility.getAccessValue(position); // picking the nearest target let target; let minDist = Math.min(); for (let ent of targets.values()) { if (!ent.position()) continue; if (sameLand && gameState.ai.accessibility.getAccessValue(ent.position()) !== land) continue; let dist = API3.SquareVectorDistance(ent.position(), position); // in normal attacks, disfavor fields if (this.type !== "Rush" && this.type !== "Raid" && ent.hasClass("Field")) dist += 100000; if (dist < minDist) { minDist = dist; target = ent; } } if (!target) return undefined; // Check that we can reach this target target = this.checkTargetObstruction(gameState, target, position); if (!target) return undefined; // Rushes can change their enemy target if nothing found with the preferred enemy // Obstruction also can change the enemy target this.targetPlayer = target.owner(); return target; }; /** Default target finder aims for conquest critical targets */ m.AttackPlan.prototype.defaultTargetFinder = function(gameState, playerEnemy) { let targets; if (gameState.getGameType() === "wonder") targets = gameState.getEnemyStructures(playerEnemy).filter(API3.Filters.byClass("Wonder")); else if (gameState.getGameType() === "regicide") targets = gameState.getEnemyUnits(playerEnemy).filter(API3.Filters.byClass("Hero")); else if (gameState.getGameType() === "capture_the_relic") targets = gameState.getEnemyUnits(playerEnemy).filter(API3.Filters.byClass("Relic")); if (targets && targets.hasEntities()) return targets; targets = gameState.getEnemyStructures(playerEnemy).filter(API3.Filters.byClass("CivCentre")); if (!targets.hasEntities()) targets = gameState.getEnemyStructures(playerEnemy).filter(API3.Filters.byClass("ConquestCritical")); // If there's nothing, attack anything else that's less critical if (!targets.hasEntities()) targets = gameState.getEnemyStructures(playerEnemy).filter(API3.Filters.byClass("Town")); if (!targets.hasEntities()) targets = gameState.getEnemyStructures(playerEnemy).filter(API3.Filters.byClass("Village")); // no buildings, attack anything conquest critical, even units if (!targets.hasEntities()) targets = gameState.getEntities(playerEnemy).filter(API3.Filters.byClass("ConquestCritical")); return targets; }; /** Rush target finder aims at isolated non-defended buildings */ m.AttackPlan.prototype.rushTargetFinder = function(gameState, playerEnemy) { let targets = new API3.EntityCollection(gameState.sharedScript); let buildings; if (playerEnemy !== undefined) buildings = gameState.getEnemyStructures(playerEnemy).toEntityArray(); else buildings = gameState.getEnemyStructures().toEntityArray(); if (!buildings.length) return targets; this.position = this.unitCollection.getCentrePosition(); if (!this.position) this.position = this.rallyPoint; let target; let minDist = Math.min(); for (let building of buildings) { if (building.owner() === 0) continue; if (building.hasDefensiveFire()) continue; let pos = building.position(); let defended = false; for (let defense of buildings) { if (!defense.hasDefensiveFire()) continue; let dist = API3.SquareVectorDistance(pos, defense.position()); if (dist < 6400) // TODO check on defense range rather than this fixed 80*80 { defended = true; break; } } if (defended) continue; let dist = API3.SquareVectorDistance(pos, this.position); if (dist > minDist) continue; minDist = dist; target = building; } if (target) targets.addEnt(target); if (!targets.hasEntities()) { if (this.type === "Attack") targets = this.defaultTargetFinder(gameState, playerEnemy); else if (this.type === "Rush" && playerEnemy) targets = this.rushTargetFinder(gameState); } return targets; }; /** Raid target finder aims at destructing foundations from which our defenseManager has attacked the builders */ m.AttackPlan.prototype.raidTargetFinder = function(gameState) { let targets = new API3.EntityCollection(gameState.sharedScript); for (let targetId of gameState.ai.HQ.defenseManager.targetList) { let target = gameState.getEntityById(targetId); if (target && target.position()) targets.addEnt(target); } return targets; }; /** * Check that we can have a path to this target * otherwise we may be blocked by walls and try to react accordingly * This is done only when attacker and target are on the same land */ m.AttackPlan.prototype.checkTargetObstruction = function(gameState, target, position) { let targetPos = target.position(); if (gameState.ai.accessibility.getAccessValue(targetPos) !== gameState.ai.accessibility.getAccessValue(position)) return target; let startPos = { "x": position[0], "y": position[1] }; let endPos = { "x": targetPos[0], "y": targetPos[1] }; let blocker; let path = Engine.ComputePath(startPos, endPos, gameState.getPassabilityClassMask("default")); if (!path.length) return undefined; let pathPos = [path[0].x, path[0].y]; let dist = API3.VectorDistance(pathPos, targetPos); let radius = target.obstructionRadius(); for (let struct of gameState.getEnemyStructures().values()) { if (!struct.position() || !struct.get("Obstruction") || struct.hasClass("Field")) continue; // we consider that we can reach the target, but nonetheless check that we did not cross any enemy gate if (dist < radius + 10 && !struct.hasClass("Gates")) continue; // Check that we are really blocked by this structure, i.e. advancing by 1+0.8(clearance)m // in the target direction would bring us inside its obstruction. let structPos = struct.position(); let x = pathPos[0] - structPos[0] + 1.8 * (targetPos[0] - pathPos[0]) / dist; let y = pathPos[1] - structPos[1] + 1.8 * (targetPos[1] - pathPos[1]) / dist; if (struct.get("Obstruction/Static")) { if (!struct.angle()) continue; let angle = struct.angle(); let width = +struct.get("Obstruction/Static/@width"); let depth = +struct.get("Obstruction/Static/@depth"); let cosa = Math.cos(angle); let sina = Math.sin(angle); let u = x * cosa - y * sina; let v = x * sina + y * cosa; if (Math.abs(u) < width/2 && Math.abs(v) < depth/2) { blocker = struct; break; } } else if (struct.get("Obstruction/Obstructions")) { if (!struct.angle()) continue; let angle = struct.angle(); let width = +struct.get("Obstruction/Obstructions/Door/@width"); let depth = +struct.get("Obstruction/Obstructions/Door/@depth"); let doorHalfWidth = width / 2; width += +struct.get("Obstruction/Obstructions/Left/@width"); depth = Math.max(depth, +struct.get("Obstruction/Obstructions/Left/@depth")); width += +struct.get("Obstruction/Obstructions/Right/@width"); depth = Math.max(depth, +struct.get("Obstruction/Obstructions/Right/@depth")); let cosa = Math.cos(angle); let sina = Math.sin(angle); let u = x * cosa - y * sina; let v = x * sina + y * cosa; if (Math.abs(u) < width/2 && Math.abs(v) < depth/2) { blocker = struct; break; } // check that the path does not cross this gate (could happen if not locked) for (let i = 1; i < path.length; ++i) { let u1 = (path[i-1].x - structPos[0]) * cosa - (path[i-1].y - structPos[1]) * sina; let v1 = (path[i-1].x - structPos[0]) * sina + (path[i-1].y - structPos[1]) * cosa; let u2 = (path[i].x - structPos[0]) * cosa - (path[i].y - structPos[1]) * sina; let v2 = (path[i].x - structPos[0]) * sina + (path[i].y - structPos[1]) * cosa; if (v1 * v2 < 0) { let u0 = (u1*v2 - u2*v1) / (v2-v1); if (Math.abs(u0) > doorHalfWidth) continue; blocker = struct; break; } } if (blocker) break; } else if (struct.get("Obstruction/Unit")) { let r = +this.get("Obstruction/Unit/@radius"); if (x*x + y*y < r*r) { blocker = struct; break; } } } if (blocker && blocker.hasClass("StoneWall")) { /* if (this.hasSiegeUnits(gameState)) { */ this.isBlocked = true; return blocker; /* } return undefined; */ } else if (blocker) { this.isBlocked = true; return blocker; } return target; }; m.AttackPlan.prototype.getPathToTarget = function(gameState) { let startAccess = gameState.ai.accessibility.getAccessValue(this.rallyPoint); let endAccess = gameState.ai.accessibility.getAccessValue(this.targetPos); if (startAccess != endAccess) return false; Engine.ProfileStart("AI Compute path"); let startPos = { "x": this.rallyPoint[0], "y": this.rallyPoint[1] }; let endPos = { "x": this.targetPos[0], "y": this.targetPos[1] }; let path = Engine.ComputePath(startPos, endPos, gameState.getPassabilityClassMask("large")); this.path = []; this.path.push(this.targetPos); for (let p in path) this.path.push([path[p].x, path[p].y]); this.path.push(this.rallyPoint); this.path.reverse(); // Change the rally point to something useful this.setRallyPoint(gameState); Engine.ProfileStop(); return true; }; /** Set rally point at the border of our territory */ m.AttackPlan.prototype.setRallyPoint = function(gameState) { for (let i = 0; i < this.path.length; ++i) { if (gameState.ai.HQ.territoryMap.getOwner(this.path[i]) === PlayerID) continue; if (i === 0) this.rallyPoint = this.path[0]; else if (i > 1 && gameState.ai.HQ.isDangerousLocation(gameState, this.path[i-1], 20)) { this.rallyPoint = this.path[i-2]; this.path.splice(0, i-2); } else { this.rallyPoint = this.path[i-1]; this.path.splice(0, i-1); } break; } }; /** * Executes the attack plan, after this is executed the update function will be run every turn * If we're here, it's because we have enough units. */ m.AttackPlan.prototype.StartAttack = function(gameState) { if (this.Config.debug > 1) API3.warn("start attack " + this.name + " with type " + this.type); // if our target was destroyed during preparation, choose a new one if (this.targetPlayer === undefined || !this.target || !gameState.getEntityById(this.target.id())) { if (!this.chooseTarget(gameState)) return false; } // check we have a target and a path. if (this.targetPos && (this.overseas || this.path)) { // erase our queue. This will stop any leftover unit from being trained. gameState.ai.queueManager.removeQueue("plan_" + this.name); gameState.ai.queueManager.removeQueue("plan_" + this.name + "_champ"); gameState.ai.queueManager.removeQueue("plan_" + this.name + "_siege"); for (let ent of this.unitCollection.values()) ent.setMetadata(PlayerID, "subrole", "walking"); this.unitCollection.setStance("aggressive"); if (gameState.ai.accessibility.getAccessValue(this.targetPos) === gameState.ai.accessibility.getAccessValue(this.rallyPoint)) { if (!this.path[0][0] || !this.path[0][1]) { if (this.Config.debug > 1) API3.warn("StartAttack: Problem with path " + uneval(this.path)); return false; } this.state = "walking"; this.unitCollection.move(this.path[0][0], this.path[0][1]); } else { this.state = "transporting"; let startIndex = gameState.ai.accessibility.getAccessValue(this.rallyPoint); let endIndex = gameState.ai.accessibility.getAccessValue(this.targetPos); let endPos = this.targetPos; // TODO require a global transport for the collection, // and put back its state to "walking" when the transport is finished for (let ent of this.unitCollection.values()) gameState.ai.HQ.navalManager.requireTransport(gameState, ent, startIndex, endIndex, endPos); } } else { gameState.ai.gameFinished = true; API3.warn("I do not have any target. So I'll just assume I won the game."); return false; } return true; }; /** Runs every turn after the attack is executed */ m.AttackPlan.prototype.update = function(gameState, events) { if (!this.unitCollection.hasEntities()) return 0; Engine.ProfileStart("Update Attack"); this.position = this.unitCollection.getCentrePosition(); let self = this; // we are transporting our units, let's wait // TODO instead of state "arrived", made a state "walking" with a new path if (this.state === "transporting") this.UpdateTransporting(gameState, events); if (this.state === "walking" && !this.UpdateWalking(gameState, events)) { Engine.ProfileStop(); return 0; } if (this.state === "arrived") { // let's proceed on with whatever happens now. this.state = ""; this.startingAttack = true; this.unitCollection.forEach( function (ent) { ent.stopMoving(); ent.setMetadata(PlayerID, "subrole", "attacking"); }); if (this.type === "Rush") // try to find a better target for rush { let newtarget = this.getNearestTarget(gameState, this.position); if (newtarget) { this.target = newtarget; this.targetPos = this.target.position(); } } } // basic state of attacking. if (this.state === "") { // First update the target and/or its position if needed if (!this.UpdateTarget(gameState, events)) { Engine.ProfileStop(); return false; } let time = gameState.ai.elapsedTime; for (let evt of events.Attacked) { if (!this.unitCollection.hasEntId(evt.target)) continue; let attacker = gameState.getEntityById(evt.attacker); if (!attacker || !attacker.position() || !attacker.hasClass("Unit")) continue; let ourUnit = gameState.getEntityById(evt.target); if (this.isSiegeUnit(gameState, ourUnit)) { // if our siege units are attacked, we'll send some units to deal with enemies. let collec = this.unitCollection.filter(API3.Filters.not(API3.Filters.byClass("Siege"))).filterNearest(ourUnit.position(), 5); for (let ent of collec.values()) { if (this.isSiegeUnit(gameState, ent)) // needed as mauryan elephants are not filtered out continue; ent.attack(attacker.id(), m.allowCapture(gameState, ent, attacker)); ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); } // And if this attacker is a non-ranged siege unit and our unit also, attack it if (this.isSiegeUnit(gameState, attacker) && attacker.hasClass("Melee") && ourUnit.hasClass("Melee")) { ourUnit.attack(attacker.id(), m.allowCapture(gameState, ourUnit, attacker)); ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); } } else { if (this.isBlocked && !ourUnit.hasClass("Ranged") && attacker.hasClass("Ranged")) { // do not react if our melee units are attacked by ranged one and we are blocked by walls // TODO check that the attacker is from behind the wall continue; } else if (this.isSiegeUnit(gameState, attacker)) { // if our unit is attacked by a siege unit, we'll send some melee units to help it. let collec = this.unitCollection.filter(API3.Filters.byClass("Melee")).filterNearest(ourUnit.position(), 5); for (let ent of collec.values()) { ent.attack(attacker.id(), m.allowCapture(gameState, ent, attacker)); ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); } } else { // if units are attacked, abandon their target (if it was a structure or a support) and retaliate // also if our unit is attacking a range unit and the attacker is a melee unit, retaliate let orderData = ourUnit.unitAIOrderData(); if (orderData && orderData.length && orderData[0].target) { if (orderData[0].target === attacker.id()) continue; let target = gameState.getEntityById(orderData[0].target); if (target && !target.hasClass("Structure") && !target.hasClass("Support")) { if (!target.hasClass("Ranged") || !attacker.hasClass("Melee")) continue; } } ourUnit.attack(attacker.id(), m.allowCapture(gameState, ourUnit, attacker)); ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); } } } let enemyUnits = gameState.getEnemyUnits(this.targetPlayer); let enemyStructures = gameState.getEnemyStructures(this.targetPlayer); // Count the number of times an enemy is targeted, to prevent all units to follow the same target let unitTargets = {}; for (let ent of this.unitCollection.values()) { if (ent.hasClass("Ship")) // TODO What to do with ships 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.hasClass("Structure")) continue; if (!(targetId in unitTargets)) { if (this.isSiegeUnit(gameState, target) || target.hasClass("Hero")) unitTargets[targetId] = -8; else if (target.hasClass("Champion") || target.hasClass("Ship")) unitTargets[targetId] = -5; else unitTargets[targetId] = -3; } ++unitTargets[targetId]; } let veto = {}; for (let target in unitTargets) if (unitTargets[target] > 0) veto[target] = true; let targetClassesUnit; let targetClassesSiege; if (this.type === "Rush") targetClassesUnit = {"attack": ["Unit", "Structure"], "avoid": ["Palisade", "StoneWall", "Tower", "Fortress"], "vetoEntities": veto}; else { if (this.target.hasClass("Fortress")) targetClassesUnit = {"attack": ["Unit", "Structure"], "avoid": ["Palisade", "StoneWall"], "vetoEntities": veto}; else if (this.target.hasClass("Palisade") || this.target.hasClass("StoneWall")) targetClassesUnit = {"attack": ["Unit", "Structure"], "avoid": ["Fortress"], "vetoEntities": veto}; else targetClassesUnit = {"attack": ["Unit", "Structure"], "avoid": ["Palisade", "StoneWall", "Fortress"], "vetoEntities": veto}; } if (this.target.hasClass("Structure")) targetClassesSiege = {"attack": ["Structure"], "avoid": [], "vetoEntities": veto}; else targetClassesSiege = {"attack": ["Unit", "Structure"], "avoid": [], "vetoEntities": veto}; // do not loose time destroying buildings which do not help enemy's defense and can be easily captured later if (this.target.hasDefensiveFire()) { targetClassesUnit.avoid = targetClassesUnit.avoid.concat("House", "Storehouse", "Farmstead", "Field", "Blacksmith"); targetClassesSiege.avoid = targetClassesSiege.avoid.concat("House", "Storehouse", "Farmstead", "Field", "Blacksmith"); } if (this.unitCollUpdateArray === undefined || !this.unitCollUpdateArray.length) this.unitCollUpdateArray = this.unitCollection.toIdArray(); // Let's check a few units each time we update (currently 10) except when attack starts - let lgth = (this.unitCollUpdateArray.length < 15 || this.startingAttack) ? this.unitCollUpdateArray.length : 10; + let lgth = this.unitCollUpdateArray.length < 15 || this.startingAttack ? this.unitCollUpdateArray.length : 10; for (let check = 0; check < lgth; check++) { let ent = gameState.getEntityById(this.unitCollUpdateArray[check]); if (!ent || !ent.position()) continue; let targetId; let orderData = ent.unitAIOrderData(); if (orderData && orderData.length && orderData[0].target) targetId = orderData[0].target; // update the order if needed let needsUpdate = false; let maybeUpdate = false; let siegeUnit = this.isSiegeUnit(gameState, ent); if (ent.isIdle()) needsUpdate = true; else if (siegeUnit && targetId) { let target = gameState.getEntityById(targetId); if (!target || gameState.isPlayerAlly(target.owner())) needsUpdate = true; else if (unitTargets[targetId] && unitTargets[targetId] > 0) { needsUpdate = true; --unitTargets[targetId]; } else if (!target.hasClass("Structure")) maybeUpdate = true; } else if (targetId) { let target = gameState.getEntityById(targetId); if (!target || gameState.isPlayerAlly(target.owner())) needsUpdate = true; else if (unitTargets[targetId] && unitTargets[targetId] > 0) { needsUpdate = true; --unitTargets[targetId]; } else if (target.hasClass("Ship") && !ent.hasClass("Ship")) maybeUpdate = true; else if (!ent.hasClass("Cavalry") && !ent.hasClass("Ranged") && target.hasClass("FemaleCitizen") && target.unitAIState().split(".")[1] == "FLEEING") maybeUpdate = true; } // don't update too soon if not necessary if (!needsUpdate) { if (!maybeUpdate) continue; let deltat = ent.unitAIState() === "INDIVIDUAL.COMBAT.APPROACHING" ? 10 : 5; let lastAttackPlanUpdateTime = ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime"); if (lastAttackPlanUpdateTime && time - lastAttackPlanUpdateTime < deltat) continue; } ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); let range = 60; let attackTypes = ent.attackTypes(); if (this.isBlocked) { if (attackTypes && attackTypes.indexOf("Ranged") !== -1) range = ent.attackRange("Ranged").max; else if (attackTypes && attackTypes.indexOf("Melee") !== -1) range = ent.attackRange("Melee").max; else range = 10; } else if (attackTypes && attackTypes.indexOf("Ranged") !== -1) range = 30 + ent.attackRange("Ranged").max; else if (ent.hasClass("Cavalry")) range += 30; range = range * range; let entIndex = gameState.ai.accessibility.getAccessValue(ent.position()); // Checking for gates if we're a siege unit. if (siegeUnit) { let mStruct = enemyStructures.filter(function (enemy) { if (!enemy.position() || (enemy.hasClass("StoneWall") && !ent.canAttackClass("StoneWall"))) return false; if (API3.SquareVectorDistance(enemy.position(), ent.position()) > range) return false; if (enemy.foundationProgress() === 0) return false; if (gameState.ai.accessibility.getAccessValue(enemy.position()) !== entIndex) return false; return true; }).toEntityArray(); if (mStruct.length) { mStruct.sort(function (structa, structb) { let vala = structa.costSum(); if (structa.hasClass("Gates") && ent.canAttackClass("StoneWall")) vala += 10000; else if (structa.hasDefensiveFire()) vala += 1000; else if (structa.hasClass("ConquestCritical")) vala += 200; let valb = structb.costSum(); if (structb.hasClass("Gates") && ent.canAttackClass("StoneWall")) valb += 10000; else if (structb.hasDefensiveFire()) valb += 1000; else if (structb.hasClass("ConquestCritical")) valb += 200; return valb - vala; }); if (mStruct[0].hasClass("Gates")) ent.attack(mStruct[0].id(), m.allowCapture(gameState, ent, mStruct[0])); else { let rand = randIntExclusive(0, mStruct.length * 0.2); ent.attack(mStruct[rand].id(), m.allowCapture(gameState, ent, mStruct[rand])); } } else { if (!ent.hasClass("Ranged")) { let targetClasses = {"attack": targetClassesSiege.attack, "avoid": targetClassesSiege.avoid.concat("Ship"), "vetoEntities": veto}; ent.attackMove(this.targetPos[0], this.targetPos[1], targetClasses); } else ent.attackMove(this.targetPos[0], this.targetPos[1], targetClassesSiege); } } else { let nearby = !ent.hasClass("Cavalry") && !ent.hasClass("Ranged"); let mUnit = enemyUnits.filter(function (enemy) { if (!enemy.position()) return false; if (enemy.hasClass("Animal")) return false; if (nearby && enemy.hasClass("FemaleCitizen") && enemy.unitAIState().split(".")[1] == "FLEEING") return false; let dist = API3.SquareVectorDistance(enemy.position(), ent.position()); if (dist > range) return false; if (gameState.ai.accessibility.getAccessValue(enemy.position()) !== entIndex) return false; // if already too much units targeting this enemy, let's continue towards our main target if (veto[enemy.id()] && API3.SquareVectorDistance(self.targetPos, ent.position()) > 2500) return false; enemy.setMetadata(PlayerID, "distance", Math.sqrt(dist)); return true; }).toEntityArray(); if (mUnit.length !== 0) { mUnit.sort(function (unitA,unitB) { let vala = unitA.hasClass("Support") ? 50 : 0; if (ent.countersClasses(unitA.classes())) vala += 100; let valb = unitB.hasClass("Support") ? 50 : 0; if (ent.countersClasses(unitB.classes())) valb += 100; let distA = unitA.getMetadata(PlayerID, "distance"); let distB = unitB.getMetadata(PlayerID, "distance"); if( distA && distB) { vala -= distA; valb -= distB; } if (veto[unitA.id()]) vala -= 20000; if (veto[unitB.id()]) valb -= 20000; return valb - vala; }); let rand = randIntExclusive(0, mUnit.length * 0.1); ent.attack(mUnit[rand].id(), m.allowCapture(gameState, ent, mUnit[rand])); } else if (this.isBlocked) ent.attack(this.target.id(), false); else if (API3.SquareVectorDistance(this.targetPos, ent.position()) > 2500 ) { let targetClasses = targetClassesUnit; if (maybeUpdate && ent.unitAIState() === "INDIVIDUAL.COMBAT.APPROACHING") // we may be blocked by walls, attack everything { if (!ent.hasClass("Ranged") && !ent.hasClass("Ship")) targetClasses = {"attack": ["Unit", "Structure"], "avoid": ["Ship"], "vetoEntities": veto}; else targetClasses = {"attack": ["Unit", "Structure"], "vetoEntities": veto}; } else if (!ent.hasClass("Ranged") && !ent.hasClass("Ship")) targetClasses = {"attack": targetClassesUnit.attack, "avoid": targetClassesUnit.avoid.concat("Ship"), "vetoEntities": veto}; ent.attackMove(this.targetPos[0], this.targetPos[1], targetClasses); } else { let mStruct = enemyStructures.filter(function (enemy) { if (self.isBlocked && enemy.id() !== this.target.id()) return false; if (!enemy.position() || (enemy.hasClass("StoneWall") && !ent.canAttackClass("StoneWall"))) return false; if (API3.SquareVectorDistance(enemy.position(), ent.position()) > range) return false; if (gameState.ai.accessibility.getAccessValue(enemy.position()) !== entIndex) return false; return true; }).toEntityArray(); if (mStruct.length !== 0) { mStruct.sort(function (structa,structb) { let vala = structa.costSum(); if (structa.hasClass("Gates") && ent.canAttackClass("StoneWall")) vala += 10000; else if (structa.hasClass("ConquestCritical")) vala += 100; let valb = structb.costSum(); if (structb.hasClass("Gates") && ent.canAttackClass("StoneWall")) valb += 10000; else if (structb.hasClass("ConquestCritical")) valb += 100; return valb - vala; }); if (mStruct[0].hasClass("Gates")) ent.attack(mStruct[0].id(), false); else { let rand = randIntExclusive(0, mStruct.length * 0.2); ent.attack(mStruct[rand].id(), m.allowCapture(gameState, ent, mStruct[rand])); } } else if (needsUpdate) // really nothing let's try to help our nearest unit { let distmin = Math.min(); let attacker; this.unitCollection.forEach( function (unit) { if (!unit.position()) return; if (unit.unitAIState().split(".")[1] !== "COMBAT" || !unit.unitAIOrderData().length || !unit.unitAIOrderData()[0].target) return; if (!gameState.getEntityById(unit.unitAIOrderData()[0].target)) return; let dist = API3.SquareVectorDistance(unit.position(), ent.position()); if (dist > distmin) return; distmin = dist; attacker = gameState.getEntityById(unit.unitAIOrderData()[0].target); }); if (attacker) ent.attack(attacker.id(), m.allowCapture(gameState, ent, attacker)); } } } } this.unitCollUpdateArray.splice(0, lgth); this.startingAttack = false; // check if this enemy has resigned if (this.target && this.target.owner() === 0 && this.targetPlayer !== 0) this.target = undefined; } this.lastPosition = this.position; Engine.ProfileStop(); return this.unitCollection.length; }; m.AttackPlan.prototype.UpdateTransporting = function(gameState, events) { let done = true; for (let ent of this.unitCollection.values()) { if (this.Config.debug > 1 && ent.getMetadata(PlayerID, "transport") !== undefined) Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [2,2,0]}); else if (this.Config.debug > 1) Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [1,1,1]}); if (!done) continue; if (ent.getMetadata(PlayerID, "transport") !== undefined) done = false; } if (done) { this.state = "arrived"; return; } // if we are attacked while waiting the rest of the army, retaliate for (let evt of events.Attacked) { if (!this.unitCollection.hasEntId(evt.target)) continue; let attacker = gameState.getEntityById(evt.attacker); if (!attacker || !gameState.getEntityById(evt.target)) continue; for (let ent of this.unitCollection.values()) { if (ent.getMetadata(PlayerID, "transport") !== undefined) continue; if (!ent.isIdle()) continue; ent.attack(attacker.id(), m.allowCapture(gameState, ent, attacker)); } break; } }; m.AttackPlan.prototype.UpdateWalking = function(gameState, events) { // we're marching towards the target // Let's check if any of our unit has been attacked. // In case yes, we'll determine if we're simply off against an enemy army, a lone unit/building // or if we reached the enemy base. Different plans may react differently. let attackedNB = 0; let attackedUnitNB = 0; for (let evt of events.Attacked) { if (!this.unitCollection.hasEntId(evt.target)) continue; let attacker = gameState.getEntityById(evt.attacker); if (attacker && (attacker.owner() !== 0 || this.targetPlayer === 0)) { attackedNB++; if (attacker.hasClass("Unit")) attackedUnitNB++; } } // Are we arrived at destination ? if (attackedNB > 1 && (attackedUnitNB || this.hasSiegeUnits(gameState))) { if (gameState.ai.HQ.territoryMap.getOwner(this.position) === this.targetPlayer || attackedNB > 3) { this.state = "arrived"; return true; } } // basically haven't moved an inch: very likely stuck) if (API3.SquareVectorDistance(this.position, this.position5TurnsAgo) < 10 && this.path.length > 0 && gameState.ai.playedTurn % 5 === 0) { // check for stuck siege units let farthest = 0; let farthestEnt; for (let ent of this.unitCollection.filter(API3.Filters.byClass("Siege")).values()) { let dist = API3.SquareVectorDistance(ent.position(), this.position); if (dist < farthest) continue; farthest = dist; farthestEnt = ent; } if (farthestEnt) farthestEnt.destroy(); } if (gameState.ai.playedTurn % 5 === 0) this.position5TurnsAgo = this.position; if (this.lastPosition && API3.SquareVectorDistance(this.position, this.lastPosition) < 20 && this.path.length > 0) { if (!this.path[0][0] || !this.path[0][1]) API3.warn("Start: Problem with path " + uneval(this.path)); // We're stuck, presumably. Check if there are no walls just close to us. If so, we're arrived, and we're gonna tear down some serious stone. let nexttoWalls = false; for (let ent of gameState.getEnemyStructures().filter(API3.Filters.byClass("StoneWall")).values()) { if (!nexttoWalls && API3.SquareVectorDistance(this.position, ent.position()) < 800) nexttoWalls = true; } // there are walls but we can attack if (nexttoWalls && this.unitCollection.filter(API3.Filters.byCanAttack("StoneWall")).hasEntities()) { if (this.Config.debug > 1) API3.warn("Attack Plan " + this.type + " " + this.name + " has met walls and is not happy."); this.state = "arrived"; return true; } else if (nexttoWalls) // abort plan { if (this.Config.debug > 1) API3.warn("Attack Plan " + this.type + " " + this.name + " has met walls and gives up."); return false; } //this.unitCollection.move(this.path[0][0], this.path[0][1]); this.unitCollection.moveIndiv(this.path[0][0], this.path[0][1]); } // check if our units are close enough from the next waypoint. if (API3.SquareVectorDistance(this.position, this.targetPos) < 10000) { if (this.Config.debug > 1) API3.warn("Attack Plan " + this.type + " " + this.name + " has arrived to destination."); this.state = "arrived"; return true; } else if (this.path.length && API3.SquareVectorDistance(this.position, this.path[0]) < 1600) { this.path.shift(); if (this.path.length) this.unitCollection.move(this.path[0][0], this.path[0][1]); else { if (this.Config.debug > 1) API3.warn("Attack Plan " + this.type + " " + this.name + " has arrived to destination."); this.state = "arrived"; return true; } } return true; }; m.AttackPlan.prototype.UpdateTarget = function(gameState, events) { // First update the target position in case it's a unit (and check if it has garrisoned) if (this.target && this.target.hasClass("Unit")) { this.targetPos = this.target.position(); if (!this.targetPos) { let holder = m.getHolder(gameState, this.target); if (holder && gameState.isPlayerEnemy(holder.owner())) { this.target = holder; this.targetPos = holder.position(); } else this.target = undefined; } } // Then update the target if needed: if (this.targetPlayer === undefined || !gameState.isPlayerEnemy(this.targetPlayer)) { this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); if (this.targetPlayer === undefined) return false; if (this.target && this.target.owner() !== this.targetPlayer) this.target = undefined; } if (this.target && this.target.owner() === 0 && this.targetPlayer !== 0) // this enemy has resigned this.target = undefined; if (!this.target || !gameState.getEntityById(this.target.id())) { if (this.Config.debug > 1) API3.warn("Seems like our target has been destroyed. Switching."); this.target = this.getNearestTarget(gameState, this.position, true); if (!this.target) { // Check if we could help any current attack let attackManager = gameState.ai.HQ.attackManager; let accessIndex = gameState.ai.accessibility.getAccessValue(this.position); for (let attackType in attackManager.startedAttacks) { for (let attack of attackManager.startedAttacks[attackType]) { if (attack.name === this.name) continue; if (!attack.target || !gameState.getEntityById(attack.target.id())) continue; if (accessIndex !== gameState.ai.accessibility.getAccessValue(attack.targetPos)) continue; if (attack.target.owner() === 0 && attack.targetPlayer !== 0) // looks like it has resigned continue; if (!gameState.isPlayerEnemy(attack.targetPlayer)) continue; this.target = attack.target; this.targetPlayer = attack.targetPlayer; this.targetPos = this.target.position(); return true; } } // If not, let's look for another enemy if (!this.target) { this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); if (this.targetPlayer !== undefined) this.target = this.getNearestTarget(gameState, this.position, true); if (!this.target) { if (this.Config.debug > 1) API3.warn("No new target found. Remaining units " + this.unitCollection.length); return false; } } if (this.Config.debug > 1) API3.warn("We will help one of our other attacks"); } this.targetPos = this.target.position(); } return true; }; /** reset any units */ m.AttackPlan.prototype.Abort = function(gameState) { this.unitCollection.unregister(); if (this.unitCollection.hasEntities()) { // If the attack was started, and we are on the same land as the rallyPoint, go back there let rallyPoint = this.rallyPoint; let withdrawal = this.isStarted() && !this.overseas; for (let ent of this.unitCollection.values()) { if (ent.getMetadata(PlayerID, "role") === "attack") ent.stopMoving(); if (withdrawal) ent.move(rallyPoint[0], rallyPoint[1]); this.removeUnit(ent); } } for (let unitCat in this.unitStat) this.unit[unitCat].unregister(); gameState.ai.queueManager.removeQueue("plan_" + this.name); gameState.ai.queueManager.removeQueue("plan_" + this.name + "_champ"); gameState.ai.queueManager.removeQueue("plan_" + this.name + "_siege"); }; m.AttackPlan.prototype.removeUnit = function(ent, update) { if (ent.hasClass("CitizenSoldier") && ent.getMetadata(PlayerID, "role") !== "worker") { ent.setMetadata(PlayerID, "role", "worker"); ent.setMetadata(PlayerID, "subrole", undefined); } ent.setMetadata(PlayerID, "plan", -1); if (update) this.unitCollection.updateEnt(ent); }; m.AttackPlan.prototype.checkEvents = function(gameState, events) { for (let evt of events.EntityRenamed) { if (!this.target || this.target.id() != evt.entity) continue; this.target = gameState.getEntityById(evt.newentity); if (this.target) this.targetPos = this.target.position(); } for (let evt of events.OwnershipChanged) // capture event if (this.target && this.target.id() == evt.entity && gameState.isPlayerAlly(evt.to)) this.target = undefined; for (let evt of events.PlayerDefeated) { if (this.targetPlayer !== evt.playerId) continue; this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); this.target = undefined; } if (!this.overseas || this.state !== "unexecuted") return; // let's check if an enemy has built a structure at our access for (let evt of events.Create) { let ent = gameState.getEntityById(evt.entity); if (!ent || !ent.position() || !ent.hasClass("Structure")) continue; if (!gameState.isPlayerEnemy(ent.owner())) continue; let access = gameState.ai.accessibility.getAccessValue(ent.position()); for (let base of gameState.ai.HQ.baseManagers) { if (!base.anchor || !base.anchor.position()) continue; if (base.accessIndex !== access) continue; this.overseas = 0; this.rallyPoint = base.anchor.position(); } } }; m.AttackPlan.prototype.waitingForTransport = function() { for (let ent of this.unitCollection.values()) if (ent.getMetadata(PlayerID, "transport") !== undefined) return true; return false; }; m.AttackPlan.prototype.hasSiegeUnits = function(gameState) { for (let ent of this.unitCollection.values()) if (this.isSiegeUnit(gameState, ent)) return true; return false; }; m.AttackPlan.prototype.hasForceOrder = function(data, value) { for (let ent of this.unitCollection.values()) { if (data && +ent.getMetadata(PlayerID, data) !== value) continue; let orders = ent.unitAIOrderData(); for (let order of orders) if (order.force) return true; } return false; }; m.AttackPlan.prototype.isSiegeUnit = function(gameState, ent) { return ent.hasClass("Siege") || (ent.hasClass("Elephant") && ent.hasClass("Champion")); }; m.AttackPlan.prototype.debugAttack = function() { API3.warn("---------- attack " + this.name); for (let unitCat in this.unitStat) { let Unit = this.unitStat[unitCat]; API3.warn(unitCat + " num=" + this.unit[unitCat].length + " min=" + Unit.minSize + " need=" + Unit.targetSize); } API3.warn("------------------------------"); }; m.AttackPlan.prototype.Serialize = function() { let properties = { "name": this.name, "type": this.type, "state": this.state, "rallyPoint": this.rallyPoint, "overseas": this.overseas, "paused": this.paused, "maxCompletingTime": this.maxCompletingTime, "neededShips": this.neededShips, "unitStat": this.unitStat, "position5TurnsAgo": this.position5TurnsAgo, "lastPosition": this.lastPosition, "position": this.position, "isBlocked": this.isBlocked, "targetPlayer": this.targetPlayer, "target": this.target !== undefined ? this.target.id() : undefined, "targetPos": this.targetPos, "path": this.path }; return { "properties": properties}; }; m.AttackPlan.prototype.Deserialize = function(gameState, data) { for (let key in data.properties) this[key] = data.properties[key]; if (this.target) this.target = gameState.getEntityById(this.target); this.failed = undefined; }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js (revision 19512) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js (revision 19513) @@ -1,2409 +1,2409 @@ 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.econState = "growth"; // existing values: growth, townPhasing and cityPhasing. this.currentPhase = undefined; // 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.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.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) { if (events.TerritoriesChanged.length || events.DiplomacyChanged.length) this.updateTerritories(gameState); for (let evt of events.DiplomacyChanged) { if (evt.player !== PlayerID && evt.otherPlayer !== PlayerID) continue; gameState.resetAllyStructures(); gameState.resetEnemyStructures(); 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 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; for (let evt of events.Attacked) { if (ent.id() != evt.target) continue; ratioMax = 0.90; break; } if (captureRatio > ratioMax) continue; ent.destroy(); } this.decayingStructures.delete(entId); } }; /** Called by the "town phase" research plan once it's started */ m.HQ.prototype.OnTownPhase = function(gameState) { }; /** Called by the "city phase" research plan once it's started */ m.HQ.prototype.OnCityPhase = function(gameState) { }; /** 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 = [["cost", 1], ["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.popForTown + 10) return; if (numberTotal > this.targetNumWorkers || (numberTotal >= this.Config.Economy.popForTown && gameState.currentPhase() == 1 && !gameState.isResearching(gameState.townPhase()))) return; if (numberQueued > 50 || (numberOfQueuedSupports > 20 && numberOfQueuedSoldiers > 20) || numberInTraining > 15) return; // Choose whether we want soldiers or support units. let supportRatio = gameState.isTemplateDisabled(gameState.applyCiv("structures/{civ}_field")) ? Math.min(this.supportRatio, 0.1) : this.supportRatio; let supportMax = supportRatio * this.targetNumWorkers; let supportNum = supportMax * Math.atan(numberTotal/supportMax) / 1.570796; let template; if (numberOfSupports + numberOfQueuedSupports > supportNum) { let requirements; if (numberTotal < 45) requirements = [ ["cost", 1], ["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 aDivParam = 0; let bDivParam = 0; let aTopParam = 0; let bTopParam = 0; for (let param of parameters) { if (param[0] == "base") { aTopParam = param[1]; bTopParam = param[1]; } if (param[0] == "strength") { aTopParam += m.getMaxStrength(a[1]) * param[1]; bTopParam += m.getMaxStrength(b[1]) * param[1]; } if (param[0] == "siegeStrength") { aTopParam += m.getMaxStrength(a[1], "Structure") * param[1]; bTopParam += m.getMaxStrength(b[1], "Structure") * param[1]; } if (param[0] == "speed") { aTopParam += a[1].walkSpeed() * param[1]; bTopParam += b[1].walkSpeed() * param[1]; } if (param[0] == "cost") { aDivParam += a[1].costSum() * param[1]; bDivParam += b[1].costSum() * param[1]; } // requires a third parameter which is the resource if (param[0] == "costsResource") { if (a[1].cost()[param[2]]) aTopParam *= param[1]; if (b[1].cost()[param[2]]) bTopParam *= param[1]; } if (param[0] == "canGather") { // checking against wood, could be anything else really. if (a[1].resourceGatherRates() && a[1].resourceGatherRates()["wood.tree"]) aTopParam *= param[1]; if (b[1].resourceGatherRates() && b[1].resourceGatherRates()["wood.tree"]) bTopParam *= param[1]; } } return -aTopParam/(aDivParam+1) + bTopParam/(bDivParam+1); }); return units[0][0]; }; /** * 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 checks for a good spot in the territory. If none, and town/city phase, checks outside // The AI will currently not build a CC if it wouldn't connect with an existing CC. 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() / 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() + 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.getMap().cellSize * gameState.getMap().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); + 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) { norm = 0; continue; } else 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(); 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() / 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 * TODO check that it is on same accessIndex */ m.HQ.prototype.findMarketLocation = function(gameState, template) { let markets = gameState.updatingCollection("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 radius = Math.ceil(template.obstructionRadius() / 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 cc 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 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) gainMultiplier = traderTemplatesGains.landGainMultiplier; else continue; if (!gainMultiplier) continue; let val = API3.SquareVectorDistance(market.position(), pos) * gainMultiplier; if (val > maxVal) maxVal = val; } if (maxVal === 0) continue; if (bestVal !== undefined && maxVal < bestVal) continue; if (this.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = maxVal; 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(bestVal / 10000); 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() + 8) / obstructions.cellSize); else radius = Math.ceil(template.obstructionRadius() / 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 // or if we are ready to switch to city phase but miss some town phase structure if (gameState.currentPhase() < 3 && gameState.getGameType() !== "regicide") { if (gameState.currentPhase() < 2 || this.econState !== "cityPhasing") return; let requirements = gameState.getPhaseEntityRequirements(3); if (!requirements.length) return; for (let entityReq of requirements) if (gameState.getOwnStructures().filter(API3.Filters.byClass(entityReq.class)).length >= entityReq.count) 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.onStart = function(gameState) { gameState.ai.queueManager.changePriority("economicBuilding", gameState.ai.Config.priorities.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")); }; /** 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 === 0 || (gameState.isTemplateDisabled(gameState.applyCiv("structures/{civ}_field")) && nCorral < gameState.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 === 0) 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.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.isGo = function (gameState) { 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()) freeSlots += ent.getPopulationBonus(); if (gameState.ai.HQ.saveResources) return freeSlots <= 10; else if (gameState.getPopulation() > 55) return freeSlots <= 21; else if (gameState.getPopulation() > 30) return freeSlots <= 15; return freeSlots <= 10; }; queues.house.addPlan(plan); } if (numPlanned > 0 && this.econState == "townPhasing" && gameState.getPhaseEntityRequirements(2).length) { let houseTemplateName = gameState.applyCiv("structures/{civ}_house"); let houseTemplate = gameState.getTemplate(houseTemplateName); let needed = 0; for (let entityReq of gameState.getPhaseEntityRequirements(2)) { 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 (this.Config.debug > 1) API3.warn("no room to place a house ... try to be less restrictive"); this.stopBuilding.delete(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].isGo = function () { return true; }; --needed; } } if (this.requireHouses) { let houseTemplate = gameState.getTemplate(gameState.applyCiv("structures/{civ}_house")); if (gameState.getPhaseEntityRequirements(2).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.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; } } 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 if (this.stopBuilding.size > 1) { if (this.Config.debug > 2) API3.warn("try to build a new base because not enough room to build " + uneval(this.stopBuilding)); this.buildNewBase(gameState, queues); return; } // then 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 && gameState.currentPhase() == 1 && !gameState.isResearching(gameState.townPhase())) return false; if (gameState.getOwnFoundations().filter(API3.Filters.byClass("CivCentre")).hasEntities() || queues.civilCentre.hasQueuedUnits()) return false; let template = this.numActiveBase() > 0 ? this.bBase[0] : gameState.applyCiv("structures/{civ}_civil_centre"); if (!this.canBuild(gameState, template)) return false; // base "-1" means new base. if (this.Config.debug > 1) API3.warn("new base 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 && (gameState.currentPhase() > 2 || gameState.isResearching(gameState.cityPhase()))) { // 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 && 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.onStart = function(gameState) { gameState.ai.queueManager.changePriority("defenseBuilding", gameState.ai.Config.priorities.defenseBuilding); }; queues.defenseBuilding.addPlan(plan); return; } } } if (this.Config.Military.numSentryTowers && gameState.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 (gameState.currentPhase() < 2 || !this.canBuild(gameState, "structures/{civ}_defense_tower")) return; let numTowers = gameState.getOwnEntitiesByClass("DefenseTower", true).filter(API3.Filters.byClass("Town")).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 && 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.onStart = function(gameState) { gameState.ai.queueManager.changePriority("defenseBuilding", gameState.ai.Config.priorities.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; if (this.canBuild(gameState, "structures/{civ}_barracks")) { let barrackNb = gameState.getOwnEntitiesByClass("Barracks", true).length; if (this.saveResources && barrackNb > 0) return; // first barracks. if (!barrackNb && (gameState.getPopulation() > this.Config.Military.popForBarracks1 || (this.econState == "townPhasing" && gameState.getOwnStructures().filter(API3.Filters.byClass("Village")).length < 5))) { gameState.ai.queueManager.changePriority("militaryBuilding", 2*this.Config.priorities.militaryBuilding); let preferredBase = this.findBestBaseForMilitary(gameState); let plan = new m.ConstructionPlan(gameState, "structures/{civ}_barracks", { "preferredBase": preferredBase }); plan.onStart = function(gameState) { gameState.ai.queueManager.changePriority("militaryBuilding", gameState.ai.Config.priorities.militaryBuilding); }; queues.militaryBuilding.addPlan(plan); return; } // second barracks, then 3rd barrack, and optional 4th for some civs as they rely on barracks more. if (barrackNb == 1 && gameState.getPopulation() > this.Config.Military.popForBarracks2) { let preferredBase = this.findBestBaseForMilitary(gameState); queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_barracks", { "preferredBase": preferredBase })); return; } if (barrackNb == 2 && gameState.getPopulation() > this.Config.Military.popForBarracks2 + 20) { let preferredBase = this.findBestBaseForMilitary(gameState); queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_barracks", { "preferredBase": preferredBase })); return; } if (barrackNb == 3 && gameState.getPopulation() > this.Config.Military.popForBarracks2 + 50 && (gameState.getPlayerCiv() === "gaul" || gameState.getPlayerCiv() === "brit" || gameState.getPlayerCiv() === "iber")) { let preferredBase = this.findBestBaseForMilitary(gameState); queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_barracks", { "preferredBase": preferredBase })); return; } } if (this.saveResources) return; if (gameState.currentPhase() < 3 || 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()) { let preferredBase = this.findBestBaseForMilitary(gameState); queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, advanced, { "preferredBase": preferredBase })); } else // not a military building, but still use this queue queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, advanced)); return; } } }; /** * Construct military building in bases nearest to the ennemies TODO revisit as the nearest one may not be accessible */ m.HQ.prototype.findBestBaseForMilitary = function(gameState) { let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")).toEntityArray(); let bestBase = 1; let distMin = Math.min(); for (let cce of ccEnts) { if (gameState.isPlayerAlly(cce.owner())) continue; for (let cc of ccEnts) { if (cc.owner() != PlayerID) continue; let dist = API3.SquareVectorDistance(cc.position(), cce.position()); 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) { 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 (gameState.isTemplateDisabled(type)) { this.stopBuilding.set(type, Infinity); return false; } let template = gameState.getTemplate(type); if (!template) { this.stopBuilding.set(type, Infinity); if (this.Config.debug > 0) API3.warn("Petra error: trying to build " + structure + " for civ " + gameState.getPlayerCiv() + " but no template found."); } if (!template || !template.available(gameState)) return false; if (!gameState.findBuilder(type)) { this.stopBuild(gameState, type, 120); 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); return false; } } // build limits let limits = gameState.getEntityLimits(); let category = template.buildCategory(); if (category && limits[category] !== undefined && gameState.getEntityCounts()[category] >= limits[category]) 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.getMap(); 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 this.stopBuilding.clear(); // 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("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()) 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; }; /** * 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(); if (this.Config.debug > 1) { gameState.getOwnUnits().forEach (function (ent) { if (!ent.position()) return; m.dumpEntity(ent); }); } this.checkEvents(gameState, events, queues); this.researchManager.checkPhase(gameState, queues); // TODO find a better way to update if (this.currentPhase != gameState.currentPhase()) this.currentPhase = gameState.currentPhase(); if (this.numActiveBase() > 0) { this.trainMoreWorkers(gameState, queues); if (gameState.ai.playedTurn % 2 == 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.Config.difficulty > 0 && gameState.ai.playedTurn % 10 == 7 && gameState.currentPhase() > 1)) this.checkBaseExpansion(gameState, queues); if (gameState.currentPhase() > 1) { if (!this.canBarter) this.buildMarket(gameState, queues); if (!this.saveResources) { this.buildBlacksmith(gameState, queues); this.buildTemple(gameState, queues); } } this.tradeManager.update(gameState, events, queues); this.garrisonManager.update(gameState, events); this.defenseManager.update(gameState, events); 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 = { "econState": this.econState, "currentPhase": this.currentPhase, "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, "bBase": this.bBase, "bAdvanced": this.bAdvanced, "saveResources": this.saveResources, "saveSpace": this.saveSpace, "needCorral": this.needCorral, "needFarm": this.needFarm, "needFish": this.needFish, "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(" 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(), "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.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.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/mapModule.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/mapModule.js (revision 19512) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/mapModule.js (revision 19513) @@ -1,216 +1,216 @@ var PETRA = function(m) { /** map functions */ m.TERRITORY_PLAYER_MASK = 0x1F; m.TERRITORY_BLINKING_MASK = 0x40; m.createObstructionMap = function(gameState, accessIndex, template) { let passabilityMap = gameState.getMap(); let territoryMap = gameState.ai.territoryMap; let ratio = territoryMap.cellSize / passabilityMap.cellSize; // default values let placementType = "land"; let buildOwn = true; let buildAlly = true; let buildNeutral = true; let buildEnemy = false; // If there is a template then replace the defaults if (template) { placementType = template.buildPlacementType(); buildOwn = template.hasBuildTerritory("own"); buildAlly = template.hasBuildTerritory("ally"); buildNeutral = template.hasBuildTerritory("neutral"); buildEnemy = template.hasBuildTerritory("enemy"); } let obstructionTiles = new Uint8Array(passabilityMap.data.length); let passMap; let obstructionMask; if (placementType == "shore") { passMap = gameState.ai.accessibility.navalPassMap; obstructionMask = gameState.getPassabilityClassMask("building-shore"); } else { passMap = gameState.ai.accessibility.landPassMap; obstructionMask = gameState.getPassabilityClassMask("building-land"); } for (let k = 0; k < territoryMap.data.length; ++k) { - let tilePlayer = (territoryMap.data[k] & m.TERRITORY_PLAYER_MASK); + let tilePlayer = territoryMap.data[k] & m.TERRITORY_PLAYER_MASK; let isConnected = (territoryMap.data[k] & m.TERRITORY_BLINKING_MASK) == 0; if (tilePlayer === PlayerID) { if (!buildOwn || !buildNeutral && !isConnected) continue; } else if (gameState.isPlayerMutualAlly(tilePlayer)) { if (!buildAlly || !buildNeutral && !isConnected) continue; } else if (tilePlayer === 0) { if (!buildNeutral) continue; } else { if (!buildEnemy) continue; } let x = ratio * (k % territoryMap.width); - let y = ratio * (Math.floor(k / territoryMap.width)); + let y = ratio * Math.floor(k / territoryMap.width); for (let ix = 0; ix < ratio; ++ix) { for (let iy = 0; iy < ratio; ++iy) { let i = x + ix + (y + iy)*passabilityMap.width; if (placementType != "shore" && accessIndex && accessIndex !== passMap[i]) continue; if (!(passabilityMap.data[i] & obstructionMask)) obstructionTiles[i] = 255; } } } let map = new API3.Map(gameState.sharedScript, "passability", obstructionTiles); map.setMaxVal(255); if (template && template.buildDistance()) { let minDist = +template.buildDistance().MinDistance; let fromClass = template.buildDistance().FromClass; if (minDist && fromClass) { let cellSize = passabilityMap.cellSize; let cellDist = 1 + minDist / cellSize; let structures = gameState.getOwnStructures().filter(API3.Filters.byClass(fromClass)); for (let ent of structures.values()) { if (!ent.position()) continue; let pos = ent.position(); let x = Math.round(pos[0] / cellSize); let z = Math.round(pos[1] / cellSize); map.addInfluence(x, z, cellDist, -255, "constant"); } } } return map; }; m.createTerritoryMap = function(gameState) { let map = gameState.ai.territoryMap; let ret = new API3.Map(gameState.sharedScript, "territory", map.data); ret.getOwner = function(p) { return this.point(p) & m.TERRITORY_PLAYER_MASK; }; ret.getOwnerIndex = function(p) { return this.map[p] & m.TERRITORY_PLAYER_MASK; }; return ret; }; /** * The borderMap contains some border and frontier information: * - border of the map filled once: * - all mini-cells (1x1) from the big cell (8x8) inaccessibles => bit 0 * - inside a given distance to the border => bit 1 * - frontier of our territory (updated regularly in updateFrontierMap) * - narrow border (inside our territory) => bit 2 * - large border (inside our territory, exclusive of narrow) => bit 3 */ m.outside_Mask = 1; m.border_Mask = 2; m.fullBorder_Mask = m.outside_Mask | m.border_Mask; m.narrowFrontier_Mask = 4; m.largeFrontier_Mask = 8; m.fullFrontier_Mask = m.narrowFrontier_Mask | m.largeFrontier_Mask; m.createBorderMap = function(gameState) { let map = new API3.Map(gameState.sharedScript, "territory"); let width = map.width; let border = Math.round(80 / map.cellSize); let passabilityMap = gameState.sharedScript.passabilityMap; let obstructionMask = gameState.getPassabilityClassMask("unrestricted"); if (gameState.circularMap) { let ic = (width - 1) / 2; let radcut = (ic - border) * (ic - border); for (let j = 0; j < map.length; ++j) { let dx = j%width - ic; let dy = Math.floor(j/width) - ic; let radius = dx*dx + dy*dy; if (radius < radcut) continue; map.map[j] = m.outside_Mask; let ind = API3.getMapIndices(j, map, passabilityMap); for (let k of ind) { if (passabilityMap.data[k] & obstructionMask) continue; map.map[j] = m.border_Mask; break; } } } else { let borderCut = width - border; for (let j = 0; j < map.length; ++j) { let ix = j%width; let iy = Math.floor(j/width); if (ix < border || ix >= borderCut || iy < border || iy >= borderCut) { map.map[j] = m.outside_Mask; let ind = API3.getMapIndices(j, map, passabilityMap); for (let k of ind) { if (passabilityMap.data[k] & obstructionMask) continue; map.map[j] = m.border_Mask; break; } } } } // map.dumpIm("border.png", 5); return map; }; m.debugMap = function(gameState, map) { let width = map.width; let cell = map.cellSize; gameState.getEntities().forEach( function (ent) { let pos = ent.position(); if (!pos) return; let x = Math.round(pos[0] / cell); let z = Math.round(pos[1] / cell); let id = x + width*z; if (map.map[id] == 1) Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [2,0,0]}); else if (map.map[id] == 2) Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,2,0]}); else if (map.map[id] == 3) Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,0,2]}); }); }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueManager.js (revision 19512) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueManager.js (revision 19513) @@ -1,599 +1,599 @@ var PETRA = function(m) { // This takes the input queues and picks which items to fund with resources until no more resources are left to distribute. // // Currently this manager keeps accounts for each queue, split between the 4 main resources // // Each time resources are available (ie not in any account), it is split between the different queues // Mostly based on priority of the queue, and existing needs. // Each turn, the queue Manager checks if a queue can afford its next item, then it does. // // A consequence of the system it's not really revertible. Once a queue has an account of 500 food, it'll keep it // If for some reason the AI stops getting new food, and this queue lacks, say, wood, no other queues will // be able to benefit form the 500 food (even if they only needed food). // This is not to annoying as long as all goes well. If the AI loses many workers, it starts being problematic. // // It also has the effect of making the AI more or less always sit on a few hundreds resources since most queues // get some part of the total, and if all queues have 70% of their needs, nothing gets done // Particularly noticeable when phasing: the AI often overshoots by a good 200/300 resources before starting. // // This system should be improved. It's probably not flexible enough. m.QueueManager = function(Config, queues) { this.Config = Config; this.queues = queues; this.priorities = {}; for (let i in Config.priorities) this.priorities[i] = Config.priorities[i]; this.accounts = {}; // the sorting is updated on priority change. this.queueArrays = []; for (let q in this.queues) { this.accounts[q] = new API3.Resources(); this.queueArrays.push([q, this.queues[q]]); } let priorities = this.priorities; this.queueArrays.sort((a,b) => priorities[b[0]] - priorities[a[0]]); }; m.QueueManager.prototype.getAvailableResources = function(gameState) { let resources = gameState.getResources(); for (let key in this.queues) resources.subtract(this.accounts[key]); return resources; }; m.QueueManager.prototype.getTotalAccountedResources = function() { let resources = new API3.Resources(); for (let key in this.queues) resources.add(this.accounts[key]); return resources; }; m.QueueManager.prototype.currentNeeds = function(gameState) { let needed = new API3.Resources(); //queueArrays because it's faster. for (let q of this.queueArrays) { let queue = q[1]; if (!queue.hasQueuedUnits() || !queue.plans[0].isGo(gameState)) continue; let costs = queue.plans[0].getCost(); needed.add(costs); } // get out current resources, not removing accounts. let current = gameState.getResources(); for (let res of needed.types) needed[res] = Math.max(0, needed[res] - current[res]); return needed; }; // calculate the gather rates we'd want to be able to start all elements in our queues // TODO: many things. m.QueueManager.prototype.wantedGatherRates = function(gameState) { // default values for first turn when we have not yet set our queues. if (gameState.ai.playedTurn === 0) { let ret = {}; for (let res of gameState.sharedScript.resourceInfo.codes) ret[res] = this.Config.queues.firstTurn[res] || this.Config.queues.firstTurn.default; return ret; } // get out current resources, not removing accounts. let current = gameState.getResources(); // short queue is the first item of a queue, assumed to be ready in 30s // medium queue is the second item of a queue, assumed to be ready in 60s // long queue contains the isGo=false items, assumed to be ready in 300s let totalShort = {}; let totalMedium = {}; let totalLong = {}; for (let res of gameState.sharedScript.resourceInfo.codes) { totalShort[res] = this.Config.queues.short[res] || this.Config.queues.short.default; totalMedium[res] = this.Config.queues.medium[res] || this.Config.queues.medium.default; totalLong[res] = this.Config.queues.long[res] || this.Config.queues.long.default; } let total; //queueArrays because it's faster. for (let q of this.queueArrays) { let queue = q[1]; if (queue.paused) continue; for (let j = 0; j < queue.length(); ++j) { if (j > 1) break; let cost = queue.plans[j].getCost(); if (queue.plans[j].isGo(gameState)) { if (j === 0) total = totalShort; else total = totalMedium; } else total = totalLong; for (let type in total) total[type] += cost[type]; if (!queue.plans[j].isGo(gameState)) break; } } // global rates let rates = {}; let diff; for (let res of gameState.sharedScript.resourceInfo.codes) { if (current[res] > 0) { diff = Math.min(current[res], totalShort[res]); totalShort[res] -= diff; current[res] -= diff; if (current[res] > 0) { diff = Math.min(current[res], totalMedium[res]); totalMedium[res] -= diff; current[res] -= diff; if (current[res] > 0) totalLong[res] -= Math.min(current[res], totalLong[res]); } } rates[res] = totalShort[res]/30 + totalMedium[res]/60 + totalLong[res]/300; } return rates; }; m.QueueManager.prototype.printQueues = function(gameState) { let numWorkers = 0; gameState.getOwnUnits().forEach (function (ent) { if (ent.getMetadata(PlayerID, "role") === "worker" && ent.getMetadata(PlayerID, "plan") === undefined) numWorkers++; }); API3.warn("---------- QUEUES ------------ with pop " + gameState.getPopulation() + " and workers " + numWorkers); for (let i in this.queues) { let q = this.queues[i]; if (q.hasQueuedUnits()) { API3.warn(i + ": ( with priority " + this.priorities[i] +" and accounts " + uneval(this.accounts[i]) +")"); API3.warn(" while maxAccountWanted(0.6) is " + uneval(q.maxAccountWanted(gameState, 0.6))); } for (let plan of q.plans) { let qStr = " " + plan.type + " "; if (plan.number) qStr += "x" + plan.number; qStr += " isGo " + plan.isGo(gameState); API3.warn(qStr); } } API3.warn("Accounts"); for (let p in this.accounts) API3.warn(p + ": " + uneval(this.accounts[p])); API3.warn("Current Resources: " + uneval(gameState.getResources())); API3.warn("Available Resources: " + uneval(this.getAvailableResources(gameState))); API3.warn("Wanted Gather Rates: " + uneval(this.wantedGatherRates(gameState))); API3.warn("Current Gather Rates: " + uneval(gameState.ai.HQ.GetCurrentGatherRates(gameState))); API3.warn("Most needed resources: " + uneval(gameState.ai.HQ.pickMostNeededResources(gameState))); API3.warn("------------------------------------"); }; m.QueueManager.prototype.clear = function() { for (let i in this.queues) this.queues[i].empty(); }; /** * set accounts of queue i from the unaccounted resources */ m.QueueManager.prototype.setAccounts = function(gameState, cost, i) { let available = this.getAvailableResources(gameState); for (let res of this.accounts[i].types) { if (this.accounts[i][res] >= cost[res]) continue; this.accounts[i][res] += Math.min(available[res], cost[res] - this.accounts[i][res]); } }; /** * transfer accounts from queue i to queue j */ m.QueueManager.prototype.transferAccounts = function(cost, i, j) { for (let res of this.accounts[i].types) { if (this.accounts[j][res] >= cost[res]) continue; let diff = Math.min(this.accounts[i][res], cost[res] - this.accounts[j][res]); this.accounts[i][res] -= diff; this.accounts[j][res] += diff; } }; /** * distribute the resources between the different queues according to their priorities */ m.QueueManager.prototype.distributeResources = function(gameState) { let availableRes = this.getAvailableResources(gameState); for (let res of availableRes.types) { if (availableRes[res] < 0) // rescale the accounts if we've spent resources already accounted (e.g. by bartering) { let total = gameState.getResources()[res]; let scale = total / (total - availableRes[res]); availableRes[res] = total; for (let j in this.queues) { this.accounts[j][res] = Math.floor(scale * this.accounts[j][res]); availableRes[res] -= this.accounts[j][res]; } } if (!availableRes[res]) { this.switchResource(gameState, res); continue; } let totalPriority = 0; let tempPrio = {}; let maxNeed = {}; // Okay so this is where it gets complicated. // If a queue requires "res" for the next elements (in the queue) // And the account is not high enough for it. // Then we add it to the total priority. // To try and be clever, we don't want a long queue to hog all resources. So two things: // -if a queue has enough of resource X for the 1st element, its priority is decreased (factor 2). // -queues accounts are capped at "resources for the first + 60% of the next" // This avoids getting a high priority queue with many elements hogging all of one resource // uselessly while it awaits for other resources. for (let j in this.queues) { // returns exactly the correct amount, ie 0 if we're not go. let queueCost = this.queues[j].maxAccountWanted(gameState, 0.6); if (this.queues[j].hasQueuedUnits() && this.accounts[j][res] < queueCost[res] && !this.queues[j].paused) { // adding us to the list of queues that need an update. tempPrio[j] = this.priorities[j]; maxNeed[j] = queueCost[res] - this.accounts[j][res]; // if we have enough of that resource for our first item in the queue, diminish our priority. if (this.accounts[j][res] >= this.queues[j].getNext().getCost()[res]) tempPrio[j] /= 2; if (tempPrio[j]) totalPriority += tempPrio[j]; } else if (this.accounts[j][res] > queueCost[res]) { - availableRes[res] += (this.accounts[j][res] - queueCost[res]); + availableRes[res] += this.accounts[j][res] - queueCost[res]; this.accounts[j][res] = queueCost[res]; } } // Now we allow resources to the accounts. We can at most allow "TempPriority/totalpriority*available" // But we'll sometimes allow less if that would overflow. let available = availableRes[res]; let missing = false; for (let j in tempPrio) { // we'll add at much what can be allowed to this queue. let toAdd = Math.floor(availableRes[res] * tempPrio[j]/totalPriority); if (toAdd >= maxNeed[j]) toAdd = maxNeed[j]; else missing = true; this.accounts[j][res] += toAdd; maxNeed[j] -= toAdd; available -= toAdd; } if (missing && available > 0) // distribute the rest (due to floor) in any queue { for (let j in tempPrio) { let toAdd = Math.min(maxNeed[j], available); this.accounts[j][res] += toAdd; available -= toAdd; if (available <= 0) break; } } if (available < 0) API3.warn("Petra: problem with remaining " + res + " in queueManager " + available); } }; m.QueueManager.prototype.switchResource = function(gameState, res) { // We have no available resources, see if we can't "compact" them in one queue. // compare queues 2 by 2, and if one with a higher priority could be completed by our amount, give it. // TODO: this isn't perfect compression. for (let j in this.queues) { if (!this.queues[j].hasQueuedUnits() || this.queues[j].paused) continue; let queue = this.queues[j]; let queueCost = queue.maxAccountWanted(gameState, 0); if (this.accounts[j][res] >= queueCost[res]) continue; for (let i in this.queues) { if (i === j) continue; let otherQueue = this.queues[i]; if (this.priorities[i] >= this.priorities[j] || otherQueue.switched !== 0) continue; if (this.accounts[j][res] + this.accounts[i][res] < queueCost[res]) continue; let diff = queueCost[res] - this.accounts[j][res]; this.accounts[j][res] += diff; this.accounts[i][res] -= diff; ++otherQueue.switched; if (this.Config.debug > 2) API3.warn ("switching queue " + res + " from " + i + " to " + j + " in amount " + diff); break; } } }; // Start the next item in the queue if we can afford it. m.QueueManager.prototype.startNextItems = function(gameState) { for (let q of this.queueArrays) { let name = q[0]; let queue = q[1]; if (queue.hasQueuedUnits() && !queue.paused) { let item = queue.getNext(); if (this.accounts[name].canAfford(item.getCost()) && item.canStart(gameState)) { // canStart may update the cost because of the costMultiplier so we must check it again if (this.accounts[name].canAfford(item.getCost())) { this.finishingTime = gameState.ai.elapsedTime; this.accounts[name].subtract(item.getCost()); queue.startNext(gameState); queue.switched = 0; } } } else if (!queue.hasQueuedUnits()) { this.accounts[name].reset(); queue.switched = 0; } } }; m.QueueManager.prototype.update = function(gameState) { Engine.ProfileStart("Queue Manager"); for (let i in this.queues) { this.queues[i].check(gameState); // do basic sanity checks on the queue if (this.priorities[i] > 0) continue; API3.warn("QueueManager received bad priorities, please report this error: " + uneval(this.priorities)); this.priorities[i] = 1; // TODO: make the Queue Manager not die when priorities are zero. } // Pause or unpause queues depending on the situation this.checkPausedQueues(gameState); // Let's assign resources to plans that need them this.distributeResources(gameState); // Start the next item in the queue if we can afford it. this.startNextItems(gameState); if (this.Config.debug > 1 && gameState.ai.playedTurn%50 === 0) this.printQueues(gameState); Engine.ProfileStop(); }; // Recovery system: if short of workers after an attack, pause (and reset) some queues to favor worker training m.QueueManager.prototype.checkPausedQueues = function(gameState) { let numWorkers = gameState.countOwnEntitiesAndQueuedWithRole("worker"); let workersMin = Math.min(Math.max(12, 24 * this.Config.popScaling), this.Config.Economy.popForTown); for (let q in this.queues) { let toBePaused = false; if (gameState.ai.HQ.numActiveBase() === 0) - toBePaused = (q !== "dock" && q !== "civilCentre"); + toBePaused = q !== "dock" && q !== "civilCentre"; else if (numWorkers < workersMin / 3) - toBePaused = (q !== "citizenSoldier" && q !== "villager" && q !== "emergency"); + toBePaused = q !== "citizenSoldier" && q !== "villager" && q !== "emergency"; else if (numWorkers < workersMin * 2 / 3) - toBePaused = (q === "civilCentre" || q === "economicBuilding" || + toBePaused = q === "civilCentre" || q === "economicBuilding" || q === "militaryBuilding" || q === "defenseBuilding" || q === "healer" || - q === "majorTech" || q === "minorTech" || q.indexOf("plan_") !== -1); + q === "majorTech" || q === "minorTech" || q.indexOf("plan_") !== -1; else if (numWorkers < workersMin) - toBePaused = (q === "civilCentre" || q === "defenseBuilding" || - q == "majorTech" || q.indexOf("_siege") != -1 || q.indexOf("_champ") != -1); + toBePaused = q === "civilCentre" || q === "defenseBuilding" || + q == "majorTech" || q.indexOf("_siege") != -1 || q.indexOf("_champ") != -1; if (toBePaused) { if (q === "field" && gameState.ai.HQ.needFarm && !gameState.getOwnStructures().filter(API3.Filters.byClass("Field")).hasEntities()) toBePaused = false; if (q === "corral" && gameState.ai.HQ.needCorral && !gameState.getOwnStructures().filter(API3.Filters.byClass("Field")).hasEntities()) toBePaused = false; if (q === "dock" && gameState.ai.HQ.needFish && !gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")).hasEntities()) toBePaused = false; if (q === "ships" && gameState.ai.HQ.needFish && !gameState.ai.HQ.navalManager.ships.filter(API3.Filters.byClass("FishingBoat")).hasEntities()) toBePaused = false; } let queue = this.queues[q]; if (!queue.paused && toBePaused) { queue.paused = true; this.accounts[q].reset(); } else if (queue.paused && !toBePaused) queue.paused = false; // And reduce the batch sizes of attack queues if (q.indexOf("plan_") != -1 && numWorkers < workersMin && queue.plans[0]) { queue.plans[0].number = 1; if (queue.plans[1]) queue.plans[1].number = 1; } } }; m.QueueManager.prototype.canAfford = function(queue, cost) { if (!this.accounts[queue]) return false; return this.accounts[queue].canAfford(cost); }; m.QueueManager.prototype.pauseQueue = function(queue, scrapAccounts) { if (!this.queues[queue]) return; this.queues[queue].paused = true; if (scrapAccounts) this.accounts[queue].reset(); }; m.QueueManager.prototype.unpauseQueue = function(queue) { if (this.queues[queue]) this.queues[queue].paused = false; }; m.QueueManager.prototype.pauseAll = function(scrapAccounts, but) { for (let q in this.queues) { if (q == but) continue; if (scrapAccounts) this.accounts[q].reset(); this.queues[q].paused = true; } }; m.QueueManager.prototype.unpauseAll = function(but) { for (let q in this.queues) if (q != but) this.queues[q].paused = false; }; m.QueueManager.prototype.addQueue = function(queueName, priority) { if (this.queues[queueName] !== undefined) return; this.queues[queueName] = new m.Queue(); this.priorities[queueName] = priority; this.accounts[queueName] = new API3.Resources(); this.queueArrays = []; for (let q in this.queues) this.queueArrays.push([q, this.queues[q]]); let priorities = this.priorities; this.queueArrays.sort((a,b) => priorities[b[0]] - priorities[a[0]]); }; m.QueueManager.prototype.removeQueue = function(queueName) { if (this.queues[queueName] === undefined) return; delete this.queues[queueName]; delete this.priorities[queueName]; delete this.accounts[queueName]; this.queueArrays = []; for (let q in this.queues) this.queueArrays.push([q, this.queues[q]]); let priorities = this.priorities; this.queueArrays.sort((a,b) => priorities[b[0]] - priorities[a[0]]); }; m.QueueManager.prototype.getPriority = function(queueName) { return this.priorities[queueName]; }; m.QueueManager.prototype.changePriority = function(queueName, newPriority) { if (this.Config.debug > 1) API3.warn(">>> Priority of queue " + queueName + " changed from " + this.priorities[queueName] + " to " + newPriority); if (this.queues[queueName] !== undefined) this.priorities[queueName] = newPriority; let priorities = this.priorities; this.queueArrays.sort((a,b) => priorities[b[0]] - priorities[a[0]]); }; m.QueueManager.prototype.Serialize = function() { let accounts = {}; let queues = {}; for (let q in this.queues) { queues[q] = this.queues[q].Serialize(); accounts[q] = this.accounts[q].Serialize(); if (this.Config.debug == -100) API3.warn("queueManager serialization: queue " + q + " >>> " + uneval(queues[q]) + " with accounts " + uneval(accounts[q])); } return { "priorities": this.priorities, "queues": queues, "accounts": accounts }; }; m.QueueManager.prototype.Deserialize = function(gameState, data) { this.priorities = data.priorities; this.queues = {}; this.accounts = {}; // the sorting is updated on priority change. this.queueArrays = []; for (let q in data.queues) { this.queues[q] = new m.Queue(); this.queues[q].Deserialize(gameState, data.queues[q]); this.accounts[q] = new API3.Resources(); this.accounts[q].Deserialize(data.accounts[q]); this.queueArrays.push([q, this.queues[q]]); } this.queueArrays.sort((a,b) => data.priorities[b[0]] - data.priorities[a[0]]); }; 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 19512) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js (revision 19513) @@ -1,801 +1,801 @@ 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; }; 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); 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.buildCategory() === "Dock") { // 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.buildCategory() === "Dock") return this.findDockPosition(gameState); if (template.hasClass("Storehouse") && this.metadata.base) { // recompute the best dropsite location in case some conditions have changed let base = gameState.ai.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 = gameState.ai.HQ.findEconomicCCLocation(gameState, template, this.metadata.resource, proximity); } else pos = gameState.ai.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 = gameState.ai.HQ.findDefensiveLocation(gameState, template); if (pos) return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": pos[2] }; if (template.hasClass("DefenseTower") || gameState.getPlayerCiv() === "mace" || gameState.getPlayerCiv() === "maur" || gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_fortress"), true) > 0 || gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_army_camp"), true) > 0) return false; // if this fortress is our first siege unit builder, just try the standard placement as we want siege units } else if (template.hasClass("Market")) // Docks (i.e. NavalMarket) are done before { let pos = gameState.ai.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. townPhasing 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 (gameState.ai.HQ.basesMap.map[j] == base) placement.map[j] = 45; } else { for (let j = 0; j < placement.map.length; ++j) if (gameState.ai.HQ.basesMap.map[j] !== 0) placement.map[j] = 45; } if (!gameState.ai.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); if (ent.resourceDropsiteTypes() && ent.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.genericName() == "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 (gameState.ai.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 preferredBase = this.metadata && this.metadata.preferredBase; if (this.metadata && this.metadata.base !== undefined) { let base = this.metadata.base; for (let j = 0; j < placement.map.length; ++j) { if (gameState.ai.HQ.basesMap.map[j] != base) placement.map[j] = 0; else if (favorBorder && gameState.ai.HQ.borderMap.map[j] & m.border_Mask) placement.map[j] += 50; else if (disfavorBorder && !(gameState.ai.HQ.borderMap.map[j] & m.fullBorder_Mask) && placement.map[j] > 0) placement.map[j] += 10; if (placement.map[j] > 0) { let x = (j % placement.width + 0.5) * cellSize; let z = (Math.floor(j / placement.width) + 0.5) * cellSize; if (gameState.ai.HQ.isNearInvadingArmy([x, z])) placement.map[j] = 0; } } } else { for (let j = 0; j < placement.map.length; ++j) { if (gameState.ai.HQ.basesMap.map[j] === 0) placement.map[j] = 0; else if (favorBorder && gameState.ai.HQ.borderMap.map[j] & m.border_Mask) placement.map[j] += 50; else if (disfavorBorder && !(gameState.ai.HQ.borderMap.map[j] & m.fullBorder_Mask) && placement.map[j] > 0) placement.map[j] += 10; if (preferredBase && gameState.ai.HQ.basesMap.map[j] == this.metadata.preferredBase) placement.map[j] += 200; if (placement.map[j] > 0) { let x = (j % placement.width + 0.5) * cellSize; let z = (Math.floor(j / placement.width) + 0.5) * cellSize; if (gameState.ai.HQ.isNearInvadingArmy([x, z])) placement.map[j] = 0; } } } // 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.buildCategory() + "_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() + 12) / obstructions.cellSize); else if (template.resourceDropsiteTypes() === undefined && !template.hasClass("House") && !template.hasClass("Field")) radius = Math.ceil((template.obstructionRadius() + 4) / obstructions.cellSize); else radius = Math.ceil((template.obstructionRadius() + 0.5) / obstructions.cellSize); let bestTile; let bestVal; if (template.hasClass("House") && !alreadyHasHouses) { // try to get some space to place several houses first bestTile = placement.findBestTile(3*radius, obstructions); bestVal = bestTile[1]; } if (bestVal === undefined || bestVal === -1) { bestTile = placement.findBestTile(radius, obstructions); bestVal = bestTile[1]; } let bestIdx = bestTile[0]; if (bestVal <= 0) return false; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; if (template.hasClass("House") || template.hasClass("Field") || template.resourceDropsiteTypes() !== undefined) { let secondBest = obstructions.findNearestObstructed(bestIdx, radius); if (secondBest >= 0) { x = (secondBest % obstructions.width + 0.5) * obstructions.cellSize; z = (Math.floor(secondBest / 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": gameState.ai.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.buildCategory() + "_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() / 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 = (i/numPoints)*2*Math.PI; + 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.Serialize = function() { let prop = { "category": this.category, "type": this.type, "ID": this.ID, "metadata": this.metadata, "cost": this.cost.Serialize(), "number": this.number, "position": this.position }; let func = { "isGo": uneval(this.isGo), "onStart": uneval(this.onStart) }; return { "prop": prop, "func": func }; }; m.ConstructionPlan.prototype.Deserialize = function(gameState, data) { for (let key in data.prop) this[key] = data.prop[key]; let cost = new API3.Resources(); cost.Deserialize(data.prop.cost); this.cost = cost; for (let fun in data.func) this[fun] = eval(data.func[fun]); }; 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 19512) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/tradeManager.js (revision 19513) @@ -1,666 +1,666 @@ 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 (!this.hasTradeRoute() || !ent.isIdle() || !ent.position()) return; if (ent.getMetadata(PlayerID, "transport") !== undefined) return; 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 prices = gameState.getBarterPrices(); // calculates conversion rates let getBarterRate = function (prices,buy,sell) { return 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(prices, 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(prices, 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(prices, 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"); 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"); 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"); 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"); return true; } return false; }; /** * 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()).toEntityArray(); let market2 = gameState.updatingCollection("ExclusiveAllyMarkets", API3.Filters.byClass("Market"), gameState.getExclusiveAllyEntities()).toEntityArray(); if (market1.length + market2.length < 2) // We have to wait ... markets will be built soon { this.tradeRoute = undefined; this.potentialTradeRoute = undefined; return false; } if (!market2.length) market2 = market1; let candidate = { "gain": 0 }; let potential = { "gain": 0 }; let bestIndex = { "gain": 0 }; let bestLand = { "gain": 0 }; let traderTemplatesGains = gameState.getTraderTemplatesGains(); for (let m1 of market1) { 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) { if (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; + let sea = sea1 && sea1 == sea2 ? sea1 : undefined; if (!land && !sea) continue; let gainMultiplier; if (land && traderTemplatesGains.landGainMultiplier) gainMultiplier = traderTemplatesGains.landGainMultiplier; else if (sea && traderTemplatesGains.navalGainMultiplier) gainMultiplier = traderTemplatesGains.navalGainMultiplier; else continue; let gain = Math.round(API3.SquareVectorDistance(m1.position(), m2.position()) * gainMultiplier / 10000); 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.stopMoving(); ent.setMetadata(PlayerID, "route", undefined); } }; 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()).length && !gameState.updatingCollection("ExclusiveAllyMarkets", API3.Filters.byClass("Market"), gameState.getExclusiveAllyEntities()).length) 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"); 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.onStart = function(gameState) { gameState.ai.queueManager.changePriority("economicBuilding", gameState.ai.Config.priorities.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); let self = this; if (this.checkEvents(gameState, events)) // true if one market was built or destroyed { this.traders.forEach(function(ent) { self.checkTrader(gameState, ent); }); this.checkRoutes(gameState); } if (this.tradeRoute) { this.traders.forEach(function(ent) { self.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]; + 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]; + 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) { this.tradeRoute = this.routeIdToEnt(gameState, data.tradeRoute); this.potentialTradeRoute = this.routeIdToEnt(gameState, data.potentialTradeRoute); this.routeProspection = data.routeProspection; this.targetNumTraders = data.targetNumTraders; this.warnedAllies = data.warnedAllies; }; return m; }(PETRA);