This was SVN commit r9309.

This commit is contained in:
James Baillie 2011-04-23 18:34:03 +00:00
parent 2d04d78db8
commit 17eae9d92a
10 changed files with 2058 additions and 0 deletions

View File

@ -0,0 +1 @@
Engine.IncludeModule("common-api");

View File

@ -0,0 +1,5 @@
{
"name": "JuBot",
"description": "Jubal's improved version of the 0AD TestBot.",
"constructor": "TestBotAI"
}

View File

@ -0,0 +1,570 @@
var EconomyManager = Class({
_init: function()
{
this.baseNumWorkers = 30; // minimum number of workers we want
this.targetNumWorkers = 55; // minimum number of workers we want
this.targetNumBuilders = 6; // number of workers we want working on construction
this.changetimeRegBui = 180*1000;
// (This is a stupid design where we just construct certain numbers
// of certain buildings in sequence)
// Greek building list
// Relative proportions of workers to assign to each resource type
this.gatherWeights = {
"food": 140,
"wood": 140,
"stone": 50,
"metal": 120,
};
},
checkBuildingList: function (gameState) {
if (gameState.displayCiv() == "hele"){
this.targetBuildings = [
{
"template": "structures/{civ}_civil_centre",
"priority": 500,
"count": 1,
},
{
"template": "structures/{civ}_house",
"priority": 110,
"count": 2,
},
{
"template": "structures/{civ}_scout_tower",
"priority": 105,
"count": 1,
},
{
"template": "structures/{civ}_field",
"priority": 103,
"count": 1,
},
{
"template": "structures/{civ}_barracks",
"priority": 101,
"count": 1,
},
{
"template": "structures/{civ}_house",
"priority": 100,
"count": 5,
},
{
"template": "structures/{civ}_scout_tower",
"priority": 90,
"count": 3,
},
{
"template": "structures/hele_gymnasion",
"priority": 80,
"count": 1,
},
{
"template": "structures/{civ}_field",
"priority": 70,
"count": 2,
},
{
"template": "structures/hele_fortress",
"priority": 60,
"count": 1,
},
{
"template": "structures/{civ}_house",
"priority": 55,
"count": 10,
},
{
"template": "structures/{civ}_scout_tower",
"priority": 50,
"count": 5,
},
{
"template": "structures/{civ}_house",
"priority": 45,
"count": 15,
},
{
"template": "structures/{civ}_field",
"priority": 40,
"count": 5,
},
{
"template": "structures/{civ}_house",
"priority": 30,
"count": 20,
},
];
}
// Celt building list
else if (gameState.displayCiv() == "celt"){
this.targetBuildings = [
{
"template": "structures/{civ}_civil_centre",
"priority": 500,
"count": 1,
},
{
"template": "structures/{civ}_house",
"priority": 110,
"count": 6,
},
{
"template": "structures/{civ}_field",
"priority": 100,
"count": 2,
},
{
"template": "structures/{civ}_barracks",
"priority": 100,
"count": 1,
},
{
"template": "structures/celt_fortress_b",
"priority": 80,
"count": 1,
},
{
"template": "structures/{civ}_scout_tower",
"priority": 70,
"count": 3,
},
{
"template": "structures/{civ}_house",
"priority": 55,
"count": 15,
},
{
"template": "structures/{civ}_field",
"priority": 40,
"count": 5,
},
{
"template": "structures/{civ}_house",
"priority": 30,
"count": 20,
},
];
}
// Celt building list
else if (gameState.displayCiv() == "iber"){
this.targetBuildings = [
{
"template": "structures/{civ}_civil_centre",
"priority": 500,
"count": 1,
},
{
"template": "structures/{civ}_house",
"priority": 110,
"count": 6,
},
{
"template": "structures/{civ}_field",
"priority": 100,
"count": 2,
},
{
"template": "structures/{civ}_barracks",
"priority": 100,
"count": 1,
},
{
"template": "structures/iber_fortress",
"priority": 80,
"count": 1,
},
{
"template": "structures/{civ}_scout_tower",
"priority": 70,
"count": 3,
},
{
"template": "structures/{civ}_house",
"priority": 55,
"count": 15,
},
{
"template": "structures/{civ}_field",
"priority": 40,
"count": 5,
},
{
"template": "structures/{civ}_house",
"priority": 30,
"count": 20,
},
];
}
// Fallback option just in case
else {
this.targetBuildings = [
{
"template": "structures/{civ}_civil_centre",
"priority": 500,
"count": 1,
},
{
"template": "structures/{civ}_house",
"priority": 110,
"count": 2,
},
{
"template": "structures/{civ}_scout_tower",
"priority": 105,
"count": 1,
},
{
"template": "structures/{civ}_house",
"priority": 100,
"count": 5,
},
{
"template": "structures/{civ}_field",
"priority": 100,
"count": 2,
},
{
"template": "structures/{civ}_barracks",
"priority": 99,
"count": 1,
},
{
"template": "structures/{civ}_house",
"priority": 98,
"count": 7,
},
{
"template": "structures/{civ}_scout_tower",
"priority": 60,
"count": 4,
},
{
"template": "structures/{civ}_house",
"priority": 55,
"count": 15,
},
{
"template": "structures/{civ}_field",
"priority": 40,
"count": 5,
},
{
"template": "structures/{civ}_house",
"priority": 30,
"count": 20,
},
];
}
},
buildMoreBuildings: function(gameState, planGroups)
{
// Limit ourselves to constructing two buildings at a time
if (gameState.findFoundations().length > 1)
return;
for each (var building in this.targetBuildings)
{
var numBuildings = gameState.countEntitiesAndQueuedWithType(gameState.applyCiv(building.template));
// If we have too few, build another
if (numBuildings < building.count)
{
planGroups.economyConstruction.addPlan(building.priority,
new BuildingConstructionPlan(gameState, building.template, 1)
);
}
}
},
trainMoreWorkers: function(gameState, planGroups)
{
// Count the workers in the world and in progress
var numWorkers = gameState.countEntitiesAndQueuedWithRole("worker");
// If we have too few, train another
// print("Want "+this.targetNumWorkers+" workers; got "+numWorkers+"\n");
if (numWorkers < this.baseNumWorkers)
{
var menorwomen = Math.random()*2;
if (gameState.displayCiv() == "hele" || gameState.displayCiv() == "celt"){
if (menorwomen < 1.5){
planGroups.economyPersonnel.addPlan(120,
new UnitTrainingPlan(gameState,
"units/{civ}_support_female_citizen", 2, { "role": "worker" })
);
}
else if (menorwomen > 1.82) {
planGroups.economyPersonnel.addPlan(120,
new UnitTrainingPlan(gameState,
"units/{civ}_infantry_spearman_b", 2, { "role": "worker" })
);
}
else {
planGroups.economyPersonnel.addPlan(120,
new UnitTrainingPlan(gameState,
"units/{civ}_infantry_javelinist_b", 2, { "role": "worker" })
);
}
}
else {
if (menorwomen < 1.5){
planGroups.economyPersonnel.addPlan(120,
new UnitTrainingPlan(gameState,
"units/{civ}_support_female_citizen", 2, { "role": "worker" })
);
}
else {
planGroups.economyPersonnel.addPlan(120,
new UnitTrainingPlan(gameState,
"units/{civ}_infantry_swordsman_b", 2, { "role": "worker" })
);
}
}
}
else if (numWorkers < this.targetNumWorkers)
{
var menorwomen = Math.random()*2;
if (gameState.displayCiv() == "hele" || gameState.displayCiv() == "celt"){
if (menorwomen < 1.5){
planGroups.economyPersonnel.addPlan(90,
new UnitTrainingPlan(gameState,
"units/{civ}_support_female_citizen", 2, { "role": "worker" })
);
}
else if (menorwomen > 1.82) {
planGroups.economyPersonnel.addPlan(90,
new UnitTrainingPlan(gameState,
"units/{civ}_infantry_spearman_b", 2, { "role": "worker" })
);
}
else {
planGroups.economyPersonnel.addPlan(90,
new UnitTrainingPlan(gameState,
"units/{civ}_infantry_javelinist_b", 2, { "role": "worker" })
);
}
}
else {
if (menorwomen < 1.5){
planGroups.economyPersonnel.addPlan(90,
new UnitTrainingPlan(gameState,
"units/{civ}_support_female_citizen", 2, { "role": "worker" })
);
}
else {
planGroups.economyPersonnel.addPlan(90,
new UnitTrainingPlan(gameState,
"units/{civ}_infantry_swordsman_b", 2, { "role": "worker" })
);
}
}
}
},
pickMostNeededResources: function(gameState)
{
var self = this;
// Find what resource type we're most in need of
var numGatherers = {};
for (var type in this.gatherWeights)
numGatherers[type] = 0;
gameState.getOwnEntitiesWithRole("worker").forEach(function(ent) {
if (ent.getMetadata("subrole") === "gatherer")
numGatherers[ent.getMetadata("gather-type")] += 1;
});
var types = Object.keys(this.gatherWeights);
types.sort(function(a, b) {
// Prefer fewer gatherers (divided by weight)
var va = numGatherers[a] / self.gatherWeights[a];
var vb = numGatherers[b] / self.gatherWeights[b];
return va - vb;
});
return types;
},
reassignRolelessUnits: function(gameState)
{
var roleless = gameState.getOwnEntitiesWithRole(undefined);
roleless.forEach(function(ent) {
if (ent.hasClass("Worker"))
ent.setMetadata("role", "worker");
else
ent.setMetadata("role", "randomcannonfodder");
});
},
reassignIdleWorkers: function(gameState, planGroups)
{
var self = this;
// Search for idle workers, and tell them to gather resources
// Maybe just pick a random nearby resource type at first;
// later we could add some timer that redistributes workers based on
// resource demand.
var idleWorkers = gameState.getOwnEntitiesWithRole("worker").filter(function(ent) {
return ent.isIdle();
});
if (idleWorkers.length)
{
var resourceSupplies = gameState.findResourceSupplies();
idleWorkers.forEach(function(ent) {
var types = self.pickMostNeededResources(gameState);
for each (var type in types)
{
// Make sure there's actually some of that type
if (!resourceSupplies[type])
continue;
// Pick the closest one.
// TODO: we should care about distance to dropsites, not (just) to the worker,
// and gather rates of workers
var workerPosition = ent.position();
var supplies = [];
resourceSupplies[type].forEach(function(supply) {
// Skip targets that are too hard to hunt
if (supply.entity.isUnhuntable())
return;
gameState.getOwnEntities().forEach(function(centre) {
if (centre.hasClass("CivCentre"))
{
var centrePosition = centre.position();
var distcheck = VectorDistance(supply.position, centrePosition);
// Skip targets that are far too far away (e.g. in the enemy base)
if (distcheck > 600)
return;
}
});
var dist = VectorDistance(supply.position, workerPosition);
// Skip targets that are far too far away (e.g. in the enemy base)
if (dist > 512)
return;
supplies.push({ dist: dist, entity: supply.entity });
});
supplies.sort(function (a, b) {
// Prefer smaller distances
if (a.dist != b.dist)
return a.dist - b.dist;
return false;
});
// Start gathering
if (supplies.length)
{
ent.gather(supplies[0].entity);
ent.setMetadata("subrole", "gatherer");
ent.setMetadata("gather-type", type);
return;
}
}
// Couldn't find any types to gather
ent.setMetadata("subrole", "idle");
});
}
},
assignToFoundations: function(gameState, planGroups)
{
// If we have some foundations, and we don't have enough builder-workers,
// try reassigning some other workers who are nearby
var foundations = gameState.findFoundations();
// Check if nothing to build
if (!foundations.length)
return;
var workers = gameState.getOwnEntitiesWithRole("worker");
var builderWorkers = workers.filter(function(ent) {
return (ent.getMetadata("subrole") === "builder");
});
// Check if enough builders
var extraNeeded = this.targetNumBuilders - builderWorkers.length;
if (extraNeeded <= 0)
return;
// Pick non-builders who are closest to the first foundation,
// and tell them to start building it
var target = foundations.toEntityArray()[0];
var nonBuilderWorkers = workers.filter(function(ent) {
return (ent.getMetadata("subrole") !== "builder");
});
var nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), extraNeeded);
// Order each builder individually, not as a formation
nearestNonBuilders.forEach(function(ent) {
ent.repair(target);
ent.setMetadata("subrole", "builder");
});
},
// This function recalls builders to the CC every 2 minutes; theoretically, this prevents the issue where they build a farm and people try to cross it and there's a traffic jam which wrecks the AI economy.
buildRegroup: function(gameState, planGroups)
{
if (gameState.getTimeElapsed() > this.changetimeRegBui){
var buildregroupers = gameState.getOwnEntitiesWithRole("worker");
buildregroupers.forEach(function(shirk) {
if (shirk.getMetadata("subrole") == "builder"){
var targets = gameState.entities.filter(function(ent) {
return (!ent.isEnemy() && ent.hasClass("CivCentre"));
});
if (targets.length){
var target = targets.toEntityArray()[0];
var targetPos = target.position();
shirk.move(targetPos[0], targetPos[1]);
}
}
});
// Wait 4 mins to do this again.
this.changetimeRegBui = this.changetimeRegBui + (120*1000);
}
},
update: function(gameState, planGroups)
{
Engine.ProfileStart("economy update");
this.buildRegroup(gameState, planGroups)
this.checkBuildingList(gameState);
this.reassignRolelessUnits(gameState);
this.buildMoreBuildings(gameState, planGroups);
this.trainMoreWorkers(gameState, planGroups);
this.reassignIdleWorkers(gameState, planGroups);
this.assignToFoundations(gameState, planGroups);
Engine.ProfileStop();
},
});

