diff --git a/binaries/data/mods/public/simulation/ai/common-api/utils.js b/binaries/data/mods/public/simulation/ai/common-api/utils.js index 3d5bf8747b..5567db5a34 100644 --- a/binaries/data/mods/public/simulation/ai/common-api/utils.js +++ b/binaries/data/mods/public/simulation/ai/common-api/utils.js @@ -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)]; + } +} diff --git a/binaries/data/mods/public/simulation/ai/qbot/attackMoveToCC.js b/binaries/data/mods/public/simulation/ai/qbot/attackMoveToCC.js index 94440c2b10..aad20894ff 100644 --- a/binaries/data/mods/public/simulation/ai/qbot/attackMoveToCC.js +++ b/binaries/data/mods/public/simulation/ai/qbot/attackMoveToCC.js @@ -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) { diff --git a/binaries/data/mods/public/simulation/ai/qbot/attackMoveToLocation.js b/binaries/data/mods/public/simulation/ai/qbot/attackMoveToLocation.js new file mode 100644 index 0000000000..082379b4b0 --- /dev/null +++ b/binaries/data/mods/public/simulation/ai/qbot/attackMoveToLocation.js @@ -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; +}; + diff --git a/binaries/data/mods/public/simulation/ai/qbot/defence.js b/binaries/data/mods/public/simulation/ai/qbot/defence.js new file mode 100644 index 0000000000..1f583d1189 --- /dev/null +++ b/binaries/data/mods/public/simulation/ai/qbot/defence.js @@ -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; + } + } + } + } +}; diff --git a/binaries/data/mods/public/simulation/ai/qbot/entity-extend.js b/binaries/data/mods/public/simulation/ai/qbot/entity-extend.js index d711efed9f..c4a64cc682 100644 --- a/binaries/data/mods/public/simulation/ai/qbot/entity-extend.js +++ b/binaries/data/mods/public/simulation/ai/qbot/entity-extend.js @@ -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; +}; \ No newline at end of file diff --git a/binaries/data/mods/public/simulation/ai/qbot/entitycollection-extend.js b/binaries/data/mods/public/simulation/ai/qbot/entitycollection-extend.js index 0e4a29b7cb..44760a0920 100644 --- a/binaries/data/mods/public/simulation/ai/qbot/entitycollection-extend.js +++ b/binaries/data/mods/public/simulation/ai/qbot/entitycollection-extend.js @@ -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; }; diff --git a/binaries/data/mods/public/simulation/ai/qbot/filters.js b/binaries/data/mods/public/simulation/ai/qbot/filters.js index 7ea34cca13..df08f7c064 100644 --- a/binaries/data/mods/public/simulation/ai/qbot/filters.js +++ b/binaries/data/mods/public/simulation/ai/qbot/filters.js @@ -47,5 +47,11 @@ var Filters = { return function(ent){ return Filters.byClassesOr(["CitizenSoldier", "Super"])(ent); }; + }, + + isIdle: function(){ + return function(ent){ + return ent.isIdle(); + }; } }; diff --git a/binaries/data/mods/public/simulation/ai/qbot/gamestate.js b/binaries/data/mods/public/simulation/ai/qbot/gamestate.js index 768f3fc66a..f871d8b27f 100644 --- a/binaries/data/mods/public/simulation/ai/qbot/gamestate.js +++ b/binaries/data/mods/public/simulation/ai/qbot/gamestate.js @@ -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) { diff --git a/binaries/data/mods/public/simulation/ai/qbot/military.js b/binaries/data/mods/public/simulation/ai/qbot/military.js index aacabdc98e..6bbbeb2653 100755 --- a/binaries/data/mods/public/simulation/ai/qbot/military.js +++ b/binaries/data/mods/public/simulation/ai/qbot/military.js @@ -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(); }; diff --git a/binaries/data/mods/public/simulation/ai/qbot/qbot.js b/binaries/data/mods/public/simulation/ai/qbot/qbot.js index 84d1c1378e..8f253e0f19 100644 --- a/binaries/data/mods/public/simulation/ai/qbot/qbot.js +++ b/binaries/data/mods/public/simulation/ai/qbot/qbot.js @@ -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){ diff --git a/binaries/data/mods/public/simulation/ai/qbot/terrain-analysis.js b/binaries/data/mods/public/simulation/ai/qbot/terrain-analysis.js index ebb934043e..e526c3b82a 100644 --- a/binaries/data/mods/public/simulation/ai/qbot/terrain-analysis.js +++ b/binaries/data/mods/public/simulation/ai/qbot/terrain-analysis.js @@ -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;