Index: binaries/data/mods/public/simulation/ai/common-api/gamestate.js =================================================================== --- binaries/data/mods/public/simulation/ai/common-api/gamestate.js +++ binaries/data/mods/public/simulation/ai/common-api/gamestate.js @@ -23,7 +23,7 @@ this.alliedVictory = SharedScript.alliedVictory; this.ceasefireActive = SharedScript.ceasefireActive; this.ceasefireTimeRemaining = SharedScript.ceasefireTimeRemaining; - + this.emergencyState = new Map(); // get the list of possible phases for this civ: // we assume all of them are researchable from the civil center this.phases = []; Index: binaries/data/mods/public/simulation/ai/petra/attackManager.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/attackManager.js +++ binaries/data/mods/public/simulation/ai/petra/attackManager.js @@ -556,7 +556,7 @@ /** * Target the player with the most advanced wonder. - * TODO currently the first built wonder is kept, should chek on the minimum wonderDuration left instead. + * TODO currently the first built wonder is kept, should check on the minimum wonderDuration left instead. */ PETRA.AttackManager.prototype.getWonderEnemyPlayer = function(gameState, attack) { @@ -737,6 +737,21 @@ return true; }; +PETRA.AttackManager.prototype.updateEmergency = function(gameState) { + // First call will stop all attacks, the second call to this + // function will essentially be a no-op. + for (const attackType in this.upcomingAttacks) + for (const attack of this.upcomingAttacks[attackType]) + attack.targetPlayer = undefined; + for (const attackType in this.startedAttacks) + for (var i = 0; i < this.startedAttacks[attackType].length; ++i) + { + const attack = this.startedAttacks[attackType][i]; + attack.Abort(gameState); + this.startedAttacks[attackType].splice(i--, 1); + } +}; + PETRA.AttackManager.prototype.Serialize = function() { let properties = { Index: binaries/data/mods/public/simulation/ai/petra/attackPlan.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/attackPlan.js +++ binaries/data/mods/public/simulation/ai/petra/attackPlan.js @@ -2131,6 +2131,10 @@ */ PETRA.AttackPlan.prototype.getAttackAccess = function(gameState) { + // JCWASMX86_TODO: Not sure why it is needed, but otherwise + // it would throw errors. + if (this.position == undefined) + return 0; for (let ent of this.unitCollection.filterNearest(this.position, 1).values()) return PETRA.getLandAccess(gameState, ent); Index: binaries/data/mods/public/simulation/ai/petra/buildManager.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/buildManager.js +++ binaries/data/mods/public/simulation/ai/petra/buildManager.js @@ -21,6 +21,14 @@ this.incrementBuilderCounters(civ, ent, 1); }; +PETRA.BuildManager.prototype.exitEmergency = function(gameState) +{ + // JCWASMX86_TODO: Check, whether this breaks anything + this.builderCounters = new Map(); + this.unbuildables = new Map(); + this.init(gameState); +}; + PETRA.BuildManager.prototype.incrementBuilderCounters = function(civ, ent, increment) { for (let buildable of ent.buildableEntities(civ)) Index: binaries/data/mods/public/simulation/ai/petra/config.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/config.js +++ binaries/data/mods/public/simulation/ai/petra/config.js @@ -6,7 +6,7 @@ // for instance "balanced", "aggressive" or "defensive" this.behavior = behavior || "random"; - // debug level: 0=none, 1=sanity checks, 2=debug, 3=detailed debug, -100=serializatio debug + // debug level: 0=none, 1=sanity checks, 2=debug, 3=detailed debug, -100=serialization debug this.debug = 0; this.chat = true; // false to prevent AI's chats @@ -159,6 +159,136 @@ }; this.garrisonHealthLevel = { "low": 0.4, "medium": 0.55, "high": 0.7 }; + + // In the emergency mode, the range around the position + // of all units in which no enemy should be to consider it + // free from enemies. + this.enemyDetectionRange = 55; + + // Number of civic centres to lose until emergency + this.civicCentreLossTrigger = 2; + + // Factors determining, how many percent of structures or + // population have to be around after a certain timespan to + // avoid triggering an emergency. + // [,] + this.emergencyFactors = [ + // Sandbox, never emergency because of huge losses + [0.0, 0.0], + // Very easy + [0.8, 0.8], + // Easy + [0.7, 0.7], + // Medium + [0.6, 0.6], + // Hard + [0.2, 0.2], + // Very hard, never emergency because of huge losses + [0.0, 0.0] + ]; + + // How much of each resource should be saved, when sending + // tributes in case of emergency. + this.retainedResourcesAfterTribute = 500; + + // How long to wait until the neutrality requests expire. + this.neutralityRequestWaitingDuration = 30; + + // The size of the area around the collect point in case + // of emergency. + this.patrouilleRange = 75; + + // If this percentage was killed in emergency mode, resign if + // this bot has defensive personality. + this.lossesForResign = 0.8; + + // These are single phases used to check for steady decline. + // Each one has a key with the max population and an array. + // Each number in this array is a "phase". This phase is reached, + // if this amount of population is reached. + // If the population is reduced, the phase is reduced, too. + // If the phase is reduced by phasesToLoseUntilEmergency, this is an emergency. + this.phasesForSteadyDecline = + { + "50": [ + 10, + 20, + ], + "100": [ + 40, + 65, + 85 + ], + "150": [ + 40, + 65, + 85, + 120 + ], + "200": [ + 40, + 100, + 140, + 170 + ], + "250": [ + 40, + 100, + 140, + 170, + 220 + ], + "300": [ + 50, + 150, + 190, + 225, + 260 + ], + "400": [ + 75, + 125, + 175, + 225, + 275 + ], + "500": [ + 100, + 175, + 275, + 350, + 450 + ], + "600": [ + 125, + 200, + 300, + 400, + 500, + 575 + ] + }; + + // If this amount of phases is lost, trigger an emergency. + this.phasesToLoseUntilEmergency = 2; + + // If the bot is attacked, this sets the delay for + // checking, how much percent of units/structures were lost. + // If this value is higher, it is easier, if it is lower, + // it is more difficult. + this.fastDestructionDelay = 60; + + // Limit how long the troops are maximum marching to get to the + // collect point. + this.maximumMarchingDuration = 100; + + // How often to check, whether the AI should resign, after it + // collected on one point. + this.resignCheckDelay = 5; + + // How often the AI should check, whether there were any losses + // in emergency mode until it returns to normal. + this.defensiveStateDuration = 20; }; PETRA.Config.prototype.setConfig = function(gameState) Index: binaries/data/mods/public/simulation/ai/petra/defenseManager.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/defenseManager.js +++ binaries/data/mods/public/simulation/ai/petra/defenseManager.js @@ -14,6 +14,15 @@ this.attackedAllies = {}; }; +PETRA.DefenseManager.prototype.exitEmergency = function() +{ + // JCWASMX86_TODO: Check, whether this breaks anything + this.armies = []; + this.targetList = []; + this.attackingArmies = {}; + this.attackingUnits = {}; + this.attackedAllies = {}; +}; PETRA.DefenseManager.prototype.update = function(gameState, events) { Engine.ProfileStart("Defense Manager"); Index: binaries/data/mods/public/simulation/ai/petra/diplomacyManager.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/diplomacyManager.js +++ binaries/data/mods/public/simulation/ai/petra/diplomacyManager.js @@ -32,6 +32,8 @@ this.receivedDiplomacyRequests = new Map(); this.sentDiplomacyRequests = new Map(); this.sentDiplomacyRequestLapseTime = 120 + randFloat(10, 100); + this.waitsForResponses = false; + this.responseCounter = -1; }; /** @@ -519,6 +521,108 @@ } }; +PETRA.DiplomacyManager.prototype.updateEmergency = function(gameState, events) +{ + const personality = this.Config.personality; + if (personality.aggressive < personality.defensive && + !gameState.sharedScript.playersData[PlayerID].teamsLocked && + personality.cooperative >= 0.5 && + this.enoughResourcesForTributes(gameState) && + !this.waitsForResponses && + !gameState.ai.HQ.EmergencyManager.troopsMarching(gameState)) + { + API3.warn("defensive + !teamsLocked + cooperative + enoughResources + !waits"); + if (!this.waitsForResponses) + { + this.tryGettingNeutral(gameState); + this.responseCounter = 0; + API3.warn("Sent neutrality requests!"); + this.waitsForResponses = true; + } + } + this.checkEvents(gameState, events); + if (this.waitsForResponses && + gameState.getEnemies().length && + this.responseCounter < this.Config.neutralityRequestWaitingDuration) + { + API3.warn("Response counter: " + this.responseCounter + "/" + this.Config.neutralityRequestWaitingDuration); + this.responseCounter++; + } +}; + +PETRA.DiplomacyManager.prototype.expiredNeutralityRequest = function() +{ + return this.responseCounter != -1 && this.responseCounter == this.Config.neutralityRequestWaitingDuration; +}; + +PETRA.DiplomacyManager.prototype.expireNeutralityRequests = function(gameState) +{ + for (const [player, data] of this.sentDiplomacyRequests) + { + if (data.requestType == "neutral") + { + PETRA.chatNewRequestDiplomacy(gameState, player, data.requestType, "requestExpired"); + this.sentDiplomacyRequests.delete(player); + } + } +}; + +PETRA.DiplomacyManager.prototype.tryGettingNeutral = function(gameState) +{ + const enemies = gameState.getEnemies(); + let numEnemies = gameState.getNumPlayerEnemies(); + for (const enemy of enemies) + { + // Exclude Gaia and defeated enemies + if (gameState.ai.HQ.attackManager.defeated[enemy] || enemy == 0) + { + numEnemies--; + continue; + } + gameState.ai.HQ.attackManager.cancelAttacksAgainstPlayer(gameState, enemy); + this.sentDiplomacyRequests.set(enemy, { + "requestType": "neutral", + "timeSent": gameState.ai.elapsedTime + }); + API3.warn("Sending neutrality request!"); + Engine.PostCommand(PlayerID, { "type": "tribute", "player": enemy, "amounts": this.buildTributeForGettingNeutral(gameState, numEnemies) }); + Engine.PostCommand(PlayerID, { "type": "diplomacy-request", "source": PlayerID, "player": enemy, "to": "neutral" }); + PETRA.chatNewRequestDiplomacy(gameState, enemy, "neutral", "sendRequest"); + numEnemies--; + } +}; + +PETRA.DiplomacyManager.prototype.buildTributeForGettingNeutral = function(gameState, numEnemies) +{ + const availableResources = gameState.ai.queueManager.getAvailableResources(gameState); + const tribute = {}; + API3.warn("Making tribute"); + for (const resource of Resources.GetTributableCodes()) + { + const tributableResourceCount = availableResources[resource] - this.Config.retainedResourcesAfterTribute; + if (tributableResourceCount <= 0) + { + tribute[resource] = 0; + continue; + } + // Weird bugfix. + tribute[resource] = Math.round(tributableResourceCount / (numEnemies == 0 ? 1 : numEnemies)); + } + return tribute; +}; + +PETRA.DiplomacyManager.prototype.enoughResourcesForTributes = function(gameState) +{ + const availableResources = gameState.ai.queueManager.getAvailableResources(gameState); + return !(!!Resources.GetTributableCodes().find(r => availableResources[r] < 50)); +}; + +PETRA.DiplomacyManager.prototype.exitEmergency = function() +{ + this.waitsForResponses = false; + this.responseCounter = -1; +}; + PETRA.DiplomacyManager.prototype.update = function(gameState, events) { this.checkEvents(gameState, events); @@ -557,7 +661,9 @@ "betrayWeighting": this.betrayWeighting, "receivedDiplomacyRequests": this.receivedDiplomacyRequests, "sentDiplomacyRequests": this.sentDiplomacyRequests, - "sentDiplomacyRequestLapseTime": this.sentDiplomacyRequestLapseTime + "sentDiplomacyRequestLapseTime": this.sentDiplomacyRequestLapseTime, + "waitsForResponses": this.waitsForResponses, + "responseCounter": this.responseCounter }; }; Index: binaries/data/mods/public/simulation/ai/petra/emergencyManager.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/ai/petra/emergencyManager.js @@ -0,0 +1,513 @@ +PETRA.EmergencyManager = function(config) +{ + this.Config = config; + this.collectedTroops = false; + // Counter to delay counting the population and structures + // to around 3 minutes. + this.counterForCheckingEmergency = 0; + // Last number of workers+soldiers+siege machines or structures + // used for calculating, whether an emergency is there, + // based on the change. + this.referencePopulation = 0; + this.referenceStructureCount = 0; + // Maximum number of built civic centres + this.peakCivicCentreCount = -1; + this.finishedMarching = false; + // Point to collect in case of emergency + this.musterPosition = [-1, -1]; + this.sentTributes = false; + // Used in aggressive: The point where to go next. + this.nextBattlePoint = [-1, -1]; + // Used for checking, whether to resign. + this.lastPeopleAlive = -1; + // A list of all neutrality requests that were sent. + this.sentRequests = []; + // Used in defensive: How long to wait to check the + // number of living people in order to decide, whether + // this bot resigns. + this.lastCounter = 0; + // Counter to wait for until the neutrality requests + // expire. + this.neutralityCounter = 0; + this.finishedWaiting = false; + // If the number of structures increased/stagnated, but + // the number of people reduced to less than this threshold factor + // then trigger an emergency. + this.attackThreshold = 0.4; + + this.phases = []; + this.currentPhase = -1; + this.maxPhase = -1; + + // Used for limiting the amount of marching. + this.marchCounter = 0; + // Used as a workaround for walking bug + this.lastPoint = [-1, -1]; + // Counter for checking whether to return to normal, if defensive+!cooperative + this.backToNormalCounter = 0; + + this.nearestEnemy = null; +}; + +PETRA.EmergencyManager.prototype.resetToNormal = function(gameState) +{ + this.initPhases(gameState); + gameState.emergencyState[PlayerID] = false; + this.collectedTroops = false; + this.counterForCheckingEmergency = 0; + this.referencePopulation = gameState.getPopulation(); + this.referenceStructureCount = gameState.getOwnStructures().length; + this.peakCivicCentreCount = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).length; + this.finishedMarching = false; + this.musterPosition = [-1, -1]; + this.sentTributes = false; + let cnter = 0; + for (const treshold of this.phases) + { + if (treshold > this.referencePopulation) + break; + cnter++; + } + this.currentPhase = cnter; + this.maxPhase = cnter; + this.marchCounter = 0; + this.lastPoint = [-1, -1]; + this.backToNormalCounter = 0; + // All other fields didn't change. +}; + +PETRA.EmergencyManager.prototype.initPhases = function(gameState) +{ + const maxPop = gameState.getPopulationMax(); + let lastLimit = 0; + for (const populationLimit in this.Config.phasesForSteadyDecline) + { + if (maxPop == populationLimit) + break; + else if (maxPop > populationLimit) + lastLimit = Number(populationLimit); + else + { + const diffToHigherLimit = Math.abs(maxPop - populationLimit); + const diffToLowerLimit = Math.abs(maxPop - lastLimit); + if (diffToHigherLimit >= diffToLowerLimit) + lastLimit = populationLimit; + break; + } + } + this.phases = this.Config.phasesForSteadyDecline[lastLimit]; +}; + +PETRA.EmergencyManager.prototype.handleEmergency = function(gameState, events) +{ + if (!this.collectedTroops) + { + const entities = gameState.getOwnEntities().toEntityArray(); + for (const ent of entities) + { + if (ent) // 101 to stop everything, not the ones that are finished <=99. + ent.stopAllProduction(101); + } + this.collectTroops(gameState); + this.collectedTroops = true; + } + // Force these people to go to the position, where all others + // will be to avoid having minor skirmishes that may lead to heavy + // losses. + // TODO: Maybe say something like: Hold the line! (Motivational speech) + if (this.troopsMarching(gameState)) + this.moveToPoint(gameState, this.musterPosition); + else + this.executeActions(gameState, events); +}; + +PETRA.EmergencyManager.prototype.moveToPoint = function(gameState, point) +{ + // Otherwise the people would walk a few steps, stay still, continue + // to walk until the destination is reached. + if (this.lastPoint == point) + return; + this.lastPoint = point; + for (const ent of gameState.getOwnEntities().toEntityArray()) + if (this.isMovableEntity(ent)) + ent.move(point[0], point[1]); +}; +PETRA.EmergencyManager.prototype.hasAvailableTerritoryRoot = function(gameState) +{ + return gameState.getOwnStructures().filter(ent => { + return ent && ent.get("TerritoryInfluence") !== undefined && ent.get("TerritoryInfluence").Root; + }).length != 0; +}; + +PETRA.EmergencyManager.prototype.executeActions = function(gameState, events) +{ + const personality = this.Config.personality; + if (personality.aggressive < personality.defensive) + { + API3.warn("Defensive"); + if (personality.cooperative >= 0.5 && + !this.sentTributes && + !gameState.sharedScript.playersData[PlayerID].teamsLocked && + gameState.ai.HQ.diplomacyManager.enoughResourcesForTributes(gameState)) + { + API3.warn("Sent tributes!"); + this.sentTributes = true; + } + else + { + if (this.sentTributes && !gameState.ai.HQ.diplomacyManager.expiredNeutralityRequest()) + { + API3.warn("Waiting until neutrality!"); + const enemies = gameState.getEnemies(); + var numEnemies = 0; + for (const enemy of enemies) + { + if (enemy <= 0 || gameState.ai.HQ.attackManager.defeated[enemy]) + continue; + numEnemies++; + } + API3.warn("" + numEnemies + " are left!"); + if (numEnemies == 0) + { + if (this.hasAvailableTerritoryRoot(gameState)) + { + API3.warn("Back to normal"); + gameState.emergencyState[PlayerID] = false; + this.resetToNormal(gameState); + } + else + { + API3.warn("No root found"); + } + return; + } + if (!gameState.ai.HQ.diplomacyManager.expiredNeutralityRequest()) + { + API3.warn("Still waiting"); + return; + } + API3.warn("Expired!"); + gameState.ai.HQ.diplomacyManager.expireNeutraliyRequests(gameState); + return; + } + API3.warn("HEREEEE!"); + // Check whether to resign. (Here: If more than 75% were killed) + const ownEntities = gameState.getOwnEntities().toEntityArray(); + let movableEntitiesCount = 0; + for (const ent of ownEntities) + { + if (ent.walkSpeed() > 0) + movableEntitiesCount++; + } + if (this.lastPeopleAlive == -1) + this.lastPeopleAlive = movableEntitiesCount; + if (this.lastCounter < this.Config.resignCheckDelay) + this.lastCounter++; + else + { + this.lastCounter = 0; + if (movableEntitiesCount < this.Config.lossesForResign * this.lastPeopleAlive) + { + var allResources = gameState.ai.queueManager.getAvailableResources(gameState); + const allies = gameState.getAllies(); + // Just give the first non-dead ally we can find all our resources + for (const ally of allies) + if (!gameState.ai.HQ.attackManager.defeated[ally]) + { + const tribute = {}; + for (const resource of Resources.GetTributableCodes()) + tribute[resource] = allResources[resource]; + Engine.PostCommand(PlayerID, { "type": "tribute", "player": ally, "amounts": tribute }); + break; + } + Engine.PostCommand(PlayerID, { "type": "resign" }); + return; + } + else if (movableEntitiesCount >= this.lastPeopleAlive * ((1 + this.Config.lossesForResign) / 2)) + { + API3.warn("Waiting until returning: " + this.backToNormalCounter + "/" + this.Config.defensiveStateDuration); + if (this.backToNormalCounter < this.Config.defensiveStateDuration) + this.backToNormalCounter++; + else if (this.hasAvailableTerritoryRoot(gameState)) + { + API3.warn("defensive + !cooperative: Back to normal"); + gameState.emergencyState[PlayerID] = false; + this.resetToNormal(gameState); + return; + } + } + } + // "Patrol" around the collect position. + for (const ent of ownEntities) + { + if (!ent.get("Attack") || !ent.position()) + continue; + if (API3.VectorDistance(ent.position(), this.musterPosition) > this.Config.patrouilleRange) + ent.move(this.musterPosition[0], this.musterPosition[1]); + } + } + } + else + { + // TODO: Destroy all our buildings? + API3.warn("Aggressive"); + // Select initial battle point + if (this.nextBattlePoint[0] == -1) + { + for (const ent of gameState.getOwnEntities().toEntityArray()) + ent.setStance("violent"); + this.selectBattlePoint(gameState); + API3.warn("Initial point: " + this.nextBattlePoint); + API3.warn(this.nearestEnemy.toString()); + } + + if (!this.isAtBattlePoint(gameState) && this.marchCounter < this.Config.maximumMarchingDuration) + { + API3.warn(this.marchCounter + "//" + this.Config.maximumMarchingDuration); + if (this.nearestEnemy !== undefined) + this.nextBattlePoint = this.nearestEnemy.position(); + this.aggressiveAttack(gameState); + if (this.nearestEnemy && this.nearestEnemy.hitpoints() == 0) { + this.selectBattlePoint(gameState); + this.aggressiveAttack(gameState); + API3.warn("New: " + this.nearestEnemy.toString()); + } else + this.marchCounter++; + } + else if (this.nearestEnemy == undefined || + this.nearestEnemy.hitpoints() == 0 || + this.marchCounter == this.Config.maximumMarchingDuration || + !this.noEnemiesNear(gameState)) + { + this.selectBattlePoint(gameState); + this.aggressiveAttack(gameState); + API3.warn("Entity: " + this.nearestEnemy.toString()); + } + // Else wait until we or the enemy are dead. + } +}; + +PETRA.EmergencyManager.prototype.aggressiveAttack = function(gameState) +{ + if (this.nearestEnemy == undefined) + return; + for (const ent of gameState.getOwnEntities().toEntityArray()) + { + // ent.attackMove(this.nearestEnemy.position()[0], this.nearestEnemy.position()[1], targetClasses, false, true); + ent.attack(this.nearestEnemy.id(), false, false, true); + // ent.move(this.nearestEnemy.position()[0], this.nearestEnemy.position()[1]); + } +}; + +PETRA.EmergencyManager.prototype.validEntity = function(ent) +{ + return ent && ent.position(); +}; + +PETRA.EmergencyManager.prototype.noEnemiesNear = function(gameState) +{ + const averagePosition = this.getAveragePositionOfMovableEntities(gameState); + return !!gameState.getEnemyEntities().toEntityArray().find(e => this.validEntity(e) && + e.owner() > 0 && + API3.VectorDistance(e.position(), averagePosition) < this.Config.enemyDetectionRange * 0.001); +}; + +PETRA.EmergencyManager.prototype.isMovableEntity = function(ent) +{ + return this.validEntity(ent) && ent.walkSpeed() > 0; +}; + +PETRA.EmergencyManager.prototype.selectBattlePoint = function(gameState) +{ + const averagePosition = this.getAveragePositionOfMovableEntities(gameState); + const enemies = gameState.getEnemyEntities().toEntityArray(); + let nearestEnemy; + let nearestEnemyDistance = Infinity; + for (const enemy of enemies) + { + // Exclude Gaia and INVALID_PLAYER + if (this.validEntity(enemy) && enemy.owner() > 0) + { + const distance = API3.VectorDistance(enemy.position(), averagePosition); + if (distance < nearestEnemyDistance) + { + nearestEnemy = enemy; + nearestEnemyDistance = distance; + } + } + } + this.marchCounter = 0; + this.nearestEnemy = nearestEnemy; + if (nearestEnemy !== undefined) + this.nextBattlePoint = nearestEnemy.position(); +}; + +PETRA.EmergencyManager.prototype.getAveragePositionOfMovableEntities = function(gameState) +{ + const entities = gameState.getOwnEntities().toEntityArray(); + if (entities.length == 0) + return [-1, -1]; + let nEntities = 0; + let sumX = 0; + let sumZ = 0; + for (const ent of entities) + { + if (this.validEntity(ent) && this.isMovableEntity(ent) && !ent.hasClass("Ship")) + { + nEntities++; + const pos = ent.position(); + sumX += pos[0]; + sumZ += pos[1]; + } + } + + if (nEntities == 0) + return [-1, -1]; + return [sumX / nEntities, sumZ / nEntities]; +}; + +PETRA.EmergencyManager.prototype.isAtBattlePoint = function(gameState) +{ + const averagePosition = this.getAveragePositionOfMovableEntities(gameState); + return API3.VectorDistance(averagePosition, this.nextBattlePoint) < 75; +}; + +PETRA.EmergencyManager.prototype.troopsMarching = function(gameState) +{ + if (this.finishedMarching) + return false; + // Ships are excluded, as they can't reach every location. + if (this.marchCounter < this.Config.maximumMarchingDuration) + { + this.marchCounter++; + for (const ent of gameState.getOwnEntities().toEntityArray()) + if (this.isMovableEntity(ent) && !ent.hasClass("Ship") && API3.VectorDistance(ent.position(), this.musterPosition) > 60) + return true; + } + this.finishedMarching = true; + return false; +}; + +PETRA.EmergencyManager.prototype.checkForEmergency = function(gameState) +{ + if (gameState.emergencyState[PlayerID] || this.steadyDeclineCheck(gameState)) + return true; + if (this.counterForCheckingEmergency < this.Config.fastDestructionDelay) + { + this.counterForCheckingEmergency++; + return false; + } + this.counterForCheckingEmergency = 0; + return this.destructionCheck(gameState); +}; + +PETRA.EmergencyManager.prototype.steadyDeclineCheck = function(gameState) +{ + const civicCentresCount = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).length; + this.peakCivicCentreCount = Math.max(this.peakCivicCentreCount, civicCentresCount); + if ((civicCentresCount == 0 && this.peakCivicCentreCount >= 1) || this.peakCivicCentreCount - this.Config.civicCentreLossTrigger >= civicCentresCount) + return true; + const currentPopulation = gameState.getPopulation(); + if (currentPopulation >= this.phases[this.currentPhase + 1]) + { + this.currentPhase++; + this.maxPhase = Math.max(this.currentPhase, this.maxPhase); + } + else if (this.currentPhase >= 1 && this.currentPopulation < this.phases[this.currentPhase - 1]) + this.currentPhase--; + return this.maxPhase - this.currentPhase >= this.phasesToLoseUntilEmergency; +}; +/** + * Check whether this is an emergency. An emergency is, if a lot of + * people are killed and/or a lot of buildings are destroyed. + */ +PETRA.EmergencyManager.prototype.destructionCheck = function(gameState) +{ + const oldPopulation = this.referencePopulation; + this.referencePopulation = gameState.getPopulation(); + if (oldPopulation == 0) + return false; + const oldNumberOfStructures = this.referenceStructureCount; + this.referenceStructureCount = gameState.getOwnStructures().length; + if (oldNumberOfStructures == 0) + return false; + const populationFactor = this.referencePopulation / oldPopulation; + const structureFactor = this.referenceStructureCount / oldNumberOfStructures; + // Growth means no emergency, no matter the difficulty + if (populationFactor >=1 && structureFactor >= 1) + return false; + // This means more an attack of the bot, no defense operation, + // no matter the difficulty. + if (structureFactor >= 1 || populationFactor >= this.attackThreshold) + return false; + const emergencyFactor = this.Config.emergencyFactors[this.Config.difficulty]; + return populationFactor < emergencyFactor[0] || structureFactor < emergencyFactor[1]; +}; + +PETRA.EmergencyManager.prototype.collectTroops = function(gameState) +{ + const entities = gameState.getOwnEntities().toEntityArray(); + if (!entities.length) + return; + if (!gameState.getOwnStructures().hasEntities()) + this.musterPosition = this.getAveragePositionOfMovableEntities(gameState); + else + this.getSpecialBuildingPosition(entities, gameState); + this.moveToPoint(gameState, this.musterPosition); +}; + +PETRA.EmergencyManager.prototype.getSpecialBuildingPosition = function(entities, gameState) +{ + let building; + var roots = gameState.getOwnStructures().filter(ent => { + return ent && ent.get("TerritoryInfluence") !== undefined && ent.get("TerritoryInfluence").Root; + }); + // The optimal solution would be probably getting the average distance from each + // entity to this building, but this would require a lot of calculation + if (roots.length) + building = roots[0]; + if (!this.validEntity(building)) + { + this.musterPosition = this.getAveragePositionOfMovableEntities(gameState); + return; + } + this.musterPosition = building.position(); +}; + +PETRA.EmergencyManager.prototype.getAveragePosition = function(gameState) +{ + this.musterPosition = this.getAveragePositionOfMovableEntities(gameState); +}; + +PETRA.EmergencyManager.prototype.Serialize = function() +{ + return { + "collectedTroops": this.collectedTroops, + "counterForCheckingEmergency": this.counterForCheckingEmergency, + "referencePopulation": this.referencePopulation, + "referenceStructureCount": this.referenceStructureCount, + "peakCivicCentreCount": this.peakCivicCentreCount, + "finishedMarching": this.finishedMarching, + "musterPosition": this.musterPosition, + "sentTributes": this.sentTributes, + "nextBattlePoint": this.nextBattlePoint, + "lastPeopleAlive": this.lastPeopleAlive, + "sentRequests": this.sentRequests, + "lastCounter": this.lastCounter, + "neutralityCounter": this.neutralityCounter, + "finishedWaiting": this.finishedWaiting, + "phases": this.phases, + "currentPhase": this.currentPhase, + "maxPhase": this.maxPhase, + "marchCounter": this.marchCounter, + "testPoint": this.testPoint, + "backToNormalCounter": this.backToNormalCounter, + "nearestEnemy": this.nearestEnemy + }; +}; + +PETRA.EmergencyManager.prototype.Deserialize = function(data) +{ + for (const key in data) + this[key] = data[key]; +}; Index: binaries/data/mods/public/simulation/ai/petra/garrisonManager.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/garrisonManager.js +++ binaries/data/mods/public/simulation/ai/petra/garrisonManager.js @@ -11,6 +11,15 @@ this.Config = Config; this.holders = new Map(); this.decayingStructures = new Map(); + this.inEmergency = false; +}; + +PETRA.GarrisonManager.prototype.updateEmergency = function(gameState, events) { + // We don't have anything else to do. + if (this.inEmergency) + return; + this.inEmergency = true; + this.ungarrisonAllUnits(gameState); }; PETRA.GarrisonManager.prototype.update = function(gameState, events) @@ -364,9 +373,22 @@ this.decayingStructures.delete(entId); }; +PETRA.GarrisonManager.prototype.ungarrisonAllUnits = function(gameState) { + for (const [id, data] of this.holders.entries()) + { + for (const garrisonedEnt of data.list) + { + const ent = gameState.getEntityById(garrisonedEnt); + if (ent) + this.leaveGarrison(ent); + } + this.holders.delete(id); + } +}; + PETRA.GarrisonManager.prototype.Serialize = function() { - return { "holders": this.holders, "decayingStructures": this.decayingStructures }; + return { "holders": this.holders, "decayingStructures": this.decayingStructures, "inEmergency": this.inEmergency }; }; PETRA.GarrisonManager.prototype.Deserialize = function(data) Index: binaries/data/mods/public/simulation/ai/petra/headquarters.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/headquarters.js +++ binaries/data/mods/public/simulation/ai/petra/headquarters.js @@ -45,6 +45,7 @@ this.diplomacyManager = new PETRA.DiplomacyManager(this.Config); this.garrisonManager = new PETRA.GarrisonManager(this.Config); this.victoryManager = new PETRA.VictoryManager(this.Config); + this.emergencyManager = new PETRA.EmergencyManager(this.Config); this.capturableTargets = new Map(); this.capturableTargetsTime = 0; @@ -69,6 +70,8 @@ this.treasures.registerUpdates(); this.currentPhase = gameState.currentPhase(); this.decayingStructures = new Set(); + + this.emergencyManager.initPhases(gameState); }; /** @@ -2663,6 +2666,23 @@ PETRA.HQ.prototype.update = function(gameState, queues, events) { Engine.ProfileStart("Headquarters update"); + if (this.emergencyManager.checkForEmergency(gameState)) + { + gameState.emergencyState[PlayerID] = true; + this.garrisonManager.updateEmergency(gameState, events); + this.diplomacyManager.updateEmergency(gameState, events); + this.attackManager.updateEmergency(gameState); + this.emergencyManager.handleEmergency(gameState, events); + if (!gameState.emergencyState[PlayerID]) + { + // GarrisonManager, AttackManager will do everything by themselves + this.diplomacyManager.exitEmergency(); + this.defenseManager.exitEmergency(); + this.buildManager.exitEmergency(gameState); + } + Engine.ProfileStop(); + return; + } this.turnCache = {}; this.territoryMap = PETRA.createTerritoryMap(gameState); this.canBarter = gameState.getOwnEntitiesByClass("Market", true).filter(API3.Filters.isBuilt()).hasEntities(); @@ -2825,6 +2845,7 @@ API3.warn(" diplomacyManager " + uneval(this.diplomacyManager.Serialize())); API3.warn(" garrisonManager " + uneval(this.garrisonManager.Serialize())); API3.warn(" victoryManager " + uneval(this.victoryManager.Serialize())); + API3.warn(" emergencyManager " + uneval(this.emergencyManager.Serialize())); } return { @@ -2840,6 +2861,7 @@ "diplomacyManager": this.diplomacyManager.Serialize(), "garrisonManager": this.garrisonManager.Serialize(), "victoryManager": this.victoryManager.Serialize(), + "emergencyManager": this.emergencyManager.Serialize() }; }; @@ -2889,4 +2911,7 @@ this.victoryManager = new PETRA.VictoryManager(this.Config); this.victoryManager.Deserialize(data.victoryManager); + + this.emergencyManager = new PETRA.EmergencyManager(this.Config); + this.emergencyManager.Deserialize(data.emergencyManager); }; Index: binaries/data/mods/public/simulation/ai/petra/startingStrategy.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/startingStrategy.js +++ binaries/data/mods/public/simulation/ai/petra/startingStrategy.js @@ -134,7 +134,7 @@ /** * determine the main land Index (or water index if none) - * as well as the list of allowed (land andf water) regions + * as well as the list of allowed (land and water) regions */ PETRA.HQ.prototype.regionAnalysis = function(gameState) {