View File

@ -0,0 +1,210 @@
/**
* Provides an API for the rest of the AI scripts to query the world state
* at a higher level than the raw data.
*/
var GameState = Class({
_init: function(ai)
{
MemoizeInit(this);
this.ai = ai;
this.timeElapsed = ai.timeElapsed;
this.templates = ai.templates;
this.entities = ai.entities;
this.playerData = ai.playerData;
},
getTimeElapsed: function()
{
return this.timeElapsed;
},
getTemplate: function(type)
{
if (!this.templates[type])
return null;
return new EntityTemplate(this.templates[type]);
},
applyCiv: function(str)
{
return str.replace(/\{civ\}/g, this.playerData.civ);
},
displayCiv: function()
{
return this.playerData.civ;
},
getResources: function()
{
return new Resources(this.playerData.resourceCounts);
},
getMap: function()
{
return this.ai.map;
},
getPassabilityClassMask: function(name)
{
if (!(name in this.ai.passabilityClasses))
error("Tried to use invalid passability class name '"+name+"'");
return this.ai.passabilityClasses[name];
},
getOwnEntities: (function()
{
return new EntityCollection(this.ai, this.ai._ownEntities);
}),
getOwnEntitiesWithRole: Memoize('getOwnEntitiesWithRole', function(role)
{
var metas = this.ai._entityMetadata;
if (role === undefined)
return this.getOwnEntities().filter_raw(function(ent) {
var metadata = metas[ent.id];
if (!metadata || !('role' in metadata))
return true;
return (metadata.role === undefined);
});
else
return this.getOwnEntities().filter_raw(function(ent) {
var metadata = metas[ent.id];
if (!metadata || !('role' in metadata))
return false;
return (metadata.role === role);
});
}),
countEntitiesWithType: function(type)
{
var count = 0;
this.getOwnEntities().forEach(function(ent) {
var t = ent.templateName();
if (t == type)
++count;
});
return count;
},
countEntitiesAndQueuedWithType: function(type)
{
var foundationType = "foundation|" + type;
var count = 0;
this.getOwnEntities().forEach(function(ent) {
var t = ent.templateName();
if (t == type || t == foundationType)
++count;
var queue = ent.trainingQueue();
if (queue)
{
queue.forEach(function(item) {
if (item.template == type)
count += item.count;
});
}
});
return count;
},
countEntitiesAndQueuedWithRole: function(role)
{
var count = 0;
this.getOwnEntities().forEach(function(ent) {
if (ent.getMetadata("role") == role)
++count;
var queue = ent.trainingQueue();
if (queue)
{
queue.forEach(function(item) {
if (item.metadata && item.metadata.role == role)
count += item.count;
});
}
});
return count;
},
/**
* Find buildings that are capable of training the given unit type,
* and aren't already too busy.
*/
findTrainers: function(template)
{
var maxQueueLength = 3; // avoid tying up resources in giant training queues
return this.getOwnEntities().filter(function(ent) {
var trainable = ent.trainableEntities();
if (!trainable || trainable.indexOf(template) == -1)
return false;
var queue = ent.trainingQueue();
if (queue)
{
if (queue.length >= maxQueueLength)
return false;
}
return true;
});
},
/**
* Find units that are capable of constructing the given building type.
*/
findBuilders: function(template)
{
return this.getOwnEntities().filter(function(ent) {
var buildable = ent.buildableEntities();
if (!buildable || buildable.indexOf(template) == -1)
return false;
return true;
});
},
findFoundations: function(template)
{
return this.getOwnEntities().filter(function(ent) {
return (typeof ent.foundationProgress() !== "undefined");
});
},
findResourceSupplies: function()
{
var supplies = {};
this.entities.forEach(function(ent) {
var type = ent.resourceSupplyType();
if (!type)
return;
var amount = ent.resourceSupplyAmount();
if (!amount)
return;
var reportedType;
if (type.generic == "treasure")
reportedType = type.specific;
else
reportedType = type.generic;
if (!supplies[reportedType])
supplies[reportedType] = [];
supplies[reportedType].push({
"entity": ent,
"amount": amount,
"type": type,
"position": ent.position(),
});
});
return supplies;
},
});

