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 @@ -49,7 +49,6 @@ { for (let evt of events.PlayerDefeated) this.defeated[evt.playerId] = true; - let answer = "decline"; let other; let targetPlayer; @@ -87,13 +86,15 @@ { if (attack.state === "completing" || attack.targetPlayer !== targetPlayer || - attack.unitCollection.length < 3) + attack.unitCollection.length < 3 || + gameState.emergencyState[PlayerID]) continue; attack.forceStart(); attack.requested = true; } } - answer = "join"; + if (!gameState.emergencyState[PlayerID]) + answer = "join"; } else if (other !== undefined) answer = "other"; @@ -556,7 +557,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 +738,21 @@ return true; }; +PETRA.AttackManager.prototype.handleEmergency = function(gameState, events) { + // First call will stop all attacks, the second call to this + // function will essentially be a no-op. + this.bombingAttacks = new Map(); + this.checkEvents(gameState, events); + for (const attackType in this.upcomingAttacks) + for (const attack of this.upcomingAttacks[attackType]) + attack.targetPlayer = undefined; + for (const attackType in this.startedAttacks) + for (let i = 0; i < this.startedAttacks[attackType].length; ++i) + { + this.startedAttacks[attackType][i].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,13 @@ */ PETRA.AttackPlan.prototype.getAttackAccess = function(gameState) { + // JCWASMX86_TODO: Not sure why it is needed, but otherwise + // it would throw errors. + if (this.position == undefined) + { + warn(new Error().stack); + 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/basesManager.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/basesManager.js +++ binaries/data/mods/public/simulation/ai/petra/basesManager.js @@ -155,7 +155,7 @@ if (!ent || ent.owner() != PlayerID || ent.getMetadata(PlayerID, "base") === undefined) continue; const base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); - if (!base.anchorId || base.anchorId != evt.entity) + if (!base || !base.anchorId || base.anchorId != evt.entity) continue; base.anchorId = evt.newentity; base.anchor = 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,15 @@ this.incrementBuilderCounters(civ, ent, 1); }; +PETRA.BuildManager.prototype.exitEmergency = function(gameState) +{ + this.builderCounters = new Map(); + this.init(gameState); + const civ = gameState.getPlayerCiv(); + for (const ent of gameState.getOwnUnits().values()) + this.incrementBuilderCounters(civ, ent, 1); +}; + PETRA.BuildManager.prototype.incrementBuilderCounters = function(civ, ent, increment) { for (let buildable of ent.buildableEntities(civ)) Index: binaries/data/mods/public/simulation/ai/petra/chatHelper.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/chatHelper.js +++ binaries/data/mods/public/simulation/ai/petra/chatHelper.js @@ -8,6 +8,18 @@ markForTranslation("I have just sent an army against %(_player_)s.") ] }; +PETRA.emergencyMessages = { + "help": [ + markForTranslation("%(_player_)s, I am in need of help. Send troops or I will be extinguished!"), + markForTranslation("My empire may fall soon. Will you, %(_player_)s, help me to survive in this dark hour?"), + markForTranslation("My enemies encircled me. I want to remind you of our alliance, %(_player_)s") + ], + "resources": [ + markForTranslation("The dark hours are over now! Send me resources to rebuild my empire to new glory!"), + markForTranslation("I was nearly extinguished. But now I need resources to get my revenge"), + markForTranslation("If you give me resources, we can conquer the world together!") + ] +}; PETRA.answerRequestAttackMessages = { "join": [ @@ -234,3 +246,14 @@ "parameters": { "_player_": player } }); }; + +PETRA.chatEmergency = function(gameState, player, type) +{ + Engine.PostCommand(PlayerID, { + "type": "aichat", + "message": "/msg " + gameState.sharedScript.playersData[player].name + " " + pickRandom(this.emergencyMessages[type]), + "translateMessage": true, + "translateParameters": ["_player_"], + "parameters": { "_player_": player } + }); +}; 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 @@ -160,6 +160,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.patrolRange = 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; + this.unusedNoAllyTechs = [ "Player/sharedLos", "Market/InternationalBonus", 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,59 @@ this.attackedAllies = {}; }; +PETRA.DefenseManager.prototype.exitEmergency = function() +{ + this.attackingArmies = {}; + this.attackingUnits = {}; + this.attackedAllies = {}; + this.armies = []; + this.targetList = []; +}; +PETRA.DefenseManager.prototype.handleEmergency = function(gameState, events) +{ + // Function is no-op after one call + for (const army of this.armies) // No other relevant code from checkEvents + army.checkEvents(gameState, events); + for (let i = 0; i < this.armies.length; ++i) + { + const army = this.armies[i]; + army.update(gameState); + for (const entId of army.foeEntities) + { + army.removeFoe(gameState, entId); + const ent = gameState.getEntityById(entId); + if (!ent || !ent.position()) + continue; + ent.deleteMetadata(PlayerID, "access"); + ent.deleteMetadata(PlayerID, "plan"); + ent.setMetadata(PlayerID, "PartOfArmy", undefined); + ent.setMetadata(PlayerID, "subrole", undefined); + } + for (const entId of army.ownEntities) + { + army.removeOwn(gameState, entId); + const ent = gameState.getEntityById(entId); + if (!ent || !ent.position()) + continue; + ent.deleteMetadata(PlayerID, "access"); + ent.deleteMetadata(PlayerID, "plan"); + ent.setMetadata(PlayerID, "PartOfArmy", undefined); + ent.setMetadata(PlayerID, "subrole", undefined); + } + while (army.ownEntities.length) + { + const entId = army.ownEntities[0]; + army.removeOwn(gameState, entId); + const ent = gameState.getEntityById(entId); + if (ent) + ent.stopMoving(); + } + while (army.foeEntities.length) + army.removeFoe(gameState, army.foeEntities[0]); + this.armies.splice(i--, 1); + } +}; + PETRA.DefenseManager.prototype.update = function(gameState, events) { Engine.ProfileStart("Defense Manager"); @@ -457,6 +510,8 @@ // Do not assign defender if it cannot attack at least part of the attacking army. if (!armiesNeeding[a].army.foeEntities.some(eEnt => { let eEntID = gameState.getEntityById(eEnt); + if (!eEntID) + return false; return ent.canAttackTarget(eEntID, PETRA.allowCapture(gameState, ent, eEntID)); })) continue; 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,9 @@ this.receivedDiplomacyRequests = new Map(); this.sentDiplomacyRequests = new Map(); this.sentDiplomacyRequestLapseTime = 120 + randFloat(10, 100); + this.waitsForResponses = false; + this.responseCounter = -1; + this.callForAidCounter = 0; }; /** @@ -373,15 +376,15 @@ let requiredTribute; let request = this.receivedDiplomacyRequests.get(player); let moreEnemiesThanAllies = gameState.getEnemies().length > gameState.getMutualAllies().length; - + const emergency = gameState.emergencyState[PlayerID]; // For any given diplomacy request be likely to permanently decline - if (!request && gameState.getPlayerCiv() !== gameState.getPlayerCiv(player) && randBool(0.6) || - !moreEnemiesThanAllies || gameState.ai.HQ.attackManager.currentEnemyPlayer === player) + if ((!request && gameState.getPlayerCiv() !== gameState.getPlayerCiv(player) && randBool(0.6) || + !moreEnemiesThanAllies || gameState.ai.HQ.attackManager.currentEnemyPlayer === player) && !emergency) { this.receivedDiplomacyRequests.set(player, { "requestType": requestType, "status": "declinedRequest" }); response = "decline"; } - else if (request && request.status !== "accepted" && request.requestType !== "ally") + else if (request && request.status !== "accepted" && request.requestType !== "ally" && !emergency) { if (request.status === "declinedRequest") response = "decline"; @@ -393,8 +396,8 @@ requiredTribute = request; } } - else if (requestType === "ally" && gameState.getEntities(player).length < gameState.getOwnEntities().length && randBool(0.4) || - requestType === "neutral" && moreEnemiesThanAllies && randBool(0.8)) + else if ((requestType === "ally" && gameState.getEntities(player).length < gameState.getOwnEntities().length && randBool(0.4) || + requestType === "neutral" && moreEnemiesThanAllies && randBool(0.8)) || emergency) { response = "accept"; this.changePlayerDiplomacy(gameState, player, requestType); @@ -519,6 +522,135 @@ } }; +PETRA.DiplomacyManager.prototype.callForAid = function(gameState) +{ + for (const ally of gameState.getAllies()) + PETRA.chatEmergency(gameState, ally, "help"); +}; + +PETRA.DiplomacyManager.prototype.askForResources = function(gameState) +{ + for (const ally of gameState.getAllies()) + PETRA.chatEmergency(gameState, ally, "resources"); +}; + +PETRA.DiplomacyManager.prototype.canSendNeutralityRequests = function(gameState) +{ + const personality = this.Config.personality; + const locked_teams = gameState.sharedScript.playersData[PlayerID].teamsLocked; + // TODO: Better names for these helper variables + const bool1 = personality.aggressive < personality.defensive && !locked_teams && personality.cooperative >= 0.5; + const bool2 = this.enoughResourcesForTributes(gameState) && !this.waitsForResponses && !gameState.ai.HQ.emergencyManager.troopsMarching(gameState); + return bool1 && bool2; +}; + +PETRA.DiplomacyManager.prototype.handleEmergency = function(gameState, events) +{ + const personality = this.Config.personality; + if (this.callForAidCounter == 0) + { + this.callForAid(gameState); + this.callForAidCounter++; + } + else if (this.callForAidCounter < 120) + this.callForAidCounter++; + else + this.callForAidCounter = 0; + if (this.canSendNeutralityRequests(gameState)) + { + if (!this.waitsForResponses) + { + this.tryGettingNeutral(gameState); + this.responseCounter = 0; + this.waitsForResponses = true; + } + } + this.checkEvents(gameState, events); + if (this.waitsForResponses && gameState.getEnemies().length) + { + if (this.responseCounter < this.Config.neutralityRequestWaitingDuration) + this.responseCounter++; + else if (this.responseCounter == this.Config.neutralityRequestWaitingDuration) + { + this.expireNeutralityRequests(gameState); + // To avoid either incrementing the counter further or expiring + // the requests all the time + 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, "neutral", "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 + }); + 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 = {}; + 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] < 500); +}; + +PETRA.DiplomacyManager.prototype.exitEmergency = function() +{ + this.waitsForResponses = false; + this.responseCounter = -1; +}; + PETRA.DiplomacyManager.prototype.update = function(gameState, events) { this.checkEvents(gameState, events); @@ -557,7 +689,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,526 @@ +/** + * Emergency manager + * + * Checks whether there is an emergency and acts accordingly based on the personality + * of the AI. + */ +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 = undefined; + this.emergencyPeakPopulation = 0; +}; + +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.lastPeopleAlive = -1; + this.sentTributes = false; + let cnter = 0; + for (const threshold of this.phases) + { + if (threshold > this.referencePopulation) + break; + cnter++; + } + this.currentPhase = cnter; + this.maxPhase = cnter; + this.marchCounter = 0; + this.lastPoint = [-1, -1]; + this.backToNormalCounter = 0; + this.emergencyPeakPopulation = 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) +{ + 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; + this.emergencyPeakPopulation = this.countMovableEntities(gameState); + } + // Force these people to go to the position, where all others + // will be to avoid having minor skirmishes that may lead to heavy + // losses. + // They will just walk a straight line. A better solution would be + // to only walk through neutral and allied territory to lose less troops. + if (this.troopsMarching(gameState)) + { + if (this.Config.personality.aggressive < this.Config.personality.defensive && + this.countMovableEntities(gameState) < this.Config.lossesForResign * this.emergencyPeakPopulation * 0.6) + { + this.resign(gameState); + return; + } + this.moveToPoint(gameState, this.musterPosition); + } + else + this.executeActions(gameState); +}; + +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.allowedToTribute = function(gameState, cooperativity) +{ + const lockedTeams = gameState.sharedScript.playersData[PlayerID].teamsLocked; + return cooperativity >= 0.5 && this.sentTributes && !lockedTeams && gameState.ai.HQ.diplomacyManager.enoughResourcesForTributes(gameState); +}; + +PETRA.EmergencyManager.prototype.executeActions = function(gameState) +{ + const personality = this.Config.personality; + if (personality.aggressive < personality.defensive) + { + if (this.allowedToTribute(gameState, personality.cooperative)) + this.sentTributes = true; + else + { + if (this.sentTributes && !gameState.ai.HQ.diplomacyManager.expiredNeutralityRequest()) + { + this.waitForExpiration(gameState); + return; + } + // Check whether to resign. (Here: If more than 75% were killed) + const movableEntitiesCount = this.countMovableEntities(gameState); + 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) + { + this.resign(gameState); + return; + } + else if (movableEntitiesCount >= this.lastPeopleAlive * ((1 + this.Config.lossesForResign) / 2)) + { + if (this.backToNormalCounter < this.Config.defensiveStateDuration) + { + if (this.backToNormalCounter == Math.round(this.Config.defensiveStateDuration * 0.75)) + gameState.ai.HQ.diplomacyManager.askForResources(gameState); + this.backToNormalCounter++; + } + else if (this.hasAvailableTerritoryRoot(gameState)) + { + gameState.emergencyState[PlayerID] = false; + this.resetToNormal(gameState); + return; + } + } + } + const halfPatrolRange = this.Config.patrolRange / 2; + // "Patrol" around the collect position. + for (const ent of gameState.getOwnEntities().toEntityArray()) + { + if (!ent.get("Attack") || !ent.position()) + continue; + const xDeviation = this.generateDeviation(halfPatrolRange); + const yDeviation = this.generateDeviation(halfPatrolRange); + if (API3.VectorDistance(ent.position(), this.musterPosition) > this.Config.patrolRange) + ent.move(this.musterPosition[0] + xDeviation, this.musterPosition[1] + yDeviation); + } + } + } + else + this.aggressiveActions(gameState); +}; + +PETRA.EmergencyManager.prototype.generateDeviation = function(max) +{ + return (Math.random() > 0.5 ? -1 : 1) * Math.floor((Math.random() * max) + 1); +}; + +PETRA.EmergencyManager.prototype.countMovableEntities = function(gameState) +{ + return gameState.getOwnEntities().toEntityArray().filter(ent => ent.walkSpeed() > 0).length; +}; +PETRA.EmergencyManager.prototype.resign = function(gameState) +{ + gameState.getOwnEntities().forEach(ent => ent.destroy()); + 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] && ally !== PlayerID) + { + 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" }); +}; + +PETRA.EmergencyManager.prototype.waitForExpiration = function(gameState) +{ + const numEnemies = gameState.getEnemies().toEntityArray() + .filter(enemy => enemy >= 0 && !gameState.ai.HQ.attackManager.defeated[enemy]) + .length; + if (numEnemies === 0 && this.hasAvailableTerritoryRoot(gameState)) + { + gameState.emergencyState[PlayerID] = false; + this.resetToNormal(gameState); + } +}; +PETRA.EmergencyManager.prototype.aggressiveActions = function(gameState) +{ + gameState.getOwnStructures().forEach(ent => ent.destroy()); + // Select initial battle point + if (this.nextBattlePoint[0] === -1) + { + gameState.getOwnEntities().forEach(ent => ent.setStance("violent")); + this.selectBattlePoint(gameState); + if (!this.nextBattlePoint) + return; + } + + if (!this.isAtBattlePoint(gameState) && this.marchCounter < this.Config.maximumMarchingDuration) + { + if (this.nearestEnemy && this.nearestEnemy.position()) + this.nextBattlePoint = this.nearestEnemy.position(); + this.aggressiveAttack(gameState); + if (this.nearestEnemy && this.nearestEnemy.hitpoints() === 0) + { + this.selectBattlePoint(gameState); + if (!this.nextBattlePoint) + return; + this.aggressiveAttack(gameState); + } + else + this.marchCounter++; + } + else if (this.nearestEnemy === undefined || + this.nearestEnemy.hitpoints() === 0 || + this.marchCounter === this.Config.maximumMarchingDuration || + !this.noEnemiesNear(gameState)) + { + this.selectBattlePoint(gameState); + if (!this.nextBattlePoint) + return; + this.aggressiveAttack(gameState); + } +}; + +PETRA.EmergencyManager.prototype.aggressiveAttack = function(gameState) +{ + if (this.nearestEnemy === undefined) + return; + gameState.getOwnEntities().forEach(ent => ent.attack(this.nearestEnemy.id(), false, false, true)); +}; + +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.validEntityWithValidOwner(e) && + API3.VectorDistance(e.position(), averagePosition) < this.Config.enemyDetectionRange); +}; + +PETRA.EmergencyManager.prototype.isMovableEntity = function(ent) +{ + return this.validEntity(ent) && ent.walkSpeed() > 0; +}; +PETRA.EmergencyManager.prototype.validEntityWithValidOwner = function(ent) +{ + // Exclude Gaia and INVALID_PLAYER + return this.validEntity(ent) && ent.owner() > 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) + { + if (this.validEntityWithValidOwner(enemy)) + { + const distance = API3.VectorDistance(enemy.position(), averagePosition); + if (distance < nearestEnemyDistance) + { + nearestEnemy = enemy; + nearestEnemyDistance = distance; + } + } + } + this.marchCounter = 0; + this.nearestEnemy = nearestEnemy; + if (nearestEnemy && nearestEnemy.position()) + this.nextBattlePoint = nearestEnemy.position(); + else + { + // We destroyed all own structures, so we can't do anything besides resigning. + gameState.getOwnEntities().forEach(ent => ent.destroy()); + Engine.PostCommand(PlayerID, { "type": "resign" }); + } +}; + +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.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.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, + "emergencyPeakPopulation": this.emergencyPeakPopulation + }; +}; + +PETRA.EmergencyManager.prototype.Deserialize = function(data) +{ + for (const key in data) + this[key] = data[key]; +}; Index: binaries/data/mods/public/simulation/ai/petra/entityExtend.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/entityExtend.js +++ binaries/data/mods/public/simulation/ai/petra/entityExtend.js @@ -159,7 +159,7 @@ /** Decide if we should try to capture (returns true) or destroy (return false) */ PETRA.allowCapture = function(gameState, ent, target) { - if (!target.isCapturable() || !ent.canCapture(target)) + if (!target || !target.isCapturable() || !ent.canCapture(target)) return false; if (target.isInvulnerable()) return true; 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 @@ -13,9 +13,15 @@ this.decayingStructures = new Map(); }; +PETRA.GarrisonManager.prototype.handleEmergency = function(gameState, events) { + this.update(gameState, events); + this.ungarrisonAllUnits(gameState); +}; + PETRA.GarrisonManager.prototype.update = function(gameState, events) { // First check for possible upgrade of a structure + const isEmergency = gameState.emergencyState[PlayerID]; for (let evt of events.EntityRenamed) { for (let id of this.holders.keys()) @@ -24,12 +30,12 @@ continue; let data = this.holders.get(id); let newHolder = gameState.getEntityById(evt.newentity); - if (newHolder && newHolder.isGarrisonHolder()) + if (!isEmergency && newHolder && newHolder.isGarrisonHolder()) { this.holders.delete(id); this.holders.set(evt.newentity, data); } - else + else if (isEmergency) { for (let entId of data.list) { @@ -87,7 +93,7 @@ let ent = gameState.getEntityById(list[j]); if (!ent) // unit must have been killed while garrisoning list.splice(j--, 1); - else if (holder.garrisoned().indexOf(list[j]) !== -1) // unit is garrisoned + else if (holder.garrisoned().indexOf(list[j]) !== -1 || isEmergency) // unit is garrisoned { this.leaveGarrison(ent); list.splice(j--, 1); @@ -164,13 +170,14 @@ for (let entId of holder.garrisoned()) { let ent = gameState.getEntityById(entId); - if (ent.owner() === PlayerID && !this.keepGarrisoned(ent, holder, around)) + if (isEmergency || + (ent.owner() === PlayerID && !this.keepGarrisoned(ent, holder, around))) holder.unload(entId); } for (let j = 0; j < list.length; ++j) { let ent = gameState.getEntityById(list[j]); - if (this.keepGarrisoned(ent, holder, around)) + if (this.keepGarrisoned(ent, holder, around) && !isEmergency) continue; if (ent.getMetadata(PlayerID, "garrisonHolder") == id) { @@ -193,7 +200,7 @@ let ent = gameState.getEntityById(id); if (!ent || ent.owner() !== PlayerID) this.decayingStructures.delete(id); - else if (this.numberOfGarrisonedSlots(ent) < gmin) + else if (this.numberOfGarrisonedSlots(ent) < gmin && !isEmergency) gameState.ai.HQ.defenseManager.garrisonUnitsInside(gameState, ent, { "min": gmin, "type": "decay" }); } }; @@ -364,6 +371,20 @@ 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); + ent.setMetadata("garrisonType", "force"); + } + } + this.holders.clear(); +}; + PETRA.GarrisonManager.prototype.Serialize = function() { return { "holders": this.holders, "decayingStructures": this.decayingStructures }; 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 @@ -44,6 +44,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; @@ -66,6 +67,8 @@ this.treasures.registerUpdates(); this.currentPhase = gameState.currentPhase(); this.decayingStructures = new Set(); + + this.emergencyManager.initPhases(gameState); }; /** @@ -2190,6 +2193,24 @@ PETRA.HQ.prototype.update = function(gameState, queues, events) { Engine.ProfileStart("Headquarters update"); + if (this.emergencyManager.checkForEmergency(gameState)) + { + gameState.emergencyState[PlayerID] = true; + this.garrisonManager.handleEmergency(gameState, events); + this.diplomacyManager.handleEmergency(gameState, events); + this.attackManager.handleEmergency(gameState, events); + this.defenseManager.handleEmergency(gameState, events); + this.emergencyManager.handleEmergency(gameState); + if (!gameState.emergencyState[PlayerID]) + { + // GarrisonManager, AttackManager will do everything by themselves + this.defenseManager.exitEmergency(); + this.diplomacyManager.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(); @@ -2337,6 +2358,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 { @@ -2352,6 +2374,7 @@ "diplomacyManager": this.diplomacyManager.Serialize(), "garrisonManager": this.garrisonManager.Serialize(), "victoryManager": this.victoryManager.Serialize(), + "emergencyManager": this.emergencyManager.Serialize() }; }; @@ -2395,4 +2418,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 @@ -94,7 +94,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) {