Large qBot update.

Key changes are:
Support for Persians
Revamped defence system supoprting groups of attackers
Dynamic priorities based on enemy strength and number of workers
Better placement of towers and fortresses
Randomised raiding in early game

This was SVN commit r10755.
This commit is contained in:
Jonathan Waller 2011-12-17 21:59:27 +00:00
parent 36ef0a376d
commit c8243a50dc
11 changed files with 674 additions and 193 deletions

View File

@ -37,3 +37,15 @@ function ShallowClone(obj)
ret[k] = obj[k];
return ret;
}
// Picks a random element from an array
function PickRandom(list){
if (list.length === 0)
{
return undefined;
}
else
{
return list[Math.floor(Math.random()*list.length)];
}
}

View File

@ -1,4 +1,4 @@
var AttackMoveToCC = function(gameState, militaryManager){
function AttackMoveToCC(gameState, militaryManager){
this.minAttackSize = 20;
this.maxAttackSize = 60;
this.idList=[];
@ -39,7 +39,7 @@ AttackMoveToCC.prototype.execute = function(gameState, militaryManager){
// If there are no critical structures, attack anything else that's critical
if (targets.length == 0) {
targets = gameState.entities.filter(function(ent) {
return (gameState.isEntityEnemy(ent) && ent.hasClass("ConquestCritical") && ent.owner() !== 0);
return (gameState.isEntityEnemy(ent) && ent.hasClass("ConquestCritical") && ent.owner() !== 0 && ent.position());
});
}
// If there's nothing, attack anything else that's less critical
@ -49,7 +49,6 @@ AttackMoveToCC.prototype.execute = function(gameState, militaryManager){
if (targets.length == 0) {
targets = militaryManager.getEnemyBuildings(gameState,"Village");
}
// If we have a target, move to it
if (targets.length) {

View File

@ -0,0 +1,220 @@
function AttackMoveToLocation(gameState, militaryManager, minAttackSize, maxAttackSize, targetFinder){
this.minAttackSize = minAttackSize || 20;
this.maxAttackSize = maxAttackSize || 60;
this.idList=[];
this.previousTime = 0;
this.state = "unexecuted";
this.targetFinder = targetFinder || this.defaultTargetFinder;
this.healthRecord = [];
};
// Returns true if the attack can be executed at the current time
AttackMoveToLocation.prototype.canExecute = function(gameState, militaryManager){
var enemyStrength = militaryManager.measureEnemyStrength(gameState);
var enemyCount = militaryManager.measureEnemyCount(gameState);
// We require our army to be >= this strength
var targetStrength = enemyStrength * 1.5;
var availableCount = militaryManager.countAvailableUnits();
var availableStrength = militaryManager.measureAvailableStrength();
debug("Troops needed for attack: " + this.minAttackSize + " Have: " + availableCount);
debug("Troops strength for attack: " + targetStrength + " Have: " + availableStrength);
return ((availableStrength >= targetStrength && availableCount >= this.minAttackSize)
|| availableCount >= this.maxAttackSize);
};
// Default target finder aims for conquest critical targets
AttackMoveToLocation.prototype.defaultTargetFinder = function(gameState, militaryManager){
// Find the critical enemy buildings we could attack
var targets = militaryManager.getEnemyBuildings(gameState,"ConquestCritical");
// If there are no critical structures, attack anything else that's critical
if (targets.length == 0) {
targets = gameState.entities.filter(function(ent) {
return (gameState.isEntityEnemy(ent) && ent.hasClass("ConquestCritical") && ent.owner() !== 0 && ent.position());
});
}
// If there's nothing, attack anything else that's less critical
if (targets.length == 0) {
targets = militaryManager.getEnemyBuildings(gameState,"Town");
}
if (targets.length == 0) {
targets = militaryManager.getEnemyBuildings(gameState,"Village");
}
return targets;
};
// Executes the attack plan, after this is executed the update function will be run every turn
AttackMoveToLocation.prototype.execute = function(gameState, militaryManager){
var availableCount = militaryManager.countAvailableUnits();
this.idList = militaryManager.getAvailableUnits(availableCount);
var pending = EntityCollectionFromIds(gameState, this.idList);
var targets = this.targetFinder(gameState, militaryManager);
if (targets.length === 0){
targets = this.defaultTargetFinder(gameState, militaryManager);
}
// If we have a target, move to it
if (targets.length) {
// Add an attack role so the economic manager doesn't try and use them
pending.forEach(function(ent) {
ent.setMetadata("role", "attack");
});
var curPos = pending.getCentrePosition();
// pick a random target from the list
var rand = Math.floor((Math.random()*targets.length));
var target = targets.toEntityArray()[rand];
this.targetPos = target.position();
// Find possible distinct paths to the enemy
var pathFinder = new PathFinder(gameState);
var pathsToEnemy = pathFinder.getPaths(curPos, this.targetPos);
if (! pathsToEnemy){
pathsToEnemy = [this.targetPos];
}
var rand = Math.floor(Math.random() * pathsToEnemy.length);
this.path = pathsToEnemy[rand];
pending.move(this.path[0][0], this.path[0][1]);
} else if (targets.length == 0 ) {
gameState.ai.gameFinished = true;
return;
}
this.state = "walking";
};
// Runs every turn after the attack is executed
// This removes idle units from the attack
AttackMoveToLocation.prototype.update = function(gameState, militaryManager, events){
// keep the list of units in good order by pruning ids with no corresponding entities (i.e. dead units)
var removeList = [];
var totalHealth = 0;
for (var idKey in this.idList){
var id = this.idList[idKey];
var ent = militaryManager.entity(id);
if (ent === undefined){
removeList.push(id);
}else{
if (ent.hitpoints()){
totalHealth += ent.hitpoints();
}
}
}
for (var i in removeList){
this.idList.splice(this.idList.indexOf(removeList[i]),1);
}
var units = EntityCollectionFromIds(gameState, this.idList);
if (this.path.length === 0){
var idleCount = 0;
var self = this;
units.forEach(function(ent){
if (ent.isIdle()){
if (ent.position() && VectorDistance(ent.position(), self.targetPos) > 30){
ent.move(self.targetPos[0], self.targetPos[1]);
}else{
militaryManager.unassignUnit(ent.id());
}
}
});
return;
}
var deltaHealth = 0;
var deltaTime = 1;
var time = gameState.getTimeElapsed();
this.healthRecord.push([totalHealth, time]);
if (this.healthRecord.length > 1){
for (var i = this.healthRecord.length - 1; i >= 0; i--){
deltaHealth = totalHealth - this.healthRecord[i][0];
deltaTime = time - this.healthRecord[i][1];
if (this.healthRecord[i][1] < time - 5*1000){
break;
}
}
}
var numUnits = this.idList.length;
if (numUnits < 1) return;
var damageRate = -deltaHealth / deltaTime * 1000;
var centrePos = units.getCentrePosition();
if (! centrePos) return;
var idleCount = 0;
// Looks for idle units away from the formations centre
for (var idKey in this.idList){
var id = this.idList[idKey];
var ent = militaryManager.entity(id);
if (ent.isIdle()){
if (ent.position() && VectorDistance(ent.position(), centrePos) > 20){
var dist = VectorDistance(ent.position(), centrePos);
var vector = [centrePos[0] - ent.position()[0], centrePos[1] - ent.position()[1]];
vector[0] *= 10/dist;
vector[1] *= 10/dist;
ent.move(centrePos[0] + vector[0], centrePos[1] + vector[1]);
}else{
idleCount++;
}
}
}
if ((damageRate / Math.sqrt(numUnits)) > 2){
if (this.state === "walking"){
var sumAttackerPos = [0,0];
var numAttackers = 0;
for (var key in events){
var e = events[key];
//{type:"Attacked", msg:{attacker:736, target:1133, type:"Melee"}}
if (e.type === "Attacked" && e.msg){
if (this.idList.indexOf(e.msg.target) !== -1){
var attacker = militaryManager.entity(e.msg.attacker);
if (attacker && attacker.position()){
sumAttackerPos[0] += attacker.position()[0];
sumAttackerPos[1] += attacker.position()[1];
numAttackers += 1;
}
}
}
}
if (numAttackers > 0){
var avgAttackerPos = [sumAttackerPos[0]/numAttackers, sumAttackerPos[1]/numAttackers];
// Stop moving
units.move(centrePos[0], centrePos[1]);
this.state = "attacking";
}
}
}else{
if (this.state === "attacking"){
units.move(this.path[0][0], this.path[0][1]);
this.state = "walking";
}
}
if (this.state === "walking"){
if (VectorDistance(centrePos, this.path[0]) < 20 || idleCount/numUnits > 0.8){
this.path.shift();
if (this.path.length > 0){
units.move(this.path[0][0], this.path[0][1]);
}
}
}
this.previousTime = time;
this.previousHealth = totalHealth;
};

View File

@ -0,0 +1,278 @@
function Defence(){
this.AQUIRE_DIST = 220;
this.RELEASE_DIST = 250;
this.GROUP_RADIUS = 20; // units will be added to a group if they are within this radius
this.GROUP_BREAK_RADIUS = 40; // units will leave a group if they are outside of this radius
this.GROUP_MERGE_RADIUS = 10; // Two groups with centres this far apart will be merged
this.DEFENCE_RATIO = 2; // How many defenders we want per attacker. Need to balance fewer losses vs. lost economy
// These are objects with the keys being entity ids and values being the entity objects
// NOTE: It is assumed that all attackers have a valid position, the attackers list must be kept up to date so this
// property is maintained
this.attackers = {}; // Enemy soldiers which are attacking our base
this.defenders = {}; // Our soldiers currently being used for defence
// A list of groups, enemy soldiers are clumped together in groups.
this.groups = [];
}
Defence.prototype.update = function(gameState, events, militaryManager){
Engine.ProfileStart("Defence Manager");
var enemyTroops = militaryManager.getEnemySoldiers();
this.updateAttackers(gameState, events, enemyTroops);
this.updateGroups();
var unassignedDefenders = this.updateDefenders(gameState);
this.assignDefenders(gameState, militaryManager, unassignedDefenders);
Engine.ProfileStop();
};
Defence.prototype.assignDefenders = function(gameState, militaryManager, unassignedDefenders){
var numAttackers = Object.keys(this.attackers).length;
var numDefenders = Object.keys(this.defenders).length;
var numUnassignedDefenders = unassignedDefenders.length;
var numAssignedDefenders = numDefenders - numUnassignedDefenders;
// TODO: this is non optimal, we may have unevenly distributed defenders
// Unassign defenders which aren't needed
if (numAttackers * this.DEFENCE_RATIO <= numAssignedDefenders){
militaryManager.unassignUnits(unassignedDefenders);
var CCs = gameState.getOwnEntities().filter(Filters.byClass("CivCentre"));
for (var i in unassignedDefenders){
var pos = this.defenders[unassignedDefenders[i]].position();
// Move back to nearest CC
if (pos){
var nearestCCArray = CCs.filterNearest(pos, 1).toEntityArray();
if (nearestCCArray.length > 0){
var movePos = nearestCCArray[0].position();
this.defenders[unassignedDefenders[i]].move(movePos[0], movePos[1]);
}
}
delete this.defenders[unassignedDefenders[i]];
}
return;
}
// Check to see if we need to recruit more defenders
if (numAttackers * this.DEFENCE_RATIO > numDefenders){
var numNeeded = Math.ceil(numAttackers * this.DEFENCE_RATIO - numDefenders);
var numIdleAvailable = militaryManager.countAvailableUnits(Filters.isIdle());
if (numIdleAvailable > numNeeded){
var newUnits = militaryManager.getAvailableUnits(numNeeded, Filters.isIdle());
for (var i in newUnits){
var ent = gameState.getEntityById(newUnits[i]);
}
unassignedDefenders = unassignedDefenders.concat(newUnits);
}else{
var newUnits = militaryManager.getAvailableUnits(numNeeded);
for (var i in newUnits){
var ent = gameState.getEntityById(newUnits[i]);
ent.setMetadata("initialPosition", ent.position());
}
unassignedDefenders = unassignedDefenders.concat(newUnits);
}
}
// Now distribute the unassigned defenders among the attacking groups.
for (var i in unassignedDefenders){
var id = unassignedDefenders[i];
var ent = gameState.getEntityById(id);
if (!ent.position()){
debug("Defender with no position! (shouldn't happen)");
debug(ent);
continue;
}
var minDist = Math.min();
var closestGroup = undefined;
for (var j in this.groups){
var dist = VectorDistance(this.groups[j].position, ent.position());
if (dist < minDist && this.groups[j].members.length * this.DEFENCE_RATIO > this.groups[j].defenders.length){
minDist = dist;
closestGroup = this.groups[j];
}
}
if (closestGroup !== undefined){
var rand = Math.floor(Math.random()*closestGroup.members.length);
ent.attack(closestGroup.members[rand]);
this.defenders[id] = ent;
closestGroup.defenders.push(id);
}
}
};
Defence.prototype.updateDefenders = function(gameState){
var newDefenders = {};
var unassignedDefenders = [];
for (var i in this.groups){
this.removeDestroyed(gameState, this.groups[i].defenders);
for (j in this.groups[i].defenders){
var id = this.groups[i].defenders[j];
newDefenders[id] = this.defenders[id];
var ent = gameState.getEntityById(id);
// If the defender is idle then set it to attack another member of the group it is targetting
if (ent && ent.isIdle()){
var rand = Math.floor(Math.random()*this.groups[i].members.length);
ent.attack(this.groups[i].members[rand]);
}
}
}
for (var id in this.defenders){
if (!gameState.getEntityById(id)){
delete this.defenders[id];
} else if (!newDefenders[id]){
unassignedDefenders.push(id);
}
}
return unassignedDefenders;
};
// Returns an entity collection of key buildings which should be defended.
// Currently just returns civ centres
Defence.prototype.getKeyBuildings = function(gameState){
return gameState.getOwnEntities().filter(Filters.byClass("CivCentre"));
};
/*
* This function puts all attacking enemy troops into this.attackers, the list from the turn before is put into
* this.oldAttackers, also any new attackers have their id's listed in this.newAttackers.
*/
Defence.prototype.updateAttackers = function(gameState, events, enemyTroops){
var self = this;
var keyBuildings = this.getKeyBuildings(gameState);
this.newAttackers = [];
this.oldAttackers = this.attackers;
this.attackers = {};
enemyTroops.forEach(function(ent){
if (ent.position()){
var minDist = Math.min();
keyBuildings.forEach(function(building){
if (building.position() && VectorDistance(ent.position(), building.position()) < minDist){
minDist = VectorDistance(ent.position(), building.position());
}
});
if (self.oldAttackers[ent.id()]){
if (minDist < self.RELEASE_DIST){
self.attackers[ent.id()] = ent;
}
}else{
if (minDist < self.AQUIRE_DIST){
self.attackers[ent.id()] = ent;
self.newAttackers.push(ent.id());
}
}
}
});
};
Defence.prototype.removeDestroyed = function(gameState, entList){
for (var i = 0; i < entList.length; i++){
if (!gameState.getEntityById(entList[i])){
entList.splice(i, 1);
i--;
}
}
};
Defence.prototype.updateGroups = function(){
// clean up groups by removing members and removing empty groups
for (var i = 0; i < this.groups.length; i++){
var group = this.groups[i];
// remove members which are no longer attackers
for (var j = 0; j < group.members.length; j++){
if (!this.attackers[group.members[j]]){
group.members.splice(j, 1);
j--;
}
}
// recalculate centre of group
group.sumPosition = [0,0];
for (var j = 0; j < group.members.length; j++){
group.sumPosition[0] += this.attackers[group.members[j]].position()[0];
group.sumPosition[1] += this.attackers[group.members[j]].position()[1];
}
group.position[0] = group.sumPosition[0]/group.members.length;
group.position[1] = group.sumPosition[1]/group.members.length;
// remove members that are too far away
for (var j = 0; j < group.members.length; j++){
if ( VectorDistance(this.attackers[group.members[j]].position(), group.position) > this.GROUP_BREAK_RADIUS){
this.newAttackers.push(group.members[j]);
group.sumPosition[0] -= this.attackers[group.members[j]].position()[0];
group.sumPosition[1] -= this.attackers[group.members[j]].position()[1];
group.members.splice(j, 1);
j--;
}
}
if (group.members.length === 0){
this.groups.splice(i, 1);
i--;
}
group.position[0] = group.sumPosition[0]/group.members.length;
group.position[1] = group.sumPosition[1]/group.members.length;
}
// add ungrouped attackers to groups
for (var j in this.newAttackers){
var ent = this.attackers[this.newAttackers[j]];
var foundGroup = false;
for (var i in this.groups){
if (VectorDistance(ent.position(), this.groups[i].position) <= this.GROUP_RADIUS){
this.groups[i].members.push(ent.id());
this.groups[i].sumPosition[0] += ent.position()[0];
this.groups[i].sumPosition[1] += ent.position()[1];
this.groups[i].position[0] = this.groups[i].sumPosition[0]/this.groups[i].members.length;
this.groups[i].position[1] = this.groups[i].sumPosition[1]/this.groups[i].members.length;
foundGroup = true;
break;
}
}
if (!foundGroup){
this.groups.push({"members": [ent.id()],
"position": [ent.position()[0], ent.position()[1]],
"sumPosition": [ent.position()[0], ent.position()[1]],
"defenders": []});
}
}
// merge groups which are close together
for (var i = 0; i < this.groups.length; i++){
for (var j = 0; j < this.groups.length; j++){
if (this.groups[i].members.length < this.groups[j].members.length){
if (VectorDistance(this.groups[i].position, this.groups[j].position) < this.GROUP_MERGE_RADIUS){
this.groups[j].members = this.groups[i].members.concat(this.groups[j].members);
this.groups[j].defenders = this.groups[i].defenders.concat(this.groups[j].defenders);
this.groups[j].sumPosition[0] += this.groups[i].sumPosition[0];
this.groups[j].sumPosition[1] += this.groups[i].sumPosition[1];
this.groups[j].position[0] = this.groups[j].sumPosition[0]/this.groups[j].members.length;
this.groups[j].position[1] = this.groups[j].sumPosition[1]/this.groups[j].members.length;
this.groups.splice(i, 1);
i--;
break;
}
}
}
}
};

View File

@ -26,3 +26,9 @@ Entity.prototype.unloadAll = function() {
Engine.PostCommand({"type": "unload-all", "garrisonHolder": this.id()});
return this;
};
Entity.prototype.attack = function(unitId)
{
Engine.PostCommand({"type": "attack", "entities": [this.id()], "target": unitId, "queued": false});
return this;
};

View File

@ -7,7 +7,7 @@ EntityCollection.prototype.attack = function(unit)
unitId = unit;
}
Engine.PostCommand({"type": "walk", "entities": this.toIdArray(), "target": unitId, "queued": false});
Engine.PostCommand({"type": "attack", "entities": this.toIdArray(), "target": unitId, "queued": false});
return this;
};

View File

@ -47,5 +47,11 @@ var Filters = {
return function(ent){
return Filters.byClassesOr(["CitizenSoldier", "Super"])(ent);
};
},
isIdle: function(){
return function(ent){
return ent.isIdle();
};
}
};

View File

@ -114,9 +114,9 @@ GameState.prototype.getEntityById = function(id){
if (this.entities._entities[id]) {
return new Entity(this.ai, this.entities._entities[id]);
}else{
debug("Entity " + id + " requested does not exist");
//debug("Entity " + id + " requested does not exist");
}
return false;
return undefined;
};
GameState.prototype.getOwnEntitiesWithRole = Memoize('getOwnEntitiesWithRole', function(role) {

View File

@ -10,18 +10,23 @@ var MilitaryAttackManager = function() {
this.targetSquadSize = 10;
this.targetScoutTowers = 10;
// these use the structure soldiers[unitId] = true|false to register the
// units
// these use the structure soldiers[unitId] = true|false to register the units
this.soldiers = {};
this.assigned = {};
this.unassigned = {};
this.garrisoned = {};
this.enemyAttackers = {};
this.attackManagers = [AttackMoveToCC];
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();
this.defineUnitsAndBuildings();
};
@ -33,6 +38,7 @@ MilitaryAttackManager.prototype.init = function(gameState) {
this.uSiege = this.uCivSiege[civ];
this.bAdvanced = this.bCivAdvanced[civ];
this.bFort = this.bCivFort[civ];
}
for (var i in this.uCitizenSoldier){
@ -44,12 +50,19 @@ MilitaryAttackManager.prototype.init = function(gameState) {
for (var i in this.uSiege){
this.uSiege[i] = gameState.applyCiv(this.uSiege[i]);
}
for (var i in this.attackManagers){
this.availableAttacks[i] = new this.attackManagers[i](gameState, this);
for (var i in this.bFort){
this.bFort[i] = gameState.applyCiv(this.bFort[i]);
}
var filter = Filters.and(Filters.isEnemy(), Filters.byClassesOr(["CitizenSoldier", "Super"]));
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, 10, 10, this.getEconomicTargets);
}
var filter = Filters.and(Filters.isEnemy(), Filters.byClassesOr(["CitizenSoldier", "Super", "Siege"]));
this.enemySoldiers = new EntityCollection(gameState.ai, gameState.entities._entities, filter, gameState);
};
@ -75,6 +88,9 @@ MilitaryAttackManager.prototype.defineUnitsAndBuildings = function(){
this.uCivAdvanced.iber = ["units/iber_cavalry_spearman_b", "units/iber_champion_cavalry", "units/iber_champion_infantry" ];
this.uCivSiege.iber = ["units/iber_mechanical_siege_ram"];
this.uCivCitizenSoldier.pers = [ "units/pers_infantry_spearman_b", "units/pers_infantry_archer_b", "units/pers_infantry_javelinist_b" ];
this.uCivAdvanced.pers = ["units/pers_cavalry_javelinist_b", "units/pers_champion_infantry", "units/pers_champion_cavalry", "units/pers_cavalry_spearman_b", "units/pers_cavalry_swordsman_b", "units/pers_cavalry_javelinist_b", "units/pers_cavalry_archer_b", "pers_kardakes_hoplite", "units/pers_kardakes_skirmisher", "units/pers_war_elephant" ];
this.uCivSiege.pers = ["units/pers_mechanical_siege_ram"];
//defaults
this.uCitizenSoldier = ["units/{civ}_infantry_spearman_b", "units/{civ}_infantry_slinger_b", "units/{civ}_infantry_swordsman_b", "units/{civ}_infantry_javelinist_b", "units/{civ}_infantry_archer_b" ];
this.uAdvanced = ["units/{civ}_cavalry_spearman_b", "units/{civ}_cavalry_javelinist_b", "units/{civ}_champion_cavalry", "units/{civ}_champion_infantry"];
@ -88,6 +104,14 @@ MilitaryAttackManager.prototype.defineUnitsAndBuildings = function(){
this.bCivAdvanced.cart = [ "structures/{civ}_fortress", "structures/{civ}_embassy_celtic", "structures/{civ}_embassy_iberian", "structures/{civ}_embassy_italiote" ];
this.bCivAdvanced.celt = [ "structures/{civ}_kennel", "structures/{civ}_fortress_b", "structures/{civ}_fortress_g" ];
this.bCivAdvanced.iber = [ "structures/{civ}_fortress" ];
this.bCivAdvanced.pers = [ "structures/{civ}_fortress", "structures/{civ}_stables", "structures/{civ}_apadana" ];
this.bCivFort = {};
this.bCivFort.hele = [ "structures/{civ}_fortress" ];
this.bCivFort.cart = [ "structures/{civ}_fortress" ];
this.bCivFort.celt = [ "structures/{civ}_fortress_b", "structures/{civ}_fortress_g" ];
this.bCivFort.iber = [ "structures/{civ}_fortress" ];
this.bCivFort.pers = [ "structures/{civ}_fortress" ];
};
/**
@ -135,14 +159,6 @@ MilitaryAttackManager.prototype.findBestNewUnit = function(gameState, queue, sol
return types[0][0];
};
MilitaryAttackManager.prototype.attackElephants = function(gameState) {
var eles = gameState.entities.filter(function(ent) {
return (ent.templateName().indexOf("elephant") > -1);
});
warn(uneval(eles._entities));
};
MilitaryAttackManager.prototype.registerSoldiers = function(gameState) {
var soldiers = gameState.getOwnEntitiesWithRole("soldier");
var self = this;
@ -154,126 +170,6 @@ MilitaryAttackManager.prototype.registerSoldiers = function(gameState) {
});
};
MilitaryAttackManager.prototype.defence = function(gameState) {
var ents = gameState.entities._entities;
var myCivCentres = gameState.getOwnEntities().filter(function(ent) {
return ent.hasClass("CivCentre");
});
if (myCivCentres.length === 0)
return;
var defenceRange = 200; // just beyond town centres territory influence
var self = this;
var newEnemyAttackers = {};
myCivCentres.forEach(function(ent) {
var pos = ent.position();
self.getEnemySoldiers(gameState).forEach(function(ent) {
if (gameState.playerData.isEnemy[ent.owner()]
&& (ent.hasClass("CitizenSoldier") || ent.hasClass("Super"))
&& ent.position()) {
var dist = VectorDistance(ent.position(), pos);
if (dist < defenceRange) {
newEnemyAttackers[ent.id()] = true;
}
}
});
});
for (var id in this.enemyAttackers){
if (!newEnemyAttackers[id]){
this.unassignDefenders(gameState, id);
}
}
this.enemyAttackers = newEnemyAttackers;
var enemyAttackStrength = 0;
var availableStrength = this.measureAvailableStrength();
var garrisonedStrength = 0;
for (var i in this.garrisoned){
if (this.entity(i) !== undefined){
if (Filters.isSoldier()(this.entity(i))){
garrisonedStrength += this.getUnitStrength(this.entity(i));
}
}
}
for (var id in this.enemyAttackers) {
var ent = new Entity(gameState.ai, ents[id]);
enemyAttackStrength+= this.getUnitStrength(ent);
}
if(2 * enemyAttackStrength < availableStrength + garrisonedStrength) {
this.ungarrisonAll(gameState);
return;
} else {
this.garrisonCitizens(gameState);
}
if(enemyAttackStrength > availableStrength + garrisonedStrength) {
this.garrisonSoldiers(gameState);
}
for (id in this.enemyAttackers) {
if(!this.assignDefenders(gameState,id)) {
break;
}
}
};
MilitaryAttackManager.prototype.assignDefenders = function(gameState,target) {
var defendersPerAttacker = 3;
var ent = new Entity(gameState.ai, gameState.entities._entities[target]);
if (ent.getMetadata("attackers") === undefined || ent.getMetadata("attackers").length < defendersPerAttacker) {
var tasked = this.getAvailableUnits(3);
if (tasked.length > 0) {
Engine.PostCommand({
"type" : "attack",
"entities" : tasked,
"target" : ent.id(),
"queued" : false
});
ent.setMetadata("attackers", tasked);
for (var i in tasked) {
this.entity(tasked[i]).setMetadata("attacking", id);
}
} else {
return false;
}
}
return true;
};
MilitaryAttackManager.prototype.unassignDefenders = function(gameState, target){
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();
}
var ent = this.entity(target);
if (ent && ent.getMetadata() && ent.getMetadata().attackers){
for (var i in ent.metadata.attackers){
var attacker = this.entity(ent.getMetadata().attackers[i]);
if (attacker){
attacker.deleteMetadata('attacking');
if (pos){
attacker.move(pos[0], pos[1]);
}
this.unassignUnit(attacker.id());
}
}
ent.deleteMetadata('attackers');
}
};
// Ungarrisons all units
MilitaryAttackManager.prototype.ungarrisonAll = function(gameState) {
debug("ungarrison units");
@ -352,7 +248,7 @@ MilitaryAttackManager.prototype.garrisonUnit = function(gameState,id) {
// 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);
return (gameState.isEntityEnemy(ent) && ent.hasClass("Structure") && ent.hasClass(cls) && ent.owner() !== 0 && ent.position());
});
return targets;
};
@ -366,18 +262,26 @@ MilitaryAttackManager.prototype.getGarrisonBuildings = function(gameState) {
};
// return n available units and makes these units unavailable
MilitaryAttackManager.prototype.getAvailableUnits = function(n) {
MilitaryAttackManager.prototype.getAvailableUnits = function(n, filter) {
var ret = [];
var count = 0;
for (var i in this.unassigned) {
ret.push(+i);
delete this.unassigned[i];
this.assigned[i] = true;
this.entity(i).setMetadata("role", "soldier");
this.entity(i).setMetadata("subrole", "unavailable");
count++;
if (count >= n) {
break;
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;
@ -392,22 +296,36 @@ MilitaryAttackManager.prototype.unassignUnit = function(unit){
// 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[unit[i]] = true;
this.assigned[unit[i]] = false;
this.unassigned[units[i]] = true;
this.assigned[units[i]] = false;
}
};
MilitaryAttackManager.prototype.countAvailableUnits = function(){
MilitaryAttackManager.prototype.countAvailableUnits = function(filter){
var count = 0;
for (var i in this.unassigned){
if (this.unassigned[i]){
count += 1;
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];
@ -416,24 +334,6 @@ MilitaryAttackManager.prototype.handleEvents = function(gameState, events) {
delete this.unassigned[id];
delete this.assigned[id];
delete this.soldiers[id];
var metadata = e.msg.metadata[gameState.ai._player];
if (metadata && metadata.attacking){
var attacking = this.entity(metadata.attacking);
if (attacking && attacking.getMetadata('attackers')){
var attackers = attacking.getMetadata('attackers');
attackers.splice(attackers.indexOf(metadata.attacking), 1);
attacking.setMetadata('attackers', attackers);
}
}
if (metadata && metadata.attackers){
for (var i in metadata.attackers){
var attacker = this.entity(metadata.attackers[i]);
if (attacker && attacker.getMetadata('attacking')){
attacker.deleteMetadata('attacking');
this.unassignUnit(attacker.id());
}
}
}
}
}
};
@ -444,7 +344,7 @@ 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");
//debug("Entity " + id + " requested does not exist");
}
return undefined;
};
@ -510,14 +410,14 @@ MilitaryAttackManager.prototype.getUnitStrength = function(ent){
MilitaryAttackManager.prototype.measureAvailableStrength = function(){
var strength = 0.0;
for (var i in this.unassigned){
if (this.unassigned[i]){
if (this.unassigned[i] && this.entity(i)){
strength += this.getUnitStrength(this.entity(i));
}
}
return strength;
};
MilitaryAttackManager.prototype.getEnemySoldiers = function(gameState){
MilitaryAttackManager.prototype.getEnemySoldiers = function(){
return this.enemySoldiers;
};
@ -571,7 +471,35 @@ MilitaryAttackManager.prototype.measureEnemyStrength = function(gameState){
MilitaryAttackManager.prototype.buildDefences = function(gameState, queues){
if (gameState.countEntitiesAndQueuedWithType(gameState.applyCiv('structures/{civ}_scout_tower'))
+ queues.defenceBuilding.totalLength() <= gameState.getBuildLimits()["ScoutTower"]) {
queues.defenceBuilding.addItem(new BuildingConstructionPlan(gameState, 'structures/{civ}_scout_tower'));
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() > 200 * 1000 * numFortresses){
if (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]));
}
}
}
}
};
@ -584,9 +512,11 @@ MilitaryAttackManager.prototype.update = function(gameState, queues, events) {
// this.attackElephants(gameState);
this.registerSoldiers(gameState);
this.defence(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);
@ -623,7 +553,7 @@ MilitaryAttackManager.prototype.update = function(gameState, queues, events) {
}
//build advanced military buildings
if (gameState.countEntitiesWithType(gameState.applyCiv("units/{civ}_support_female_citizen")) >
gameState.ai.modules[0].targetNumWorkers * 0.8){
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){
@ -633,12 +563,24 @@ MilitaryAttackManager.prototype.update = function(gameState, queues, events) {
}
}
// Look for attack plans which can be executed
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]);
this.availableAttacks.splice(i, 1, new this.attackManagers[i](gameState, this));
// Look for attack plans which can be executed, only do this once every minute
if (gameState.getTimeElapsed() - 60*1000 > this.lastAttackTime){
this.lastAttackTime = gameState.getTimeElapsed();
for (var i = 0; i < this.availableAttacks.length; i++){
if (this.availableAttacks[i].canExecute(gameState, this)){
// Make it so raids happen a bit randomly
if (this.attackCount > 4 || Math.random() < 0.25){
this.availableAttacks[i].execute(gameState, this);
this.currentAttacks.push(this.availableAttacks[i]);
}
if (this.attackCount < 4){
this.availableAttacks.splice(i, 1, new this.attackManagers[i](gameState, this, 10, 10, this.getEconomicTargets));
}else{
this.availableAttacks.splice(i, 1, new this.attackManagers[i](gameState, this, 20, 40 + 10 * Math.max(this.attackCount, 8)));
}
this.attackCount++;
}
}
}
// Keep current attacks updated
@ -652,6 +594,24 @@ MilitaryAttackManager.prototype.update = function(gameState, queues, events) {
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();
};

View File

@ -22,7 +22,7 @@ function QBotAI(settings) {
this.productionQueues = [];
var priorities = {
this.priorities = {
house : 500,
citizenSoldier : 100,
villager : 100,
@ -30,11 +30,11 @@ function QBotAI(settings) {
field: 4,
advancedSoldier : 30,
siege : 10,
militaryBuilding : 30,
defenceBuilding: 5,
militaryBuilding : 50,
defenceBuilding: 17,
civilCentre: 1000
};
this.queueManager = new QueueManager(this.queues, priorities);
this.queueManager = new QueueManager(this.queues, this.priorities);
this.firstTime = true;
@ -121,7 +121,7 @@ QBotAI.prototype.OnUpdate = function() {
this.turn++;
};
var debugOn = false;
var debugOn = true;
function debug(output){
if (debugOn){

View File

@ -169,7 +169,7 @@ PathFinder.prototype.walkGradient = function(start, mode){
var blockPoint = undefined;
var blockPlacementRadius = 45;
var blockRadius = 30;
var blockRadius = 23;
var count = 0;
var cur = start;
@ -208,7 +208,7 @@ PathFinder.prototype.walkGradient = function(start, mode){
this.addInfluence(blockPoint[0], blockPoint[1], blockRadius, -10000, 'constant');
if (mode === 'entryPoints'){
// returns the point where the path enters the blockPlacementRadius
return blockPoint;
return [blockPoint[0] * this.cellSize, blockPoint[1] * this.cellSize];
}else{
// return a path of points 20 squares apart on the route
return path;