View File

@ -0,0 +1,751 @@
/*
* Military strategy:
* * Try training an attack squad of a specified size
* * When it's the appropriate size, send it to attack the enemy
* * Repeat forever
*
*/
var MilitaryAttackManager = Class({
_init: function()
{
this.baserate = 11;
this.defsquad = 10;
this.defsquadmin = 2;
this.findatype = 1;
this.killstrat = 3;
this.changetime = 60*1000;
this.changetimeReg = 60*5000;
this.changetimeRegDef = 60*5000;
this.attacknumbers = 0.4
this.squadTypes = [
"units/{civ}_infantry_spearman_b",
"units/{civ}_infantry_javelinist_b",
// "units/{civ}_infantry_archer_b", // TODO: should only include this if hele
];
},
/**
* Returns the unit type we should begin training.
* (Currently this is whatever we have least of.)
*/
findBestNewUnit: function(gameState)
{
// Count each type
var types = [];
for each (var t in this.squadTypes)
types.push([t, gameState.countEntitiesAndQueuedWithType(t)]);
// Sort by increasing count
types.sort(function (a, b) { return a[1] - b[1]; });
// TODO: we shouldn't return units that we don't have any
// buildings capable of training
// Let's make this shizz random...
var randomiser = Math.floor(Math.random()*types.length);
return types[randomiser][0];
},
regroup: function(gameState, planGroups)
{
if (gameState.getTimeElapsed() > this.changetimeReg && this.killstrat != 3){
var regroupneeded = gameState.getOwnEntitiesWithRole("attack");
regroupneeded.forEach(function(ent) {
ent.setMetadata("role", "attack-pending");
});
var regroupneeded = gameState.getOwnEntitiesWithRole("attack_3p1");
regroupneeded.forEach(function(ent) {
ent.setMetadata("role", "attack-pending");
});
var regroupneeded = gameState.getOwnEntitiesWithRole("attack_3p2");
regroupneeded.forEach(function(ent) {
ent.setMetadata("role", "attack-pending");
});
var regroupneeded = gameState.getOwnEntitiesWithRole("attack_3p3");
regroupneeded.forEach(function(ent) {
ent.setMetadata("role", "attack-pending");
});
var regroupneeded = gameState.getOwnEntitiesWithRole("attack-pending_3p1");
regroupneeded.forEach(function(ent) {
ent.setMetadata("role", "attack-pending");
});
var regroupneeded = gameState.getOwnEntitiesWithRole("attack-pending_3p2");
regroupneeded.forEach(function(ent) {
ent.setMetadata("role", "attack-pending");
});
var regroupneeded = gameState.getOwnEntitiesWithRole("attack-pending_3p3");
regroupneeded.forEach(function(ent) {
ent.setMetadata("role", "attack-pending");
});
var regroupneeded = gameState.getOwnEntitiesWithRole("fighting");
regroupneeded.forEach(function(ent) {
ent.setMetadata("role", "attack-pending");
});
var regroupneededPartB = gameState.getOwnEntitiesWithRole("attack-pending");
//Find a friendsly CC
var targets = gameState.entities.filter(function(ent) {
return (!ent.isEnemy() && ent.hasClass("CivCentre"));
});
if (targets.length){
var target = targets.toEntityArray()[0];
var targetPos = target.position();
// TODO: this should be an attack-move command
regroupneededPartB.move(targetPos[0], targetPos[1]);
}
// Wait 4 mins to do this again.
this.changetimeReg = this.changetimeReg + (60*4000);
}
else if (gameState.getTimeElapsed() > this.changetimeReg && this.killstrat == 3){
var regroupneeded = gameState.getOwnEntitiesWithRole("attack");
regroupneeded.forEach(function(ent) {
var section = Math.random();
if (section < 0.3){
ent.setMetadata("role", "attack-pending_3p1");
}
else if (section < 0.6){
ent.setMetadata("role", "attack-pending_3p2");
}
else {
ent.setMetadata("role", "attack-pending_3p3");
}
});
var regroupneeded = gameState.getOwnEntitiesWithRole("attack-pending");
regroupneeded.forEach(function(ent) {
var section = Math.random();
if (section < 0.3){
ent.setMetadata("role", "attack-pending_3p1");
}
else if (section < 0.6){
ent.setMetadata("role", "attack-pending_3p2");
}
else {
ent.setMetadata("role", "attack-pending_3p3");
}
});
var regroupneeded = gameState.getOwnEntitiesWithRole("attack_3p1");
regroupneeded.forEach(function(ent) {
ent.setMetadata("role", "attack-pending_3p1");
});
var regroupneeded = gameState.getOwnEntitiesWithRole("attack_3p2");
regroupneeded.forEach(function(ent) {
ent.setMetadata("role", "attack-pending_3p2");
});
var regroupneeded = gameState.getOwnEntitiesWithRole("attack_3p3");
regroupneeded.forEach(function(ent) {
ent.setMetadata("role", "attack-pending_3p3");
});
var regroupneeded = gameState.getOwnEntitiesWithRole("fighting");
regroupneeded.forEach(function(ent) {
var section = Math.random();
if (section < 0.3){
ent.setMetadata("role", "attack-pending_3p1");
}
else if (section < 0.6){
ent.setMetadata("role", "attack-pending_3p2");
}
else {
ent.setMetadata("role", "attack-pending_3p3");
}
});
var regroupneededPartB = gameState.getOwnEntitiesWithRole("attack-pending");
//Find a friendsly CC
var targets = gameState.entities.filter(function(ent) {
return (!ent.isEnemy() && ent.hasClass("CivCentre"));
});
if (targets.length){
var target = targets.toEntityArray()[0];
var targetPos = target.position();
// TODO: this should be an attack-move command
regroupneededPartB.move(targetPos[0], targetPos[1]);
}
// Wait 4 mins to do this again.
this.changetimeReg = this.changetimeReg + (60*4000);
}
},
combatcheck: function(gameState, planGroups)
{
var regroupneeded = gameState.getOwnEntitiesWithRole("attack");
regroupneeded.forEach(function(troop) {
var currentPosition = troop.position();
var targets = gameState.entities.filter(function(ent) {
var foeposition = ent.position();
if (foeposition){
var dist = VectorDistance(foeposition, currentPosition);
return (ent.isEnemy() && ent.owner()!= 0 && dist < 50);
}
});
if (targets.length >= 5){
regroupneeded.forEach(function(person) {
var targetrandomiser = Math.floor(Math.random()*targets.length);
var target = targets.toEntityArray()[targetrandomiser];
var targetPos = target.position();
// TODO: this should be an attack-move command
person.move(targetPos[0], targetPos[1]);
person.setMetadata("role", "fighting");
});
}
});
},
combatcheck3p1: function(gameState, planGroups)
{
var regroupneeded = gameState.getOwnEntitiesWithRole("attack_3p1");
regroupneeded.forEach(function(troop) {
var currentPosition = troop.position();
var targets = gameState.entities.filter(function(ent) {
var foeposition = ent.position();
if (foeposition){
var dist = VectorDistance(foeposition, currentPosition);
return (ent.isEnemy() && ent.owner()!= 0 && dist < 50);
}
});
if (targets.length >= 5){
regroupneeded.forEach(function(person) {
var targetrandomiser = Math.floor(Math.random()*targets.length);
var target = targets.toEntityArray()[targetrandomiser];
var targetPos = target.position();
// TODO: this should be an attack-move command
person.move(targetPos[0], targetPos[1]);
person.setMetadata("role", "fighting");
});
}
});
},
combatcheck3p2: function(gameState, planGroups)
{
var regroupneeded = gameState.getOwnEntitiesWithRole("attack_3p2");
regroupneeded.forEach(function(troop) {
var currentPosition = troop.position();
var targets = gameState.entities.filter(function(ent) {
var foeposition = ent.position();
if (foeposition){
var dist = VectorDistance(foeposition, currentPosition);
return (ent.isEnemy() && ent.owner()!= 0 && dist < 50);
}
});
if (targets.length >= 5){
regroupneeded.forEach(function(person) {
var targetrandomiser = Math.floor(Math.random()*targets.length);
var target = targets.toEntityArray()[targetrandomiser];
var targetPos = target.position();
// TODO: this should be an attack-move command
person.move(targetPos[0], targetPos[1]);
person.setMetadata("role", "fighting");
});
}
});
},
combatcheck3p3: function(gameState, planGroups)
{
var regroupneeded = gameState.getOwnEntitiesWithRole("attack_3p3");
regroupneeded.forEach(function(troop) {
var currentPosition = troop.position();
var targets = gameState.entities.filter(function(ent) {
var foeposition = ent.position();
if (foeposition){
var dist = VectorDistance(foeposition, currentPosition);
return (ent.isEnemy() && ent.owner()!= 0 && dist < 50);
}
});
if (targets.length >= 5){
regroupneeded.forEach(function(person) {
var targetrandomiser = Math.floor(Math.random()*targets.length);
var target = targets.toEntityArray()[targetrandomiser];
var targetPos = target.position();
// TODO: this should be an attack-move command
person.move(targetPos[0], targetPos[1]);
person.setMetadata("role", "fighting");
});
}
});
},
defenseregroup: function(gameState, planGroups)
{
if (gameState.getTimeElapsed() > this.changetimeRegDef){
var defenseregroupers = gameState.getOwnEntitiesWithRole("defenders");
//Find a friendsly CC
var targets = gameState.entities.filter(function(ent) {
return (!ent.isEnemy() && ent.hasClass("CivCentre"));
});
if (targets.length){
var target = targets.toEntityArray()[0];
var targetPos = target.position();
// TODO: this should be an attack-move command
defenseregroupers.move(targetPos[0], targetPos[1]);
}
// Wait 4 mins to do this again.
this.changetimeRegDef = this.changetimeRegDef + (60*1500);
}
},
trainDefenderSquad: function(gameState, planGroups)
{
var pendingdefense = gameState.getOwnEntitiesWithRole("defenders");
//TestBotAI.prototype.chat("Number of defenders is" + pendingdefense.length);
if (pendingdefense.length < this.defsquadmin && gameState.displayCiv() == "iber"){
planGroups.economyPersonnel.addPlan(122,
new UnitTrainingPlan(gameState,
"units/{civ}_infantry_swordsman_b", 3, { "role": "defenders" })
);
}
else if (pendingdefense.length < this.defsquadmin){
planGroups.economyPersonnel.addPlan(122,
new UnitTrainingPlan(gameState,
"units/{civ}_infantry_spearman_b", 3, { "role": "defenders" })
);
//TestBotAI.prototype.chat("Training defenders");
}
else if (pendingdefense.length < this.defsquad && gameState.displayCiv() == "iber"){
planGroups.economyPersonnel.addPlan(110,
new UnitTrainingPlan(gameState,
"units/{civ}_infantry_swordsman_b", 3, { "role": "defenders" })
);
//TestBotAI.prototype.chat("Training defenders");
}
else if (pendingdefense.length < this.defsquad){
planGroups.economyPersonnel.addPlan(110,
new UnitTrainingPlan(gameState,
"units/{civ}_infantry_spearman_b", 3, { "role": "defenders" })
);
//TestBotAI.prototype.chat("Training defenders");
}
},
trainSomeTroops: function(gameState, planGroups, type)
{
var trainers = gameState.findTrainers(gameState.applyCiv(type));
if (trainers.length != 0){
planGroups.economyPersonnel.addPlan(100,
new UnitTrainingPlan(gameState,
type, 3, { "role": "attack-pending" })
);
}
else {
this.attacknumbers = 0.9;
}
},
trainMachine: function(gameState, planGroups, type)
{
var trainers = gameState.findTrainers(gameState.applyCiv(type));
if (trainers.length != 0){
planGroups.economyPersonnel.addPlan(100,
new UnitTrainingPlan(gameState,
type, 1, { "role": "attack-pending" })
);
}
else {
this.attacknumbers = 0.9;
}
},
trainSomeTroops3prong: function(gameState, planGroups, type)
{
var trainers = gameState.findTrainers(gameState.applyCiv(type));
var section = Math.random();
if (trainers.length != 0 && section < 0.3){
planGroups.economyPersonnel.addPlan(100,
new UnitTrainingPlan(gameState,
type, 3, { "role": "attack-pending_3p1" })
);
}
else if (trainers.length != 0 && section < 0.6){
planGroups.economyPersonnel.addPlan(100,
new UnitTrainingPlan(gameState,
type, 3, { "role": "attack-pending_3p2" })
);
}
else if (trainers.length != 0){
planGroups.economyPersonnel.addPlan(100,
new UnitTrainingPlan(gameState,
type, 3, { "role": "attack-pending_3p3" })
);
}
else {
this.attacknumbers = 0.9;
}
},
trainAttackSquad: function(gameState, planGroups)
{
if (gameState.getTimeElapsed() > this.changetime){
this.attacknumbers = Math.random();
this.changetime = this.changetime + (60*1000);
}
// Training lists for full assaults
if (this.killstrat == 1){
//Greeks
if (gameState.displayCiv() == "hele"){
if (this.attacknumbers < 0.19){
this.trainSomeTroops(gameState, planGroups, "units/hele_super_infantry_polis");
}
else if (this.attacknumbers < 0.26){
this.trainSomeTroops(gameState, planGroups, "units/hele_super_ranged_polis");
}
else if (this.attacknumbers < 0.35){
this.trainSomeTroops(gameState, planGroups, "units/hele_super_cavalry_mace");
}
else if (this.attacknumbers < 0.45){
this.trainSomeTroops(gameState, planGroups, "units/{civ}_infantry_archer_b");
}
else if (this.attacknumbers < 0.55){
this.trainSomeTroops(gameState, planGroups, "units/{civ}_cavalry_swordsman_b");
}
else if (this.attacknumbers < 0.65){
this.trainSomeTroops(gameState, planGroups, "units/{civ}_infantry_javelinist_b");
}
else if (this.attacknumbers < 0.75){
this.trainMachine(gameState, planGroups, "units/hele_mechanical_siege_lithobolos");
}
else {
this.trainSomeTroops(gameState, planGroups, "units/{civ}_infantry_spearman_b");
}
}
//Celts
else if (gameState.displayCiv() == "celt"){
if (this.attacknumbers < 0.25){
this.trainSomeTroops(gameState, planGroups, "units/celt_super_infantry_brit");
}
else if (this.attacknumbers < 0.45){
this.trainSomeTroops(gameState, planGroups, "units/{civ}_cavalry_swordsman_b");
}
else if (this.attacknumbers < 0.6){
this.trainSomeTroops(gameState, planGroups, "units/{civ}_infantry_javelinist_b");
}
else if (this.attacknumbers < 0.7){
this.trainMachine(gameState, planGroups, "units/celt_mechanical_siege_ram");
}
else {
this.trainSomeTroops(gameState, planGroups, "units/{civ}_infantry_spearman_b");
}
}
//Iberians
else if (gameState.displayCiv() == "iber"){
if (this.attacknumbers < 0.2){
this.trainSomeTroops(gameState, planGroups, "units/iber_super_infantry");
}
else if (this.attacknumbers < 0.3){
this.trainSomeTroops(gameState, planGroups, "units/iber_super_cavalry");
}
else if (this.attacknumbers < 0.4){
this.trainSomeTroops(gameState, planGroups, "units/{civ}_infantry_slinger_b");
}
else if (this.attacknumbers < 0.5){
this.trainSomeTroops(gameState, planGroups, "units/{civ}_infantry_spearman_b");
}
else if (this.attacknumbers < 0.6){
this.trainSomeTroops(gameState, planGroups, "units/{civ}_cavalry_spearman_b");
}
else if (this.attacknumbers < 0.7){
this.trainMachine(gameState, planGroups, "units/iber_mechanical_siege_ram");
}
else {
this.trainSomeTroops(gameState, planGroups, "units/{civ}_infantry_swordsman_b");
}
}
}
// Cav raiders training list
else if (this.killstrat == 2){
if (this.attacknumbers < 0.4 && gameState.displayCiv() == "hele"){
this.trainSomeTroops(gameState, planGroups, "units/{civ}_cavalry_javelinist_b");
}
else if (this.attacknumbers < 0.25 && gameState.displayCiv() == "celt"){
this.trainSomeTroops(gameState, planGroups, "units/celt_super_cavalry_brit");
}
else if (this.attacknumbers < 0.35 && gameState.displayCiv() == "iber"){
this.trainSomeTroops(gameState, planGroups, "units/iber_super_cavalry");
}
else if (this.attacknumbers < 0.6 && gameState.displayCiv() == "celt"){
this.trainSomeTroops(gameState, planGroups, "units/{civ}_cavalry_swordsman_b");
}
else if (this.attacknumbers < 0.6 && gameState.displayCiv() == "hele"){
this.trainSomeTroops(gameState, planGroups, "units/hele_super_cavalry_mace");
}
else if (gameState.displayCiv() == "iber"){
this.trainSomeTroops(gameState, planGroups, "units/{civ}_cavalry_spearman_b");
}
else if (gameState.displayCiv() == "celt"){
this.trainSomeTroops(gameState, planGroups, "units/{civ}_cavalry_javelinist_b");
}
else if (gameState.displayCiv() == "hele"){
this.trainSomeTroops(gameState, planGroups, "units/{civ}_cavalry_swordsman_b");
}
}
// 3 prong attack training list
else if (this.killstrat == 3){
//Greeks
if (gameState.displayCiv() == "hele"){
if (this.attacknumbers < 0.25){
this.trainSomeTroops3prong(gameState, planGroups, "units/{civ}_infantry_archer_b");
}
else if (this.attacknumbers < 0.5){
this.trainSomeTroops3prong(gameState, planGroups, "units/{civ}_infantry_javelinist_b");
}
else {
this.trainSomeTroops3prong(gameState, planGroups, "units/{civ}_infantry_spearman_b");
}
}
//Celts
else if (gameState.displayCiv() == "celt"){
if (this.attacknumbers < 0.45){
this.trainSomeTroops3prong(gameState, planGroups, "units/{civ}_cavalry_swordsman_b");
}
else if (this.attacknumbers < 0.6){
this.trainSomeTroops3prong(gameState, planGroups, "units/{civ}_infantry_javelinist_b");
}
else {
this.trainSomeTroops3prong(gameState, planGroups, "units/{civ}_infantry_spearman_b");
}
}
//Iberians
else if (gameState.displayCiv() == "iber"){
if (this.attacknumbers < 0.2){
this.trainSomeTroops3prong(gameState, planGroups, "units/{civ}_cavalry_spearman_b");
}
else if (this.attacknumbers < 0.4){
this.trainSomeTroops3prong(gameState, planGroups, "units/{civ}_infantry_slinger_b");
}
else {
this.trainSomeTroops3prong(gameState, planGroups, "units/{civ}_infantry_swordsman_b");
}
}
}
// Generic training list
else {
//Greeks
if (gameState.displayCiv() == "hele"){
if (this.attacknumbers < 0.25){
this.trainSomeTroops(gameState, planGroups, "units/{civ}_infantry_archer_b");
}
else if (this.attacknumbers < 0.5){
this.trainSomeTroops(gameState, planGroups, "units/{civ}_infantry_javelinist_b");
}
else {
this.trainSomeTroops(gameState, planGroups, "units/{civ}_infantry_spearman_b");
}
}
//Celts
else if (gameState.displayCiv() == "celt"){
if (this.attacknumbers < 0.45){
this.trainSomeTroops(gameState, planGroups, "units/{civ}_cavalry_swordsman_b");
}
else if (this.attacknumbers < 0.6){
this.trainSomeTroops(gameState, planGroups, "units/{civ}_infantry_javelinist_b");
}
else {
this.trainSomeTroops(gameState, planGroups, "units/{civ}_infantry_spearman_b");
}
}
//Iberians
else if (gameState.displayCiv() == "iber"){
if (this.attacknumbers < 0.2){
this.trainSomeTroops(gameState, planGroups, "units/{civ}_cavalry_spearman_b");
}
else if (this.attacknumbers < 0.4){
this.trainSomeTroops(gameState, planGroups, "units/{civ}_infantry_slinger_b");
}
else {
this.trainSomeTroops(gameState, planGroups, "units/{civ}_infantry_swordsman_b");
}
}
}
},
update: function(gameState, planGroups)
{
// Pause for a minute before starting any work, to give the economy a chance
// to start up
if (gameState.getTimeElapsed() < 60*1000)
return;
Engine.ProfileStart("military update");
// Also train up some defenders
this.combatcheck(gameState, planGroups);
this.combatcheck3p1(gameState, planGroups);
this.combatcheck3p2(gameState, planGroups);
this.combatcheck3p3(gameState, planGroups);
this.trainDefenderSquad(gameState, planGroups);
this.trainAttackSquad(gameState, planGroups);
this.regroup(gameState, planGroups);
this.defenseregroup(gameState, planGroups);
// Variable for impetuousness, so squads vary in size.
if (this.killstrat == 1){
this.baserate = 31;
}
else if (this.killstrat == 2) {
this.baserate = 10;
}
else if (this.killstrat == 3) {
this.baserate = 8;
}
else {
this.baserate = 15;
}
// Check we're doing a normal, not 3 pronged, attack
if (this.killstrat != 3){
// Find the units ready to join the attack
var pending = gameState.getOwnEntitiesWithRole("attack-pending");
if (pending.length >= this.baserate)
{
//Point full assaults at civ centres
if (this.killstrat == 1){
// Find the enemy CCs we could attack
var targets = gameState.entities.filter(function(ent) {
return (ent.isEnemy() && ent.hasClass("CivCentre"));
});
// If there's no CCs, attack anything else that's critical
if (targets.length == 0)
{
targets = gameState.entities.filter(function(ent) {
return (ent.isEnemy() && ent.hasClass("ConquestCritical"));
});
}
}
//Other attacks can go to any low-level structure
else {
var targets = gameState.entities.filter(function(ent) {
return (ent.isEnemy() && ent.hasClass("Village"));
});
}
// If we have a target, move to it
if (targets.length)
{
// Remove the pending role
pending.forEach(function(ent) {
ent.setMetadata("role", "attack");
});
var targetrandomiser = Math.floor(Math.random()*targets.length);
var target = targets.toEntityArray()[targetrandomiser];
var targetPos = target.position();
// TODO: this should be an attack-move command
pending.move(targetPos[0], targetPos[1]);
var otherguys = gameState.getOwnEntitiesWithRole("randomcannonfodder");
otherguys.move(targetPos[0], targetPos[1]);
}
//Now set whether to do a raid or full attack next time
var whatnext = Math.random();
if (whatnext > 0.85){
this.killstrat = 0;
// Regular "train a few guys and go kill stuff" type attack.
//TestBotAI.prototype.chat("Regular attack (" + gameState.displayCiv() + ")");
//TestBotAI.prototype.chat(whatnext);
}
else if (whatnext > 0.55) {
this.killstrat = 2;
//TestBotAI.prototype.chat("Cavalry raid (" + gameState.displayCiv() + ")");
//TestBotAI.prototype.chat(whatnext);
// Cavalry raid
}
else if (whatnext > 0.2) {
this.killstrat = 3;
//TestBotAI.prototype.chat("3 pronged assault (" + gameState.displayCiv() + ")");
//TestBotAI.prototype.chat(whatnext);
// 3 prong
}
else {
this.killstrat = 1;
//TestBotAI.prototype.chat("Full assault (" + gameState.displayCiv() + ")");
//TestBotAI.prototype.chat(whatnext);
//Full Assault!
}
}
}
// Here's the 3 pronged attack
else{
// Find the units ready to join the attack
var pending1 = gameState.getOwnEntitiesWithRole("attack-pending_3p1");
var pending2 = gameState.getOwnEntitiesWithRole("attack-pending_3p2");
var pending3 = gameState.getOwnEntitiesWithRole("attack-pending_3p3");
if (pending1.length >= this.baserate && pending2.length >= this.baserate && pending3.length >= this.baserate)
{
//Copy the target selector 3 times, once per attack squad
var targets1 = gameState.entities.filter(function(ent) {
return (ent.isEnemy() && ent.hasClass("Village"));
});
// If we have a target, move to it
if (targets1.length)
{
// Remove the pending role
pending1.forEach(function(ent) {
ent.setMetadata("role", "attack_3p1");
});
var targetrandomiser1 = Math.floor(Math.random()*targets1.length);
var target1 = targets1.toEntityArray()[targetrandomiser1];
var targetPos1 = target1.position();
pending1.move(targetPos1[0], targetPos1[1]);
var otherguys = gameState.getOwnEntitiesWithRole("randomcannonfodder");
otherguys.move(targetPos1[0], targetPos1[1]);
}
var targets2 = gameState.entities.filter(function(ent) {
return (ent.isEnemy() && ent.hasClass("Village"));
});
// If we have a target, move to it
if (targets2.length)
{
// Remove the pending role
pending2.forEach(function(ent) {
ent.setMetadata("role", "attack_3p2");
});
var targetrandomiser2 = Math.floor(Math.random()*targets2.length);
var target2 = targets2.toEntityArray()[targetrandomiser2];
var targetPos2 = target2.position();
pending2.move(targetPos2[0], targetPos2[1]);
}
var targets3 = gameState.entities.filter(function(ent) {
return (ent.isEnemy() && ent.hasClass("Village"));
});
// If we have a target, move to it
if (targets3.length)
{
// Remove the pending role
pending3.forEach(function(ent) {
ent.setMetadata("role", "attack_3p3");
});
var targetrandomiser3 = Math.floor(Math.random()*targets3.length);
var target3 = targets3.toEntityArray()[targetrandomiser3];
var targetPos3 = target3.position();
pending3.move(targetPos3[0], targetPos3[1]);
}
//Now set whether to do a raid or full attack next time
var whatnext = Math.random();
if (whatnext > 0.8){
this.killstrat = 0;
// Regular "train a few guys and go kill stuff" type attack.
//TestBotAI.prototype.chat("Regular attack (" + gameState.displayCiv() + ")");
//TestBotAI.prototype.chat(whatnext);
}
else if (whatnext > 0.5) {
this.killstrat = 2;
//TestBotAI.prototype.chat("Cavalry raid (" + gameState.displayCiv() + ")");
//TestBotAI.prototype.chat(whatnext);
// Cavalry raid
}
else if (whatnext > 0.3) {
this.killstrat = 3;
//TestBotAI.prototype.chat("3 pronged assault (" + gameState.displayCiv() + ")");
//TestBotAI.prototype.chat(whatnext);
// 3 prong
}
else {
this.killstrat = 1;
//TestBotAI.prototype.chat("Full assault (" + gameState.displayCiv() + ")");
//TestBotAI.prototype.chat(whatnext);
//Full Assault!
}
}
}
Engine.ProfileStop();
},
});

