forked from 0ad/0ad
This was SVN commit r9309.
This commit is contained in:
parent
2d04d78db8
commit
17eae9d92a
1
binaries/data/mods/public/simulation/ai/jubot/_init.js
Normal file
1
binaries/data/mods/public/simulation/ai/jubot/_init.js
Normal file
@ -0,0 +1 @@
|
||||
Engine.IncludeModule("common-api");
|
5
binaries/data/mods/public/simulation/ai/jubot/data.json
Normal file
5
binaries/data/mods/public/simulation/ai/jubot/data.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "JuBot",
|
||||
"description": "Jubal's improved version of the 0AD TestBot.",
|
||||
"constructor": "TestBotAI"
|
||||
}
|
570
binaries/data/mods/public/simulation/ai/jubot/economy.js
Normal file
570
binaries/data/mods/public/simulation/ai/jubot/economy.js
Normal 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();
|
||||
},
|
||||
|
||||
});
|
210
binaries/data/mods/public/simulation/ai/jubot/gamestate.js
Normal file
210
binaries/data/mods/public/simulation/ai/jubot/gamestate.js
Normal 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;
|
||||
},
|
||||
});
|
751
binaries/data/mods/public/simulation/ai/jubot/military.js
Normal file
751
binaries/data/mods/public/simulation/ai/jubot/military.js
Normal 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();
|
||||
},
|
||||
|
||||
});
|
220
binaries/data/mods/public/simulation/ai/jubot/plan-building.js
Normal file
220
binaries/data/mods/public/simulation/ai/jubot/plan-building.js
Normal 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
|
||||
};
|
||||
},
|
||||
});
|
@ -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;
|
||||
},
|
||||
});
|
67
binaries/data/mods/public/simulation/ai/jubot/plan.js
Normal file
67
binaries/data/mods/public/simulation/ai/jubot/plan.js
Normal 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;
|
||||
},
|
||||
});
|
36
binaries/data/mods/public/simulation/ai/jubot/resources.js
Normal file
36
binaries/data/mods/public/simulation/ai/jubot/resources.js
Normal 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;
|
||||
},
|
||||
});
|
146
binaries/data/mods/public/simulation/ai/jubot/testbot.js
Normal file
146
binaries/data/mods/public/simulation/ai/jubot/testbot.js
Normal 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++;
|
||||
};
|
Loading…
Reference in New Issue
Block a user