From b146f53d7bc5488c0c86a220cd4090d0e7318c7a Mon Sep 17 00:00:00 2001 From: quantumstate Date: Fri, 2 Dec 2011 12:44:31 +0000 Subject: [PATCH] This was SVN commit r10654. --- .../mods/public/simulation/ai/qbot/_init.js | 1 + .../simulation/ai/qbot/attackMoveToCC.js | 206 ++++++ .../mods/public/simulation/ai/qbot/data.json | 5 + .../mods/public/simulation/ai/qbot/economy.js | 482 +++++++++++++ .../simulation/ai/qbot/entity-extend.js | 28 + .../ai/qbot/entitycollection-extend.js | 103 +++ .../mods/public/simulation/ai/qbot/filters.js | 51 ++ .../public/simulation/ai/qbot/gamestate.js | 288 ++++++++ .../mods/public/simulation/ai/qbot/housing.js | 29 + .../simulation/ai/qbot/license_gpl-2.0.txt | 339 +++++++++ .../public/simulation/ai/qbot/map-module.js | 257 +++++++ .../public/simulation/ai/qbot/military.js | 657 ++++++++++++++++++ .../simulation/ai/qbot/plan-building.js | 126 ++++ .../simulation/ai/qbot/plan-training.js | 56 ++ .../mods/public/simulation/ai/qbot/qbot.js | 143 ++++ .../simulation/ai/qbot/queue-manager.js | 293 ++++++++ .../mods/public/simulation/ai/qbot/queue.js | 114 +++ .../mods/public/simulation/ai/qbot/readme.txt | 6 + .../public/simulation/ai/qbot/resources.js | 66 ++ .../simulation/ai/qbot/terrain-analysis.js | 302 ++++++++ .../public/simulation/ai/qbot/walkToCC.js | 85 +++ 21 files changed, 3637 insertions(+) create mode 100644 binaries/data/mods/public/simulation/ai/qbot/_init.js create mode 100644 binaries/data/mods/public/simulation/ai/qbot/attackMoveToCC.js create mode 100644 binaries/data/mods/public/simulation/ai/qbot/data.json create mode 100644 binaries/data/mods/public/simulation/ai/qbot/economy.js create mode 100644 binaries/data/mods/public/simulation/ai/qbot/entity-extend.js create mode 100644 binaries/data/mods/public/simulation/ai/qbot/entitycollection-extend.js create mode 100644 binaries/data/mods/public/simulation/ai/qbot/filters.js create mode 100644 binaries/data/mods/public/simulation/ai/qbot/gamestate.js create mode 100644 binaries/data/mods/public/simulation/ai/qbot/housing.js create mode 100644 binaries/data/mods/public/simulation/ai/qbot/license_gpl-2.0.txt create mode 100644 binaries/data/mods/public/simulation/ai/qbot/map-module.js create mode 100755 binaries/data/mods/public/simulation/ai/qbot/military.js create mode 100644 binaries/data/mods/public/simulation/ai/qbot/plan-building.js create mode 100644 binaries/data/mods/public/simulation/ai/qbot/plan-training.js create mode 100644 binaries/data/mods/public/simulation/ai/qbot/qbot.js create mode 100644 binaries/data/mods/public/simulation/ai/qbot/queue-manager.js create mode 100644 binaries/data/mods/public/simulation/ai/qbot/queue.js create mode 100644 binaries/data/mods/public/simulation/ai/qbot/readme.txt create mode 100644 binaries/data/mods/public/simulation/ai/qbot/resources.js create mode 100644 binaries/data/mods/public/simulation/ai/qbot/terrain-analysis.js create mode 100644 binaries/data/mods/public/simulation/ai/qbot/walkToCC.js diff --git a/binaries/data/mods/public/simulation/ai/qbot/_init.js b/binaries/data/mods/public/simulation/ai/qbot/_init.js new file mode 100644 index 0000000000..46c4996e55 --- /dev/null +++ b/binaries/data/mods/public/simulation/ai/qbot/_init.js @@ -0,0 +1 @@ +Engine.IncludeModule("common-api"); \ No newline at end of file diff --git a/binaries/data/mods/public/simulation/ai/qbot/attackMoveToCC.js b/binaries/data/mods/public/simulation/ai/qbot/attackMoveToCC.js new file mode 100644 index 0000000000..d466ce5e62 --- /dev/null +++ b/binaries/data/mods/public/simulation/ai/qbot/attackMoveToCC.js @@ -0,0 +1,206 @@ +var AttackMoveToCC = function(gameState, militaryManager){ + this.minAttackSize = 20; + this.maxAttackSize = 60; + this.idList=[]; + + this.previousTime = 0; + this.state = "unexecuted"; + + this.healthRecord = []; +}; + +// Returns true if the attack can be executed at the current time +AttackMoveToCC.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); +}; + +// Executes the attack plan, after this is executed the update function will be run every turn +AttackMoveToCC.prototype.execute = function(gameState, militaryManager){ + var availableCount = militaryManager.countAvailableUnits(); + this.idList = militaryManager.getAvailableUnits(availableCount); + + var pending = EntityCollectionFromIds(gameState, this.idList); + + // 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); + }); + } + // 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"); + } + + + // 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(); + + var target = targets.toEntityArray()[0]; + 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; + } + + this.state = "walking"; +}; + +// Runs every turn after the attack is executed +// This removes idle units from the attack +AttackMoveToCC.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(); + + + 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/data.json b/binaries/data/mods/public/simulation/ai/qbot/data.json new file mode 100644 index 0000000000..8f5dc4c21b --- /dev/null +++ b/binaries/data/mods/public/simulation/ai/qbot/data.json @@ -0,0 +1,5 @@ +{ + "name": "qBot", + "description": "Quantumstate's improved version of the Test Bot", + "constructor": "QBotAI" +} diff --git a/binaries/data/mods/public/simulation/ai/qbot/economy.js b/binaries/data/mods/public/simulation/ai/qbot/economy.js new file mode 100644 index 0000000000..641ca02338 --- /dev/null +++ b/binaries/data/mods/public/simulation/ai/qbot/economy.js @@ -0,0 +1,482 @@ +var EconomyManager = function() { + this.targetNumBuilders = 5; // number of workers we want building stuff + this.targetNumFields = 5; + + this.resourceMaps = {}; // Contains maps showing the density of wood, stone and metal + + this.setCount = 0; //stops villagers being reassigned to other resources too frequently, count a set number of + //turns before trying to reassign them. + + this.dropsiteNumbers = {wood: 2, stone: 1, metal: 1}; +}; +// More initialisation for stuff that needs the gameState +EconomyManager.prototype.init = function(gameState){ + this.targetNumWorkers = Math.floor(gameState.getPopulationMax()/3); +}; + +EconomyManager.prototype.trainMoreWorkers = function(gameState, queues) { + // Count the workers in the world and in progress + var numWorkers = gameState.countEntitiesAndQueuedWithType(gameState.applyCiv("units/{civ}_support_female_citizen")); + numWorkers += queues.villager.countTotalQueuedUnits(); + + // If we have too few, train more + if (numWorkers < this.targetNumWorkers) { + for ( var i = 0; i < this.targetNumWorkers - numWorkers; i++) { + queues.villager.addItem(new UnitTrainingPlan(gameState, "units/{civ}_support_female_citizen", { + "role" : "worker" + })); + } + } +}; + +// Pick the resource which most needs another worker +EconomyManager.prototype.pickMostNeededResources = function(gameState) { + + var self = this; + + // Find what resource type we're most in need of + this.gatherWeights = gameState.ai.queueManager.futureNeeds(gameState); + + var numGatherers = {}; + for ( var type in this.gatherWeights) + numGatherers[type] = 0; + + gameState.getOwnEntitiesWithRole("worker").forEach(function(ent) { + if (ent.getMetadata("subrole") === "gatherer") + numGatherers[ent.getMetadata("gather-type")] += 1; + }); + + var types = Object.keys(this.gatherWeights); + types.sort(function(a, b) { + // Prefer fewer gatherers (divided by weight) + var va = numGatherers[a] / (self.gatherWeights[a]+1); + var vb = numGatherers[b] / (self.gatherWeights[b]+1); + return va-vb; + }); + + return types; +}; + +EconomyManager.prototype.reassignRolelessUnits = function(gameState) { + //TODO: Move this out of the economic section + var roleless = gameState.getOwnEntitiesWithRole(undefined); + + roleless.forEach(function(ent) { + if (ent.hasClass("Worker")){ + ent.setMetadata("role", "worker"); + }else if(ent.hasClass("CitizenSoldier") || ent.hasClass("Super")){ + ent.setMetadata("role", "soldier"); + }else{ + ent.setMetadata("role", "unknown"); + } + }); +}; + +// If the numbers of workers on the resources is unbalanced then set some of workers to idle so +// they can be reassigned by reassignIdleWorkers. +EconomyManager.prototype.setWorkersIdleByPriority = function(gameState){ + this.gatherWeights = gameState.ai.queueManager.futureNeeds(gameState); + + var numGatherers = {}; + var totalGatherers = 0; + var totalWeight = 0; + for ( var type in this.gatherWeights){ + numGatherers[type] = 0; + totalWeight += this.gatherWeights[type]; + } + + gameState.getOwnEntitiesWithRole("worker").forEach(function(ent) { + if (ent.getMetadata("subrole") === "gatherer"){ + numGatherers[ent.getMetadata("gather-type")] += 1; + totalGatherers += 1; + } + }); + + for ( var type in this.gatherWeights){ + var allocation = Math.floor(totalGatherers * (this.gatherWeights[type]/totalWeight)); + if (allocation < numGatherers[type]){ + var numToTake = numGatherers[type] - allocation; + gameState.getOwnEntitiesWithRole("worker").forEach(function(ent) { + if (ent.getMetadata("subrole") === "gatherer" && ent.getMetadata("gather-type") === type && numToTake > 0){ + ent.setMetadata("subrole", "idle"); + numToTake -= 1; + } + }); + } + } +}; + +EconomyManager.prototype.reassignIdleWorkers = function(gameState) { + + var self = this; + + // Search for idle workers, and tell them to gather resources based on demand + + var idleWorkers = gameState.getOwnEntitiesWithRole("worker").filter(function(ent) { + return (ent.isIdle() || ent.getMetadata("subrole") === "idle"); + }); + + if (idleWorkers.length) { + var resourceSupplies = gameState.findResourceSupplies(); + + idleWorkers.forEach(function(ent) { + // Check that the worker isn't garrisoned + if (ent.position() === undefined){ + return; + } + + var types = self.pickMostNeededResources(gameState); + for ( var typeKey in types) { + var type = types[typeKey]; + // Make sure there are actually some resources of that type + if (!resourceSupplies[type]) + continue; + + // TODO: we should care about gather rates of workers + + // Find the nearest dropsite for this resource from the worker + var nearestDropsite = undefined; + var minDropsiteDist = Math.min(); // set to infinity initially + gameState.getOwnEntities().forEach(function(dropsiteEnt) { + if (dropsiteEnt.resourceDropsiteTypes() && dropsiteEnt.resourceDropsiteTypes().indexOf(type) !== -1){ + if (dropsiteEnt.position() && dropsiteEnt.getMetadata("resourceQuantity_" + type) > 0){ + var dist = VectorDistance(ent.position(), dropsiteEnt.position()); + if (dist < minDropsiteDist){ + nearestDropsite = dropsiteEnt; + minDropsiteDist = dist; + } + } + } + }); + + var workerPosition = ent.position(); + var supplies = []; + resourceSupplies[type].forEach(function(supply) { + // Skip targets that are too hard to hunt + if (supply.entity.isUnhuntable()){ + return; + } + + // And don't go for the bloody fish! + if (supply.entity.hasClass("SeaCreature")){ + return; + } + + // Check we can actually reach the resource + if (!gameState.ai.accessibility.isAccessible(supply.position)){ + return; + } + + // measure the distance to the resource + var dist = VectorDistance(supply.position, workerPosition); + // Add on a factor for the nearest dropsite if one exists + if (nearestDropsite){ + dist += 5 * VectorDistance(supply.position, nearestDropsite.position()); + } + + // Skip targets that are far too far away (e.g. in the + // enemy base) + if (dist > 512){ + return; + } + + supplies.push({ + dist : dist, + entity : supply.entity + }); + }); + + supplies.sort(function(a, b) { + // Prefer smaller distances + if (a.dist != b.dist) + return a.dist - b.dist; + + return false; + }); + + // Start gathering the best resource (by distance from the dropsite and unit) + if (supplies.length) { + ent.gather(supplies[0].entity); + ent.setMetadata("subrole", "gatherer"); + ent.setMetadata("gather-type", type); + return; + } + } + + // Couldn't find any types to gather + ent.setMetadata("subrole", "idle"); + }); + } +}; + +EconomyManager.prototype.assignToFoundations = function(gameState) { + // If we have some foundations, and we don't have enough + // builder-workers, + // try reassigning some other workers who are nearby + + var foundations = gameState.findFoundations(); + + // Check if nothing to build + if (!foundations.length){ + return; + } + + var workers = gameState.getOwnEntitiesWithRole("worker"); + + var builderWorkers = workers.filter(function(ent) { + return (ent.getMetadata("subrole") === "builder"); + }); + + // Check if enough builders + var extraNeeded = this.targetNumBuilders - builderWorkers.length; + if (extraNeeded <= 0){ + return; + } + + // Pick non-builders who are closest to the first foundation, + // and tell them to start building it + + var target = foundations.toEntityArray()[0]; + + var nonBuilderWorkers = workers.filter(function(ent) { + // check position so garrisoned units aren't tasked + return (ent.getMetadata("subrole") !== "builder" && ent.position() !== undefined); + }); + + var nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), extraNeeded); + + // Order each builder individually, not as a formation + nearestNonBuilders.forEach(function(ent) { + ent.repair(target); + ent.setMetadata("subrole", "builder"); + }); +}; + +EconomyManager.prototype.buildMoreFields = function(gameState, queues) { + // give time for treasures to be gathered + if (gameState.getTimeElapsed() < 30 * 1000) + return; + var numFields = gameState.countEntitiesAndQueuedWithType(gameState.applyCiv("structures/{civ}_field")); + numFields += queues.field.totalLength(); + + for ( var i = numFields; i < this.targetNumFields; i++) { + queues.field.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_field")); + } +}; + +// If all the CC's are destroyed then build a new one +EconomyManager.prototype.buildNewCC= function(gameState, queues) { + var numCCs = gameState.countEntitiesAndQueuedWithType(gameState.applyCiv("structures/{civ}_civil_centre")); + numCCs += queues.civilCentre.totalLength(); + + for ( var i = numCCs; i < 1; i++) { + queues.civilCentre.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_civil_centre")); + } +}; + +//creates and maintains a map of tree density +EconomyManager.prototype.updateResourceMaps = function(gameState, events){ + // The weight of the influence function is amountOfResource/decreaseFactor + var decreaseFactor = {'wood': 15, 'stone': 100, 'metal': 100, 'food': 20}; + // This is the maximum radius of the influence + var radius = {'wood':13, 'stone': 10, 'metal': 10, 'food': 10}; + + for (var resource in radius){ + // if there is no resourceMap create one with an influence for everything with that resource + if (! this.resourceMaps[resource]){ + this.resourceMaps[resource] = new Map(gameState); + + var supplies = gameState.findResourceSupplies(); + for (var i in supplies[resource]){ + var current = supplies[resource][i]; + var x = Math.round(current.position[0] / gameState.cellSize); + var z = Math.round(current.position[1] / gameState.cellSize); + var strength = Math.round(current.entity.resourceSupplyMax()/decreaseFactor[resource]); + this.resourceMaps[resource].addInfluence(x, z, radius[resource], strength); + } + } + // Look for destroy events and subtract the entities original influence from the resourceMap + for (var i in events) { + var e = events[i]; + + if (e.type === "Destroy") { + if (e.msg.rawEntity.template){ + var ent = new Entity(gameState.ai, e.msg.rawEntity); + if (ent && ent.resourceSupplyType() && ent.resourceSupplyType().generic === resource){ + var x = Math.round(ent.position()[0] / gameState.cellSize); + var z = Math.round(ent.position()[1] / gameState.cellSize); + var strength = Math.round(ent.resourceSupplyMax()/decreaseFactor[resource]); + this.resourceMaps[resource].addInfluence(x, z, radius[resource], -1*strength); + } + } + } + } + } + + //this.resourceMaps[resource].dumpIm("tree_density.png"); +}; + +// Returns the position of the best place to build a new dropsite for the specified resource +EconomyManager.prototype.getBestResourceBuildSpot = function(gameState, resource){ + // A map which gives a positive weight for all CCs and adds a negative weight near all dropsites + var friendlyTiles = new Map(gameState); + gameState.getOwnEntities().forEach(function(ent) { + // We want to build near a CC of ours + if (ent.hasClass("CivCentre")){ + var infl = 200; + + var pos = ent.position(); + var x = Math.round(pos[0] / gameState.cellSize); + var z = Math.round(pos[1] / gameState.cellSize); + friendlyTiles.addInfluence(x, z, infl, 0.1 * infl); + friendlyTiles.addInfluence(x, z, infl/2, 0.1 * infl); + } + // We don't want multiple dropsites at one spot so add a negative for all dropsites + if (ent.resourceDropsiteTypes() && ent.resourceDropsiteTypes().indexOf(resource) !== -1){ + var infl = 20; + + var pos = ent.position(); + var x = Math.round(pos[0] / gameState.cellSize); + var z = Math.round(pos[1] / gameState.cellSize); + + friendlyTiles.addInfluence(x, z, infl, -50, 'quadratic'); + } + }); + + // Multiply by tree density to get a combination of the two maps + friendlyTiles.multiply(this.resourceMaps[resource]); + + //friendlyTiles.dumpIm(resource + "_density_fade.png", 10000); + + var obstructions = Map.createObstructionMap(gameState); + obstructions.expandInfluences(); + + var bestIdx = friendlyTiles.findBestTile(4, obstructions)[0]; + + // Convert from 1d map pixel coordinates to game engine coordinates + var x = ((bestIdx % friendlyTiles.width) + 0.5) * gameState.cellSize; + var z = (Math.floor(bestIdx / friendlyTiles.width) + 0.5) * gameState.cellSize; + return [x,z]; +}; + +EconomyManager.prototype.updateResourceConcentrations = function(gameState){ + var self = this; + var resources = ["food", "wood", "stone", "metal"]; + for (key in resources){ + var resource = resources[key]; + gameState.getOwnEntities().forEach(function(ent) { + if (ent.resourceDropsiteTypes() && ent.resourceDropsiteTypes().indexOf(resource) !== -1){ + var radius = 14; + + var pos = ent.position(); + var x = Math.round(pos[0] / gameState.cellSize); + var z = Math.round(pos[1] / gameState.cellSize); + + var quantity = self.resourceMaps[resource].sumInfluence(x, z, radius); + + ent.setMetadata("resourceQuantity_" + resource, quantity); + } + }); + } +}; + +//return the number of resource dropsites with an acceptable amount of the resource nearby +EconomyManager.prototype.checkResourceConcentrations = function(gameState, resource){ + //TODO: make these values adaptive + var requiredInfluence = {wood: 16000, stone: 300, metal: 300}; + var count = 0; + gameState.getOwnEntities().forEach(function(ent) { + if (ent.resourceDropsiteTypes() && ent.resourceDropsiteTypes().indexOf(resource) !== -1){ + var quantity = ent.getMetadata("resourceQuantity_" + resource); + + if (quantity >= requiredInfluence[resource]){ + count ++; + } + } + }); + return count; +}; + +EconomyManager.prototype.buildDropsites = function(gameState, queues){ + if (queues.economicBuilding.totalLength() === 0 && + gameState.countFoundationsWithType(gameState.applyCiv("structures/{civ}_mill")) === 0 && + gameState.countFoundationsWithType(gameState.applyCiv("structures/{civ}_civil_centre")) === 0){ + //only ever build one mill/CC at a time + if (gameState.getTimeElapsed() > 30 * 1000){ + for (var resource in this.dropsiteNumbers){ + if (this.checkResourceConcentrations(gameState, resource) < this.dropsiteNumbers[resource]){ + var spot = this.getBestResourceBuildSpot(gameState, resource); + + var myCivCentres = gameState.getOwnEntities().filter(function(ent) { + if (!ent.hasClass("CivCentre") || ent.position() === undefined){ + return false; + } + var dx = (spot[0]-ent.position()[0]); + var dy = (spot[1]-ent.position()[1]); + var dist2 = dx*dx + dy*dy; + return (ent.hasClass("CivCentre") && dist2 < 180*180); + }); + + if (myCivCentres.length === 0){ + queues.economicBuilding.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_civil_centre", spot)); + }else{ + queues.economicBuilding.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_mill", spot)); + } + break; + } + } + } + } +}; + +EconomyManager.prototype.update = function(gameState, queues, events) { + Engine.ProfileStart("economy update"); + + this.reassignRolelessUnits(gameState); + + this.buildNewCC(gameState,queues); + + Engine.ProfileStart("Train workers and build farms"); + this.trainMoreWorkers(gameState, queues); + + this.buildMoreFields(gameState, queues); + Engine.ProfileStop(); + + //Later in the game we want to build stuff faster. + if (gameState.countEntitiesWithType(gameState.applyCiv("units/{civ}_support_female_citizen")) > this.targetNumWorkers * 0.5) { + this.targetNumBuilders = 10; + }else{ + this.targetNumBuilders = 5; + } + + if (gameState.countEntitiesWithType(gameState.applyCiv("units/{civ}_support_female_citizen")) > this.targetNumWorkers * 0.8) { + this.dropsiteNumbers = {wood: 3, stone: 2, metal: 2}; + }else{ + this.dropsiteNumbers = {wood: 2, stone: 1, metal: 1}; + } + + Engine.ProfileStart("Update Resource Maps and Concentrations"); + this.updateResourceMaps(gameState, events); + this.updateResourceConcentrations(gameState); + Engine.ProfileStop(); + + this.buildDropsites(gameState, queues); + + + // TODO: implement a timer based system for this + this.setCount += 1; + if (this.setCount >= 20){ + this.setWorkersIdleByPriority(gameState); + this.setCount = 0; + } + + Engine.ProfileStart("Reassign Idle Workers"); + this.reassignIdleWorkers(gameState); + Engine.ProfileStop(); + + Engine.ProfileStart("Assign builders"); + this.assignToFoundations(gameState); + Engine.ProfileStop(); + + Engine.ProfileStop(); +}; diff --git a/binaries/data/mods/public/simulation/ai/qbot/entity-extend.js b/binaries/data/mods/public/simulation/ai/qbot/entity-extend.js new file mode 100644 index 0000000000..d711efed9f --- /dev/null +++ b/binaries/data/mods/public/simulation/ai/qbot/entity-extend.js @@ -0,0 +1,28 @@ +Entity.prototype.deleteMetadata = function(id) { + delete this._ai._entityMetadata[this.id()]; +}; + +Entity.prototype.garrisonMax = function() { + if (!this._template.GarrisonHolder) + return undefined; + return this._template.GarrisonHolder.Max; +}; + +Entity.prototype.garrison = function(target) { + Engine.PostCommand({"type": "garrison", "entities": [this.id()], "target": target.id(),"queued": false}); + return this; +}; + +Entity.prototype.unload = function(id) { + if (!this._template.GarrisonHolder) + return undefined; + Engine.PostCommand({"type": "unload", "garrisonHolder": this.id(), "entity": id}); + return this; +}; + +Entity.prototype.unloadAll = function() { + if (!this._template.GarrisonHolder) + return undefined; + Engine.PostCommand({"type": "unload-all", "garrisonHolder": this.id()}); + return this; +}; diff --git a/binaries/data/mods/public/simulation/ai/qbot/entitycollection-extend.js b/binaries/data/mods/public/simulation/ai/qbot/entitycollection-extend.js new file mode 100644 index 0000000000..0e4a29b7cb --- /dev/null +++ b/binaries/data/mods/public/simulation/ai/qbot/entitycollection-extend.js @@ -0,0 +1,103 @@ +EntityCollection.prototype.attack = function(unit) +{ + var unitId; + if (typeOf(unit) === "Entity"){ + unitId = unit.id(); + }else{ + unitId = unit; + } + + Engine.PostCommand({"type": "walk", "entities": this.toIdArray(), "target": unitId, "queued": false}); + return this; +}; + +function EntityCollectionFromIds(gameState, idList){ + var ents = {}; + for (var i in idList){ + var id = idList[i]; + if (gameState.entities._entities[id]) { + ents[id] = gameState.entities._entities[id]; + } + } + return new EntityCollection(gameState.ai, ents); +} + +EntityCollection.prototype.attackMove = function(x, z){ + Engine.PostCommand({"type": "attack-move", "entities": this.toIdArray(), "x": x, "z": z, "queued": false}); + return this; +}; + +// Do naughty stuff to replace the entity collection constructor for updating entity collections +var tmpEntityCollection = function(baseAI, entities, filter, gameState){ + this._ai = baseAI; + this._entities = entities; + if (filter){ + var tmp = 3; + this.filterFunc = filter; + this._entities = this.filter(function(ent){ + return filter(ent, gameState); + })._entities; + this._ai.registerUpdate(this); + } + + // Compute length lazily on demand, since it can be + // expensive for large collections + // This is updated by the update() function. + this._length = undefined; + Object.defineProperty(this, "length", { + get: function () { + if (this._length === undefined) + { + this._length = 0; + for (var id in entities) + ++this._length; + } + return this._length; + } + }); +}; + +tmpEntityCollection.prototype = new EntityCollection; +EntityCollection = tmpEntityCollection; + +// Keeps an EntityCollection with a filter function up to date by watching for events +EntityCollection.prototype.update = function(gameState, events){ + if (!this.filterFunc) + return; + for (var i in events){ + if (events[i].type === "Create"){ + var ent = gameState.getEntityById(events[i].msg.entity); + if (ent){ + var raw_ent = ent._entity; + if (ent && this.filterFunc(ent, gameState)){ + this._entities[events[i].msg.entity] = raw_ent; + if (this._length !== undefined) + this._length ++; + } + } + }else if(events[i].type === "Destroy"){ + if (this._entities[events[i].msg.entity]){ + delete this._entities[events[i].msg.entity]; + if (this._length !== undefined) + this._length --; + } + } + } +}; + +EntityCollection.prototype.getCentrePosition = function(){ + var sumPos = [0, 0]; + var count = 0; + this.forEach(function(ent){ + if (ent.position()){ + sumPos[0] += ent.position()[0]; + sumPos[1] += ent.position()[1]; + count ++; + } + }); + if (count === 0){ + return undefined; + }else{ + return [sumPos[0]/count, sumPos[1]/count]; + } +}; \ No newline at end of file diff --git a/binaries/data/mods/public/simulation/ai/qbot/filters.js b/binaries/data/mods/public/simulation/ai/qbot/filters.js new file mode 100644 index 0000000000..7ea34cca13 --- /dev/null +++ b/binaries/data/mods/public/simulation/ai/qbot/filters.js @@ -0,0 +1,51 @@ +var Filters = { + byClass: function(cls){ + return function(ent){ + return ent.hasClass(cls); + }; + }, + + byClassesAnd: function(clsList){ + return function(ent){ + var ret = true; + for (var i in clsList){ + ret = ret && ent.hasClass(clsList[i]); + } + return ret; + }; + }, + + byClassesOr: function(clsList){ + return function(ent){ + var ret = false; + for (var i in clsList){ + ret = ret || ent.hasClass(clsList[i]); + } + return ret; + }; + }, + + and: function(filter1, filter2){ + return function(ent, gameState){ + return filter1(ent, gameState) && filter2(ent, gameState); + }; + }, + + or: function(filter1, filter2){ + return function(ent, gameState){ + return filter1(ent, gameState) || filter2(ent, gameState); + }; + }, + + isEnemy: function(){ + return function(ent, gameState){ + return gameState.isEntityEnemy(ent); + }; + }, + + isSoldier: function(){ + return function(ent){ + return Filters.byClassesOr(["CitizenSoldier", "Super"])(ent); + }; + } +}; diff --git a/binaries/data/mods/public/simulation/ai/qbot/gamestate.js b/binaries/data/mods/public/simulation/ai/qbot/gamestate.js new file mode 100644 index 0000000000..768f3fc66a --- /dev/null +++ b/binaries/data/mods/public/simulation/ai/qbot/gamestate.js @@ -0,0 +1,288 @@ +/** + * Provides an API for the rest of the AI scripts to query the world state at a + * higher level than the raw data. + */ +var GameState = function(ai) { + MemoizeInit(this); + + this.ai = ai; + this.timeElapsed = ai.timeElapsed; + this.templates = ai.templates; + this.entities = ai.entities; + this.player = ai.player; + this.playerData = ai.playerData; + this.buildingsBuilt = 0; + + this.cellSize = 4; // Size of each map tile +}; + +GameState.prototype.getTimeElapsed = function() { + return this.timeElapsed; +}; + +GameState.prototype.getTemplate = function(type) { + if (!this.templates[type]) + return null; + return new EntityTemplate(this.templates[type]); +}; + +GameState.prototype.applyCiv = function(str) { + return str.replace(/\{civ\}/g, this.playerData.civ); +}; + +/** + * @returns {Resources} + */ +GameState.prototype.getResources = function() { + return new Resources(this.playerData.resourceCounts); +}; + +GameState.prototype.getMap = function() { + return this.ai.passabilityMap; +}; + +GameState.prototype.getTerritoryMap = function() { + return this.ai.territoryMap; +}; + +GameState.prototype.getPopulation = function() { + return this.playerData.popCount; +}; + +GameState.prototype.getPopulationLimit = function() { + return this.playerData.popLimit; +}; + +GameState.prototype.getPopulationMax = function() { + return this.playerData.popMax; +}; + +GameState.prototype.getPassabilityClassMask = function(name) { + if (!(name in this.ai.passabilityClasses)) + error("Tried to use invalid passability class name '" + name + "'"); + return this.ai.passabilityClasses[name]; +}; + +GameState.prototype.getPlayerID = function() { + return this.player; +}; + +GameState.prototype.isPlayerAlly = function(id) { + return this.playerData.isAlly[id]; +}; + +GameState.prototype.isPlayerEnemy = function(id) { + return this.playerData.isEnemy[id]; +}; + +GameState.prototype.isEntityAlly = function(ent) { + if (ent && ent.owner && (typeof ent.owner) === "function"){ + return this.playerData.isAlly[ent.owner()]; + } else if (ent && ent.owner){ + return this.playerData.isAlly[ent.owner]; + } + return false; +}; + +GameState.prototype.isEntityEnemy = function(ent) { + if (ent && ent.owner && (typeof ent.owner) === "function"){ + return this.playerData.isEnemy[ent.owner()]; + } else if (ent && ent.owner){ + return this.playerData.isEnemy[ent.owner]; + } + return false; +}; + +GameState.prototype.isEntityOwn = function(ent) { + if (ent && ent.owner && (typeof ent.owner) === "function"){ + return ent.owner() == this.player; + } else if (ent && ent.owner){ + return ent.owner == this.player; + } + return false; +}; + +GameState.prototype.getOwnEntities = function() { + return new EntityCollection(this.ai, this.ai._ownEntities); +}; + +GameState.prototype.getEntities = function() { + return this.entities; +}; + +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"); + } + return false; +}; + +GameState.prototype.getOwnEntitiesWithRole = Memoize('getOwnEntitiesWithRole', function(role) { + var metas = this.ai._entityMetadata; + if (role === undefined) + return this.getOwnEntities().filter_raw(function(ent) { + var metadata = metas[ent.id]; + if (!metadata || !('role' in metadata)) + return true; + return (metadata.role === undefined); + }); + else + return this.getOwnEntities().filter_raw(function(ent) { + var metadata = metas[ent.id]; + if (!metadata || !('role' in metadata)) + return false; + return (metadata.role === role); + }); +}); + +GameState.prototype.countEntitiesWithType = function(type) { + var count = 0; + this.getOwnEntities().forEach(function(ent) { + var t = ent.templateName(); + if (t == type) + ++count; + }); + return count; +}; + +GameState.prototype.countEntitiesAndQueuedWithType = function(type) { + var foundationType = "foundation|" + type; + var count = 0; + this.getOwnEntities().forEach(function(ent) { + + var t = ent.templateName(); + if (t == type || t == foundationType) + ++count; + + var queue = ent.trainingQueue(); + if (queue) { + queue.forEach(function(item) { + if (item.template == type) + count += item.count; + }); + } + }); + return count; +}; + +GameState.prototype.countFoundationsWithType = function(type) { + var foundationType = "foundation|" + type; + var count = 0; + this.getOwnEntities().forEach(function(ent) { + var t = ent.templateName(); + if (t == foundationType) + ++count; + }); + return count; +}; + +GameState.prototype.countEntitiesAndQueuedWithRole = function(role) { + var count = 0; + this.getOwnEntities().forEach(function(ent) { + + if (ent.getMetadata("role") == role) + ++count; + + var queue = ent.trainingQueue(); + if (queue) { + queue.forEach(function(item) { + if (item.metadata && item.metadata.role == role) + count += item.count; + }); + } + }); + return count; +}; + +/** + * Find buildings that are capable of training the given unit type, and aren't + * already too busy. + */ +GameState.prototype.findTrainers = function(template) { + var maxQueueLength = 2; // avoid tying up resources in giant training + // queues + + return this.getOwnEntities().filter(function(ent) { + + var trainable = ent.trainableEntities(); + if (!trainable || trainable.indexOf(template) == -1) + return false; + + var queue = ent.trainingQueue(); + if (queue) { + if (queue.length >= maxQueueLength) + return false; + } + + return true; + }); +}; + +/** + * Find units that are capable of constructing the given building type. + */ +GameState.prototype.findBuilders = function(template) { + return this.getOwnEntities().filter(function(ent) { + + var buildable = ent.buildableEntities(); + if (!buildable || buildable.indexOf(template) == -1) + return false; + + return true; + }); +}; + +GameState.prototype.findFoundations = function(template) { + return this.getOwnEntities().filter(function(ent) { + return (typeof ent.foundationProgress() !== "undefined"); + }); +}; + +GameState.prototype.findResourceSupplies = function() { + var supplies = {}; + this.entities.forEach(function(ent) { + var type = ent.resourceSupplyType(); + if (!type) + return; + var amount = ent.resourceSupplyAmount(); + if (!amount) + return; + + var reportedType; + if (type.generic == "treasure") + reportedType = type.specific; + else + reportedType = type.generic; + + if (!supplies[reportedType]) + supplies[reportedType] = []; + + supplies[reportedType].push({ + "entity" : ent, + "amount" : amount, + "type" : type, + "position" : ent.position() + }); + }); + return supplies; +}; + + +GameState.prototype.getBuildLimits = function() { + return this.playerData.buildLimits; +}; + +GameState.prototype.getBuildCounts = function() { + return this.playerData.buildCounts; +}; + +GameState.prototype.isBuildLimitReached = function(category) { + if(this.playerData.buildLimits[category] === undefined || this.playerData.buildCounts[category] === undefined) + return false; + if(this.playerData.buildLimits[category].LimitsPerCivCentre != undefined) + return (this.playerData.buildCounts[category] >= this.playerData.buildCounts["CivilCentre"]*this.playerData.buildLimits[category].LimitPerCivCentre); + else + return (this.playerData.buildCounts[category] >= this.playerData.buildLimits[category]); +}; \ No newline at end of file diff --git a/binaries/data/mods/public/simulation/ai/qbot/housing.js b/binaries/data/mods/public/simulation/ai/qbot/housing.js new file mode 100644 index 0000000000..299d151136 --- /dev/null +++ b/binaries/data/mods/public/simulation/ai/qbot/housing.js @@ -0,0 +1,29 @@ +// Decides when to a new house needs to be built +var HousingManager = function() { + +}; + +HousingManager.prototype.buildMoreHouses = function(gameState, queues) { + // temporary 'remaining population space' based check, need to do + // predictive in future + if (gameState.getPopulationLimit() - gameState.getPopulation() < 20 + && gameState.getPopulationLimit() < gameState.getPopulationMax()) { + var numConstructing = gameState.countEntitiesWithType(gameState.applyCiv("foundation|structures/{civ}_house")); + var numPlanned = queues.house.totalLength(); + + var additional = Math.ceil((20 - (gameState.getPopulationLimit() - gameState.getPopulation())) / 10) + - numConstructing - numPlanned; + + for ( var i = 0; i < additional; i++) { + queues.house.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_house")); + } + } +}; + +HousingManager.prototype.update = function(gameState, queues) { + Engine.ProfileStart("housing update"); + + this.buildMoreHouses(gameState, queues); + + Engine.ProfileStop(); +}; diff --git a/binaries/data/mods/public/simulation/ai/qbot/license_gpl-2.0.txt b/binaries/data/mods/public/simulation/ai/qbot/license_gpl-2.0.txt new file mode 100644 index 0000000000..82fa1daad4 --- /dev/null +++ b/binaries/data/mods/public/simulation/ai/qbot/license_gpl-2.0.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/binaries/data/mods/public/simulation/ai/qbot/map-module.js b/binaries/data/mods/public/simulation/ai/qbot/map-module.js new file mode 100644 index 0000000000..6d0dd42ba9 --- /dev/null +++ b/binaries/data/mods/public/simulation/ai/qbot/map-module.js @@ -0,0 +1,257 @@ +// TODO: Make this cope with negative cell values + +function Map(gameState, originalMap){ + // get the map to find out the correct dimensions + var gameMap = gameState.getMap(); + this.width = gameMap.width; + this.height = gameMap.height; + this.length = gameMap.data.length; + if (originalMap){ + this.map = originalMap; + }else{ + this.map = new Uint16Array(this.length); + } + this.cellSize = gameState.cellSize; +} + +Map.prototype.gamePosToMapPos = function(p){ + return [Math.round(p[0]/this.cellSize), Math.round(p[1]/this.cellSize)]; +}; + +Map.createObstructionMap = function(gameState, template){ + var passabilityMap = gameState.getMap(); + var territoryMap = gameState.getTerritoryMap(); + + const TERRITORY_PLAYER_MASK = 0x7F; + + // default values + var placementType = "land"; + var buildOwn = true; + var buildAlly = true; + var buildNeutral = true; + var buildEnemy = false; + // If there is a template then replace the defaults + if (template){ + placementType = template.buildPlacementType(); + buildOwn = template.hasBuildTerritory("own"); + buildAlly = template.hasBuildTerritory("ally"); + buildNeutral = template.hasBuildTerritory("neutral"); + buildEnemy = template.hasBuildTerritory("enemy"); + } + + var obstructionMask = gameState.getPassabilityClassMask("foundationObstruction"); + // Only accept valid land tiles (we don't handle docks yet) + switch(placementType){ + case "shore": + obstructionMask |= gameState.getPassabilityClassMask("building-shore"); + break; + case "land": + default: + obstructionMask |= gameState.getPassabilityClassMask("building-land"); + break; + } + + var playerID = gameState.getPlayerID(); + + var obstructionTiles = new Uint16Array(passabilityMap.data.length); + for (var i = 0; i < passabilityMap.data.length; ++i) + { + var tilePlayer = (territoryMap.data[i] & TERRITORY_PLAYER_MASK); + var invalidTerritory = ( + (!buildOwn && tilePlayer == playerID) || + (!buildAlly && gameState.isPlayerAlly(tilePlayer) && tilePlayer != playerID) || + (!buildNeutral && tilePlayer == 0) || + (!buildEnemy && gameState.isPlayerEnemy(tilePlayer) && tilePlayer !=0) + ); + var tileAccessible = (gameState.ai.accessibility.map[i] == 1); + obstructionTiles[i] = (!tileAccessible || invalidTerritory || (passabilityMap.data[i] & obstructionMask)) ? 0 : 65535; + } + + var map = new Map(gameState, obstructionTiles); + if (template && template.buildDistance()){ + var minDist = template.buildDistance().MinDistance; + var category = template.buildDistance().FromCategory; + if (minDist !== undefined && category !== undefined){ + gameState.getOwnEntities().forEach(function(ent) { + if (ent.buildCategory() === category && ent.position()){ + var pos = ent.position(); + var x = Math.round(pos[0] / gameState.cellSize); + var z = Math.round(pos[1] / gameState.cellSize); + map.addInfluence(x, z, minDist/gameState.cellSize, -65535, 'constant'); + } + }); + } + } + + return map; +}; + +Map.createTerritoryMap = function(gameState){ + var map = gameState.ai.territoryMap; + + var obstructionTiles = new Uint16Array(map.data.length); + for ( var i = 0; i < map.data.length; ++i){ + obstructionTiles[i] = map.data[i] & 0x7F; + } + + return new Map(gameState, obstructionTiles); +}; + +Map.prototype.addInfluence = function(cx, cy, maxDist, strength, type) { + strength = strength ? strength : maxDist; + type = type ? type : 'linear'; + + var x0 = Math.max(0, cx - maxDist); + var y0 = Math.max(0, cy - maxDist); + var x1 = Math.min(this.width, cx + maxDist); + var y1 = Math.min(this.height, cy + maxDist); + var maxDist2 = maxDist * maxDist; + + var str = 0; + switch (type){ + case 'linear': + str = strength / maxDist; + break; + case 'quadratic': + str = strength / maxDist2; + break; + case 'constant': + str = strength; + break; + } + + for ( var y = y0; y < y1; ++y) { + for ( var x = x0; x < x1; ++x) { + var dx = x - cx; + var dy = y - cy; + var r2 = dx*dx + dy*dy; + if (r2 < maxDist2){ + var quant = 0; + switch (type){ + case 'linear': + var r = Math.sqrt(r2); + quant = str * (maxDist - r); + break; + case 'quadratic': + quant = str * (maxDist2 - r2); + break; + case 'constant': + quant = str; + break; + } + + if (-1 * quant > this.map[x + y * this.width]){ + this.map[x + y * this.width] = 0; //set anything which would have gone negative to 0 + }else{ + this.map[x + y * this.width] += quant; + } + } + } + } +}; + +Map.prototype.sumInfluence = function(cx, cy, radius){ + var x0 = Math.max(0, cx - radius); + var y0 = Math.max(0, cy - radius); + var x1 = Math.min(this.width, cx + radius); + var y1 = Math.min(this.height, cy + radius); + var radius2 = radius * radius; + + var sum = 0; + + for ( var y = y0; y < y1; ++y) { + for ( var x = x0; x < x1; ++x) { + var dx = x - cx; + var dy = y - cy; + var r2 = dx*dx + dy*dy; + if (r2 < radius2){ + sum += this.map[x + y * this.width]; + } + } + } + return sum; +}; + +/** + * Make each cell's 16-bit value at least one greater than each of its + * neighbours' values. (If the grid is initialised with 0s and 65535s, the + * result of each cell is its Manhattan distance to the nearest 0.) + * + * TODO: maybe this should be 8-bit (and clamp at 255)? + */ +Map.prototype.expandInfluences = function() { + var w = this.width; + var h = this.height; + var grid = this.map; + for ( var y = 0; y < h; ++y) { + var min = 65535; + for ( var x = 0; x < w; ++x) { + var g = grid[x + y * w]; + if (g > min) + grid[x + y * w] = min; + else if (g < min) + min = g; + ++min; + } + + for ( var x = w - 2; x >= 0; --x) { + var g = grid[x + y * w]; + if (g > min) + grid[x + y * w] = min; + else if (g < min) + min = g; + ++min; + } + } + + for ( var x = 0; x < w; ++x) { + var min = 65535; + for ( var y = 0; y < h; ++y) { + var g = grid[x + y * w]; + if (g > min) + grid[x + y * w] = min; + else if (g < min) + min = g; + ++min; + } + + for ( var y = h - 2; y >= 0; --y) { + var g = grid[x + y * w]; + if (g > min) + grid[x + y * w] = min; + else if (g < min) + min = g; + ++min; + } + } +}; + +Map.prototype.findBestTile = function(radius, obstructionTiles){ + // Find the best non-obstructed tile + var bestIdx = 0; + var bestVal = -1; + for ( var i = 0; i < this.length; ++i) { + if (obstructionTiles.map[i] > radius) { + var v = this.map[i]; + if (v > bestVal) { + bestVal = v; + bestIdx = i; + } + } + } + + return [bestIdx, bestVal]; +}; + +// Multiplies current map by the parameter map pixelwise +Map.prototype.multiply = function(map){ + for (var i = 0; i < this.length; i++){ + this.map[i] *= map.map[i]; + } +}; + +Map.prototype.dumpIm = function(name, threshold){ + name = name ? name : "default.png"; + threshold = threshold ? threshold : 256; + Engine.DumpImage(name, this.map, this.width, this.height, threshold); +}; diff --git a/binaries/data/mods/public/simulation/ai/qbot/military.js b/binaries/data/mods/public/simulation/ai/qbot/military.js new file mode 100755 index 0000000000..aacabdc98e --- /dev/null +++ b/binaries/data/mods/public/simulation/ai/qbot/military.js @@ -0,0 +1,657 @@ +/* + * 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() { + this.targetSquadSize = 10; + this.targetScoutTowers = 10; + + // 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.availableAttacks = []; + this.currentAttacks = []; + + this.defineUnitsAndBuildings(); +}; + +MilitaryAttackManager.prototype.init = function(gameState) { + var civ = gameState.playerData.civ; + if (civ in this.uCivCitizenSoldier) { + this.uCitizenSoldier = this.uCivCitizenSoldier[civ]; + this.uAdvanced = this.uCivAdvanced[civ]; + this.uSiege = this.uCivSiege[civ]; + + this.bAdvanced = this.bCivAdvanced[civ]; + } + + 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.attackManagers){ + this.availableAttacks[i] = new this.attackManagers[i](gameState, this); + } + + var filter = Filters.and(Filters.isEnemy(), Filters.byClassesOr(["CitizenSoldier", "Super"])); + this.enemySoldiers = new EntityCollection(gameState.ai, gameState.entities._entities, filter, gameState); +}; + +MilitaryAttackManager.prototype.defineUnitsAndBuildings = function(){ + // units + this.uCivCitizenSoldier= {}; + this.uCivAdvanced = {}; + this.uCivSiege = {}; + + this.uCivCitizenSoldier.hele = [ "units/hele_infantry_spearman_b", "units/hele_infantry_javelinist_b", "units/hele_infantry_archer_b" ]; + this.uCivAdvanced.hele = [ "units/hele_cavalry_swordsman_b", "units/hele_cavalry_javelinist_b", "units/hele_champion_cavalry_mace", "units/hele_champion_infantry_mace", "units/hele_champion_infantry_polis", "units/hele_champion_ranged_polis" , "units/thebes_sacred_band_hoplitai", "units/thespian_melanochitones","units/sparta_hellenistic_phalangitai", "units/thrace_black_cloak"]; + this.uCivSiege.hele = [ "units/hele_mechanical_siege_oxybeles", "units/hele_mechanical_siege_lithobolos" ]; + + this.uCivCitizenSoldier.cart = [ "units/cart_infantry_spearman_b", "units/cart_infantry_archer_b" ]; + this.uCivAdvanced.cart = [ "units/cart_cavalry_javelinist_b", "units/cart_champion_cavalry", "units/cart_infantry_swordsman_2_b", "units/cart_cavalry_spearman_b", "units/cart_infantry_javelinist_b", "units/cart_infantry_slinger_b", "units/cart_cavalry_swordsman_b", "units/cart_infantry_swordsman_b", "units/cart_cavalry_swordsman_2_b", "units/cart_sacred_band_cavalry"]; + this.uCivSiege.cart = ["units/cart_mechanical_siege_ballista", "units/cart_mechanical_siege_oxybeles"]; + + this.uCivCitizenSoldier.celt = [ "units/celt_infantry_spearman_b", "units/celt_infantry_javelinist_b" ]; + this.uCivAdvanced.celt = [ "units/celt_cavalry_javelinist_b", "units/celt_cavalry_swordsman_b", "units/celt_champion_cavalry_gaul", "units/celt_champion_infantry_gaul", "units/celt_champion_cavalry_brit", "units/celt_champion_infantry_brit", "units/celt_fanatic" ]; + this.uCivSiege.celt = ["units/celt_mechanical_siege_ram"]; + + this.uCivCitizenSoldier.iber = [ "units/iber_infantry_spearman_b", "units/iber_infantry_slinger_b", "units/iber_infantry_swordsman_b", "units/iber_infantry_javelinist_b" ]; + this.uCivAdvanced.iber = ["units/iber_cavalry_spearman_b", "units/iber_champion_cavalry", "units/iber_champion_infantry" ]; + this.uCivSiege.iber = ["units/iber_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"]; + this.uSiege = ["units/{civ}_mechanical_siege_oxybeles", "units/{civ}_mechanical_siege_lithobolos", "units/{civ}_mechanical_siege_ballista","units/{civ}_mechanical_siege_ram"]; + + // buildings + this.bModerate = [ "structures/{civ}_barracks" ]; //same for all civs + + this.bCivAdvanced = {}; + this.bCivAdvanced.hele = [ "structures/{civ}_gymnasion", "structures/{civ}_fortress" ]; + 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" ]; +}; + +/** + * @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.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; + + soldiers.forEach(function(ent) { + ent.setMetadata("role", "registeredSoldier"); + self.soldiers[ent.id()] = true; + self.unassigned[ent.id()] = true; + }); +}; + +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"); + + 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); + }); + 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) { + 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; + } + } + 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[unit[i]] = true; + this.assigned[unit[i]] = false; + } +}; + +MilitaryAttackManager.prototype.countAvailableUnits = function(){ + var count = 0; + for (var i in this.unassigned){ + if (this.unassigned[i]){ + count += 1; + } + } + return count; +}; + +MilitaryAttackManager.prototype.handleEvents = function(gameState, events) { + 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]; + 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()); + } + } + } + } + } +}; + +// 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]){ + strength += this.getUnitStrength(this.entity(i)); + } + } + return strength; +}; + +MilitaryAttackManager.prototype.getEnemySoldiers = function(gameState){ + 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"]) { + queues.defenceBuilding.addItem(new BuildingConstructionPlan(gameState, 'structures/{civ}_scout_tower')); + } +}; + +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); + + // 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.8){ + 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 + 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)); + } + } + // 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"); + } + } + + Engine.ProfileStop(); +}; diff --git a/binaries/data/mods/public/simulation/ai/qbot/plan-building.js b/binaries/data/mods/public/simulation/ai/qbot/plan-building.js new file mode 100644 index 0000000000..de078d6af4 --- /dev/null +++ b/binaries/data/mods/public/simulation/ai/qbot/plan-building.js @@ -0,0 +1,126 @@ +var BuildingConstructionPlan = function(gameState, type, position) { + this.type = gameState.applyCiv(type); + this.position = position; + + var template = gameState.getTemplate(this.type); + if (!template) { + this.invalidTemplate = true; + debug("Cannot build " + this.type); + return; + } + this.category = "building"; + this.cost = new Resources(template.cost()); + this.number = 1; // The number of buildings to build +}; + +BuildingConstructionPlan.prototype.canExecute = function(gameState) { + if (this.invalidTemplate){ + return false; + } + + // TODO: verify numeric limits etc + + var builders = gameState.findBuilders(this.type); + + return (builders.length != 0); +}; + +BuildingConstructionPlan.prototype.execute = function(gameState) { + + var builders = gameState.findBuilders(this.type).toEntityArray(); + + // We don't care which builder we assign, since they won't actually + // do the building themselves - all we care about is that there is + // some unit that can start the foundation + + var pos = this.findGoodPosition(gameState); + if (!pos){ + debug("No room to place " + this.type); + return; + } + + builders[0].construct(this.type, pos.x, pos.z, pos.angle); +}; + +BuildingConstructionPlan.prototype.getCost = function() { + return this.cost; +}; + +BuildingConstructionPlan.prototype.findGoodPosition = function(gameState) { + var template = gameState.getTemplate(this.type); + + var cellSize = gameState.cellSize; // size of each tile + + // First, find all tiles that are far enough away from obstructions: + + var obstructionMap = Map.createObstructionMap(gameState,template); + + //obstructionMap.dumpIm("obstructions.png"); + + obstructionMap.expandInfluences(); + + // Compute each tile's closeness to friendly structures: + + var friendlyTiles = new Map(gameState); + + // If a position was specified then place the building as close to it as possible + if (this.position){ + var x = Math.round(this.position[0] / cellSize); + var z = Math.round(this.position[1] / cellSize); + friendlyTiles.addInfluence(x, z, 200); + //friendlyTiles.dumpIm("pos.png", 200); + }else{ + // Not position was specified so try and find a sensible place to build + gameState.getOwnEntities().forEach(function(ent) { + if (ent.hasClass("Structure")) { + var infl = 32; + if (ent.hasClass("CivCentre")) + infl *= 4; + + var pos = ent.position(); + var x = Math.round(pos[0] / cellSize); + var z = Math.round(pos[1] / cellSize); + if (template._template.BuildRestrictions.Category === "Field"){ + // Only care about being near a place where we can deposit food for fields + if (ent.resourceDropsiteTypes() && ent.resourceDropsiteTypes().indexOf("food") !== -1){ + friendlyTiles.addInfluence(x, z, infl, infl); + } + }else{ + friendlyTiles.addInfluence(x, z, infl); + // If this is not a field add a negative influence near the CivCentre because we want to leave this + // area for fields. + if (ent.hasClass("CivCentre")){ + friendlyTiles.addInfluence(x, z, infl/8, -infl/2); + } + } + + + } + }); + } + + // Find target building's approximate obstruction radius, and expand by a bit to make sure we're not too close, this + // allows room for units to walk between buildings. + var radius = Math.ceil(template.obstructionRadius() / cellSize) + 2; + + // Find the best non-obstructed tile + var bestTile = friendlyTiles.findBestTile(radius, obstructionMap); + var bestIdx = bestTile[0]; + var bestVal = bestTile[1]; + + if (bestVal === -1){ + return false; + } + + var x = ((bestIdx % friendlyTiles.width) + 0.5) * cellSize; + var z = (Math.floor(bestIdx / friendlyTiles.width) + 0.5) * cellSize; + + // default angle + var angle = 3*Math.PI/4; + + return { + "x" : x, + "z" : z, + "angle" : angle + }; +}; diff --git a/binaries/data/mods/public/simulation/ai/qbot/plan-training.js b/binaries/data/mods/public/simulation/ai/qbot/plan-training.js new file mode 100644 index 0000000000..48688017c6 --- /dev/null +++ b/binaries/data/mods/public/simulation/ai/qbot/plan-training.js @@ -0,0 +1,56 @@ +var UnitTrainingPlan = function(gameState, type, metadata, number) { + this.type = gameState.applyCiv(type); + this.metadata = metadata; + + var template = gameState.getTemplate(this.type); + if (!template) { + this.invalidTemplate = true; + return; + } + this.category= "unit"; + this.cost = new Resources(template.cost(), template._template.Cost.Population); + if (!number){ + this.number = 1; + }else{ + this.number = number; + } +}; + +UnitTrainingPlan.prototype.canExecute = function(gameState) { + if (this.invalidTemplate) + return false; + + // TODO: we should probably check pop caps + + var trainers = gameState.findTrainers(this.type); + + return (trainers.length != 0); +}; + +UnitTrainingPlan.prototype.execute = function(gameState) { + //warn("Executing UnitTrainingPlan " + uneval(this)); + + var trainers = gameState.findTrainers(this.type).toEntityArray(); + + // Prefer training buildings with short queues + // (TODO: this should also account for units added to the queue by + // plans that have already been executed this turn) + if (trainers.length > 0){ + trainers.sort(function(a, b) { + return a.trainingQueueTime() - b.trainingQueueTime(); + }); + + trainers[0].train(this.type, this.number, this.metadata); + } +}; + +UnitTrainingPlan.prototype.getCost = function(){ + var multCost = new Resources(); + multCost.add(this.cost); + multCost.multiply(this.number); + return multCost; +}; + +UnitTrainingPlan.prototype.addItem = function(){ + this.number += 1; +}; \ No newline at end of file diff --git a/binaries/data/mods/public/simulation/ai/qbot/qbot.js b/binaries/data/mods/public/simulation/ai/qbot/qbot.js new file mode 100644 index 0000000000..84d1c1378e --- /dev/null +++ b/binaries/data/mods/public/simulation/ai/qbot/qbot.js @@ -0,0 +1,143 @@ + +function QBotAI(settings) { + BaseAI.call(this, settings); + + this.turn = 0; + + this.modules = [ new EconomyManager(), new MilitaryAttackManager(), new HousingManager() ]; + + // this.queues cannot be modified past initialisation or queue-manager will break + this.queues = { + house : new Queue(), + citizenSoldier : new Queue(), + villager : new Queue(), + economicBuilding : new Queue(), + field : new Queue(), + advancedSoldier : new Queue(), + siege : new Queue(), + militaryBuilding : new Queue(), + defenceBuilding : new Queue(), + civilCentre: new Queue() + }; + + this.productionQueues = []; + + var priorities = { + house : 500, + citizenSoldier : 100, + villager : 100, + economicBuilding : 30, + field: 4, + advancedSoldier : 30, + siege : 10, + militaryBuilding : 30, + defenceBuilding: 5, + civilCentre: 1000 + }; + this.queueManager = new QueueManager(this.queues, priorities); + + this.firstTime = true; + + this.savedEvents = []; + + this.toUpdate = []; +} + +QBotAI.prototype = new BaseAI(); + +//Some modules need the gameState to fully initialise +QBotAI.prototype.runInit = function(gameState){ + if (this.firstTime){ + for (var i = 0; i < this.modules.length; i++){ + if (this.modules[i].init){ + this.modules[i].init(gameState); + } + } + + var myCivCentres = gameState.getOwnEntities().filter(function(ent) { + return ent.hasClass("CivCentre"); + }); + + var filter = Filters.and(Filters.isEnemy(), Filters.byClass("CivCentre")); + var enemyCivCentres = gameState.getEntities().filter(function(ent) { + return ent.hasClass("CivCentre") && gameState.isEntityEnemy(ent); + }); + + this.accessibility = new Accessibility(gameState, myCivCentres.toEntityArray()[0].position()); + + var pathFinder = new PathFinder(gameState); + this.pathsToMe = pathFinder.getPaths(enemyCivCentres.toEntityArray()[0].position(), myCivCentres.toEntityArray()[0].position(), 'entryPoints'); + + this.firstTime = false; + } +}; + +QBotAI.prototype.registerUpdate = function(obj){ + this.toUpdate.push(obj); +}; + +QBotAI.prototype.OnUpdate = function() { + if (this.gameFinished){ + return; + } + if (this.events.length > 0){ + this.savedEvents = this.savedEvents.concat(this.events); + } + + // Run the update every n turns, offset depending on player ID to balance + // the load + if ((this.turn + this.player) % 10 == 0) { + Engine.ProfileStart("qBot"); + + var gameState = new GameState(this); + + // Run these updates before the init so they don't get hammered by the initial creation + // events at the start of the game. + for (var i = 0; i < this.toUpdate.length; i++){ + this.toUpdate[i].update(gameState, this.savedEvents); + } + + this.runInit(gameState); + + for (var i = 0; i < this.modules.length; i++){ + this.modules[i].update(gameState, this.queues, this.savedEvents); + } + + this.queueManager.update(gameState); + + // Generate some entropy in the random numbers (against humans) until the engine gets random initialised numbers + // TODO: remove this when the engine gives a random seed + var n = this.savedEvents.length % 29; + for (var i = 0; i < n; i++){ + Math.random(); + } + + delete this.savedEvents; + this.savedEvents = []; + + Engine.ProfileStop(); + } + + this.turn++; +}; + +var debugOn = false; + +function debug(output){ + if (debugOn){ + if (typeof output === "string"){ + warn(output); + }else{ + warn(uneval(output)); + } + } +} + +function copyPrototype(descendant, parent) { + var sConstructor = parent.toString(); + var aMatch = sConstructor.match( /\s*function (.*)\(/ ); + if ( aMatch != null ) { descendant.prototype[aMatch[1]] = parent; } + for (var m in parent.prototype) { + descendant.prototype[m] = parent.prototype[m]; + } +} diff --git a/binaries/data/mods/public/simulation/ai/qbot/queue-manager.js b/binaries/data/mods/public/simulation/ai/qbot/queue-manager.js new file mode 100644 index 0000000000..19b8c80ea4 --- /dev/null +++ b/binaries/data/mods/public/simulation/ai/qbot/queue-manager.js @@ -0,0 +1,293 @@ +//This takes the input queues and picks which items to fund with resources until no more resources are left to distribute. +// +//In this manager all resources are 'flattened' into a single type=(food+wood+metal+stone+pop*50 (see resources.js)) +//the following refers to this simple as resource +// +// Each queue has an account which records the amount of resource it can spend. If no queue has an affordable item +// then the amount of resource is increased to all accounts in direct proportion to the priority until an item on one +// of the queues becomes affordable. +// +// A consequence of the system is that a rarely used queue will end up with a very large account. I am unsure if this +// is good or bad or neither. +// +// Each queue object has two queues in it, one with items waiting for resources and the other with items which have been +// allocated resources and are due to be executed. The secondary queues are helpful because then units can be trained +// in groups of 5 and buildings are built once per turn to avoid placement clashes. + +var QueueManager = function(queues, priorities) { + this.queues = queues; + this.priorities = priorities; + this.account = {}; + for (p in this.queues) { + this.account[p] = 0; + } + this.curItemQueue = []; +}; + +QueueManager.prototype.getAvailableResources = function(gameState) { + var resources = gameState.getResources(); + for (key in this.queues) { + resources.subtract(this.queues[key].outQueueCost()); + } + return resources; +}; + +QueueManager.prototype.futureNeeds = function(gameState) { + // Work out which plans will be executed next using priority and return the total cost of these plans + var recurse = function(queues, qm, number, depth){ + var needs = new Resources(); + var totalPriority = 0; + for (var i = 0; i < queues.length; i++){ + totalPriority += qm.priorities[queues[i]]; + } + for (var i = 0; i < queues.length; i++){ + var num = Math.round(((qm.priorities[queues[i]]/totalPriority) * number)); + if (num < qm.queues[queues[i]].countQueuedUnits()){ + var cnt = 0; + for ( var j = 0; cnt < num; j++) { + cnt += qm.queues[queues[i]].queue[j].number; + needs.add(qm.queues[queues[i]].queue[j].getCost()); + number -= qm.queues[queues[i]].queue[j].number; + } + }else{ + for ( var j = 0; j < qm.queues[queues[i]].length(); j++) { + needs.add(qm.queues[queues[i]].queue[j].getCost()); + number -= qm.queues[queues[i]].queue[j].number; + } + queues.splice(i, 1); + i--; + } + } + // Check that more items were selected this call and that there are plans left to be allocated + // Also there is a fail-safe max depth + if (queues.length > 0 && number > 0 && depth < 20){ + needs.add(recurse(queues, qm, number, depth + 1)); + } + return needs; + }; + + //number of plans to look at + var current = this.getAvailableResources(gameState); + + var futureNum = 20; + var queues = []; + for (q in this.queues){ + queues.push(q); + } + var needs = recurse(queues, this, futureNum, 0); + // Return predicted values minus the current stockpiles along with a base rater for all resources + return { + "food" : Math.max(needs.food - current.food, 0) + 150, + "wood" : Math.max(needs.wood + 15*needs.population - current.wood, 0) + 150, //TODO: read the house cost in case it changes in the future + "stone" : Math.max(needs.stone - current.stone, 0) + 50, + "metal" : Math.max(needs.metal - current.metal, 0) + 100 + }; +}; + +// runs through the curItemQueue and allocates resources be sending the +// affordable plans to the Out Queues. Returns a list of the unneeded resources +// so they can be used by lower priority plans. +QueueManager.prototype.affordableToOutQueue = function(gameState) { + var availableRes = this.getAvailableResources(gameState); + if (this.curItemQueue.length === 0) { + return availableRes; + } + + var resources = this.getAvailableResources(gameState); + + // Check everything in the curItemQueue, if it is affordable then mark it + // for execution + for ( var i = 0; i < this.curItemQueue.length; i++) { + availableRes.subtract(this.queues[this.curItemQueue[i]].getNext().getCost()); + if (resources.canAfford(this.queues[this.curItemQueue[i]].getNext().getCost())) { + this.account[this.curItemQueue[i]] -= this.queues[this.curItemQueue[i]].getNext().getCost().toInt(); + this.queues[this.curItemQueue[i]].nextToOutQueue(); + resources = this.getAvailableResources(gameState); + this.curItemQueue[i] = null; + } + } + + // Clear the spent items + var tmpQueue = []; + for ( var i = 0; i < this.curItemQueue.length; i++) { + if (this.curItemQueue[i] !== null) { + tmpQueue.push(this.curItemQueue[i]); + } + } + this.curItemQueue = tmpQueue; + + return availableRes; +}; + +QueueManager.prototype.onlyUsesSpareAndUpdateSpare = function(unitCost, spare){ + // This allows plans to be given resources if there are >500 spare after all the + // higher priority plan queues have been looked at and there are still enough resources + // We make it >0 so that even if we have no stone available we can still have non stone + // plans being given resources. + var spareNonNegRes = { + food: Math.max(0, spare.food - 500), + wood: Math.max(0, spare.wood - 500), + stone: Math.max(0, spare.stone - 500), + metal: Math.max(0, spare.metal - 500) + }; + var spareNonNeg = new Resources(spareNonNegRes); + var ret = false; + if (spareNonNeg.canAfford(unitCost)){ + ret = true; + } + + // If there are no negative resources then there weren't any higher priority items so we + // definitely want to say that this can be added to the list. + var tmp = true; + for (key in spare.types){ + var type = spare.types[key]; + if (spare[type] < 0){ + tmp = false; + } + } + // If either to the above sections returns true then + ret = ret || tmp; + + spare.subtract(unitCost); // take the resources of the current unit from spare since this + // must be higher priority than any which are looked at + // afterwards. + + return ret; +}; + +String.prototype.rpad = function(padString, length) { + var str = this; + while (str.length < length) + str = str + padString; + return str; +}; + +QueueManager.prototype.printQueues = function(){ + debug("OUTQUEUES"); + for (var i in this.queues){ + var qStr = ""; + var q = this.queues[i]; + for (var j in q.outQueue){ + qStr += q.outQueue[j].type + " "; + if (q.outQueue[j].number) + qStr += "x" + q.outQueue[j].number; + } + if (qStr != ""){ + debug((i + ":").rpad(" ", 20) + qStr); + } + } + + debug("INQUEUES"); + for (var i in this.queues){ + var qStr = ""; + var q = this.queues[i]; + for (var j in q.queue){ + qStr += q.queue[j].type + " "; + if (q.queue[j].number) + qStr += "x" + q.queue[j].number; + qStr += " "; + } + if (qStr != ""){ + debug((i + ":").rpad(" ", 20) + qStr); + } + } + debug("Accounts: " + uneval(this.account)); +}; + +QueueManager.prototype.update = function(gameState) { + Engine.ProfileStart("Queue Manager"); + //this.printQueues(); + + Engine.ProfileStart("Pick items from queues"); + // See if there is a high priority item from last time. + this.affordableToOutQueue(gameState); + do { + // pick out all affordable items, and list the ratios of (needed + // cost)/priority for unaffordable items. + var ratio = {}; + var ratioMin = 1000000; + var ratioMinQueue = undefined; + for (p in this.queues) { + if (this.queues[p].length() > 0 && this.curItemQueue.indexOf(p) === -1) { + var cost = this.queues[p].getNext().getCost().toInt(); + if (cost < this.account[p]) { + this.curItemQueue.push(p); + // break; + } else { + ratio[p] = (cost - this.account[p]) / this.priorities[p]; + if (ratio[p] < ratioMin) { + ratioMin = ratio[p]; + ratioMinQueue = p; + } + } + } + } + + // Checks to see that there is an item in at least one queue, otherwise + // breaks the loop. + if (this.curItemQueue.length === 0 && ratioMinQueue === undefined) { + break; + } + + var availableRes = this.affordableToOutQueue(gameState); + + var allSpare = availableRes["food"] > 0 && availableRes["wood"] > 0 && availableRes["stone"] > 0 && availableRes["metal"] > 0; + // if there are no affordable items use any resources which aren't + // wanted by a higher priority item + if ((availableRes["food"] > 0 || availableRes["wood"] > 0 || availableRes["stone"] > 0 || availableRes["metal"] > 0) + && ratioMinQueue !== undefined) { + while (Object.keys(ratio).length > 0 && (availableRes["food"] > 0 || availableRes["wood"] > 0 || availableRes["stone"] > 0 || availableRes["metal"] > 0)){ + ratioMin = Math.min(); //biggest value + for (key in ratio){ + if (ratio[key] < ratioMin){ + ratioMin = ratio[key]; + ratioMinQueue = key; + } + } + if (this.onlyUsesSpareAndUpdateSpare(this.queues[ratioMinQueue].getNext().getCost(), availableRes)){ + if (allSpare){ + for (p in this.queues) { + this.account[p] += ratioMin * this.priorities[p]; + } + } + //this.account[ratioMinQueue] -= this.queues[ratioMinQueue].getNext().getCost().toInt(); + this.curItemQueue.push(ratioMinQueue); + allSpare = availableRes["food"] > 0 && availableRes["wood"] > 0 && availableRes["stone"] > 0 && availableRes["metal"] > 0; + } + delete ratio[ratioMinQueue]; + } + + } + + this.affordableToOutQueue(gameState); + } while (this.curItemQueue.length === 0); + Engine.ProfileStop(); + + Engine.ProfileStart("Execute items"); + // Handle output queues by executing items where possible + for (p in this.queues) { + while (this.queues[p].outQueueLength() > 0) { + var next = this.queues[p].outQueueNext(); + if (next.category === "building") { + if (gameState.buildingsBuilt == 0) { + if (this.queues[p].outQueueNext().canExecute(gameState)) { + this.queues[p].executeNext(gameState); + gameState.buildingsBuilt += 1; + } else { + break; + } + } else { + break; + } + } else { + if (this.queues[p].outQueueNext().canExecute(gameState)){ + this.queues[p].executeNext(gameState); + }else{ + break; + } + } + } + } + Engine.ProfileStop(); + Engine.ProfileStop(); +}; diff --git a/binaries/data/mods/public/simulation/ai/qbot/queue.js b/binaries/data/mods/public/simulation/ai/qbot/queue.js new file mode 100644 index 0000000000..a36242920e --- /dev/null +++ b/binaries/data/mods/public/simulation/ai/qbot/queue.js @@ -0,0 +1,114 @@ +/* + * Holds a list of wanted items to train or construct + */ + +var Queue = function() { + this.queue = []; + this.outQueue = []; +}; + +Queue.prototype.addItem = function(plan) { + this.queue.push(plan); +}; + +Queue.prototype.getNext = function() { + if (this.queue.length > 0) { + return this.queue[0]; + } else { + return null; + } +}; + +Queue.prototype.outQueueNext = function(){ + if (this.outQueue.length > 0) { + return this.outQueue[0]; + } else { + return null; + } +}; + +Queue.prototype.outQueueCost = function(){ + var cost = new Resources(); + for (key in this.outQueue){ + cost.add(this.outQueue[key].getCost()); + } + return cost; +}; + +Queue.prototype.nextToOutQueue = function(){ + if (this.queue.length > 0){ + if (this.outQueue.length > 0 && + this.getNext().category === "unit" && + this.outQueue[this.outQueue.length-1].type === this.getNext().type && + this.outQueue[this.outQueue.length-1].number < 5){ + this.queue.shift(); + this.outQueue[this.outQueue.length-1].addItem(); + }else{ + this.outQueue.push(this.queue.shift()); + } + } +}; + +Queue.prototype.executeNext = function(gameState) { + if (this.outQueue.length > 0) { + this.outQueue.shift().execute(gameState); + return true; + } else { + return false; + } +}; + +Queue.prototype.length = function() { + return this.queue.length; +}; + +Queue.prototype.countQueuedUnits = function(){ + var count = 0; + for (var i in this.queue){ + count += this.queue[i].number; + } + return count; +}; + +Queue.prototype.countOutQueuedUnits = function(){ + var count = 0; + for (var i in this.outQueue){ + count += this.outQueue[i].number; + } + return count; +}; + +Queue.prototype.countTotalQueuedUnits = function(){ + var count = 0; + for (var i in this.queue){ + count += this.queue[i].number; + } + for (var i in this.outQueue){ + count += this.outQueue[i].number; + } + return count; +}; + +Queue.prototype.totalLength = function(){ + return this.queue.length + this.outQueue.length; +}; + +Queue.prototype.outQueueLength = function(){ + return this.outQueue.length; +}; + +Queue.prototype.countAllByType = function(t){ + var count = 0; + + for (var i = 0; i < this.queue.length; i++){ + if (this.queue[i].type === t){ + count += this.queue[i].number; + } + } + for (var i = 0; i < this.outQueue.length; i++){ + if (this.outQueue[i].type === t){ + count += this.outQueue[i].number; + } + } + return count; +}; \ No newline at end of file diff --git a/binaries/data/mods/public/simulation/ai/qbot/readme.txt b/binaries/data/mods/public/simulation/ai/qbot/readme.txt new file mode 100644 index 0000000000..0a5521df42 --- /dev/null +++ b/binaries/data/mods/public/simulation/ai/qbot/readme.txt @@ -0,0 +1,6 @@ +This is an AI for 0 AD based on the testBot. + +Install by placing the files into the data/mods/public/simulation/ai/qbot folder. + +If you are developing you might find it helpful to change the debugOn line in qBot.js. This will make it spew random warnings depending on what I have been working on. Use the debug() function to make your own warnings. + diff --git a/binaries/data/mods/public/simulation/ai/qbot/resources.js b/binaries/data/mods/public/simulation/ai/qbot/resources.js new file mode 100644 index 0000000000..4f00c7de84 --- /dev/null +++ b/binaries/data/mods/public/simulation/ai/qbot/resources.js @@ -0,0 +1,66 @@ +function Resources(amounts, population) { + if (amounts === undefined) { + amounts = { + food : 0, + wood : 0, + stone : 0, + metal : 0 + }; + } + for ( var tKey in this.types) { + var t = this.types[tKey]; + this[t] = amounts[t] || 0; + } + + if (population > 0) { + this.population = parseInt(population); + } else { + this.population = 0; + } +} + +Resources.prototype.types = [ "food", "wood", "stone", "metal" ]; + +Resources.prototype.canAfford = function(that) { + for ( var tKey in this.types) { + var t = this.types[tKey]; + if (this[t] < that[t]) { + return false; + } + } + return true; +}; + +Resources.prototype.add = function(that) { + for ( var tKey in this.types) { + var t = this.types[tKey]; + this[t] += that[t]; + } + this.population += that.population; +}; + +Resources.prototype.subtract = function(that) { + for ( var tKey in this.types) { + var t = this.types[tKey]; + this[t] -= that[t]; + } + this.population += that.population; +}; + +Resources.prototype.multiply = function(n) { + for ( var tKey in this.types) { + var t = this.types[tKey]; + this[t] *= n; + } + this.population *= n; +}; + +Resources.prototype.toInt = function() { + var sum = 0; + for ( var tKey in this.types) { + var t = this.types[tKey]; + sum += this[t]; + } + sum += this.population * 50; // based on typical unit costs + return sum; +}; diff --git a/binaries/data/mods/public/simulation/ai/qbot/terrain-analysis.js b/binaries/data/mods/public/simulation/ai/qbot/terrain-analysis.js new file mode 100644 index 0000000000..fcf4292752 --- /dev/null +++ b/binaries/data/mods/public/simulation/ai/qbot/terrain-analysis.js @@ -0,0 +1,302 @@ +/* + * TerrainAnalysis inherits from Map + * + * This creates a suitable passability map for pathfinding units and provides the findClosestPassablePoint() function. + * This is intended to be a base object for the terrain analysis modules to inherit from. + */ + +function TerrainAnalysis(gameState){ + var passabilityMap = gameState.getMap(); + + var obstructionMask = gameState.getPassabilityClassMask("pathfinderObstruction"); + obstructionMask |= gameState.getPassabilityClassMask("default"); + + var obstructionTiles = new Uint16Array(passabilityMap.data.length); + for (var i = 0; i < passabilityMap.data.length; ++i) + { + obstructionTiles[i] = (passabilityMap.data[i] & obstructionMask) ? 0 : 65535; + } + + this.Map(gameState, obstructionTiles); +}; + +copyPrototype(TerrainAnalysis, Map); + +// Returns the (approximately) closest point which is passable by searching in a spiral pattern +TerrainAnalysis.prototype.findClosestPassablePoint = function(startPoint){ + var w = this.width; + var p = startPoint; + var direction = 1; + + if (p[0] + w*p[1] > 0 && p[0] + w*p[1] < this.length && + this.map[p[0] + w*p[1]] != 0){ + return p; + } + + // search in a spiral pattern. + for (var i = 1; i < w; i++){ + for (var j = 0; j < 2; j++){ + for (var k = 0; k < i; k++){ + p[j] += direction; + if (p[0] + w*p[1] > 0 && p[0] + w*p[1] < this.length && + this.map[p[0] + w*p[1]] != 0){ + return p; + } + } + } + direction *= -1; + } + + return undefined; +}; + +/* + * PathFinder inherits from TerrainAnalysis + * + * Used to create a list of distinct paths between two points. + * + * Currently it works basically. + * + * TODO: Make this use territories. + */ + + +function PathFinder(gameState){ + this.TerrainAnalysis(gameState); + + this.territoryMap = Map.createTerritoryMap(gameState); +} + +copyPrototype(PathFinder, TerrainAnalysis); + +/* + * Returns a list of distinct paths to the destination. Curerntly paths are distinct if they are more than + * blockRadius apart at a distance of blockPlacementRadius from the destination. Where blockRadius and + * blockPlacementRadius are defined in walkGradient + */ +PathFinder.prototype.getPaths = function(start, end, mode){ + var s = this.findClosestPassablePoint(this.gamePosToMapPos(start)); + var e = this.findClosestPassablePoint(this.gamePosToMapPos(end)); + + if (!s || !e){ + return undefined; + } + + var paths = []; + + while (true){ + this.makeGradient(s,e); + var curPath = this.walkGradient(e, mode); + if (curPath !== undefined){ + paths.push(curPath); + }else{ + break; + } + this.wipeGradient(); + } + + //this.dumpIm("terrainanalysis.png", 511); + + if (paths.length > 0){ + return paths; + }else{ + return undefined; + } +}; + +// Creates a potential gradient with the start point having the lowest potential +PathFinder.prototype.makeGradient = function(start, end){ + var w = this.width; + var map = this.map; + + // Holds the list of current points to work outwards from + var stack = []; + // We store the next level in its own stack + var newStack = []; + // Relative positions or new cells from the current one. We alternate between the adjacent 4 and 8 cells + // so that there is an average 1.5 distance for diagonals which is close to the actual sqrt(2) ~ 1.41 + var positions = [[[0,1], [0,-1], [1,0], [-1,0]], + [[0,1], [0,-1], [1,0], [-1,0], [1,1], [-1,-1], [1,-1], [-1,1]]]; + + //Set the distance of the start point to be 1 to distinguish it from the impassable areas + map[start[0] + w*(start[1])] = 1; + stack.push(start); + + // while there are new points being added to the stack + while (stack.length > 0){ + //run through the current stack + while (stack.length > 0){ + var cur = stack.pop(); + // stop when we reach the end point + if (cur[0] == end[0] && cur[1] == end[1]){ + return; + } + + var dist = map[cur[0] + w*(cur[1])] + 1; + // Check the positions adjacent to the current cell + for (var i = 0; i < positions[dist % 2].length; i++){ + var pos = positions[dist % 2][i]; + var cell = cur[0]+pos[0] + w*(cur[1]+pos[1]); + if (cell >= 0 && cell < this.length && map[cell] > dist){ + map[cell] = dist; + newStack.push([cur[0]+pos[0], cur[1]+pos[1]]); + } + } + } + // Replace the old empty stack with the newly filled one. + stack = newStack; + newStack = []; + } + +}; + +// Clears the map to just have the obstructions marked on it. +PathFinder.prototype.wipeGradient = function(){ + for (var i = 0; i < this.length; i++){ + if (this.map[i] > 0){ + this.map[i] = 65535; + } + } +}; + +// Returns the path down a gradient from the start to the bottom of the gradient, returns a point for every 20 cells in normal mode +// in entryPoints mode this returns the point where the path enters the region near the destination, currently defined +// by blockPlacementRadius. Note doesn't return a path when the destination is within the blockpoint radius. +PathFinder.prototype.walkGradient = function(start, mode){ + var positions = [[0,1], [0,-1], [1,0], [-1,0], [1,1], [-1,-1], [1,-1], [-1,1]]; + + var path = [[start[0]*this.cellSize, start[1]*this.cellSize]]; + + var blockPoint = undefined; + var blockPlacementRadius = 45; + var blockRadius = 30; + var count = 0; + + var cur = start; + var w = this.width; + var dist = this.map[cur[0] + w*cur[1]]; + var moved = false; + while (this.map[cur[0] + w*cur[1]] !== 0){ + for (var i = 0; i < positions.length; i++){ + var pos = positions[i]; + var cell = cur[0]+pos[0] + w*(cur[1]+pos[1]); + if (cell >= 0 && cell < this.length && this.map[cell] > 0 && this.map[cell] < dist){ + dist = this.map[cell]; + cur = [cur[0]+pos[0], cur[1]+pos[1]]; + moved = true; + count++; + // Mark the point to put an obstruction at before calculating the next path + if (count === blockPlacementRadius){ + blockPoint = cur; + } + // Add waypoints to the path, fairly well spaced apart. + if (count % 40 === 0){ + path.unshift([cur[0]*this.cellSize, cur[1]*this.cellSize]); + } + break; + } + } + if (!moved){ + break; + } + moved = false; + } + if (blockPoint === undefined){ + return undefined; + } + // Add an obstruction to the map at the blockpoint so the next path will take a different route. + this.addInfluence(blockPoint[0], blockPoint[1], blockRadius, -10000, 'constant'); + if (mode === 'entryPoints'){ + // returns the point where the path enters the blockPlacementRadius + return blockPoint; + }else{ + // return a path of points 20 squares apart on the route + return path; + } +}; + +// Would be used to calculate the width of a chokepoint +// NOTE: Doesn't currently work. +PathFinder.prototype.countAttached = function(pos){ + var positions = [[0,1], [0,-1], [1,0], [-1,0]]; + var w = this.width; + var val = this.map[pos[0] + w*pos[1]]; + + var stack = [pos]; + var used = {}; + + while (stack.length > 0){ + var cur = stack.pop(); + used[cur[0] + " " + cur[1]] = true; + for (var i = 0; i < positions.length; i++){ + var p = positions[i]; + var cell = cur[0]+p[0] + w*(cur[1]+p[1]); + + } + } +}; + +/* + * Accessibility inherits from TerrainAnalysis + * + * Determines whether there is a path from one point to another. It is initialised with a single point (p1) and then + * can efficiently determine if another point is reachable from p1. Initialising the object is costly so it should be + * cached. + */ + +function Accessibility(gameState, location){ + this.TerrainAnalysis(gameState); + + var start = this.findClosestPassablePoint(this.gamePosToMapPos(location)); + + // Put the value 1 in all accessible points on the map + this.floodFill(start); +} + +copyPrototype(Accessibility, TerrainAnalysis); + +// Return true if the given point is accessible from the point given when initialising the Accessibility object. # +// If the given point is impassable the closest passable point is used. +Accessibility.prototype.isAccessible = function(position){ + var s = this.findClosestPassablePoint(this.gamePosToMapPos(position)); + + return this.map[s[0] + this.width * s[1]] === 1; +}; + +// fill all of the accessible areas with value 1 +Accessibility.prototype.floodFill = function(start){ + var w = this.width; + var map = this.map; + + // Holds the list of current points to work outwards from + var stack = []; + // We store new points to be added to the stack temporarily in here while we run through the current stack + var newStack = []; + // Relative positions or new cells from the current one. + var positions = [[0,1], [0,-1], [1,0], [-1,0]]; + + // Set the start point to be accessible + map[start[0] + w*(start[1])] = 1; + stack.push(start); + + // while there are new points being added to the stack + while (stack.length > 0){ + //run through the current stack + while (stack.length > 0){ + var cur = stack.pop(); + + // Check the positions adjacent to the current cell + for (var i = 0; i < positions.length; i++){ + var pos = positions[i]; + var cell = cur[0]+pos[0] + w*(cur[1]+pos[1]); + if (cell >= 0 && cell < this.length && map[cell] > 1){ + map[cell] = 1; + newStack.push([cur[0]+pos[0], cur[1]+pos[1]]); + } + } + } + // Replace the old empty stack with the newly filled one. + stack = newStack; + newStack = []; + } +}; \ No newline at end of file diff --git a/binaries/data/mods/public/simulation/ai/qbot/walkToCC.js b/binaries/data/mods/public/simulation/ai/qbot/walkToCC.js new file mode 100644 index 0000000000..3237b2ab2d --- /dev/null +++ b/binaries/data/mods/public/simulation/ai/qbot/walkToCC.js @@ -0,0 +1,85 @@ +var WalkToCC = function(gameState, militaryManager){ + this.minAttackSize = 20; + this.maxAttackSize = 60; + this.idList=[]; +}; + +// Returns true if the attack can be executed at the current time +WalkToCC.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); +}; + +// Executes the attack plan, after this is executed the update function will be run every turn +WalkToCC.prototype.execute = function(gameState, militaryManager){ + var availableCount = militaryManager.countAvailableUnits(); + this.idList = militaryManager.getAvailableUnits(availableCount); + + var pending = EntityCollectionFromIds(gameState, this.idList); + + // 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); + }); + } + // 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"); + } + + // If we have a target, move to it + if (targets.length) { + // Remove the pending role + pending.forEach(function(ent) { + ent.setMetadata("role", "attack"); + }); + + var target = targets.toEntityArray()[0]; + var targetPos = target.position(); + + // TODO: this should be an attack-move command + pending.move(targetPos[0], targetPos[1]); + } else if (targets.length == 0 ) { + gameState.ai.gameFinished = true; + } +}; + +// Runs every turn after the attack is executed +// This removes idle units from the attack +WalkToCC.prototype.update = function(gameState, militaryManager){ + var removeList = []; + for (var idKey in this.idList){ + var id = this.idList[idKey]; + var ent = militaryManager.entity(id); + if(ent) + { + if(ent.isIdle()) { + militaryManager.unassignUnit(id); + removeList.push(id); + } + } else { + removeList.push(id); + } + } + for (var i in removeList){ + this.idList.splice(this.idList.indexOf(removeList[i]),1); + } +}; \ No newline at end of file