View File

@ -0,0 +1,220 @@
var BuildingConstructionPlan = Class({
_init: function(gameState, type, indno)
{
this.type = gameState.applyCiv(type);
var template = gameState.getTemplate(this.type);
if (!template)
{
this.invalidTemplate = true;
return;
}
this.cost = new Resources(template.cost());
},
canExecute: function(gameState)
{
if (this.invalidTemplate)
return false;
// TODO: verify numeric limits etc
var builders = gameState.findBuilders(this.type);
return (builders.length != 0);
},
execute: function(gameState)
{
// warn("Executing BuildingConstructionPlan "+uneval(this));
var builders = gameState.findBuilders(this.type).toEntityArray();
// We don't care which builder we assign, since they won't actually
// do the building themselves - all we care about is that there is
// some unit that can start the foundation
var pos = this.findGoodPosition(gameState);
builders[0].construct(this.type, pos.x, pos.z, pos.angle);
},
getCost: function()
{
return this.cost;
},
/**
* Make each cell's 16-bit value at least one greater than each of its
* neighbours' values. (If the grid is initialised with 0s and 65535s,
* the result of each cell is its Manhattan distance to the nearest 0.)
*
* TODO: maybe this should be 8-bit (and clamp at 255)?
*/
expandInfluences: function(grid, w, h)
{
for (var y = 0; y < h; ++y)
{
var min = 65535;
for (var x = 0; x < w; ++x)
{
var g = grid[x + y*w];
if (g > min) grid[x + y*w] = min;
else if (g < min) min = g;
++min;
}
for (var x = w-2; x >= 0; --x)
{
var g = grid[x + y*w];
if (g > min) grid[x + y*w] = min;
else if (g < min) min = g;
++min;
}
}
for (var x = 0; x < w; ++x)
{
var min = 65535;
for (var y = 0; y < h; ++y)
{
var g = grid[x + y*w];
if (g > min) grid[x + y*w] = min;
else if (g < min) min = g;
++min;
}
for (var y = h-2; y >= 0; --y)
{
var g = grid[x + y*w];
if (g > min) grid[x + y*w] = min;
else if (g < min) min = g;
++min;
}
}
},
/**
* Add a circular linear-falloff shape to a grid.
*/
addInfluence: function(grid, w, h, cx, cy, maxDist)
{
var x0 = Math.max(0, cx - maxDist);
var y0 = Math.max(0, cy - maxDist);
var x1 = Math.min(w, cx + maxDist);
var y1 = Math.min(h, cy + maxDist);
for (var y = y0; y < y1; ++y)
{
for (var x = x0; x < x1; ++x)
{
var dx = x - cx;
var dy = y - cy;
var r = Math.sqrt(dx*dx + dy*dy);
if (r < maxDist)
grid[x + y*w] += maxDist - r;
}
}
},
findGoodPosition: function(gameState)
{
var self = this;
var cellSize = 4; // size of each tile
// First, find all tiles that are far enough away from obstructions:
var map = gameState.getMap();
var obstructionMask = gameState.getPassabilityClassMask("foundationObstruction");
// Only accept valid land tiles (we don't handle docks yet)
obstructionMask |= gameState.getPassabilityClassMask("building-land");
var obstructionTiles = new Uint16Array(map.data.length);
for (var i = 0; i < map.data.length; ++i)
obstructionTiles[i] = (map.data[i] & obstructionMask) ? 0 : 65535;
// Engine.DumpImage("tiles0.png", obstructionTiles, map.width, map.height, 64);
this.expandInfluences(obstructionTiles, map.width, map.height);
// Compute each tile's closeness to friendly structures:
var friendlyTiles = new Uint16Array(map.data.length);
gameState.getOwnEntities().forEach(function(ent) {
if (ent.hasClass("Structure"))
{
var infl = 32;
if (ent.hasClass("CivCentre"))
infl *= 4;
var pos = ent.position();
var x = Math.round(pos[0] / cellSize);
var z = Math.round(pos[1] / cellSize);
self.addInfluence(friendlyTiles, map.width, map.height, x, z, infl);
}
});
//Look at making sure we're a long way from enemy civ centres as well.
var enemyTiles = new Uint16Array(map.data.length);
var foetargets = gameState.entities.filter(function(ent) {
return (ent.isEnemy());
});
foetargets.forEach(function(ent) {
if (ent.hasClass("CivCentre"))
{
var infl = 32;
var pos = ent.position();
var x = Math.round(pos[0] / cellSize);
var z = Math.round(pos[1] / cellSize);
self.addInfluence(enemyTiles, map.width, map.height, x, z, infl);
}
});
// Find target building's approximate obstruction radius,
// and expand by a bit to make sure we're not too close
var template = gameState.getTemplate(this.type);
var radius = Math.ceil(template.obstructionRadius() / cellSize) + 1;
// Find the best non-obstructed tile
var bestIdx = 0;
var bestVal = -1;
for (var i = 0; i < map.data.length; ++i)
{
if (obstructionTiles[i] > radius)
{
var v = friendlyTiles[i];
var foe = enemyTiles[i];
//TestBotAI.prototype.chat(v);
//TestBotAI.prototype.chat(i);
//TestBotAI.prototype.chat(foe);
if (v >= bestVal)
{
bestVal = v;
bestIdx = i;
//TestBotAI.prototype.chat("BestVal is " + bestVal + ", and bestIdx is " + bestIdx + ".");
}
}
}
var x = ((bestIdx % map.width) + 0.5) * cellSize;
var z = (Math.floor(bestIdx / map.width) + 0.5) * cellSize;
// Engine.DumpImage("tiles1.png", obstructionTiles, map.width, map.height, 32);
// Engine.DumpImage("tiles2.png", friendlyTiles, map.width, map.height, 256);
// Randomise the angle a little, to look less artificial
var angle = Math.PI + (Math.random()*2-1) * Math.PI/24;
return {
"x": x,
"z": z,
"angle": angle
};
},
});

