Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/config.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/config.js (revision 25922) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/config.js (revision 25923) @@ -1,292 +1,298 @@ PETRA.Config = function(difficulty, behavior) { // 0 is sandbox, 1 is very easy, 2 is easy, 3 is medium, 4 is hard and 5 is very hard. this.difficulty = difficulty !== undefined ? difficulty : 3; // 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 this.debug = 0; this.chat = true; // false to prevent AI's chats this.popScaling = 1; // scale factor depending on the max population this.Military = { "towerLapseTime": 90, // Time to wait between building 2 towers "fortressLapseTime": 390, // Time to wait between building 2 fortresses "popForBarracks1": 25, "popForBarracks2": 95, "popForForge": 65, "numSentryTowers": 1 }; this.DamageTypeImportance = { "Hack": 0.085, "Pierce": 0.075, "Crush": 0.065, "Fire": 0.095 }; this.Economy = { "popPhase2": 38, // How many units we want before aging to phase2. "workPhase3": 65, // How many workers we want before aging to phase3. "workPhase4": 80, // How many workers we want before aging to phase4 or higher. "popForDock": 25, "targetNumWorkers": 40, // dummy, will be changed later "targetNumTraders": 5, // Target number of traders "targetNumFishers": 1, // Target number of fishers per sea "supportRatio": 0.35, // fraction of support workers among the workforce "provisionFields": 2 }; // Note: attack settings are set directly in attack_plan.js // defense this.Defense = { "defenseRatio": { "ally": 1.4, "neutral": 1.8, "own": 2 }, // ratio of defenders/attackers. "armyCompactSize": 2000, // squared. Half-diameter of an army. "armyBreakawaySize": 3500, // squared. "armyMergeSize": 1400 // squared. }; // Additional buildings that the AI does not yet know when to build // and that it will try to build on phase 3 when enough resources. this.buildings = { "default": [], "athen": [ "structures/{civ}/gymnasium", "structures/{civ}/prytaneion", "structures/{civ}/theater" ], "brit": [], "cart": [ "structures/{civ}/embassy_celtic", "structures/{civ}/embassy_iberian", "structures/{civ}/embassy_italic" ], "gaul": [ "structures/{civ}/assembly" ], "iber": [ "structures/{civ}/monument" ], "kush": [ "structures/{civ}/camp_blemmye", "structures/{civ}/camp_noba", "structures/{civ}/pyramid_large", "structures/{civ}/pyramid_small", "structures/{civ}/temple_amun" ], "mace": [ "structures/{civ}/theater" ], "maur": [ "structures/{civ}/palace", "structures/{civ}/pillar_ashoka" ], "pers": [ "structures/{civ}/apadana" ], "ptol": [ "structures/{civ}/library", "structures/{civ}/theater" ], "rome": [ "structures/{civ}/army_camp", "structures/{civ}/temple_vesta" ], "sele": [ "structures/{civ}/theater" ], "spart": [ "structures/{civ}/syssiton", "structures/{civ}/theater" ] }; this.priorities = { "villager": 30, // should be slightly lower than the citizen soldier one to not get all the food "citizenSoldier": 60, "trader": 50, "healer": 20, "ships": 70, "house": 350, "dropsites": 200, "field": 400, "dock": 90, "corral": 100, "economicBuilding": 90, "militaryBuilding": 130, "defenseBuilding": 70, "civilCentre": 950, "majorTech": 700, "minorTech": 250, "wonder": 1000, "emergency": 1000 // used only in emergency situations, should be the highest one }; // Default personality (will be updated in setConfig) this.personality = { "aggressive": 0.5, "cooperative": 0.5, "defensive": 0.5 }; // See PETRA.QueueManager.prototype.wantedGatherRates() this.queues = { "firstTurn": { "food": 10, "wood": 10, "default": 0 }, "short": { "food": 200, "wood": 200, "default": 100 }, "medium": { "default": 0 }, "long": { "default": 0 } }; this.garrisonHealthLevel = { "low": 0.4, "medium": 0.55, "high": 0.7 }; + + this.unusedNoAllyTechs = [ + "Player/sharedLos", + "Market/InternationalBonus", + "Player/sharedDropsites" + ]; }; PETRA.Config.prototype.setConfig = function(gameState) { if (this.difficulty > 0) { // Setup personality traits according to the user choice: // The parameter used to define the personality is basically the aggressivity or (1-defensiveness) // as they are anticorrelated, although some small smearing to decorelate them will be added. // And for each user choice, this parameter can vary between min and max let personalityList = { "random": { "min": 0, "max": 1 }, "defensive": { "min": 0, "max": 0.27 }, "balanced": { "min": 0.37, "max": 0.63 }, "aggressive": { "min": 0.73, "max": 1 } }; let behavior = randFloat(-0.5, 0.5); // make agressive and defensive quite anticorrelated (aggressive ~ 1 - defensive) but not completelety let variation = 0.15 * randFloat(-1, 1) * Math.sqrt(Math.square(0.5) - Math.square(behavior)); let aggressive = Math.max(Math.min(behavior + variation, 0.5), -0.5) + 0.5; let defensive = Math.max(Math.min(-behavior + variation, 0.5), -0.5) + 0.5; let min = personalityList[this.behavior].min; let max = personalityList[this.behavior].max; this.personality = { "aggressive": min + aggressive * (max - min), "defensive": 1 - max + defensive * (max - min), "cooperative": randFloat(0, 1) }; } // Petra usually uses the continuous values of personality.aggressive and personality.defensive // to define its behavior according to personality. But when discontinuous behavior is needed, // it uses the following personalityCut which should be set such that: // behavior="aggressive" => personality.aggressive > personalityCut.strong && // personality.defensive < personalityCut.weak // and inversely for behavior="defensive" this.personalityCut = { "weak": 0.3, "medium": 0.5, "strong": 0.7 }; if (gameState.playerData.teamsLocked) this.personality.cooperative = Math.min(1, this.personality.cooperative + 0.30); else if (gameState.getAlliedVictory()) this.personality.cooperative = Math.min(1, this.personality.cooperative + 0.15); // changing settings based on difficulty or personality this.Military.towerLapseTime = Math.round(this.Military.towerLapseTime * (1.1 - 0.2 * this.personality.defensive)); this.Military.fortressLapseTime = Math.round(this.Military.fortressLapseTime * (1.1 - 0.2 * this.personality.defensive)); this.priorities.defenseBuilding = Math.round(this.priorities.defenseBuilding * (0.9 + 0.2 * this.personality.defensive)); if (this.difficulty < 2) { this.popScaling = 0.5; this.Economy.supportRatio = 0.5; this.Economy.provisionFields = 1; this.Military.numSentryTowers = this.personality.defensive > this.personalityCut.strong ? 1 : 0; } else if (this.difficulty < 3) { this.popScaling = 0.7; this.Economy.supportRatio = 0.4; this.Economy.provisionFields = 1; this.Military.numSentryTowers = this.personality.defensive > this.personalityCut.strong ? 1 : 0; } else { if (this.difficulty == 3) this.Military.numSentryTowers = 1; else this.Military.numSentryTowers = 2; if (this.personality.defensive > this.personalityCut.strong) ++this.Military.numSentryTowers; else if (this.personality.defensive < this.personalityCut.weak) --this.Military.numSentryTowers; if (this.personality.aggressive > this.personalityCut.strong) { this.Military.popForBarracks1 = 12; this.Economy.popPhase2 = 50; this.priorities.healer = 10; } } let maxPop = gameState.getPopulationMax(); if (this.difficulty < 2) this.Economy.targetNumWorkers = Math.max(1, Math.min(40, maxPop)); else if (this.difficulty < 3) this.Economy.targetNumWorkers = Math.max(1, Math.min(60, Math.floor(maxPop/2))); else this.Economy.targetNumWorkers = Math.max(1, Math.min(120, Math.floor(maxPop/3))); this.Economy.targetNumTraders = 2 + this.difficulty; if (gameState.getVictoryConditions().has("wonder")) { this.Economy.workPhase3 = Math.floor(0.9 * this.Economy.workPhase3); this.Economy.workPhase4 = Math.floor(0.9 * this.Economy.workPhase4); } if (maxPop < 300) this.popScaling *= Math.sqrt(maxPop / 300); this.Military.popForBarracks1 = Math.min(Math.max(Math.floor(this.Military.popForBarracks1 * this.popScaling), 12), Math.floor(maxPop/5)); this.Military.popForBarracks2 = Math.min(Math.max(Math.floor(this.Military.popForBarracks2 * this.popScaling), 45), Math.floor(maxPop*2/3)); this.Military.popForForge = Math.min(Math.max(Math.floor(this.Military.popForForge * this.popScaling), 30), Math.floor(maxPop/2)); this.Economy.popPhase2 = Math.min(Math.max(Math.floor(this.Economy.popPhase2 * this.popScaling), 20), Math.floor(maxPop/2)); this.Economy.workPhase3 = Math.min(Math.max(Math.floor(this.Economy.workPhase3 * this.popScaling), 40), Math.floor(maxPop*2/3)); this.Economy.workPhase4 = Math.min(Math.max(Math.floor(this.Economy.workPhase4 * this.popScaling), 45), Math.floor(maxPop*2/3)); this.Economy.targetNumTraders = Math.round(this.Economy.targetNumTraders * this.popScaling); this.Economy.targetNumWorkers = Math.max(this.Economy.targetNumWorkers, this.Economy.popPhase2); this.Economy.workPhase3 = Math.min(this.Economy.workPhase3, this.Economy.targetNumWorkers); this.Economy.workPhase4 = Math.min(this.Economy.workPhase4, this.Economy.targetNumWorkers); if (this.difficulty < 2) this.Economy.workPhase3 = Infinity; // prevent the phasing to city phase if (this.debug < 2) return; API3.warn(" >>> Petra bot: personality = " + uneval(this.personality)); }; PETRA.Config.prototype.Serialize = function() { var data = {}; for (let key in this) if (this.hasOwnProperty(key) && key != "debug") data[key] = this[key]; return data; }; PETRA.Config.prototype.Deserialize = function(data) { for (let key in data) this[key] = data[key]; }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/researchManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/researchManager.js (revision 25922) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/researchManager.js (revision 25923) @@ -1,242 +1,242 @@ /** * Manage the research */ PETRA.ResearchManager = function(Config) { this.Config = Config; }; /** * Check if we can go to the next phase */ PETRA.ResearchManager.prototype.checkPhase = function(gameState, queues) { if (queues.majorTech.hasQueuedUnits()) return; // Don't try to phase up if already trying to gather resources for a civil-centre or wonder if (queues.civilCentre.hasQueuedUnits() || queues.wonder.hasQueuedUnits()) return; let currentPhaseIndex = gameState.currentPhase(); let nextPhaseName = gameState.getPhaseName(currentPhaseIndex+1); if (!nextPhaseName) return; let petraRequirements = currentPhaseIndex == 1 && gameState.ai.HQ.getAccountedPopulation(gameState) >= this.Config.Economy.popPhase2 || currentPhaseIndex == 2 && gameState.ai.HQ.getAccountedWorkers(gameState) > this.Config.Economy.workPhase3 || currentPhaseIndex >= 3 && gameState.ai.HQ.getAccountedWorkers(gameState) > this.Config.Economy.workPhase4; if (petraRequirements && gameState.hasResearchers(nextPhaseName, true)) { gameState.ai.HQ.phasing = currentPhaseIndex + 1; // Reset the queue priority in case it was changed during a previous phase update gameState.ai.queueManager.changePriority("majorTech", gameState.ai.Config.priorities.majorTech); queues.majorTech.addPlan(new PETRA.ResearchPlan(gameState, nextPhaseName, true)); } }; PETRA.ResearchManager.prototype.researchPopulationBonus = function(gameState, queues) { if (queues.minorTech.hasQueuedUnits()) return; let techs = gameState.findAvailableTech(); for (let tech of techs) { if (!tech[1]._template.modifications) continue; // TODO may-be loop on all modifs and check if the effect if positive ? if (tech[1]._template.modifications[0].value !== "Population/Bonus") continue; queues.minorTech.addPlan(new PETRA.ResearchPlan(gameState, tech[0])); break; } }; PETRA.ResearchManager.prototype.researchTradeBonus = function(gameState, queues) { if (queues.minorTech.hasQueuedUnits()) return; let techs = gameState.findAvailableTech(); for (let tech of techs) { if (!tech[1]._template.modifications || !tech[1]._template.affects) continue; if (tech[1]._template.affects.indexOf("Trader") === -1) continue; // TODO may-be loop on all modifs and check if the effect if positive ? if (tech[1]._template.modifications[0].value !== "UnitMotion/WalkSpeed" && tech[1]._template.modifications[0].value !== "Trader/GainMultiplier") continue; queues.minorTech.addPlan(new PETRA.ResearchPlan(gameState, tech[0])); break; } }; /** Techs to be searched for as soon as they are available */ PETRA.ResearchManager.prototype.researchWantedTechs = function(gameState, techs) { let phase1 = gameState.currentPhase() === 1; let available = phase1 ? gameState.ai.queueManager.getAvailableResources(gameState) : null; let numWorkers = phase1 ? gameState.getOwnEntitiesByRole("worker", true).length : 0; for (let tech of techs) { if (tech[0].indexOf("unlock_champion") == 0) return { "name": tech[0], "increasePriority": true }; if (tech[0] == "traditional_army_sele" || tech[0] == "reformed_army_sele") return { "name": pickRandom(["traditional_army_sele", "reformed_army_sele"]), "increasePriority": true }; if (!tech[1]._template.modifications) continue; let template = tech[1]._template; if (phase1) { let cost = template.cost; let costMax = 0; for (let res in cost) costMax = Math.max(costMax, Math.max(cost[res]-available[res], 0)); if (10*numWorkers < costMax) continue; } for (let i in template.modifications) { if (gameState.ai.HQ.navalMap && template.modifications[i].value === "ResourceGatherer/Rates/food.fish") return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 }; else if (template.modifications[i].value === "ResourceGatherer/Rates/food.fruit") return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 }; else if (template.modifications[i].value === "ResourceGatherer/Rates/food.grain") return { "name": tech[0], "increasePriority": false }; else if (template.modifications[i].value === "ResourceGatherer/Rates/wood.tree") return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 }; else if (template.modifications[i].value.startsWith("ResourceGatherer/Capacities")) return { "name": tech[0], "increasePriority": false }; else if (template.modifications[i].value === "Attack/Ranged/MaxRange") return { "name": tech[0], "increasePriority": false }; } } return null; }; /** Techs to be searched for as soon as they are available, but only after phase 2 */ PETRA.ResearchManager.prototype.researchPreferredTechs = function(gameState, techs) { let phase2 = gameState.currentPhase() === 2; let available = phase2 ? gameState.ai.queueManager.getAvailableResources(gameState) : null; let numWorkers = phase2 ? gameState.getOwnEntitiesByRole("worker", true).length : 0; for (let tech of techs) { if (!tech[1]._template.modifications) continue; let template = tech[1]._template; if (phase2) { let cost = template.cost; let costMax = 0; for (let res in cost) costMax = Math.max(costMax, Math.max(cost[res]-available[res], 0)); if (10*numWorkers < costMax) continue; } for (let i in template.modifications) { if (template.modifications[i].value === "ResourceGatherer/Rates/stone.rock") return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 }; else if (template.modifications[i].value === "ResourceGatherer/Rates/metal.ore") return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 }; else if (template.modifications[i].value === "BuildingAI/DefaultArrowCount") return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 }; else if (template.modifications[i].value === "Health/RegenRate") return { "name": tech[0], "increasePriority": false }; else if (template.modifications[i].value === "Health/IdleRegenRate") return { "name": tech[0], "increasePriority": false }; } } return null; }; PETRA.ResearchManager.prototype.update = function(gameState, queues) { if (queues.minorTech.hasQueuedUnits() || queues.majorTech.hasQueuedUnits()) return; let techs = gameState.findAvailableTech(); let techName = this.researchWantedTechs(gameState, techs); if (techName) { if (techName.increasePriority) { gameState.ai.queueManager.changePriority("minorTech", 2*this.Config.priorities.minorTech); let plan = new PETRA.ResearchPlan(gameState, techName.name); plan.queueToReset = "minorTech"; queues.minorTech.addPlan(plan); } else queues.minorTech.addPlan(new PETRA.ResearchPlan(gameState, techName.name)); return; } if (gameState.currentPhase() < 2) return; techName = this.researchPreferredTechs(gameState, techs); if (techName) { if (techName.increasePriority) { gameState.ai.queueManager.changePriority("minorTech", 2*this.Config.priorities.minorTech); let plan = new PETRA.ResearchPlan(gameState, techName.name); plan.queueToReset = "minorTech"; queues.minorTech.addPlan(plan); } else queues.minorTech.addPlan(new PETRA.ResearchPlan(gameState, techName.name)); return; } if (gameState.currentPhase() < 3) return; // remove some techs not yet used by this AI // remove also sharedLos if we have no ally for (let i = 0; i < techs.length; ++i) { let template = techs[i][1]._template; if (template.affects && template.affects.length === 1 && (template.affects[0] === "Healer" || template.affects[0] === "Outpost" || template.affects[0] === "Wall")) { techs.splice(i--, 1); continue; } if (template.modifications && template.modifications.length === 1 && - template.modifications[0].value === "Player/sharedLos" && + this.Config.unusedNoAllyTechs.includes(template.modifications[0].value) && !gameState.hasAllies()) { techs.splice(i--, 1); continue; } } if (!techs.length) return; // randomly pick one. No worries about pairs in that case. queues.minorTech.addPlan(new PETRA.ResearchPlan(gameState, pickRandom(techs)[0])); }; PETRA.ResearchManager.prototype.CostSum = function(cost) { let costSum = 0; for (let res in cost) costSum += cost[res]; return costSum; }; PETRA.ResearchManager.prototype.Serialize = function() { return {}; }; PETRA.ResearchManager.prototype.Deserialize = function(data) { };