0ad/binaries/data/mods/public/simulation/ai/qbot/military.js

588 lines
17 KiB
JavaScript
Executable File

/*
* Military strategy:
* * Try training an attack squad of a specified size
* * When it's the appropriate size, send it to attack the enemy
* * Repeat forever
*
*/
var MilitaryAttackManager = function() {
// these use the structure soldiers[unitId] = true|false to register the units
this.soldiers = {};
this.assigned = {};
this.unassigned = {};
this.garrisoned = {};
this.enemyAttackers = {};
this.attackManagers = [AttackMoveToLocation];
this.availableAttacks = [];
this.currentAttacks = [];
// Counts how many attacks we have sent at the enemy.
this.attackCount = 0;
this.lastAttackTime = 0;
this.defenceManager = new Defence();
};
MilitaryAttackManager.prototype.init = function(gameState) {
var civ = gameState.playerData.civ;
// load units and buildings from the config files
if (civ in Config.units.citizenSoldier){
this.uCitizenSoldier = Config.units.citizenSoldier[civ];
}else{
this.uCitizenSoldier = Config.units.citizenSoldier['default'];
}
if (civ in Config.units.advanced){
this.uAdvanced = Config.units.advanced[civ];
}else{
this.uAdvanced = Config.units.advanced['default'];
}
if (civ in Config.units.siege){
this.uSiege = Config.units.siege[civ];
}else{
this.uSiege = Config.units.siege['default'];
}
if (civ in Config.buildings.moderate){
this.bModerate = Config.buildings.moderate[civ];
}else{
this.bModerate = Config.buildings.moderate['default'];
}
if (civ in Config.buildings.advanced){
this.bAdvanced = Config.buildings.advanced[civ];
}else{
this.bAdvanced = Config.buildings.advanced['default'];
}
if (civ in Config.buildings.fort){
this.bFort = Config.buildings.fort[civ];
}else{
this.bFort = Config.buildings.fort['default'];
}
for (var i in this.uCitizenSoldier){
this.uCitizenSoldier[i] = gameState.applyCiv(this.uCitizenSoldier[i]);
}
for (var i in this.uAdvanced){
this.uAdvanced[i] = gameState.applyCiv(this.uAdvanced[i]);
}
for (var i in this.uSiege){
this.uSiege[i] = gameState.applyCiv(this.uSiege[i]);
}
for (var i in this.bAdvanced){
this.bAdvanced[i] = gameState.applyCiv(this.bAdvanced[i]);
}
for (var i in this.bFort){
this.bFort[i] = gameState.applyCiv(this.bFort[i]);
}
this.getEconomicTargets = function(gameState, militaryManager){
return militaryManager.getEnemyBuildings(gameState, "Economic");
};
// TODO: figure out how to make this generic
for (var i in this.attackManagers){
this.availableAttacks[i] = new this.attackManagers[i](gameState, this);
}
var filter = Filters.and(Filters.isEnemy(), Filters.byClassesOr(["CitizenSoldier", "Super", "Siege"]));
this.enemySoldiers = new EntityCollection(gameState.ai, gameState.entities._entities, filter, gameState);
};
/**
* @param (GameState) gameState
* @returns array of soldiers for which training buildings exist
*/
MilitaryAttackManager.prototype.findTrainableUnits = function(gameState, soldierTypes){
var ret = [];
gameState.getOwnEntities().forEach(function(ent) {
var trainable = ent.trainableEntities();
for (var i in trainable){
if (soldierTypes.indexOf(trainable[i]) !== -1){
if (ret.indexOf(trainable[i]) === -1){
ret.push(trainable[i]);
}
}
}
return true;
});
return ret;
};
/**
* Returns the unit type we should begin training. (Currently this is whatever
* we have least of.)
*/
MilitaryAttackManager.prototype.findBestNewUnit = function(gameState, queue, soldierTypes) {
var units = this.findTrainableUnits(gameState, soldierTypes);
// Count each type
var types = [];
for ( var tKey in units) {
var t = units[tKey];
types.push([t, gameState.countEntitiesAndQueuedWithType(gameState.applyCiv(t))
+ queue.countAllByType(gameState.applyCiv(t)) ]);
}
// Sort by increasing count
types.sort(function(a, b) {
return a[1] - b[1];
});
if (types.length === 0){
return false;
}
return types[0][0];
};
MilitaryAttackManager.prototype.registerSoldiers = function(gameState) {
var soldiers = gameState.getOwnEntitiesWithRole("soldier");
var self = this;
soldiers.forEach(function(ent) {
ent.setMetadata("role", "registeredSoldier");
self.soldiers[ent.id()] = true;
self.unassigned[ent.id()] = true;
});
};
// Ungarrisons all units
MilitaryAttackManager.prototype.ungarrisonAll = function(gameState) {
debug("ungarrison units");
this.getGarrisonBuildings(gameState).forEach(function(bldg){
bldg.unloadAll();
});
for ( var i in this.garrisoned) {
if(this.assigned[i]) {
this.unassignUnit(i);
}
if (this.entity(i)){
this.entity(i).setMetadata("subrole","idle");
}
}
this.garrisoned = {};
};
//Garrisons citizens
MilitaryAttackManager.prototype.garrisonCitizens = function(gameState) {
var self = this;
debug("garrison Citizens");
gameState.getOwnEntities().forEach(function(ent) {
var dogarrison = false;
// Look for workers which have a position (i.e. not garrisoned)
if(ent.hasClass("Worker") && ent.position()) {
for (id in self.enemyAttackers) {
if(self.entity(id).visionRange() >= VectorDistance(self.entity(id).position(),ent.position())) {
dogarrison = true;
break;
}
}
if(dogarrison) {
self.garrisonUnit(gameState,ent.id());
}
}
return true;
});
};
// garrison the soldiers
MilitaryAttackManager.prototype.garrisonSoldiers = function(gameState) {
debug("garrison Soldiers");
var units = this.getAvailableUnits(this.countAvailableUnits());
for (var i in units) {
this.garrisonUnit(gameState,units[i]);
if(!this.garrisoned[units[i]]) {
this.unassignUnit(units[i]);
}
}
};
MilitaryAttackManager.prototype.garrisonUnit = function(gameState,id) {
if (this.entity(id).position() === undefined){
return;
}
var garrisonBuildings = this.getGarrisonBuildings(gameState).toEntityArray();
var bldgDistance = [];
for (var i in garrisonBuildings) {
var bldg = garrisonBuildings[i];
if(bldg.garrisoned().length <= bldg.garrisonMax()) {
bldgDistance.push([i,VectorDistance(bldg.position(),this.entity(id).position())]);
}
}
if(bldgDistance.length > 0) {
bldgDistance.sort(function(a,b) { return (a[1]-b[1]); });
var building = garrisonBuildings[bldgDistance[0][0]];
//debug("garrison id "+id+"into building "+building.id()+"walking distance "+bldgDistance[0][1]);
this.entity(id).garrison(building);
this.garrisoned[id] = true;
this.entity(id).setMetadata("subrole","garrison");
}
};
// return count of enemy buildings for a given building class
MilitaryAttackManager.prototype.getEnemyBuildings = function(gameState,cls) {
var targets = gameState.entities.filter(function(ent) {
return (gameState.isEntityEnemy(ent) && ent.hasClass("Structure") && ent.hasClass(cls) && ent.owner() !== 0 && ent.position());
});
return targets;
};
// return count of own buildings for a given building class
MilitaryAttackManager.prototype.getGarrisonBuildings = function(gameState) {
var targets = gameState.getOwnEntities().filter(function(ent) {
return (ent.hasClass("Structure") && ent.garrisonableClasses());
});
return targets;
};
// return n available units and makes these units unavailable
MilitaryAttackManager.prototype.getAvailableUnits = function(n, filter) {
var ret = [];
var count = 0;
for (var i in this.unassigned) {
if (this.unassigned[i]){
var ent = this.entity(i);
if (filter){
if (!filter(ent)){
continue;
}
}
ret.push(+i);
delete this.unassigned[i];
this.assigned[i] = true;
ent.setMetadata("role", "assigned");
ent.setMetadata("subrole", "unavailable");
count++;
if (count >= n) {
break;
}
}
}
return ret;
};
// Takes a single unit id, and marks it unassigned
MilitaryAttackManager.prototype.unassignUnit = function(unit){
this.unassigned[unit] = true;
this.assigned[unit] = false;
};
// Takes an array of unit id's and marks all of them unassigned
MilitaryAttackManager.prototype.unassignUnits = function(units){
for (var i in units){
this.unassigned[units[i]] = true;
this.assigned[units[i]] = false;
}
};
MilitaryAttackManager.prototype.countAvailableUnits = function(filter){
var count = 0;
for (var i in this.unassigned){
if (this.unassigned[i]){
if (filter){
if (filter(this.entity(i))){
count += 1;
}
}else{
count += 1;
}
}
}
return count;
};
MilitaryAttackManager.prototype.handleEvents = function(gameState, events) {
var myCivCentres = gameState.getOwnEntities().filter(function(ent) {
return ent.hasClass("CivCentre");
}).toEntityArray();
var pos = undefined;
if (myCivCentres.length > 0 && myCivCentres[0].position()){
pos = myCivCentres[0].position();
}
for (var i in events) {
var e = events[i];
if (e.type === "Destroy") {
var id = e.msg.entity;
delete this.unassigned[id];
delete this.assigned[id];
delete this.soldiers[id];
}
}
};
// Takes an entity id and returns an entity object or false if there is no entity with that id
// Also sends a debug message warning if the id has no entity
MilitaryAttackManager.prototype.entity = function(id) {
if (this.gameState.entities._entities[id]) {
return new Entity(this.gameState.ai, this.gameState.entities._entities[id]);
}else{
//debug("Entity " + id + " requested does not exist");
}
return undefined;
};
// Returns the military strength of unit
MilitaryAttackManager.prototype.getUnitStrength = function(ent){
var strength = 0.0;
var attackTypes = ent.attackTypes();
var armourStrength = ent.armourStrengths();
var hp = 2 * ent.hitpoints() / (160 + 1*ent.maxHitpoints()); //100 = typical number of hitpoints
for (var typeKey in attackTypes) {
var type = attackTypes[typeKey];
var attackStrength = ent.attackStrengths(type);
var attackRange = ent.attackRange(type);
var attackTimes = ent.attackTimes(type);
for (var str in attackStrength) {
var val = parseFloat(attackStrength[str]);
switch (str) {
case "crush":
strength += (val * 0.085) / 3;
break;
case "hack":
strength += (val * 0.075) / 3;
break;
case "pierce":
strength += (val * 0.065) / 3;
break;
}
}
if (attackRange){
strength += (attackRange.max * 0.0125) ;
}
for (var str in attackTimes) {
var val = parseFloat(attackTimes[str]);
switch (str){
case "repeat":
strength += (val / 100000);
break;
case "prepare":
strength -= (val / 100000);
break;
}
}
}
for (var str in armourStrength) {
var val = parseFloat(armourStrength[str]);
switch (str) {
case "crush":
strength += (val * 0.085) / 3;
break;
case "hack":
strength += (val * 0.075) / 3;
break;
case "pierce":
strength += (val * 0.065) / 3;
break;
}
}
return strength * hp;
};
// Returns the strength of the available units of ai army
MilitaryAttackManager.prototype.measureAvailableStrength = function(){
var strength = 0.0;
for (var i in this.unassigned){
if (this.unassigned[i] && this.entity(i)){
strength += this.getUnitStrength(this.entity(i));
}
}
return strength;
};
MilitaryAttackManager.prototype.getEnemySoldiers = function(){
return this.enemySoldiers;
};
// Returns the number of units in the largest enemy army
MilitaryAttackManager.prototype.measureEnemyCount = function(gameState){
// Measure enemy units
var isEnemy = gameState.playerData.isEnemy;
var enemyCount = [];
var maxCount = 0;
for ( var i = 1; i < isEnemy.length; i++) {
enemyCount[i] = 0;
}
// Loop through the enemy soldiers and add one to the count for that soldiers player's count
this.enemySoldiers.forEach(function(ent) {
enemyCount[ent.owner()]++;
if (enemyCount[ent.owner()] > maxCount) {
maxCount = enemyCount[ent.owner()];
}
});
return maxCount;
};
// Returns the strength of the largest enemy army
MilitaryAttackManager.prototype.measureEnemyStrength = function(gameState){
// Measure enemy strength
var isEnemy = gameState.playerData.isEnemy;
var enemyStrength = [];
var maxStrength = 0;
var self = this;
for ( var i = 1; i < isEnemy.length; i++) {
enemyStrength[i] = 0;
}
// Loop through the enemy soldiers and add the strength to that soldiers player's total strength
this.enemySoldiers.forEach(function(ent) {
enemyStrength[ent.owner()] += self.getUnitStrength(ent);
if (enemyStrength[ent.owner()] > maxStrength) {
maxStrength = enemyStrength[ent.owner()];
}
});
return maxStrength;
};
// Adds towers to the defenceBuilding queue
MilitaryAttackManager.prototype.buildDefences = function(gameState, queues){
if (gameState.countEntitiesAndQueuedWithType(gameState.applyCiv('structures/{civ}_scout_tower'))
+ queues.defenceBuilding.totalLength() < gameState.getBuildLimits()["ScoutTower"]) {
gameState.getOwnEntities().forEach(function(dropsiteEnt) {
if (dropsiteEnt.resourceDropsiteTypes() && dropsiteEnt.getMetadata("scoutTower") !== true){
var position = dropsiteEnt.position();
if (position){
queues.defenceBuilding.addItem(new BuildingConstructionPlan(gameState, 'structures/{civ}_scout_tower', position));
}
dropsiteEnt.setMetadata("scoutTower", true);
}
});
}
var numFortresses = 0;
for (var i in this.bFort){
numFortresses += gameState.countEntitiesAndQueuedWithType(gameState.applyCiv(this.bFort[i]));
}
if (numFortresses + queues.defenceBuilding.totalLength() < gameState.getBuildLimits()["Fortress"]) {
if (gameState.countEntitiesWithType(gameState.applyCiv("units/{civ}_support_female_citizen")) > gameState.ai.modules[0].targetNumWorkers * 0.5){
if (gameState.getTimeElapsed() > 350 * 1000 * numFortresses){
if (gameState.ai.pathsToMe && gameState.ai.pathsToMe.length > 0){
var position = gameState.ai.pathsToMe.shift();
// TODO: pick a fort randomly from the list.
queues.defenceBuilding.addItem(new BuildingConstructionPlan(gameState, this.bFort[0], position));
}else{
queues.defenceBuilding.addItem(new BuildingConstructionPlan(gameState, this.bFort[0]));
}
}
}
}
};
MilitaryAttackManager.prototype.update = function(gameState, queues, events) {
Engine.ProfileStart("military update");
this.gameState = gameState;
this.handleEvents(gameState, events);
// this.attackElephants(gameState);
this.registerSoldiers(gameState);
//this.defence(gameState);
this.buildDefences(gameState, queues);
this.defenceManager.update(gameState, events, this);
// Continually try training new units, in batches of 5
if (queues.citizenSoldier.length() < 6) {
var newUnit = this.findBestNewUnit(gameState, queues.citizenSoldier, this.uCitizenSoldier);
if (newUnit){
queues.citizenSoldier.addItem(new UnitTrainingPlan(gameState, newUnit, {
"role" : "soldier"
}, 5));
}
}
if (queues.advancedSoldier.length() < 2) {
var newUnit = this.findBestNewUnit(gameState, queues.advancedSoldier, this.uAdvanced);
if (newUnit){
queues.advancedSoldier.addItem(new UnitTrainingPlan(gameState, newUnit, {
"role" : "soldier"
}, 5));
}
}
if (queues.siege.length() < 4) {
var newUnit = this.findBestNewUnit(gameState, queues.siege, this.uSiege);
if (newUnit){
queues.siege.addItem(new UnitTrainingPlan(gameState, newUnit, {
"role" : "soldier"
}, 2));
}
}
// Build more military buildings
// TODO: make military building better
if (gameState.countEntitiesWithType(gameState.applyCiv("units/{civ}_support_female_citizen")) > 30) {
if (gameState.countEntitiesAndQueuedWithType(gameState.applyCiv(this.bModerate[0]))
+ queues.militaryBuilding.totalLength() < 1) {
queues.militaryBuilding.addItem(new BuildingConstructionPlan(gameState, this.bModerate[0]));
}
}
//build advanced military buildings
if (gameState.countEntitiesWithType(gameState.applyCiv("units/{civ}_support_female_citizen")) >
gameState.ai.modules[0].targetNumWorkers * 0.7){
if (queues.militaryBuilding.totalLength() === 0){
for (var i in this.bAdvanced){
if (gameState.countEntitiesAndQueuedWithType(gameState.applyCiv(this.bAdvanced[i])) < 1){
queues.militaryBuilding.addItem(new BuildingConstructionPlan(gameState, this.bAdvanced[i]));
}
}
}
}
// Look for attack plans which can be executed, only do this once every minute
for (var i = 0; i < this.availableAttacks.length; i++){
if (this.availableAttacks[i].canExecute(gameState, this)){
this.availableAttacks[i].execute(gameState, this);
this.currentAttacks.push(this.availableAttacks[i]);
debug("Attacking!");
}
this.availableAttacks.splice(i, 1, new this.attackManagers[i](gameState, this));
}
// Keep current attacks updated
for (i in this.currentAttacks){
this.currentAttacks[i].update(gameState, this, events);
}
// Set unassigned to be workers
for (var i in this.unassigned){
if (this.entity(i).hasClass("CitizenSoldier") && ! this.entity(i).hasClass("Cavalry")){
this.entity(i).setMetadata("role", "worker");
}
}
// Dynamically change priorities
var females = gameState.countEntitiesWithType(gameState.applyCiv("units/{civ}_support_female_citizen"));
var femalesTarget = gameState.ai.modules[0].targetNumWorkers;
var enemyStrength = this.measureEnemyStrength(gameState);
var availableStrength = this.measureAvailableStrength();
var additionalPriority = (enemyStrength - availableStrength) * 5;
additionalPriority = Math.min(Math.max(additionalPriority, -50), 220);
var advancedProportion = (availableStrength / 40) * (females/femalesTarget);
advancedProportion = Math.min(advancedProportion, 0.7);
gameState.ai.priorities.citizenSoldier = (1-advancedProportion) * (150 + additionalPriority) + 1;
gameState.ai.priorities.advancedSoldier = advancedProportion * (150 + additionalPriority) + 1;
if (females/femalesTarget > 0.7){
gameState.ai.priorities.defenceBuilding = 70;
}
Engine.ProfileStop();
};