View File

@ -0,0 +1,52 @@
var UnitTrainingPlan = Class({
_init: function(gameState, type, amount, metadata)
{
this.type = gameState.applyCiv(type);
this.amount = amount;
this.metadata = metadata;
var template = gameState.getTemplate(this.type);
if (!template)
{
this.invalidTemplate = true;
return;
}
this.cost = new Resources(template.cost());
this.cost.multiply(amount); // (assume no batch discount)
},
canExecute: function(gameState)
{
if (this.invalidTemplate)
return false;
// TODO: we should probably check pop caps
var trainers = gameState.findTrainers(this.type);
return (trainers.length != 0);
},
execute: function(gameState)
{
// warn("Executing UnitTrainingPlan "+uneval(this));
var trainers = gameState.findTrainers(this.type).toEntityArray();
// Prefer training buildings with short queues
// (TODO: this should also account for units added to the queue by
// plans that have already been executed this turn)
trainers.sort(function(a, b) {
return a.trainingQueueTime() - b.trainingQueueTime();
});
trainers[0].train(this.type, this.amount, this.metadata);
},
getCost: function()
{
return this.cost;
},
});

View File

@ -0,0 +1,67 @@
/**
* All plan classes must implement this interface.
*/
var IPlan = Class({
_init: function() { /* ... */ },
canExecute: function(gameState) { /* ... */ },
execute: function(gameState) { /* ... */ },
getCost: function() { /* ... */ },
});
/**
* Represents a prioritised collection of plans.
*/
var PlanGroup = Class({
_init: function()
{
this.escrow = new Resources({});
this.plans = [];
},
addPlan: function(priority, plan)
{
this.plans.push({"priority": priority, "plan": plan});
},
/**
* Executes all plans that we can afford, ordered by priority,
* and returns the highest-priority plan we couldn't afford (or null
* if none).
*/
executePlans: function(gameState)
{
// Ignore impossible plans
var plans = this.plans.filter(function(p) { return p.plan.canExecute(gameState); });
// Sort by decreasing priority
plans.sort(function(a, b) { return b.priority - a.priority; });
// Execute as many plans as we can afford
while (plans.length && this.escrow.canAfford(plans[0].plan.getCost()))
{
var plan = plans.shift().plan;
this.escrow.subtract(plan.getCost());
plan.execute(gameState);
}
if (plans.length)
return plans[0];
else
return null;
},
resetPlans: function()
{
this.plans = [];
},
getEscrow: function()
{
return this.escrow;
},
});

