diff --git a/binaries/data/mods/public/simulation/ai/qbot-wc/attack_plan.js b/binaries/data/mods/public/simulation/ai/qbot-wc/attack_plan.js index 56517af3b1..41378f91d9 100755 --- a/binaries/data/mods/public/simulation/ai/qbot-wc/attack_plan.js +++ b/binaries/data/mods/public/simulation/ai/qbot-wc/attack_plan.js @@ -1,5 +1,5 @@ // basically an attack plan. The name is an artifact. -function CityAttack(gameState, militaryManager, uniqueID, targetEnemy, type , targetFinder){ +function CityAttack(gameState, militaryManager, uniqueID, targetEnemy, type , targetFinder) { //This is the list of IDs of the units in the plan this.idList=[]; @@ -35,12 +35,33 @@ function CityAttack(gameState, militaryManager, uniqueID, targetEnemy, type , ta this.onArrivalReaction = "proceedOnTargets"; + // priority is relative. If all are 0, the only relevant criteria is "currentsize/targetsize". + // if not, this is a "bonus". The higher the priority, the more this unit will get built. + // Should really be clamped to [0.1-1.5] (assuming 1 is default/the norm) + // Eg: if all are priority 1, and the siege is 0.5, the siege units will get built + // only once every other category is at least 50% of its target size. this.unitStat = {}; this.unitStat["RangedInfantry"] = { "priority" : 1, "minSize" : 4, "targetSize" : 10, "batchSize" : 5, "classes" : ["Infantry","Ranged"], "templates" : [] }; this.unitStat["MeleeInfantry"] = { "priority" : 1, "minSize" : 4, "targetSize" : 10, "batchSize" : 5, "classes" : ["Infantry","Melee"], "templates" : [] }; this.unitStat["MeleeCavalry"] = { "priority" : 1, "minSize" : 3, "targetSize" : 8 , "batchSize" : 3, "classes" : ["Cavalry","Melee"], "templates" : [] }; this.unitStat["RangedCavalry"] = { "priority" : 1, "minSize" : 3, "targetSize" : 8 , "batchSize" : 3, "classes" : ["Cavalry","Ranged"], "templates" : [] }; - this.unitStat["Siege"] = { "priority" : 1, "minSize" : 0, "targetSize" : 3 , "batchSize" : 1, "classes" : ["Siege"], "templates" : [] }; + this.unitStat["Siege"] = { "priority" : 0.5, "minSize" : 0, "targetSize" : 3 , "batchSize" : 1, "classes" : ["Siege"], "templates" : [] }; + + + if (type === "superSized") { + this.unitStat["RangedInfantry"] = { "priority" : 1, "minSize" : 5, "targetSize" : 18, "batchSize" : 5, "classes" : ["Infantry","Ranged"], "templates" : [] }; + this.unitStat["MeleeInfantry"] = { "priority" : 1, "minSize" : 6, "targetSize" : 24, "batchSize" : 5, "classes" : ["Infantry","Melee"], "templates" : [] }; + this.unitStat["MeleeCavalry"] = { "priority" : 1, "minSize" : 4, "targetSize" : 12 , "batchSize" : 5, "classes" : ["Cavalry","Melee"], "templates" : [] }; + this.unitStat["RangedCavalry"] = { "priority" : 1, "minSize" : 4, "targetSize" : 12 , "batchSize" : 5, "classes" : ["Cavalry","Ranged"], "templates" : [] }; + this.unitStat["Siege"] = { "priority" : 0.5, "minSize" : 3, "targetSize" : 6 , "batchSize" : 3, "classes" : ["Siege"], "templates" : [] }; + this.maxPreparationTime = 450*1000; + } + + /* + this.unitStat["Siege"]["filter"] = function (ent) { + var strength = [ent.attackStrengths("Melee")["crush"],ent.attackStrengths("Ranged")["crush"]]; + return (strength[0] > 15 || strength[1] > 15); + };*/ var filter = Filters.and(Filters.byMetadata("plan",this.name),Filters.byOwner(gameState.player)); this.unitCollection = gameState.getOwnEntities().filter(filter); @@ -58,7 +79,7 @@ function CityAttack(gameState, militaryManager, uniqueID, targetEnemy, type , ta var cat = unitCat; var Unit = this.unitStat[cat]; - var filter = Filters.and(Filters.byClassesAnd(Unit["classes"]),Filters.and(Filters.byMetadata("plan",this.name),Filters.byOwner(gameState.player))); + filter = Filters.and(Filters.byClassesAnd(Unit["classes"]),Filters.and(Filters.byMetadata("plan",this.name),Filters.byOwner(gameState.player))); this.unit[cat] = gameState.getOwnEntities().filter(filter); this.unit[cat].registerUpdates(); this.unit[cat].length; @@ -110,15 +131,22 @@ function CityAttack(gameState, militaryManager, uniqueID, targetEnemy, type , ta // taking this so that fortresses won't crash it for now. TODO: change the rally point if it becomes invalid if(gameState.ai.pathsToMe.length > 1) var position = [(gameState.ai.pathsToMe[0][0]+gameState.ai.pathsToMe[1][0])/2.0,(gameState.ai.pathsToMe[0][1]+gameState.ai.pathsToMe[1][1])/2.0]; - else + else if (gameState.ai.pathsToMe.length !== 0) var position = [gameState.ai.pathsToMe[0][0],gameState.ai.pathsToMe[0][1]]; + else + var position = [-1,-1]; var CCs = gameState.getOwnEntities().filter(Filters.byClass("CivCentre")); var nearestCCArray = CCs.filterNearest(position, 1).toEntityArray(); var CCpos = nearestCCArray[0].position(); this.rallyPoint = [0,0]; - this.rallyPoint[0] = (position[0]*3 + CCpos[0]) / 4.0; - this.rallyPoint[1] = (position[1]*3 + CCpos[1]) / 4.0; + if (position[0] !== -1) { + this.rallyPoint[0] = (position[0]*3 + CCpos[0]) / 4.0; + this.rallyPoint[1] = (position[1]*3 + CCpos[1]) / 4.0; + } else { + this.rallyPoint[0] = CCpos[0]; + this.rallyPoint[1] = CCpos[1]; + } if (type == 'harass_raid') { this.rallyPoint[0] = (position[0]*3.9 + 0.1 * CCpos[0]) / 4.0; @@ -132,12 +160,13 @@ function CityAttack(gameState, militaryManager, uniqueID, targetEnemy, type , ta this.threatList = []; // sounds so FBI this.tactics = undefined; - gameState.ai.queueManager.addQueue("plan_" + this.name, 130); // high priority: some may gather anyway + gameState.ai.queueManager.addQueue("plan_" + this.name, 100); // high priority: some may gather anyway this.queue = gameState.ai.queues["plan_" + this.name]; - this.assignUnits(gameState); - + + // get a good path to an estimated target. + this.pathFinder = new aStarPath(gameState,false); }; CityAttack.prototype.getName = function(){ @@ -160,6 +189,8 @@ CityAttack.prototype.canStart = function(gameState){ // TODO: check if our target is valid and a few other stuffs (good moment to attack?) }; CityAttack.prototype.isStarted = function(){ + if ((this.state !== "unexecuted")) + debug ("Attack plan already started"); return !(this.state == "unexecuted"); }; @@ -178,6 +209,8 @@ CityAttack.prototype.setPaused = function(gameState, boolValue){ } }; CityAttack.prototype.mustStart = function(gameState){ + if (this.isPaused()) + return false; var MaxReachedEverywhere = true; for (unitCat in this.unitStat) { var Unit = this.unitStat[unitCat]; @@ -191,74 +224,152 @@ CityAttack.prototype.mustStart = function(gameState){ }; // Three returns possible: 1 is "keep going", 0 is "failed plan", 2 is "start" +// 3 is a special case: no valid path returned. Right now I stop attacking alltogether. CityAttack.prototype.updatePreparation = function(gameState, militaryManager,events) { - if (this.isPaused()) - return 1; // continue + var self = this; + + if (this.path == undefined || this.target == undefined) { + // find our target + var targets = this.targetFinder(gameState, militaryManager); + if (targets.length === 0){ + targets = this.defaultTargetFinder(gameState, militaryManager); + } + if (targets.length) { + var rand = Math.floor((Math.random()*targets.length)); + this.targetPos = undefined; + var count = 0; + while (!this.targetPos){ + this.target = targets.toEntityArray()[rand]; + this.targetPos = this.target.position(); + count++; + if (count > 1000){ + debug("No target with a valid position found"); + return false; + } + } + this.path = this.pathFinder.getPath(this.rallyPoint,this.targetPos, false, 2); + if (this.path === undefined || this.path[1] === true) { + return 3; + } + this.path = this.path[0]; + } else if (targets.length == 0 ) { + gameState.ai.gameFinished = true; + debug ("I do not have any target. So I'll just assume I won the game."); + return 0; + } + } Engine.ProfileStart("Update Preparation"); - // let's sort by training advancement, ie 'current size / target size' - this.buildOrder.sort(function (a,b) { - a[0] = a[2].length/a[3]["targetSize"]; - b[0] = b[2].length/b[3]["targetSize"]; - return (a[0]) - (b[0]); - }); - - this.assignUnits(gameState); - - if ( (gameState.ai.turn + gameState.ai.player) % 40 == 0) - this.AllToRallyPoint(gameState, false); - - var canstart = this.canStart(gameState); - - Engine.ProfileStart("Creating units and looking through events"); - - // gets the number in training of the same kind as the first one. - var specialData = "Plan_"+this.name+"_"+this.buildOrder[0][4]; - var inTraining = gameState.countOwnQueuedEntitiesWithMetadata("special",specialData); - if (this.queue.countTotalQueuedUnits() + inTraining + this.buildOrder[0][2].length < Math.min(15,this.buildOrder[0][3]["targetSize"]) ) { - if (this.buildOrder[0][0] < 1 && this.queue.countTotalQueuedUnits() < 5) { - - var template = militaryManager.findBestTrainableUnit(gameState, this.buildOrder[0][1], [ ["strength",1], ["cost",1] ] ); - //debug ("tried " + uneval(this.buildOrder[0][1]) +", and " + template); - // HACK (TODO replace) : if we have no trainable template... Then we'll simply remove the buildOrder, effectively removing the unit from the plan. - if (template === undefined) { - delete this.unitStat[this.buildOrder[0][4]]; // deleting the associated unitstat. - this.buildOrder.splice(0,1); - - } else { - if (gameState.getTemplate(template).hasClasses(["CitizenSoldier", "Infantry"])) - this.queue.addItem( new UnitTrainingPlan(gameState,template, { "role" : "worker", "plan" : this.name, "special" : specialData },this.buildOrder[0][3]["batchSize"] ) ); - else - this.queue.addItem( new UnitTrainingPlan(gameState,template, { "role" : "attack", "plan" : this.name, "special" : specialData },this.buildOrder[0][3]["batchSize"] ) ); + // keep on while the units finish being trained. + if (this.mustStart(gameState) && gameState.countOwnQueuedEntitiesWithMetadata("plan", +this.name) ) { + this.assignUnits(gameState); + if ( (gameState.ai.turn + gameState.ai.player) % 40 == 0) { + this.AllToRallyPoint(gameState, true); // gain some time, start regrouping + this.unitCollection.forEach(function (entity) { entity.setMetadata("role","attack"); }); + } + Engine.ProfileStop(); + return 1; + } else if (!this.mustStart(gameState)) { + // We still have time left to recruit units and do stuffs. + + // let's sort by training advancement, ie 'current size / target size' + // count the number of queued units too. + // substract priority. + this.buildOrder.sort(function (a,b) { //}) { + + var aQueued = gameState.countOwnQueuedEntitiesWithMetadata("special","Plan_"+self.name+"_"+self.buildOrder[0][4]); + aQueued += self.queue.countTotalQueuedUnitsWithMetadata("special","Plan_"+self.name+"_"+self.buildOrder[0][4]); + a[0] = (a[2].length + aQueued)/a[3]["targetSize"]; + + var bQueued = gameState.countOwnQueuedEntitiesWithMetadata("special","Plan_"+self.name+"_"+self.buildOrder[0][4]); + bQueued += self.queue.countTotalQueuedUnitsWithMetadata("special","Plan_"+self.name+"_"+self.buildOrder[0][4]); + b[0] = (b[2].length + bQueued)/b[3]["targetSize"]; + + a[0] -= a[3]["priority"]; + b[0] -= b[3]["priority"]; + return (a[0]) - (b[0]); + }); + + if (!this.isPaused()) { + this.assignUnits(gameState); + + if ( (gameState.ai.turn + gameState.ai.player) % 40 == 0) { + this.AllToRallyPoint(gameState, false); + this.unitCollection.setStance("defensive"); // make sure units won't disperse out of control } } - } - // can happen for now - if (this.buildOrder.length === 0) { - debug ("Ending plan: no build orders"); - return 0; // will abort the plan, should return something else - } - - for (var key in events){ - var e = events[key]; - if (e.type === "Attacked" && e.msg){ - if (this.unitCollection.toIdArray().indexOf(e.msg.target) !== -1){ - var attacker = gameState.getEntityById(e.msg.attacker); - if (attacker && attacker.position()) { - this.unitCollection.attack(e.msg.attacker); - break; + + Engine.ProfileStart("Creating units."); + + // gets the number in training of the same kind as the first one. + var specialData = "Plan_"+this.name+"_"+this.buildOrder[0][4]; + var inTraining = gameState.countOwnQueuedEntitiesWithMetadata("special",specialData); + if (this.queue.countTotalQueuedUnits() + inTraining + this.buildOrder[0][2].length < Math.min(15,this.buildOrder[0][3]["targetSize"]) ) { + if (this.buildOrder[0][0] < 1 && this.queue.length() < 4) { + + var template = militaryManager.findBestTrainableUnit(gameState, this.buildOrder[0][1], [ ["strength",1], ["cost",1] ] ); + //debug ("tried " + uneval(this.buildOrder[0][1]) +", and " + template); + // HACK (TODO replace) : if we have no trainable template... Then we'll simply remove the buildOrder, effectively removing the unit from the plan. + if (template === undefined) { + delete this.unitStat[this.buildOrder[0][4]]; // deleting the associated unitstat. + this.buildOrder.splice(0,1); + + } else { + if (gameState.getTemplate(template).hasClasses(["CitizenSoldier", "Infantry"])) + this.queue.addItem( new UnitTrainingPlan(gameState,template, { "role" : "worker", "plan" : this.name, "special" : specialData },this.buildOrder[0][3]["batchSize"] ) ); + else + this.queue.addItem( new UnitTrainingPlan(gameState,template, { "role" : "attack", "plan" : this.name, "special" : specialData },this.buildOrder[0][3]["batchSize"] ) ); } } } + /* + if (!this.startedPathing && this.path === undefined) { + + // find our target + var targets = this.targetFinder(gameState, militaryManager); + if (targets.length === 0){ + targets = this.defaultTargetFinder(gameState, militaryManager); + } + if (targets.length) { + var rand = Math.floor((Math.random()*targets.length)); + this.targetPos = undefined; + var count = 0; + while (!this.targetPos){ + var target = targets.toEntityArray()[rand]; + this.targetPos = target.position(); + count++; + if (count > 1000){ + debug("No target with a valid position found"); + return false; + } + } + this.startedPathing = true; + // Start pathfinding using the optimized version, with a minimal sampling of 2 + this.pathFinder.getPath(this.rallyPoint,this.targetPos, false, 2, gameState); + } + } else if (this.startedPathing) { + var path = this.pathFinder.continuePath(gameState); + if (path !== "toBeContinued") { + this.startedPathing = false; + this.path = path; + debug("Pathing ended"); + } + } + */ + // can happen for now + if (this.buildOrder.length === 0) { + debug ("Ending plan: no build orders"); + return 0; // will abort the plan, should return something else + } + Engine.ProfileStop(); + Engine.ProfileStop(); + return 1; } Engine.ProfileStop(); - // we count our units by triggering "canStart" - // returns false if we can no longer have time and cannot start. - // returns 0 if I must start and can't, returns 1 if I don't have to start, and returns 2 if I must start and can - if (!this.mustStart(gameState)) - return 1; - else if (canstart) + // if we're here, it means we must start (and have no units in training left). + // if we can, do, else, abort. + if (this.canStart(gameState)) return 2; else return 0; @@ -268,21 +379,13 @@ CityAttack.prototype.assignUnits = function(gameState){ var self = this; // TODO: assign myself units that fit only, right now I'm getting anything. - - /* - // I'll take any unit set to "Defense" that has no subrole (ie is set to be a defensive unit, but has no particular task) - // I assign it to myself, and then it's mine, the entity collection will detect it. - var Defenders = gameState.getOwnEntitiesByRole("defence"); - Defenders.forEach(function(ent) { - if (ent.getMetadata("subrole") == "idle" || !ent.getMetadata("subrole")) { - ent.setMetadata("role", "attack"); - ent.setMetadata("plan", self.name); - } - });*/ // Assign all no-roles that fit (after a plan aborts, for example). var NoRole = gameState.getOwnEntitiesByRole(undefined); NoRole.forEach(function(ent) { - ent.setMetadata("role", "attack"); + if (ent.hasClasses(["CitizenSoldier", "Infantry"])) + ent.setMetadata("role", "worker"); + else + ent.setMetadata("role", "attack"); ent.setMetadata("plan", self.name); }); @@ -314,7 +417,10 @@ CityAttack.prototype.AllToRallyPoint = function(gameState, evenWorkers) { CityAttack.prototype.defaultTargetFinder = function(gameState, militaryManager){ var targets = undefined; - targets = militaryManager.enemyWatchers[this.targetPlayer].getEnemyBuildings("ConquestCritical"); + targets = militaryManager.enemyWatchers[this.targetPlayer].getEnemyBuildings("CivCentre"); + if (targets.length == 0) { + targets = militaryManager.enemyWatchers[this.targetPlayer].getEnemyBuildings("ConquestCritical"); + } // If there's nothing, attack anything else that's less critical if (targets.length == 0) { targets = militaryManager.enemyWatchers[this.targetPlayer].getEnemyBuildings("Town"); @@ -322,6 +428,10 @@ CityAttack.prototype.defaultTargetFinder = function(gameState, militaryManager){ if (targets.length == 0) { targets = militaryManager.enemyWatchers[this.targetPlayer].getEnemyBuildings("Village"); } + // no buildings, attack anything conquest critical, even units (it's assuming it won't move). + if (targets.length == 0) { + targets = gameState.getEnemyEntities().filter(Filters.byClass("ConquestCritical")); + } return targets; }; @@ -355,59 +465,21 @@ CityAttack.prototype.raidingTargetFinder = function(gameState, militaryManager, // If we're here, it's because we have in our IDlist enough units. // now the IDlist units are treated turn by turn CityAttack.prototype.StartAttack = function(gameState, militaryManager){ - - var targets = []; - if (this.type === "harass_raid") - targets = this.targetFinder(gameState, militaryManager, "villager"); - else - { - targets = this.targetFinder(gameState, militaryManager); - if (targets.length === 0){ - targets = this.defaultTargetFinder(gameState, militaryManager); - } - } + // check we have a target and a path. - // If we have a target, move to it - if (targets.length) { + if (this.targetPos && this.path !== undefined) { + // erase our queue. This will stop any leftover unit from being trained. + gameState.ai.queueManager.removeQueue("plan_" + this.name); + var curPos = this.unitCollection.getCentrePosition(); - // pick a random target from the list - var rand = Math.floor((Math.random()*targets.length)); - this.targetPos = undefined; - var count = 0; - while (!this.targetPos){ - var target = targets.toEntityArray()[rand]; - this.targetPos = target.position(); - count++; - if (count > 1000){ - warn("No target with a valid position found"); - return false; - } - } - - // Find possible distinct paths to the enemy - var pathFinder = new PathFinder(gameState); - var pathsToEnemy = pathFinder.getPaths(curPos, this.targetPos); - if (! pathsToEnemy){ - pathsToEnemy = [[this.targetPos]]; - } - this.path = []; - - if (this.type !== "harass_raid") - { - var rand = Math.floor(Math.random() * pathsToEnemy.length); - this.path = pathsToEnemy[rand]; - } else { - this.path = pathsToEnemy[Math.min(2,pathsToEnemy.length-1)]; - } - this.unitCollection.forEach(function(ent) { ent.setMetadata("subrole", "attacking"); ent.setMetadata("role", "attack") ;}); // filtering by those that started to attack only var filter = Filters.byMetadata("subrole","attacking"); this.unitCollection = this.unitCollection.filter(filter); - //this.unitCollection.registerUpdates(); + this.unitCollection.registerUpdates(); //this.unitCollection.length; for (unitCat in this.unitStat) { @@ -418,7 +490,7 @@ CityAttack.prototype.StartAttack = function(gameState, militaryManager){ this.unitCollection.move(this.path[0][0], this.path[0][1]); debug ("Started to attack with the plan " + this.name); this.state = "walking"; - } else if (targets.length == 0 ) { + } else { gameState.ai.gameFinished = true; debug ("I do not have any target. So I'll just assume I won the game."); return true; @@ -428,6 +500,7 @@ CityAttack.prototype.StartAttack = function(gameState, militaryManager){ // Runs every turn after the attack is executed CityAttack.prototype.update = function(gameState, militaryManager, events){ + var self = this; Engine.ProfileStart("Update Attack"); // we're marching towards the target @@ -545,10 +618,10 @@ CityAttack.prototype.update = function(gameState, militaryManager, events){ }*/ } if (this.state === "walking"){ - if (SquareVectorDistance(this.position, this.lastPosition) < 400 && this.path.length > 0) { + if (SquareVectorDistance(this.position, this.lastPosition) < 20 && this.path.length > 0) { this.unitCollection.move(this.path[0][0], this.path[0][1]); } - if (SquareVectorDistance(this.unitCollection.getCentrePosition(), this.path[0]) < 400){ + if (SquareVectorDistance(this.unitCollection.getCentrePosition(), this.path[0]) < 900){ this.path.shift(); if (this.path.length > 0){ this.unitCollection.move(this.path[0][0], this.path[0][1]); @@ -564,31 +637,10 @@ CityAttack.prototype.update = function(gameState, militaryManager, events){ // let's proceed on with whatever happens now. // There's a ton of TODOs on this part. if (this.onArrivalReaction == "proceedOnTargets") { - // Each unit will randomly pick a target and attack it and then they'll do what they feel like doing for now. TODO - // only the targeted enemy. I've seen the units attack gazelles otherwise. - var enemyUnits = gameState.getEnemyEntities().filter(Filters.and(Filters.byStaticDistance(this.unitCollection.getCentrePosition(), 100), Filters.byClass("Unit"))); - enemyUnits = enemyUnits.filter(Filters.byOwner(this.targetEnemy)); - var enemyStructures = gameState.getEnemyEntities().filter(Filters.and(Filters.byStaticDistance(this.unitCollection.getCentrePosition(), 150), Filters.byClass("Structure"))); - enemyStructures = enemyStructures.filter(Filters.byOwner(this.targetEnemy)); - enemyUnits = enemyUnits.toEntityArray(); - enemyStructures = enemyStructures.toEntityArray(); - - this.unitCollection.forEach( function (ent) { //}) { - if (ent.hasClass("Siege")) { - if (enemyStructures.length !== 0) { - var rand = Math.floor(Math.random() * enemyStructures.length*0.99); - ent.attack(enemyStructures[+rand].id()); - } else - ent.stopMoving(); - } else { - if (enemyUnits.length !== 0) { - var rand = Math.floor(Math.random() * enemyUnits.length*0.99); - ent.attack(enemyUnits[(+rand)].id()); - } else - ent.stopMoving(); - } - }); this.state = ""; + this.unitCollection.forEach( function (ent) { //}) { + ent.stopMoving(); + }); } else if (this.onArrivalReaction == "huntVillagers") { // let's get any villager and target them with a tactics manager var enemyCitizens = gameState.entities.filter(function(ent) { @@ -608,30 +660,56 @@ CityAttack.prototype.update = function(gameState, militaryManager, events){ } } } - if (this.state === ""){ + + if (this.state === "" && gameState.ai.playedTurn % 3 === 0) { // Each unit will randomly pick a target and attack it and then they'll do what they feel like doing for now. TODO // only the targeted enemy. I've seen the units attack gazelles otherwise. - var enemyUnits = gameState.getEnemyEntities().filter(Filters.and(Filters.byStaticDistance(this.unitCollection.getCentrePosition(), 200), Filters.byClass("Unit"))); - enemyUnits = enemyUnits.filter(Filters.byOwner(this.targetEnemy)); - var enemyStructures = gameState.getEnemyEntities().filter(Filters.and(Filters.byStaticDistance(this.unitCollection.getCentrePosition(), 250), Filters.byClass("Structure"))); - enemyStructures = enemyStructures.filter(Filters.byOwner(this.targetEnemy)); - enemyUnits = enemyUnits.toEntityArray(); - enemyStructures = enemyStructures.toEntityArray(); - + var enemyUnits = gameState.getEnemyEntities().filter(Filters.and(Filters.byOwner(this.targetPlayer), Filters.byClass("Unit"))); + var enemyStructures = gameState.getEnemyEntities().filter(Filters.and(Filters.byOwner(this.targetPlayer), Filters.byClass("Structure"))); this.unitCollection.forEach( function (ent) { //}) { if (ent.isIdle()) { + var mStruct = enemyStructures.filter(function (enemy) {// }){ + if (!enemy.position()) { + return false; + } + if (SquareVectorDistance(enemy.position(),ent.position()) > ent.visionRange()*ent.visionRange() + 100) { + return false; + } + return true; + }); + var mUnit = enemyUnits.filter(function (enemy) {// }){ + if (!enemy.position()) { + return false; + } + if (SquareVectorDistance(enemy.position(),ent.position()) > ent.visionRange()*ent.visionRange() + 100) { + return false; + } + return true; + }); + mUnit = mUnit.toEntityArray(); + mStruct = mStruct.toEntityArray(); if (ent.hasClass("Siege")) { - if (enemyStructures.length !== 0) { - var rand = Math.floor(Math.random() * enemyStructures.length*0.99); - ent.attack(enemyStructures[+rand].id()); - } else - ent.stopMoving(); + if (mStruct.length !== 0) { + var rand = Math.floor(Math.random() * mStruct.length*0.99); + ent.attack(mStruct[+rand].id()); + //debug ("Siege units attacking a structure from " +mStruct[+rand].owner() + " , " +mStruct[+rand].templateName()); + } else if (SquareVectorDistance(self.targetPos, ent.position()) > 900 ){ + //debug ("Siege units moving to " + uneval(self.targetPos)); + ent.move((self.targetPos[0] + ent.position()[0])/2,(self.targetPos[1] + ent.position()[1])/2); + } } else { - if (enemyUnits.length !== 0) { - var rand = Math.floor(Math.random() * enemyUnits.length*0.99); - ent.attack(enemyUnits[(+rand)].id()); - } else - ent.stopMoving(); + if (mUnit.length !== 0) { + var rand = Math.floor(Math.random() * mUnit.length*0.99); + ent.attack(mUnit[(+rand)].id()); + //debug ("Units attacking a unit from " +mUnit[+rand].owner() + " , " +mUnit[+rand].templateName()); + } else if (mStruct.length !== 0) { + var rand = Math.floor(Math.random() * mStruct.length*0.99); + ent.attack(mStruct[+rand].id()); + //debug ("Units attacking a structure from " +mStruct[+rand].owner() + " , " +mStruct[+rand].templateName()); + } else if (SquareVectorDistance(self.targetPos, ent.position()) > 900 ){ + //debug ("Units moving to " + uneval(self.targetPos)); + ent.move((self.targetPos[0] + ent.position()[0])/2,(self.targetPos[1] + ent.position()[1])/2); + } } } }); @@ -654,6 +732,7 @@ CityAttack.prototype.update = function(gameState, militaryManager, events){ }*/ this.lastPosition = this.position; Engine.ProfileStop(); + return this.unitCollection.length; }; CityAttack.prototype.totalCountUnits = function(gameState){ diff --git a/binaries/data/mods/public/simulation/ai/qbot-wc/config.js b/binaries/data/mods/public/simulation/ai/qbot-wc/config.js index e7cc7f37b6..e53883b9ca 100644 --- a/binaries/data/mods/public/simulation/ai/qbot-wc/config.js +++ b/binaries/data/mods/public/simulation/ai/qbot-wc/config.js @@ -43,13 +43,13 @@ var baseConfig = { // qbot "priorities" : { // Note these are dynamic, you are only setting the initial values "house" : 500, - "citizenSoldier" : 100, - "villager" : 150, - "economicBuilding" : 50, + "citizenSoldier" : 65, + "villager" : 95, + "economicBuilding" : 80, "field" : 20, "advancedSoldier" : 30, "siege" : 10, - "militaryBuilding" : 80, + "militaryBuilding" : 90, "defenceBuilding" : 17, "civilCentre" : 1000 }, @@ -58,7 +58,7 @@ var baseConfig = { }; var Config = { - "debug": false + "debug": true }; Config.__proto__ = baseConfig; \ No newline at end of file diff --git a/binaries/data/mods/public/simulation/ai/qbot-wc/defence.js b/binaries/data/mods/public/simulation/ai/qbot-wc/defence.js index 9f890f15ac..2e50a2c4a6 100755 --- a/binaries/data/mods/public/simulation/ai/qbot-wc/defence.js +++ b/binaries/data/mods/public/simulation/ai/qbot-wc/defence.js @@ -28,12 +28,11 @@ function Defence(){ // This allows the Defence manager to take units from the plans for Defence. // Defcon levels -// 6 (or more): no danger whatsoever detected -// 5: local zones of danger (ie a tower somewhere, things like that) -// 4: a few enemy units inbound, like a scout or something. (local danger). Usually seen as the last level before a true "attack" -// 3: reasonnably sized enemy army inbound, local danger -// 2: well sized enemy army inbound, general danger -// 1: Sizable enemy army inside of my base, general danger. +// 5: no danger whatsoever detected +// 4: a few enemy units are being dealt with, but nothing too dangerous. +// 3: A reasonnably sized enemy army is being dealt with, but it should not be a problem. +// 2: A big enemy army is in the base, but we are not outnumbered +// 1: Huge army in the base, outnumbering us. Defence.prototype.update = function(gameState, events, militaryManager){ @@ -150,6 +149,7 @@ Defence.prototype.defendFromEnemyArmies = function(gameState, events, militaryMa var nbOfAttackers = 0; + var newEnemies = []; // clean up before adding new units (slight speeding up, since new units can't already be dead) for (i in this.listOfEnemies) { if (this.listOfEnemies[i].length === 0) { @@ -164,6 +164,9 @@ Defence.prototype.defendFromEnemyArmies = function(gameState, events, militaryMa var enemyWatcher = militaryManager.enemyWatchers[unit.owner()]; if (enemyWatcher.isPartOfDangerousArmy(unit.id())) { nbOfAttackers++; + if (this.attackerCache[unit.id()].length == 0) { + newEnemies.push(unit); + } } else { // if we had defined the attackerCache, ie if we had tried to attack this unit. if (this.attackerCache[unit.id()] != undefined) { @@ -175,7 +178,6 @@ Defence.prototype.defendFromEnemyArmies = function(gameState, events, militaryMa } } } - // okay so now, for every dangerous armies, we loop. for (armyID in dangerArmies) { // looping through army units @@ -196,6 +198,7 @@ Defence.prototype.defendFromEnemyArmies = function(gameState, events, militaryMa self.attackerCache[ent.id()] = self.myUnits.filter(filter); self.attackerCache[ent.id()].registerUpdates(); nbOfAttackers++; + newEnemies.push(ent); } }); } @@ -214,46 +217,77 @@ Defence.prototype.defendFromEnemyArmies = function(gameState, events, militaryMa militaryManager.unpauseAllPlans(gameState); return; } + // If I'm here, I have a list of enemy units, and a list of my units attacking it (in absolute terms, I could use a list of any unit attacking it). // now I'll list my idle defenders, then my idle soldiers that could defend. // and then I'll assign my units. // and then rock on. - /* - if (nbOfAttackers === 0) { - return; - } else if (nbOfAttackers < 5){ - gameState.upDefcon(4); // few local units - } else if (nbOfAttackers >= 5){ - gameState.upDefcon(3); // local attack, dangerous but not hugely threatening for my survival + + if (nbOfAttackers < 10){ + gameState.setDefcon(4); // few local units + } else if (nbOfAttackers >= 10){ + gameState.setDefcon(3); } - if (this.idleDefs.length < nbOfAttackers) { - gameState.upDefcon(2); // general danger - } - */ + var nonDefenders = this.myUnits.filter(Filters.or( Filters.not(Filters.byMetadata("role","defence")),Filters.isIdle())); + nonDefenders = nonDefenders.filter(Filters.not(Filters.byClass("Female"))); - // todo: improve the logic against attackers. + var defenceRatio = this.defenceRatio; + if (newEnemies.length * defenceRatio> nonDefenders.length) { + defenceRatio = 1; + } + + // For each enemy, we'll pick two units. + for each (enemy in newEnemies) { + if (nonDefenders.length === 0) + break; - // reupdate the existing defenders. - - this.idleDefs.forEach(function(ent) { - ent.setMetadata("subrole","newdefender"); - }); + var assigned = self.attackerCache[enemy.id()].length; + if (assigned >= defenceRatio) + return; - - - nbOfAttackers *= this.defenceRatio; - // Assume those taken care of. - nbOfAttackers -= +(this.defenders.length); - - // need new units? - if (nbOfAttackers <= 0) - return; - + // let's check for a counter. + //debug ("Enemy is a " + uneval(enemy._template.Identity.Classes._string) ); + var potCounters = gameState.ai.templateManager.getCountersToClasses(gameState,enemy.classes(),enemy.templateName()); + //debug ("Counters are" +uneval(potCounters)); + var counters = []; + for (o in potCounters) { + var counter = nonDefenders.filter(Filters.and(Filters.byType(potCounters[o][0]), Filters.byStaticDistance(enemy.position(), 150) )).toEntityArray(); + if (counter.length !== 0) + for (unit in counter) + counters.push(counter[unit]); + } + //debug ("I have " +counters.length +"countering units"); + for (var i = 0; i < defenceRatio && i < counters.length; i++) { + if (counters[i].getMetadata("plan") !== undefined) + militaryManager.pausePlan(gameState,counters[i].getMetadata("plan")); + counters[i].setMetadata("formerrole", counters[i].getMetadata("role")); + counters[i].setMetadata("role","defence"); + counters[i].setMetadata("subrole","defending"); + counters[i].attack(+enemy.id()); + nonDefenders.updateEnt(counters[i]); + assigned++; + //debug ("Sending a " +counters[i].templateName() +" to counter a " + enemy.templateName()); + } + if (assigned !== defenceRatio) { + // take closest units + nonDefenders.filter(Filters.byClass("CitizenSoldier")).filterNearest(enemy.position(),defenceRatio-assigned).forEach(function (defender) { //}){ + if (defender.getMetadata("plan") !== undefined) + militaryManager.pausePlan(gameState,defender.getMetadata("plan")); + defender.setMetadata("formerrole", defender.getMetadata("role")); + defender.setMetadata("role","defence"); + defender.setMetadata("subrole","defending"); + defender.attack(+enemy.id()); + nonDefenders.updateEnt(defender); + assigned++; + }); + } + } + /* // yes. We'll pick new units (pretty randomly for now, todo) // first from attack plans, then from workers. var newSoldiers = gameState.getOwnEntities().filter(function (ent) { - if (ent.getMetadata("plan") != undefined) + if (ent.getMetadata("plan") != undefined && ent.getMetadata("role") != "defence") return true; return false; }); @@ -287,6 +321,15 @@ Defence.prototype.defendFromEnemyArmies = function(gameState, events, militaryMa } // okay newSoldiers = gameState.getOwnEntitiesByMetadata("subrole","newdefender"); + + // we're okay, but there's a big amount of units + // todo: check against total number of soldiers + if (nbOfAttackers <= 0 && newSoldiers.length > 35) + gameState.setDefcon(2); + else if (nbOfAttackers > 0) { + // we are actually lacking units + gameState.setDefcon(1); + } // TODO. For now, each unit will pick the closest unit that is attacked by only one/zero guy, or any if there is none. // ought to regroup them first for optimization. @@ -304,7 +347,7 @@ Defence.prototype.defendFromEnemyArmies = function(gameState, events, militaryMa ent.setMetadata("subrole","defending"); ent.attack(+target); }); - +*/ return; } @@ -324,6 +367,11 @@ Defence.prototype.MessageProcess = function(gameState,events, militaryManager) { // if we're not on enemy territory var territory = +this.territoryMap.point(attacker.position()) - 64; + // we do not consider units that are defenders, and we do not consider units that are part of an attacking attack plan + // (attacking attacking plans are dealing with threats on their own). + if (ourUnit !== undefined && (ourUnit.getMetadata("role") == "defence" || ourUnit.getMetadata("subrole") == "attacking")) + continue; + // let's check for animals if (attacker.owner() == 0) { // if our unit is still alive, we make it react @@ -371,7 +419,7 @@ Defence.prototype.MessageProcess = function(gameState,events, militaryManager) { this.WantedUnitsAttacker[attacker.id()].registerUpdates(); this.WantedUnitsAttacker[attacker.id()].length; } - if (ourUnit && ourUnit.hasClass("Unit") && ourUnit.getMetadata("role") != "attack") { + if (ourUnit && ourUnit.hasClass("Unit")) { if (ourUnit.hasClass("Support")) { // TODO: it's a villager. Garrison it. // TODO: make other neighboring villagers garrison @@ -393,7 +441,7 @@ Defence.prototype.MessageProcess = function(gameState,events, militaryManager) { } } }; -// At most, this will put defcon to 5 +// At most, this will put defcon to 4 Defence.prototype.DealWithWantedUnits = function(gameState, events, militaryManager) { //if (gameState.defcon() < 3) // return; @@ -424,8 +472,8 @@ Defence.prototype.DealWithWantedUnits = function(gameState, events, militaryMana if (nbOfAttackers === 0) return; - // at most, we'll deal with two enemies at once. - if (nbOfDealtWith >= 2) + // at most, we'll deal with 3 enemies at once. + if (nbOfDealtWith >= 3) return; // dynamic properties are not updated nearly fast enough here so a little caching @@ -434,11 +482,14 @@ Defence.prototype.DealWithWantedUnits = function(gameState, events, militaryMana // we send 3 units to each target just to be sure. TODO refine. // we do not use plan units this.idleDefs.forEach(function(ent) { - if (nbOfDealtWith < 2 && nbOfAttackers > 0 && ent.getMetadata("plan") == undefined) + if (nbOfDealtWith < 3 && nbOfAttackers > 0 && ent.getMetadata("plan") == undefined) for (o in self.listOfWantedUnits) { if ( (addedto[o] == undefined && self.WantedUnitsAttacker[o].length < 3) || (addedto[o] && self.WantedUnitsAttacker[o].length + addedto[o] < 3)) { if (self.WantedUnitsAttacker[o].length === 0) nbOfDealtWith++; + + ent.setMetadata("formerrole", ent.getMetadata("role")); + ent.setMetadata("role","defence"); ent.setMetadata("subrole", "defending"); ent.attack(+o); if (addedto[o]) @@ -455,17 +506,19 @@ Defence.prototype.DealWithWantedUnits = function(gameState, events, militaryMana // still some undealt with attackers, recruit citizen soldiers if (nbOfAttackers > 0 && nbOfDealtWith < 2) { - //gameState.upDefcon(5); + gameState.setDefcon(4); var newSoldiers = gameState.getOwnEntitiesByRole("worker"); newSoldiers.forEach(function(ent) { // If we're not female, we attack if (ent.hasClass("CitizenSoldier")) - if (nbOfDealtWith < 2 && nbOfAttackers > 0) + if (nbOfDealtWith < 3 && nbOfAttackers > 0) for (o in self.listOfWantedUnits) { if ( (addedto[o] == undefined && self.WantedUnitsAttacker[o].length < 3) || (addedto[o] && self.WantedUnitsAttacker[o].length + addedto[o] < 3)) { if (self.WantedUnitsAttacker[o].length === 0) nbOfDealtWith++; + ent.setMetadata("formerrole", ent.getMetadata("role")); + ent.setMetadata("role","defence"); ent.setMetadata("subrole", "defending"); ent.attack(+o); if (addedto[o]) diff --git a/binaries/data/mods/public/simulation/ai/qbot-wc/economy.js b/binaries/data/mods/public/simulation/ai/qbot-wc/economy.js index 03a996a2be..d394f3b196 100644 --- a/binaries/data/mods/public/simulation/ai/qbot-wc/economy.js +++ b/binaries/data/mods/public/simulation/ai/qbot-wc/economy.js @@ -7,15 +7,27 @@ var EconomyManager = function() { this.setCount = 0; //stops villagers being reassigned to other resources too frequently, count a set number of //turns before trying to reassign them. + // this means we'll have about a big third of women, and thus we can maximize resource gathering rates. this.femaleRatio = 0.4; this.farmingFields = false; - this.dropsiteNumbers = {wood: 2, stone: 1, metal: 1}; + this.dropsiteNumbers = {"wood": 1, "stone": 0.5, "metal": 0.5}; }; // More initialisation for stuff that needs the gameState EconomyManager.prototype.init = function(gameState){ - this.targetNumWorkers = Math.max(Math.floor(gameState.getPopulationMax()/2.5), 1); + this.targetNumWorkers = Math.max(Math.floor(gameState.getPopulationMax()*0.55), 1); + + // initialize once all the resource maps. + this.updateResourceMaps(gameState, ["food","wood","stone","metal"]); + this.updateResourceConcentrations(gameState,"food"); + this.updateResourceConcentrations(gameState,"wood"); + this.updateResourceConcentrations(gameState,"stone"); + this.updateResourceConcentrations(gameState,"metal"); + this.updateNearbyResources(gameState, "food"); + this.updateNearbyResources(gameState, "wood"); + this.updateNearbyResources(gameState, "stone"); + this.updateNearbyResources(gameState, "metal"); }; // okay, so here we'll create both females and male workers. @@ -29,26 +41,45 @@ EconomyManager.prototype.trainMoreWorkers = function(gameState, queues) { // Count the workers in the world and in progress var numFemales = gameState.countEntitiesAndQueuedByType(gameState.applyCiv("units/{civ}_support_female_citizen")); numFemales += queues.villager.countTotalQueuedUnits(); - var numWorkers = gameState.countOwnEntitiesAndQueuedWithRole("worker"); - numWorkers += queues.citizenSoldier.countTotalQueuedUnits(); - var numTotal = numWorkers + queues.villager.countTotalQueuedUnits() + queues.citizenSoldier.countTotalQueuedUnits(); + // counting the workers that aren't part of a plan + var numWorkers = 0; + gameState.getOwnEntities().forEach (function (ent) { + if (ent.getMetadata("role") == "worker" && ent.getMetadata("plan") == undefined) + numWorkers++; + }); + gameState.getOwnTrainingFacilities().forEach(function(ent) { + ent.trainingQueue().forEach(function(item) { + if (item.metadata && item.metadata.role == "worker" && item.metadata.plan == undefined) + numWorkers += item.count; + }); + }); + var numQueued = queues.villager.countTotalQueuedUnits() + queues.citizenSoldier.countTotalQueuedUnits(); + var numTotal = numWorkers + numQueued; + + this.targetNumFields = numFemales/15; + if ((gameState.ai.playedTurn+2) % 3 === 0) { + this.dropsiteNumbers = {"wood": Math.ceil((numWorkers)/25)/2, "stone": Math.ceil((numWorkers)/40)/2, "metal": Math.ceil((numWorkers)/30)/2}; + } + + //debug (numTotal + "/" +this.targetNumWorkers + ", " +numFemales +"/" +numTotal); + // If we have too few, train more - if (numTotal < this.targetNumWorkers && (queues.villager.countTotalQueuedUnits() < 10 || queues.citizenSoldier.countTotalQueuedUnits() < 10) ) { - var template = "units/{civ}_support_female_citizen"; - var size = 1; - if (numFemales/numWorkers > this.femaleRatio && gameState.getTimeElapsed() > 60*1000) { + // should plan enough to always have females… + if (numTotal < this.targetNumWorkers && numQueued < 20) { + var template = gameState.applyCiv("units/{civ}_support_female_citizen"); + var size = Math.min(Math.ceil(gameState.getTimeElapsed() / 240000),5); + if (numFemales/numTotal > this.femaleRatio && gameState.getTimeElapsed() > 60*1000) { + var size = Math.min(Math.ceil(gameState.getTimeElapsed() / 120000),5); template = this.findBestTrainableUnit(gameState, ["CitizenSoldier", "Infantry"], [ ["cost",1], ["speed",0.5]]); - size = 5; if (!template) { - template = "units/{civ}_support_female_citizen"; - size = 1; + template = gameState.applyCiv("units/{civ}_support_female_citizen"); } } - if (size == 5) - queues.citizenSoldier.addItem(new UnitTrainingPlan(gameState, template, { "role" : "worker" },size )); - else + if (template === gameState.applyCiv("units/{civ}_support_female_citizen")) queues.villager.addItem(new UnitTrainingPlan(gameState, template, { "role" : "worker" },size )); + else + queues.citizenSoldier.addItem(new UnitTrainingPlan(gameState, template, { "role" : "worker" },size )); } }; // picks the best template based on parameters and classes @@ -101,7 +132,7 @@ EconomyManager.prototype.pickMostNeededResources = function(gameState) { } var numGatherers = {}; - for ( var type in this.gatherWeights){ + for (type in this.gatherWeights){ numGatherers[type] = gameState.updatingCollection("workers-gathering-" + type, Filters.byMetadata("gather-type", type), gameState.getOwnEntitiesByRole("worker")).length; } @@ -200,44 +231,77 @@ 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 + + // up to 2.5 buildings at once (that is 3, but one won't be complete). - var foundations = gameState.getOwnFoundations(); + var foundations = gameState.getOwnFoundations().toEntityArray(); + var damagedBuildings = gameState.getOwnEntities().filter(function (ent) { if (ent.needsRepair() && ent.getMetadata("plan") == undefined) { return true; } return false; }).toEntityArray(); // Check if nothing to build - if (!foundations.length){ + if (!foundations.length && !damagedBuildings.length){ return; } var workers = gameState.getOwnEntitiesByRole("worker"); - var builderWorkers = this.workersBySubrole(gameState, "builder"); - // Check if enough builders - var extraNeeded = this.targetNumBuilders*foundations.length - builderWorkers.length; - if (extraNeeded <= 0){ - return; + var addedWorkers = 0; + + for (i in foundations) { + var target = foundations[i]; + if (target._template.BuildRestrictions.Category === "Field") + continue; // we do not build fields + var assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target).length; + if (assigned < this.targetNumBuilders) { + if (builderWorkers.length + addedWorkers < this.targetNumBuilders*Math.min(2.5,gameState.getTimeElapsed()/60000)) { + var nonBuilderWorkers = workers.filter(function(ent) { return (ent.getMetadata("subrole") !== "builder" && ent.getMetadata("gather-type") !== "food" && ent.position() !== undefined); }); + var nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), this.targetNumBuilders - assigned); + + nearestNonBuilders.forEach(function(ent) { + addedWorkers++; + ent.setMetadata("subrole", "builder"); + ent.setMetadata("target-foundation", target); + }); + if (this.targetNumBuilders - assigned - nearestNonBuilders.length > 0) { + var nonBuilderWorkers = workers.filter(function(ent) { return (ent.getMetadata("subrole") !== "builder" && ent.position() !== undefined); }); + var nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), this.targetNumBuilders - assigned); + nearestNonBuilders.forEach(function(ent) { + addedWorkers++; + ent.setMetadata("subrole", "builder"); + ent.setMetadata("target-foundation", target); + }); + } + } + } + } + // don't repair if we're still under attack, unless it's like a vital (civcentre or wall) building that's getting destroyed. + for (i in damagedBuildings) { + var target = damagedBuildings[i]; + if (gameState.defcon() < 5) { + if (target.healthLevel() > 0.5 || !target.hasClass("CivCentre") || !target.hasClass("StoneWall")) { + continue; + } + } + var assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target).length; + if (assigned < this.targetNumBuilders) { + if (builderWorkers.length + addedWorkers < this.targetNumBuilders*2.5) { + + var nonBuilderWorkers = workers.filter(function(ent) { return (ent.getMetadata("subrole") !== "builder" && ent.position() !== undefined); }); + if (gameState.defcon() < 5) + nonBuilderWorkers = workers.filter(function(ent) { return (ent.getMetadata("subrole") !== "builder" && ent.hasClass("Female") && ent.position() !== undefined); }); + var nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), this.targetNumBuilders - assigned); + + nearestNonBuilders.forEach(function(ent) { + addedWorkers++; + ent.setMetadata("subrole", "builder"); + ent.setMetadata("target-foundation", target); + }); + } + } } - - // 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.setMetadata("subrole", "builder"); - ent.setMetadata("target-foundation", target); - }); }; EconomyManager.prototype.buildMoreFields = function(gameState, queues) { - if (this.farmingFields) { + if (this.farmingFields === true) { var numFarms = gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_field")); numFarms += queues.field.countTotalQueuedUnits(); @@ -245,12 +309,13 @@ EconomyManager.prototype.buildMoreFields = function(gameState, queues) { queues.field.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_field")); } else { var foodAmount = 0; - gameState.updatingCollection("active-dropsite-food", Filters.byMetadata("active-dropsite-food", true), - gameState.getOwnDropsites("food")).forEach(function (dropsite){ - dropsite.getMetadata("nearby-resources-food").forEach(function (supply) { - foodAmount += supply.resourceSupplyAmount(); - }); - }); + gameState.getOwnDropsites("food").forEach( function (ent) { //}){ + if (ent.getMetadata("resource-quantity-food") != undefined) { + foodAmount += ent.getMetadata("resource-quantity-food"); + } else { + foodAmount = 300; // wait till we initialize + } + }); if (foodAmount < 300) this.farmingFields = true; } @@ -266,13 +331,22 @@ EconomyManager.prototype.buildNewCC= function(gameState, queues) { } }; -//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':9, 'stone': 10, 'metal': 10, 'food': 12}; +// creates and maintains a map of unused resource density +// this also takes dropsites into account. +// resources that are "part" of a dropsite are not counted. +EconomyManager.prototype.updateResourceMaps = function(gameState, events) { + // TODO: centralize with that other function that uses the same variables + // The weight of the influence function is amountOfResource/decreaseFactor + var decreaseFactor = {'wood': 12.0, 'stone': 10.0, 'metal': 10.0, 'food': 20.0}; + // This is the maximum radius of the influence + var radius = {'wood':25.0, 'stone': 24.0, 'metal': 24.0, 'food': 24.0}; + // smallRadius is the distance necessary to mark a resource as linked to a dropsite. + var smallRadius = { 'food':70*70,'wood':120*120,'stone':60*60,'metal':60*60 }; + // bigRadius is the distance for a weak link (resources are considered when building other dropsites) + // and their resource amount is divided by 3 when checking for dropsite resource level. + var bigRadius = { 'food':100*100,'wood':180*180,'stone':120*120,'metal':120*120 }; + var self = this; for (var resource in radius){ @@ -291,25 +365,84 @@ EconomyManager.prototype.updateResourceMaps = function(gameState, events){ self.resourceMaps[resource].addInfluence(x, z, radius[resource], strength); }); } - // TODO: fix for treasure and move out of loop - // 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.entityObj){ - var ent = e.msg.entityObj; - if (ent && ent.position() && ent.resourceSupplyType() && ent.resourceSupplyType().generic === resource){ + } + // Look for destroy events and subtract the entities original influence from the resourceMap + // also look for dropsite destruction and add the associated entities (along with unmarking them) + for (var i in events) { + var e = events[i]; + if (e.type === "Destroy") { + + if (e.msg.entityObj){ + var ent = e.msg.entityObj; + if (ent && ent.position() && ent.resourceSupplyType() && ent.resourceSupplyType().generic !== "treasure") { + if (e.msg.metadata[gameState.getPlayerID()] && !e.msg.metadata[gameState.getPlayerID()]["linked-dropsite"]) { + var resource = ent.resourceSupplyType().generic; 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], -strength); } } - }else if (e.type === "Create") { - if (e.msg.entityObj){ - var ent = e.msg.entityObj; - if (ent && ent.position() && ent.resourceSupplyType() && ent.resourceSupplyType().generic === resource){ + if (ent && ent.owner() == gameState.player && ent.resourceDropsiteTypes() !== undefined) { + var resources = ent.resourceDropsiteTypes(); + for (i in resources) { + var resource = resources[i]; + // loop through all dropsites to see if the resources of his entity collection could + // be taken over by another dropsite + var dropsites = gameState.getOwnDropsites(resource); + var metadata = e.msg.metadata[gameState.getPlayerID()]; + metadata["linked-resources-" + resource].filter( function (supply) { //}){ + var takenOver = false; + dropsites.forEach( function (otherDropsite) { //}) { + var distance = SquareVectorDistance(supply.position(), otherDropsite.position()); + if (supply.getMetadata("linked-dropsite") == undefined || supply.getMetadata("linked-dropsite-dist") > distance) { + if (distance < bigRadius[resource]) { + supply.setMetadata("linked-dropsite", otherDropsite.id() ); + supply.setMetadata("linked-dropsite-dist", +distance); + if (distance < smallRadius[resource]) { + takenOver = true; + supply.setMetadata("linked-dropsite-nearby", true ); + } else { + supply.setMetadata("linked-dropsite-nearby", false ); + } + } + } + }); + if (!takenOver) { + var x = Math.round(supply.position()[0] / gameState.cellSize); + var z = Math.round(supply.position()[1] / gameState.cellSize); + var strength = Math.round(supply.resourceSupplyMax()/decreaseFactor[resource]); + self.resourceMaps[resource].addInfluence(x, z, radius[resource], strength); + } + }); + } + } + } + } else if (e.type === "Create") { + if (e.msg.entityObj){ + var ent = e.msg.entityObj; + if (ent && ent.position() && ent.resourceSupplyType() && ent.resourceSupplyType().generic !== "treasure"){ + var resource = ent.resourceSupplyType().generic; + + var addToMap = true; + var dropsites = gameState.getOwnDropsites(resource); + dropsites.forEach( function (otherDropsite) { //}) { + var distance = SquareVectorDistance(ent.position(), otherDropsite.position()); + if (ent.getMetadata("linked-dropsite") == undefined || ent.getMetadata("linked-dropsite-dist") > distance) { + if (distance < bigRadius[resource]) { + if (distance < smallRadius[resource]) { + if (ent.getMetadata("linked-dropsite") == undefined) + addToMap = false; + ent.setMetadata("linked-dropsite-nearby", true ); + } else { + ent.setMetadata("linked-dropsite-nearby", false ); + } + ent.setMetadata("linked-dropsite", otherDropsite.id() ); + ent.setMetadata("linked-dropsite-dist", +distance); + } + } + }); + if (addToMap) { 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]); @@ -318,140 +451,197 @@ EconomyManager.prototype.updateResourceMaps = function(gameState, events){ } } } - } - + } //this.resourceMaps['wood'].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; + friendlyTiles.add(this.resourceMaps[resource]); + + for (i in this.resourceMaps) + if (i !== "food") + friendlyTiles.multiply(this.resourceMaps[i],true,100,1.5); + + //friendlyTiles.dumpIm(gameState.getTimeElapsed() + "_" + resource + "_density_fade_base.png", 65000); + + var territory = Map.createTerritoryMap(gameState); + friendlyTiles.multiplyTerritory(gameState,territory); + + var resources = ["wood","stone","metal"]; + for (i in resources) { + gameState.getOwnDropsites(resources[i]).forEach(function(ent) { //)){ + // We don't want multiple dropsites at one spot so set to zero if too close. 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); - + friendlyTiles.setInfluence(x, z, 17, 0); + }); + } + //friendlyTiles.dumpIm(gameState.getTimeElapsed() + "_" + resource + "_density_fade_final.png", 10000); + friendlyTiles.multiply(gameState.ai.distanceFromMeMap,true,gameState.ai.distanceFromMeMap.width/3,2); + //friendlyTiles.dumpIm(gameState.getTimeElapsed() + "_" + resource + "_density_fade_final2.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 bestIdx = friendlyTiles.findBestTile(2, obstructions)[0]; 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 (var 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); - } - }); + if (territory.getOwner([x,z]) === 0) { + bestIdx = friendlyTiles.findBestTile(4, obstructions)[0]; + x = ((bestIdx % friendlyTiles.width) + 0.5) * gameState.cellSize; + z = (Math.floor(bestIdx / friendlyTiles.width) + 0.5) * gameState.cellSize; + return [true, [x,z]]; } + return [false, [x,z]]; +}; +EconomyManager.prototype.updateResourceConcentrations = function(gameState, resource){ + var self = this; + gameState.getOwnDropsites(resource).forEach(function(dropsite) { //}){ + var amount = 0; + var amountFar = 0; + if (dropsite.getMetadata("linked-resources-" + resource) == undefined) + return; + dropsite.getMetadata("linked-resources-" + resource).forEach(function(supply){ //}){ + if (supply.getMetadata("full") == true) + return; + if (supply.getMetadata("linked-dropsite-nearby") == true) + amount += supply.resourceSupplyAmount(); + else + amountFar += supply.resourceSupplyAmount(); + supply.setMetadata("dp-update-value",supply.resourceSupplyAmount()); + }); + dropsite.setMetadata("resource-quantity-" + resource, amount); + dropsite.setMetadata("resource-quantity-far-" + resource, amountFar); + }); }; // Stores lists of nearby resources -EconomyManager.prototype.updateNearbyResources = function(gameState){ +EconomyManager.prototype.updateNearbyResources = function(gameState,resource){ var self = this; var resources = ["food", "wood", "stone", "metal"]; var resourceSupplies; - var radius = 100; - for (var key in resources){ - var resource = resources[key]; + + // TODO: centralize with that other function that uses the same variables + // The weight of the influence function is amountOfResource/decreaseFactor + var decreaseFactor = {'wood': 12.0, 'stone': 10.0, 'metal': 10.0, 'food': 20.0}; + // This is the maximum radius of the influence + var radius = {'wood':25.0, 'stone': 24.0, 'metal': 24.0, 'food': 24.0}; + // smallRadius is the distance necessary to mark a resource as linked to a dropsite. + var smallRadius = { 'food':80*80,'wood':60*60,'stone':70*70,'metal':70*70 }; + // bigRadius is the distance for a weak link (resources are considered when building other dropsites) + // and their resource amount is divided by 3 when checking for dropsite resource level. + var bigRadius = { 'food':140*140,'wood':140*140,'stone':140*140,'metal':140*140 }; + + gameState.getOwnDropsites(resource).forEach(function(ent) { //}){ - gameState.getOwnDropsites(resource).forEach(function(ent) { - if (ent.getMetadata("nearby-resources-" + resource) === undefined){ - var filterPos = Filters.byStaticDistance(ent.position(), radius); + if (ent.getMetadata("nearby-resources-" + resource) === undefined){ + // let's defined the entity collections (by metadata) + gameState.getResourceSupplies(resource).filter( function (supply) { //}){ + var distance = SquareVectorDistance(supply.position(), ent.position()); + // if we're close than the current linked-dropsite, or if it's not linked + // TODO: change when actualy resource counting is implemented. - var collection = gameState.getResourceSupplies(resource).filter(filterPos); - collection.registerUpdates(); - - ent.setMetadata("nearby-resources-" + resource, collection); - ent.setMetadata("active-dropsite-" + resource, true); - } + if (supply.getMetadata("linked-dropsite") == undefined || supply.getMetadata("linked-dropsite-dist") > distance) { + if (distance < bigRadius[resource]) { + if (distance < smallRadius[resource]) { + // it's new to the game, remove it from the resource maps + if (supply.getMetadata("linked-dropsite") == undefined || supply.getMetadata("linked-dropsite-nearby") == false) { + var x = Math.round(supply.position()[0] / gameState.cellSize); + var z = Math.round(supply.position()[1] / gameState.cellSize); + var strength = Math.round(supply.resourceSupplyMax()/decreaseFactor[resource]); + self.resourceMaps[resource].addInfluence(x, z, radius[resource], -strength); + } + supply.setMetadata("linked-dropsite-nearby", true ); + } else { + supply.setMetadata("linked-dropsite-nearby", false ); + } + supply.setMetadata("linked-dropsite", ent.id() ); + supply.setMetadata("linked-dropsite-dist", +distance); + } + } + }); + // This one is both for the nearby and the linked + var filter = Filters.byMetadata("linked-dropsite", ent.id()); + var collection = gameState.getResourceSupplies(resource).filter(filter); + collection.registerUpdates(); + ent.setMetadata("linked-resources-" + resource, collection); - if (ent.getMetadata("nearby-resources-" + resource).length === 0){ - ent.setMetadata("active-dropsite-" + resource, false); - }else{ - ent.setMetadata("active-dropsite-" + resource, true); - } - /* - // Make resources glow wildly - if (resource == "food"){ - ent.getMetadata("nearby-resources-" + resource).forEach(function(ent){ - Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [10,0,0]}); - }); - } - if (resource == "wood"){ - ent.getMetadata("nearby-resources-" + resource).forEach(function(ent){ - Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,10,0]}); - }); - } - if (resource == "metal"){ - ent.getMetadata("nearby-resources-" + resource).forEach(function(ent){ - Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,0,10]}); - }); - }*/ - }); - } + filter = Filters.byMetadata("linked-dropsite-nearby",true); + var collection2 = collection.filter(filter); + collection2.registerUpdates(); + ent.setMetadata("nearby-resources-" + resource, collection2); + + } + + /* + // Make resources glow wildly + if (resource == "food"){ + ent.getMetadata("linked-resources-" + resource).forEach(function(ent){ + Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [1,0,0]}); + }); + ent.getMetadata("nearby-resources-" + resource).forEach(function(ent){ + Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [10,0,0]}); + }); + } + if (resource == "wood"){ + ent.getMetadata("linked-resources-" + resource).forEach(function(ent){ + Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,1,0]}); + }); + ent.getMetadata("nearby-resources-" + resource).forEach(function(ent){ + Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,10,0]}); + }); + } + if (resource == "metal"){ + ent.getMetadata("linked-resources-" + resource).forEach(function(ent){ + Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,0,1]}); + }); + ent.getMetadata("nearby-resources-" + resource).forEach(function(ent){ + Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,0,10]}); + }); + } + if (resource == "stone"){ + ent.getMetadata("linked-resources-" + resource).forEach(function(ent){ + Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,0.5,1]}); + }); + ent.getMetadata("nearby-resources-" + resource).forEach(function(ent){ + Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,5,10]}); + }); + }*/ + }); }; //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: 1400, stone: 200, metal: 200}; + //TODO: make these values adaptive + var requiredInfluence = {"wood": 2500, "stone": 600, "metal": 600}; 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 ++; - } + gameState.getOwnDropsites(resource).forEach(function(ent) { //}){ + if (ent.getMetadata("resource-quantity-" + resource) == undefined || typeof(ent.getMetadata("resource-quantity-" + resource)) !== "number") { + count++; // assume it's OK if we don't know. + return; + } + var quantity = +ent.getMetadata("resource-quantity-" + resource); + var quantityFar = +ent.getMetadata("resource-quantity-far-" + resource); + + if (quantity >= requiredInfluence[resource]) { + count++; + } else if (quantity + quantityFar >= requiredInfluence[resource]) { + count += 0.5 + (quantity/requiredInfluence[resource])/2; + } else { + count += ((quantity + quantityFar)/requiredInfluence[resource])/2; } }); return count; }; EconomyManager.prototype.buildMarket = function(gameState, queues){ - if (gameState.getTimeElapsed() > 360 * 1000){ + if (gameState.getTimeElapsed() > 620 * 1000){ if (queues.economicBuilding.countTotalQueuedUnitsWithClass("BarterMarket") === 0 && gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_market")) === 0){ //only ever build one mill/CC/market at a time @@ -485,29 +675,19 @@ EconomyManager.prototype.tryBartering = function(gameState){ }; // so this always try to build dropsites. EconomyManager.prototype.buildDropsites = function(gameState, queues){ - if (queues.economicBuilding.totalLength() === 0 && - gameState.countFoundationsWithType(gameState.applyCiv("structures/{civ}_mill")) === 0 && + 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/market 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)); + if (spot[0] === true){ + queues.economicBuilding.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_civil_centre", spot[1])); + } else { + queues.economicBuilding.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_mill", spot[1])); } break; } @@ -524,38 +704,38 @@ EconomyManager.prototype.update = function(gameState, queues, events) { this.buildNewCC(gameState,queues); + // this function also deals with a few things that are number-of-workers related Engine.ProfileStart("Train workers and build farms"); this.trainMoreWorkers(gameState, queues); - if (gameState.getTimeElapsed() > 5000) + if ((gameState.ai.playedTurn+1) % 3 === 0) this.buildMoreFields(gameState,queues); + Engine.ProfileStop(); //Later in the game we want to build stuff faster. - if (gameState.countEntitiesByType(gameState.applyCiv("units/{civ}_support_female_citizen")) > this.targetNumWorkers * 0.5) { + if (gameState.getTimeElapsed() > 15*60*1000) { this.targetNumBuilders = 6; }else{ this.targetNumBuilders = 3; } - - if (gameState.getTimeElapsed() > 20*60*1000) { - 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); if (gameState.ai.playedTurn % 2 === 0) { - this.updateResourceMaps(gameState, events); - this.updateResourceConcentrations(gameState); - this.updateNearbyResources(gameState); + var resources = ["food", "wood", "stone", "metal"]; + this.updateNearbyResources(gameState, resources[(gameState.ai.playedTurn % 8)/2]); + } else if (gameState.ai.playedTurn % 2 === 1) { + var resources = ["food", "wood", "stone", "metal"]; + this.updateResourceConcentrations(gameState, resources[((gameState.ai.playedTurn+1) % 8)/2]); } Engine.ProfileStop(); - Engine.ProfileStart("Build new Dropsites"); - this.buildDropsites(gameState, queues); - Engine.ProfileStop(); - + if (gameState.ai.playedTurn % 8 === 0) { + Engine.ProfileStart("Build new Dropsites"); + this.buildDropsites(gameState, queues); + Engine.ProfileStop(); + } this.tryBartering(gameState); this.buildMarket(gameState, queues); @@ -575,32 +755,33 @@ EconomyManager.prototype.update = function(gameState, queues, events) { this.reassignIdleWorkers(gameState); Engine.ProfileStop(); - Engine.ProfileStart("Swap Workers"); - var gathererGroups = {}; - gameState.getOwnEntitiesByRole("worker").forEach(function(ent){ - var key = uneval(ent.resourceGatherRates()); - if (!gathererGroups[key]){ - gathererGroups[key] = {"food": [], "wood": [], "metal": [], "stone": []}; - } - if (ent.getMetadata("gather-type") in gathererGroups[key]){ - gathererGroups[key][ent.getMetadata("gather-type")].push(ent); - } - }); - - for (var i in gathererGroups){ - for (var j in gathererGroups){ - var a = eval(i); - var b = eval(j); - if (a["food.grain"]/b["food.grain"] > a["wood.tree"]/b["wood.tree"] && gathererGroups[i]["wood"].length > 0 && gathererGroups[j]["food"].length > 0){ - for (var k = 0; k < Math.min(gathererGroups[i]["wood"].length, gathererGroups[j]["food"].length); k++){ - gathererGroups[i]["wood"][k].setMetadata("gather-type", "food"); - gathererGroups[j]["food"][k].setMetadata("gather-type", "wood"); + // this is pretty slow, run it once in a while + if (gameState.ai.playedTurn % 4 === 0) { + Engine.ProfileStart("Swap Workers"); + var gathererGroups = {}; + gameState.getOwnEntitiesByRole("worker").forEach(function(ent){ + var key = uneval(ent.resourceGatherRates()); + if (!gathererGroups[key]){ + gathererGroups[key] = {"food": [], "wood": [], "metal": [], "stone": []}; + } + if (ent.getMetadata("gather-type") in gathererGroups[key]){ + gathererGroups[key][ent.getMetadata("gather-type")].push(ent); + } + }); + for (var i in gathererGroups){ + for (var j in gathererGroups){ + var a = eval(i); + var b = eval(j); + if (a["food.grain"]/b["food.grain"] > a["wood.tree"]/b["wood.tree"] && gathererGroups[i]["wood"].length > 0 && gathererGroups[j]["food"].length > 0){ + for (var k = 0; k < Math.min(gathererGroups[i]["wood"].length, gathererGroups[j]["food"].length); k++){ + gathererGroups[i]["wood"][k].setMetadata("gather-type", "food"); + gathererGroups[j]["food"][k].setMetadata("gather-type", "wood"); + } } } } + Engine.ProfileStop(); } - Engine.ProfileStop(); - Engine.ProfileStart("Run Workers"); gameState.getOwnEntitiesByRole("worker").forEach(function(ent){ if (!ent.getMetadata("worker-object")){ diff --git a/binaries/data/mods/public/simulation/ai/qbot-wc/enemy-watcher.js b/binaries/data/mods/public/simulation/ai/qbot-wc/enemy-watcher.js index 4b0d24675a..400f56c881 100755 --- a/binaries/data/mods/public/simulation/ai/qbot-wc/enemy-watcher.js +++ b/binaries/data/mods/public/simulation/ai/qbot-wc/enemy-watcher.js @@ -177,6 +177,9 @@ enemyWatcher.prototype.splitArmies = function(gameState){ army.forEach( function (enemy) { if (enemy.position() == undefined) return; + + // debug ("entity " +enemy.templateName() + " is currently " +enemy.visibility(gameState.player)); + if (!inRange(enemy.position(),centre, 3500) ) { var newArmyID = uneval( gameState.player + "" + self.totalNBofArmies); if (self.dangerousArmies.indexOf(armyID) !== -1) diff --git a/binaries/data/mods/public/simulation/ai/qbot-wc/entity-extend.js b/binaries/data/mods/public/simulation/ai/qbot-wc/entity-extend.js index 1f9920859b..115be6eceb 100644 --- a/binaries/data/mods/public/simulation/ai/qbot-wc/entity-extend.js +++ b/binaries/data/mods/public/simulation/ai/qbot-wc/entity-extend.js @@ -44,6 +44,23 @@ EntityTemplate.prototype.hasClasses = function(array) { return true; }; +// returns the classes this counters: +// each countered class is an array specifying what is required (even if only one) and the Multiplier [ ["whatever","other whatever"] , 0 ]. +EntityTemplate.prototype.getCounteredClasses = function() { + if (!this._template.Attack) + return undefined; + + var Classes = []; + for (i in this._template.Attack) { + if (!this._template.Attack[i].Bonuses) + continue; + for (o in this._template.Attack[i].Bonuses) { + Classes.push([this._template.Attack[i].Bonuses[o].Classes.split(" "), +this._template.Attack[i].Bonuses[o].Multiplier]); + } + } + return Classes; +}; + EntityTemplate.prototype.getMaxStrength = function() { var strength = 0.0; @@ -113,11 +130,18 @@ EntityTemplate.prototype.costSum = function() { - Entity.prototype.deleteMetadata = function(id) { delete this._ai._entityMetadata[this.id()]; }; +Entity.prototype.healthLevel = function() { + return (this.hitpoints() / this.maxHitpoints()); +}; + +Entity.prototype.visibility = function(player) { + return this._entity.visibility[player-1]; +}; + Entity.prototype.unload = function(id) { if (!this._template.GarrisonHolder) return undefined; @@ -158,3 +182,4 @@ Entity.prototype.barter = function(buyType, sellType, amount) { Engine.PostCommand({"type": "barter", "sell" : sellType, "buy" : buyType, "amount" : amount }); return this; }; + diff --git a/binaries/data/mods/public/simulation/ai/qbot-wc/entitycollection-extend.js b/binaries/data/mods/public/simulation/ai/qbot-wc/entitycollection-extend.js index 68a01e653f..eb7a5f5262 100644 --- a/binaries/data/mods/public/simulation/ai/qbot-wc/entitycollection-extend.js +++ b/binaries/data/mods/public/simulation/ai/qbot-wc/entitycollection-extend.js @@ -10,13 +10,12 @@ EntityCollection.prototype.attack = function(unit) Engine.PostCommand({"type": "attack", "entities": this.toIdArray(), "target": unitId, "queued": false}); return this; }; - -EntityCollection.prototype.attackMove = function(x, z){ - Engine.PostCommand({"type": "attack-move", "entities": this.toIdArray(), "x": x, "z": z, "queued": false}); +// violent, aggressive, defensive, passive, standground +EntityCollection.prototype.setStance = function(stance){ + Engine.PostCommand({"type": "stance", "entities": this.toIdArray(), "name" : stance, "queued": false}); return this; }; - function EntityCollectionFromIds(gameState, idList){ var ents = {}; for (var i in idList){ diff --git a/binaries/data/mods/public/simulation/ai/qbot-wc/filters-extend.js b/binaries/data/mods/public/simulation/ai/qbot-wc/filters-extend.js index 0a23904f4c..725a4dd435 100644 --- a/binaries/data/mods/public/simulation/ai/qbot-wc/filters-extend.js +++ b/binaries/data/mods/public/simulation/ai/qbot-wc/filters-extend.js @@ -30,4 +30,13 @@ Filters["byTerritory"] = function(Map, territoryIndex){ } }, "dynamicProperties": ['position']}; -}; \ No newline at end of file +}; +Filters["isDropsite"] = function(resourceType){ + return {"func": function(ent){ + return (ent.resourceDropsiteTypes() && ent.resourceDropsiteTypes().indexOf(resourceType) !== -1 + && ent.foundationProgress() === undefined); + }, + "dynamicProperties": []}; +}; + + diff --git a/binaries/data/mods/public/simulation/ai/qbot-wc/gamestate.js b/binaries/data/mods/public/simulation/ai/qbot-wc/gamestate.js index a5f3e56fb6..3d7bec7275 100644 --- a/binaries/data/mods/public/simulation/ai/qbot-wc/gamestate.js +++ b/binaries/data/mods/public/simulation/ai/qbot-wc/gamestate.js @@ -256,7 +256,7 @@ GameState.prototype.countOwnQueuedEntitiesWithMetadata = function(data, value) { var count = 0; this.getOwnTrainingFacilities().forEach(function(ent) { ent.trainingQueue().forEach(function(item) { - if (item.metadata && item.metadata.data && item.metadata.data == value) + if (item.metadata && item.metadata[data] && item.metadata[data] == value) count += item.count; }); }); @@ -344,15 +344,29 @@ GameState.prototype.findTrainableUnits = function(classes){ for (var i in allTrainable) { var template = this.getTemplate(allTrainable[i]); var okay = true; + for (o in classes) if (!template.hasClass(classes[o])) okay = false; + if (template.hasClass("Hero")) // disabling heroes for now okay = false; + if (okay) ret.push( [allTrainable[i], template] ); } return ret; }; - - +// defcon utilities +GameState.prototype.timeSinceDefconChange = function() { + return this.getTimeElapsed()-this.ai.defconChangeTime; +}; +GameState.prototype.setDefcon = function(level,force) { + if (this.ai.defcon >= level || force) { + this.ai.defcon = level; + this.ai.defconChangeTime = this.getTimeElapsed(); + } +}; +GameState.prototype.defcon = function() { + return this.ai.defcon; +}; diff --git a/binaries/data/mods/public/simulation/ai/qbot-wc/map-module.js b/binaries/data/mods/public/simulation/ai/qbot-wc/map-module.js index 3df3b30fe9..088f9cf5dc 100644 --- a/binaries/data/mods/public/simulation/ai/qbot-wc/map-module.js +++ b/binaries/data/mods/public/simulation/ai/qbot-wc/map-module.js @@ -1,15 +1,20 @@ const TERRITORY_PLAYER_MASK = 0x3F; //TODO: Make this cope with negative cell values -function Map(gameState, originalMap){ +function Map(gameState, originalMap, actualCopy){ // 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){ + + if (originalMap && actualCopy){ + this.map = new Uint16Array(this.length); + for (var i = 0; i < originalMap.length; ++i) + this.map[i] = originalMap[i]; + } else if (originalMap) { this.map = originalMap; - }else{ + } else { this.map = new Uint16Array(this.length); } this.cellSize = gameState.cellSize; @@ -99,12 +104,28 @@ Map.createTerritoryMap = function(gameState) { ret.getOwner = function(p) { return this.point(p) & TERRITORY_PLAYER_MASK; } - + ret.getOwnerIndex = function(p) { + return this.map[p] & TERRITORY_PLAYER_MASK; + } return ret; }; - +Map.prototype.drawDistance = function(gameState, elements) { + for ( var y = 0; y < this.height; ++y) { + for ( var x = 0; x < this.width; ++x) { + var minDist = 500000; + for (i in elements) { + var px = elements[i].position()[0]/gameState.cellSize; + var py = elements[i].position()[1]/gameState.cellSize; + var dist = VectorDistance([px,py], [x,y]); + if (dist < minDist) + minDist = dist; + } + this.map[x + y*this.width] = Math.max(1,this.width - minDist); + } + } +}; Map.prototype.addInfluence = function(cx, cy, maxDist, strength, type) { - strength = strength ? strength : maxDist; + strength = strength ? +strength : +maxDist; type = type ? type : 'linear'; var x0 = Math.max(0, cx - maxDist); @@ -113,19 +134,18 @@ Map.prototype.addInfluence = function(cx, cy, maxDist, strength, type) { var y1 = Math.min(this.height, cy + maxDist); var maxDist2 = maxDist * maxDist; - var str = 0; + var str = 0.0; switch (type){ case 'linear': - str = strength / maxDist; + str = +strength / +maxDist; break; case 'quadratic': - str = strength / maxDist2; + str = +strength / +maxDist2; break; case 'constant': - str = strength; + str = +strength; break; } - for ( var y = y0; y < y1; ++y) { for ( var x = x0; x < x1; ++x) { var dx = x - cx; @@ -145,7 +165,6 @@ Map.prototype.addInfluence = function(cx, cy, maxDist, strength, type) { 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{ @@ -156,6 +175,79 @@ Map.prototype.addInfluence = function(cx, cy, maxDist, strength, type) { } }; +Map.prototype.multiplyInfluence = function(cx, cy, maxDist, strength, type) { + strength = strength ? +strength : +maxDist; + type = type ? type : 'constant'; + + 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.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; + } + var machin = this.map[x + y * this.width] * quant; + if (machin <= 0){ + this.map[x + y * this.width] = 0; //set anything which would have gone negative to 0 + }else{ + this.map[x + y * this.width] = machin; + } + } + } + } +}; +Map.prototype.setInfluence = function(cx, cy, maxDist, value) { + value = value ? value : 0; + + 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; + + 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){ + this.map[x + y * this.width] = value; + } + } + } +}; + Map.prototype.sumInfluence = function(cx, cy, radius){ var x0 = Math.max(0, cx - radius); var y0 = Math.max(0, cy - radius); @@ -249,15 +341,37 @@ Map.prototype.findBestTile = function(radius, obstructionTiles){ 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]; +// Multiplies current map by 3 if in my territory +Map.prototype.multiplyTerritory = function(gameState,map){ + for (var i = 0; i < this.length; ++i){ + if (map.getOwnerIndex(i) === gameState.player) + this.map[i] *= 2.5; + } +}; +// Multiplies current map by the parameter map pixelwise +Map.prototype.multiply = function(map, onlyBetter,divider,maxMultiplier){ + for (var i = 0; i < this.length; ++i){ + if (map.map[i]/divider > 1) + this.map[i] = Math.min(maxMultiplier*this.map[i], this.map[i] * (map.map[i]/divider)); + } +}; +// add to current map by the parameter map pixelwise +Map.prototype.add = function(map){ + for (var i = 0; i < this.length; ++i){ + this.map[i] += +map.map[i]; + } +}; +// add to current map by the parameter map pixelwise +Map.prototype.subtract = function(map){ + for (var i = 0; i < this.length; ++i){ + this.map[i] += map.map[i]; + if (this.map[i] <= 0) + this.map[i] = 0; } }; Map.prototype.dumpIm = function(name, threshold){ name = name ? name : "default.png"; - threshold = threshold ? threshold : 256; + threshold = threshold ? threshold : 65500; Engine.DumpImage(name, this.map, this.width, this.height, threshold); }; diff --git a/binaries/data/mods/public/simulation/ai/qbot-wc/military.js b/binaries/data/mods/public/simulation/ai/qbot-wc/military.js index c6d183f393..60066c1c2d 100755 --- a/binaries/data/mods/public/simulation/ai/qbot-wc/military.js +++ b/binaries/data/mods/public/simulation/ai/qbot-wc/military.js @@ -7,16 +7,8 @@ */ var MilitaryAttackManager = function() { - // these use the structure soldiers[unitId] = true|false to register the units - this.attackManagers = [AttackMoveToLocation]; - this.availableAttacks = []; - this.currentAttacks = []; - - // Counts how many attacks we have sent at the enemy. - this.attackCount = 0; - this.lastAttackTime = 0; - this.defenceManager = new Defence(); + this.TotalAttackNumber = 0; this.upcomingAttacks = { "CityAttack" : [] }; this.startedAttacks = { "CityAttack" : [] }; @@ -144,9 +136,11 @@ MilitaryAttackManager.prototype.findBestNewUnit = function(gameState, queue, sol // picks the best template based on parameters and classes MilitaryAttackManager.prototype.findBestTrainableUnit = function(gameState, classes, parameters) { var units = gameState.findTrainableUnits(classes); + if (units.length === 0) return undefined; + units.sort(function(a, b) { //}) { var aDivParam = 0, bDivParam = 0; var aTopParam = 0, bTopParam = 0; @@ -407,7 +401,7 @@ MilitaryAttackManager.prototype.constructTrainingBuildings = function(gameState, // Build more military buildings // TODO: make military building better Engine.ProfileStart("Build buildings"); - if (gameState.countEntitiesByType(gameState.applyCiv("units/{civ}_support_female_citizen")) > 35) { + if (gameState.countEntitiesByType(gameState.applyCiv("units/{civ}_support_female_citizen")) > 25) { if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bModerate[0])) + queues.militaryBuilding.totalLength() < 1) { queues.militaryBuilding.addItem(new BuildingConstructionPlan(gameState, this.bModerate[0])); @@ -543,37 +537,36 @@ MilitaryAttackManager.prototype.update = function(gameState, queues, events) { var attack = this.upcomingAttacks[attackType][i]; - if (!attack.isPaused()) { - // okay so we'll get the support plan - if (!attack.isStarted()) { - if (1) { //gameState.ai.status["underAttack"] == false) { - var updateStep = attack.updatePreparation(gameState, this,events); - - // now we're gonna check if the preparation time is over - if (updateStep === 1) { - // just chillin' - } else if (updateStep === 0) { - debug ("Military Manager: " +attack.getType() +" plan " +attack.getName() +" aborted."); - attack.Abort(gameState, this); - - //this.abortedAttacks.push(attack); - - this.upcomingAttacks[attackType].splice(i,1); - i--; - } else if (updateStep === 2) { - debug ("Military Manager: Starting " +attack.getType() +" plan " +attack.getName()); - attack.StartAttack(gameState,this); - this.startedAttacks[attackType].push(attack); - this.upcomingAttacks[attackType].splice(i,1); - i--; - } + // okay so we'll get the support plan + if (!attack.isStarted()) { + var updateStep = attack.updatePreparation(gameState, this,events); + + // now we're gonna check if the preparation time is over + if (updateStep === 1 || attack.isPaused() ) { + // just chillin' + } else if (updateStep === 0 || updateStep === 3) { + debug ("Military Manager: " +attack.getType() +" plan " +attack.getName() +" aborted."); + if (updateStep === 3) { + this.attackPlansEncounteredWater = true; + debug("I dare not wet my feet"); } - } else { - debug ("Military Manager: Starting " +attack.getType() +" plan " +attack.getName()); - this.startedAttacks[attackType].push(attack); - this.upcomingAttacks[attackType].splice(i,1); + attack.Abort(gameState, this); + //this.abortedAttacks.push(attack); + i--; + this.upcomingAttacks[attackType].splice(i,1); + } else if (updateStep === 2) { + debug ("Military Manager: Starting " +attack.getType() +" plan " +attack.getName()); + attack.StartAttack(gameState,this); + this.startedAttacks[attackType].push(attack); + i--; + this.upcomingAttacks[attackType].splice(i-1,1); } + } else { + debug ("Military Manager: Starting " +attack.getType() +" plan " +attack.getName()); + this.startedAttacks[attackType].push(attack); + i--; + this.upcomingAttacks[attackType].splice(i-1,1); } } } @@ -596,12 +589,17 @@ MilitaryAttackManager.prototype.update = function(gameState, queues, events) { } } // creating plans after updating because an aborted plan might be reused in that case. - if (gameState.countEntitiesByType(gameState.applyCiv(this.bModerate[0])) >= 1) { - if (this.upcomingAttacks["CityAttack"].length == 0) { + if (gameState.countEntitiesByType(gameState.applyCiv(this.bModerate[0])) >= 1 && !this.attackPlansEncounteredWater) { + if (this.upcomingAttacks["CityAttack"].length == 0 && gameState.getTimeElapsed() < 25*60000) { var Lalala = new CityAttack(gameState, this,this.TotalAttackNumber, -1); debug ("Military Manager: Creating the plan " +this.TotalAttackNumber); this.TotalAttackNumber++; this.upcomingAttacks["CityAttack"].push(Lalala); + } else if (this.upcomingAttacks["CityAttack"].length == 0) { + var Lalala = new CityAttack(gameState, this,this.TotalAttackNumber, -1, "superSized"); + debug ("Military Manager: Creating the super sized plan " +this.TotalAttackNumber); + this.TotalAttackNumber++; + this.upcomingAttacks["CityAttack"].push(Lalala); } } /* diff --git a/binaries/data/mods/public/simulation/ai/qbot-wc/plan-building.js b/binaries/data/mods/public/simulation/ai/qbot-wc/plan-building.js index 69580ff7e0..d8717860a8 100644 --- a/binaries/data/mods/public/simulation/ai/qbot-wc/plan-building.js +++ b/binaries/data/mods/public/simulation/ai/qbot-wc/plan-building.js @@ -56,7 +56,7 @@ BuildingConstructionPlan.prototype.findGoodPosition = function(gameState) { var obstructionMap = Map.createObstructionMap(gameState,template); - //obstructionMap.dumpIm("obstructions.png"); + ///obstructionMap.dumpIm("obstructions.png"); obstructionMap.expandInfluences(); @@ -64,12 +64,13 @@ BuildingConstructionPlan.prototype.findGoodPosition = function(gameState) { var friendlyTiles = new Map(gameState); + var alreadyHasHouses = false; + // 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{ // No position was specified so try and find a sensible place to build gameState.getOwnEntities().forEach(function(ent) { @@ -96,9 +97,10 @@ BuildingConstructionPlan.prototype.findGoodPosition = function(gameState) { } }else{ - if (template.genericName() == "House" && ent.genericName() == "House") - friendlyTiles.addInfluence(x, z, infl*2.0); // houses are close to other houses - else if (template.genericName() == "House") { + if (template.genericName() == "House" && ent.genericName() == "House") { + friendlyTiles.addInfluence(x, z, 15.0,20,'linear'); // houses are close to other houses + alreadyHasHouses = true; + } else if (template.genericName() == "House") { friendlyTiles.addInfluence(x, z, Math.ceil(infl/2.0),infl); // houses are farther away from other buildings but houses friendlyTiles.addInfluence(x, z, Math.ceil(infl/4.0),-infl/2.0); // houses are farther away from other buildings but houses } else if (ent.genericName() != "House") // houses have no influence on other buildings @@ -108,14 +110,17 @@ BuildingConstructionPlan.prototype.findGoodPosition = function(gameState) { if (ent.hasClass("CivCentre") && template.genericName() != "House"){ friendlyTiles.addInfluence(x, z, Math.floor(infl/8), Math.floor(-infl/2)); } else if (ent.hasClass("CivCentre")) { - friendlyTiles.addInfluence(x, z, Math.floor(infl/3.0), infl + 1); - friendlyTiles.addInfluence(x, z, Math.floor(infl/4), -Math.floor(infl)); + friendlyTiles.addInfluence(x, z, infl/3.0, infl + 1); + friendlyTiles.addInfluence(x, z, Math.ceil(infl/5.0), -(infl/2.0), 'linear'); } } } }); } + //friendlyTiles.dumpIm("Building " +gameState.getTimeElapsed() + ".png", 200); + + // 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. // note: not for houses and dropsites who ought to be closer to either each other or a resource. @@ -125,18 +130,26 @@ BuildingConstructionPlan.prototype.findGoodPosition = function(gameState) { else if (template.buildCategory() === "Dock") var radius = 0; else if (template.genericName() != "House" && !template.hasClass("DropsiteWood") && !template.hasClass("DropsiteStone") && !template.hasClass("DropsiteMetal")) - var radius = Math.ceil(template.obstructionRadius() / cellSize) + 2; + var radius = Math.ceil(template.obstructionRadius() / cellSize) + 1; else var radius = Math.ceil(template.obstructionRadius() / cellSize); + // further contract cause walls if (gameState.playerData.civ == "iber") radius *= 0.95; - // Find the best non-obstructed tile - var bestTile = friendlyTiles.findBestTile(radius, obstructionMap); - var bestIdx = bestTile[0]; - var bestVal = bestTile[1]; - + // Find the best non-obstructed + if (template.genericName() == "House" && !alreadyHasHouses) { + // try to get some space first + var bestTile = friendlyTiles.findBestTile(10, obstructionMap); + var bestIdx = bestTile[0]; + var bestVal = bestTile[1]; + } + if (bestVal === undefined || bestVal === -1) { + var bestTile = friendlyTiles.findBestTile(radius, obstructionMap); + var bestIdx = bestTile[0]; + var bestVal = bestTile[1]; + } if (bestVal === -1){ return false; } diff --git a/binaries/data/mods/public/simulation/ai/qbot-wc/plan-training.js b/binaries/data/mods/public/simulation/ai/qbot-wc/plan-training.js index d2ae0e5757..818a645088 100644 --- a/binaries/data/mods/public/simulation/ai/qbot-wc/plan-training.js +++ b/binaries/data/mods/public/simulation/ai/qbot-wc/plan-training.js @@ -30,7 +30,7 @@ UnitTrainingPlan.prototype.canExecute = function(gameState) { UnitTrainingPlan.prototype.execute = function(gameState) { //warn("Executing UnitTrainingPlan " + uneval(this)); - + var self = this; var trainers = gameState.findTrainers(this.type).toEntityArray(); // Prefer training buildings with short queues @@ -38,6 +38,16 @@ UnitTrainingPlan.prototype.execute = function(gameState) { // plans that have already been executed this turn) if (trainers.length > 0){ trainers.sort(function(a, b) { + + if (self.metadata["plan"] !== undefined) { + var aa = a.trainingQueueTime(); + var bb = b.trainingQueueTime(); + if (a.hasClass("Civic")) + aa += 20; + if (b.hasClass("Civic")) + bb += 20; + return (a.trainingQueueTime() - b.trainingQueueTime()); + } return a.trainingQueueTime() - b.trainingQueueTime(); }); diff --git a/binaries/data/mods/public/simulation/ai/qbot-wc/qbot.js b/binaries/data/mods/public/simulation/ai/qbot-wc/qbot.js index 5f8167f8b1..4ba96343f1 100644 --- a/binaries/data/mods/public/simulation/ai/qbot-wc/qbot.js +++ b/binaries/data/mods/public/simulation/ai/qbot-wc/qbot.js @@ -35,6 +35,9 @@ function QBotAI(settings) { this.firstTime = true; this.savedEvents = []; + + this.defcon = 5; + this.defconChangeTime = -10000000; } QBotAI.prototype = new BaseAI(); @@ -78,8 +81,9 @@ QBotAI.prototype.runInit = function(gameState){ this.templateManager = new TemplateManager(gameState); - - + this.distanceFromMeMap = new Map(gameState); + this.distanceFromMeMap.drawDistance(gameState,myKeyEntities.toEntityArray()); + //this.distanceFromMeMap.dumpIm("dumping.png", this.distanceFromMeMap.width*1.5); } }; @@ -92,6 +96,12 @@ QBotAI.prototype.OnUpdate = function() { this.savedEvents = this.savedEvents.concat(this.events); } + if (this.turn == 0) { + debug ("Initializing"); + var gameState = new GameState(this); + this.runInit(gameState); + } + // Run the update every n turns, offset depending on player ID to balance // the load if ((this.turn + this.player) % 10 == 0) { @@ -105,13 +115,17 @@ QBotAI.prototype.OnUpdate = function() { return; // With no entities to control the AI cannot do anything } + // defcon cooldown + if (this.defcon < 5 && gameState.timeSinceDefconChange() > 20000) + this.defcon++; + this.runInit(gameState); for (var i in this.modules){ this.modules[i].update(gameState, this.queues, this.savedEvents); } - this.updateDynamicPriorities(gameState, this.queues); + //this.updateDynamicPriorities(gameState, this.queues); this.queueManager.update(gameState); diff --git a/binaries/data/mods/public/simulation/ai/qbot-wc/queue.js b/binaries/data/mods/public/simulation/ai/qbot-wc/queue.js index 6009f740b9..8141d24832 100644 --- a/binaries/data/mods/public/simulation/ai/qbot-wc/queue.js +++ b/binaries/data/mods/public/simulation/ai/qbot-wc/queue.js @@ -100,6 +100,18 @@ Queue.prototype.countTotalQueuedUnitsWithClass = function(classe){ } return count; }; +Queue.prototype.countTotalQueuedUnitsWithMetadata = function(data,value){ + var count = 0; + for (var i in this.queue){ + if (this.queue[i].metadata[data] && this.queue[i].metadata[data] == value) + count += this.queue[i].number; + } + for (var i in this.outQueue){ + if (this.outQueue[i].metadata[data] && this.outQueue[i].metadata[data] == value) + count += this.outQueue[i].number; + } + return count; +}; Queue.prototype.totalLength = function(){ return this.queue.length + this.outQueue.length; diff --git a/binaries/data/mods/public/simulation/ai/qbot-wc/template-manager.js b/binaries/data/mods/public/simulation/ai/qbot-wc/template-manager.js index c3bf8e30f7..e558bdaaa3 100755 --- a/binaries/data/mods/public/simulation/ai/qbot-wc/template-manager.js +++ b/binaries/data/mods/public/simulation/ai/qbot-wc/template-manager.js @@ -8,7 +8,9 @@ var TemplateManager = function(gameState) { this.knownTemplatesList = []; this.buildingTemplates = []; this.unitTemplates = []; - + this.templateCounters = {}; + this.templateCounteredBy = {}; + // this will store templates that exist this.AcknowledgeTemplates(gameState); this.getBuildableSubtemplates(gameState); @@ -17,6 +19,8 @@ var TemplateManager = function(gameState) { this.getTrainableSubtemplates(gameState); // should be enough in 100% of the cases. + this.getTemplateCounters(gameState); + }; TemplateManager.prototype.AcknowledgeTemplates = function(gameState) { @@ -74,3 +78,38 @@ TemplateManager.prototype.getTrainableSubtemplates = function(gameState) } } } +TemplateManager.prototype.getTemplateCounters = function(gameState) +{ + for (i in this.unitTemplates) + { + var tp = gameState.getTemplate(this.unitTemplates[i]); + var tpname = this.unitTemplates[i]; + this.templateCounters[tpname] = tp.getCounteredClasses(); + } +} +// features auto-caching +TemplateManager.prototype.getCountersToClasses = function(gameState,classes,templateName) +{ + if (templateName !== undefined && this.templateCounteredBy[templateName]) + return this.templateCounteredBy[templateName]; + + var templates = []; + for (i in this.templateCounters) { + var okay = false; + for each (ticket in this.templateCounters[i]) { + var okaya = true; + for (a in ticket[0]) { + if (classes.indexOf(ticket[0][a]) === -1) + okaya = false; + } + if (okaya && templates.indexOf(i) === -1) + templates.push([i, ticket[1]]); + } + } + templates.sort (function (a,b) { return -a[1] + b[1]; }); + + if (templateName !== undefined) + this.templateCounteredBy[templateName] = templates; + return templates; +} + diff --git a/binaries/data/mods/public/simulation/ai/qbot-wc/terrain-analysis.js b/binaries/data/mods/public/simulation/ai/qbot-wc/terrain-analysis.js index 8e9904035f..59e053c353 100644 --- a/binaries/data/mods/public/simulation/ai/qbot-wc/terrain-analysis.js +++ b/binaries/data/mods/public/simulation/ai/qbot-wc/terrain-analysis.js @@ -116,7 +116,10 @@ PathFinder.prototype.getPaths = function(start, end, mode){ } var paths = []; + var i = 0; while (true){ + i++; + //this.dumpIm("terrainanalysis_"+i+".png", 511); this.makeGradient(s,e); var curPath = this.walkGradient(e, mode); @@ -134,7 +137,7 @@ PathFinder.prototype.getPaths = function(start, end, mode){ if (paths.length > 0){ return paths; }else{ - return undefined; + return []; } }; @@ -292,7 +295,7 @@ function Accessibility(gameState, location){ start = this.findClosestPassablePoint(this.gamePosToMapPos(location)); iterations += 1; } - + //this.dumpIm("accessibility.png"); } copyPrototype(Accessibility, TerrainAnalysis); @@ -347,4 +350,362 @@ Accessibility.prototype.floodFill = function(start){ newStack = []; } return count; -}; \ No newline at end of file +}; + + + + +// Some different take on the idea of Quantumstate... What I'll do is make a list of any terrain obstruction... + +function aStarPath(gameState, onWater){ + var self = this; + + this.passabilityMap = gameState.getMap(); + + var obstructionMaskLand = gameState.getPassabilityClassMask("default"); + var obstructionMaskWater = gameState.getPassabilityClassMask("ship"); + + var obstructionTiles = new Uint16Array(this.passabilityMap.data.length); + for (var i = 0; i < this.passabilityMap.data.length; ++i) + { + if (onWater) { + obstructionTiles[i] = (this.passabilityMap.data[i] & obstructionMaskWater) ? 0 : 255; + } else { + obstructionTiles[i] = (this.passabilityMap.data[i] & obstructionMaskLand) ? 0 : 255; + // We allow water, but we set it at a different index. + if (!(this.passabilityMap.data[i] & obstructionMaskWater) && obstructionTiles[i] === 0) + obstructionTiles[i] = 200; + } + } + if (onWater) + this.onWater = true; + else + this.onWater = false; + this.pathRequiresWater = this.onWater; + + this.cellSize = gameState.cellSize; + + this.Map(gameState, obstructionTiles); + this.passabilityMap = new Map(gameState, obstructionTiles, true); + + var type = ["wood","stone", "metal"]; + if (onWater) // trees can perhaps be put into water, I'd doubt so about the rest. + type = ["wood"]; + for (o in type) { + var entities = gameState.getResourceSupplies(type[o]); + entities.forEach(function (supply) { //}){ + var radius = Math.floor(supply.obstructionRadius() / self.cellSize); + if (type[o] === "wood") { + for (var xx = -1; xx <= 1;xx++) + for (var yy = -1; yy <= 1;yy++) + { + var x = self.gamePosToMapPos(supply.position())[0]; + var y = self.gamePosToMapPos(supply.position())[1]; + if (x+xx >= 0 && x+xx < self.width && y+yy >= 0 && y+yy < self.height) + { + self.passabilityMap.map[x+xx + (y+yy)*self.width] = 100; // tree + } + } + self.map[x + y*self.width] = 0; + self.passabilityMap.map[x + y*self.width] = 0; + } else { + for (var xx = -radius; xx <= radius;xx++) + for (var yy = -radius; yy <= radius;yy++) + { + var x = self.gamePosToMapPos(supply.position())[0]; + var y = self.gamePosToMapPos(supply.position())[1]; + if (x+xx >= 0 && x+xx < self.width && y+yy >= 0 && y+yy < self.height) + { + self.map[x+xx + (y+yy)*self.width] = 0; + self.passabilityMap.map[x+xx + (y+yy)*self.width] = 0; + } + } + } + }); + } + //this.dumpIm("Non-Expanded Obstructions.png",255); + this.expandInfluences(); + //this.dumpIm("Expanded Obstructions.png",10); + //this.BluringRadius = 10; + //this.Blur(this.BluringRadius); // first steop of bluring +} +copyPrototype(aStarPath, TerrainAnalysis); + +aStarPath.prototype.getPath = function(start,end,optimized, minSampling, iterationLimit , gamestate) +{ + if (minSampling === undefined) + this.minSampling = 2; + else this.minSampling = minSampling; + + if (start[0] < 0 || this.gamePosToMapPos(start)[0] >= this.width || start[1] < 0 || this.gamePosToMapPos(start)[1] >= this.height) + return undefined; + + var s = this.findClosestPassablePoint(this.gamePosToMapPos(start)); + var e = this.findClosestPassablePoint(this.gamePosToMapPos(end)); + + if (!s || !e){ + return undefined; + } + + var w = this.width; + var h = this.height; + + this.optimized = optimized; + if (this.minSampling < 1) + this.minSampling = 1; + + if (gamestate !== undefined) + { + this.TotorMap = new Map(gamestate); + this.TotorMap.addInfluence(s[0],s[1],1,200,'constant'); + this.TotorMap.addInfluence(e[0],e[1],1,200,'constant'); + } + this.iterationLimit = 65500; + if (iterationLimit !== undefined) + this.iterationLimit = iterationLimit; + + this.s = s[0] + w*s[1]; + this.e = e[0] + w*e[1]; + + // I was using incredibly slow associative arrays before… + this.openList = []; + this.parentSquare = new Uint32Array(this.map.length); + this.isOpened = new Boolean(this.map.length); + this.fCostArray = new Uint32Array(this.map.length); + this.gCostArray = new Uint32Array(this.map.length); + this.currentSquare = this.s; + + this.totalIteration = 0; + + this.isOpened[this.s] = true; + this.openList.push(this.s); + this.fCostArray[this.s] = SquareVectorDistance([this.s%w, Math.floor(this.s/w)], [this.e%w, Math.floor(this.e/w)]); + this.gCostArray[this.s] = 0; + this.parentSquare[this.s] = this.s; + //debug ("Initialized okay"); + return this.continuePath(gamestate); + +} +// in case it's not over yet, this can carry on the calculation of a path over multiple turn until it's over +aStarPath.prototype.continuePath = function(gamestate) +{ + var w = this.width; + var h = this.height; + var positions = [[0,1], [0,-1], [1,0], [-1,0], [1,1], [-1,-1], [1,-1], [-1,1]]; + var cost = [100,100,100,100,150,150,150,150]; + var invCost = [1,1,1,1,0.8,0.8,0.8,0.8]; + //creation of variables used in the loop + var found = false; + var nouveau = false; + var shortcut = false; + var Sampling = this.minSampling; + var closeToEnd = false; + var infinity = Math.min(); + var currentDist = infinity; + var e = this.e; + var s = this.s; + + var iteration = 0; + // on to A* + while (found === false && this.openList.length !== 0 && iteration < this.iterationLimit){ + currentDist = infinity; + + if (shortcut === true) { + this.currentSquare = this.openList.shift(); + } else { + for (i in this.openList) + { + var sum = this.fCostArray[this.openList[i]] + this.gCostArray[this.openList[i]]; + if (sum < currentDist) + { + this.currentSquare = this.openList[i]; + currentDist = sum; + } + } + this.openList.splice(this.openList.indexOf(this.currentSquare),1); + } + if (!this.onWater && this.passabilityMap.map[this.currentSquare] === 200) { + this.onWater = true; + this.pathRequiresWater = true; + } else if (this.onWater && this.passabilityMap.map[this.currentSquare] !== 200) + this.onWater = false; + + shortcut = false; + this.isOpened[this.currentSquare] = false; + + // optimizaiton: can make huge jumps if I know there's nothing in the way + Sampling = this.minSampling; + if (this.optimized === true) { + Sampling = Math.floor( (+this.map[this.currentSquare]-this.minSampling)/Sampling )*Sampling; + if (Sampling < this.minSampling) + Sampling = this.minSampling; + } + /* + var diagSampling = Math.floor(Sampling / 1.5); + if (diagSampling < this.minSampling) + diagSampling = this.minSampling; + */ + var target = [this.e%w, Math.floor(this.e/w)]; + closeToEnd = false; + if (SquareVectorDistance([this.currentSquare%w, Math.floor(this.currentSquare/w)], target) <= Sampling*Sampling) + { + closeToEnd = true; + Sampling = 1; + } + if (gamestate !== undefined) + this.TotorMap.addInfluence(this.currentSquare % w, Math.floor(this.currentSquare / w),1,40,'constant'); + + for (i in positions) + { + //var hereSampling = cost[i] == 1 ? Sampling : diagSampling; + var index = 0 + this.currentSquare +positions[i][0]*Sampling +w*Sampling*positions[i][1]; + if (this.map[index] >= Sampling) + { + if(this.isOpened[index] === undefined) + { + this.parentSquare[index] = this.currentSquare; + + this.fCostArray[index] = SquareVectorDistance([index%w, Math.floor(index/w)], target);// * cost[i]; + this.gCostArray[index] = this.gCostArray[this.currentSquare] + cost[i] * Sampling - this.map[index]; + + if (!this.onWater && this.passabilityMap.map[index] === 200) { + this.gCostArray[index] += this.fCostArray[index]*2; + } else if (this.onWater && this.passabilityMap.map[index] !== 200) { + this.gCostArray[index] += this.fCostArray[index]*2; + } else if (!this.onWater && this.passabilityMap.map[index] === 100) { + this.gCostArray[index] += 100; + } + + if (this.openList[0] !== undefined && this.fCostArray[this.openList[0]] + this.gCostArray[this.openList[0]] > this.fCostArray[index] + this.gCostArray[index]) + { + this.openList.unshift(index); + shortcut = true; + } else { + this.openList.push(index); + } + this.isOpened[index] = true; + if (closeToEnd === true && (index === e || index - 1 === e || index + 1 === e || index - w === e || index + w === e + || index + 1 + w === e || index + 1 - w === e || index - 1 + w === e|| index - 1 - w === e)) { + this.parentSquare[this.e] = this.currentSquare; + found = true; + break; + } + } else { + var addCost = 0; + if (!this.onWater && this.passabilityMap.map[index] === 200) { + addCost = this.fCostArray[index]*2; + } else if (this.onWater && this.passabilityMap.map[index] !== 200) { + addCost = this.fCostArray[index]*2; + } else if (!this.onWater && this.passabilityMap.map[index] === 100) { + addCost += 100; + } + addCost -= this.map[index]; + // already on the Open or closed list + if (this.gCostArray[index] > cost[i] * Sampling + addCost + this.gCostArray[this.currentSquare]) + { + this.parentSquare[index] = this.currentSquare; + this.gCostArray[index] = cost[i] * Sampling + addCost + this.gCostArray[this.currentSquare]; + } + } + } + } + iteration++; + } + this.totalIteration += iteration; + if (iteration === this.iterationLimit && found === false && this.openList.length !== 0) + { + + // we've got to assume that we stopped because we reached the upper limit of iterations + return "toBeContinued"; + } + + //debug (this.totalIteration); + var paths = []; + if (found) { + this.currentSquare = e; + var lastPos = [0,0]; + while (this.parentSquare[this.currentSquare] !== s) + { + this.currentSquare = this.parentSquare[this.currentSquare]; + if (gamestate !== undefined) + this.TotorMap.addInfluence(this.currentSquare % w, Math.floor(this.currentSquare / w),1,50,'constant'); + if (SquareVectorDistance(lastPos,[this.currentSquare % w, Math.floor(this.currentSquare / w)]) > 300) + { + lastPos = [ (this.currentSquare % w) * this.cellSize, Math.floor(this.currentSquare / w) * this.cellSize]; + paths.push(lastPos); + if (gamestate !== undefined) + this.TotorMap.addInfluence(this.currentSquare % w, Math.floor(this.currentSquare / w),1,100,'constant'); + } + } + } + + if (gamestate !== undefined) + this.TotorMap.dumpIm("Path From " +s +" to " +e +".png",255); + + if (paths.length > 0) { + return [paths, this.pathRequiresWater]; + } else { + return undefined; + } + +} + +/** + * Make each cell's 8-bit value at least one greater than each of its + * neighbours' values. (If the grid is initialised with 0s and things high enough (> 100 on most maps), the + * result of each cell is its Manhattan distance to the nearest 0.) + */ +aStarPath.prototype.expandInfluences = function() { + var w = this.width; + var h = this.height; + var grid = this.map; + for ( var y = 0; y < h; ++y) { + var min = 8; + 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; + if (min > 8) + min = 8; + } + + 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; + if (min > 8) + min = 8; + } + } + + for ( var x = 0; x < w; ++x) { + var min = 8; + 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; + if (min > 8) + min = 8; + } + + 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; + if (min > 8) + min = 8; + } + } +}; diff --git a/binaries/data/mods/public/simulation/ai/qbot-wc/worker.js b/binaries/data/mods/public/simulation/ai/qbot-wc/worker.js index 12a89bb1db..338586cc3e 100644 --- a/binaries/data/mods/public/simulation/ai/qbot-wc/worker.js +++ b/binaries/data/mods/public/simulation/ai/qbot-wc/worker.js @@ -16,8 +16,8 @@ Worker.prototype.update = function(gameState) { } if (subrole === "gatherer"){ - if (!(this.ent.unitAIState().split(".")[1] === "GATHER" && this.ent.unitAIOrderData().type - && this.getResourceType(this.ent.unitAIOrderData().type) === this.ent.getMetadata("gather-type")) + if (!(this.ent.unitAIState().split(".")[1] === "GATHER" && this.ent.unitAIOrderData()[0].type + && this.getResourceType(this.ent.unitAIOrderData()[0].type) === this.ent.getMetadata("gather-type")) && !(this.ent.unitAIState().split(".")[1] === "RETURNRESOURCE")){ // TODO: handle combat for hunting animals if (!this.ent.resourceCarrying() || this.ent.resourceCarrying().length === 0 || @@ -42,9 +42,11 @@ Worker.prototype.update = function(gameState) { if (this.gatheringFrom){ var ent = gameState.getEntityById(this.gatheringFrom); if (ent && ent.resourceSupplyAmount() == ent.resourceSupplyMax()){ - if (this.approachCount > 0){ + // if someone gathers from it, it's only that the pathfinder sucks. + if (this.approachCount > 0 && ent.getMetadata("gatherer-count") <= 2){ ent.setMetadata("inaccessible", true); this.ent.setMetadata("subrole", "idle"); + this.ent.flee(ent); } this.approachCount++; }else{ @@ -55,7 +57,7 @@ Worker.prototype.update = function(gameState) { } } } - }else if(subrole === "builder"){ + } else if(subrole === "builder") { if (this.ent.unitAIState().split(".")[1] !== "REPAIR"){ var target = this.ent.getMetadata("target-foundation"); this.ent.repair(target); @@ -72,44 +74,89 @@ Worker.prototype.update = function(gameState) { Worker.prototype.updateGathererCounts = function(gameState, dead){ // update gatherer counts for the resources if (this.ent.unitAIState().split(".")[2] === "GATHERING" && !dead){ - if (this.gatheringFrom !== this.ent.unitAIOrderData().target){ + if (this.gatheringFrom !== this.ent.unitAIOrderData()[0].target){ if (this.gatheringFrom){ var ent = gameState.getEntityById(this.gatheringFrom); - if (ent){ + if (ent && ent.resourceSupplyType()){ ent.setMetadata("gatherer-count", ent.getMetadata("gatherer-count") - 1); - this.markFull(ent); + this.markFull(gameState,ent); } } - this.gatheringFrom = this.ent.unitAIOrderData().target; + this.gatheringFrom = this.ent.unitAIOrderData()[0].target; if (this.gatheringFrom){ var ent = gameState.getEntityById(this.gatheringFrom); - if (ent){ + if (ent && ent.resourceSupplyType()){ ent.setMetadata("gatherer-count", (ent.getMetadata("gatherer-count") || 0) + 1); - this.markFull(ent); + this.markFull(gameState,ent); } } } - }else{ + } else if (this.ent.unitAIState().split(".")[2] === "RETURNRESOURCE" && !dead) { + // We remove us from the counting is we have no following order or its not "return to collected resource". + if (this.ent.unitAIOrderData().length === 1) { + var ent = gameState.getEntityById(this.gatheringFrom); + if (ent && ent.resourceSupplyType()){ + ent.setMetadata("gatherer-count", ent.getMetadata("gatherer-count") - 1); + this.markFull(gameState,ent); + } + this.gatheringFrom = undefined; + } + if (!this.ent.unitAIOrderData()[1].target || this.gatheringFrom !== this.ent.unitAIOrderData()[1].target){ + if (this.gatheringFrom){ + var ent = gameState.getEntityById(this.gatheringFrom); + if (ent && ent.resourceSupplyType()){ + ent.setMetadata("gatherer-count", ent.getMetadata("gatherer-count") - 1); + this.markFull(gameState,ent); + } + } + this.gatheringFrom = undefined; + } + } else { if (this.gatheringFrom){ var ent = gameState.getEntityById(this.gatheringFrom); - if (ent){ + if (ent && ent.resourceSupplyType()){ ent.setMetadata("gatherer-count", ent.getMetadata("gatherer-count") - 1); - this.markFull(ent); + this.markFull(gameState,ent); } this.gatheringFrom = undefined; } } }; -Worker.prototype.markFull = function(ent){ - var maxCounts = {"food": 15, "wood": 5, "metal": 15, "stone": 15, "treasure": 1}; - if (ent.resourceSupplyType() && ent.getMetadata("gatherer-count") >= maxCounts[ent.resourceSupplyType().generic]){ +Worker.prototype.markFull = function(gameState,ent){ + var maxCounts = {"food": 15, "wood": 6, "metal": 15, "stone": 15, "treasure": 1}; + var resource = ent.resourceSupplyType().generic; + if (ent.resourceSupplyType() && ent.getMetadata("gatherer-count") >= maxCounts[resource]){ if (!ent.getMetadata("full")){ ent.setMetadata("full", true); + // update the dropsite + var dropsite = gameState.getEntityById(ent.getMetadata("linked-dropsite")); + if (dropsite == undefined || dropsite.getMetadata("linked-resources-" + resource) === undefined) + return; + if (ent.getMetadata("linked-dropsite-nearby") == true) { + dropsite.setMetadata("resource-quantity-" + resource, +dropsite.getMetadata("resource-quantity-" + resource) - (+ent.getMetadata("dp-update-value"))); + dropsite.getMetadata("linked-resources-" + resource).updateEnt(ent); + dropsite.getMetadata("nearby-resources-" + resource).updateEnt(ent); + } else { + dropsite.setMetadata("resource-quantity-far-" + resource, +dropsite.getMetadata("resource-quantity-" + resource) - (+ent.getMetadata("dp-update-value"))); + dropsite.getMetadata("linked-resources-" + resource).updateEnt(ent); + } } }else{ if (ent.getMetadata("full")){ ent.setMetadata("full", false); + // update the dropsite + var dropsite = gameState.getEntityById(ent.getMetadata("linked-dropsite")); + if (dropsite == undefined || dropsite.getMetadata("linked-resources-" + resource) === undefined) + return; + if (ent.getMetadata("linked-dropsite-nearby") == true) { + dropsite.setMetadata("resource-quantity-" + resource, +dropsite.getMetadata("resource-quantity-" + resource) + ent.resourceSupplyAmount()); + dropsite.getMetadata("linked-resources-" + resource).updateEnt(ent); + dropsite.getMetadata("nearby-resources-" + resource).updateEnt(ent); + } else { + dropsite.setMetadata("resource-quantity-far-" + resource, +dropsite.getMetadata("resource-quantity-" + resource) + ent.resourceSupplyAmount()); + dropsite.getMetadata("linked-resources-" + resource).updateEnt(ent); + } } } }; @@ -123,23 +170,45 @@ Worker.prototype.startGathering = function(gameState){ return; } + // TODO: this is not necessarily optimal. + // find closest dropsite which has nearby resources of the correct type var minDropsiteDist = Math.min(); // set to infinity initially var nearestResources = undefined; var nearestDropsite = undefined; - gameState.updatingCollection("active-dropsite-" + resource, Filters.byMetadata("active-dropsite-" + resource, true), - gameState.getOwnDropsites(resource)).forEach(function (dropsite){ - if (dropsite.position()){ + // first, look for nearby resources. + var number = 0; + gameState.getOwnDropsites(resource).forEach(function (dropsite){ if (dropsite.getMetadata("linked-resources-" +resource) !== undefined + && dropsite.getMetadata("linked-resources-" +resource).length > 3) { number++; } }); + + gameState.getOwnDropsites(resource).forEach(function (dropsite){ //}){ + if (dropsite.getMetadata("resource-quantity-" +resource) == undefined) + return; + if (dropsite.position() && (dropsite.getMetadata("resource-quantity-" +resource) > 10 || number <= 1) ) { var dist = SquareVectorDistance(ent.position(), dropsite.position()); if (dist < minDropsiteDist){ minDropsiteDist = dist; - nearestResources = dropsite.getMetadata("nearby-resources-" + resource); + nearestResources = dropsite.getMetadata("linked-resources-" + resource); nearestDropsite = dropsite; } } }); - + // none, check even low level of resources and far away + if (!nearestResources || nearestResources.length === 0){ + gameState.getOwnDropsites(resource).forEach(function (dropsite){ //}){ + if (dropsite.position() && + (dropsite.getMetadata("resource-quantity-" +resource)+dropsite.getMetadata("resource-quantity-far-" +resource) > 10 || number <= 1)) { + var dist = SquareVectorDistance(ent.position(), dropsite.position()); + if (dist < minDropsiteDist){ + minDropsiteDist = dist; + nearestResources = dropsite.getMetadata("linked-resources-" + resource); + nearestDropsite = dropsite; + } + } + }); + } + // else, just get the closest to our closest dropsite. if (!nearestResources || nearestResources.length === 0){ nearestResources = gameState.getResourceSupplies(resource); gameState.getOwnDropsites(resource).forEach(function (dropsite){ @@ -161,28 +230,32 @@ Worker.prototype.startGathering = function(gameState){ return; } + if (!nearestDropsite) { + debug ("No dropsite for " +resource); + return; + } + var supplies = []; var nearestSupplyDist = Math.min(); var nearestSupply = undefined; - nearestResources.forEach(function(supply) { + nearestResources.forEach(function(supply) { //}){ // TODO: handle enemy territories if (!supply.position()){ return; } - if (supply.getMetadata("full") == true) { - return; - } - // measure the distance to the resource - var dist = VectorDistance(supply.position(), ent.position()); + // measure the distance to the resource (largely irrelevant) + var dist = SquareVectorDistance(supply.position(), ent.position()); + // Add on a factor for the nearest dropsite if one exists - if (nearestDropsite){ - dist += 5 * VectorDistance(supply.position(), nearestDropsite.position()); + if (supply.getMetadata("linked-dropsite") !== undefined){ + dist += 4*SquareVectorDistance(supply.position(), nearestDropsite.position()); + dist /= 5.0; } // Go for treasure as a priority - if (dist < 1000 && supply.resourceSupplyType().generic == "treasure"){ + if (dist < 40000 && supply.resourceSupplyType().generic == "treasure"){ dist /= 1000; } @@ -197,8 +270,11 @@ Worker.prototype.startGathering = function(gameState){ // if the resource is far away, try to build a farm instead. var tried = false; - if (resource === "food" && SquareVectorDistance(pos,this.ent.position()) > 50000) + if (resource === "food" && SquareVectorDistance(pos,this.ent.position()) > 22500) tried = this.buildAnyField(gameState); + if (!tried && SquareVectorDistance(pos,this.ent.position()) > 62500) { + return; // wait. a farm should appear. + } if (!tried) { var territoryOwner = gameState.getTerritoryMap().getOwner(pos); if (!gameState.ai.accessibility.isAccessible(pos) || diff --git a/binaries/data/mods/public/simulation/ai/qbot/worker.js b/binaries/data/mods/public/simulation/ai/qbot/worker.js index 196afb35bf..f31bc6de3c 100644 --- a/binaries/data/mods/public/simulation/ai/qbot/worker.js +++ b/binaries/data/mods/public/simulation/ai/qbot/worker.js @@ -16,8 +16,8 @@ Worker.prototype.update = function(gameState) { } if (subrole === "gatherer"){ - if (!(this.ent.unitAIState().split(".")[1] === "GATHER" && this.ent.unitAIOrderData().type - && this.getResourceType(this.ent.unitAIOrderData().type) === this.ent.getMetadata("gather-type")) + if (!(this.ent.unitAIState().split(".")[1] === "GATHER" && this.ent.unitAIOrderData()[0].type + && this.getResourceType(this.ent.unitAIOrderData()[0].type) === this.ent.getMetadata("gather-type")) && !(this.ent.unitAIState().split(".")[1] === "RETURNRESOURCE")){ // TODO: handle combat for hunting animals if (!this.ent.resourceCarrying() || this.ent.resourceCarrying().length === 0 || @@ -72,7 +72,7 @@ Worker.prototype.update = function(gameState) { Worker.prototype.updateGathererCounts = function(gameState, dead){ // update gatherer counts for the resources if (this.ent.unitAIState().split(".")[2] === "GATHERING" && !dead){ - if (this.gatheringFrom !== this.ent.unitAIOrderData().target){ + if (this.gatheringFrom !== this.ent.unitAIOrderData()[0].target){ if (this.gatheringFrom){ var ent = gameState.getEntityById(this.gatheringFrom); if (ent){ @@ -80,7 +80,7 @@ Worker.prototype.updateGathererCounts = function(gameState, dead){ this.markFull(ent); } } - this.gatheringFrom = this.ent.unitAIOrderData().target; + this.gatheringFrom = this.ent.unitAIOrderData()[0].target; if (this.gatheringFrom){ var ent = gameState.getEntityById(this.gatheringFrom); if (ent){ diff --git a/binaries/data/mods/public/simulation/components/UnitAI.js b/binaries/data/mods/public/simulation/components/UnitAI.js index 3f8cb2413c..0515c6d330 100644 --- a/binaries/data/mods/public/simulation/components/UnitAI.js +++ b/binaries/data/mods/public/simulation/components/UnitAI.js @@ -2171,10 +2171,12 @@ UnitAI.prototype.AddOrders = function(orders) UnitAI.prototype.GetOrderData = function() { - if (this.order && this.order.data) - return deepcopy(this.order.data); - else - return undefined; + var orders = []; + for (i in this.orderQueue) { + if (this.orderQueue[i].data) + orders.push(deepcopy(this.orderQueue[i].data)); + } + return orders; }; UnitAI.prototype.TimerHandler = function(data, lateness)