1
0
forked from 0ad/0ad

Initial terrible AI player scripts.

This was SVN commit r8891.
This commit is contained in:
Ykkrosh 2011-02-05 20:35:34 +00:00
parent ff785853ad
commit 57e5bb878a
23 changed files with 973 additions and 86 deletions

View File

@ -6,6 +6,8 @@ function BaseAI(settings)
// Make some properties non-enumerable, so they won't be serialised
Object.defineProperty(this, "_player", {value: settings.player, enumerable: false});
Object.defineProperty(this, "_templates", {value: settings.templates, enumerable: false});
this._entityMetadata = {};
}
BaseAI.prototype.HandleMessage = function(state)
@ -15,24 +17,41 @@ BaseAI.prototype.HandleMessage = function(state)
else
this.ApplyEntitiesDelta(state);
//print("### "+uneval(state)+"\n\n");
//print("@@@ "+uneval(this._rawEntities)+"\n\n");
this.entities = new EntityCollection(this, this._rawEntities);
this.player = this._player;
this.playerData = state.players[this._player];
this.templates = this._templates;
this.timeElapsed = state.timeElapsed;
this.OnUpdate();
// Clean up temporary properties, so they don't disturb the serializer
delete this.entities;
delete this.player;
delete this.playerData;
delete this.templates;
delete this.timeElapsed;
};
BaseAI.prototype.ApplyEntitiesDelta = function(state)
{
for each (var evt in state.events)
{
if (evt.type == "Destroy")
if (evt.type == "Create")
{
this._rawEntities[evt.msg.entity] = {};
}
else if (evt.type == "Destroy")
{
delete this._rawEntities[evt.msg.entity];
delete this._entityMetadata[evt.msg.entity];
}
else if (evt.type == "TrainingFinished")
{
for each (var ent in evt.msg.entities)
{
this._entityMetadata[ent] = evt.msg.metadata;
}
}
}

View File

@ -0,0 +1,36 @@
/**
* Provides a nicer syntax for defining classes,
* with support for OO-style inheritance.
*/
function Class(data)
{
var ctor;
if (data._init)
ctor = data._init;
else
ctor = function() { };
if (data._super)
{
ctor.prototype = { "__proto__": data._super.prototype };
}
for (var key in data)
{
ctor.prototype[key] = data[key];
}
return ctor;
}
/* Test inheritance:
var A = Class({foo:1, bar:10});
print((new A).foo+" "+(new A).bar+"\n");
var B = Class({foo:2, bar:20});
print((new A).foo+" "+(new A).bar+"\n");
print((new B).foo+" "+(new B).bar+"\n");
var C = Class({_super:A, foo:3});
print((new A).foo+" "+(new A).bar+"\n");
print((new B).foo+" "+(new B).bar+"\n");
print((new C).foo+" "+(new C).bar+"\n");
//*/

View File

@ -1,86 +1,85 @@
function Entity(baseAI, entity)
{
this._ai = baseAI;
this._entity = entity;
this._template = baseAI._templates[entity.template];
}
var EntityTemplate = Class({
Entity.prototype = {
get rank() {
_init: function(template)
{
this._template = template;
},
rank: function() {
if (!this._template.Identity)
return undefined;
return this._template.Identity.Rank;
},
get classes() {
classes: function() {
if (!this._template.Identity || !this._template.Identity.Classes)
return undefined;
return this._template.Identity.Classes._string.split(/\s+/);
},
get civ() {
hasClass: function(name) {
var classes = this.classes();
return (classes && classes.indexOf(name) != -1);
},
civ: function() {
if (!this._template.Identity)
return undefined;
return this._template.Identity.Civ;
},
cost: function() {
if (!this._template.Cost)
return undefined;
get position() { return this._entity.position; },
var ret = {};
for (var type in this._template.Cost.Resources)
ret[type] = +this._template.Cost.Resources[type];
return ret;
},
get hitpoints() { return this._entity.hitpoints; },
get maxHitpoints() { return this._template.Health.Max; },
get isHurt() { return this.hitpoints < this.maxHitpoints; },
get needsHeal() { return this.isHurt && (this._template.Health.Healable == "true"); },
get needsRepair() { return this.isHurt && (this._template.Health.Repairable == "true"); },
maxHitpoints: function() { return this._template.Health.Max; },
isHealable: function() { return this._template.Health.Healable === "true"; },
isRepairable: function() { return this._template.Health.Repairable === "true"; },
// TODO: attack, armour
get buildableEntities() {
buildableEntities: function() {
if (!this._template.Builder)
return undefined;
var templates = this._template.Builder.Entities._string.replace(/\{civ\}/g, this.civ).split(/\s+/);
var civ = this.civ();
var templates = this._template.Builder.Entities._string.replace(/\{civ\}/g, civ).split(/\s+/);
return templates; // TODO: map to Entity?
},
get trainableEntities() {
trainableEntities: function() {
if (!this._template.TrainingQueue)
return undefined;
var templates = this._template.TrainingQueue.Entities._string.replace(/\{civ\}/g, this.civ).split(/\s+/);
var civ = this.civ();
var templates = this._template.TrainingQueue.Entities._string.replace(/\{civ\}/g, civ).split(/\s+/);
return templates;
},
get trainingQueue() { return this._entity.trainingQueue; },
get foundationProgress() { return this._entities.foundationProgress; },
get owner() { return this._entity.owner; },
get isOwn() { return this._entity.owner == this._ai._player; },
get isFriendly() { return this.isOwn; }, // TODO: diplomacy
get isEnemy() { return !this.isOwn; }, // TODO: diplomacy
get resourceSupplyType() {
resourceSupplyType: function() {
if (!this._template.ResourceSupply)
return undefined;
var [type, subtype] = this._template.ResourceSupply.Type.split('.');
return { "generic": type, "specific": subtype };
},
get resourceSupplyMax() {
resourceSupplyMax: function() {
if (!this._template.ResourceSupply)
return undefined;
return +this._template.ResourceSupply.Amount;
},
get resourceSupplyAmount() { return this._entity.resourceSupplyAmount; },
get resourceGatherRates() {
resourceGatherRates: function() {
if (!this._template.ResourceGatherer)
return undefined;
var ret = {};
@ -89,35 +88,161 @@ Entity.prototype = {
return ret;
},
get resourceCarrying() { return this._entity.resourceCarrying; },
get resourceDropsiteTypes() {
resourceDropsiteTypes: function() {
if (!this._template.ResourceDropsite)
return undefined;
return this._template.ResourceDropsite.Types.split(/\s+/);
},
get garrisoned() { return new EntityCollection(this._ai, this._entity.garrisoned); },
get garrisonableClasses() {
garrisonableClasses: function() {
if (!this._template.GarrisonHolder)
return undefined;
return this._template.GarrisonHolder.List._string.split(/\s+/);
},
});
var Entity = Class({
_super: EntityTemplate,
_init: function(baseAI, entity)
{
this._super.call(this, baseAI._templates[entity.template]);
this._ai = baseAI;
this._templateName = entity.template;
this._entity = entity;
},
toString: function() {
return "[Entity " + this.id() + " " + this.templateName() + "]";
},
id: function() {
return this._entity.id;
},
templateName: function() {
return this._templateName;
},
/**
* Returns extra data that the AI scripts have associated with this entity,
* for arbitrary local annotations.
* (This data is not shared with any other AI scripts.)
*/
getMetadata: function(id) {
var metadata = this._ai._entityMetadata[this.id()];
if (!metadata)
return undefined;
return metadata[id];
},
/**
* Sets extra data to be associated with this entity.
*/
setMetadata: function(id, value) {
var metadata = this._ai._entityMetadata[this.id()];
if (!metadata)
metadata = this._ai._entityMetadata[this.id()] = {};
metadata[id] = value;
},
position: function() { return this._entity.position; },
isIdle: function() {
if (typeof this._entity.idle === "undefined")
return undefined;
return this._entity.idle;
},
hitpoints: function() { return this._entity.hitpoints; },
isHurt: function() { return this.hitpoints < this.maxHitpoints; },
needsHeal: function() { return this.isHurt && this.isHealable; },
needsRepair: function() { return this.isHurt && this.isRepairable; },
/**
* Returns the current training queue state, of the form
* [ { "id": 0, "template": "...", "count": 1, "progress": 0.5, "metadata": ... }, ... ]
*/
trainingQueue: function() {
var queue = this._entity.trainingQueue;
return queue;
},
trainingQueueTime: function() {
var queue = this._entity.trainingQueue;
if (!queue)
return undefined;
// TODO: compute total time for units in training queue
return queue.length;
},
foundationProgress: function() { return this._entity.foundationProgress; },
owner: function() {
return this._entity.owner;
},
isOwn: function() {
if (typeof this._entity.owner === "undefined")
return false;
return this._entity.owner === this._ai._player;
},
isFriendly: function() {
return this.isOwn(); // TODO: diplomacy
},
isEnemy: function() {
return !this.isOwn(); // TODO: diplomacy
},
resourceSupplyAmount: function() { return this._entity.resourceSupplyAmount; },
resourceCarrying: function() { return this._entity.resourceCarrying; },
garrisoned: function() { return new EntityCollection(this._ai, this._entity.garrisoned); },
// TODO: visibility
move: function(x, z) {
Engine.PostCommand({"type": "walk", "entities": [this.entity.id], "x": x, "z": z, "queued": false});
Engine.PostCommand({"type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": false});
return this;
},
gather: function(target) {
Engine.PostCommand({"type": "gather", "entities": [this.id()], "target": target.id(), "queued": false});
return this;
},
destroy: function() {
Engine.PostCommand({"type": "delete-entities", "entities": [this.entity.id]});
Engine.PostCommand({"type": "delete-entities", "entities": [this.id()]});
return this;
},
};
train: function(type, count, metadata)
{
var trainable = this.trainableEntities();
if (!trainable)
{
error("Called train("+type+", "+count+") on non-training entity "+this);
return this;
}
if (trainable.indexOf(type) === -1)
{
error("Called train("+type+", "+count+") on entity "+this+" which can't train that");
return this;
}
Engine.PostCommand({
"type": "train",
"entity": this.id(),
"template": type,
"count": count,
"metadata": metadata
});
return this;
},
});

View File

@ -2,9 +2,14 @@ function EntityCollection(baseAI, entities)
{
this._ai = baseAI;
this._entities = entities;
var length = 0;
for (var id in entities)
++length;
this.length = length;
}
EntityCollection.prototype.ToIdArray = function()
EntityCollection.prototype.toIdArray = function()
{
var ret = [];
for (var id in this._entities)
@ -12,6 +17,19 @@ EntityCollection.prototype.ToIdArray = function()
return ret;
};
EntityCollection.prototype.toEntityArray = function()
{
var ret = [];
for each (var ent in this._entities)
ret.push(new Entity(this._ai, ent));
return ret;
};
EntityCollection.prototype.toString = function()
{
return "[EntityCollection " + this.toEntityArray().join(" ") + "]";
};
EntityCollection.prototype.filter = function(callback, thisp)
{
var ret = {};
@ -25,14 +43,25 @@ EntityCollection.prototype.filter = function(callback, thisp)
return new EntityCollection(this._ai, ret);
};
EntityCollection.prototype.forEach = function(callback, thisp)
{
for (var id in this._entities)
{
var ent = this._entities[id];
var val = new Entity(this._ai, ent);
callback.call(thisp, val, id, this);
}
return this;
};
EntityCollection.prototype.move = function(x, z)
{
Engine.PostCommand({"type": "walk", "entities": this.ToIdArray(), "x": x, "z": z, "queued": false});
Engine.PostCommand({"type": "walk", "entities": this.toIdArray(), "x": x, "z": z, "queued": false});
return this;
};
EntityCollection.prototype.destroy = function()
{
Engine.PostCommand({"type": "delete-entities", "entities": this.ToIdArray()});
Engine.PostCommand({"type": "delete-entities", "entities": this.toIdArray()});
return this;
};

View File

@ -0,0 +1,6 @@
function VectorDistance(a, b)
{
var dx = a[0] - b[0];
var dz = a[1] - b[1];
return Math.sqrt(dx*dx + dz*dz);
}

View File

@ -21,7 +21,7 @@ ScaredyBotAI.prototype.OnUpdate = function()
{
this.chat("I quake in my boots! My troops cannot hope to survive against a power such as yours.");
this.entities.filter(function(ent) { return ent.isOwn; }).destroy();
this.entities.filter(function(ent) { return ent.isOwn(); }).destroy();
}
this.turn++;

View File

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

View File

@ -0,0 +1,5 @@
{
"name": "Test Bot",
"description": "A simple AI for testing the framework.",
"constructor": "TestBotAI"
}

View File

@ -0,0 +1,69 @@
var EconomyManager = Class({
_init: function()
{
this.targetNumWorkers = 10; // minimum number of workers we want
},
update: 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.targetNumWorkers)
{
planGroups.economyPersonnel.addPlan(100,
new UnitTrainingPlan(gameState,
"units/hele_support_female_citizen", 1, { "role": "worker" })
);
}
// 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.entities.filter(function(ent) {
return (ent.getMetadata("role") === "worker" && ent.isIdle());
});
if (idleWorkers.length)
{
var resourceSupplies = gameState.findResourceSupplies(gameState);
idleWorkers.forEach(function(ent) {
// Pick a resource type at random
// TODO: should limit to what this worker can gather
var type = Resources.prototype.types[Math.floor(Math.random()*Resources.prototype.types.length)];
// Make sure there's actually some of that type
// (We probably shouldn't pick impossible ones in the first place)
if (!resourceSupplies[type])
return;
// Pick the closest one.
// TODO: we should care about distance to dropsites,
// and gather rates of workers
var workerPosition = ent.position();
var closestEntity = null;
var closestDist = Infinity;
resourceSupplies[type].forEach(function(supply) {
var dist = VectorDistance(supply.position, workerPosition);
if (dist < closestDist)
{
closestDist = dist;
closestEntity = supply.entity;
}
});
// Start gathering
if (closestEntity)
ent.gather(closestEntity);
});
}
},
});

View File

@ -0,0 +1,126 @@
/**
* 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(timeElapsed, templates, entities, playerData)
{
this.timeElapsed = timeElapsed;
this.templates = templates;
this.entities = entities;
this.playerData = playerData;
},
getTimeElapsed: function()
{
return this.timeElapsed;
},
getTemplate: function(type)
{
if (!this.templates[type])
return null;
return new EntityTemplate(this.templates[type]);
},
getResources: function()
{
return new Resources(this.playerData.resourceCounts);
},
getOwnEntities: function()
{
return this.entities.filter(function(ent) { return ent.isOwn(); });
},
countEntitiesAndQueuedWithType: function(type)
{
var count = 0;
this.getOwnEntities().forEach(function(ent) {
if (ent.templateName() == type)
++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;
});
},
findResourceSupplies: function(gameState)
{
var supplies = {};
this.entities.forEach(function(ent) {
var type = ent.resourceSupplyType();
if (!type)
return;
var amount = ent.resourceSupplyAmount();
if (!amount)
return;
if (!supplies[type.generic])
supplies[type.generic] = [];
supplies[type.generic].push({
"entity": ent,
"amount": amount,
"type": type,
"position": ent.position(),
});
});
return supplies;
},
});

View File

@ -0,0 +1,89 @@
/*
* 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.targetSquadSize = 10;
this.squadTypes = [
"units/hele_infantry_spearman_b",
"units/hele_infantry_javelinist_b",
"units/hele_infantry_archer_b",
];
},
/**
* 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]; });
return types[0][0];
},
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;
// Continually try training new units, in batches of 5
planGroups.militaryPersonnel.addPlan(100,
new UnitTrainingPlan(gameState,
this.findBestNewUnit(gameState), 5, { "role": "attack-pending" })
);
// Find the units ready to join the attack
var pending = gameState.entities.filter(function(ent) {
return (ent.getMetadata("role") === "attack-pending");
});
// If we have enough units yet, start the attack
if (pending.length >= this.targetSquadSize)
{
// 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"));
});
}
// If we have a target, move to it
if (targets.length)
{
// Remove the pending role
pending.forEach(function(ent) {
ent.setMetadata("role", "attack");
});
var target = targets.toEntityArray()[0];
var targetPos = target.position();
// TODO: this should be an attack-move command
pending.move(targetPos[0], targetPos[1]);
}
}
},
});

View File

@ -0,0 +1,42 @@
var UnitTrainingPlan = Class({
_init: function(gameState, type, amount, metadata)
{
this.type = type;
this.amount = amount;
this.metadata = metadata;
this.cost = new Resources(gameState.getTemplate(type).cost());
this.cost.multiply(amount); // (assume no batch discount)
},
canExecute: function(gameState)
{
// 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 a.priority > b.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,138 @@
/*
* 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:
*
* * Construct buildings (houses, farms, barracks)
* * Play as non-hele civs
* * 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(),
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.timeElapsed, this.templates, this.entities, this.playerData);
// 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());
// Compute plans from each module
for each (var module in this.modules)
module.update(gameState, this.planGroups);
// print(uneval(this.planGroups)+"\n");
// 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});
}
this.ShareResources(remainingResources, unaffordablePlans);
// print(uneval(this.planGroups)+"\n");
// Reset the temporary plan data
for each (var planGroup in this.planGroups)
planGroup.resetPlans();
}
this.turn++;
};

View File

@ -33,35 +33,13 @@ AIInterface.prototype.GetRepresentation = function()
return state;
};
// Set up a load of event handlers to capture interesting things going on
// in the world, which we will report to AI:
// (This shouldn't include extremely high-frequency events, like PositionChanged,
// because that would be very expensive and AI will rarely care about all those
// events.)
// AIProxy sets up a load of event handlers to capture interesting things going on
// in the world, which we will report to AI. Handle those, and add a few more handlers
// for events that AIProxy won't capture.
AIInterface.prototype.OnGlobalCreate = function(msg)
AIInterface.prototype.PushEvent = function(type, msg)
{
this.events.push({"type": "Create", "msg": msg});
};
AIInterface.prototype.OnGlobalDestroy = function(msg)
{
this.events.push({"type": "Destroy", "msg": msg});
};
AIInterface.prototype.OnGlobalOwnershipChanged = function(msg)
{
this.events.push({"type": "OwnershipChanged", "msg": msg});
};
AIInterface.prototype.OnGlobalAttacked = function(msg)
{
this.events.push({"type": "Attacked", "msg": msg});
};
AIInterface.prototype.OnGlobalConstructionFinished = function(msg)
{
this.events.push({"type": "ConstructionFinished", "msg": msg});
this.events.push({"type": type, "msg": msg});
};
AIInterface.prototype.OnGlobalPlayerDefeated = function(msg)

View File

@ -58,6 +58,8 @@ AIProxy.prototype.GetRepresentation = function()
return ret;
};
// AI representation-updating event handlers:
AIProxy.prototype.OnPositionChanged = function(msg)
{
if (!this.changes)
@ -85,6 +87,23 @@ AIProxy.prototype.OnOwnershipChanged = function(msg)
this.changes.owner = msg.to;
};
AIProxy.prototype.OnUnitIdleChanged = function(msg)
{
if (!this.changes)
this.changes = {};
this.changes.idle = msg.idle;
};
AIProxy.prototype.OnTrainingQueueChanged = function(msg)
{
if (!this.changes)
this.changes = {};
var cmpTrainingQueue = Engine.QueryInterface(this.entity, IID_TrainingQueue);
this.changes.trainingQueue = cmpTrainingQueue.GetQueue();
}
// TODO: event handlers for all the other things
AIProxy.prototype.GetFullRepresentation = function()
@ -127,9 +146,17 @@ AIProxy.prototype.GetFullRepresentation = function()
ret.owner = cmpOwnership.GetOwner();
}
var cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
if (cmpUnitAI)
{
// Updated by OnUnitIdleChanged
ret.idle = cmpUnitAI.IsIdle();
}
var cmpTrainingQueue = Engine.QueryInterface(this.entity, IID_TrainingQueue);
if (cmpTrainingQueue)
{
// Updated by OnTrainingQueueChanged
ret.trainingQueue = cmpTrainingQueue.GetQueue();
}
@ -160,4 +187,41 @@ AIProxy.prototype.GetFullRepresentation = function()
return ret;
};
// AI event handlers:
// (These are passed directly as events to the AI scripts, rather than updating
// our proxy representation.)
// (This shouldn't include extremely high-frequency events, like PositionChanged,
// because that would be very expensive and AI will rarely care about all those
// events.)
AIProxy.prototype.OnCreate = function(msg)
{
var cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface);
cmpAIInterface.PushEvent("Create", msg);
};
AIProxy.prototype.OnDestroy = function(msg)
{
var cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface);
cmpAIInterface.PushEvent("Destroy", msg);
};
AIProxy.prototype.OnAttacked = function(msg)
{
var cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface);
cmpAIInterface.PushEvent("Attacked", msg);
};
AIProxy.prototype.OnConstructionFinished = function(msg)
{
var cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface);
cmpAIInterface.PushEvent("ConstructionFinished", msg);
};
AIProxy.prototype.OnTrainingFinished = function(msg)
{
var cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface);
cmpAIInterface.PushEvent("TrainingFinished", msg);
};
Engine.RegisterComponentType(IID_AIProxy, "AIProxy", AIProxy);

View File

@ -50,7 +50,7 @@ TrainingQueue.prototype.GetEntitiesList = function()
return string.split(/\s+/);
};
TrainingQueue.prototype.AddBatch = function(templateName, count)
TrainingQueue.prototype.AddBatch = function(templateName, count, metadata)
{
// TODO: there should probably be a limit on the number of queued batches
// TODO: there should be a way for the GUI to determine whether it's going
@ -88,12 +88,14 @@ TrainingQueue.prototype.AddBatch = function(templateName, count)
"player": cmpPlayer.GetPlayerID(),
"template": templateName,
"count": count,
"metadata": metadata,
"resources": costs,
"population": population,
"trainingStarted": false,
"timeTotal": time*1000,
"timeRemaining": time*1000,
});
Engine.PostMessage(this.entity, MT_TrainingQueueChanged, { });
// If this is the first item in the queue, start the timer
if (!this.timer)
@ -132,6 +134,8 @@ TrainingQueue.prototype.RemoveBatch = function(id)
// Remove from the queue
// (We don't need to remove the timer - it'll expire if it discovers the queue is empty)
this.queue.splice(i, 1);
Engine.PostMessage(this.entity, MT_TrainingQueueChanged, { });
return;
}
};
@ -146,6 +150,7 @@ TrainingQueue.prototype.GetQueue = function()
"template": item.template,
"count": item.count,
"progress": 1-(item.timeRemaining/item.timeTotal),
"metadata": item.metadata,
});
}
return out;
@ -192,7 +197,7 @@ TrainingQueue.prototype.OnDestroy = function()
};
TrainingQueue.prototype.SpawnUnits = function(templateName, count)
TrainingQueue.prototype.SpawnUnits = function(templateName, count, metadata)
{
var cmpFootprint = Engine.QueryInterface(this.entity, IID_Footprint);
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
@ -247,6 +252,8 @@ TrainingQueue.prototype.SpawnUnits = function(templateName, count)
});
}
}
Engine.PostMessage(this.entity, MT_TrainingFinished, { "entities": ents, "metadata": metadata });
};
TrainingQueue.prototype.ProgressTimeout = function(data)
@ -291,8 +298,9 @@ TrainingQueue.prototype.ProgressTimeout = function(data)
// This item is finished now
time -= item.timeRemaining;
cmpPlayer.UnReservePopulationSlots(item.population);
this.SpawnUnits(item.template, item.count);
this.SpawnUnits(item.template, item.count, item.metadata);
this.queue.shift();
Engine.PostMessage(this.entity, MT_TrainingQueueChanged, { });
}
// If the queue's empty, delete the timer, else repeat it

View File

@ -310,6 +310,13 @@ var UnitFsmSpec = {
// get stuck with an incorrect animation
this.SelectAnimation("idle");
// The GUI and AI want to know when a unit is idle, but we don't
// want to send frequent spurious messages if the unit's only
// idle for an instant and will quickly go off and do something else.
// So we'll set a timer here and only report the idle event if we
// remain idle
this.StartTimer(1000);
// If we entered the idle state we must have nothing better to do,
// so immediately check whether there's anybody nearby to attack.
// (If anyone approaches later, it'll be handled via LosRangeUpdate.)
@ -328,6 +335,14 @@ var UnitFsmSpec = {
"leave": function() {
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
rangeMan.DisableActiveQuery(this.losRangeQuery);
this.StopTimer();
if (this.isIdle)
{
this.isIdle = false;
Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle });
}
},
"LosRangeUpdate": function(msg) {
@ -337,6 +352,14 @@ var UnitFsmSpec = {
this.AttackVisibleEntity(msg.data.added);
}
},
"Timer": function(msg) {
if (!this.isIdle)
{
this.isIdle = true;
Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle });
}
},
},
"WALKING": {
@ -774,6 +797,7 @@ UnitAI.prototype.Init = function()
this.orderQueue = []; // current order is at the front of the list
this.order = undefined; // always == this.orderQueue[0]
this.formationController = INVALID_ENTITY; // entity with IID_Formation that we belong to
this.isIdle = false;
this.SetStance("aggressive");
};
@ -783,6 +807,11 @@ UnitAI.prototype.IsFormationController = function()
return (this.template.FormationController == "true");
};
UnitAI.prototype.IsIdle = function()
{
return this.isIdle;
};
UnitAI.prototype.OnCreate = function()
{
if (this.IsFormationController())
@ -925,8 +954,15 @@ UnitAI.prototype.ReplaceOrder = function(type, data)
UnitAI.prototype.TimerHandler = function(data, lateness)
{
// Reset the timer
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.timer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "TimerHandler", data.timerRepeat - lateness, data);
if (data.timerRepeat === undefined)
{
this.timer = undefined;
}
else
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.timer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "TimerHandler", data.timerRepeat - lateness, data);
}
UnitFsm.ProcessMessage(this, {"type": "Timer", "data": data, "lateness": lateness});
};

View File

@ -1 +1,9 @@
Engine.RegisterInterface("TrainingQueue");
// Message of the form { } (use GetQueue if you want the current details),
// sent to the current entity whenever the training queue changes.
Engine.RegisterMessageType("TrainingQueueChanged");
// Message of the form { entities: [id, ...], metadata: ... }
// sent to the current entity whenever a unit has been trained.
Engine.RegisterMessageType("TrainingFinished");

View File

@ -1 +1,5 @@
Engine.RegisterInterface("UnitAI");
// Message of the form { "idle": true },
// sent whenever the unit's idle status changes.
Engine.RegisterMessageType("UnitIdleChanged");

View File

@ -55,7 +55,7 @@ function ProcessCommand(player, cmd)
case "train":
var queue = Engine.QueryInterface(cmd.entity, IID_TrainingQueue);
if (queue)
queue.AddBatch(cmd.template, +cmd.count);
queue.AddBatch(cmd.template, +cmd.count, cmd.metadata);
break;
case "stop-train":

View File

@ -488,6 +488,7 @@ void CCmpTemplateManager::CopyFoundationSubset(CParamNode& out, const CParamNode
permittedComponentTypes.insert("Cost");
permittedComponentTypes.insert("Sound");
permittedComponentTypes.insert("Vision");
permittedComponentTypes.insert("AIProxy");
CParamNode::LoadXMLString(out, "<Entity/>");
out.CopyFilteredChildrenOfChild(in, "Entity", permittedComponentTypes);