View File

@ -0,0 +1,36 @@
var Resources = Class({
types: ["food", "wood", "stone", "metal"],
_init: function(amounts)
{
for each (var t in this.types)
this[t] = amounts[t] || 0;
},
canAfford: function(that)
{
for each (var t in this.types)
if (this[t] < that[t])
return false;
return true;
},
add: function(that)
{
for each (var t in this.types)
this[t] += that[t];
},
subtract: function(that)
{
for each (var t in this.types)
this[t] -= that[t];
},
multiply: function(n)
{
for each (var t in this.types)
this[t] *= n;
},
});

View File

@ -0,0 +1,146 @@
/*
* This is a primitive initial attempt at an AI player.
* The design isn't great and maybe the whole thing should be rewritten -
* the aim here is just to have something that basically works, and to
* learn more about what's really needed for a decent AI design.
*
* The basic idea is we have a collection of independent modules
* (EconomyManager, etc) which produce a list of plans.
* The modules are mostly stateless - each turn they look at the current
* world state, and produce some plans that will improve the state.
* E.g. if there's too few worker units, they'll do a plan to train
* another one. Plans are discarded after the current turn, if they
* haven't been executed.
*
* Plans are grouped into a small number of PlanGroups, and for each
* group we try to execute the highest-priority plans.
* If some plan groups need more resources to execute their highest-priority
* plan, we'll distribute any unallocated resources to that group's
* escrow account. Eventually they'll accumulate enough to afford their plan.
* (The purpose is to ensure resources are shared fairly between all the
* plan groups - none of them should be starved even if they're trying to
* execute a really expensive plan.)
*/
/*
* Lots of things we should fix:
*
* * Find entities with no assigned role, and give them something to do
* * Keep some units back for defence
* * Consistent terminology (type vs template etc)
* * ...
*
*/
function TestBotAI(settings)
{
// warn("Constructing TestBotAI for player "+settings.player);
BaseAI.call(this, settings);
this.turn = 0;
this.modules = [
new EconomyManager(),
new MilitaryAttackManager(),
];
this.planGroups = {
economyPersonnel: new PlanGroup(),
economyConstruction: new PlanGroup(),
militaryPersonnel: new PlanGroup(),
};
}
TestBotAI.prototype = new BaseAI();
TestBotAI.prototype.ShareResources = function(remainingResources, unaffordablePlans)
{
// Share our remaining resources among the plangroups that need
// to accumulate more resources, in proportion to their priorities
for each (var type in remainingResources.types)
{
// Skip resource types where we don't have any spare
if (remainingResources[type] <= 0)
continue;
// Find the plans that require some of this resource type,
// and the sum of their priorities
var ps = [];
var sumPriority = 0;
for each (var p in unaffordablePlans)
{
if (p.plan.getCost()[type] > p.group.getEscrow()[type])
{
ps.push(p);
sumPriority += p.priority;
}
}
// Avoid divisions-by-zero
if (!sumPriority)
continue;
// Share resources by priority, clamped to the amount the plan actually needs
for each (var p in ps)
{
var amount = Math.floor(remainingResources[type] * p.priority / sumPriority);
var max = p.plan.getCost()[type] - p.group.getEscrow()[type];
p.group.getEscrow()[type] += Math.min(max, amount);
}
}
};
TestBotAI.prototype.OnUpdate = function()
{
// Run the update every n turns, offset depending on player ID to balance the load
if ((this.turn + this.player) % 4 == 0)
{
var gameState = new GameState(this);
// Find the resources we have this turn that haven't already
// been allocated to an escrow account.
// (We need to do this before executing any plans, because those will
// distort the escrow figures.)
var remainingResources = gameState.getResources();
for each (var planGroup in this.planGroups)
remainingResources.subtract(planGroup.getEscrow());
Engine.ProfileStart("plan setup");
// Compute plans from each module
for each (var module in this.modules)
module.update(gameState, this.planGroups);
// print(uneval(this.planGroups)+"\n");
Engine.ProfileStop();
Engine.ProfileStart("plan execute");
// Execute as many plans as possible, and keep a record of
// which ones we can't afford yet
var unaffordablePlans = [];
for each (var planGroup in this.planGroups)
{
var plan = planGroup.executePlans(gameState);
if (plan)
unaffordablePlans.push({"group": planGroup, "priority": plan.priority, "plan": plan.plan});
}
Engine.ProfileStop();
this.ShareResources(remainingResources, unaffordablePlans);
// print(uneval(this.planGroups)+"\n");
// Reset the temporary plan data
for each (var planGroup in this.planGroups)
planGroup.resetPlans();
}
if (this.turn == 0){
this.chat("Good morning. Please prepare for annhilation. Jubal apologises for any inconvenience likely to be caused by your imminent demise.");
}
this.turn++;
};