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.updateEmergency = 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) + { + const attack = 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); + let civ = gameState.getPlayerCiv(); + for (let 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/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,54 @@ this.attackedAllies = {}; }; +PETRA.DefenseManager.prototype.exitEmergency = function() +{ + this.attackingArmies = {}; + this.attackingUnits = {}; + this.attackedAllies = {}; + this.armies = []; + this.targetList = []; +}; +PETRA.DefenseManager.prototype.updateEmergency = 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.targetList.length; ++i) + { + const target = gameState.getEntityById(this.targetList[i]); + if (!target || !target.position() || !gameState.isPlayerEnemy(target.owner())) + this.targetList.splice(i--, 1); + } + for (let i = 0; i < this.armies.length; ++i) + { + const army = this.armies[i]; + army.update(gameState); + for (let entId of army.foeEntities) + { + let 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 (let entId of army.ownEntities) + { + let 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); + } + army.clear(gameState); + this.armies.splice(i--, 1); + } +}; + PETRA.DefenseManager.prototype.update = function(gameState, events) { Engine.ProfileStart("Defense Manager"); @@ -457,6 +505,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,8 @@ this.receivedDiplomacyRequests = new Map(); this.sentDiplomacyRequests = new Map(); this.sentDiplomacyRequestLapseTime = 120 + randFloat(10, 100); + this.waitsForResponses = false; + this.responseCounter = -1; }; /** @@ -373,15 +375,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"; @@ -394,7 +396,7 @@ } } else if (requestType === "ally" && gameState.getEntities(player).length < gameState.getOwnEntities().length && randBool(0.4) || - requestType === "neutral" && moreEnemiesThanAllies && randBool(0.8)) + requestType === "neutral" && moreEnemiesThanAllies && randBool(0.8) || emergency) { response = "accept"; this.changePlayerDiplomacy(gameState, player, requestType); @@ -519,6 +521,125 @@ } }; +PETRA.DiplomacyManager.prototype.updateEmergency = function(gameState, events) +{ + const personality = this.Config.personality; + API3.warn(personality.aggressive < personality.defensive); + API3.warn(!gameState.sharedScript.playersData[PlayerID].teamsLocked); + API3.warn(personality.cooperative >= 0.1); + API3.warn(this.enoughResourcesForTributes(gameState)); + API3.warn(!this.waitsForResponses); + API3.warn(!gameState.ai.HQ.emergencyManager.troopsMarching(gameState)); + if (personality.aggressive < personality.defensive && + !gameState.sharedScript.playersData[PlayerID].teamsLocked && + personality.cooperative >= 0.1 && + 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) + { + if (this.responseCounter < this.Config.neutralityRequestWaitingDuration) + { + API3.warn("Response counter: " + 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, 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)); + } + API3.warn(JSON.stringify(tribute)); + return tribute; +}; + +PETRA.DiplomacyManager.prototype.enoughResourcesForTributes = function(gameState) +{ + const availableResources = gameState.ai.queueManager.getAvailableResources(gameState); + API3.warn(JSON.stringify(availableResources)); + 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 +678,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,600 @@ +/** + * Emergency manager + * + * Checks, whether there is an emergency and acts accordingly based on the personality + * of the AI. + */ +// Petra error: attacking army 14 without access +/* +ERROR: JavaScript error: simulation/ai/petra/entityExtend.js line 162 +target is undefined + PETRA.allowCapture@simulation/ai/petra/entityExtend.js:162:6 + PETRA.DefenseManager.prototype.assignDefenders/<@simulation/ai/petra/defenseManager.js:494:47 + PETRA.DefenseManager.prototype.assignDefenders@simulation/ai/petra/defenseManager.js:492:44 + PETRA.DefenseManager.prototype.update@simulation/ai/petra/defenseManager.js:97:7 + PETRA.HQ.prototype.update@simulation/ai/petra/headquarters.js:2291:22 + PETRA.PetraBot.prototype.OnUpdate@simulation/ai/petra/_petrabot.js:118:11 + m.BaseAI.prototype.HandleMessage@simulation/ai/common-api/baseAI.js:64:7 +*/ +/* +ERROR: JavaScript error: simulation/ai/petra/basesManager.js line 158 +base is undefined + PETRA.BasesManager.prototype.checkEvents@simulation/ai/petra/basesManager.js:158:7 + PETRA.HQ.prototype.checkEvents@simulation/ai/petra/headquarters.js:119:20 + PETRA.HQ.prototype.update@simulation/ai/petra/headquarters.js:2242:7 + PETRA.PetraBot.prototype.OnUpdate@simulation/ai/petra/_petrabot.js:118:11 + m.BaseAI.prototype.HandleMessage@simulation/ai/common-api/baseAI.js:64:7 +*/ +/* +ERROR: JavaScript error: simulation/ai/common-api/entity.js line 784 +target is undefined + canAttackTarget@simulation/ai/common-api/entity.js:784:16 + PETRA.DefenseManager.prototype.assignDefenders/<@simulation/ai/petra/defenseManager.js:494:17 + PETRA.DefenseManager.prototype.assignDefenders@simulation/ai/petra/defenseManager.js:492:44 + PETRA.DefenseManager.prototype.update@simulation/ai/petra/defenseManager.js:97:7 + PETRA.HQ.prototype.update@simulation/ai/petra/headquarters.js:2291:22 + PETRA.PetraBot.prototype.OnUpdate@simulation/ai/petra/_petrabot.js:118:11 + m.BaseAI.prototype.HandleMessage@simulation/ai/common-api/baseAI.js:64:7 +*/ +// Petra error in incrementBuilderCounters for structures/maur/farmstead with count < 0 +// unknown type in garrisonManager undefined for Athenian Hoplite id 13212 inside Barracks id 12541 +/* +undefined has no properties + PETRA.DefenseManager.prototype.checkEvents@simulation/ai/petra/defenseManager.js:664:8 + PETRA.DefenseManager.prototype.update@simulation/ai/petra/defenseManager.js:59:7 + PETRA.HQ.prototype.update@simulation/ai/petra/headquarters.js:2291:22 + PETRA.PetraBot.prototype.OnUpdate@simulation/ai/petra/_petrabot.js:118:11 + m.BaseAI.prototype.HandleMessage@simulation/ai/common-api/baseAI.js:64:7 +ERROR: JavaScript error: simulation/ai/petra/defenseManager.js line 664 +army is undefined + PETRA.DefenseManager.prototype.checkEvents@simulation/ai/petra/defenseManager.js:664:8 + PETRA.DefenseManager.prototype.update@simulation/ai/petra/defenseManager.js:59:7 + PETRA.HQ.prototype.update@simulation/ai/petra/headquarters.js:2291:22 + PETRA.PetraBot.prototype.OnUpdate@simulation/ai/petra/_petrabot.js:118:11 + m.BaseAI.prototype.HandleMessage@simulation/ai/common-api/baseAI.js:64:7 +*/ +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; +}; + +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 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.1 && + !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()) + { + this.waitForExpiration(gameState); + return; + } + API3.warn("HEREEEE!"); + // 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)) + { + 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 gameState.getOwnEntities().toEntityArray()) + { + if (!ent.get("Attack") || !ent.position()) + continue; + if (API3.VectorDistance(ent.position(), this.musterPosition) > this.Config.patrolRange) + ent.move(this.musterPosition[0] + this.generateDeviation(this.Config.patrolRange / 2), + this.musterPosition[1] + this.generateDeviation(this.Config.patrolRange / 2)); + } + } + } + else + this.aggressiveActions(gameState); +}; + +PETRA.EmergencyManager.prototype.generateDeviation = function(max) +{ + return Math.floor((Math.random() * max) + 1); +}; + +PETRA.EmergencyManager.prototype.countMovableEntities = function(gameState) +{ + const ownEntities = gameState.getOwnEntities().toEntityArray(); + let movableEntitiesCount = 0; + for (const ent of ownEntities) + { + if (ent.walkSpeed() > 0) + movableEntitiesCount++; + } + return movableEntitiesCount; +}; +PETRA.EmergencyManager.prototype.resign = function(gameState) +{ + for (const ent of gameState.getOwnEntities().toEntityArray()) + 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) +{ + 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; + } +}; +PETRA.EmergencyManager.prototype.aggressiveActions = function(gameState) +{ + for (const ent of gameState.getOwnStructures().toEntityArray()) + ent.destroy(); + 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); + if (!this.nextBattlePoint) + return; + 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 && 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); + 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); + if (!this.nextBattlePoint) + return; + this.aggressiveAttack(gameState); + API3.warn("Entity: " + this.nearestEnemy.toString()); + } +}; + +PETRA.EmergencyManager.prototype.aggressiveAttack = function(gameState) +{ + if (this.nearestEnemy === undefined) + return; + for (const ent of gameState.getOwnEntities().toEntityArray()) + 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 { // TODO: Destroy all own + for (const ent of gameState.getOwnEntities().toEntityArray()) + 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.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/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,6 +13,11 @@ this.decayingStructures = new Map(); }; +PETRA.GarrisonManager.prototype.updateEmergency = 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 @@ -24,12 +29,12 @@ continue; let data = this.holders.get(id); let newHolder = gameState.getEntityById(evt.newentity); - if (newHolder && newHolder.isGarrisonHolder()) + if (!gameState.emergencyState[PlayerID] && newHolder && newHolder.isGarrisonHolder()) { this.holders.delete(id); this.holders.set(evt.newentity, data); } - else + else if (gameState.emergencyState[PlayerID]) { for (let entId of data.list) { @@ -87,7 +92,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 || gameState.emergencyState[PlayerID]) // unit is garrisoned { this.leaveGarrison(ent); list.splice(j--, 1); @@ -164,13 +169,14 @@ for (let entId of holder.garrisoned()) { let ent = gameState.getEntityById(entId); - if (ent.owner() === PlayerID && !this.keepGarrisoned(ent, holder, around)) + if (gameState.emergencyState[PlayerID] || + (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) && !gameState.emergencyState[PlayerID]) continue; if (ent.getMetadata(PlayerID, "garrisonHolder") == id) { @@ -193,7 +199,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 && !gameState.emergencyState[PlayerID]) gameState.ai.HQ.defenseManager.garrisonUnitsInside(gameState, ent, { "min": gmin, "type": "decay" }); } }; @@ -364,6 +370,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,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, events); + 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(); @@ -2337,6 +2357,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 +2373,7 @@ "diplomacyManager": this.diplomacyManager.Serialize(), "garrisonManager": this.garrisonManager.Serialize(), "victoryManager": this.victoryManager.Serialize(), + "emergencyManager": this.emergencyManager.Serialize() }; }; @@ -2395,4 +2417,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) {