0ad/binaries/data/mods/public/simulation/ai/petra/config.js
2026-07-23 15:15:59 +02:00

356 lines
11 KiB
JavaScript

import { aiWarn } from "simulation/ai/common-api/utils.js";
import * as difficultyLevel from "simulation/ai/petra/difficultyLevel.js";
export class Config
{
// debug level: 0=none, 1=sanity checks, 2=debug, 3=detailed debug, -100=serializatio debug
debug = 0;
chat = true; // false to prevent AI's chats
popScaling = 1; // scale factor depending on the max population
Military = {
"towerLapseTime": 360, // Time to wait between building 2 towers
"fortressLapseTime": 390, // Time to wait between building 2 fortresses
"popForBarracks1": 25,
"popForBarracks2": 55,
"popForForge": 65,
"numSentryTowers": 1
};
DamageTypeImportance = {
"Hack": 0.075,
"Pierce": 0.085,
"Crush": 0.045,
"Fire": 0.001
};
Economy = {
"popPhase2": 150, // How many units we want before aging to phase2.
"workPhase3": 180, // How many workers we want before aging to phase3.
"workPhase4": 200, // How many workers we want before aging to phase4 or higher.
"popForDock": 25,
"targetNumWorkers": 60, // dummy, will be changed later
"targetNumTraders": 1, // Target number of traders
"targetNumFishers": 1, // Target number of fishers per sea
"supportRatio": 0.3, // fraction of support workers among the workforce
"provisionFields": 2
};
// Note: attack settings are set directly in attack_plan.js
// defense
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.
buildings =
{
"default": [],
"achae": [
"structures/{civ}/tachara"
],
"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"
],
"han": [
"structures/{civ}/academy"
],
"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"
],
"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"
]
};
priorities =
{
"villager": 300, // should be slightly lower than the citizen soldier one to not get all the food
"citizenSoldier": 600,
"trader": 1,
"healer": 20,
"ships": 1,
"house": 250,
"dropsites": 950,
"field": 480,
"dock": 90,
"corral": 1,
"economicBuilding": 700,
"militaryBuilding": 330,
"defenseBuilding": 70,
"civilCentre": 1,
"majorTech": 700,
"minorTech": 250,
"wonder": 1,
"emergency": 1000 // used only in emergency situations, should be the highest one
};
// Default personality (will be updated in setConfig)
personality =
{
"aggressive": 0.5,
"cooperative": 0.5,
"defensive": 0.5
};
// See QueueManager.prototype.wantedGatherRates()
queues =
{
"firstTurn": {
"food": 10,
"wood": 10,
"default": 0
},
"short": {
"food": 200,
"wood": 200,
"default": 100
},
"medium": {
"default": 0
},
"long": {
"default": 0
}
};
garrisonHealthLevel = { "low": 0.4, "medium": 0.55, "high": 0.7 };
unusedNoAllyTechs = [
"Player/sharedLos",
"Market/InternationalBonus",
"Player/sharedDropsites"
];
criticalPopulationFactors = [
0.8,
0.8,
0.7,
0.6,
0.5,
0.35
];
criticalStructureFactors = [
0.8,
0.8,
0.7,
0.6,
0.5,
0.35
];
criticalRootFactors = [
0.8,
0.8,
0.67,
0.5,
0.35,
0.2
];
constructor(difficulty = difficultyLevel.MEDIUM, behavior)
{
this.difficulty = difficulty;
// for instance "balanced", "aggressive" or "defensive"
this.behavior = behavior || "random";
}
setConfig(gameState)
{
if (this.difficulty > difficultyLevel.SANDBOX)
{
// 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
const 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 }
};
const behavior = randFloat(-0.5, 0.5);
// make agressive and defensive quite anticorrelated (aggressive ~ 1 - defensive) but not completelety
const variation = 0.15 * randFloat(-1, 1) * Math.sqrt(Math.square(0.5) - Math.square(behavior));
const aggressive = Math.max(Math.min(behavior + variation, 0.5), -0.5) + 0.5;
const defensive = Math.max(Math.min(-behavior + variation, 0.5), -0.5) + 0.5;
const min = personalityList[this.behavior].min;
const 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 < difficultyLevel.EASY)
{
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 < difficultyLevel.MEDIUM)
{
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 == difficultyLevel.MEDIUM)
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;
}
}
const maxPop = gameState.getPopulationMax();
if (this.difficulty < difficultyLevel.EASY)
this.Economy.targetNumWorkers = Math.max(1, Math.min(40, maxPop));
else if (this.difficulty < difficultyLevel.MEDIUM)
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 < difficultyLevel.EASY)
this.Economy.workPhase3 = Infinity; // prevent the phasing to city phase
this.emergencyValues = {
"population": this.criticalPopulationFactors[this.difficulty],
"structures": this.criticalStructureFactors[this.difficulty],
"roots": this.criticalRootFactors[this.difficulty],
};
this.Cheat(gameState);
if (this.debug < 2)
return;
aiWarn(" >>> Petra bot: personality = " + uneval(this.personality));
}
Cheat(gameState)
{
// Sandbox, Very Easy, Easy, Medium, Hard, Very Hard
// rate apply on resource stockpiling as gathering and trading
// time apply on building, upgrading, packing, training and technologies
const rate = [ 0.42, 0.56, 0.75, 1.00, 1.25, 1.56 ];
const time = [ 1.40, 1.25, 1.10, 1.00, 1.00, 1.00 ];
const AIDiff = Math.min(this.difficulty, rate.length - 1);
SimEngine.QueryInterface(Sim.SYSTEM_ENTITY, Sim.IID_ModifiersManager).AddModifiers("AI Bonus", {
"ResourceGatherer/BaseSpeed": [{ "affects": ["Unit", "Structure"], "multiply": rate[AIDiff] }],
"Trader/GainMultiplier": [{ "affects": ["Unit", "Structure"], "multiply": rate[AIDiff] }],
"Cost/BuildTime": [{ "affects": ["Unit", "Structure"], "multiply": time[AIDiff] }],
}, gameState.playerData.entity);
}
Serialize()
{
var data = {};
for (const key in this)
if (Object.hasOwn(this, key) && key != "debug")
data[key] = this[key];
return data;
}
Deserialize(data)
{
for (const key in data)
this[key] = data[key];
}
}