AI API V3 along with a new version of Aegis. Support for a JS shared component that can be used or not for each AI.

This was SVN commit r13225.
This commit is contained in:
wraitii 2013-03-05 22:52:48 +00:00
parent c29a8d8ac0
commit e33d4a52e9
57 changed files with 6372 additions and 2945 deletions

View File

@ -0,0 +1,296 @@
var PlayerID = -1;
function BaseAI(settings)
{
if (!settings)
return;
// Make some properties non-enumerable, so they won't be serialised
Object.defineProperty(this, "_player", {value: settings.player, enumerable: false});
PlayerID = this._player;
/*
Object.defineProperty(this, "_templates", {value: settings.templates, enumerable: false});
Object.defineProperty(this, "_derivedTemplates", {value: {}, enumerable: false});
this._entityMetadata = {};
this._entityCollections = [];
this._entityCollectionsByDynProp = {};
this._entityCollectionsUID = 0;
*/
this.turn = 0;
}
//Return a simple object (using no classes etc) that will be serialized
//into saved games
BaseAI.prototype.Serialize = function()
{
return {};
// TODO: ought to get the AI script subclass to serialize its own state
};
//Called after the constructor when loading a saved game, with 'data' being
//whatever Serialize() returned
BaseAI.prototype.Deserialize = function(data)
{
/*
var rawEntities = data._rawEntities;
this._entityMetadata = data._entityMetadata;
this._entities = {}
for (var id in rawEntities)
{
this._entities[id] = new Entity(this, rawEntities[id]);
}
*/
// TODO: ought to get the AI script subclass to deserialize its own state
};
/*BaseAI.prototype.GetTemplate = function(name)
{
if (this._templates[name])
return this._templates[name];
if (this._derivedTemplates[name])
return this._derivedTemplates[name];
// If this is a foundation template, construct it automatically
if (name.substr(0, 11) === "foundation|")
{
var base = this.GetTemplate(name.substr(11));
var foundation = {};
for (var key in base)
if (!g_FoundationForbiddenComponents[key])
foundation[key] = base[key];
this._derivedTemplates[name] = foundation;
return foundation;
}
error("Tried to retrieve invalid template '"+name+"'");
return null;
};
*/
BaseAI.prototype.InitWithSharedScript = function(state, sharedAI)
{
this.accessibility = sharedAI.accessibility;
this.terrainAnalyzer = sharedAI.terrainAnalyzer;
this.passabilityClasses = sharedAI.passabilityClasses;
this.passabilityMap = sharedAI.passabilityMap;
var gameState = sharedAI.gameState[PlayerID];
gameState.ai = this;
this.InitShared(gameState, sharedAI);
delete gameState.ai;
}
BaseAI.prototype.HandleMessage = function(state, sharedAI)
{
/*
if (!this._entities)
{
// Do a (shallow) clone of all the initial entity properties (in order
// to copy into our own script context and minimise cross-context
// weirdness)
this._entities = {};
for (var id in state.entities)
{
this._entities[id] = new Entity(this, state.entities[id]);
}
}
else
{
this.ApplyEntitiesDelta(state);
}
*/
Engine.ProfileStart("HandleMessage setup");
this.entities = sharedAI.entities;
this.events = sharedAI.events;
this.passabilityClasses = sharedAI.passabilityClasses;
this.passabilityMap = sharedAI.passabilityMap;
this.player = this._player;
this.playerData = sharedAI.playersData[this._player];
this.templates = sharedAI.templates;
this.territoryMap = sharedAI.territoryMap;
this.timeElapsed = sharedAI.timeElapsed;
this.accessibility = sharedAI.accessibility;
this.terrainAnalyzer = sharedAI.terrainAnalyzer;
this.techModifications = sharedAI.techModifications[this._player];
Engine.ProfileStop();
this.OnUpdate(sharedAI);
// Clean up temporary properties, so they don't disturb the serializer
delete this.entities;
delete this.events;
delete this.passabilityClasses;
delete this.passabilityMap;
delete this.player;
delete this.playerData;
delete this.templates;
delete this.territoryMap;
delete this.timeElapsed;
};
BaseAI.prototype.ApplyEntitiesDelta = function(state)
{
Engine.ProfileStart("ApplyEntitiesDelta");
for each (var evt in state.events)
{
if (evt.type == "Create")
{
if (! state.entities[evt.msg.entity])
{
continue; // Sometimes there are things like foundations which get destroyed too fast
}
this._entities[evt.msg.entity] = new Entity(this, state.entities[evt.msg.entity]);
// Update all the entity collections since the create operation affects static properties as well as dynamic
for each (var entCollection in this._entityCollections)
{
entCollection.updateEnt(this._entities[evt.msg.entity]);
}
}
else if (evt.type == "Destroy")
{
if (!this._entities[evt.msg.entity])
{
continue;
}
// The entity was destroyed but its data may still be useful, so
// remember the entity and this AI's metadata concerning it
evt.msg.metadata = (evt.msg.metadata || []);
evt.msg.entityObj = (evt.msg.entityObj || this._entities[evt.msg.entity]);
evt.msg.metadata[this._player] = this._entityMetadata[evt.msg.entity];
for each (var entCol in this._entityCollections)
{
entCol.removeEnt(this._entities[evt.msg.entity]);
}
delete this._entities[evt.msg.entity];
delete this._entityMetadata[evt.msg.entity];
}
else if (evt.type == "TrainingFinished")
{
// Apply metadata stored in training queues, but only if they
// look like they were added by us
if (evt.msg.owner === this._player)
{
for each (var ent in evt.msg.entities)
{
for (key in evt.msg.metadata)
{
this.setMetadata(this._entities[ent], key, evt.msg.metadata[key])
}
}
}
}
}
for (var id in state.entities)
{
var changes = state.entities[id];
for (var prop in changes)
{
if (prop == "position" || prop == "resourceSupplyAmount") {
if (this.turn % 10 === 0) {
this._entities[id]._entity[prop] = changes[prop];
this.updateEntityCollections(prop, this._entities[id]);
}
} else {
this._entities[id]._entity[prop] = changes[prop];
this.updateEntityCollections(prop, this._entities[id]);
}
}
}
Engine.ProfileStop();
};
BaseAI.prototype.OnUpdate = function()
{ // AIs override this function
// They should do at least this.turn++;
};
BaseAI.prototype.chat = function(message)
{
Engine.PostCommand({"type": "chat", "message": message});
};
BaseAI.prototype.registerUpdatingEntityCollection = function(entCollection)
{
entCollection.setUID(this._entityCollectionsUID);
this._entityCollections.push(entCollection);
for each (var prop in entCollection.dynamicProperties())
{
this._entityCollectionsByDynProp[prop] = this._entityCollectionsByDynProp[prop] || [];
this._entityCollectionsByDynProp[prop].push(entCollection);
}
this._entityCollectionsUID++;
};
BaseAI.prototype.removeUpdatingEntityCollection = function(entCollection)
{
for (var i in this._entityCollections)
{
if (this._entityCollections[i].getUID() === entCollection.getUID())
{
this._entityCollections.splice(i, 1);
}
}
for each (var prop in entCollection.dynamicProperties())
{
for (var i in this._entityCollectionsByDynProp[prop])
{
if (this._entityCollectionsByDynProp[prop][i].getUID() === entCollection.getUID())
{
this._entityCollectionsByDynProp[prop].splice(i, 1);
}
}
}
};
BaseAI.prototype.updateEntityCollections = function(property, ent)
{
if (this._entityCollectionsByDynProp[property] !== undefined)
{
for each (var entCollection in this._entityCollectionsByDynProp[property])
{
entCollection.updateEnt(ent);
}
}
}
BaseAI.prototype.setMetadata = function(ent, key, value)
{
var metadata = this._entityMetadata[ent.id()];
if (!metadata)
metadata = this._entityMetadata[ent.id()] = {};
metadata[key] = value;
this.updateEntityCollections('metadata', ent);
this.updateEntityCollections('metadata.' + key, ent);
}
BaseAI.prototype.getMetadata = function(ent, key)
{
var metadata = this._entityMetadata[ent.id()];
if (!metadata || !(key in metadata))
return undefined;
return metadata[key];
}

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

@ -0,0 +1,653 @@
var EntityTemplate = Class({
// techModifications should be the tech modifications of only one player.
// gamestates handle "GetTemplate" and should push the player's
// while entities should push the owner's
_init: function(template, techModifications)
{
this._techModifications = techModifications;
this._template = template;
},
genericName: function() {
if (!this._template.Identity || !this._template.Identity.GenericName)
return undefined;
return this._template.Identity.GenericName;
},
rank: function() {
if (!this._template.Identity)
return undefined;
return this._template.Identity.Rank;
},
classes: function() {
if (!this._template.Identity || !this._template.Identity.Classes)
return undefined;
return this._template.Identity.Classes._string.split(/\s+/);
},
requiredTech: function() {
if (!this._template.Identity || !this._template.Identity.RequiredTechnology)
return undefined;
return this._template.Identity.RequiredTechnology;
},
phase: function() {
if (!this._template.Identity || !this._template.Identity.RequiredTechnology)
return 0;
if (this.template.Identity.RequiredTechnology == "phase_village")
return 1;
if (this.template.Identity.RequiredTechnology == "phase_town")
return 2;
if (this.template.Identity.RequiredTechnology == "phase_city")
return 3;
return 0;
},
hasClass: function(name) {
var classes = this.classes();
return (classes && classes.indexOf(name) != -1);
},
hasClasses: function(array) {
var classes = this.classes();
if (!classes)
return false;
for (i in array)
if (classes.indexOf(array[i]) === -1)
return false;
return true;
},
civ: function() {
if (!this._template.Identity)
return undefined;
return this._template.Identity.Civ;
},
cost: function() {
if (!this._template.Cost)
return undefined;
var ret = {};
for (var type in this._template.Cost.Resources)
ret[type] = GetTechModifiedProperty(this._techModifications, this._template, "Cost/Resources/"+type, +this._template.Cost.Resources[type]);
return ret;
},
costSum: function() {
if (!this._template.Cost)
return undefined;
var ret = 0;
for (var type in this._template.Cost.Resources)
ret += +this._template.Cost.Resources[type];
return ret;
},
/**
* Returns the radius of a circle surrounding this entity's
* obstruction shape, or undefined if no obstruction.
*/
obstructionRadius: function() {
if (!this._template.Obstruction)
return undefined;
if (this._template.Obstruction.Static)
{
var w = +this._template.Obstruction.Static["@width"];
var h = +this._template.Obstruction.Static["@depth"];
return Math.sqrt(w*w + h*h) / 2;
}
if (this._template.Obstruction.Unit)
return +this._template.Obstruction.Unit["@radius"];
return 0; // this should never happen
},
/**
* Returns the radius of a circle surrounding this entity's
* footprint.
*/
footprintRadius: function() {
if (!this._template.Footprint)
return undefined;
if (this._template.Footprint.Square)
{
var w = +this._template.Footprint.Square["@width"];
var h = +this._template.Footprint.Square["@depth"];
return Math.sqrt(w*w + h*h) / 2;
}
if (this._template.Footprint.Circle)
return +this._template.Footprint.Circle["@radius"];
return 0; // this should never happen
},
maxHitpoints: function()
{
if (this._template.Health !== undefined)
return this._template.Health.Max;
return 0;
},
isHealable: function()
{
if (this._template.Health !== undefined)
return this._template.Health.Unhealable !== "true";
return false;
},
isRepairable: function()
{
if (this._template.Health !== undefined)
return this._template.Health.Repairable === "true";
return false;
},
getPopulationBonus: function() {
if (!this._template.Cost || !this._template.Cost.PopulationBonus)
return undefined;
return this._template.Cost.PopulationBonus;
},
armourStrengths: function() {
if (!this._template.Armour)
return undefined;
return {
hack: GetTechModifiedProperty(this._techModifications, this._template, "Armour/Hack", +this._template.Armour.Hack),
pierce: GetTechModifiedProperty(this._techModifications, this._template, "Armour/Pierce", +this._template.Armour.Pierce),
crush: GetTechModifiedProperty(this._techModifications, this._template, "Armour/Crush", +this._template.Armour.Crush)
};
},
attackTypes: function() {
if (!this._template.Attack)
return undefined;
var ret = [];
for (var type in this._template.Attack)
ret.push(type);
return ret;
},
attackRange: function(type) {
if (!this._template.Attack || !this._template.Attack[type])
return undefined;
return {
max: GetTechModifiedProperty(this._techModifications, this._template, "Attack/MaxRange", +this._template.Attack[type].MaxRange),
min: GetTechModifiedProperty(this._techModifications, this._template, "Attack/MinRange", +(this._template.Attack[type].MinRange || 0))
};
},
attackStrengths: function(type) {
if (!this._template.Attack || !this._template.Attack[type])
return undefined;
return {
hack: GetTechModifiedProperty(this._techModifications, this._template, "Attack/"+type+"/Hack", +(this._template.Attack[type].Hack || 0)),
pierce: GetTechModifiedProperty(this._techModifications, this._template, "Attack/"+type+"/Pierce", +(this._template.Attack[type].Pierce || 0)),
crush: GetTechModifiedProperty(this._techModifications, this._template, "Attack/"+type+"/Crush", +(this._template.Attack[type].Crush || 0))
};
},
attackTimes: function(type) {
if (!this._template.Attack || !this._template.Attack[type])
return undefined;
return {
prepare: GetTechModifiedProperty(this._techModifications, this._template, "Attack/"+type+"/PrepareTime", +(this._template.Attack[type].PrepareTime || 0)),
repeat: GetTechModifiedProperty(this._techModifications, this._template, "Attack/"+type+"/RepeatTime", +(this._template.Attack[type].RepeatTime || 1000))
};
},
// returns the classes this templates counters:
// Return type is [ [-neededClasses-] , multiplier ].
getCounteredClasses: function() {
if (!this._template.Attack)
return undefined;
var Classes = [];
for (i in this._template.Attack) {
if (!this._template.Attack[i].Bonuses)
continue;
for (o in this._template.Attack[i].Bonuses) {
if (this._template.Attack[i].Bonuses[o].Classes == undefined)
continue;
Classes.push([this._template.Attack[i].Bonuses[o].Classes.split(" "), +this._template.Attack[i].Bonuses[o].Multiplier]);
}
}
return Classes;
},
// returns true if the entity counters those classes.
// TODO: refine using the multiplier
countersClasses: function(classes) {
if (!this._template.Attack)
return false;
var mcounter = [];
for (i in this._template.Attack) {
if (!this._template.Attack[i].Bonuses)
continue;
for (o in this._template.Attack[i].Bonuses) {
if (this._template.Attack[i].Bonuses[o].Classes == undefined)
continue;
mcounter.concat(this._template.Attack[i].Bonuses[o].Classes.split(" "));
}
}
for (i in classes)
{
if (mcounter.indexOf(classes[i]) !== -1)
return true;
}
return false;
},
// returns, if it exists, the multiplier from each attack against a given class
getMultiplierAgainst: function(type, againstClass) {
if (!this._template.Attack || !this._template.Attack[type])
return undefined;
if (this._template.Attack[type].Bonuses)
for (o in this._template.Attack[type].Bonuses)
if (this._template.Attack[type].Bonuses[o].Classes !== undefined)
{
var total = this._template.Attack[type].Bonuses[o].Classes.split(" ");
for (j in total)
if (total[j] === againstClass)
return this._template.Attack[type].Bonuses[o].Multiplier;
}
return 1;
},
buildableEntities: function() {
if (!this._template.Builder || !this._template.Builder.Entities._string)
return undefined;
var civ = this.civ();
var templates = this._template.Builder.Entities._string.replace(/\{civ\}/g, civ).split(/\s+/);
return templates; // TODO: map to Entity?
},
trainableEntities: function() {
if (!this._template.ProductionQueue || !this._template.ProductionQueue.Entities)
return undefined;
var civ = this.civ();
var templates = this._template.ProductionQueue.Entities._string.replace(/\{civ\}/g, civ).split(/\s+/);
return templates;
},
researchableTechs: function() {
if (!this._template.ProductionQueue || !this._template.ProductionQueue.Technologies)
return undefined;
var templates = this._template.ProductionQueue.Technologies._string.split(/\s+/);
return templates;
},
resourceSupplyType: function() {
if (!this._template.ResourceSupply)
return undefined;
var [type, subtype] = this._template.ResourceSupply.Type.split('.');
return { "generic": type, "specific": subtype };
},
// will return either "food", "wood", "stone", "metal" and not treasure.
getResourceType: function() {
if (!this._template.ResourceSupply)
return undefined;
var [type, subtype] = this._template.ResourceSupply.Type.split('.');
if (type == "treasure")
return subtype;
return type;
},
resourceSupplyMax: function() {
if (!this._template.ResourceSupply)
return undefined;
return +this._template.ResourceSupply.Amount;
},
resourceGatherRates: function() {
if (!this._template.ResourceGatherer)
return undefined;
var ret = {};
var baseSpeed = GetTechModifiedProperty(this._techModifications, this._template, "ResourceGatherer/BaseSpeed", +this._template.ResourceGatherer.BaseSpeed);
for (var r in this._template.ResourceGatherer.Rates)
ret[r] = GetTechModifiedProperty(this._techModifications, this._template, "ResourceGatherer/Rates/"+r, +this._template.ResourceGatherer.Rates[r]) * baseSpeed;
return ret;
},
resourceDropsiteTypes: function() {
if (!this._template.ResourceDropsite)
return undefined;
return this._template.ResourceDropsite.Types.split(/\s+/);
},
garrisonableClasses: function() {
if (!this._template.GarrisonHolder)
return undefined;
return this._template.GarrisonHolder.List._string.split(/\s+/);
},
garrisonMax: function() {
if (!this._template.GarrisonHolder)
return undefined;
return this._template.GarrisonHolder.Max;
},
/**
* Returns whether this is an animal that is too difficult to hunt.
* (Currently this just includes skittish animals, which are probably
* too fast to chase.)
*/
isUnhuntable: function() {
if (!this._template.UnitAI || !this._template.UnitAI.NaturalBehaviour)
return false;
// return (this._template.UnitAI.NaturalBehaviour == "skittish");
// Actually, since the AI is currently rubbish at hunting, skip all animals
// that aren't really weak:
return this._template.Health.Max >= 10;
},
walkSpeed: function() {
if (!this._template.UnitMotion || !this._template.UnitMotion.WalkSpeed)
return undefined;
return this._template.UnitMotion.WalkSpeed;
},
buildCategory: function() {
if (!this._template.BuildRestrictions || !this._template.BuildRestrictions.Category)
return undefined;
return this._template.BuildRestrictions.Category;
},
buildTime: function() {
if (!this._template.Cost || !this._template.Cost.buildTime)
return undefined;
return this._template.Cost.buildTime;
},
buildDistance: function() {
if (!this._template.BuildRestrictions || !this._template.BuildRestrictions.Distance)
return undefined;
return this._template.BuildRestrictions.Distance;
},
buildPlacementType: function() {
if (!this._template.BuildRestrictions || !this._template.BuildRestrictions.PlacementType)
return undefined;
return this._template.BuildRestrictions.PlacementType;
},
buildTerritories: function() {
if (!this._template.BuildRestrictions || !this._template.BuildRestrictions.Territory)
return undefined;
return this._template.BuildRestrictions.Territory.split(/\s+/);
},
hasBuildTerritory: function(territory) {
var territories = this.buildTerritories();
return (territories && territories.indexOf(territory) != -1);
},
visionRange: function() {
if (!this._template.Vision)
return undefined;
return this._template.Vision.Range;
}
});
var Entity = Class({
_super: EntityTemplate,
_init: function(sharedAI, entity)
{
this._super.call(this, sharedAI.GetTemplate(entity.template), sharedAI.techModifications[entity.owner]);
this._ai = sharedAI;
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(player, key) {
return this._ai.getMetadata(player, this, key);
},
/**
* Sets extra data to be associated with this entity.
*/
setMetadata: function(player, key, value) {
this._ai.setMetadata(player, this, key, value);
},
deleteMetadata: function(player) {
delete this._ai._entityMetadata[player][this.id()];
},
position: function() { return this._entity.position; },
isIdle: function() {
if (typeof this._entity.idle === "undefined")
return undefined;
return this._entity.idle;
},
unitAIState: function() { return this._entity.unitAIState; },
unitAIOrderData: function() { return this._entity.unitAIOrderData; },
hitpoints: function() { if (this._entity.hitpoints !== undefined) return this._entity.hitpoints; return undefined; },
isHurt: function() { return this.hitpoints() < this.maxHitpoints(); },
healthLevel: 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() {
if (typeof this._entity.foundationProgress === "undefined")
return undefined;
return this._entity.foundationProgress;
},
owner: function() {
return this._entity.owner;
},
isOwn: function(player) {
if (typeof(this._entity.owner) === "undefined")
return false;
return this._entity.owner === player;
},
isFriendly: function(player) {
return this.isOwn(player); // TODO: diplomacy
},
isEnemy: function(player) {
return !this.isOwn(player); // TODO: diplomacy
},
resourceSupplyAmount: function() {
if(this._entity.resourceSupplyAmount === undefined)
return undefined;
return this._entity.resourceSupplyAmount;
},
resourceCarrying: function() {
if(this._entity.resourceCarrying === undefined)
return undefined;
return this._entity.resourceCarrying;
},
garrisoned: function() { return new EntityCollection(this._ai, this._entity.garrisoned); },
// TODO: visibility
move: function(x, z, queued) {
queued = queued || false;
Engine.PostCommand({"type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": queued });
return this;
},
// violent, aggressive, defensive, passive, standground
setStance: function(stance,queued){
Engine.PostCommand({"type": "stance", "entities": [this.id()], "name" : stance, "queued": queued });
return this;
},
// TODO: replace this with the proper "STOP" command
stopMoving: function() {
if (this.position() !== undefined)
Engine.PostCommand({"type": "walk", "entities": [this.id()], "x": this.position()[0], "z": this.position()[1], "queued": false});
},
unload: function(id) {
if (!this._template.GarrisonHolder)
return undefined;
Engine.PostCommand({"type": "unload", "garrisonHolder": this.id(), "entity": id});
return this;
},
unloadAll: function() {
if (!this._template.GarrisonHolder)
return undefined;
Engine.PostCommand({"type": "unload-all", "garrisonHolders": [this.id()]});
return this;
},
garrison: function(target) {
Engine.PostCommand({"type": "garrison", "entities": [this.id()], "target": target.id(),"queued": false});
return this;
},
attack: function(unitId) {
Engine.PostCommand({"type": "attack", "entities": [this.id()], "target": unitId, "queued": false});
return this;
},
// Flees from a unit in the opposite direction.
flee: function(unitToFleeFrom) {
if (this.position() !== undefined && unitToFleeFrom.position() !== undefined) {
var FleeDirection = [unitToFleeFrom.position()[0] - this.position()[0],unitToFleeFrom.position()[1] - this.position()[1]];
var dist = VectorDistance(unitToFleeFrom.position(), this.position() );
FleeDirection[0] = (FleeDirection[0]/dist) * 5;
FleeDirection[1] = (FleeDirection[1]/dist) * 5;
Engine.PostCommand({"type": "walk", "entities": [this.id()], "x": this.position()[0] + FleeDirection[0]*5, "z": this.position()[1] + FleeDirection[1]*5, "queued": false});
}
return this;
},
gather: function(target, queued) {
queued = queued || false;
Engine.PostCommand({"type": "gather", "entities": [this.id()], "target": target.id(), "queued": queued});
return this;
},
repair: function(target, queued) {
queued = queued || false;
Engine.PostCommand({"type": "repair", "entities": [this.id()], "target": target.id(), "autocontinue": false, "queued": queued});
return this;
},
returnResources: function(target, queued) {
queued = queued || false;
Engine.PostCommand({"type": "returnresource", "entities": [this.id()], "target": target.id(), "queued": queued});
return this;
},
destroy: function() {
Engine.PostCommand({"type": "delete-entities", "entities": [this.id()] });
return this;
},
barter: function(buyType, sellType, amount) {
Engine.PostCommand({"type": "barter", "sell" : sellType, "buy" : buyType, "amount" : amount });
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",
"entities": [this.id()],
"template": type,
"count": count,
"metadata": metadata
});
return this;
},
construct: function(template, x, z, angle) {
// TODO: verify this unit can construct this, just for internal
// sanity-checking and error reporting
Engine.PostCommand({
"type": "construct",
"entities": [this.id()],
"template": template,
"x": x,
"z": z,
"angle": angle,
"autorepair": false,
"autocontinue": false,
"queued": false
});
return this;
},
research: function(template) {
Engine.PostCommand({ "type": "research", "entity": this.id(), "template": template });
return this;
},
});

View File

@ -0,0 +1,319 @@
function EntityCollection(sharedAI, entities, filters)
{
this._ai = sharedAI;
this._entities = entities || {};
this._filters = filters || [];
// Compute length lazily on demand, since it can be
// expensive for large collections
this._length = undefined;
Object.defineProperty(this, "length", {
get: function () {
if (this._length === undefined)
{
this._length = 0;
for (var id in entities)
++this._length;
}
return this._length;
}
});
this.frozen = false;
}
// If an entitycollection is frozen, it will never automatically add a unit.
// But can remove one.
EntityCollection.prototype.freeze = function()
{
this.frozen = true;
};
EntityCollection.prototype.defreeze = function()
{
this.frozen = false;
};
EntityCollection.prototype.toIdArray = function()
{
var ret = [];
for (var id in this._entities)
ret.push(+id);
return ret;
};
EntityCollection.prototype.toEntityArray = function()
{
var ret = [];
for each (var ent in this._entities)
ret.push(ent);
return ret;
};
EntityCollection.prototype.toString = function()
{
return "[EntityCollection " + this.toEntityArray().join(" ") + "]";
};
/**
* Returns the (at most) n entities nearest to targetPos.
*/
EntityCollection.prototype.filterNearest = function(targetPos, n)
{
// Compute the distance of each entity
var data = []; // [ [id, ent, distance], ... ]
for (var id in this._entities)
{
var ent = this._entities[id];
if (ent.position())
data.push([id, ent, VectorDistance(targetPos, ent.position())]);
}
// Sort by increasing distance
data.sort(function (a, b) { return (a[2] - b[2]); });
// Extract the first n
var ret = {};
for each (var val in data.slice(0, n))
ret[val[0]] = val[1];
return new EntityCollection(this._ai, ret);
};
EntityCollection.prototype.filter = function(filter, thisp)
{
if (typeof(filter) == "function")
filter = {"func": filter, "dynamicProperties": []};
var ret = {};
for (var id in this._entities)
{
var ent = this._entities[id];
if (filter.func.call(thisp, ent, id, this))
ret[id] = ent;
}
return new EntityCollection(this._ai, ret, this._filters.concat([filter]));
};
EntityCollection.prototype.filter_raw = function(callback, thisp)
{
var ret = {};
for (var id in this._entities)
{
var ent = this._entities[id];
var val = this._entities[id]._entity;
if (callback.call(thisp, val, id, this))
ret[id] = ent;
}
return new EntityCollection(this._ai, ret);
};
EntityCollection.prototype.filterNearest = function(targetPos, n)
{
// Compute the distance of each entity
var data = []; // [ [id, ent, distance], ... ]
for (var id in this._entities)
{
var ent = this._entities[id];
if (ent.position())
data.push([id, ent, SquareVectorDistance(targetPos, ent.position())]);
}
// Sort by increasing distance
data.sort(function (a, b) { return (a[2] - b[2]); });
if (n === undefined)
n = this._length;
// Extract the first n
var ret = {};
for each (var val in data.slice(0, n))
ret[val[0]] = val[1];
return new EntityCollection(this._ai, ret);
};
EntityCollection.prototype.forEach = function(callback, thisp)
{
for (var id in this._entities)
{
var ent = this._entities[id];
callback.call(thisp, ent, id, this);
}
return this;
};
EntityCollection.prototype.move = function(x, z, queued)
{
queued = queued || false;
Engine.PostCommand({"type": "walk", "entities": this.toIdArray(), "x": x, "z": z, "queued": queued});
return this;
};
EntityCollection.prototype.moveIndiv = function(x, z, queued)
{
queued = queued || false;
for (var id in this._entities)
Engine.PostCommand({"type": "walk", "entities": [this._entities[id].id()], "x": x, "z": z, "queued": queued});
return this;
};
EntityCollection.prototype.destroy = function()
{
Engine.PostCommand({"type": "delete-entities", "entities": this.toIdArray()});
return this;
};
EntityCollection.prototype.attack = function(unit)
{
var unitId;
if (typeof(unit) === "Entity")
{
unitId = unit.id();
}
else
{
unitId = unit;
}
Engine.PostCommand({"type": "attack", "entities": this.toIdArray(), "target": unitId, "queued": false});
return this;
};
// violent, aggressive, defensive, passive, standground
EntityCollection.prototype.setStance = function(stance)
{
Engine.PostCommand({"type": "stance", "entities": this.toIdArray(), "name" : stance, "queued": false});
return this;
};
// Returns the average position of all units
EntityCollection.prototype.getCentrePosition = function()
{
var sumPos = [0, 0];
var count = 0;
this.forEach(function(ent)
{
if (ent.position())
{
sumPos[0] += ent.position()[0];
sumPos[1] += ent.position()[1];
count ++;
}
});
if (count === 0)
{
return undefined;
}
else
{
return [sumPos[0]/count, sumPos[1]/count];
}
};
// returns the average position from the sample first units.
// This might be faster for huge collections, but there's
// always a risk that it'll be unprecise.
EntityCollection.prototype.getApproximatePosition = function(sample)
{
var sumPos = [0, 0];
var i = 0;
for (var id in this._entities)
{
var ent = this._entities[id];
if (ent.position())
{
sumPos[0] += ent.position()[0];
sumPos[1] += ent.position()[1];
i++;
}
if (i === sample)
break;
}
if (sample === 0)
{
return undefined;
}
else
{
return [sumPos[0]/i, sumPos[1]/i];
}
};
// Removes an entity from the collection, returns true if the entity was a member, false otherwise
EntityCollection.prototype.removeEnt = function(ent)
{
if (this._entities[ent.id()])
{
// Checking length may initialize it, so do it before deleting.
if (this.length !== undefined)
this._length--;
delete this._entities[ent.id()];
return true;
}
else
{
return false;
}
};
// Adds an entity to the collection, returns true if the entity was not member, false otherwise
EntityCollection.prototype.addEnt = function(ent)
{
if (this._entities[ent.id()])
{
return false;
}
else
{
// Checking length may initialize it, so do it before adding.
if (this.length !== undefined)
this._length++;
this._entities[ent.id()] = ent;
return true;
}
};
// Checks the entity against the filters, and adds or removes it appropriately, returns true if the
// entity collection was modified.
// Force can add a unit despite a freezing.
// If an entitycollection is frozen, it will never automatically add a unit.
// But can remove one.
EntityCollection.prototype.updateEnt = function(ent, force)
{
var passesFilters = true;
for each (var filter in this._filters)
{
passesFilters = passesFilters && filter.func(ent);
}
if (passesFilters)
{
if (!force && this.frozen)
return false;
return this.addEnt(ent);
}
else
{
return this.removeEnt(ent);
}
};
EntityCollection.prototype.registerUpdates = function(noPush)
{
this._ai.registerUpdatingEntityCollection(this,noPush);
};
EntityCollection.prototype.dynamicProperties = function()
{
var ret = [];
for each (var filter in this._filters)
{
ret = ret.concat(filter.dynamicProperties);
}
return ret;
};
EntityCollection.prototype.setUID = function(id)
{
this._UID = id;
};
EntityCollection.prototype.getUID = function()
{
return this._UID;
};

View File

@ -0,0 +1,211 @@
var Filters = {
byType: function(type){
return {"func" : function(ent){
return ent.templateName() === type;
},
"dynamicProperties": []};
},
byClass: function(cls){
return {"func" : function(ent){
return ent.hasClass(cls);
},
"dynamicProperties": []};
},
byClassesAnd: function(clsList){
return {"func" : function(ent){
var ret = true;
for (var i in clsList){
ret = ret && ent.hasClass(clsList[i]);
}
return ret;
},
"dynamicProperties": []};
},
byClassesOr: function(clsList){
return {"func" : function(ent){
var ret = false;
for (var i in clsList){
ret = ret || ent.hasClass(clsList[i]);
}
return ret;
},
"dynamicProperties": []};
},
byMetadata: function(player, key, value){
return {"func" : function(ent){
return (ent.getMetadata(player, key) == value);
},
"dynamicProperties": ['metadata.' + key]};
},
byHasMetadata: function(key){
return {"func" : function(ent){
return (ent.getMetadata(PlayerID, key) != undefined);
},
"dynamicProperties": ['metadata.' + key]};
},
and: function(filter1, filter2){
return {"func": function(ent){
return filter1.func(ent) && filter2.func(ent);
},
"dynamicProperties": filter1.dynamicProperties.concat(filter2.dynamicProperties)};
},
or: function(filter1, filter2){
return {"func" : function(ent){
return filter1.func(ent) || filter2.func(ent);
},
"dynamicProperties": filter1.dynamicProperties.concat(filter2.dynamicProperties)};
},
not: function(filter){
return {"func": function(ent){
return !filter.func(ent);
},
"dynamicProperties": filter.dynamicProperties};
},
byOwner: function(owner){
return {"func" : function(ent){
return (ent.owner() === owner);
},
"dynamicProperties": ['owner']};
},
byNotOwner: function(owner){
return {"func" : function(ent){
return (ent.owner() !== owner);
},
"dynamicProperties": ['owner']};
},
byOwners: function(owners){
return {"func" : function(ent){
for (var i in owners){
if (ent.owner() == +owners[i]){
return true;
}
}
return false;
},
"dynamicProperties": ['owner']};
},
byTrainingQueue: function(){
return {"func" : function(ent){
return ent.trainingQueue();
},
"dynamicProperties": ['trainingQueue']};
},
byResearchAvailable: function(){
return {"func" : function(ent){
return ent.researchableTechs() !== undefined;
},
"dynamicProperties": []};
},
byTargetedEntity: function(targetID){
return {"func": function(ent) {
return (ent.unitAIOrderData().length && ent.unitAIOrderData()[0]["target"] && ent.unitAIOrderData()[0]["target"] == targetID);
},
"dynamicProperties": ['unitAIOrderData']};
},
isSoldier: function(){
return {"func" : function(ent){
return Filters.byClassesOr(["CitizenSoldier", "Super"])(ent);
},
"dynamicProperties": []};
},
isIdle: function(){
return {"func" : function(ent){
return ent.isIdle();
},
"dynamicProperties": ['idle']};
},
isFoundation: function(){
return {"func": function(ent){
return ent.foundationProgress() !== undefined;
},
"dynamicProperties": []};
},
byDistance: function(startPoint, dist){
return {"func": function(ent){
if (ent.position() === undefined){
return false;
}else{
return (SquareVectorDistance(startPoint, ent.position()) < dist*dist);
}
},
"dynamicProperties": ['position']};
},
// Distance filter with no auto updating, use with care
byStaticDistance: function(startPoint, dist){
return {"func": function(ent){
if (!ent.position()){
return false;
}else{
return (SquareVectorDistance(startPoint, ent.position()) < dist*dist);
}
},
"dynamicProperties": []};
},
byTerritory: function(Map, territoryIndex){
return {"func": function(ent){
if (Map.point(ent.position()) == territoryIndex) {
return true;
} else {
return false;
}
},
"dynamicProperties": ['position']};
},
isDropsite: function(resourceType){
return {"func": function(ent){
return (ent.resourceDropsiteTypes() && ent.resourceDropsiteTypes().indexOf(resourceType) !== -1);
},
"dynamicProperties": []};
},
byResource: function(resourceType){
return {"func" : function(ent){
var type = ent.resourceSupplyType();
if (!type)
return false;
var amount = ent.resourceSupplyMax();
if (!amount)
return false;
// Skip targets that are too hard to hunt
if (ent.isUnhuntable())
return false;
// And don't go for the bloody fish! TODO: better accessibility checks
if (ent.hasClass("SeaCreature")){
return false;
}
// Don't go for floating treasures since we won't be able to reach them and it kills the pathfinder.
if (ent.templateName() == "other/special_treasure_shipwreck_debris" ||
ent.templateName() == "other/special_treasure_shipwreck" ){
return false;
}
if (type.generic == "treasure"){
return (resourceType == type.specific);
} else {
return (resourceType == type.generic);
}
},
"dynamicProperties": []};
}
};

View File

@ -0,0 +1,595 @@
/**
* Provides an API for the rest of the AI scripts to query the world state at a
* higher level than the raw data.
*/
var GameState = function(SharedScript, state, player) {
this.sharedScript = SharedScript;
this.EntCollecNames = SharedScript._entityCollectionsName;
this.EntCollec = SharedScript._entityCollections;
this.timeElapsed = state.timeElapsed;
this.templates = SharedScript._templates;
this.techTemplates = SharedScript._techTemplates;
this.entities = SharedScript.entities;
this.player = player;
this.playerData = state.players[player];
this.techModifications = SharedScript.techModifications[player];
this.buildingsBuilt = 0;
this.ai = null; // must be updated by the AIs.
this.cellSize = 4; // Size of each map tile
this.turnCache = {};
};
GameState.prototype.update = function(SharedScript, state) {
this.sharedScript = SharedScript;
this.EntCollecNames = SharedScript._entityCollectionsName;
this.EntCollec = SharedScript._entityCollections;
this.timeElapsed = state.timeElapsed;
this.templates = SharedScript._templates;
this.techTemplates = SharedScript._techTemplates;
this.entities = SharedScript.entities;
this.playerData = state.players[this.player];
this.techModifications = SharedScript.techModifications[this.player];
this.buildingsBuilt = 0;
this.turnCache = {};
};
GameState.prototype.updatingCollection = function(id, filter, collection){
// automatically add the player ID
id = this.player + "-" + id;
if (!this.EntCollecNames[id]){
if (collection !== undefined)
this.EntCollecNames[id] = collection.filter(filter);
else {
this.EntCollecNames[id] = this.entities.filter(filter);
}
this.EntCollecNames[id].registerUpdates();
}
return this.EntCollecNames[id];
};
GameState.prototype.destroyCollection = function(id){
// automatically add the player ID
id = this.player + "-" + id;
if (this.EntCollecNames[id] !== undefined){
this.sharedScript.removeUpdatingEntityCollection(this.EntCollecNames[id]);
delete this.EntCollecNames[id];
}
};
GameState.prototype.getEC = function(id){
// automatically add the player ID
id = this.player + "-" + id;
if (this.EntCollecNames[id] !== undefined)
return this.EntCollecNames[id];
return undefined;
};
GameState.prototype.updatingGlobalCollection = function(id, filter, collection) {
if (!this.EntCollecNames[id]){
if (collection !== undefined)
this.EntCollecNames[id] = collection.filter(filter);
else
this.EntCollecNames[id] = this.entities.filter(filter);
this.EntCollecNames[id].registerUpdates();
}
return this.EntCollecNames[id];
};
GameState.prototype.destroyGlobalCollection = function(id)
{
if (this.EntCollecNames[id] !== undefined){
this.sharedScript.removeUpdatingEntityCollection(this.EntCollecNames[id]);
delete this.EntCollecNames[id];
}
};
GameState.prototype.getGEC = function(id)
{
if (this.EntCollecNames[id] !== undefined)
return this.EntCollecNames[id];
return undefined;
};
GameState.prototype.currentPhase = function()
{
if (this.isResearched("phase_city"))
return 3;
if (this.isResearched("phase_town"))
return 2;
if (this.isResearched("phase_village"))
return 1;
return 0;
};
GameState.prototype.isResearched = function(template)
{
return this.playerData.researchedTechs[template] !== undefined;
};
// this is an absolute check that doesn't check if we have a building to research from.
GameState.prototype.canResearch = function(techTemplateName, noRequirementCheck)
{
var template = this.getTemplate(techTemplateName);
if (!template)
return false;
// researching or already researched: NOO.
if (this.playerData.researchQueued[techTemplateName] || this.playerData.researchStarted[techTemplateName] || this.playerData.researchedTechs[techTemplateName])
return false;
if (noRequirementCheck === false)
return true;
// not already researched, check if we can.
// basically a copy of the function in technologyManager since we can't use it.
// Checks the requirements for a technology to see if it can be researched at the current time
// The technology which this technology supersedes is required
if (template.supersedes() && !this.playerData.researchedTechs[template.supersedes()])
return false;
// if this is a pair, we must check that the paire tech is not being researched
if (template.pair())
{
var other = template.pairedWith();
if (this.playerData.researchQueued[other] || this.playerData.researchStarted[other] || this.playerData.researchedTechs[other])
return false;
}
return this.checkTechRequirements(template.requirements());
}
// Private function for checking a set of requirements is met
// basically copies TechnologyManager's
GameState.prototype.checkTechRequirements = function (reqs)
{
// If there are no requirements then all requirements are met
if (!reqs)
return true;
if (reqs.tech)
{
return (this.playerData.researchedTechs[reqs.tech] !== undefined && this.playerData.researchedTechs[reqs.tech]);
}
else if (reqs.all)
{
for (var i = 0; i < reqs.all.length; i++)
{
if (!this.checkTechRequirements(reqs.all[i]))
return false;
}
return true;
}
else if (reqs.any)
{
for (var i = 0; i < reqs.any.length; i++)
{
if (this.checkTechRequirements(reqs.any[i]))
return true;
}
return false;
}
else if (reqs.class)
{
if (reqs.numberOfTypes)
{
if (this.playerData.typeCountsByClass[reqs.class])
return (reqs.numberOfTypes <= Object.keys(this.playerData.typeCountsByClass[reqs.class]).length);
else
return false;
}
else if (reqs.number)
{
if (this.playerData.classCounts[reqs.class])
return (reqs.number <= this.playerData.classCounts[reqs.class]);
else
return false;
}
}
else if (reqs.civ)
{
if (this.playerData.civ == reqs.civ)
return true;
else
return false;
}
// The technologies requirements are not a recognised format
error("Bad requirements " + uneval(reqs));
return false;
};
GameState.prototype.getTimeElapsed = function()
{
return this.timeElapsed;
};
GameState.prototype.getTemplate = function(type)
{
if (this.techTemplates[type] !== undefined)
return new Technology(this.techTemplates, type);
if (!this.templates[type])
return null;
return new EntityTemplate(this.templates[type], this.techModifications);
};
GameState.prototype.applyCiv = function(str) {
return str.replace(/\{civ\}/g, this.playerData.civ);
};
GameState.prototype.civ = function() {
return this.playerData.civ;
};
/**
* @returns {Resources}
*/
GameState.prototype.getResources = function() {
return new Resources(this.playerData.resourceCounts);
};
GameState.prototype.getMap = function() {
return this.sharedScript.passabilityMap;
};
GameState.prototype.getPopulation = function() {
return this.playerData.popCount;
};
GameState.prototype.getPopulationLimit = function() {
return this.playerData.popLimit;
};
GameState.prototype.getPopulationMax = function() {
return this.playerData.popMax;
};
GameState.prototype.getPassabilityClassMask = function(name) {
if (!(name in this.sharedScript.passabilityClasses)){
error("Tried to use invalid passability class name '" + name + "'");
}
return this.sharedScript.passabilityClasses[name];
};
GameState.prototype.getPlayerID = function() {
return this.player;
};
GameState.prototype.isPlayerAlly = function(id) {
return this.playerData.isAlly[id];
};
GameState.prototype.isPlayerEnemy = function(id) {
return this.playerData.isEnemy[id];
};
GameState.prototype.getEnemies = function(){
var ret = [];
for (var i in this.playerData.isEnemy){
if (this.playerData.isEnemy[i]){
ret.push(i);
}
}
return ret;
};
GameState.prototype.isEntityAlly = function(ent) {
if (ent && ent.owner && (typeof ent.owner) === "function"){
return this.playerData.isAlly[ent.owner()];
} else if (ent && ent.owner){
return this.playerData.isAlly[ent.owner];
}
return false;
};
GameState.prototype.isEntityEnemy = function(ent) {
if (ent && ent.owner && (typeof ent.owner) === "function"){
return this.playerData.isEnemy[ent.owner()];
} else if (ent && ent.owner){
return this.playerData.isEnemy[ent.owner];
}
return false;
};
GameState.prototype.isEntityOwn = function(ent) {
if (ent && ent.owner && (typeof ent.owner) === "function"){
return ent.owner() == this.player;
} else if (ent && ent.owner){
return ent.owner == this.player;
}
return false;
};
GameState.prototype.getOwnEntities = function() {
return this.updatingCollection("own-entities", Filters.byOwner(this.player));
};
GameState.prototype.getEnemyEntities = function() {
var diplomacyChange = false;
var enemies = this.getEnemies();
if (this.enemies){
if (this.enemies.length != enemies.length){
diplomacyChange = true;
}else{
for (var i = 0; i < enemies.length; i++){
if (enemies[i] !== this.enemies[i]){
diplomacyChange = true;
}
}
}
}
if (diplomacyChange || !this.enemies){
return this.updatingCollection("enemy-entities", Filters.byOwners(enemies));
this.enemies = enemies;
}
return this.getEC("enemy-entities");
};
GameState.prototype.getEntities = function() {
return this.entities;
};
GameState.prototype.getEntityById = function(id){
if (this.entities._entities[id]) {
return this.entities._entities[id];
}else{
//debug("Entity " + id + " requested does not exist");
}
return undefined;
};
GameState.prototype.getOwnEntitiesByMetadata = function(key, value){
return this.updatingCollection(key + "-" + value, Filters.byMetadata(this.player, key, value),this.getOwnEntities());
};
GameState.prototype.getOwnEntitiesByRole = function(role){
return this.getOwnEntitiesByMetadata("role", role);
};
GameState.prototype.getOwnTrainingFacilities = function(){
return this.updatingCollection("own-training-facilities", Filters.byTrainingQueue(), this.getOwnEntities());
};
GameState.prototype.getOwnResearchFacilities = function(){
return this.updatingCollection("own-research-facilities", Filters.byResearchAvailable(), this.getOwnEntities());
};
GameState.prototype.getOwnEntitiesByType = function(type){
var filter = Filters.byType(type);
return this.updatingCollection("own-by-type-" + type, filter, this.getOwnEntities());
};
GameState.prototype.countEntitiesByType = function(type) {
return this.getOwnEntitiesByType(type).length;
};
GameState.prototype.countEntitiesAndQueuedByType = function(type) {
var count = this.countEntitiesByType(type);
// Count building foundations
count += this.countEntitiesByType("foundation|" + type);
// Count animal resources
count += this.countEntitiesByType("resource|" + type);
// Count entities in building production queues
this.getOwnTrainingFacilities().forEach(function(ent){
ent.trainingQueue().forEach(function(item) {
if (item.template == type){
count += item.count;
}
});
});
return count;
};
GameState.prototype.countFoundationsWithType = function(type) {
var foundationType = "foundation|" + type;
var count = 0;
this.getOwnEntities().forEach(function(ent) {
var t = ent.templateName();
if (t == foundationType)
++count;
});
return count;
};
GameState.prototype.countOwnEntitiesByRole = function(role) {
return this.getOwnEntitiesByRole(role).length;
};
GameState.prototype.countOwnEntitiesAndQueuedWithRole = function(role) {
var count = this.countOwnEntitiesByRole(role);
// Count entities in building production queues
this.getOwnTrainingFacilities().forEach(function(ent) {
ent.trainingQueue().forEach(function(item) {
if (item.metadata && item.metadata.role == role)
count += item.count;
});
});
return count;
};
GameState.prototype.countOwnQueuedEntitiesWithMetadata = function(data, value) {
// Count entities in building production queues
var count = 0;
this.getOwnTrainingFacilities().forEach(function(ent) {
ent.trainingQueue().forEach(function(item) {
if (item.metadata && item.metadata[data] && item.metadata[data] == value)
count += item.count;
});
});
return count;
};
/**
* Find buildings that are capable of training the given unit type, and aren't
* already too busy.
*/
GameState.prototype.findTrainers = function(template) {
var maxQueueLength = 3; // avoid tying up resources in giant training queues
return this.getOwnTrainingFacilities().filter(function(ent) {
var trainable = ent.trainableEntities();
if (!trainable || trainable.indexOf(template) == -1)
return false;
var queue = ent.trainingQueue();
if (queue) {
if (queue.length >= maxQueueLength)
return false;
}
return true;
});
};
/**
* Find units that are capable of constructing the given building type.
*/
GameState.prototype.findBuilders = function(template) {
return this.getOwnEntities().filter(function(ent) {
var buildable = ent.buildableEntities();
if (!buildable || buildable.indexOf(template) == -1)
return false;
return true;
});
};
/**
* Find buildings that are capable of researching the given tech, and aren't
* already too busy.
*/
GameState.prototype.findResearchers = function(templateName) {
// let's check we can research the tech.
if (!this.canResearch(templateName))
return [];
var template = this.getTemplate(templateName);
return this.getOwnResearchFacilities().filter(function(ent) { //}){
var techs = ent.researchableTechs();
if (!techs || (template.pair() && techs.indexOf(template.pair()) === -1) || (!template.pair() && techs.indexOf(templateName) === -1))
return false;
return true;
});
};
GameState.prototype.getOwnFoundations = function() {
return this.updatingCollection("ownFoundations", Filters.isFoundation(), this.getOwnEntities());
};
GameState.prototype.getOwnDropsites = function(resource){
return this.updatingCollection("dropsite-own-" + resource, Filters.isDropsite(resource), this.getOwnEntities());
};
GameState.prototype.getResourceSupplies = function(resource){
return this.updatingGlobalCollection("resource-" + resource, Filters.byResource(resource), this.getEntities());
};
GameState.prototype.getEntityLimits = function() {
return this.playerData.entityLimits;
};
GameState.prototype.getEntityCounts = function() {
return this.playerData.entityCounts;
};
// Checks whether the maximum number of buildings have been cnstructed for a certain catergory
GameState.prototype.isEntityLimitReached = function(category) {
if(this.playerData.entityLimits[category] === undefined || this.playerData.entityCounts[category] === undefined)
return false;
if(this.playerData.entityLimits[category].LimitsPerCivCentre != undefined)
return (this.playerData.entityCounts[category] >= this.playerData.entityCounts["CivilCentre"]*this.playerData.entityLimits[category].LimitPerCivCentre);
else
return (this.playerData.entityCounts[category] >= this.playerData.entityLimits[category]);
};
GameState.prototype.findTrainableUnits = function(classes){
var allTrainable = [];
this.getOwnEntities().forEach(function(ent) {
var trainable = ent.trainableEntities();
if (ent.hasClass("Structure"))
for (var i in trainable){
if (allTrainable.indexOf(trainable[i]) === -1){
allTrainable.push(trainable[i]);
}
}
});
var ret = [];
for (var i in allTrainable) {
var template = this.getTemplate(allTrainable[i]);
var okay = true;
for (o in classes)
if (!template.hasClass(classes[o]))
okay = false;
if (template.hasClass("Hero")) // disabling heroes for now
okay = false;
if (okay)
ret.push( [allTrainable[i], template] );
}
return ret;
};
// Return all techs which can currently be researched
// Does not factor cost.
// If there are pairs, both techs are returned.
GameState.prototype.findAvailableTech = function() {
var allResearchable = [];
this.getOwnEntities().forEach(function(ent) {
var searchable = ent.researchableTechs();
for (var i in searchable) {
if (allResearchable.indexOf(searchable[i]) === -1) {
allResearchable.push(searchable[i]);
}
}
});
var ret = [];
for (var i in allResearchable) {
var template = this.getTemplate(allResearchable[i]);
if (template.pairDef())
{
var techs = template.getPairedTechs();
if (this.canResearch(techs[0]._templateName))
ret.push([techs[0]._templateName, techs[0]] );
if (this.canResearch(techs[1]._templateName))
ret.push([techs[1]._templateName, techs[1]] );
} else {
if (this.canResearch(allResearchable[i]) && template._templateName != "phase_town" && template._templateName != "phase_city_generic")
ret.push( [allResearchable[i], template] );
}
}
return ret;
};
// defcon utilities
GameState.prototype.timeSinceDefconChange = function() {
return this.getTimeElapsed()-this.ai.defconChangeTime;
};
GameState.prototype.setDefcon = function(level,force) {
if (this.ai.defcon >= level || force) {
this.ai.defcon = level;
this.ai.defconChangeTime = this.getTimeElapsed();
}
};
GameState.prototype.defcon = function() {
return this.ai.defcon;
};

View File

@ -0,0 +1,272 @@
/* The map module.
* Copied with changes from QuantumState's original for qBot, it's a component for storing 8 bit values.
*/
function Map(sharedScript, originalMap, actualCopy){
// get the map to find out the correct dimensions
var gameMap = sharedScript.passabilityMap;
this.width = gameMap.width;
this.height = gameMap.height;
this.length = gameMap.data.length;
if (originalMap && actualCopy){
this.map = new Uint8Array(this.length);
for (var i = 0; i < originalMap.length; ++i)
this.map[i] = originalMap[i];
} else if (originalMap) {
this.map = originalMap;
} else {
this.map = new Uint8Array(this.length);
}
this.cellSize = 4;
}
Map.prototype.gamePosToMapPos = function(p){
return [Math.round(p[0]/this.cellSize), Math.round(p[1]/this.cellSize)];
};
Map.prototype.point = function(p){
var q = this.gamePosToMapPos(p);
return this.map[q[0] + this.width * q[1]];
};
Map.prototype.addInfluence = function(cx, cy, maxDist, strength, type) {
strength = strength ? +strength : +maxDist;
type = type ? type : 'linear';
var x0 = Math.max(0, cx - maxDist);
var y0 = Math.max(0, cy - maxDist);
var x1 = Math.min(this.width, cx + maxDist);
var y1 = Math.min(this.height, cy + maxDist);
var maxDist2 = maxDist * maxDist;
var str = 0.0;
switch (type){
case 'linear':
str = +strength / +maxDist;
break;
case 'quadratic':
str = +strength / +maxDist2;
break;
case 'constant':
str = +strength;
break;
}
for ( var y = y0; y < y1; ++y) {
for ( var x = x0; x < x1; ++x) {
var dx = x - cx;
var dy = y - cy;
var r2 = dx*dx + dy*dy;
if (r2 < maxDist2){
var quant = 0;
switch (type){
case 'linear':
var r = Math.sqrt(r2);
quant = str * (maxDist - r);
break;
case 'quadratic':
quant = str * (maxDist2 - r2);
break;
case 'constant':
quant = str;
break;
}
if (this.map[x + y * this.width] + quant > 255){
this.map[x + y * this.width] = 255;
} else if (this.map[x + y * this.width] + quant < 0){
this.map[x + y * this.width] = 0;
} else {
this.map[x + y * this.width] += quant;
}
}
}
}
};
Map.prototype.multiplyInfluence = function(cx, cy, maxDist, strength, type) {
strength = strength ? +strength : +maxDist;
type = type ? type : 'constant';
var x0 = Math.max(0, cx - maxDist);
var y0 = Math.max(0, cy - maxDist);
var x1 = Math.min(this.width, cx + maxDist);
var y1 = Math.min(this.height, cy + maxDist);
var maxDist2 = maxDist * maxDist;
var str = 0.0;
switch (type){
case 'linear':
str = strength / maxDist;
break;
case 'quadratic':
str = strength / maxDist2;
break;
case 'constant':
str = strength;
break;
}
for ( var y = y0; y < y1; ++y) {
for ( var x = x0; x < x1; ++x) {
var dx = x - cx;
var dy = y - cy;
var r2 = dx*dx + dy*dy;
if (r2 < maxDist2){
var quant = 0;
switch (type){
case 'linear':
var r = Math.sqrt(r2);
quant = str * (maxDist - r);
break;
case 'quadratic':
quant = str * (maxDist2 - r2);
break;
case 'constant':
quant = str;
break;
}
var machin = this.map[x + y * this.width] * quant;
if (machin <= 0){
this.map[x + y * this.width] = 0; //set anything which would have gone negative to 0
}else{
this.map[x + y * this.width] = machin;
}
}
}
}
};
Map.prototype.setInfluence = function(cx, cy, maxDist, value) {
value = value ? value : 0;
var x0 = Math.max(0, cx - maxDist);
var y0 = Math.max(0, cy - maxDist);
var x1 = Math.min(this.width, cx + maxDist);
var y1 = Math.min(this.height, cy + maxDist);
var maxDist2 = maxDist * maxDist;
for ( var y = y0; y < y1; ++y) {
for ( var x = x0; x < x1; ++x) {
var dx = x - cx;
var dy = y - cy;
var r2 = dx*dx + dy*dy;
if (r2 < maxDist2){
this.map[x + y * this.width] = value;
}
}
}
};
Map.prototype.sumInfluence = function(cx, cy, radius){
var x0 = Math.max(0, cx - radius);
var y0 = Math.max(0, cy - radius);
var x1 = Math.min(this.width, cx + radius);
var y1 = Math.min(this.height, cy + radius);
var radius2 = radius * radius;
var sum = 0;
for ( var y = y0; y < y1; ++y) {
for ( var x = x0; x < x1; ++x) {
var dx = x - cx;
var dy = y - cy;
var r2 = dx*dx + dy*dy;
if (r2 < radius2){
sum += this.map[x + y * this.width];
}
}
}
return sum;
};
/**
* Make each cell's 8-bit value at least one greater than each of its
* neighbours' values. Possible assignment of a cap (maximum).
*/
Map.prototype.expandInfluences = function(maximum, map) {
var grid = this.map;
if (map !== undefined)
grid = map;
if (maximum == undefined)
maximum = 255;
var w = this.width;
var h = this.height;
for ( var y = 0; y < h; ++y) {
var min = maximum;
for ( var x = 0; x < w; ++x) {
var g = grid[x + y * w];
if (g > min)
grid[x + y * w] = min;
else if (g < min)
min = g;
++min;
if (min > maximum)
min = maximum;
}
for ( var x = w - 2; x >= 0; --x) {
var g = grid[x + y * w];
if (g > min)
grid[x + y * w] = min;
else if (g < min)
min = g;
++min;
if (min > maximum)
min = maximum;
}
}
for ( var x = 0; x < w; ++x) {
var min = maximum;
for ( var y = 0; y < h; ++y) {
var g = grid[x + y * w];
if (g > min)
grid[x + y * w] = min;
else if (g < min)
min = g;
++min;
if (min > maximum)
min = maximum;
}
for ( var y = h - 2; y >= 0; --y) {
var g = grid[x + y * w];
if (g > min)
grid[x + y * w] = min;
else if (g < min)
min = g;
++min;
if (min > maximum)
min = maximum;
}
}
};
Map.prototype.findBestTile = function(radius, obstructionTiles){
// Find the best non-obstructed tile
var bestIdx = 0;
var bestVal = -1;
for ( var i = 0; i < this.length; ++i) {
if (obstructionTiles.map[i] > radius) {
var v = this.map[i];
if (v > bestVal) {
bestVal = v;
bestIdx = i;
}
}
}
return [bestIdx, bestVal];
};
// Multiplies current map by the parameter map pixelwise
Map.prototype.multiply = function(map, onlyBetter, divider, maxMultiplier){
for (var i = 0; i < this.length; ++i){
if (map.map[i]/divider > 1)
this.map[i] = Math.min(maxMultiplier*this.map[i], this.map[i] * (map.map[i]/divider));
}
};
// add to current map by the parameter map pixelwise
Map.prototype.add = function(map){
for (var i = 0; i < this.length; ++i){
this.map[i] += +map.map[i];
}
};

View File

@ -21,6 +21,14 @@ function Resources(amounts, population) {
Resources.prototype.types = [ "food", "wood", "stone", "metal" ];
Resources.prototype.reset = function() {
for ( var tKey in this.types) {
var t = this.types[tKey];
this[t] = 0;
}
this.population = 0;
};
Resources.prototype.canAfford = function(that) {
for ( var tKey in this.types) {
var t = this.types[tKey];

View File

@ -0,0 +1,314 @@
// Shared script handling templates and basic terrain analysis
function SharedScript(settings)
{
if (!settings)
return;
// Make some properties non-enumerable, so they won't be serialised
Object.defineProperty(this, "_players", {value: settings.players, enumerable: false});
Object.defineProperty(this, "_templates", {value: settings.templates, enumerable: false});
Object.defineProperty(this, "_derivedTemplates", {value: {}, enumerable: false});
Object.defineProperty(this, "_techTemplates", {value: settings.techTemplates, enumerable: false});
this._entityMetadata = {};
for (i in this._players)
this._entityMetadata[this._players[i]] = {};
// always create for 8 + gaia players, since _players isn't aware of the human.
this._techModifications = { 0 : {}, 1 : {}, 2 : {}, 3 : {}, 4 : {}, 5 : {}, 6 : {}, 7 : {}, 8 : {} };
// array of entity collections
this._entityCollections = [];
// each name is a reference to the actual one.
this._entityCollectionsName = {};
this._entityCollectionsByDynProp = {};
this._entityCollectionsUID = 0;
this.turn = 0;
}
//Return a simple object (using no classes etc) that will be serialized
//into saved games
// TODO: that
SharedScript.prototype.Serialize = function()
{
};
//Called after the constructor when loading a saved game, with 'data' being
//whatever Serialize() returned
// TODO: that
SharedScript.prototype.Deserialize = function(data)
{
};
// Components that will be disabled in foundation entity templates.
// (This is a bit yucky and fragile since it's the inverse of
// CCmpTemplateManager::CopyFoundationSubset and only includes components
// that our EntityTemplate class currently uses.)
var g_FoundationForbiddenComponents = {
"ProductionQueue": 1,
"ResourceSupply": 1,
"ResourceDropsite": 1,
"GarrisonHolder": 1,
};
// Components that will be disabled in resource entity templates.
// Roughly the inverse of CCmpTemplateManager::CopyResourceSubset.
var g_ResourceForbiddenComponents = {
"Cost": 1,
"Decay": 1,
"Health": 1,
"UnitAI": 1,
"UnitMotion": 1,
"Vision": 1
};
SharedScript.prototype.GetTemplate = function(name)
{
if (this._templates[name])
return this._templates[name];
if (this._derivedTemplates[name])
return this._derivedTemplates[name];
// If this is a foundation template, construct it automatically
if (name.indexOf("foundation|") !== -1)
{
var base = this.GetTemplate(name.substr(11));
var foundation = {};
for (var key in base)
if (!g_FoundationForbiddenComponents[key])
foundation[key] = base[key];
this._derivedTemplates[name] = foundation;
return foundation;
}
else if (name.indexOf("resource|") !== -1)
{
var base = this.GetTemplate(name.substr(9));
var resource = {};
for (var key in base)
if (!g_ResourceForbiddenComponents[key])
resource[key] = base[key];
this._derivedTemplates[name] = resource;
return resource;
}
error("Tried to retrieve invalid template '"+name+"'");
return null;
};
// initialize the shared component using a given gamestate (the initial gamestate after map creation, usually)
// this is called right at the end of map generation, before you actually reach the map.
SharedScript.prototype.initWithState = function(state) {
this.passabilityClasses = state.passabilityClasses;
this.passabilityMap = state.passabilityMap;
for (o in state.players)
this._techModifications[o] = state.players[o].techModifications;
this.techModifications = this._techModifications;
this._entities = {};
for (var id in state.entities)
{
this._entities[id] = new Entity(this, state.entities[id]);
}
// entity collection updated on create/destroy event.
this.entities = new EntityCollection(this, this._entities);
// create the terrain analyzer
this.terrainAnalyzer = new TerrainAnalysis(this, state);
this.accessibility = new Accessibility(state, this.terrainAnalyzer);
this.gameState = {};
for (i in this._players)
{
this.gameState[this._players[i]] = new GameState(this,state,this._players[i]);
}
};
// General update of the shared script, before each AI's update
// applies entity deltas, and each gamestate.
SharedScript.prototype.onUpdate = function(state)
{
this.ApplyEntitiesDelta(state);
Engine.ProfileStart("onUpdate");
this.events = state.events;
this.passabilityClasses = state.passabilityClasses;
this.passabilityMap = state.passabilityMap;
this.players = this._players;
this.playersData = state.players;
this.territoryMap = state.territoryMap;
this.timeElapsed = state.timeElapsed;
for (o in state.players)
this._techModifications[o] = state.players[o].techModifications;
for (i in this.gameState)
this.gameState[i].update(this,state);
this.terrainAnalyzer.updateMapWithEvents(this);
//this.OnUpdate();
this.turn++;
Engine.ProfileStop();
};
SharedScript.prototype.ApplyEntitiesDelta = function(state)
{
Engine.ProfileStart("Shared ApplyEntitiesDelta");
for each (var evt in state.events)
{
if (evt.type == "Create")
{
if (!state.entities[evt.msg.entity])
{
continue; // Sometimes there are things like foundations which get destroyed too fast
}
this._entities[evt.msg.entity] = new Entity(this, state.entities[evt.msg.entity]);
this.entities.addEnt(this._entities[evt.msg.entity]);
// Update all the entity collections since the create operation affects static properties as well as dynamic
for each (var entCollection in this._entityCollections)
{
entCollection.updateEnt(this._entities[evt.msg.entity]);
}
}
else if (evt.type == "Destroy")
{
// A small warning: javascript "delete" does not actually delete, it only removes the reference in this object/
// the "deleted" object remains in memory, and any older reference to will still reference it as if it were not "deleted".
// Worse, they might prevent it from being garbage collected, thus making it stay alive and consuming ram needlessly.
// So take care, and if you encounter a weird bug with deletion not appearing to work correctly, this is probably why.
if (!this._entities[evt.msg.entity])
{
continue;
}
// The entity was destroyed but its data may still be useful, so
// remember the entity and this AI's metadata concerning it
evt.msg.metadata = (evt.msg.metadata || []);
evt.msg.entityObj = (evt.msg.entityObj || this._entities[evt.msg.entity]);
//evt.msg.metadata[this._player] = this._entityMetadata[evt.msg.entity];
for each (var entCol in this._entityCollections)
{
entCol.removeEnt(this._entities[evt.msg.entity]);
}
this.entities.removeEnt(this._entities[evt.msg.entity]);
delete this._entities[evt.msg.entity];
for (i in this._players)
delete this._entityMetadata[this._players[i]][evt.msg.entity];
}
else if (evt.type == "TrainingFinished")
{
// Apply metadata stored in training queues
for each (var ent in evt.msg.entities)
{
for (key in evt.msg.metadata)
{
this.setMetadata(evt.msg.owner, this._entities[ent], key, evt.msg.metadata[key])
}
}
}
}
for (var id in state.entities)
{
var changes = state.entities[id];
for (var prop in changes)
{
this._entities[id]._entity[prop] = changes[prop];
this.updateEntityCollections(prop, this._entities[id]);
}
}
Engine.ProfileStop();
};
SharedScript.prototype.registerUpdatingEntityCollection = function(entCollection, noPush)
{
if (!noPush) {
this._entityCollections.push(entCollection);
}
entCollection.setUID(this._entityCollectionsUID);
for each (var prop in entCollection.dynamicProperties())
{
this._entityCollectionsByDynProp[prop] = this._entityCollectionsByDynProp[prop] || [];
this._entityCollectionsByDynProp[prop].push(entCollection);
}
this._entityCollectionsUID++;
};
SharedScript.prototype.removeUpdatingEntityCollection = function(entCollection)
{
for (var i in this._entityCollections)
{
if (this._entityCollections[i].getUID() === entCollection.getUID())
{
this._entityCollections.splice(i, 1);
}
}
for each (var prop in entCollection.dynamicProperties())
{
for (var i in this._entityCollectionsByDynProp[prop])
{
if (this._entityCollectionsByDynProp[prop][i].getUID() === entCollection.getUID())
{
this._entityCollectionsByDynProp[prop].splice(i, 1);
}
}
}
};
SharedScript.prototype.updateEntityCollections = function(property, ent)
{
if (this._entityCollectionsByDynProp[property] !== undefined)
{
for each (var entCollection in this._entityCollectionsByDynProp[property])
{
entCollection.updateEnt(ent);
}
}
}
SharedScript.prototype.setMetadata = function(player, ent, key, value)
{
var metadata = this._entityMetadata[player][ent.id()];
if (!metadata)
metadata = this._entityMetadata[player][ent.id()] = {};
metadata[key] = value;
this.updateEntityCollections('metadata', ent);
this.updateEntityCollections('metadata.' + key, ent);
};
SharedScript.prototype.getMetadata = function(player, ent, key)
{
var metadata = this._entityMetadata[player][ent.id()];
if (!metadata || !(key in metadata))
return undefined;
return metadata[key];
};
function copyPrototype(descendant, parent) {
var sConstructor = parent.toString();
var aMatch = sConstructor.match( /\s*function (.*)\(/ );
if ( aMatch != null ) { descendant.prototype[aMatch[1]] = parent; }
for (var m in parent.prototype) {
descendant.prototype[m] = parent.prototype[m];
}
};

View File

@ -0,0 +1,138 @@
// Wrapper around a technology template
function Technology(allTemplates, templateName)
{
this._templateName = templateName;
var template = allTemplates[templateName];
// check if this is one of two paired technologies.
this._isPair = template.pair === undefined ? false : true;
if (this._isPair)
{
if (allTemplates[template.pair].top == templateName)
this._pairedWith = allTemplates[template.pair].bottom;
else
this._pairedWith = allTemplates[template.pair].top;
}
// check if it only defines a pair:
this._definesPair = template.top === undefined ? false : true;
this._template = template;
this._techTemplates = allTemplates;
}
// returns generic, or specific if civ provided.
Technology.prototype.name = function(civ)
{
if (civ === undefined)
{
return this._template.genericName;
}
else
{
if (this._template.specificName === undefined || this._template.specificName[civ] === undefined)
return undefined;
return this._template.specificName[civ];
}
};
Technology.prototype.pairDef = function()
{
return this._definesPair;
};
// in case this defines a pair only, returns the two paired technologies.
Technology.prototype.getPairedTechs = function()
{
if (!this._definesPair)
return undefined;
var techOne = new Technology(this._techTemplates, this._template.top);
var techTwo = new Technology(this._techTemplates, this._template.bottom);
return [techOne,techTwo];
};
Technology.prototype.pair = function()
{
if (!this._isPair)
return undefined;
return this._template.pair;
};
Technology.prototype.pairedWith = function()
{
if (!this._isPair)
return undefined;
return this._pairedWith;
};
Technology.prototype.cost = function()
{
if (!this._template.cost)
return undefined;
return this._template.cost;
};
// seconds
Technology.prototype.researchTime = function()
{
if (!this._template.researchTime)
return undefined;
return this._template.researchTime;
};
Technology.prototype.requirements = function()
{
if (!this._template.requirements)
return undefined;
return this._template.requirements;
};
Technology.prototype.autoResearch = function()
{
if (!this._template.autoResearch)
return undefined;
return this._template.autoResearch;
};
Technology.prototype.supersedes = function()
{
if (!this._template.supersedes)
return undefined;
return this._template.supersedes;
};
Technology.prototype.modifications = function()
{
if (!this._template.modifications)
return undefined;
return this._template.modifications;
};
Technology.prototype.affects = function()
{
if (!this._template.affects)
return undefined;
return this._template.affects;
};
Technology.prototype.isAffected = function(classes)
{
if (!this._template.affects)
return false;
for (index in this._template.affects)
{
var reqClasses = this._template.affects[index].split(" ");
var fitting = true;
for (i in reqClasses)
{
if (classes.indexOf(reqClasses[i]) === -1)
{
fitting = false;
break;
}
}
if (fitting === true)
return true;
}
return false;
};

View File

@ -0,0 +1,309 @@
// An implementation of A* as a pathfinder.
// It's oversamplable, and has a specific "distance from block"
// variable to avoid narrow passages if wanted.
// It can carry a calculation over multiple turns.
// It can work over water, or land.
// Note: while theoretically possible, when this goes from land to water
// It will return the path to the "boarding" point and
// a new path will need to be created.
// this should only be called by an AI player after setting gamestate.ai
// The initializer creates an expanded influence map for checking.
// It's not extraordinarily slow, but it might be.
function aStarPath(gameState, onWater, disregardEntities) {
var self = this;
// get the terrain analyzer map as a reference.
this.Map(gameState.ai, gameState.ai.terrainAnalyzer.map);
// get the accessibility as a reference
this.accessibility = gameState.ai.accessibility;
this.terrainAnalyzer = gameState.ai.terrainAnalyzer;
if (onWater) {
this.waterPathfinder = true;
} else
this.waterPathfinder = false;
this.widthMap = new Uint8Array(this.map.length);
for (var i = 0; i < this.map.length; ++i) {
if (this.map[i] === 0)
this.widthMap[i] = 0;
else if (!disregardEntities && this.map[i] === 30)
this.widthMap[i] = 0;
else if (!disregardEntities && this.map[i] === 40)
this.widthMap[i] = 0;
else if (!disregardEntities && this.map[i] === 41)
this.widthMap[i] = 2;
else if (!disregardEntities && this.map[i] === 42)
this.widthMap[i] = 1;
else
this.widthMap[i] = 255;
}
this.expandInfluences(255,this.widthMap);
}
copyPrototype(aStarPath, Map);
// marks some points of the map as impassable. This can be used to create different paths, or to avoid going through some areas.
aStarPath.prototype.markImpassableArea = function(cx, cy, Distance) {
[cx,cy] = this.gamePosToMapPos([cx,cy]);
var x0 = Math.max(0, cx - Distance);
var y0 = Math.max(0, cy - Distance);
var x1 = Math.min(this.width, cx + Distance);
var y1 = Math.min(this.height, cy + Distance);
var maxDist2 = Distance * Distance;
for ( var y = y0; y < y1; ++y) {
for ( var x = x0; x < x1; ++x) {
var dx = x - cx;
var dy = y - cy;
var r2 = dx*dx + dy*dy;
if (r2 < maxDist2){
this.widthMap[x + y * this.width] = 0;
}
}
}
};
// sending gamestate creates a map
aStarPath.prototype.getPath = function(start, end, Sampling, minWidth, iterationLimit, gamestate)
{
this.Sampling = Sampling >= 1 ? Sampling : 1;
this.minWidth = (minWidth !== undefined && minWidth >= this.Sampling) ? minWidth : this.Sampling;
if (start[0] < 0 || this.gamePosToMapPos(start)[0] >= this.width || start[1] < 0 || this.gamePosToMapPos(start)[1] >= this.height)
return undefined;
var s = this.terrainAnalyzer.findClosestPassablePoint(this.gamePosToMapPos(start), !this.waterPathfinder,500,true);
var e = this.terrainAnalyzer.findClosestPassablePoint(this.gamePosToMapPos(end), !this.waterPathfinder,500,true);
var w = this.width;
if (!s || !e) {
return undefined;
}
if (gamestate !== undefined)
{
this.TotorMap = new Map(gamestate);
this.TotorMap.addInfluence(s[0],s[1],1,200,'constant');
this.TotorMap.addInfluence(e[0],e[1],1,200,'constant');
}
this.iterationLimit = 9000000000;
if (iterationLimit !== undefined)
this.iterationLimit = iterationLimit;
this.s = s[0] + w*s[1];
this.e = e[0] + w*e[1];
if (this.waterPathfinder && this.map[this.s] !== 200 && this.map[this.s] !== 201)
{
debug ("Trying a path over water, but we are on land, aborting");
return undefined;
} else if (!this.waterPathfinder && this.map[this.s] === 200)
{
debug ("Trying a path over land, but we are over water, aborting");
return undefined;
}
this.onWater = this.waterPathfinder;
this.pathChangesTransport = false;
// We are going to create a map, it's going to take memory. To avoid OOM errors, GC before we do so.
//Engine.ForceGC();
this.openList = [];
this.parentSquare = new Uint32Array(this.map.length);
this.isOpened = new Boolean(this.map.length);
this.fCostArray = new Uint32Array(this.map.length);
this.gCostArray = new Uint32Array(this.map.length);
this.currentSquare = this.s;
this.totalIteration = 0;
this.isOpened[this.s] = true;
this.openList.push(this.s);
this.fCostArray[this.s] = SquareVectorDistance([this.s%w, Math.floor(this.s/w)], [this.e%w, Math.floor(this.e/w)]);
this.gCostArray[this.s] = 0;
this.parentSquare[this.s] = this.s;
return this.continuePath(gamestate);
}
// in case it's not over yet, this can carry on the calculation of a path over multiple turn until it's over
aStarPath.prototype.continuePath = function(gamestate)
{
var w = this.width;
var h = this.height;
var positions = [[0,1], [0,-1], [1,0], [-1,0], [1,1], [-1,-1], [1,-1], [-1,1]];
var cost = [100,100,100,100,150,150,150,150];
//creation of variables used in the loop
var found = false;
var shortcut = false;
var infinity = Math.min();
var currentDist = infinity;
var e = this.e;
var s = this.s;
var iteration = 0;
var target = [this.e%w, Math.floor(this.e/w)];
var changes = {};
var tIndex = 0;
// on to A*
while (found === false && this.openList.length !== 0 && iteration < this.iterationLimit) {
currentDist = infinity;
if (shortcut === true) {
this.currentSquare = this.openList.shift();
} else {
for (i in this.openList)
{
var sum = this.fCostArray[this.openList[i]] + this.gCostArray[this.openList[i]];
if (sum < currentDist)
{
this.currentSquare = this.openList[i];
tIndex = i;
currentDist = sum;
}
}
this.openList.splice(tIndex,1);
}
if (!this.onWater && this.map[this.currentSquare] === 200) {
this.onWater = true;
} else if (this.onWater && (this.map[this.currentSquare] !== 200 && this.map[this.currentSquare] !== 201)) {
this.onWater = false;
}
shortcut = false;
this.isOpened[this.currentSquare] = false;
if (gamestate !== undefined)
this.TotorMap.addInfluence(this.currentSquare % w, Math.floor(this.currentSquare / w),1,40,'constant');
for (i in positions)
{
var index = 0 + this.currentSquare +positions[i][0]*this.Sampling +w*this.Sampling*positions[i][1];
if (this.widthMap[index] >= this.minWidth || (this.onWater && this.map[index] > 0 && this.map[index] !== 200 && this.map[index] !== 201)
|| (!this.onWater && this.map[this.index] === 200))
{
if(this.isOpened[index] === undefined)
{
this.parentSquare[index] = this.currentSquare;
this.fCostArray[index] = SquareVectorDistance([index%w, Math.floor(index/w)], target);// * cost[i];
this.gCostArray[index] = this.gCostArray[this.currentSquare] + cost[i] * this.Sampling;// - this.map[index];
if (!this.onWater && this.map[index] === 200) {
this.gCostArray[index] += 10000;
} else if (this.onWater && this.map[index] !== 200) {
this.gCostArray[index] += 10000;
}
if (this.map[index] === 200 || (this.map[index] === 201 && this.onWater))
this.gCostArray[index] += 1000;
if (this.openList[0] !== undefined && this.fCostArray[this.openList[0]] + this.gCostArray[this.openList[0]] > this.fCostArray[index] + this.gCostArray[index])
{
this.openList.unshift(index);
shortcut = true;
} else {
this.openList.push(index);
}
this.isOpened[index] = true;
if (SquareVectorDistance( [this.currentSquare%w, Math.floor(this.currentSquare/w)] , target) <= this.Sampling*this.Sampling) {
if (this.e != this.currentSquare)
this.parentSquare[this.e] = this.currentSquare;
found = true;
break;
}
} else {
var addCost = 0;
if (!this.onWater && this.map[index] === 200) {
addCost += 10000;
} else if (this.onWater && this.map[index] !== 200) {
addCost += 10000;
}
if (this.map[index] === 200 || (this.map[index] === 201 && this.onWater))
addCost += 1000;
// already on the Open or closed list
if (this.gCostArray[index] > cost[i] * this.Sampling + addCost + this.gCostArray[this.currentSquare])
{
this.parentSquare[index] = this.currentSquare;
this.gCostArray[index] = cost[i] * this.Sampling + addCost + this.gCostArray[this.currentSquare];
}
}
}
}
iteration++;
}
this.totalIteration += iteration;
if (iteration === this.iterationLimit && found === false && this.openList.length !== 0)
{
// we've got to assume that we stopped because we reached the upper limit of iterations
return "toBeContinued";
}
//debug (this.totalIteration);
var paths = [];
if (found) {
this.currentSquare = e;
var lastPosx = 0;
var lastPosy = 0;
while (this.parentSquare[this.currentSquare] !== s)
{
this.currentSquare = this.parentSquare[this.currentSquare];
if (!this.onWater && this.map[this.currentSquare] === 200) {
//debug ("We must cross water, going " +this.currentSquare + " from parent " + this.parentSquare[this.currentSquare]);
this.pathChangesTransport = true;
changes[this.currentSquare] = true;
this.onWater = true;
} else if (this.onWater && (this.map[this.currentSquare] !== 200 && this.map[this.currentSquare] !== 201)) {
//debug ("We must cross to the ground, going " +this.currentSquare + " from parent " + this.parentSquare[this.currentSquare]);
this.pathChangesTransport = true;
changes[this.currentSquare] = true;
this.onWater = false;
}
if (gamestate !== undefined && changes[this.currentSquare])
this.TotorMap.addInfluence(this.currentSquare % w, Math.floor(this.currentSquare / w),2,200,'constant');
if (gamestate !== undefined)
this.TotorMap.addInfluence(this.currentSquare % w, Math.floor(this.currentSquare / w),1,50,'constant');
if (SquareVectorDistance([lastPosx,lastPosy],[this.currentSquare % w, Math.floor(this.currentSquare / w)]) > 300 || changes[this.currentSquare])
{
lastPosx = (this.currentSquare % w);
lastPosy = Math.floor(this.currentSquare / w);
paths.push([ [lastPosx*this.cellSize,lastPosy*this.cellSize], changes[this.currentSquare] ]);
if (gamestate !== undefined)
this.TotorMap.addInfluence(this.currentSquare % w, Math.floor(this.currentSquare / w),1,50 + paths.length,'constant');
}
}
} else {
// we have not found a path.
// what do we do then?
}
if (gamestate !== undefined)
this.TotorMap.dumpIm("Path From " +s +" to " +e +".png",255);
delete this.parentSquare;
delete this.isOpened;
delete this.fCostArray;
delete this.gCostArray;
// the return, if defined is [ [path, each waypoint being [position, mustchangeTransport] ], is there any transport change, ]
if (paths.length > 0) {
return [paths, this.pathChangesTransport];
} else {
return undefined;
}
}

View File

@ -0,0 +1,571 @@
/*
* TerrainAnalysis, inheriting from the Map Component.
*
* This creates a suitable passability map for pathfinding units and provides the findClosestPassablePoint() function.
* This is part of the Shared Script, and thus should only be used for things that are non-player specific.
* This.map is a map of the world, where particular stuffs are pointed with a value
* For example, impassable land is 0, water is 200, areas near tree (ie forest grounds) are 41
* This is intended for use with 8 bit maps for reduced memory usage.
* Upgraded from QuantumState's original TerrainAnalysis for qBot.
* You may notice a lot of the A* star codes differ only by a few things.
* It's wanted: each does a very slightly different things
* But truly separating optimizes.
*/
function TerrainAnalysis(sharedScript,rawState){
var self = this;
this.cellSize = 4;
var passabilityMap = rawState.passabilityMap;
this.width = passabilityMap.width;
this.height = passabilityMap.height;
// the first two won't change, the third is a reference to a value updated by C++
this.obstructionMaskLand = rawState.passabilityClasses["default"];
this.obstructionMaskWater = rawState.passabilityClasses["ship"];
this.obstructionMask = rawState.passabilityClasses["pathfinderObstruction"];
var obstructionTiles = new Uint8Array(passabilityMap.data.length);
/* Generated map legend:
0 is impassable
200 is deep water (ie non-passable by land units)
201 is shallow water (passable by land units and water units)
255 is land (or extremely shallow water where ships can't go).
40 is "tree".
The following 41-49 range is "near a tree", with the second number showing how many trees this tile neighbors.
30 is "geological component", such as a mine
*/
for (var i = 0; i < passabilityMap.data.length; ++i)
{
// If impassable for land units, set to 0, else to 255.
obstructionTiles[i] = (passabilityMap.data[i] & this.obstructionMaskLand) ? 0 : 255;
if (!(passabilityMap.data[i] & this.obstructionMaskWater) && obstructionTiles[i] === 0)
obstructionTiles[i] = 200; // if navigable and not walkable (ie basic water), set to 200.
else if (!(passabilityMap.data[i] & this.obstructionMaskWater) && obstructionTiles[i] === 255)
obstructionTiles[i] = 201; // navigable and walkable.
}
var square = [ [-1,-1], [-1,0], [-1, 1], [0,1], [1,1], [1,0], [1,-1], [0,-1], [0,0] ];
var xx = 0;
var yy = 0;
var value = 0;
var pos = [];
var x = 0;
var y = 0;
var radius = 0;
for (var entI in sharedScript._entities)
{
var ent = sharedScript._entities[entI];
if (ent.hasClass("ForestPlant") === true) {
pos = this.gamePosToMapPos(ent.position());
x = pos[0];
y = pos[1];
// unless it's impassable already, mark it as 40.
if (obstructionTiles[x + y*this.width] !== 0)
obstructionTiles[x + y*this.width] = 40;
for (i in square)
{
xx = square[i][0];
yy = square[i][1];
if (x+i[0] >= 0 && x+xx < this.width && y+yy >= 0 && y+yy < this.height) {
value = obstructionTiles[(x+xx) + (y+yy)*this.width];
if (value === 255)
obstructionTiles[(x+xx) + (y+yy)*this.width] = 41;
else if (value < 49 && value > 40)
obstructionTiles[(x+xx) + (y+yy)*this.width] = value + 1;
}
}
} else if (ent.hasClass("Geology") === true) {
radius = Math.floor(ent.obstructionRadius() / 4);
pos = this.gamePosToMapPos(ent.position());
x = pos[0];
y = pos[1];
// Unless it's impassable, mark as 30. This takes precedence over trees.
obstructionTiles[x + y*this.width] = obstructionTiles[x + y*this.width] === 0 ? 0 : 30;
for (var xx = -radius; xx <= radius;xx++)
for (var yy = -radius; yy <= radius;yy++)
if (x+xx >= 0 && x+xx < this.width && y+yy >= 0 && y+yy < this.height)
obstructionTiles[(x+xx) + (y+yy)*this.width] = obstructionTiles[(x+xx) + (y+yy)*this.width] === 0 ? 0 : 30;
}
}
// Okay now we have a pretty good knowledge of the map.
this.Map(rawState, obstructionTiles);
this.obstructionMaskLand = null;
this.obstructionMaskWater = null;
this.obstructionMask = null;
};
copyPrototype(TerrainAnalysis, Map);
// Returns the (approximately) closest point which is passable by searching in a spiral pattern
TerrainAnalysis.prototype.findClosestPassablePoint = function(startPoint, onLand, limitDistance, quickscope){
var w = this.width;
var p = startPoint;
var direction = 1;
if (p[0] + w*p[1] < 0 || p[0] + w*p[1] >= this.length) {
return undefined;
}
// quickscope
if (this.map[p[0] + w*p[1]] === 255) {
if (this.countConnected(p[0] + w*p[1], onLand) >= 2) {
return p;
}
}
var count = 0;
// search in a spiral pattern. We require a value that is actually accessible in this case, ie 255, 201 or 41 if land, 200/201 if water.
for (var i = 1; i < w; i++){
for (var j = 0; j < 2; j++){
for (var k = 0; k < i; k++){
p[j] += direction;
// if the value is not markedly inaccessible
var index = p[0] + w*p[1];
if (this.map[index] !== 0 && this.map[index] !== 90 && this.map[index] !== 120 && this.map[index] !== 30 && this.map[index] !== 40){
if (quickscope || this.countConnected(index, onLand) >= 2){
return p;
}
}
if (limitDistance !== undefined && count > limitDistance){
return undefined;
}
count++;
}
}
direction *= -1;
}
return undefined;
};
// Returns an estimate of a tile accessibility. It checks neighboring cells over two levels.
// returns a count. It's not integer. About 2 should be fairly accessible already.
TerrainAnalysis.prototype.countConnected = function(startIndex, byLand){
var count = 0.0;
var w = this.width;
var positions = [[0,1], [0,-1], [1,0], [-1,0], [1,1], [-1,-1], [1,-1], [-1,1],
[0,2], [0,-2], [2,0], [-2,0], [2,2], [-2,-2], [2,-2], [-2,2]/*,
[1,2], [1,-2], [2,1], [-2,1], [-1,2], [-1,-2], [2,-1], [-2,-1]*/];
for (i in positions) {
var index = startIndex + positions[i][0] + positions[i][1]*w;
if (this.map[index] !== 0) {
if (byLand) {
if (this.map[index] === 201) count++;
else if (this.map[index] === 255) count++;
else if (this.map[index] === 41) count++;
else if (this.map[index] === 42) count += 0.5;
else if (this.map[index] === 43) count += 0.3;
else if (this.map[index] === 44) count += 0.13;
else if (this.map[index] === 45) count += 0.08;
else if (this.map[index] === 46) count += 0.05;
else if (this.map[index] === 47) count += 0.03;
} else {
if (this.map[index] === 201) count++;
if (this.map[index] === 200) count++;
}
}
}
return count;
};
// TODO: for now this resets to 255.
TerrainAnalysis.prototype.updateMapWithEvents = function(sharedAI) {
var self = this;
var events = sharedAI.events;
var passabilityMap = sharedAI.passabilityMap;
// looking for creation or destruction of entities, and updates the map accordingly.
for (var i in events) {
var e = events[i];
if (e.type === "Destroy") {
if (e.msg.entityObj){
var ent = e.msg.entityObj;
if (ent.hasClass("Geology")) {
var x = self.gamePosToMapPos(ent.position())[0];
var y = self.gamePosToMapPos(ent.position())[1];
// remove it. Don't really care about surrounding and possible overlappings.
var radius = Math.floor(ent.obstructionRadius() / self.cellSize);
for (var xx = -radius; xx <= radius;xx++)
for (var yy = -radius; yy <= radius;yy++)
{
if (x+xx >= 0 && x+xx < self.width && y+yy >= 0 && y+yy < self.height && this.map[(x+xx) + (y+yy)*self.width] === 30)
{
this.map[(x+xx) + (y+yy)*self.width] = 255;
}
}
} else if (ent.hasClass("ForestPlant")){
var x = self.gamePosToMapPos(ent.position())[0];
var y = self.gamePosToMapPos(ent.position())[1];
var nbOfNeigh = 0;
for (var xx = -1; xx <= 1;xx++)
for (var yy = -1; yy <= 1;yy++)
{
if (xx == 0 && yy == 0)
continue;
if (this.map[(x+xx) + (y+yy)*self.width] === 40)
nbOfNeigh++;
else if (this.map[(x+xx) + (y+yy)*self.width] === 41)
{
this.map[(x+xx) + (y+yy)*self.width] = 255;
}
else if (this.map[(x+xx) + (y+yy)*self.width] > 41 && this.map[(x+xx) + (y+yy)*self.width] < 50)
this.map[(x+xx) + (y+yy)*self.width] = this.map[(x+xx) + (y+yy)*self.width] - 1;
}
if (nbOfNeigh > 0)
this.map[x + y*self.width] = this.map[x + y*self.width] = 40 + nbOfNeigh;
else
this.map[x + y*self.width] = this.map[x + y*self.width] = 255;
}
}
}
}
}
/*
* Accessibility inherits from TerrainAnalysis
*
* This can easily and efficiently determine if any two points are connected.
* it can also determine if any point is "probably" reachable, assuming the unit can get close enough
* for optimizations it's called after the TerrainAnalyser has finished initializing his map
* so this can use the land regions already.
*/
function Accessibility(rawState, terrainAnalyser){
var self = this;
this.Map(rawState, terrainAnalyser.map);
this.passMap = new Uint8Array(terrainAnalyser.length);
this.regionSize = [];
this.regionSize.push(0);
// initialized to 0, so start to 1 for optimization
this.regionID = 1;
for (var i = 0; i < this.passMap.length; ++i) {
if (this.passMap[i] === 0 && this.map[i] !== 0) { // any non-painted, non-inacessible area.
this.regionSize.push(0); // updated
this.floodFill(i,this.regionID,false);
this.regionID++;
} else if (this.passMap[i] === 0) { // any non-painted, inacessible area.
this.floodFill(i,1,false);
}
}
}
copyPrototype(Accessibility, TerrainAnalysis);
Accessibility.prototype.getAccessValue = function(position){
var gamePos = this.gamePosToMapPos(position);
return this.passMap[gamePos[0] + this.width*gamePos[1]];
};
// Returns true if a point is deemed currently accessible (is not blocked by surrounding trees...)
// NB: accessible means that you can reach it from one side, not necessariliy that you can go ON it.
Accessibility.prototype.isAccessible = function(gameState, position, onLand){
var gamePos = this.gamePosToMapPos(position);
// quick check
if (this.countConnected(gamePos[0] + this.width*gamePos[1], onLand) >= 2) {
return true;
}
return false;
};
// Return true if you can go from a point to a point without switching means of transport
// Hardcore means is also checks for isAccessible at the end (it checks for either water or land though, beware).
// This is a blind check and not a pathfinder: for all it knows there is a huge block of trees in the middle.
Accessibility.prototype.pathAvailable = function(gameState, start,end, hardcore){
var pstart = this.gamePosToMapPos(start);
var istart = pstart[0] + pstart[1]*this.width;
var pend = this.gamePosToMapPos(end);
var iend = pend[0] + pend[1]*this.width;
if (this.passMap[istart] === this.passMap[iend]) {
if (hardcore && (this.isAccessible(gameState, end,true) || this.isAccessible(gameState, end,false)))
return true;
else if (hardcore)
return false;
return true;
}
return false;
};
Accessibility.prototype.getRegionSize = function(position){
var pos = this.gamePosToMapPos(position);
var index = pos[0] + pos[1]*this.width;
if (this.regionSize[this.passMap[index]] === undefined)
return 0;
return this.regionSize[this.passMap[index]];
};
Accessibility.prototype.getRegionSizei = function(index) {
if (this.regionSize[this.passMap[index]] === undefined)
return 0;
return this.regionSize[this.passMap[index]];
};
// Implementation of a fast flood fill. Reasonably good performances. Runs once at startup.
// TODO: take big zones of impassable trees into account?
Accessibility.prototype.floodFill = function(startIndex, value, onWater)
{
this.s = startIndex;
if (this.passMap[this.s] !== 0) {
return false; // already painted.
}
this.floodFor = "land";
if (this.map[this.s] === 200 || (this.map[this.s] === 201 && onWater === true))
this.floodFor = "water";
else if (this.map[this.s] === 0)
this.floodFor = "impassable";
var w = this.width;
var h = this.height;
var x = 0;
var y = 0;
// Get x and y from index
var IndexArray = [this.s];
var newIndex = 0;
while(IndexArray.length){
newIndex = IndexArray.pop();
y = 0;
var loop = false;
// vertical iteration
do {
--y;
loop = false;
var index = +newIndex + w*y;
if (index < 0)
break;
if (this.floodFor === "impassable" && this.map[index] === 0 && this.passMap[index] === 0) {
loop = true;
} else if (this.floodFor === "land" && this.passMap[index] === 0 && this.map[index] !== 0 && this.map[index] !== 200) {
loop = true;
} else if (this.floodFor === "water" && this.passMap[index] === 0 && (this.map[index] === 200 || (this.map[index] === 201 && this.onWater)) ) {
loop = true;
} else {
break;
}
} while (loop === true) // should actually break
++y;
var reachLeft = false;
var reachRight = false;
loop = true;
do {
var index = +newIndex + w*y;
if (this.floodFor === "impassable" && this.map[index] === 0 && this.passMap[index] === 0) {
this.passMap[index] = value;
this.regionSize[value]++;
} else if (this.floodFor === "land" && this.passMap[index] === 0 && this.map[index] !== 0 && this.map[index] !== 200) {
this.passMap[index] = value;
this.regionSize[value]++;
} else if (this.floodFor === "water" && this.passMap[index] === 0 && (this.map[index] === 200 || (this.map[index] === 201 && this.onWater)) ) {
this.passMap[index] = value;
this.regionSize[value]++;
} else {
break;
}
if (index%w > 0)
{
if (this.floodFor === "impassable" && this.map[index -1] === 0 && this.passMap[index -1] === 0) {
if(!reachLeft) {
IndexArray.push(index -1);
reachLeft = true;
}
} else if (this.floodFor === "land" && this.passMap[index -1] === 0 && this.map[index -1] !== 0 && this.map[index -1] !== 200) {
if(!reachLeft) {
IndexArray.push(index -1);
reachLeft = true;
}
} else if (this.floodFor === "water" && this.passMap[index -1] === 0 && (this.map[index -1] === 200 || (this.map[index -1] === 201 && this.onWater)) ) {
if(!reachLeft) {
IndexArray.push(index -1);
reachLeft = true;
}
} else if(reachLeft) {
reachLeft = false;
}
}
if (index%w < w - 1)
{
if (this.floodFor === "impassable" && this.map[index +1] === 0 && this.passMap[index +1] === 0) {
if(!reachRight) {
IndexArray.push(index +1);
reachRight = true;
}
} else if (this.floodFor === "land" && this.passMap[index +1] === 0 && this.map[index +1] !== 0 && this.map[index +1] !== 200) {
if(!reachRight) {
IndexArray.push(index +1);
reachRight = true;
}
} else if (this.floodFor === "water" && this.passMap[index +1] === 0 && (this.map[index +1] === 200 || (this.map[index +1] === 201 && this.onWater)) ) {
if(!reachRight) {
IndexArray.push(index +1);
reachRight = true;
}
} else if(reachRight) {
reachRight = false;
}
}
++y;
} while (index/w < w) // should actually break
}
return true;
}
function landSizeCounter(rawState,terrainAnalyzer) {
var self = this;
this.passMap = terrainAnalyzer.map;
var map = new Uint8Array(this.passMap.length);
this.Map(rawState,map);
for (var i = 0; i < this.passMap.length; ++i) {
if (this.passMap[i] !== 0)
this.map[i] = 255;
else
this.map[i] = 0;
}
this.expandInfluences();
}
copyPrototype(landSizeCounter, TerrainAnalysis);
// Implementation of A* as a flood fill. Possibility of (clever) oversampling
// for efficiency or for disregarding too small passages.
// can operate over several turns, though default is only one turn.
landSizeCounter.prototype.getAccessibleLandSize = function(position, sampling, mode, OnlyBuildable, sizeLimit, iterationLimit)
{
if (sampling === undefined)
this.Sampling = 1;
else
this.Sampling = sampling < 1 ? 1 : sampling;
// this checks from the actual starting point. If that is inaccessible (0), it returns undefined;
if (position.length !== undefined) {
// this is an array
if (position[0] < 0 || this.gamePosToMapPos(position)[0] >= this.width || position[1] < 0 || this.gamePosToMapPos(position)[1] >= this.height)
return undefined;
var s = this.gamePosToMapPos(position);
this.s = s[0] + w*s[1];
if (this.map[this.s] === 0 || this.map[this.s] === 200 || (OnlyBuildable === true && this.map[this.s] === 201) ) {
return undefined;
}
} else {
this.s = position;
if (this.map[this.s] === 0 || this.map[this.s] === 200 || (OnlyBuildable === true && this.map[this.s] === 201) ) {
return undefined;
}
}
if (mode === undefined)
this.mode = "default";
else
this.mode = mode;
if (sizeLimit === undefined)
this.sizeLimit = 300000;
else
this.sizeLimit = sizeLimit;
var w = this.width;
var h = this.height;
// max map size is 512*512, this is higher.
this.iterationLimit = 300000;
if (iterationLimit !== undefined)
this.iterationLimit = iterationLimit;
this.openList = [];
this.isOpened = new Boolean(this.map.length);
this.gCostArray = new Uint16Array(this.map.length);
this.currentSquare = this.s;
this.isOpened[this.s] = true;
this.openList.push(this.s);
this.gCostArray[this.s] = 0;
this.countedValue = 1;
this.countedArray = [this.s];
if (OnlyBuildable !== undefined)
this.onlyBuildable = OnlyBuildable;
else
this.onlyBuildable = true;
return this.continueLandSizeCalculation();
}
landSizeCounter.prototype.continueLandSizeCalculation = function()
{
var w = this.width;
var h = this.height;
var positions = [[0,1], [0,-1], [1,0], [-1,0], [1,1], [-1,-1], [1,-1], [-1,1]];
var cost = [10,10,10,10,15,15,15,15];
//creation of variables used in the loop
var nouveau = false;
var shortcut = false;
var Sampling = this.Sampling;
var infinity = Math.min();
var currentDist = infinity;
var iteration = 0;
while (this.openList.length !== 0 && iteration < this.iterationLimit && this.countedValue < this.sizeLimit && this.countedArray.length < this.sizeLimit){
currentDist = infinity;
for (i in this.openList)
{
var sum = this.gCostArray[this.openList[i]];
if (sum < currentDist)
{
this.currentSquare = this.openList[i];
currentDist = sum;
}
}
this.openList.splice(this.openList.indexOf(this.currentSquare),1);
shortcut = false;
this.isOpened[this.currentSquare] = false;
for (i in positions) {
var index = 0 + this.currentSquare + positions[i][0]*Sampling + w*Sampling*positions[i][1];
if (this.passMap[index] !== 0 && this.passMap[index] !== 200 && this.map[index] >= Sampling && (!this.onlyBuildable || this.passMap[index] !== 201)) {
if(this.isOpened[index] === undefined) {
if (this.mode === "default")
this.countedValue++;
else if (this.mode === "array")
this.countedArray.push(index);
this.gCostArray[index] = this.gCostArray[this.currentSquare] + cost[i] * Sampling;
this.openList.push(index);
this.isOpened[index] = true;
}
}
}
iteration++;
}
if (iteration === this.iterationLimit && this.openList.length !== 0 && this.countedValue !== this.sizeLimit && this.countedArray.length !== this.sizeLimit)
{
// we've got to assume that we stopped because we reached the upper limit of iterations
return "toBeContinued";
}
delete this.parentSquare;
delete this.isOpened;
delete this.fCostArray;
delete this.gCostArray;
if (this.mode === "default")
return this.countedValue;
else if (this.mode === "array")
return this.countedArray;
return undefined;
}

View File

@ -0,0 +1,77 @@
function VectorDistance(a, b)
{
var dx = a[0] - b[0];
var dz = a[1] - b[1];
return Math.sqrt(dx*dx + dz*dz);
}
function SquareVectorDistance(a, b)
{
var dx = a[0] - b[0];
var dz = a[1] - b[1];
return (dx*dx + dz*dz);
}
// A is the reference, B must be in "range" of A
// this supposes the range is already squared
function inRange(a, b, range)// checks for X distance
{
// will avoid unnecessary checking for position in some rare cases... I'm lazy
if (a === undefined || b === undefined || range === undefined)
return undefined;
var dx = a[0] - b[0];
var dz = a[1] - b[1];
return ((dx*dx + dz*dz ) < range);
}
// slower than SquareVectorDistance, faster than VectorDistance but not exactly accurate.
function ManhattanDistance(a, b)
{
var dx = a[0] - b[0];
var dz = a[1] - b[1];
return Math.abs(dx) + Math.abs(dz);
}
function AssocArraytoArray(assocArray) {
var endArray = [];
for (i in assocArray)
endArray.push(assocArray[i]);
return endArray;
};
function MemoizeInit(obj)
{
obj._memoizeCache = {};
}
function Memoize(funcname, func)
{
return function() {
var args = funcname + '|' + Array.prototype.join.call(arguments, '|');
if (args in this._memoizeCache)
return this._memoizeCache[args];
var ret = func.apply(this, arguments);
this._memoizeCache[args] = ret;
return ret;
};
}
function ShallowClone(obj)
{
var ret = {};
for (var k in obj)
ret[k] = obj[k];
return ret;
}
// Picks a random element from an array
function PickRandom(list){
if (list.length === 0)
{
return undefined;
}
else
{
return list[Math.floor(Math.random()*list.length)];
}
}

View File

@ -0,0 +1,13 @@
Aegis (working name). Experimental AI for 0 A.D. ( http://play0ad.com/ ). An effort to improve over two bots: qBot (by Quantumstate, based on TestBot) and Marilyn (by Wraitii, itself based on qBot).
Install by placing files into the data/mods/public/simulation/ai/qbot-wc folder.
You may want to set "debug : true" in config.js if you are developping, you will get a better understanding of what the AI does. There are also many commented debug outputs, and many commented map outputs that you may want to uncomment.
This bot is not yet default as it was mostly a work in progress for the past few months. It features early technological support, early naval support, better economic management, better defense and better attack management than qBot. It is generally much stronger than the former, and should hopefully be able to handle more situations properly. It is, however, not faultless.
Please report any error to the wildfire games forum ( http://www.wildfiregames.com/forum/index.php?act=idx ), thanks for playing!
Requires common-api-v3.
(note: no saved game support as of yet. A very basic difficulty setting can be found in config.js).

View File

@ -1 +1 @@
Engine.IncludeModule("common-api-v2");
Engine.IncludeModule("common-api-v3");

View File

@ -1,205 +0,0 @@
function AttackMoveToCC(gameState, militaryManager){
this.minAttackSize = 20;
this.maxAttackSize = 60;
this.idList=[];
this.previousTime = 0;
this.state = "unexecuted";
this.healthRecord = [];
};
// Returns true if the attack can be executed at the current time
AttackMoveToCC.prototype.canExecute = function(gameState, militaryManager){
var enemyStrength = militaryManager.measureEnemyStrength(gameState);
var enemyCount = militaryManager.measureEnemyCount(gameState);
// We require our army to be >= this strength
var targetStrength = enemyStrength * 1.5;
var availableCount = militaryManager.countAvailableUnits();
var availableStrength = militaryManager.measureAvailableStrength();
debug("Troops needed for attack: " + this.minAttackSize + " Have: " + availableCount);
debug("Troops strength for attack: " + targetStrength + " Have: " + availableStrength);
return ((availableStrength >= targetStrength && availableCount >= this.minAttackSize)
|| availableCount >= this.maxAttackSize);
};
// Executes the attack plan, after this is executed the update function will be run every turn
AttackMoveToCC.prototype.execute = function(gameState, militaryManager){
var availableCount = militaryManager.countAvailableUnits();
this.idList = militaryManager.getAvailableUnits(availableCount);
var pending = EntityCollectionFromIds(gameState, this.idList);
// Find the critical enemy buildings we could attack
var targets = militaryManager.getEnemyBuildings(gameState,"ConquestCritical");
// If there are no critical structures, attack anything else that's critical
if (targets.length == 0) {
targets = gameState.entities.filter(function(ent) {
return (gameState.isEntityEnemy(ent) && ent.hasClass("ConquestCritical") && ent.owner() !== 0 && ent.position());
});
}
// If there's nothing, attack anything else that's less critical
if (targets.length == 0) {
targets = militaryManager.getEnemyBuildings(gameState,"Town");
}
if (targets.length == 0) {
targets = militaryManager.getEnemyBuildings(gameState,"Village");
}
// If we have a target, move to it
if (targets.length) {
// Add an attack role so the economic manager doesn't try and use them
pending.forEach(function(ent) {
ent.setMetadata("role", "attack");
});
var curPos = pending.getCentrePosition();
var target = targets.toEntityArray()[0];
this.targetPos = target.position();
// Find possible distinct paths to the enemy
var pathFinder = new PathFinder(gameState);
var pathsToEnemy = pathFinder.getPaths(curPos, this.targetPos);
if (! pathsToEnemy){
pathsToEnemy = [this.targetPos];
}
var rand = Math.floor(Math.random() * pathsToEnemy.length);
this.path = pathsToEnemy[rand];
pending.move(this.path[0][0], this.path[0][1]);
} else if (targets.length == 0 ) {
gameState.ai.gameFinished = true;
}
this.state = "walking";
};
// Runs every turn after the attack is executed
// This removes idle units from the attack
AttackMoveToCC.prototype.update = function(gameState, militaryManager, events){
// keep the list of units in good order by pruning ids with no corresponding entities (i.e. dead units)
var removeList = [];
var totalHealth = 0;
for (var idKey in this.idList){
var id = this.idList[idKey];
var ent = militaryManager.entity(id);
if (ent === undefined){
removeList.push(id);
}else{
if (ent.hitpoints()){
totalHealth += ent.hitpoints();
}
}
}
for (var i in removeList){
this.idList.splice(this.idList.indexOf(removeList[i]),1);
}
var units = EntityCollectionFromIds(gameState, this.idList);
if (this.path.length === 0){
var idleCount = 0;
var self = this;
units.forEach(function(ent){
if (ent.isIdle()){
if (ent.position() && VectorDistance(ent.position(), self.targetPos) > 30){
ent.move(self.targetPos[0], self.targetPos[1]);
}else{
militaryManager.unassignUnit(ent.id());
}
}
});
return;
}
var deltaHealth = 0;
var deltaTime = 1;
var time = gameState.getTimeElapsed();
this.healthRecord.push([totalHealth, time]);
if (this.healthRecord.length > 1){
for (var i = this.healthRecord.length - 1; i >= 0; i--){
deltaHealth = totalHealth - this.healthRecord[i][0];
deltaTime = time - this.healthRecord[i][1];
if (this.healthRecord[i][1] < time - 5*1000){
break;
}
}
}
var numUnits = this.idList.length;
if (numUnits < 1) return;
var damageRate = -deltaHealth / deltaTime * 1000;
var centrePos = units.getCentrePosition();
if (! centrePos) return;
var idleCount = 0;
// Looks for idle units away from the formations centre
for (var idKey in this.idList){
var id = this.idList[idKey];
var ent = militaryManager.entity(id);
if (ent.isIdle()){
if (ent.position() && VectorDistance(ent.position(), centrePos) > 20){
var dist = VectorDistance(ent.position(), centrePos);
var vector = [centrePos[0] - ent.position()[0], centrePos[1] - ent.position()[1]];
vector[0] *= 10/dist;
vector[1] *= 10/dist;
ent.move(centrePos[0] + vector[0], centrePos[1] + vector[1]);
}else{
idleCount++;
}
}
}
if ((damageRate / Math.sqrt(numUnits)) > 2){
if (this.state === "walking"){
var sumAttackerPos = [0,0];
var numAttackers = 0;
for (var key in events){
var e = events[key];
//{type:"Attacked", msg:{attacker:736, target:1133, type:"Melee"}}
if (e.type === "Attacked" && e.msg){
if (this.idList.indexOf(e.msg.target) !== -1){
var attacker = militaryManager.entity(e.msg.attacker);
if (attacker && attacker.position()){
sumAttackerPos[0] += attacker.position()[0];
sumAttackerPos[1] += attacker.position()[1];
numAttackers += 1;
}
}
}
}
if (numAttackers > 0){
var avgAttackerPos = [sumAttackerPos[0]/numAttackers, sumAttackerPos[1]/numAttackers];
// Stop moving
units.move(centrePos[0], centrePos[1]);
this.state = "attacking";
}
}
}else{
if (this.state === "attacking"){
units.move(this.path[0][0], this.path[0][1]);
this.state = "walking";
}
}
if (this.state === "walking"){
if (VectorDistance(centrePos, this.path[0]) < 20 || idleCount/numUnits > 0.8){
this.path.shift();
if (this.path.length > 0){
units.move(this.path[0][0], this.path[0][1]);
}
}
}
this.previousTime = time;
this.previousHealth = totalHealth;
};

View File

@ -1,238 +0,0 @@
function AttackMoveToLocation(gameState, militaryManager, minAttackSize, maxAttackSize, targetFinder){
this.minAttackSize = minAttackSize || Config.attack.minAttackSize;
this.maxAttackSize = maxAttackSize || Config.attack.maxAttackSize;
this.idList=[];
this.previousTime = 0;
this.state = "unexecuted";
this.targetFinder = targetFinder || this.defaultTargetFinder;
this.healthRecord = [];
};
// Returns true if the attack can be executed at the current time
AttackMoveToLocation.prototype.canExecute = function(gameState, militaryManager){
var enemyStrength = militaryManager.measureEnemyStrength(gameState);
var enemyCount = militaryManager.measureEnemyCount(gameState);
// We require our army to be >= this strength
var targetStrength = enemyStrength * Config.attack.enemyRatio;
var availableCount = militaryManager.countAvailableUnits();
var availableStrength = militaryManager.measureAvailableStrength();
debug("Troops needed for attack: " + this.minAttackSize + " Have: " + availableCount);
debug("Troops strength for attack: " + targetStrength + " Have: " + availableStrength);
return ((availableStrength >= targetStrength && availableCount >= this.minAttackSize)
|| availableCount >= this.maxAttackSize);
};
// Default target finder aims for conquest critical targets
AttackMoveToLocation.prototype.defaultTargetFinder = function(gameState, militaryManager){
// Find the critical enemy buildings we could attack
var targets = militaryManager.getEnemyBuildings(gameState,"ConquestCritical");
// If there are no critical structures, attack anything else that's critical
if (targets.length == 0) {
targets = gameState.entities.filter(function(ent) {
return (gameState.isEntityEnemy(ent) && ent.hasClass("ConquestCritical") && ent.owner() !== 0 && ent.position());
});
}
// If there's nothing, attack anything else that's less critical
if (targets.length == 0) {
targets = militaryManager.getEnemyBuildings(gameState,"Town");
}
if (targets.length == 0) {
targets = militaryManager.getEnemyBuildings(gameState,"Village");
}
return targets;
};
// Executes the attack plan, after this is executed the update function will be run every turn
AttackMoveToLocation.prototype.execute = function(gameState, militaryManager){
var availableCount = militaryManager.countAvailableUnits();
var numWanted = Math.min(availableCount, this.maxAttackSize);
this.idList = militaryManager.getAvailableUnits(numWanted);
var pending = EntityCollectionFromIds(gameState, this.idList);
var targets = this.targetFinder(gameState, militaryManager);
if (targets.length === 0){
targets = this.defaultTargetFinder(gameState, militaryManager);
}
// If we have a target, move to it
if (targets.length) {
// Add an attack role so the economic manager doesn't try and use them
pending.forEach(function(ent) {
ent.setMetadata("role", "attack");
});
var curPos = pending.getCentrePosition();
// pick a random target from the list
var rand = Math.floor((Math.random()*targets.length));
this.targetPos = undefined;
var count = 0;
while (!this.targetPos){
var target = targets.toEntityArray()[rand];
this.targetPos = target.position();
count++;
if (count > 1000){
warn("No target with a valid position found");
return;
}
}
// Find possible distinct paths to the enemy
var pathFinder = new PathFinder(gameState);
var pathsToEnemy = pathFinder.getPaths(curPos, this.targetPos);
if (!pathsToEnemy || !pathsToEnemy[0] || pathsToEnemy[0][0] === undefined || pathsToEnemy[0][1] === undefined){
pathsToEnemy = [[this.targetPos]];
}
var rand = Math.floor(Math.random() * pathsToEnemy.length);
this.path = pathsToEnemy[rand];
pending.move(this.path[0][0], this.path[0][1]);
} else if (targets.length == 0 ) {
gameState.ai.gameFinished = true;
return;
}
this.state = "walking";
};
// Runs every turn after the attack is executed
// This removes idle units from the attack
AttackMoveToLocation.prototype.update = function(gameState, militaryManager, events){
if (!this.targetPos){
for (var idKey in this.idList){
var id = this.idList[idKey];
militaryManager.unassignUnit(id);
}
this.idList = [];
}
// keep the list of units in good order by pruning ids with no corresponding entities (i.e. dead units)
var removeList = [];
var totalHealth = 0;
for (var idKey in this.idList){
var id = this.idList[idKey];
var ent = militaryManager.entity(id);
if (ent === undefined){
removeList.push(id);
}else{
if (ent.hitpoints()){
totalHealth += ent.hitpoints();
}
}
}
for (var i in removeList){
this.idList.splice(this.idList.indexOf(removeList[i]),1);
}
var units = EntityCollectionFromIds(gameState, this.idList);
if (!this.path || this.path.length === 0){
var idleCount = 0;
var self = this;
units.forEach(function(ent){
if (ent.isIdle()){
if (ent.position() && VectorDistance(ent.position(), self.targetPos) > 30){
ent.move(self.targetPos[0], self.targetPos[1]);
}else{
militaryManager.unassignUnit(ent.id());
}
}
});
return;
}
var deltaHealth = 0;
var deltaTime = 1;
var time = gameState.getTimeElapsed();
this.healthRecord.push([totalHealth, time]);
if (this.healthRecord.length > 1){
for (var i = this.healthRecord.length - 1; i >= 0; i--){
deltaHealth = totalHealth - this.healthRecord[i][0];
deltaTime = time - this.healthRecord[i][1];
if (this.healthRecord[i][1] < time - 5*1000){
break;
}
}
}
var numUnits = this.idList.length;
if (numUnits < 1) return;
var damageRate = -deltaHealth / deltaTime * 1000;
var centrePos = units.getCentrePosition();
if (! centrePos) return;
var idleCount = 0;
// Looks for idle units away from the formations centre
for (var idKey in this.idList){
var id = this.idList[idKey];
var ent = militaryManager.entity(id);
if (ent.isIdle()){
if (ent.position() && VectorDistance(ent.position(), centrePos) > 20){
var dist = VectorDistance(ent.position(), centrePos);
var vector = [centrePos[0] - ent.position()[0], centrePos[1] - ent.position()[1]];
vector[0] *= 10/dist;
vector[1] *= 10/dist;
ent.move(centrePos[0] + vector[0], centrePos[1] + vector[1]);
}else{
idleCount++;
}
}
}
if ((damageRate / Math.sqrt(numUnits)) > 2){
if (this.state === "walking"){
var sumAttackerPos = [0,0];
var numAttackers = 0;
for (var key in events){
var e = events[key];
//{type:"Attacked", msg:{attacker:736, target:1133, type:"Melee"}}
if (e.type === "Attacked" && e.msg){
if (this.idList.indexOf(e.msg.target) !== -1){
var attacker = militaryManager.entity(e.msg.attacker);
if (attacker && attacker.position()){
sumAttackerPos[0] += attacker.position()[0];
sumAttackerPos[1] += attacker.position()[1];
numAttackers += 1;
}
}
}
}
if (numAttackers > 0){
var avgAttackerPos = [sumAttackerPos[0]/numAttackers, sumAttackerPos[1]/numAttackers];
// Stop moving
units.move(centrePos[0], centrePos[1]);
this.state = "attacking";
}
}
}else{
if (this.state === "attacking"){
units.move(this.path[0][0], this.path[0][1]);
this.state = "walking";
}
}
if (this.state === "walking"){
if (VectorDistance(centrePos, this.path[0]) < 20 || idleCount/numUnits > 0.8){
this.path.shift();
if (this.path.length > 0){
units.move(this.path[0][0], this.path[0][1]);
}
}
}
this.previousTime = time;
this.previousHealth = totalHealth;
};

View File

@ -14,7 +14,7 @@ function CityAttack(gameState, militaryManager, uniqueID, targetEnemy, type , ta
gameState.getEntities().forEach(function(ent) { if (gameState.isEntityEnemy(ent) && ent.owner() !== 0) { enemyCount[ent.owner()]++; } });
var max = 0;
for (i in enemyCount)
if (enemyCount[i] >= max)
if (enemyCount[i] > max && +i !== PlayerID)
{
this.targetPlayer = +i;
max = enemyCount[i];
@ -41,29 +41,41 @@ function CityAttack(gameState, militaryManager, uniqueID, targetEnemy, type , ta
// Eg: if all are priority 1, and the siege is 0.5, the siege units will get built
// only once every other category is at least 50% of its target size.
this.unitStat = {};
this.unitStat["RangedInfantry"] = { "priority" : 1, "minSize" : 4, "targetSize" : 10, "batchSize" : 5, "classes" : ["Infantry","Ranged"], "templates" : [] };
this.unitStat["MeleeInfantry"] = { "priority" : 1, "minSize" : 4, "targetSize" : 10, "batchSize" : 5, "classes" : ["Infantry","Melee"], "templates" : [] };
this.unitStat["MeleeCavalry"] = { "priority" : 1, "minSize" : 3, "targetSize" : 8 , "batchSize" : 3, "classes" : ["Cavalry","Melee"], "templates" : [] };
this.unitStat["RangedCavalry"] = { "priority" : 1, "minSize" : 3, "targetSize" : 8 , "batchSize" : 3, "classes" : ["Cavalry","Ranged"], "templates" : [] };
this.unitStat["Siege"] = { "priority" : 0.5, "minSize" : 0, "targetSize" : 3 , "batchSize" : 1, "classes" : ["Siege"], "templates" : [] };
this.unitStat["RangedInfantry"] = { "priority" : 1, "minSize" : 4, "targetSize" : 10, "batchSize" : 5, "classes" : ["Infantry","Ranged"],
"interests" : [ ["strength",2], ["cost",1] ], "templates" : [] };
this.unitStat["MeleeInfantry"] = { "priority" : 1, "minSize" : 4, "targetSize" : 10, "batchSize" : 5, "classes" : ["Infantry","Melee"],
"interests" : [ ["strength",2], ["cost",1] ], "templates" : [] };
this.unitStat["MeleeCavalry"] = { "priority" : 1, "minSize" : 3, "targetSize" : 8 , "batchSize" : 3, "classes" : ["Cavalry","Melee"],
"interests" : [ ["strength",2], ["cost",1] ], "templates" : [] };
this.unitStat["RangedCavalry"] = { "priority" : 1, "minSize" : 3, "targetSize" : 8 , "batchSize" : 3, "classes" : ["Cavalry","Ranged"],
"interests" : [ ["strength",2], ["cost",1] ], "templates" : [] };
this.unitStat["Siege"] = { "priority" : 0.5, "minSize" : 0, "targetSize" : 3 , "batchSize" : 1, "classes" : ["Siege"], "interests" : [ ["siegeStrength", 3], ["cost",1] ], "templates" : [] };
var priority = 60;
if (type === "superSized") {
this.unitStat["RangedInfantry"] = { "priority" : 1, "minSize" : 5, "targetSize" : 18, "batchSize" : 5, "classes" : ["Infantry","Ranged"], "templates" : [] };
this.unitStat["MeleeInfantry"] = { "priority" : 1, "minSize" : 6, "targetSize" : 24, "batchSize" : 5, "classes" : ["Infantry","Melee"], "templates" : [] };
this.unitStat["MeleeCavalry"] = { "priority" : 1, "minSize" : 4, "targetSize" : 12 , "batchSize" : 5, "classes" : ["Cavalry","Melee"], "templates" : [] };
this.unitStat["RangedCavalry"] = { "priority" : 1, "minSize" : 4, "targetSize" : 12 , "batchSize" : 5, "classes" : ["Cavalry","Ranged"], "templates" : [] };
this.unitStat["Siege"] = { "priority" : 0.5, "minSize" : 3, "targetSize" : 6 , "batchSize" : 3, "classes" : ["Siege"], "templates" : [] };
this.unitStat["RangedInfantry"] = { "priority" : 1, "minSize" : 5, "targetSize" : 18, "batchSize" : 5, "classes" : ["Infantry","Ranged"],
"interests" : [ ["strength",2], ["cost",1] ], "templates" : [] };
this.unitStat["MeleeInfantry"] = { "priority" : 1, "minSize" : 6, "targetSize" : 24, "batchSize" : 5, "classes" : ["Infantry","Melee"],
"interests" : [ ["strength",2], ["cost",1] ], "templates" : [] };
this.unitStat["MeleeCavalry"] = { "priority" : 1, "minSize" : 4, "targetSize" : 12 , "batchSize" : 5, "classes" : ["Cavalry","Melee"],
"interests" : [ ["strength",2], ["cost",1] ], "templates" : [] };
this.unitStat["RangedCavalry"] = { "priority" : 1, "minSize" : 4, "targetSize" : 12 , "batchSize" : 5, "classes" : ["Cavalry","Ranged"],
"interests" : [ ["strength",2], ["cost",1] ], "templates" : [] };
this.unitStat["Siege"] = { "priority" : 0.5, "minSize" : 3, "targetSize" : 6 , "batchSize" : 3, "classes" : ["Siege"],"interests" : [ ["siegeStrength",3], ["cost",1] ], "templates" : [] };
this.maxPreparationTime = 450*1000;
priority = 70;
}
gameState.ai.queueManager.addQueue("plan_" + this.name, priority);
this.queue = gameState.ai.queues["plan_" + this.name];
/*
this.unitStat["Siege"]["filter"] = function (ent) {
var strength = [ent.attackStrengths("Melee")["crush"],ent.attackStrengths("Ranged")["crush"]];
return (strength[0] > 15 || strength[1] > 15);
};*/
var filter = Filters.and(Filters.byMetadata("plan",this.name),Filters.byOwner(gameState.player));
var filter = Filters.and(Filters.byMetadata(PlayerID, "plan",this.name),Filters.byOwner(PlayerID));
this.unitCollection = gameState.getOwnEntities().filter(filter);
this.unitCollection.registerUpdates();
this.unitCollection.length;
@ -79,7 +91,7 @@ function CityAttack(gameState, militaryManager, uniqueID, targetEnemy, type , ta
var cat = unitCat;
var Unit = this.unitStat[cat];
filter = Filters.and(Filters.byClassesAnd(Unit["classes"]),Filters.and(Filters.byMetadata("plan",this.name),Filters.byOwner(gameState.player)));
filter = Filters.and(Filters.byClassesAnd(Unit["classes"]),Filters.and(Filters.byMetadata(PlayerID, "plan",this.name),Filters.byOwner(PlayerID)));
this.unit[cat] = gameState.getOwnEntities().filter(filter);
this.unit[cat].registerUpdates();
this.unit[cat].length;
@ -160,13 +172,18 @@ function CityAttack(gameState, militaryManager, uniqueID, targetEnemy, type , ta
this.threatList = []; // sounds so FBI
this.tactics = undefined;
gameState.ai.queueManager.addQueue("plan_" + this.name, 100); // high priority: some may gather anyway
this.queue = gameState.ai.queues["plan_" + this.name];
this.assignUnits(gameState);
//debug ("Before");
//Engine.DumpHeap();
// get a good path to an estimated target.
this.pathFinder = new aStarPath(gameState,false);
this.onBoat = false; // tells us if our units are loaded on boats.
this.needsShip = false;
//debug ("after");
//Engine.DumpHeap();
};
CityAttack.prototype.getName = function(){
@ -235,6 +252,7 @@ CityAttack.prototype.updatePreparation = function(gameState, militaryManager,eve
targets = this.defaultTargetFinder(gameState, militaryManager);
}
if (targets.length) {
// picking a target
var rand = Math.floor((Math.random()*targets.length));
this.targetPos = undefined;
var count = 0;
@ -247,11 +265,36 @@ CityAttack.prototype.updatePreparation = function(gameState, militaryManager,eve
return false;
}
}
this.path = this.pathFinder.getPath(this.rallyPoint,this.targetPos, false, 2);
if (this.path === undefined || this.path[1] === true) {
return 3;
// when we have a target, we path to it.
this.path = this.pathFinder.getPath(this.rallyPoint,this.targetPos, 2, 2);//,300000,gameState);
if (this.path === undefined) {
delete this.pathFinder;
return 3; // no path.
} else if (this.path[1] === true) {
// okay so we need a ship.
// Basically we'll add it as a new class to train compulsorily, and we'll recompute our path.
debug ("We need a ship.");
if (!gameState.ai.waterMap)
{
debug ("This is actually a water map.");
gameState.ai.waterMap = true;
}
this.unitStat["TransportShip"] = { "priority" : 1.1, "minSize" : 2, "targetSize" : 2, "batchSize" : 1, "classes" : ["Warship"], "templates" : [] };
if (type === "superSized") {
this.unitStat["TransportShip"]["minSize"] = 4;
this.unitStat["TransportShip"]["targetSize"] = 4;
}
var Unit = this.unitStat["TransportShip"];
var filter = Filters.and(Filters.byClassesAnd(Unit["classes"]),Filters.and(Filters.byMetadata(PlayerID, "plan",this.name),Filters.byOwner(PlayerID)));
this.unit["TransportShip"] = gameState.getOwnEntities().filter(filter);
this.unit["TransportShip"].registerUpdates();
this.unit["TransportShip"].length;
this.buildOrder.push([0, Unit["classes"], this.unit["TransportShip"], Unit, "TransportShip"]);
this.needsShip = true;
}
this.path = this.path[0];
this.path = this.path[0].reverse();
delete this.pathFinder;
} else if (targets.length == 0 ) {
gameState.ai.gameFinished = true;
debug ("I do not have any target. So I'll just assume I won the game.");
@ -262,11 +305,10 @@ CityAttack.prototype.updatePreparation = function(gameState, militaryManager,eve
Engine.ProfileStart("Update Preparation");
// keep on while the units finish being trained.
if (this.mustStart(gameState) && gameState.countOwnQueuedEntitiesWithMetadata("plan", +this.name) ) {
if (this.mustStart(gameState) && (gameState.countOwnQueuedEntitiesWithMetadata("plan", +this.name) + this.queue.countTotalQueuedUnits()) > 0 ) {
this.assignUnits(gameState);
if ( (gameState.ai.turn + gameState.ai.player) % 40 == 0) {
this.AllToRallyPoint(gameState, true); // gain some time, start regrouping
this.unitCollection.forEach(function (entity) { entity.setMetadata("role","attack"); });
}
Engine.ProfileStop();
return 1;
@ -307,19 +349,21 @@ CityAttack.prototype.updatePreparation = function(gameState, militaryManager,eve
var inTraining = gameState.countOwnQueuedEntitiesWithMetadata("special",specialData);
if (this.queue.countTotalQueuedUnits() + inTraining + this.buildOrder[0][2].length < Math.min(15,this.buildOrder[0][3]["targetSize"]) ) {
if (this.buildOrder[0][0] < 1 && this.queue.length() < 4) {
var template = militaryManager.findBestTrainableUnit(gameState, this.buildOrder[0][1], [ ["strength",1], ["cost",1] ] );
var template = militaryManager.findBestTrainableUnit(gameState, this.buildOrder[0][1], this.buildOrder[0][3]["interests"] );
//debug ("tried " + uneval(this.buildOrder[0][1]) +", and " + template);
// HACK (TODO replace) : if we have no trainable template... Then we'll simply remove the buildOrder, effectively removing the unit from the plan.
if (template === undefined) {
delete this.unitStat[this.buildOrder[0][4]]; // deleting the associated unitstat.
this.buildOrder.splice(0,1);
} else {
var max = this.buildOrder[0][3]["batchSize"];
if (this.type === "superSized")
max *= 2;
if (gameState.getTemplate(template).hasClasses(["CitizenSoldier", "Infantry"]))
this.queue.addItem( new UnitTrainingPlan(gameState,template, { "role" : "worker", "plan" : this.name, "special" : specialData },this.buildOrder[0][3]["batchSize"] ) );
this.queue.addItem( new UnitTrainingPlan(gameState,template, { "role" : "worker", "plan" : this.name, "special" : specialData }, this.buildOrder[0][3]["batchSize"],max ) );
else
this.queue.addItem( new UnitTrainingPlan(gameState,template, { "role" : "attack", "plan" : this.name, "special" : specialData },this.buildOrder[0][3]["batchSize"] ) );
this.queue.addItem( new UnitTrainingPlan(gameState,template, { "role" : "attack", "plan" : this.name, "special" : specialData }, this.buildOrder[0][3]["batchSize"],max ) );
}
}
}
@ -366,6 +410,8 @@ CityAttack.prototype.updatePreparation = function(gameState, militaryManager,eve
Engine.ProfileStop();
return 1;
}
this.unitCollection.forEach(function (entity) { entity.setMetadata(PlayerID, "role","attack"); });
Engine.ProfileStop();
// if we're here, it means we must start (and have no units in training left).
// if we can, do, else, abort.
@ -383,10 +429,10 @@ CityAttack.prototype.assignUnits = function(gameState){
var NoRole = gameState.getOwnEntitiesByRole(undefined);
NoRole.forEach(function(ent) {
if (ent.hasClasses(["CitizenSoldier", "Infantry"]))
ent.setMetadata("role", "worker");
ent.setMetadata(PlayerID, "role", "worker");
else
ent.setMetadata("role", "attack");
ent.setMetadata("plan", self.name);
ent.setMetadata(PlayerID, "role", "attack");
ent.setMetadata(PlayerID, "plan", self.name);
});
};
@ -397,16 +443,20 @@ CityAttack.prototype.ToRallyPoint = function(gameState,id)
gameState.getEntityById(id).move(this.rallyPoint[0],this.rallyPoint[1]);
}
// this sends all units back to the "rally point" by entity collections.
// It doesn't disturb ones that could be currently defending, even if the plan is not (yet) paused.
CityAttack.prototype.AllToRallyPoint = function(gameState, evenWorkers) {
var self = this;
if (evenWorkers) {
for (unitCat in this.unit) {
this.unit[unitCat].move(this.rallyPoint[0],this.rallyPoint[1]);
this.unit[unitCat].forEach(function (ent) {
if (ent.getMetadata(PlayerID, "role") != "defence" && !ent.hasClass("Warship"))
ent.move(self.rallyPoint[0],self.rallyPoint[1]);
});
}
} else {
for (unitCat in this.unit) {
this.unit[unitCat].forEach(function (ent) {
if (ent.getMetadata("role") != "worker")
if (ent.getMetadata(PlayerID, "role") != "worker" && ent.getMetadata(PlayerID, "role") != "defence" && !ent.hasClass("Warship"))
ent.move(self.rallyPoint[0],self.rallyPoint[1]);
});
}
@ -417,21 +467,22 @@ CityAttack.prototype.AllToRallyPoint = function(gameState, evenWorkers) {
CityAttack.prototype.defaultTargetFinder = function(gameState, militaryManager){
var targets = undefined;
targets = militaryManager.enemyWatchers[this.targetPlayer].getEnemyBuildings("CivCentre");
targets = militaryManager.enemyWatchers[this.targetPlayer].getEnemyBuildings(gameState, "CivCentre",true);
if (targets.length == 0) {
targets = militaryManager.enemyWatchers[this.targetPlayer].getEnemyBuildings("ConquestCritical");
targets = militaryManager.enemyWatchers[this.targetPlayer].getEnemyBuildings(gameState, "ConquestCritical");
}
// If there's nothing, attack anything else that's less critical
if (targets.length == 0) {
targets = militaryManager.enemyWatchers[this.targetPlayer].getEnemyBuildings("Town");
targets = militaryManager.enemyWatchers[this.targetPlayer].getEnemyBuildings(gameState, "Town",true);
}
if (targets.length == 0) {
targets = militaryManager.enemyWatchers[this.targetPlayer].getEnemyBuildings("Village");
targets = militaryManager.enemyWatchers[this.targetPlayer].getEnemyBuildings(gameState, "Village",true);
}
// no buildings, attack anything conquest critical, even units (it's assuming it won't move).
if (targets.length == 0) {
targets = gameState.getEnemyEntities().filter(Filters.byClass("ConquestCritical"));
}
debug ("target is " + targets);
return targets;
};
@ -467,17 +518,16 @@ CityAttack.prototype.raidingTargetFinder = function(gameState, militaryManager,
CityAttack.prototype.StartAttack = function(gameState, militaryManager){
// check we have a target and a path.
if (this.targetPos && this.path !== undefined) {
// erase our queue. This will stop any leftover unit from being trained.
gameState.ai.queueManager.removeQueue("plan_" + this.name);
var curPos = this.unitCollection.getCentrePosition();
this.unitCollection.forEach(function(ent) { ent.setMetadata("subrole", "attacking"); ent.setMetadata("role", "attack") ;});
this.unitCollection.forEach(function(ent) { ent.setMetadata(PlayerID, "subrole", "attacking"); ent.setMetadata(PlayerID, "role", "attack") ;});
// filtering by those that started to attack only
var filter = Filters.byMetadata("subrole","attacking");
var filter = Filters.byMetadata(PlayerID, "subrole","attacking");
this.unitCollection = this.unitCollection.filter(filter);
this.unitCollection.registerUpdates();
//this.unitCollection.length;
@ -487,17 +537,14 @@ CityAttack.prototype.StartAttack = function(gameState, militaryManager){
this.unit[cat] = this.unit[cat].filter(filter);
}
this.unitCollection.move(this.path[0][0], this.path[0][1]);
this.unitCollection.move(this.path[0][0][0], this.path[0][0][1]);
this.unitCollection.setStance("aggressive"); // make sure units won't disperse out of control
delete this.pathFinder;
debug ("Started to attack with the plan " + this.name);
this.state = "walking";
} else {
gameState.ai.gameFinished = true;
debug ("I do not have any target. So I'll just assume I won the game.");
delete this.pathFinder;
return true;
}
return true;
@ -513,10 +560,6 @@ CityAttack.prototype.update = function(gameState, militaryManager, events){
var bool_attacked = false;
// raids don't care about attacks much
// we're over, abort immediately.
if (this.unitCollection.length === 0)
return 0;
this.position = this.unitCollection.getCentrePosition();
var IDs = this.unitCollection.toIdArray();
@ -535,7 +578,7 @@ CityAttack.prototype.update = function(gameState, militaryManager, events){
var attacker = gameState.getEntityById(e.msg.attacker);
var ourUnit = gameState.getEntityById(e.msg.target);
if (attacker && attacker.position() && attacker.hasClass("Unit") && attacker.owner() != 0 && attacker.owner() != gameState.player) {
if (attacker && attacker.position() && attacker.hasClass("Unit") && attacker.owner() != 0 && attacker.owner() != PlayerID) {
var territoryMap = Map.createTerritoryMap(gameState);
if ( +territoryMap.point(attacker.position()) - 64 === +this.targetPlayer)
@ -627,13 +670,144 @@ CityAttack.prototype.update = function(gameState, militaryManager, events){
}*/
}
if (this.state === "walking"){
this.position = this.unitCollection.filter(Filters.not(Filters.byClass("Warship"))).getCentrePosition();
if (SquareVectorDistance(this.position, this.lastPosition) < 20 && this.path.length > 0) {
this.unitCollection.move(this.path[0][0], this.path[0][1]);
this.unitCollection.filter(Filters.not(Filters.byClass("Warship"))).moveIndiv(this.path[0][0][0], this.path[0][0][1]);
}
if (SquareVectorDistance(this.unitCollection.getCentrePosition(), this.path[0]) < 900){
if (SquareVectorDistance(this.unitCollection.filter(Filters.not(Filters.byClass("Warship"))).getCentrePosition(), this.path[0][0]) < 600) {
// okay so here basically two cases. The first one is "we need a boat at this point".
// the second one is "we need to unload at this point". The third is "normal".
if (this.path[0][1] !== true)
{
this.path.shift();
if (this.path.length > 0){
this.unitCollection.filter(Filters.not(Filters.byClass("Warship"))).moveIndiv(this.path[0][0][0], this.path[0][0][1]);
} else {
debug ("Attack Plan " +this.type +" " +this.name +" has arrived to destination.");
// we must assume we've arrived at the end of the trail.
this.state = "arrived";
}
} else if (this.path[0][1] === true)
{
// okay we must load our units.
// check if we have some kind of ships.
var ships = this.unitCollection.filter(Filters.byClass("Warship"));
if (ships.length === 0)
return 0; // abort
debug ("switch to boarding");
this.state = "boarding";
}
}
} else if (this.state === "shipping") {
this.position = this.unitCollection.filter(Filters.byClass("Warship")).getCentrePosition();
if (SquareVectorDistance(this.position, this.lastPosition) < 20 && this.path.length > 0) {
this.unitCollection.filter(Filters.byClass("Warship")).moveIndiv(this.path[0][0][0], this.path[0][0][1]);
}
if (SquareVectorDistance(this.position, this.path[0][0]) < 1600) {
if (this.path[0][1] !== true)
{
this.path.shift();
if (this.path.length > 0){
this.unitCollection.filter(Filters.byClass("Warship")).moveIndiv(this.path[0][0][0], this.path[0][0][1]);
} else {
debug ("Attack Plan " +this.type +" " +this.name +" has arrived to destination, but it's still on the ship…");
return 0; // abort
}
} else if (this.path[0][1] === true)
{
debug ("switch to unboarding");
// we unload
this.state = "unboarding";
}
}
} else if (this.state === "boarding") {
this.position = this.unitCollection.filter(Filters.not(Filters.byClass("Warship"))).getCentrePosition();
var ships = this.unitCollection.filter(Filters.byClass("Warship"));
if (ships.length === 0)
return 0; // abort
var globalPos = this.unitCollection.filter(Filters.not(Filters.byClass("Warship"))).getCentrePosition();
var shipPos = ships.getCentrePosition();
if (globalPos !== undefined && SquareVectorDistance(globalPos,shipPos) > 800)
{ // get them closer
ships.moveIndiv(globalPos[0],globalPos[1]);
this.unitCollection.filter(Filters.not(Filters.byClass("Warship"))).moveIndiv(shipPos[0],shipPos[1]);
} else {
// okay try to garrison.
var shipsArray = ships.toEntityArray();
this.unitCollection.filter(Filters.not(Filters.byClass("Warship"))).forEach(function (ent) { //}){
if (ent.position()) // if we're not garrisoned
for (var shipId = 0; shipId < shipsArray.length; shipId++) {
if (shipsArray[shipId].garrisoned().length < shipsArray[shipId].garrisonMax())
{
ent.garrison(shipsArray[shipId]);
break;
}
}
});
var garrLength = 0;
for (var shipId = 0; shipId < shipsArray.length; shipId++)
garrLength += shipsArray[shipId].garrisoned().length;
if (garrLength == this.unitCollection.filter(Filters.not(Filters.byClass("Warship"))).length) {
// okay.
this.path.shift();
if (this.path.length > 0){
ships.moveIndiv(this.path[0][0][0], this.path[0][0][1]);
debug ("switch to shipping");
this.state = "shipping";
} else {
debug ("Attack Plan " +this.type +" " +this.name +" has arrived to destination.");
// we must assume we've arrived at the end of the trail.
this.state = "arrived";
}
}
}
} else if (this.state === "unboarding") {
var ships = this.unitCollection.filter(Filters.byClass("Warship"));
if (ships.length === 0)
return 0; // abort
this.position = ships.getCentrePosition();
// the procedure is pretty simple: we move the ships to the next point and try to unload until all units are over.
// TODO: make it better, like avoiding collisions, and so on.
if (this.path.length > 1)
ships.moveIndiv(this.path[1][0][0], this.path[1][0][1]);
ships.forEach(function (ship) {
ship.unloadAll();
});
var shipsArray = ships.toEntityArray();
var garrLength = 0;
for (var shipId = 0; shipId < shipsArray.length; shipId++)
garrLength += shipsArray[shipId].garrisoned().length;
if (garrLength == 0) {
// release the ships
ships.forEach(function (ent) {
ent.setMetadata(PlayerID, "role",undefined);
ent.setMetadata(PlayerID, "subrole",undefined);
ent.setMetadata(PlayerID, "plan",undefined);
});
for (var shipId = 0; shipId < shipsArray.length; shipId++)
this.unitCollection.removeEnt(shipsArray[shipId]);
this.path.shift();
if (this.path.length > 0){
this.unitCollection.move(this.path[0][0], this.path[0][1]);
this.unitCollection.moveIndiv(this.path[0][0][0], this.path[0][0][1]);
debug ("switch to walking");
this.state = "walking";
} else {
debug ("Attack Plan " +this.type +" " +this.name +" has arrived to destination.");
// we must assume we've arrived at the end of the trail.
@ -641,6 +815,8 @@ CityAttack.prototype.update = function(gameState, militaryManager, events){
}
}
}
// todo: re-implement raiding
if (this.state === "arrived"){
// let's proceed on with whatever happens now.
@ -681,7 +857,7 @@ CityAttack.prototype.update = function(gameState, militaryManager, events){
if (!enemy.position()) {
return false;
}
if (SquareVectorDistance(enemy.position(),ent.position()) > ent.visionRange()*ent.visionRange() + 100) {
if (SquareVectorDistance(enemy.position(),ent.position()) > ent.visionRange()*ent.visionRange() + 300) {
return false;
}
return true;
@ -690,16 +866,22 @@ CityAttack.prototype.update = function(gameState, militaryManager, events){
if (!enemy.position()) {
return false;
}
if (SquareVectorDistance(enemy.position(),ent.position()) > ent.visionRange()*ent.visionRange() + 100) {
if (SquareVectorDistance(enemy.position(),ent.position()) > ent.visionRange()*ent.visionRange() + 300) {
return false;
}
return true;
});
mUnit = mUnit.toEntityArray();
mStruct = mStruct.toEntityArray();
mStruct.sort(function (struct) {
if (struct.hasClass("ConquestCritical"))
return 100 + struct.costSum();
else
return struct.costSum();
})
if (ent.hasClass("Siege")) {
if (mStruct.length !== 0) {
var rand = Math.floor(Math.random() * mStruct.length*0.99);
var rand = Math.floor(Math.random() * mStruct.length*0.2);
ent.attack(mStruct[+rand].id());
//debug ("Siege units attacking a structure from " +mStruct[+rand].owner() + " , " +mStruct[+rand].templateName());
} else if (SquareVectorDistance(self.targetPos, ent.position()) > 900 ){
@ -755,9 +937,9 @@ CityAttack.prototype.totalCountUnits = function(gameState){
// reset any units
CityAttack.prototype.Abort = function(gameState){
this.unitCollection.forEach(function(ent) {
ent.setMetadata("role",undefined);
ent.setMetadata("subrole",undefined);
ent.setMetadata("plan",undefined);
ent.setMetadata(PlayerID, "role",undefined);
ent.setMetadata(PlayerID, "subrole",undefined);
ent.setMetadata(PlayerID, "plan",undefined);
});
for (unitCat in this.unitStat) {
delete this.unitStat[unitCat];

View File

@ -1,19 +1,30 @@
// Baseconfig is the highest difficulty.
var baseConfig = {
"attack" : {
"minAttackSize" : 20, // attackMoveToLocation
"maxAttackSize" : 60, // attackMoveToLocation
"enemyRatio" : 1.5, // attackMoveToLocation
"groupSize" : 10 // military
"Military" : {
"fortressStartTime" : 840, // Time to wait before building one fortress.
"fortressLapseTime" : 300, // Time to wait between building 2 fortresses (minimal)
"defenceBuildingTime" : 300, // Time to wait before building towers or fortresses
"advancedMilitaryStartTime" : 720, // Time to wait before building advanced military buildings. Also limited by phase 2.
"attackPlansStartTime" : 0 // time to wait before attacking. Start as soon as possible (first barracks)
},
"Economy" : {
"townPhase" : 180, // time to start trying to reach town phase (might be a while after. Still need the requirements + ress )
"cityPhase" : 540, // time to start trying to reach city phase
"farmsteadStartTime" : 240, // Time to wait before building a farmstead.
"marketStartTime" : 620, // Time to wait before building the market.
"dockStartTime" : 240, // Time to wait before building the dock
"techStartTime" : 600, // time to wait before teching.
"targetNumBuilders" : 1.5, // Base number of builders per foundation. Later updated, but this remains a multiplier.
"femaleRatio" : 0.4 // percent of females among the workforce.
},
// Note: attack settings are set directly in attack_plan.js
// defence
"defence" : {
"acquireDistance" : 220,
"releaseDistance" : 250,
"groupRadius" : 20,
"groupBreakRadius" : 40,
"groupMergeRadius" : 10,
"defenderRatio" : 2
"Defence" : {
"defenceRatio" : 3, // see defence.js for more info.
"armyCompactSize" : 700, // squared. Half-diameter of an army.
"armyBreakawaySize" : 900 // squared.
},
// military
@ -42,23 +53,75 @@ var baseConfig = {
// qbot
"priorities" : { // Note these are dynamic, you are only setting the initial values
"house" : 500,
"citizenSoldier" : 65,
"villager" : 95,
"economicBuilding" : 95,
"field" : 20,
"advancedSoldier" : 30,
"siege" : 10,
"militaryBuilding" : 90,
"house" : 250,
"citizenSoldier" : 50,
"villager" : 60,
"economicBuilding" : 80,
"dropsites" : 180,
"field" : 500,
"militaryBuilding" : 120,
"defenceBuilding" : 17,
"civilCentre" : 1000
"majorTech" : 100,
"minorTech" : 40,
"civilCentre" : 10000 // will hog all resources
},
"difficulty" : 2, // for now 2 is "hard", ie default. 1 is normal, 0 is easy.
"debug" : false
};
var Config = {
"debug": false
"debug": true,
"difficulty" : 2
};
Config.__proto__ = baseConfig;
Config.__proto__ = baseConfig;
// changing settings based on difficulty.
if (Config.difficulty === 1)
{
Config["Military"] = {
"fortressStartTime" : 1000,
"fortressLapseTime" : 400,
"defenceBuildingTime" : 350,
"advancedMilitaryStartTime" : 1000,
"attackPlansStartTime" : 600
};
Config["Economy"] = {
"townPhase" : 240,
"cityPhase" : 660,
"farmsteadStartTime" : 600,
"marketStartTime" : 800,
"techStartTime" : 1320,
"targetNumBuilders" : 2,
"femaleRatio" : 0.5
};
Config["Defence"] = {
"defenceRatio" : 2.0,
"armyCompactSize" : 700,
"armyBreakawaySize" : 900
};
} else if (Config.difficulty === 0)
{
Config["Military"] = {
"fortressStartTime" : 1500,
"fortressLapseTime" : 1000000,
"defenceBuildingTime" : 500,
"advancedMilitaryStartTime" : 1300,
"attackPlansStartTime" : 1200 // 20 minutes ought to give enough times for beginners
};
Config["Economy"] = {
"townPhase" : 360,
"cityPhase" : 840,
"farmsteadStartTime" : 1200,
"marketStartTime" : 1000,
"techStartTime" : 600000, // never
"targetNumBuilders" : 1,
"femaleRatio" : 0.0
};
Config["Defence"] = {
"defenceRatio" : 1.0,
"armyCompactSize" : 700,
"armyBreakawaySize" : 900
};
}

View File

@ -1,5 +1,6 @@
{
"name": "Aegis Bot",
"description": "Improvements over qBot by Wraitii. Still experimental(bugs possible).\nThis bot may be harder to defeat than the regular qBot. Please report any problems on the Wildfire forums.",
"constructor": "QBotAI"
"description": "An WIP AI improving upon qBot.\nThis bot is stronger than qBot, use the former if you're a beginner.\nPlease report any problems on the Wildfire forums.",
"constructor": "QBotAI",
"useShared": true
}

View File

@ -1,18 +1,31 @@
// directly imported from Marilyn, with slight modifications to work with qBot.
function Defence(){
this.defenceRatio = 1.8; // How many defenders we want per attacker. Need to balance fewer losses vs. lost economy
// note: the choice should be a no-brainer most of the time: better deflect the attack.
this.defenceRatio = Config.Defence.defenceRatio;// How many defenders we want per attacker. Need to balance fewer losses vs. lost economy
// note: the choice should be a no-brainer most of the time: better deflect the attack.
// This is also sometimes forcebly overcome by the defense manager.
this.armyCompactSize = Config.Defence.armyCompactSize; // a bit more than 40 wide in diameter
this.armyBreakawaySize = Config.Defence.armyBreakawaySize; // a bit more than 45 wide in diameter
this.totalAttackNb = 0; // used for attack IDs
this.attacks = [];
this.toKill = [];
// keeps a list of targeted enemy at instant T
this.enemyArmy = {}; // array of players, storing for each an array of armies.
this.attackerCache = {};
this.listOfEnemies = {};
this.listedEnemyCollection = null; // entity collection of this.listOfEnemies
// Some Stats
this.nbAttackers = 0;
this.nbDefenders = 0;
// Caching variables
this.totalArmyNB = 0;
this.enemyUnits = {};
this.enemyArmyLoop = {};
// boolean 0/1 that's for optimization
this.attackerCacheLoopIndicator = 0;
@ -38,56 +51,67 @@ function Defence(){
Defence.prototype.update = function(gameState, events, militaryManager){
Engine.ProfileStart("Defence Manager");
// a litlle cache-ing
if (!this.idleDefs) {
var filter = Filters.and(Filters.byMetadata("role", "defence"), Filters.isIdle());
var filter = Filters.and(Filters.byMetadata(PlayerID, "role", "defence"), Filters.isIdle());
this.idleDefs = gameState.getOwnEntities().filter(filter);
this.idleDefs.registerUpdates();
}
if (!this.defenders) {
var filter = Filters.byMetadata("role", "defence");
var filter = Filters.byMetadata(PlayerID, "role", "defence");
this.defenders = gameState.getOwnEntities().filter(filter);
this.defenders.registerUpdates();
}
if (!this.listedEnemyCollection) {
var filter = Filters.byMetadata("listed-enemy", true);
/*if (!this.listedEnemyCollection) {
var filter = Filters.byMetadata(PlayerID, "listed-enemy", true);
this.listedEnemyCollection = gameState.getEnemyEntities().filter(filter);
this.listedEnemyCollection.registerUpdates();
}
this.myBuildings = gameState.getOwnEntities().filter(Filters.byClass("Structure")).toEntityArray();
this.myUnits = gameState.getOwnEntities().filter(Filters.byClass("Unit"));
*/
var filter = Filters.and(Filters.byClassesOr(["CitizenSoldier", "Hero", "Champion", "Siege"]), Filters.byOwner(PlayerID));
this.myUnits = gameState.updatingGlobalCollection("player-" +PlayerID + "-soldiers", filter);
filter = Filters.and(Filters.byClass("Structure"), Filters.byOwner(PlayerID));
this.myBuildings = gameState.updatingGlobalCollection("player-" +PlayerID + "-structures", filter);
this.territoryMap = Map.createTerritoryMap(gameState); // used by many func
// First step: we deal with enemy armies, those are the highest priority.
this.defendFromEnemyArmies(gameState, events, militaryManager);
this.defendFromEnemies(gameState, events, militaryManager);
// second step: we loop through messages, and sort things as needed (dangerous buildings, attack by animals, ships, lone units, whatever).
// TODO
// TODO : a lot.
this.MessageProcess(gameState,events,militaryManager);
this.DealWithWantedUnits(gameState,events,militaryManager);
var self = this;
// putting unneeded units at rest
this.idleDefs.forEach(function(ent) {
if (ent.getMetadata("formerrole"))
ent.setMetadata("role", ent.getMetadata("formerrole") );
if (ent.getMetadata(PlayerID, "formerrole"))
ent.setMetadata(PlayerID, "role", ent.getMetadata(PlayerID, "formerrole") );
else
ent.setMetadata("role", "worker");
ent.setMetadata("subrole", undefined);
ent.setMetadata(PlayerID, "role", "worker");
ent.setMetadata(PlayerID, "subrole", undefined);
self.nbDefenders--;
});
Engine.ProfileStop();
return;
};
/*
// returns armies that are still seen as dangerous (in the LOS of any of my buildings for now)
Defence.prototype.reevaluateDangerousArmies = function(gameState, armies) {
var stillDangerousArmies = {};
for (i in armies) {
var pos = armies[i].getCentrePosition();
if (armies[i].getCentrePosition() && +this.territoryMap.point(armies[i].getCentrePosition()) - 64 === +gameState.player) {
if (pos === undefined)
if (+this.territoryMap.point(pos) - 64 === +PlayerID) {
stillDangerousArmies[i] = armies[i];
continue;
}
@ -105,118 +129,203 @@ Defence.prototype.reevaluateDangerousArmies = function(gameState, armies) {
Defence.prototype.evaluateArmies = function(gameState, armies) {
var DangerousArmies = {};
for (i in armies) {
if (armies[i].getCentrePosition() && +this.territoryMap.point(armies[i].getCentrePosition()) - 64 === +gameState.player) {
if (armies[i].getCentrePosition() && +this.territoryMap.point(armies[i].getCentrePosition()) - 64 === +PlayerID) {
DangerousArmies[i] = armies[i];
}
}
return DangerousArmies;
}
// This deals with incoming enemy armies, setting the defcon if needed. It will take new soldiers, and assign them to attack
// it's still a fair share of dumb, so TODO improve
Defence.prototype.defendFromEnemyArmies = function(gameState, events, militaryManager) {
// The enemy Watchers keep a list of armies. This class here tells them if an army is dangerous, and they manage the merging/splitting/disbanding.
// With this system, we can get any dangerous armies. Thus, we can know where the danger is, and react.
// So Defence deals with attacks from animals too (which aren't watched).
// The attackrs here are dealt with on a per unit basis.
// We keep a list of idle defenders. For any new attacker, we'll check if we have any idle defender available, and if not, we assign available units.
// At the end of each turn, if we still have idle defenders, we either assign them to neighboring units, or we release them.
var dangerArmies = {};
this.enemyUnits = {};
// for now armies are never seen as "no longer dangerous"... TODO
for (enemyID in militaryManager.enemyWatchers) {
this.enemyUnits[enemyID] = militaryManager.enemyWatchers[enemyID].getAllEnemySoldiers();
var dangerousArmies = militaryManager.enemyWatchers[enemyID].getDangerousArmies();
// we check if all the dangerous armies are still dangerous.
var newDangerArmies = this.reevaluateDangerousArmies(gameState,dangerousArmies);
var safeArmies = militaryManager.enemyWatchers[enemyID].getSafeArmies();
// we check not dangerous armies, to see if they suddenly became dangerous
var unsafeArmies = this.evaluateArmies(gameState,safeArmies);
for (i in unsafeArmies)
newDangerArmies[i] = unsafeArmies[i];
// and any dangerous armies we push in "dangerArmies"
militaryManager.enemyWatchers[enemyID].resetDangerousArmies();
for (o in newDangerArmies)
militaryManager.enemyWatchers[enemyID].setAsDangerous(o);
for (i in newDangerArmies)
dangerArmies[i] = newDangerArmies[i];
}
var self = this;
var nbOfAttackers = 0;
var newEnemies = [];
// clean up before adding new units (slight speeding up, since new units can't already be dead)
for (i in this.listOfEnemies) {
if (this.listOfEnemies[i].length === 0) {
// if we had defined the attackerCache, ie if we had tried to attack this unit.
if (this.attackerCache[i] !== undefined) {
this.attackerCache[i].forEach(function(ent) { ent.stopMoving(); });
delete this.attackerCache[i];
}
delete this.listOfEnemies[i];
} else {
var unit = this.listOfEnemies[i].toEntityArray()[0];
var enemyWatcher = militaryManager.enemyWatchers[unit.owner()];
if (enemyWatcher.isPartOfDangerousArmy(unit.id())) {
nbOfAttackers++;
if (this.attackerCache[unit.id()].length == 0) {
newEnemies.push(unit);
}
}*/
// Incorporates an entity in an army. If no army fits, it creates a new one around this one.
// an army is basically an entity collection.
Defence.prototype.armify = function(gameState, entity, militaryManager) {
if (entity.position() === undefined)
return;
if (this.enemyArmy[entity.owner()] === undefined)
{
this.enemyArmy[entity.owner()] = {};
} else {
for (armyIndex in this.enemyArmy[entity.owner()])
{
var army = this.enemyArmy[entity.owner()][armyIndex];
if (army.getCentrePosition() === undefined)
{
} else {
// if we had defined the attackerCache, ie if we had tried to attack this unit.
if (this.attackerCache[unit.id()] != undefined) {
this.attackerCache[unit.id()].forEach(function(ent) { ent.stopMoving(); });
delete this.attackerCache[unit.id()];
if (SquareVectorDistance(army.getCentrePosition(), entity.position()) < this.armyCompactSize)
{
entity.setMetadata(PlayerID, "inArmy", armyIndex);
army.addEnt(entity);
return;
}
this.listOfEnemies[unit.id()].toEntityArray()[0].setMetadata("listed-enemy",undefined);
delete this.listOfEnemies[unit.id()];
}
}
}
// okay so now, for every dangerous armies, we loop.
for (armyID in dangerArmies) {
// looping through army units
dangerArmies[armyID].forEach(function(ent) {
// do we have already registered an entityCollection for it?
if (self.listOfEnemies[ent.id()] === undefined) {
// no, we register a new entity collection in listOfEnemies, listing exactly one unit as long as it remains alive and owned by my enemy.
// can't be bothered to recode everything
var owner = ent.owner();
var filter = Filters.and(Filters.byOwner(owner),Filters.byID(ent.id()));
self.listOfEnemies[ent.id()] = self.enemyUnits[owner].filter(filter);
self.listOfEnemies[ent.id()].registerUpdates();
self.listOfEnemies[ent.id()].length;
self.listOfEnemies[ent.id()].toEntityArray()[0].setMetadata("listed-enemy",true);
// let's also register an entity collection for units attacking this unit (so we can new if it's attacked)
filter = Filters.and(Filters.byOwner(gameState.player),Filters.byTargetedEntity(ent.id()));
self.attackerCache[ent.id()] = self.myUnits.filter(filter);
self.attackerCache[ent.id()].registerUpdates();
nbOfAttackers++;
newEnemies.push(ent);
// if we're here, we need to create an army for it, and freeze it to make sure no unit will be added automatically
var newArmy = new EntityCollection(gameState.sharedScript, {}, [Filters.byOwner(entity.owner())]);
newArmy.addEnt(entity);
newArmy.freeze();
newArmy.registerUpdates();
entity.setMetadata(PlayerID, "inArmy", this.totalArmyNB);
this.enemyArmy[entity.owner()][this.totalArmyNB] = newArmy;
if (militaryManager)
{
var self = this;
militaryManager.enemyWatchers[entity.owner()].enemySoldiers.forEach(function (ent) { //}){
if (ent.position() !== undefined && SquareVectorDistance(entity.position(), ent.position()) < self.armyCompactSize)
{
ent.setMetadata(PlayerID, "inArmy", self.totalArmyNB);
self.enemyArmy[ent.owner()][self.totalArmyNB].addEnt(ent);
}
});
}
this.totalArmyNB++;
return;
}
// Returns if a unit should be seen as dangerous or not.
Defence.prototype.evaluateRawEntity = function(gameState, entity) {
if (entity.position && +this.territoryMap.point(entity.position) - 64 === +PlayerID && entity._template.Attack !== undefined)
return true;
return false;
}
Defence.prototype.evaluateEntity = function(gameState, entity) {
if (entity.position() && +this.territoryMap.point(entity.position()) - 64 === +PlayerID && entity.attackTypes() !== undefined)
return true;
return false;
}
// This deals with incoming enemy armies, setting the defcon if needed. It will take new soldiers, and assign them to attack
// TODO: still is still pretty dumb, it could use improvements.
Defence.prototype.defendFromEnemies = function(gameState, events, militaryManager) {
var self = this;
// New, faster system will loop for enemy soldiers, and also females on occasions ( TODO )
// if a dangerous unit is found, it will check for neighbors and make them into an "army", an entityCollection
// > updated against owner, for the day when I throw healers in the deal.
// armies are checked against each other now and then to see if they should be merged, and units in armies are checked to see if they should be taken away from the army.
// We keep a list of idle defenders. For any new attacker, we'll check if we have any idle defender available, and if not, we assign available units.
// At the end of each turn, if we still have idle defenders, we either assign them to neighboring units, or we release them.
var nbOfAttackers = 0; // actually new attackers.
var newEnemies = [];
// clean up using events.
for each(evt in events)
{
if (evt.type == "Destroy")
{
if (this.listOfEnemies[evt.msg.entity] !== undefined)
{
if (this.attackerCache[evt.msg.entity] !== undefined) {
this.attackerCache[evt.msg.entity].forEach(function(ent) { ent.stopMoving(); });
delete self.attackerCache[evt.msg.entity];
}
delete this.listOfEnemies[evt.msg.entity];
this.nbAttackers--;
}
}
}
// Optimizations: this will slowly iterate over all units (saved at an instant T) and all armies.
// It'll add new units if they are now dangerous and were not before
// It'll also deal with cleanup of armies.
// When it's finished it'll start over.
for (enemyID in this.enemyArmy)
{
//this.enemyUnits[enemyID] = militaryManager.enemyWatchers[enemyID].getAllEnemySoldiers();
if (this.enemyUnits[enemyID] === undefined || this.enemyUnits[enemyID].length === 0)
{
this.enemyUnits[enemyID] = militaryManager.enemyWatchers[enemyID].enemySoldiers.toEntityArray();
continue;
}
// we have some units still to check in this array. Check 15 (TODO: DIFFLEVEL)
// Note: given the way memory works, if the entity has been recently deleted, its reference may still exist.
// and this.enemyUnits[enemyID][0] may still point to that reference, "reviving" the unit.
// So we've got to make sure it's not supposed to be dead.
for (var check = 0; check < 15; check++)
{
if (this.enemyUnits[enemyID].length > 0 && gameState.getEntityById(this.enemyUnits[enemyID][0].id()) !== undefined)
{
if (this.enemyUnits[enemyID][0].getMetadata(PlayerID, "inArmy") !== undefined)
{
this.enemyUnits[enemyID].splice(0,1);
continue;
}
var dangerous = this.evaluateEntity(gameState, this.enemyUnits[enemyID][0]);
if (dangerous)
{
this.armify(gameState, this.enemyUnits[enemyID][0], militaryManager);
}
this.enemyUnits[enemyID].splice(0,1);
}
}
// okay then we'll check one of the armies
// creating the array to iterate over.
if (this.enemyArmyLoop[enemyID] === undefined || this.enemyArmyLoop[enemyID].length === 0)
{
this.enemyArmyLoop[enemyID] = [];
for (i in this.enemyArmy[enemyID])
this.enemyArmyLoop[enemyID].push(this.enemyArmy[enemyID][i]);
}
// and now we check the last known army.
if (this.enemyArmyLoop[enemyID].length !== 0) {
var army = this.enemyArmyLoop[enemyID][0];
var position = army.getCentrePosition();
if (!position)
{
// todo: scrap that army, it means all units are likely garrisoned.
this.enemyArmyLoop[enemyID].splice(0,1);
}
army.forEach(function (ent) { //}){
// check if the unit is a breakaway
if (ent.position() && SquareVectorDistance(position, ent.position()) > self.armyBreakawaySize)
{
ent.setMetadata(PlayerID, "inArmy", undefined);
army.removeEnt(ent);
if (self.evaluateEntity(gameState,ent))
self.armify(gameState,ent);
} else {
// check if we have registered that unit already.
if (self.listOfEnemies[ent.id()] === undefined) {
self.listOfEnemies[ent.id()] = new EntityCollection(gameState.sharedScript, {}, [Filters.byOwner(ent.owner())]);
self.listOfEnemies[ent.id()].freeze();
self.listOfEnemies[ent.id()].addEnt(ent);
self.listOfEnemies[ent.id()].registerUpdates();
self.attackerCache[ent.id()] = self.myUnits.filter(Filters.byTargetedEntity(ent.id()));
self.attackerCache[ent.id()].registerUpdates();
nbOfAttackers++;
self.nbAttackers++;
newEnemies.push(ent);
} else if (self.attackerCache[ent.id()] === undefined || self.attackerCache[ent.id()].length == 0) {
nbOfAttackers++;
newEnemies.push(ent);
}
}
});
// TODO: check if the army itself is not dangerous anymore.
this.enemyArmyLoop[enemyID].splice(0,1);
}
// okay so now the army update is done.
}
// Reordering attack because the pathfinder is for now not dynamically updated
for (o in this.attackerCache) {
if ((this.attackerCacheLoopIndicator + o) % 2 === 0) {
this.attackerCache[o].forEach(function (ent) {
ent.attack(+o);
});
ent.attack(+o);
});
}
}
this.attackerCacheLoopIndicator++;
this.attackerCacheLoopIndicator = this.attackerCacheLoopIndicator % 2;
//debug ("total number "+ this.nbAttackers);
//debug ("total number "+ this.nbDefenders);
if (nbOfAttackers === 0) {
if (this.nbAttackers === 0) {
militaryManager.unpauseAllPlans(gameState);
return;
}
@ -226,38 +335,49 @@ Defence.prototype.defendFromEnemyArmies = function(gameState, events, militaryMa
// and then I'll assign my units.
// and then rock on.
if (nbOfAttackers < 10){
if (this.nbAttackers < 10){
gameState.setDefcon(4); // few local units
} else if (nbOfAttackers >= 10){
} else if (this.nbAttackers >= 10){
gameState.setDefcon(3);
}
// we're having too many.
if (this.myUnits.filter(Filters.byMetadata("role","defence")).length > nbOfAttackers*this.defenceRatio*1.3) {
this.myUnits.filter(Filters.byMetadata("role","defence")).forEach(function (defender) { //}){
if (this.myUnits.filter(Filters.byMetadata(PlayerID, "role","defence")).length > nbOfAttackers*this.defenceRatio*1.3) {
this.myUnits.filter(Filters.byMetadata(PlayerID, "role","defence")).forEach(function (defender) { //}){
if (defender.unitAIOrderData() && defender.unitAIOrderData()["target"]) {
if (self.attackerCache[defender.unitAIOrderData()["target"]].length > 3) {
// okay release me.
defender.stopMoving();
if (defender.getMetadata("formerrole"))
defender.setMetadata("role", defender.getMetadata("formerrole") );
if (defender.getMetadata(PlayerID, "formerrole"))
defender.setMetadata(PlayerID, "role", defender.getMetadata(PlayerID, "formerrole") );
else
defender.setMetadata("role", "worker");
defender.setMetadata("subrole", undefined);
defender.setMetadata(PlayerID, "role", "worker");
defender.setMetadata(PlayerID, "subrole", undefined);
self.nbDefenders--;
}
}
});
}
var nonDefenders = this.myUnits.filter(Filters.or(Filters.not(Filters.byMetadata("role","defence")),Filters.isIdle()));
var nonDefenders = this.myUnits.filter(Filters.or(Filters.not(Filters.byMetadata(PlayerID, "role","defence")),Filters.isIdle()));
nonDefenders = nonDefenders.filter(Filters.not(Filters.byClass("Female")));
nonDefenders = nonDefenders.filter(Filters.not(Filters.byMetadata("subrole","attacking")));
nonDefenders = nonDefenders.filter(Filters.not(Filters.byMetadata(PlayerID, "subrole","attacking")));
var defenceRatio = this.defenceRatio;
if (newEnemies.length * defenceRatio > nonDefenders.length) {
defenceRatio = 1;
}
//debug ("newEnemies.length "+ newEnemies.length);
//debug ("nonDefenders.length "+ nonDefenders.length);
if (newEnemies.length * defenceRatio > nonDefenders.length)
defenceRatio--;
if (newEnemies.length * defenceRatio > nonDefenders.length)
defenceRatio--;
if (defenceRatio < 1)
defenceRatio = 1;
if (gameState.defcon() < 3)
militaryManager.pauseAllPlans(gameState);
// For each enemy, we'll pick two units.
for each (enemy in newEnemies) {
if (nonDefenders.length === 0)
@ -270,62 +390,60 @@ Defence.prototype.defendFromEnemyArmies = function(gameState, events, militaryMa
if (assigned >= defenceRatio)
return;
// let's check for a counter.
//debug ("Enemy is a " + uneval(enemy._template.Identity.Classes._string) );
var potCounters = gameState.ai.templateManager.getCountersToClasses(gameState,enemy.classes(),enemy.templateName());
//debug ("Counters are" +uneval(potCounters));
var counters = [];
for (o in potCounters) {
var counter = nonDefenders.filter(Filters.and(Filters.byType(potCounters[o][0]), Filters.byStaticDistance(enemy.position(), 150) )).toEntityArray();
if (counter.length !== 0)
for (unit in counter)
counters.push(counter[unit]);
// We'll sort through our units that can legitimately attack.
// Sorting is done by distance, and if the unit counters the attacker it's "assumed" it's a little closer.
var data = [];
for (var id in nonDefenders._entities)
{
var ent = nonDefenders._entities[id];
if (ent.position())
data.push([id, ent, SquareVectorDistance(enemy.position(), ent.position())]);
}
//debug ("I have " +counters.length +"countering units");
for (var i = 0; i < defenceRatio && i < counters.length; i++) {
if (counters[i].getMetadata("plan") !== undefined)
militaryManager.pausePlan(gameState,counters[i].getMetadata("plan"));
if (counters[i].getMetadata("role") == "worker" || counters[i].getMetadata("role") == "attack")
counters[i].setMetadata("formerrole", counters[i].getMetadata("role"));
counters[i].setMetadata("role","defence");
counters[i].setMetadata("subrole","defending");
counters[i].attack(+enemy.id());
nonDefenders.updateEnt(counters[i]);
data.sort(function (a, b) {
var vala = a[2];
var valb = b[2];
if (a[1].countersClasses(b[1].classes()))
vala *= 0.8;
if (b[1].countersClasses(a[1].classes()))
valb *= 0.8;
return (vala - valb); });
var ret = {};
for each (var val in data.slice(0, Math.min(nonDefenders._length, defenceRatio - assigned)))
ret[val[0]] = val[1];
var defs = new EntityCollection(nonDefenders._ai, ret);
// successfully sorted
defs.forEach(function (defender) { //}){
if (defender.getMetadata(PlayerID, "role") == "worker" || defender.getMetadata(PlayerID, "role") == "attack")
defender.setMetadata(PlayerID, "formerrole", defender.getMetadata(PlayerID, "role"));
defender.setMetadata(PlayerID, "role","defence");
defender.setMetadata(PlayerID, "subrole","defending");
defender.attack(+enemy.id());
nonDefenders.updateEnt(defender);
assigned++;
//debug ("Sending a " +counters[i].templateName() +" to counter a " + enemy.templateName());
}
if (assigned !== defenceRatio) {
// take closest units
nonDefenders.filter(Filters.byClass("CitizenSoldier")).filterNearest(enemy.position(),defenceRatio-assigned).forEach(function (defender) { //}){
if (defender.getMetadata("plan") !== undefined)
militaryManager.pausePlan(gameState,defender.getMetadata("plan"));
if (defender.getMetadata("role") == "worker" || defender.getMetadata("role") == "attack")
defender.setMetadata("formerrole", defender.getMetadata("role"));
defender.setMetadata("role","defence");
defender.setMetadata("subrole","defending");
defender.attack(+enemy.id());
nonDefenders.updateEnt(defender);
assigned++;
});
}
self.nbDefenders++;
});
}
/*
// yes. We'll pick new units (pretty randomly for now, todo)
// first from attack plans, then from workers.
var newSoldiers = gameState.getOwnEntities().filter(function (ent) {
if (ent.getMetadata("plan") != undefined && ent.getMetadata("role") != "defence")
if (ent.getMetadata(PlayerID, "plan") != undefined && ent.getMetadata(PlayerID, "role") != "defence")
return true;
return false;
});
newSoldiers.forEach(function(ent) {
if (ent.getMetadata("subrole","attacking")) // gone with the wind to avenge their brothers.
if (ent.getMetadata(PlayerID, "subrole","attacking")) // gone with the wind to avenge their brothers.
return;
if (nbOfAttackers <= 0)
return;
militaryManager.pausePlan(gameState,ent.getMetadata("plan"));
ent.setMetadata("formerrole", ent.getMetadata("role"));
ent.setMetadata("role","defence");
ent.setMetadata("subrole","newdefender");
militaryManager.pausePlan(gameState,ent.getMetadata(PlayerID, "plan"));
ent.setMetadata(PlayerID, "formerrole", ent.getMetadata(PlayerID, "role"));
ent.setMetadata(PlayerID, "role","defence");
ent.setMetadata(PlayerID, "subrole","newdefender");
nbOfAttackers--;
});
@ -337,10 +455,10 @@ Defence.prototype.defendFromEnemyArmies = function(gameState, events, militaryMa
return;
// If we're not female, we attack
// and if we're not already assigned from above (might happen, not sure, rather be cautious)
if (ent.hasClass("CitizenSoldier") && ent.getMetadata("subrole") != "newdefender") {
ent.setMetadata("formerrole", "worker");
ent.setMetadata("role","defence");
ent.setMetadata("subrole","newdefender");
if (ent.hasClass("CitizenSoldier") && ent.getMetadata(PlayerID, "subrole") != "newdefender") {
ent.setMetadata(PlayerID, "formerrole", "worker");
ent.setMetadata(PlayerID, "role","defence");
ent.setMetadata(PlayerID, "subrole","newdefender");
nbOfAttackers--;
}
});
@ -370,7 +488,7 @@ Defence.prototype.defendFromEnemyArmies = function(gameState, events, militaryMa
break;
}
}
ent.setMetadata("subrole","defending");
ent.setMetadata(PlayerID, "subrole","defending");
ent.attack(+target);
});
*/
@ -381,6 +499,8 @@ Defence.prototype.defendFromEnemyArmies = function(gameState, events, militaryMa
// So that a unit that gets attacked will not be completely dumb.
// warning: huge levels of indentation coming.
Defence.prototype.MessageProcess = function(gameState,events, militaryManager) {
var self = this;
for (var key in events){
var e = events[key];
if (e.type === "Attacked" && e.msg){
@ -388,14 +508,14 @@ Defence.prototype.MessageProcess = function(gameState,events, militaryManager) {
var attacker = gameState.getEntityById(e.msg.attacker);
var ourUnit = gameState.getEntityById(e.msg.target);
// the attacker must not be already dead, and it must not be me (think catapults that miss).
if (attacker !== undefined && attacker.owner() !== gameState.player && attacker.position() !== undefined) {
if (attacker !== undefined && attacker.owner() !== PlayerID && attacker.position() !== undefined) {
// note: our unit can already by dead by now... We'll then have to rely on the enemy to react.
// if we're not on enemy territory
var territory = +this.territoryMap.point(attacker.position()) - 64;
// we do not consider units that are defenders, and we do not consider units that are part of an attacking attack plan
// (attacking attacking plans are dealing with threats on their own).
if (ourUnit !== undefined && (ourUnit.getMetadata("role") == "defence" || ourUnit.getMetadata("subrole") == "attacking"))
if (ourUnit !== undefined && (ourUnit.getMetadata(PlayerID, "role") == "defence" || ourUnit.getMetadata(PlayerID, "subrole") == "attacking"))
continue;
// let's check for animals
@ -410,15 +530,19 @@ Defence.prototype.MessageProcess = function(gameState,events, militaryManager) {
}
}
// anyway we'll register the animal as dangerous, and attack it.
var filter = Filters.byID(attacker.id());
this.listOfWantedUnits[attacker.id()] = gameState.getEntities().filter(filter);
this.listOfWantedUnits[attacker.id()] = new EntityCollection(gameState.sharedScript);
this.listOfWantedUnits[attacker.id()].addEnt(attacker);
this.listOfWantedUnits[attacker.id()].freeze();
this.listOfWantedUnits[attacker.id()].registerUpdates();
this.listOfWantedUnits[attacker.id()].length;
filter = Filters.and(Filters.byOwner(gameState.player),Filters.byTargetedEntity(attacker.id()));
var filter = Filters.byTargetedEntity(attacker.id());
this.WantedUnitsAttacker[attacker.id()] = this.myUnits.filter(filter);
this.WantedUnitsAttacker[attacker.id()].registerUpdates();
this.WantedUnitsAttacker[attacker.id()].length;
} else if (territory != attacker.owner()) { // preliminary check: attacks in enemy territory are not counted as attacks
} // preliminary check: we do not count attacked military units (for sanity for now, TODO).
else if ( (territory != attacker.owner() && ourUnit.hasClass("Support")) || (!ourUnit.hasClass("Support") && territory == PlayerID))
{
// Also TODO: this does not differentiate with buildings...
// These ought to be treated differently.
// units in attack plans will react independently, but we still list the attacks here.
@ -427,23 +551,40 @@ Defence.prototype.MessageProcess = function(gameState,events, militaryManager) {
} else {
// TODO: right now a soldier always retaliate... Perhaps it should be set in "Defence" mode.
if (!attacker.hasClass("Female") && !attacker.hasClass("Ship")) {
// This unit is dangerous. We'll ask the enemy manager if it's part of a big army, in which case we'll list it as dangerous (so it'll be treated next turn by the other manager)
// If it's not part of a big army, depending on our priority we may want to kill it (using the same things as animals for that)
// TODO (perhaps not any more, but let's mark it anyway)
var army = militaryManager.enemyWatchers[attacker.owner()].getArmyFromMember(attacker.id());
if (army !== undefined && army[1].length > 5) {
militaryManager.enemyWatchers[attacker.owner()].setAsDangerous(army[0]);
} else if (army !== undefined && !militaryManager.enemyWatchers[attacker.owner()].isDangerous(army[0])) {
// This unit is dangerous. if it's in an army, it's being dealt with.
// if it's not in an army, it means it's either a lone raider, or it has got friends.
// In which case we must check for other dangerous units around, and perhaps armify them.
// TODO: perhaps someday army detection will have improved and this will require change.
var armyID = attacker.getMetadata(PlayerID, "inArmy");
if (armyID == undefined || !this.enemyArmy[attacker.owner()] || !this.enemyArmy[attacker.owner()][armyID]) {
// armify it, then armify units close to him.
this.armify(gameState,attacker);
armyID = attacker.getMetadata(PlayerID, "inArmy");
var position = attacker.position();
var close = militaryManager.enemyWatchers[attacker.owner()].enemySoldiers.filter(Filters.byDistance(position, self.armyCompactSize));
if (close.length > 15)
{
close.forEach(function (ent) { //}){
if (SquareVectorDistance(position, ent.position()) < self.armyCompactSize)
{
ent.setMetadata(PlayerID, "inArmy", armyID);
self.enemyArmy[ent.owner()][armyID].addEnt(ent)
}
});
}
// Defencemanager will deal with them in the next turn.
/*
// we register this unit as wanted, TODO register the whole army
// another function will deal with it.
var filter = Filters.and(Filters.byOwner(attacker.owner()),Filters.byID(attacker.id()));
this.listOfWantedUnits[attacker.id()] = this.enemyUnits[attacker.owner()].filter(filter);
this.listOfWantedUnits[attacker.id()] = new EntityCollection(gameState.sharedScript, {}, [Filters.byOwner(attacker.owner())]);
this.listOfWantedUnits[attacker.id()].addEnt(attacker);
this.listOfWantedUnits[attacker.id()].registerUpdates();
this.listOfWantedUnits[attacker.id()].length;
filter = Filters.and(Filters.byOwner(gameState.player),Filters.byTargetedEntity(attacker.id()));
filter = Filters.and(Filters.byOwner(PlayerID),Filters.byTargetedEntity(attacker.id()));
this.WantedUnitsAttacker[attacker.id()] = this.myUnits.filter(filter);
this.WantedUnitsAttacker[attacker.id()].registerUpdates();
this.WantedUnitsAttacker[attacker.id()].length;
*/
}
if (ourUnit && ourUnit.hasClass("Unit")) {
if (ourUnit.hasClass("Support")) {
@ -458,7 +599,6 @@ Defence.prototype.MessageProcess = function(gameState,events, militaryManager) {
ourUnit.attack(e.msg.attacker);
}
}
}
}
}
@ -466,7 +606,8 @@ Defence.prototype.MessageProcess = function(gameState,events, militaryManager) {
}
}
}
};
}; // nice sets of closing brackets, isn't it?
// At most, this will put defcon to 4
Defence.prototype.DealWithWantedUnits = function(gameState, events, militaryManager) {
//if (gameState.defcon() < 3)
@ -508,15 +649,15 @@ Defence.prototype.DealWithWantedUnits = function(gameState, events, militaryMana
// we send 3 units to each target just to be sure. TODO refine.
// we do not use plan units
this.idleDefs.forEach(function(ent) {
if (nbOfDealtWith < 3 && nbOfAttackers > 0 && ent.getMetadata("plan") == undefined)
if (nbOfDealtWith < 3 && nbOfAttackers > 0 && ent.getMetadata(PlayerID, "plan") == undefined)
for (o in self.listOfWantedUnits) {
if ( (addedto[o] == undefined && self.WantedUnitsAttacker[o].length < 3) || (addedto[o] && self.WantedUnitsAttacker[o].length + addedto[o] < 3)) {
if (self.WantedUnitsAttacker[o].length === 0)
nbOfDealtWith++;
ent.setMetadata("formerrole", ent.getMetadata("role"));
ent.setMetadata("role","defence");
ent.setMetadata("subrole", "defending");
ent.setMetadata(PlayerID, "formerrole", ent.getMetadata(PlayerID, "role"));
ent.setMetadata(PlayerID, "role","defence");
ent.setMetadata(PlayerID, "subrole", "defending");
ent.attack(+o);
if (addedto[o])
addedto[o]++;
@ -543,9 +684,9 @@ Defence.prototype.DealWithWantedUnits = function(gameState, events, militaryMana
if ( (addedto[o] == undefined && self.WantedUnitsAttacker[o].length < 3) || (addedto[o] && self.WantedUnitsAttacker[o].length + addedto[o] < 3)) {
if (self.WantedUnitsAttacker[o].length === 0)
nbOfDealtWith++;
ent.setMetadata("formerrole", ent.getMetadata("role"));
ent.setMetadata("role","defence");
ent.setMetadata("subrole", "defending");
ent.setMetadata(PlayerID, "formerrole", ent.getMetadata(PlayerID, "role"));
ent.setMetadata(PlayerID, "role","defence");
ent.setMetadata(PlayerID, "subrole", "defending");
ent.attack(+o);
if (addedto[o])
addedto[o]++;

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,24 @@
/*
* A class that keeps track of enem buildings, units, and pretty much anything I can think of (still a LOT TODO here)
* A class that keeps track of enemy buildings, units, and pretty much anything I can think of (still a LOT TODO here)
* Only watches one enemy, you'll need one per enemy.
* Note: pretty much unused in the current version.
*/
var enemyWatcher = function(gameState, playerToWatch) {
this.watched = playerToWatch;
// creating fitting entity collections
// using global entity collections, shared by any AI that knows the name of this.
var filter = Filters.and(Filters.byClass("Structure"), Filters.byOwner(this.watched));
this.enemyBuildings = gameState.getEnemyEntities().filter(filter);
this.enemyBuildings.registerUpdates();
this.enemyBuildings = gameState.updatingGlobalCollection("player-" +this.watched + "-structures", filter);
filter = Filters.and(Filters.byClass("Worker"), Filters.byOwner(this.watched));
this.enemyCivilians = gameState.updatingGlobalCollection("player-" +this.watched + "-civilians", filter);
filter = Filters.and(Filters.byClassesOr(["CitizenSoldier", "Hero", "Champion", "Siege"]), Filters.byOwner(this.watched));
this.enemySoldiers = gameState.updatingGlobalCollection("player-" +this.watched + "-soldiers", filter);
filter = Filters.and(Filters.byClass("Worker"), Filters.byOwner(this.watched));
this.enemyCivilians = gameState.getEnemyEntities().filter(filter);
this.enemyCivilians.registerUpdates();
@ -19,16 +26,6 @@ var enemyWatcher = function(gameState, playerToWatch) {
filter = Filters.and(Filters.byClassesOr(["CitizenSoldier", "Hero", "Champion", "Siege"]), Filters.byOwner(this.watched));
this.enemySoldiers = gameState.getEnemyEntities().filter(filter);
this.enemySoldiers.registerUpdates();
// okay now we register here only enemy soldiers that we are monitoring (ie we see as part of an army…)
filter = Filters.and(Filters.byClassesOr(["CitizenSoldier", "Hero", "Champion", "Siege"]), Filters.and(Filters.byMetadata("monitored","true"),Filters.byOwner(this.watched)));
this.monitoredEnemySoldiers = gameState.getEnemyEntities().filter(filter);
this.monitoredEnemySoldiers.registerUpdates();
// and here those that we do not monitor
filter = Filters.and(Filters.byClassesOr(["CitizenSoldier","Hero","Champion","Siege"]), Filters.and(Filters.not(Filters.byMetadata("monitored","true")),Filters.byOwner(this.watched)));
this.unmonitoredEnemySoldiers = gameState.getEnemyEntities().filter(filter);
this.unmonitoredEnemySoldiers.registerUpdates();
// entity collections too.
this.armies = {};
@ -46,16 +43,16 @@ enemyWatcher.prototype.getAllEnemySoldiers = function() {
enemyWatcher.prototype.getAllEnemyBuildings = function() {
return this.enemyBuildings;
};
enemyWatcher.prototype.getEnemyBuildings = function(specialClass, OneTime) {
var filter = Filters.byClass(specialClass);
var returnable = this.enemyBuildings.filter(filter);
if (!this.enemyBuildingClass[specialClass] && !OneTime) {
this.enemyBuildingClass[specialClass] = returnable;
this.enemyBuildingClass[specialClass].registerUpdates();
return this.enemyBuildingClass[specialClass];
}
return returnable;
enemyWatcher.prototype.getEnemyBuildings = function(gameState, specialClass, OneTime) {
var filter = Filters.byClass(specialClass);
if (OneTime && gameState.getGEC("player-" +this.watched + "-structures-" +specialClass))
return gameState.getGEC("player-" +this.watched + "-structures-" +specialClass);
else if (OneTime)
return this.enemyBuildings.filter(filter);
return gameState.updatingGlobalCollection("player-" +this.watched + "-structures-" +specialClass, filter, gameState.getGEC("player-" +this.watched + "-structure"));
};
enemyWatcher.prototype.getDangerousArmies = function() {
var toreturn = {};
@ -117,11 +114,11 @@ enemyWatcher.prototype.detectArmies = function(gameState){
return;
// this was an unmonitored unit, we do not know any army associated with it. We assign it a new army (we'll merge later if needed)
enemy.setMetadata("monitored","true");
enemy.setMetadata(PlayerID, "monitored","true");
var armyID = gameState.player + "" + self.totalNBofArmies;
self.totalNBofArmies++,
enemy.setMetadata("EnemyWatcherArmy",armyID);
var filter = Filters.byMetadata("EnemyWatcherArmy",armyID);
enemy.setMetadata(PlayerID, "EnemyWatcherArmy",armyID);
var filter = Filters.byMetadata(PlayerID, "EnemyWatcherArmy",armyID);
var army = self.enemySoldiers.filter(filter);
self.armies[armyID] = army;
self.armies[armyID].registerUpdates();
@ -157,7 +154,7 @@ enemyWatcher.prototype.mergeArmies = function(){
this.dangerousArmies.push(army);
secondArmy.forEach( function(ent) {
ent.setMetadata("EnemyWatcherArmy",army);
ent.setMetadata(PlayerID, "EnemyWatcherArmy",army);
});
}
}
@ -184,7 +181,7 @@ enemyWatcher.prototype.ScrapEmptyArmies = function(){
enemyWatcher.prototype.splitArmies = function(gameState){
var self = this;
var map = gameState.getTerritoryMap();
var map = Map.createTerritoryMap(gameState);
for (armyID in this.armies) {
var army = this.armies[armyID];
@ -203,8 +200,8 @@ enemyWatcher.prototype.splitArmies = function(gameState){
self.dangerousArmies.push(newArmyID);
self.totalNBofArmies++,
enemy.setMetadata("EnemyWatcherArmy",newArmyID);
var filter = Filters.byMetadata("EnemyWatcherArmy",newArmyID);
enemy.setMetadata(PlayerID, "EnemyWatcherArmy",newArmyID);
var filter = Filters.byMetadata(PlayerID, "EnemyWatcherArmy",newArmyID);
var newArmy = self.enemySoldiers.filter(filter);
self.armies[newArmyID] = newArmy;
self.armies[newArmyID].registerUpdates();

View File

@ -1,81 +1,20 @@
EntityTemplate.prototype.genericName = function() {
if (!this._template.Identity || !this._template.Identity.GenericName)
return undefined;
return this._template.Identity.GenericName;
};
EntityTemplate.prototype.walkSpeed = function() {
if (!this._template.UnitMotion || !this._template.UnitMotion.WalkSpeed)
return undefined;
return this._template.UnitMotion.WalkSpeed;
};
EntityTemplate.prototype.buildTime = function() {
if (!this._template.Cost || !this._template.Cost.buildTime)
return undefined;
return this._template.Cost.buildTime;
};
EntityTemplate.prototype.getPopulationBonus = function() {
if (!this._template.Cost || !this._template.Cost.PopulationBonus)
return undefined;
return this._template.Cost.PopulationBonus;
};
// will return either "food", "wood", "stone", "metal" and not treasure.
EntityTemplate.prototype.getResourceType = function() {
if (!this._template.ResourceSupply)
return undefined;
var [type, subtype] = this._template.ResourceSupply.Type.split('.');
if (type == "treasure")
return subtype;
return type;
};
EntityTemplate.prototype.garrisonMax = function() {
if (!this._template.GarrisonHolder)
return undefined;
return this._template.GarrisonHolder.Max;
};
EntityTemplate.prototype.hasClasses = function(array) {
var classes = this.classes();
if (!classes)
return false;
for (i in array)
if (classes.indexOf(array[i]) === -1)
return false;
return true;
};
// returns the classes this counters:
// each countered class is an array specifying what is required (even if only one) and the Multiplier [ ["whatever","other whatever"] , 0 ].
EntityTemplate.prototype.getCounteredClasses = function() {
if (!this._template.Attack)
return undefined;
var Classes = [];
for (i in this._template.Attack) {
if (!this._template.Attack[i].Bonuses)
continue;
for (o in this._template.Attack[i].Bonuses) {
if (this._template.Attack[i].Bonuses[o].Classes == undefined)
continue;
Classes.push([this._template.Attack[i].Bonuses[o].Classes.split(" "), +this._template.Attack[i].Bonuses[o].Multiplier]);
}
}
return Classes;
};
EntityTemplate.prototype.getMaxStrength = function()
// returns some sort of DPS * health factor. If you specify a class, it'll use the modifiers against that class too.
function getMaxStrength(ent, againstClass)
{
var strength = 0.0;
var attackTypes = this.attackTypes();
var armourStrength = this.armourStrengths();
var hp = this.maxHitpoints() / 100.0; // some normalization
var attackTypes = ent.attackTypes();
var armourStrength = ent.armourStrengths();
var hp = ent.maxHitpoints() / 100.0; // some normalization
for (var typeKey in attackTypes) {
var type = attackTypes[typeKey];
var attackStrength = this.attackStrengths(type);
var attackRange = this.attackRange(type);
var attackTimes = this.attackTimes(type);
var attackStrength = ent.attackStrengths(type);
var attackRange = ent.attackRange(type);
var attackTimes = ent.attackTimes(type);
for (var str in attackStrength) {
var val = parseFloat(attackStrength[str]);
if (againstClass)
val *= ent.getMultiplierAgainst(type, againstClass);
switch (str) {
case "crush":
strength += (val * 0.085) / 3;
@ -119,73 +58,3 @@ EntityTemplate.prototype.getMaxStrength = function()
}
return strength * hp;
};
EntityTemplate.prototype.costSum = function() {
if (!this._template.Cost)
return undefined;
var ret = 0;
for (var type in this._template.Cost.Resources)
ret += +this._template.Cost.Resources[type];
return ret;
};
Entity.prototype.deleteMetadata = function(id) {
delete this._ai._entityMetadata[this.id()];
};
Entity.prototype.healthLevel = function() {
return (this.hitpoints() / this.maxHitpoints());
};
Entity.prototype.visibility = function(player) {
return this._entity.visibility[player-1];
};
Entity.prototype.unload = function(id) {
if (!this._template.GarrisonHolder)
return undefined;
Engine.PostCommand({"type": "unload", "garrisonHolder": this.id(), "entity": id});
return this;
};
Entity.prototype.unloadAll = function() {
if (!this._template.GarrisonHolder)
return undefined;
Engine.PostCommand({"type": "unload-all", "garrisonHolder": this.id()});
return this;
};
Entity.prototype.garrison = function(target) {
Engine.PostCommand({"type": "garrison", "entities": [this.id()], "target": target.id(),"queued": false});
return this;
};
Entity.prototype.stopMoving = function() {
if (this.position() !== undefined)
Engine.PostCommand({"type": "walk", "entities": [this.id()], "x": this.position()[0], "z": this.position()[1], "queued": false});
};
// from from a unit in the opposite direction.
Entity.prototype.flee = function(unitToFleeFrom) {
if (this.position() !== undefined && unitToFleeFrom.position() !== undefined) {
var FleeDirection = [unitToFleeFrom.position()[0] - this.position()[0],unitToFleeFrom.position()[1] - this.position()[1]];
var dist = VectorDistance(unitToFleeFrom.position(), this.position() );
FleeDirection[0] = (FleeDirection[0]/dist) * 5;
FleeDirection[1] = (FleeDirection[1]/dist) * 5;
Engine.PostCommand({"type": "walk", "entities": [this.id()], "x": this.position()[0] + FleeDirection[0]*5, "z": this.position()[1] + FleeDirection[1]*5, "queued": false});
}
return this;
};
Entity.prototype.barter = function(buyType, sellType, amount) {
Engine.PostCommand({"type": "barter", "sell" : sellType, "buy" : buyType, "amount" : amount });
return this;
};
Entity.prototype.disband = function() {
Engine.PostCommand({"type": "delete-entities", "entities" : [this.id()] });
return this;
};

View File

@ -1,21 +1,3 @@
EntityCollection.prototype.attack = function(unit)
{
var unitId;
if (typeof(unit) === "Entity"){
unitId = unit.id();
}else{
unitId = unit;
}
Engine.PostCommand({"type": "attack", "entities": this.toIdArray(), "target": unitId, "queued": false});
return this;
};
// violent, aggressive, defensive, passive, standground
EntityCollection.prototype.setStance = function(stance){
Engine.PostCommand({"type": "stance", "entities": this.toIdArray(), "name" : stance, "queued": false});
return this;
};
function EntityCollectionFromIds(gameState, idList){
var ents = {};
for (var i in idList){
@ -26,64 +8,3 @@ function EntityCollectionFromIds(gameState, idList){
}
return new EntityCollection(gameState.ai, ents);
}
EntityCollection.prototype.getCentrePosition = function(){
var sumPos = [0, 0];
var count = 0;
this.forEach(function(ent){
if (ent.position()){
sumPos[0] += ent.position()[0];
sumPos[1] += ent.position()[1];
count ++;
}
});
if (count === 0){
return undefined;
}else{
return [sumPos[0]/count, sumPos[1]/count];
}
};
EntityCollection.prototype.getApproximatePosition = function(sample){
var sumPos = [0, 0];
var i = 0;
for (var id in this._entities)
{
var ent = this._entities[id];
if (ent.position()) {
sumPos[0] += ent.position()[0];
sumPos[1] += ent.position()[1];
i++;
}
if (i === sample)
break;
}
if (sample === 0){
return undefined;
}else{
return [sumPos[0]/i, sumPos[1]/i];
}
};
EntityCollection.prototype.filterNearest = function(targetPos, n)
{
// Compute the distance of each entity
var data = []; // [ [id, ent, distance], ... ]
for (var id in this._entities)
{
var ent = this._entities[id];
if (ent.position())
data.push([id, ent, SquareVectorDistance(targetPos, ent.position())]);
}
// Sort by increasing distance
data.sort(function (a, b) { return (a[2] - b[2]); });
if (n === undefined)
n = this._length;
// Extract the first n
var ret = {};
for each (var val in data.slice(0, n))
ret[val[0]] = val[1];
return new EntityCollection(this._ai, ret);
};

View File

@ -1,42 +0,0 @@
// Some new filters I use in entity Collections
Filters["byID"] =
function(id){
return {"func": function(ent){
return (ent.id() == id);
},
"dynamicProperties": ['id']};
};
Filters["byTargetedEntity"] =
function(targetID){
return {"func": function(ent){
return (ent.unitAIOrderData() && ent.unitAIOrderData()["target"] && ent.unitAIOrderData()["target"] == targetID);
},
"dynamicProperties": ['unitAIOrderData']};
};
Filters["byHasMetadata"] =
function(key){
return {"func" : function(ent){
return (ent.getMetadata(key) != undefined);
},
"dynamicProperties": ['metadata.' + key]};
};
Filters["byTerritory"] = function(Map, territoryIndex){
return {"func": function(ent){
if (Map.point(ent.position()) == territoryIndex) {
return true;
} else {
return false;
}
},
"dynamicProperties": ['position']};
};
Filters["isDropsite"] = function(resourceType){
return {"func": function(ent){
return (ent.resourceDropsiteTypes() && ent.resourceDropsiteTypes().indexOf(resourceType) !== -1
&& ent.foundationProgress() === undefined);
},
"dynamicProperties": []};
};

View File

@ -1,376 +0,0 @@
/**
* Provides an API for the rest of the AI scripts to query the world state at a
* higher level than the raw data.
*/
var GameState = function(ai) {
MemoizeInit(this);
this.ai = ai;
this.timeElapsed = ai.timeElapsed;
this.templates = ai.templates;
this.entities = ai.entities;
this.player = ai.player;
this.playerData = ai.playerData;
this.buildingsBuilt = 0;
if (!this.ai._gameStateStore){
this.ai._gameStateStore = {};
}
this.store = this.ai._gameStateStore;
this.cellSize = 4; // Size of each map tile
this.turnCache = {};
};
GameState.prototype.updatingCollection = function(id, filter, collection){
if (!this.store[id]){
this.store[id] = collection.filter(filter);
this.store[id].registerUpdates();
}
return this.store[id];
};
GameState.prototype.getTimeElapsed = function() {
return this.timeElapsed;
};
GameState.prototype.getTemplate = function(type) {
if (!this.templates[type]){
return null;
}
return new EntityTemplate(this.templates[type]);
};
GameState.prototype.applyCiv = function(str) {
return str.replace(/\{civ\}/g, this.playerData.civ);
};
/**
* @returns {Resources}
*/
GameState.prototype.getResources = function() {
return new Resources(this.playerData.resourceCounts);
};
GameState.prototype.getMap = function() {
return this.ai.passabilityMap;
};
GameState.prototype.getTerritoryMap = function() {
return Map.createTerritoryMap(this);
};
GameState.prototype.getPopulation = function() {
return this.playerData.popCount;
};
GameState.prototype.getPopulationLimit = function() {
return this.playerData.popLimit;
};
GameState.prototype.getPopulationMax = function() {
return this.playerData.popMax;
};
GameState.prototype.getPassabilityClassMask = function(name) {
if (!(name in this.ai.passabilityClasses)){
error("Tried to use invalid passability class name '" + name + "'");
}
return this.ai.passabilityClasses[name];
};
GameState.prototype.getPlayerID = function() {
return this.player;
};
GameState.prototype.isPlayerAlly = function(id) {
return this.playerData.isAlly[id];
};
GameState.prototype.isPlayerEnemy = function(id) {
return this.playerData.isEnemy[id];
};
GameState.prototype.getEnemies = function(){
var ret = [];
for (var i in this.playerData.isEnemy){
if (this.playerData.isEnemy[i]){
ret.push(i);
}
}
return ret;
};
GameState.prototype.isEntityAlly = function(ent) {
if (ent && ent.owner && (typeof ent.owner) === "function"){
return this.playerData.isAlly[ent.owner()];
} else if (ent && ent.owner){
return this.playerData.isAlly[ent.owner];
}
return false;
};
GameState.prototype.isEntityEnemy = function(ent) {
if (ent && ent.owner && (typeof ent.owner) === "function"){
return this.playerData.isEnemy[ent.owner()];
} else if (ent && ent.owner){
return this.playerData.isEnemy[ent.owner];
}
return false;
};
GameState.prototype.isEntityOwn = function(ent) {
if (ent && ent.owner && (typeof ent.owner) === "function"){
return ent.owner() == this.player;
} else if (ent && ent.owner){
return ent.owner == this.player;
}
return false;
};
GameState.prototype.getOwnEntities = function() {
if (!this.store.ownEntities){
this.store.ownEntities = this.getEntities().filter(Filters.byOwner(this.player));
this.store.ownEntities.registerUpdates();
}
return this.store.ownEntities;
};
GameState.prototype.getEnemyEntities = function() {
var diplomacyChange = false;
var enemies = this.getEnemies();
if (this.store.enemies){
if (this.store.enemies.length != enemies.length){
diplomacyChange = true;
}else{
for (var i = 0; i < enemies.length; i++){
if (enemies[i] !== this.store.enemies[i]){
diplomacyChange = true;
}
}
}
}
if (diplomacyChange || !this.store.enemyEntities){
var filter = Filters.byOwners(enemies);
this.store.enemyEntities = this.getEntities().filter(filter);
this.store.enemyEntities.registerUpdates();
this.store.enemies = enemies;
}
return this.store.enemyEntities;
};
GameState.prototype.getEntities = function() {
return this.entities;
};
GameState.prototype.getEntityById = function(id){
if (this.entities._entities[id]) {
return this.entities._entities[id];
}else{
//debug("Entity " + id + " requested does not exist");
}
return undefined;
};
GameState.prototype.getOwnEntitiesByMetadata = function(key, value){
if (!this.store[key + "-" + value]){
var filter = Filters.byMetadata(key, value);
this.store[key + "-" + value] = this.getOwnEntities().filter(filter);
this.store[key + "-" + value].registerUpdates();
}
return this.store[key + "-" + value];
};
GameState.prototype.getOwnEntitiesByRole = function(role){
return this.getOwnEntitiesByMetadata("role", role);
};
// TODO: fix this so it picks up not in use training stuff
GameState.prototype.getOwnTrainingFacilities = function(){
return this.updatingCollection("own-training-facilities", Filters.byTrainingQueue(), this.getOwnEntities());
};
GameState.prototype.getOwnEntitiesByType = function(type){
var filter = Filters.byType(type);
return this.updatingCollection("own-by-type-" + type, filter, this.getOwnEntities());
};
GameState.prototype.countEntitiesByType = function(type) {
return this.getOwnEntitiesByType(type).length;
};
GameState.prototype.countEntitiesAndQueuedByType = function(type) {
var count = this.countEntitiesByType(type);
// Count building foundations
count += this.countEntitiesByType("foundation|" + type);
// Count animal resources
count += this.countEntitiesByType("resource|" + type);
// Count entities in building production queues
this.getOwnTrainingFacilities().forEach(function(ent){
ent.trainingQueue().forEach(function(item) {
if (item.template == type){
count += item.count;
}
});
});
return count;
};
GameState.prototype.countFoundationsWithType = function(type) {
var foundationType = "foundation|" + type;
var count = 0;
this.getOwnEntities().forEach(function(ent) {
var t = ent.templateName();
if (t == foundationType)
++count;
});
return count;
};
GameState.prototype.countOwnEntitiesByRole = function(role) {
return this.getOwnEntitiesByRole(role).length;
};
GameState.prototype.countOwnEntitiesAndQueuedWithRole = function(role) {
var count = this.countOwnEntitiesByRole(role);
// Count entities in building production queues
this.getOwnTrainingFacilities().forEach(function(ent) {
ent.trainingQueue().forEach(function(item) {
if (item.metadata && item.metadata.role == role)
count += item.count;
});
});
return count;
};
GameState.prototype.countOwnQueuedEntitiesWithMetadata = function(data, value) {
// Count entities in building production queues
var count = 0;
this.getOwnTrainingFacilities().forEach(function(ent) {
ent.trainingQueue().forEach(function(item) {
if (item.metadata && item.metadata[data] && item.metadata[data] == value)
count += item.count;
});
});
return count;
};
/**
* Find buildings that are capable of training the given unit type, and aren't
* already too busy.
*/
GameState.prototype.findTrainers = function(template) {
var maxQueueLength = 2; // avoid tying up resources in giant training queues
return this.getOwnTrainingFacilities().filter(function(ent) {
var trainable = ent.trainableEntities();
if (!trainable || trainable.indexOf(template) == -1)
return false;
var queue = ent.trainingQueue();
if (queue) {
if (queue.length >= maxQueueLength)
return false;
}
return true;
});
};
/**
* Find units that are capable of constructing the given building type.
*/
GameState.prototype.findBuilders = function(template) {
return this.getOwnEntities().filter(function(ent) {
var buildable = ent.buildableEntities();
if (!buildable || buildable.indexOf(template) == -1)
return false;
return true;
});
};
GameState.prototype.getOwnFoundations = function() {
return this.updatingCollection("ownFoundations", Filters.isFoundation(), this.getOwnEntities());
};
GameState.prototype.getOwnDropsites = function(resource){
return this.updatingCollection("dropsite-own-" + resource, Filters.isDropsite(resource), this.getOwnEntities());
};
GameState.prototype.getResourceSupplies = function(resource){
return this.updatingCollection("resource-" + resource, Filters.byResource(resource), this.getEntities());
};
GameState.prototype.getEntityLimits = function() {
return this.playerData.entityLimits;
};
GameState.prototype.getEntityCounts = function() {
return this.playerData.entityCounts;
};
// Checks whether the maximum number of buildings have been constructed for a certain catergory
GameState.prototype.isEntityLimitReached = function(category) {
if(this.playerData.entityLimits[category] === undefined || this.playerData.entityCounts[category] === undefined)
return false;
if(this.playerData.entityLimits[category].LimitsPerCivCentre != undefined)
return (this.playerData.entityCounts[category] >=
this.playerData.entityCounts["CivilCentre"] * this.playerData.entityLimits[category].LimitPerCivCentre);
else
return (this.playerData.entityCounts[category] >= this.playerData.entityLimits[category]);
};
GameState.prototype.findTrainableUnits = function(classes){
var allTrainable = [];
this.getOwnEntities().forEach(function(ent) {
var trainable = ent.trainableEntities();
for (var i in trainable){
if (allTrainable.indexOf(trainable[i]) === -1){
allTrainable.push(trainable[i]);
}
}
});
var ret = [];
for (var i in allTrainable) {
var template = this.getTemplate(allTrainable[i]);
var okay = true;
for (o in classes)
if (!template.hasClass(classes[o]))
okay = false;
if (template.hasClass("Hero")) // disabling heroes for now
okay = false;
if (okay)
ret.push( [allTrainable[i], template] );
}
return ret;
};
// defcon utilities
GameState.prototype.timeSinceDefconChange = function() {
return this.getTimeElapsed()-this.ai.defconChangeTime;
};
GameState.prototype.setDefcon = function(level,force) {
if (this.ai.defcon >= level || force) {
this.ai.defcon = level;
this.ai.defconChangeTime = this.getTimeElapsed();
}
};
GameState.prototype.defcon = function() {
return this.ai.defcon;
};

View File

@ -1,31 +0,0 @@
// Decides when to a new house needs to be built
var HousingManager = function() {
};
HousingManager.prototype.buildMoreHouses = function(gameState, queues) {
if (gameState.getTimeElapsed() < 25000)
return;
// temporary 'remaining population space' based check, need to do
// predictive in future
if (gameState.getPopulationLimit() - gameState.getPopulation() < 20
&& gameState.getPopulationLimit() < gameState.getPopulationMax()) {
var numConstructing = gameState.countEntitiesByType(gameState.applyCiv("foundation|structures/{civ}_house"));
var numPlanned = queues.house.totalLength();
var additional = Math.ceil((20 - (gameState.getPopulationLimit() - gameState.getPopulation())) / 10)
- numConstructing - numPlanned;
for ( var i = 0; i < additional; i++) {
queues.house.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_house"));
}
}
};
HousingManager.prototype.update = function(gameState, queues) {
Engine.ProfileStart("housing update");
this.buildMoreHouses(gameState, queues);
Engine.ProfileStop();
};

View File

@ -21,7 +21,7 @@ function Map(gameState, originalMap, actualCopy){
}
Map.prototype.gamePosToMapPos = function(p){
return [Math.round(p[0]/this.cellSize), Math.round(p[1]/this.cellSize)];
return [Math.floor(p[0]/this.cellSize), Math.floor(p[1]/this.cellSize)];
};
Map.prototype.point = function(p){
@ -48,34 +48,78 @@ Map.createObstructionMap = function(gameState, template){
buildEnemy = template.hasBuildTerritory("enemy");
}
var obstructionMask = gameState.getPassabilityClassMask("foundationObstruction");
// Only accept valid land tiles (we don't handle docks yet)
switch(placementType){
case "shore":
obstructionMask |= gameState.getPassabilityClassMask("building-shore");
break;
case "land":
default:
obstructionMask |= gameState.getPassabilityClassMask("building-land");
break;
}
var obstructionMask = gameState.getPassabilityClassMask("foundationObstruction") | gameState.getPassabilityClassMask("building-land");
var playerID = gameState.getPlayerID();
var obstructionTiles = new Uint16Array(passabilityMap.data.length);
for (var i = 0; i < passabilityMap.data.length; ++i)
if (placementType == "shore")
{
var tilePlayer = (territoryMap.data[i] & TERRITORY_PLAYER_MASK);
var invalidTerritory = (
(!buildOwn && tilePlayer == playerID) ||
(!buildAlly && gameState.isPlayerAlly(tilePlayer) && tilePlayer != playerID) ||
(!buildNeutral && tilePlayer == 0) ||
(!buildEnemy && gameState.isPlayerEnemy(tilePlayer) && tilePlayer != 0)
);
var tileAccessible = (gameState.ai.accessibility.map[i] == 1);
obstructionTiles[i] = (!tileAccessible || invalidTerritory || (passabilityMap.data[i] & obstructionMask)) ? 0 : 65535;
// assume Dock, TODO.
var obstructionTiles = new Uint16Array(passabilityMap.data.length);
var okay = false;
for (var x = 0; x < passabilityMap.width; ++x)
{
for (var y = 0; y < passabilityMap.height; ++y)
{
okay = false;
var i = x + y*passabilityMap.width;
var tilePlayer = (territoryMap.data[i] & TERRITORY_PLAYER_MASK);
var positions = [[0,1], [1,1], [1,0], [1,-1], [0,-1], [-1,-1], [-1,0], [-1,1]];
var available = 0;
for each (stuff in positions)
{
var index = x + stuff[0] + (y+stuff[1])*passabilityMap.width;
var index2 = x + stuff[0]*2 + (y+stuff[1]*2)*passabilityMap.width;
var index3 = x + stuff[0]*3 + (y+stuff[1]*3)*passabilityMap.width;
var index4 = x + stuff[0]*4 + (y+stuff[1]*4)*passabilityMap.width;
if ((passabilityMap.data[index] & gameState.getPassabilityClassMask("default")) && gameState.ai.accessibility.getRegionSizei(index) > 500)
if ((passabilityMap.data[index2] & gameState.getPassabilityClassMask("default")) && gameState.ai.accessibility.getRegionSizei(index2) > 500)
if ((passabilityMap.data[index3] & gameState.getPassabilityClassMask("default")) && gameState.ai.accessibility.getRegionSizei(index3) > 500)
if ((passabilityMap.data[index4] & gameState.getPassabilityClassMask("default")) && gameState.ai.accessibility.getRegionSizei(index4) > 500) {
if (available < 2)
available++;
else
okay = true;
}
}
// checking for accessibility: if a neighbor is inaccessible, this is too. If it's not on the same "accessible map" as us, we crash-i~u.
var radius = 3;
for (var xx = -radius;xx <= radius; xx++)
for (var yy = -radius;yy <= radius; yy++)
{
var id = x + xx + (y+yy)*passabilityMap.width;
if (id > 0 && id < passabilityMap.data.length)
if (gameState.ai.terrainAnalyzer.map[id] === 0 || gameState.ai.terrainAnalyzer.map[id] == 30 || gameState.ai.terrainAnalyzer.map[id] == 40)
okay = false;
}
if (gameState.ai.myIndex !== gameState.ai.accessibility.passMap[i])
okay = false;
if (gameState.isPlayerEnemy(tilePlayer) && tilePlayer !== 0)
okay = false;
if ((passabilityMap.data[i] & (gameState.getPassabilityClassMask("building-shore") | gameState.getPassabilityClassMask("default"))))
okay = false;
obstructionTiles[i] = okay ? 65535 : 0;
}
}
} else {
var playerID = PlayerID;
var obstructionTiles = new Uint16Array(passabilityMap.data.length);
for (var i = 0; i < passabilityMap.data.length; ++i)
{
var tilePlayer = (territoryMap.data[i] & TERRITORY_PLAYER_MASK);
var invalidTerritory = (
(!buildOwn && tilePlayer == playerID) ||
(!buildAlly && gameState.isPlayerAlly(tilePlayer) && tilePlayer != playerID) ||
(!buildNeutral && tilePlayer == 0) ||
(!buildEnemy && gameState.isPlayerEnemy(tilePlayer) && tilePlayer != 0)
);
var tileAccessible = (gameState.ai.myIndex === gameState.ai.accessibility.passMap[i]);
if (placementType === "shore")
tileAccessible = true;
obstructionTiles[i] = (!tileAccessible || invalidTerritory || (passabilityMap.data[i] & obstructionMask)) ? 0 : 65535;
}
}
var map = new Map(gameState, obstructionTiles);
if (template && template.buildDistance()){
@ -109,6 +153,7 @@ Map.createTerritoryMap = function(gameState) {
}
return ret;
};
Map.prototype.drawDistance = function(gameState, elements) {
for ( var y = 0; y < this.height; ++y) {
for ( var x = 0; x < this.width; ++x) {
@ -120,7 +165,7 @@ Map.prototype.drawDistance = function(gameState, elements) {
if (dist < minDist)
minDist = dist;
}
this.map[x + y*this.width] = Math.max(1,this.width - minDist);
this.map[x + y*this.width] = Math.max(1,(this.width - minDist)*2.5);
}
}
};
@ -146,6 +191,7 @@ Map.prototype.addInfluence = function(cx, cy, maxDist, strength, type) {
str = +strength;
break;
}
for ( var y = y0; y < y1; ++y) {
for ( var x = x0; x < x1; ++x) {
var dx = x - cx;
@ -342,17 +388,19 @@ Map.prototype.findBestTile = function(radius, obstructionTiles){
};
// Multiplies current map by 3 if in my territory
Map.prototype.multiplyTerritory = function(gameState,map){
Map.prototype.multiplyTerritory = function(gameState,map,evenNeutral){
for (var i = 0; i < this.length; ++i){
if (map.getOwnerIndex(i) === gameState.player)
if (map.getOwnerIndex(i) === PlayerID)
this.map[i] *= 2.5;
else if (map.getOwnerIndex(i) !== PlayerID && (map.getOwnerIndex(i) !== 0 || evenNeutral))
this.map[i] = 0;
}
};
// sets to 0 any enemy territory
Map.prototype.annulateTerritory = function(gameState,map){
Map.prototype.annulateTerritory = function(gameState,map,evenNeutral){
for (var i = 0; i < this.length; ++i){
if (map.getOwnerIndex(i) !== gameState.player && map.getOwnerIndex(i) !== 0)
if (map.getOwnerIndex(i) !== PlayerID && (map.getOwnerIndex(i) !== 0 || evenNeutral))
this.map[i] = 0;
}
};
@ -370,12 +418,29 @@ Map.prototype.add = function(map){
this.map[i] += +map.map[i];
}
};
// add to current map by the parameter map pixelwise with a multiplier
Map.prototype.madd = function(map, multiplier){
for (var i = 0; i < this.length; ++i){
this.map[i] += +map.map[i]*multiplier;
}
};
// add to current map if the map is not null in that point
Map.prototype.addIfNotNull = function(map){
for (var i = 0; i < this.length; ++i){
if (this.map[i] !== 0)
this.map[i] += +map.map[i];
}
};
// add to current map by the parameter map pixelwise
Map.prototype.subtract = function(map){
for (var i = 0; i < this.length; ++i){
this.map[i] += map.map[i];
if (this.map[i] <= 0)
this.map[i] = 0;
this.map[i] = this.map[i] - map.map[i] < 0 ? 0 : this.map[i] - map.map[i];
}
};
// add to current map by the parameter map pixelwise with a multiplier
Map.prototype.subtractMultiplied = function(map,multiple){
for (var i = 0; i < this.length; ++i){
this.map[i] = this.map[i] - map.map[i]*multiple < 0 ? 0 : this.map[i] - map.map[i]*multiple;
}
};

View File

@ -1,14 +1,17 @@
/*
* 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
*
* Military Manager. NOT cleaned up from qBot, many of the functions here are deprecated for functions in attack_plan.js
*/
var MilitaryAttackManager = function() {
this.fortressStartTime = Config.Military.fortressStartTime * 1000;
this.fortressLapseTime = Config.Military.fortressLapseTime * 1000;
this.defenceBuildingTime = Config.Military.defenceBuildingTime * 1000;
this.advancedMilitaryStartTime = Config.Military.advancedMilitaryStartTime * 1000;
this.attackPlansStartTime = Config.Military.attackPlansStartTime * 1000;
this.defenceManager = new Defence();
this.TotalAttackNumber = 0;
this.upcomingAttacks = { "CityAttack" : [] };
this.startedAttacks = { "CityAttack" : [] };
@ -62,9 +65,10 @@ MilitaryAttackManager.prototype.init = function(gameState) {
this.enemyWatchers = {};
this.ennWatcherIndex = [];
for (var i = 1; i <= 8; i++)
if (gameState.player != i && gameState.isPlayerEnemy(i)) {
if (PlayerID != i && gameState.isPlayerEnemy(i)) {
this.enemyWatchers[i] = new enemyWatcher(gameState, i);
this.ennWatcherIndex.push(i);
this.defenceManager.enemyArmy[i] = [];
}
};
@ -154,8 +158,12 @@ MilitaryAttackManager.prototype.findBestTrainableUnit = function(gameState, clas
bTopParam = param[1];
}
if (param[0] == "strength") {
aTopParam += a[1].getMaxStrength() * param[1];
bTopParam += b[1].getMaxStrength() * param[1];
aTopParam += getMaxStrength(a[1]) * param[1];
bTopParam += getMaxStrength(b[1]) * param[1];
}
if (param[0] == "siegeStrength") {
aTopParam += getMaxStrength(a[1], "Structure") * param[1];
bTopParam += getMaxStrength(b[1], "Structure") * param[1];
}
if (param[0] == "speed") {
aTopParam += a[1].walkSpeed() * param[1];
@ -179,8 +187,8 @@ MilitaryAttackManager.prototype.registerSoldiers = function(gameState) {
var self = this;
soldiers.forEach(function(ent) {
ent.setMetadata("role", "military");
ent.setMetadata("military", "unassigned");
ent.setMetadata(PlayerID, "role", "military");
ent.setMetadata(PlayerID, "military", "unassigned");
});
};
@ -207,8 +215,8 @@ MilitaryAttackManager.prototype.getAvailableUnits = function(n, filter) {
units.forEach(function(ent){
ret.push(ent.id());
ent.setMetadata("military", "assigned");
ent.setMetadata("role", "military");
ent.setMetadata(PlayerID, "military", "assigned");
ent.setMetadata(PlayerID, "role", "military");
count++;
if (count >= n) {
return;
@ -219,7 +227,7 @@ MilitaryAttackManager.prototype.getAvailableUnits = function(n, filter) {
// Takes a single unit id, and marks it unassigned
MilitaryAttackManager.prototype.unassignUnit = function(unit){
this.entity(unit).setMetadata("military", "unassigned");
this.entity(unit).setMetadata(PlayerID, "military", "unassigned");
};
// Takes an array of unit id's and marks all of them unassigned
@ -368,15 +376,15 @@ MilitaryAttackManager.prototype.measureEnemyStrength = function(gameState){
// Adds towers to the defenceBuilding queue
MilitaryAttackManager.prototype.buildDefences = function(gameState, queues){
if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv('structures/{civ}_defense_tower'))
+ queues.defenceBuilding.totalLength() < gameState.getEntityLimits()["DefenseTower"]) {
+ queues.defenceBuilding.totalLength() < gameState.getEntityLimits()["DefenseTower"]
&& gameState.currentPhase() > 1) {
gameState.getOwnEntities().forEach(function(dropsiteEnt) {
if (dropsiteEnt.resourceDropsiteTypes() && dropsiteEnt.getMetadata("defenseTower") !== true){
if (dropsiteEnt.resourceDropsiteTypes() && dropsiteEnt.getMetadata(PlayerID, "defenseTower") !== true){
var position = dropsiteEnt.position();
if (position){
queues.defenceBuilding.addItem(new BuildingConstructionPlan(gameState, 'structures/{civ}_defense_tower', position));
}
dropsiteEnt.setMetadata("defenseTower", true);
dropsiteEnt.setMetadata(PlayerID, "defenseTower", true);
}
});
}
@ -386,8 +394,9 @@ MilitaryAttackManager.prototype.buildDefences = function(gameState, queues){
numFortresses += gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bFort[i]));
}
if (numFortresses + queues.defenceBuilding.totalLength() < 1){ //gameState.getEntityLimits()["Fortress"]) {
if (gameState.getTimeElapsed() > 840 * 1000 + numFortresses * 300 * 1000){
if (numFortresses + queues.defenceBuilding.totalLength() < 1 && gameState.currentPhase() > 2)
{
if (gameState.getTimeElapsed() > this.fortressStartTime + numFortresses * this.fortressLapseTime){
if (gameState.ai.pathsToMe && gameState.ai.pathsToMe.length > 0){
var position = gameState.ai.pathsToMe.shift();
// TODO: pick a fort randomly from the list.
@ -403,14 +412,14 @@ MilitaryAttackManager.prototype.constructTrainingBuildings = function(gameState,
// Build more military buildings
// TODO: make military building better
Engine.ProfileStart("Build buildings");
if (gameState.countEntitiesByType(gameState.applyCiv("units/{civ}_support_female_citizen")) > 25) {
if (gameState.countEntitiesByType(gameState.applyCiv("units/{civ}_support_female_citizen")) > 25 && gameState.currentPhase() > 1) {
if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bModerate[0]))
+ queues.militaryBuilding.totalLength() < 1) {
queues.militaryBuilding.addItem(new BuildingConstructionPlan(gameState, this.bModerate[0]));
}
}
//build advanced military buildings
if (gameState.getTimeElapsed() > 720*1000){
if (gameState.getTimeElapsed() > this.advancedMilitaryStartTime && gameState.currentPhase() > 2){
if (queues.militaryBuilding.totalLength() === 0){
var inConst = 0;
for (var i in this.bAdvanced)
@ -425,36 +434,6 @@ MilitaryAttackManager.prototype.constructTrainingBuildings = function(gameState,
}
Engine.ProfileStop();
};
MilitaryAttackManager.prototype.trainMilitaryUnits = function(gameState, queues){
Engine.ProfileStart("Train Units");
// Continually try training new units, in batches of 5
if (queues.citizenSoldier.length() < 6) {
var newUnit = this.findBestNewUnit(gameState, queues.citizenSoldier, "citizenSoldier");
if (newUnit){
queues.citizenSoldier.addItem(new UnitTrainingPlan(gameState, newUnit, {
"role" : "soldier"
}, 5));
}
}
if (queues.advancedSoldier.length() < 2) {
var newUnit = this.findBestNewUnit(gameState, queues.advancedSoldier, "advanced");
if (newUnit){
queues.advancedSoldier.addItem(new UnitTrainingPlan(gameState, newUnit, {
"role" : "soldier"
}, 5));
}
}
if (queues.siege.length() < 4) {
var newUnit = this.findBestNewUnit(gameState, queues.siege, "siege");
if (newUnit){
queues.siege.addItem(new UnitTrainingPlan(gameState, newUnit, {
"role" : "soldier"
}, 2));
}
}
Engine.ProfileStop();
};
MilitaryAttackManager.prototype.pausePlan = function(gameState, planName) {
for (attackType in this.upcomingAttacks) {
for (i in this.upcomingAttacks[attackType]) {
@ -496,20 +475,16 @@ MilitaryAttackManager.prototype.update = function(gameState, queues, events) {
this.gameState = gameState;
//this.registerSoldiers(gameState);
//this.trainMilitaryUnits(gameState, queues);
Engine.ProfileStart("Constructing military buildings and building defences");
this.constructTrainingBuildings(gameState, queues);
if(gameState.getTimeElapsed() > 300*1000)
if(gameState.getTimeElapsed() > this.defenceBuildingTime)
this.buildDefences(gameState, queues);
Engine.ProfileStop();
Engine.ProfileStart("Updating enemy watchers");
this.enemyWatchers[ this.ennWatcherIndex[gameState.ai.playedTurn % this.ennWatcherIndex.length] ].detectArmies(gameState,this);
Engine.ProfileStop();
//Engine.ProfileStart("Updating enemy watchers");
//this.enemyWatchers[ this.ennWatcherIndex[gameState.ai.playedTurn % this.ennWatcherIndex.length] ].detectArmies(gameState,this);
//Engine.ProfileStop();
this.defenceManager.update(gameState, events, this);
@ -543,7 +518,7 @@ MilitaryAttackManager.prototype.update = function(gameState, queues, events) {
//if (gameState.defcon() >= 3) {
if (1) {
for (attackType in this.upcomingAttacks) {
for (var i = 0; i < this.upcomingAttacks[attackType].length; ++i) {
for (var i = 0;i < this.upcomingAttacks[attackType].length; ++i) {
var attack = this.upcomingAttacks[attackType][i];
@ -581,7 +556,7 @@ MilitaryAttackManager.prototype.update = function(gameState, queues, events) {
// this.abortedAttacks[gameState.ai.mainCounter % this.abortedAttacks.length].releaseAnyUnit(gameState);
}
for (attackType in this.startedAttacks) {
for (i in this.startedAttacks[attackType]) {
for (var i = 0; i < this.startedAttacks[attackType].length; ++i) {
var attack = this.startedAttacks[attackType][i];
// okay so then we'll update the raid.
var remaining = attack.update(gameState,this,events);
@ -595,13 +570,14 @@ MilitaryAttackManager.prototype.update = function(gameState, queues, events) {
}
}
// creating plans after updating because an aborted plan might be reused in that case.
if (gameState.countEntitiesByType(gameState.applyCiv(this.bModerate[0])) >= 1 && !this.attackPlansEncounteredWater) {
if (this.upcomingAttacks["CityAttack"].length == 0 && gameState.getTimeElapsed() < 25*60000) {
if (gameState.countEntitiesByType(gameState.applyCiv(this.bModerate[0])) >= 1 && !this.attackPlansEncounteredWater
&& gameState.getTimeElapsed() > this.attackPlansStartTime) {
if (this.upcomingAttacks["CityAttack"].length == 0 && (gameState.getTimeElapsed() < 25*60000 || Config.difficulty < 2)) {
var Lalala = new CityAttack(gameState, this,this.TotalAttackNumber, -1);
debug ("Military Manager: Creating the plan " +this.TotalAttackNumber);
this.TotalAttackNumber++;
this.upcomingAttacks["CityAttack"].push(Lalala);
} else if (this.upcomingAttacks["CityAttack"].length == 0) {
} else if (this.upcomingAttacks["CityAttack"].length == 0 && Config.difficulty !== 0) {
var Lalala = new CityAttack(gameState, this,this.TotalAttackNumber, -1, "superSized");
debug ("Military Manager: Creating the super sized plan " +this.TotalAttackNumber);
this.TotalAttackNumber++;
@ -611,7 +587,7 @@ MilitaryAttackManager.prototype.update = function(gameState, queues, events) {
/*
if (this.HarassRaiding && this.preparingRaidNumber + this.startedRaidNumber < 1 && gameState.getTimeElapsed() < 780000) {
var Lalala = new CityAttack(gameState, this,this.totalStartedAttackNumber, -1, "harass_raid");
if (!Lalala.createSupportPlans(gameState, this, queues.advancedSoldier)) {
if (!Lalala.createSupportPlans(gameState, this, )) {
debug ("Military Manager: harrassing plan not a valid option");
this.HarassRaiding = false;
} else {
@ -632,7 +608,7 @@ MilitaryAttackManager.prototype.update = function(gameState, queues, events) {
// Set unassigned to be workers TODO: fix this so it doesn't scan all units every time
this.getUnassignedUnits(gameState).forEach(function(ent){
if (self.getSoldierType(ent) === "citizenSoldier"){
ent.setMetadata("role", "worker");
ent.setMetadata(PlayerID, "role", "worker");
}
});
Engine.ProfileStop();*/

View File

@ -20,7 +20,9 @@ BuildingConstructionPlan.prototype.canExecute = function(gameState) {
}
// TODO: verify numeric limits etc
if (this.template.requiredTech() && !gameState.isResearched(this.template.requiredTech()))
return false;
var builders = gameState.findBuilders(this.type);
return (builders.length != 0);
@ -40,7 +42,14 @@ BuildingConstructionPlan.prototype.execute = function(gameState) {
return;
}
builders[0].construct(this.type, pos.x, pos.z, pos.angle);
if (gameState.getTemplate(this.type).buildCategory() === "Dock")
{
for (var angle = 0; angle < Math.PI * 2; angle += Math.PI/4)
{
builders[0].construct(this.type, pos.x, pos.z, angle);
}
} else
builders[0].construct(this.type, pos.x, pos.z, pos.angle);
};
BuildingConstructionPlan.prototype.getCost = function() {
@ -56,10 +65,11 @@ BuildingConstructionPlan.prototype.findGoodPosition = function(gameState) {
var obstructionMap = Map.createObstructionMap(gameState,template);
///obstructionMap.dumpIm("obstructions.png");
//obstructionMap.dumpIm(template.buildCategory() + "_obstructions.png");
if (template.buildCategory() !== "Dock")
obstructionMap.expandInfluences();
obstructionMap.expandInfluences();
// Compute each tile's closeness to friendly structures:
var friendlyTiles = new Map(gameState);
@ -71,7 +81,7 @@ BuildingConstructionPlan.prototype.findGoodPosition = function(gameState) {
var x = Math.round(this.position[0] / cellSize);
var z = Math.round(this.position[1] / cellSize);
friendlyTiles.addInfluence(x, z, 200);
}else{
} else {
// No position was specified so try and find a sensible place to build
gameState.getOwnEntities().forEach(function(ent) {
if (ent.hasClass("Structure")) {
@ -118,25 +128,26 @@ BuildingConstructionPlan.prototype.findGoodPosition = function(gameState) {
});
}
//friendlyTiles.dumpIm("Building " +gameState.getTimeElapsed() + ".png", 200);
//friendlyTiles.dumpIm(template.buildCategory() + "_" +gameState.getTimeElapsed() + ".png", 200);
// Find target building's approximate obstruction radius, and expand by a bit to make sure we're not too close, this
// allows room for units to walk between buildings.
// note: not for houses and dropsites who ought to be closer to either each other or a resource.
// also not for fields who can be stacked quite a bit
var radius = 0;
if (template.genericName() == "Field")
var radius = Math.ceil(template.obstructionRadius() / cellSize) - 0.7;
radius = Math.ceil(template.obstructionRadius() / cellSize) - 0.7;
else if (template.buildCategory() === "Dock")
var radius = 0;
radius = 1;//Math.floor(template.obstructionRadius() / cellSize);
else if (template.genericName() != "House" && !template.hasClass("DropsiteWood") && !template.hasClass("DropsiteStone") && !template.hasClass("DropsiteMetal"))
var radius = Math.ceil(template.obstructionRadius() / cellSize) + 1;
radius = Math.ceil(template.obstructionRadius() / cellSize) + 1;
else
var radius = Math.ceil(template.obstructionRadius() / cellSize);
radius = Math.ceil(template.obstructionRadius() / cellSize);
// further contract cause walls
if (gameState.playerData.civ == "iber")
radius *= 0.95;
// Note: I'm currently destroying them so that doesn't matter.
//if (gameState.playerData.civ == "iber")
// radius *= 0.95;
// Find the best non-obstructed
if (template.genericName() == "House" && !alreadyHasHouses) {
@ -144,24 +155,26 @@ BuildingConstructionPlan.prototype.findGoodPosition = function(gameState) {
var bestTile = friendlyTiles.findBestTile(10, obstructionMap);
var bestIdx = bestTile[0];
var bestVal = bestTile[1];
} else if (template.genericName() == "House") {
radius *= 0.9;
}
if (bestVal === undefined || bestVal === -1) {
var bestTile = friendlyTiles.findBestTile(radius, obstructionMap);
var bestIdx = bestTile[0];
var bestVal = bestTile[1];
}
if (bestVal === -1){
if (bestVal === -1) {
return false;
}
//friendlyTiles.setInfluence((bestIdx % friendlyTiles.width), Math.floor(bestIdx / friendlyTiles.width), 1, 200);
//friendlyTiles.dumpIm(template.buildCategory() + "_" +gameState.getTimeElapsed() + ".png", 200);
var x = ((bestIdx % friendlyTiles.width) + 0.5) * cellSize;
var z = (Math.floor(bestIdx / friendlyTiles.width) + 0.5) * cellSize;
// default angle
var angle = 3*Math.PI/4;
return {
"x" : x,
"z" : z,

View File

@ -0,0 +1,47 @@
var ResearchPlan = function(gameState, type) {
this.type = type;
this.template = gameState.getTemplate(this.type);
if (!this.template || this.template.researchTime === undefined) {
this.invalidTemplate = true;
this.template = undefined;
debug ("Invalid research");
return false;
}
this.category = "technology";
this.cost = new Resources(this.template.cost(),0);
this.number = 1; // Obligatory for compatibility
return true;
};
ResearchPlan.prototype.canExecute = function(gameState) {
if (this.invalidTemplate)
return false;
// also checks canResearch
return (gameState.findResearchers(this.type).length !== 0);
};
ResearchPlan.prototype.execute = function(gameState) {
var self = this;
//debug ("Starting the research plan for " + this.type);
var trainers = gameState.findResearchers(this.type).toEntityArray();
//for (i in trainers)
// warn (this.type + " - " +trainers[i].genericName());
// Prefer training buildings with short queues
// (TODO: this should also account for units added to the queue by
// plans that have already been executed this turn)
if (trainers.length > 0){
trainers.sort(function(a, b) {
return (a.trainingQueueTime() - b.trainingQueueTime());
});
trainers[0].research(this.type);
}
};
ResearchPlan.prototype.getCost = function(){
return this.cost;
};

View File

@ -1,4 +1,4 @@
var UnitTrainingPlan = function(gameState, type, metadata, number) {
var UnitTrainingPlan = function(gameState, type, metadata, number, maxMerge) {
this.type = gameState.applyCiv(type);
this.metadata = metadata;
@ -15,6 +15,10 @@ var UnitTrainingPlan = function(gameState, type, metadata, number) {
}else{
this.number = number;
}
if (!maxMerge)
this.maxMerge = 5;
else
this.maxMerge = maxMerge;
};
UnitTrainingPlan.prototype.canExecute = function(gameState) {
@ -62,6 +66,8 @@ UnitTrainingPlan.prototype.getCost = function(){
return multCost;
};
UnitTrainingPlan.prototype.addItem = function(){
this.number += 1;
UnitTrainingPlan.prototype.addItem = function(amount){
if (amount === undefined)
amount = 1;
this.number += amount;
};

View File

@ -1,4 +1,3 @@
function QBotAI(settings) {
BaseAI.call(this, settings);
@ -8,8 +7,7 @@ function QBotAI(settings) {
this.modules = {
"economy": new EconomyManager(),
"military": new MilitaryAttackManager(),
"housing": new HousingManager()
"military": new MilitaryAttackManager()
};
// this.queues can only be modified by the queue manager or things will go awry.
@ -18,14 +16,15 @@ function QBotAI(settings) {
citizenSoldier : new Queue(),
villager : new Queue(),
economicBuilding : new Queue(),
dropsites : new Queue(),
field : new Queue(),
advancedSoldier : new Queue(),
siege : new Queue(),
militaryBuilding : new Queue(),
defenceBuilding : new Queue(),
civilCentre: new Queue()
civilCentre: new Queue(),
majorTech: new Queue(),
minorTech: new Queue()
};
this.productionQueues = [];
this.priorities = Config.priorities;
@ -36,65 +35,101 @@ function QBotAI(settings) {
this.savedEvents = [];
this.waterMap = false;
this.defcon = 5;
this.defconChangeTime = -10000000;
}
QBotAI.prototype = new BaseAI();
//Some modules need the gameState to fully initialise
QBotAI.prototype.runInit = function(gameState){
if (this.firstTime){
for (var i in this.modules){
if (this.modules[i].init){
this.modules[i].init(gameState);
}
}
this.timer = new Timer();
this.firstTime = false;
var myKeyEntities = gameState.getOwnEntities().filter(function(ent) {
// Bit of a hack: I run the pathfinder early, before the map apears, to avoid a sometimes substantial lag right at the start.
QBotAI.prototype.InitShared = function(gameState, sharedScript) {
var ents = gameState.getEntities().filter(Filters.byOwner(PlayerID));
var myKeyEntities = ents.filter(function(ent) {
return ent.hasClass("CivCentre");
});
if (myKeyEntities.length == 0){
myKeyEntities = gameState.getOwnEntities();
}
// disband the walls themselves
if (gameState.playerData.civ == "iber") {
gameState.getOwnEntities().filter(function(ent) { //}){
if (ent.hasClass("StoneWall") && !ent.hasClass("Tower"))
ent.disband();
});
}
var filter = Filters.byClass("CivCentre");
var enemyKeyEntities = gameState.getEnemyEntities().filter(filter);
if (enemyKeyEntities.length == 0){
enemyKeyEntities = gameState.getEnemyEntities();
}
this.accessibility = new Accessibility(gameState, myKeyEntities.toEntityArray()[0].position());
if (enemyKeyEntities.length == 0)
return;
var pathFinder = new PathFinder(gameState);
this.pathsToMe = pathFinder.getPaths(enemyKeyEntities.toEntityArray()[0].position(), myKeyEntities.toEntityArray()[0].position(), 'entryPoints');
});
this.templateManager = new TemplateManager(gameState);
this.distanceFromMeMap = new Map(gameState);
this.distanceFromMeMap.drawDistance(gameState,myKeyEntities.toEntityArray());
//this.distanceFromMeMap.dumpIm("dumping.png", this.distanceFromMeMap.width*1.5);
if (myKeyEntities.length == 0){
myKeyEntities = gameState.getEntities().filter(Filters.byOwner(PlayerID));
}
var filter = Filters.byClass("CivCentre");
var enemyKeyEntities = gameState.getEntities().filter(Filters.not(Filters.byOwner(PlayerID))).filter(filter);
if (enemyKeyEntities.length == 0){
enemyKeyEntities = gameState.getEntities().filter(Filters.not(Filters.byOwner(PlayerID)));
}
this.pathFinder = new aStarPath(gameState, false, true);
this.pathsToMe = [];
this.pathInfo = { "angle" : 0, "needboat" : true, "mkeyPos" : myKeyEntities.toEntityArray()[0].position(), "ekeyPos" : enemyKeyEntities.toEntityArray()[0].position() };
var pos = [this.pathInfo.mkeyPos[0] + 140*Math.cos(this.pathInfo.angle),this.pathInfo.mkeyPos[1] + 140*Math.sin(this.pathInfo.angle)];
var path = this.pathFinder.getPath(this.pathInfo.ekeyPos, pos, 3, 3);// uncomment for debug:*/, 300000, gameState);
if (path !== undefined && path[1] !== undefined && path[1] == false) {
// path is viable and doesn't require boating.
// blackzone the last two waypoints.
this.pathFinder.markImpassableArea(path[0][0][0],path[0][0][1],20);
this.pathsToMe.push(path[0][0][0]);
this.pathInfo.needboat = false;
}
this.pathInfo.angle += Math.PI/3.0;
}
//Some modules need the gameState to fully initialise
QBotAI.prototype.runInit = function(gameState, events){
for (var i in this.modules){
if (this.modules[i].init){
this.modules[i].init(gameState, events);
}
}
debug ("inited");
this.timer = new Timer();
var ents = gameState.getOwnEntities();
var myKeyEntities = gameState.getOwnEntities().filter(function(ent) {
return ent.hasClass("CivCentre");
});
if (myKeyEntities.length == 0){
myKeyEntities = gameState.getOwnEntities();
}
// disband the walls themselves
if (gameState.playerData.civ == "iber") {
gameState.getOwnEntities().filter(function(ent) { //}){
if (ent.hasClass("StoneWall") && !ent.hasClass("Tower"))
ent.destroy();
});
}
var filter = Filters.byClass("CivCentre");
var enemyKeyEntities = gameState.getEnemyEntities().filter(filter);
if (enemyKeyEntities.length == 0){
enemyKeyEntities = gameState.getEnemyEntities();
}
//this.accessibility = new Accessibility(gameState, myKeyEntities.toEntityArray()[0].position());
this.myIndex = this.accessibility.getAccessValue(myKeyEntities.toEntityArray()[0].position());
if (enemyKeyEntities.length == 0)
return;
this.templateManager = new TemplateManager(gameState);
this.distanceFromMeMap = new Map(gameState);
this.distanceFromMeMap.drawDistance(gameState,myKeyEntities.toEntityArray());
//this.distanceFromMeMap.dumpIm("dumping.png", this.distanceFromMeMap.width*1.5);
};
QBotAI.prototype.OnUpdate = function() {
QBotAI.prototype.OnUpdate = function(sharedScript) {
if (this.gameFinished){
return;
}
@ -103,39 +138,91 @@ QBotAI.prototype.OnUpdate = function() {
this.savedEvents = this.savedEvents.concat(this.events);
}
if (this.turn == 0) {
debug ("Initializing");
var gameState = new GameState(this);
this.runInit(gameState);
}
// Run the update every n turns, offset depending on player ID to balance
// the load
if ((this.turn + this.player) % 10 == 0) {
Engine.ProfileStart("qBot-xp");
// Run the update every n turns, offset depending on player ID to balance the load
// this also means that init at turn 0 always happen and is never run in parallel to the first played turn so I use an else if.
if (this.turn == 0) {
//Engine.DumpImage("terrain.png", this.accessibility.map, this.accessibility.width,this.accessibility.height,255)
//Engine.DumpImage("Access.png", this.accessibility.passMap, this.accessibility.width,this.accessibility.height,this.accessibility.regionID+1)
var gameState = sharedScript.gameState[PlayerID];
gameState.ai = this;
this.runInit(gameState, this.savedEvents);
// Delete creation events
delete this.savedEvents;
this.savedEvents = [];
} else if ((this.turn + this.player) % 10 == 0) {
Engine.ProfileStart("Aegis bot");
this.playedTurn++;
var gameState = new GameState(this);
var gameState = sharedScript.gameState[PlayerID];
gameState.ai = this;
if (gameState.getOwnEntities().length === 0){
Engine.ProfileStop();
return; // With no entities to control the AI cannot do anything
}
if (this.pathInfo !== undefined)
{
var pos = [this.pathInfo.mkeyPos[0] + 140*Math.cos(this.pathInfo.angle),this.pathInfo.mkeyPos[1] + 140*Math.sin(this.pathInfo.angle)];
var path = this.pathFinder.getPath(this.pathInfo.ekeyPos, pos, 6, 6);// uncomment for debug:*/, 300000, gameState);
if (path !== undefined && path[1] !== undefined && path[1] == false) {
// path is viable and doesn't require boating.
// blackzone the last two waypoints.
this.pathFinder.markImpassableArea(path[0][0][0],path[0][0][1],20);
this.pathsToMe.push(path[0][0][0]);
this.pathInfo.needboat = false;
}
this.pathInfo.angle += Math.PI/3.0;
if (this.pathInfo.angle > Math.PI*2.0)
{
if (this.pathInfo.needboat)
{
debug ("Assuming this is a water map");
this.waterMap = true;
}
delete this.pathFinder;
delete this.pathInfo;
}
}
// try going up phases.
if (gameState.canResearch("phase_town",true) && gameState.getTimeElapsed() > (Config.Economy.townPhase*1000)
&& gameState.findResearchers("phase_town").length != 0 && this.queues.majorTech.length() === 0) {
this.queues.majorTech.addItem(new ResearchPlan(gameState, "phase_town"));
debug ("Trying to reach town phase");
} else if (gameState.canResearch("phase_city_generic",true) && gameState.getTimeElapsed() > (Config.Economy.cityPhase*1000)
&& gameState.findResearchers("phase_city_generic").length != 0 && this.queues.majorTech.length() === 0) {
debug ("Trying to reach city phase");
this.queues.majorTech.addItem(new ResearchPlan(gameState, "phase_city_generic"));
}
// defcon cooldown
if (this.defcon < 5 && gameState.timeSinceDefconChange() > 20000)
{
this.defcon++;
this.runInit(gameState);
debug ("updefconing to " +this.defcon);
}
for (var i in this.modules){
this.modules[i].update(gameState, this.queues, this.savedEvents);
}
//this.updateDynamicPriorities(gameState, this.queues);
this.queueManager.update(gameState);
//if (this.playedTurn % 20 === 0)
// this.queueManager.printQueues(gameState);
// Generate some entropy in the random numbers (against humans) until the engine gets random initialised numbers
// TODO: remove this when the engine gives a random seed
var n = this.savedEvents.length % 29;
@ -152,28 +239,6 @@ QBotAI.prototype.OnUpdate = function() {
this.turn++;
};
QBotAI.prototype.updateDynamicPriorities = function(gameState, queues){
// Dynamically change priorities
Engine.ProfileStart("Change Priorities");
var females = gameState.countEntitiesByType(gameState.applyCiv("units/{civ}_support_female_citizen"));
var femalesTarget = this.modules["economy"].targetNumWorkers;
var enemyStrength = this.modules["military"].measureEnemyStrength(gameState);
var availableStrength = this.modules["military"].measureAvailableStrength();
var additionalPriority = (enemyStrength - availableStrength) * 5;
additionalPriority = Math.min(Math.max(additionalPriority, -50), 220);
var advancedProportion = (availableStrength / 40) * (females/femalesTarget);
advancedProportion = Math.min(advancedProportion, 0.7);
this.priorities.advancedSoldier = advancedProportion * (150 + additionalPriority) + 1;
if (females/femalesTarget > 0.7){
this.priorities.defenceBuilding = 70;
}
Engine.ProfileStop();
};
// TODO: Remove override when the whole AI state is serialised
QBotAI.prototype.Deserialize = function(data)
{
@ -183,9 +248,8 @@ QBotAI.prototype.Deserialize = function(data)
// Override the default serializer
QBotAI.prototype.Serialize = function()
{
var ret = BaseAI.prototype.Serialize.call(this);
ret._entityMetadata = {};
return ret;
//var ret = BaseAI.prototype.Serialize.call(this);
return {};
};
function debug(output){

View File

@ -18,21 +18,37 @@ var QueueManager = function(queues, priorities) {
this.queues = queues;
this.priorities = priorities;
this.account = {};
this.accounts = {};
// the sorting would need to be updated on priority change but there is currently none.
var self = this;
this.queueArrays = [];
for (var p in this.queues) {
this.account[p] = 0;
this.accounts[p] = new Resources();
this.queueArrays.push([p,this.queues[p]]);
}
this.queueArrays.sort(function (a,b) { return (self.priorities[b[0]] - self.priorities[a[0]]) });
this.curItemQueue = [];
};
QueueManager.prototype.getAvailableResources = function(gameState) {
QueueManager.prototype.getAvailableResources = function(gameState, noAccounts) {
var resources = gameState.getResources();
if (Config.difficulty == 1)
resources.multiply(0.75);
else if (Config.difficulty == 1)
resources.multiply(0.5);
if (noAccounts)
return resources;
for (var key in this.queues) {
resources.subtract(this.queues[key].outQueueCost());
resources.subtract(this.accounts[key]);
}
return resources;
};
QueueManager.prototype.futureNeeds = function(gameState, onlyNeeds) {
QueueManager.prototype.futureNeeds = function(gameState, EcoManager) {
// Work out which plans will be executed next using priority and return the total cost of these plans
var recurse = function(queues, qm, number, depth){
var needs = new Resources();
@ -67,8 +83,8 @@ QueueManager.prototype.futureNeeds = function(gameState, onlyNeeds) {
};
//number of plans to look at
var current = this.getAvailableResources(gameState);
var current = this.getAvailableResources(gameState, true);
var futureNum = 20;
var queues = [];
for (var q in this.queues){
@ -76,121 +92,36 @@ QueueManager.prototype.futureNeeds = function(gameState, onlyNeeds) {
}
var needs = recurse(queues, this, futureNum, 0);
if (onlyNeeds) {
if (EcoManager === false) {
return {
"food" : Math.max(needs.food - current.food, 0),
"wood" : Math.max(needs.wood + 15*needs.population - current.wood, 0),
"stone" : Math.max(needs.stone - current.stone, 0),
"metal" : Math.max(needs.metal - current.metal, 0)
};
} else if (gameState.getTimeElapsed() > 300*1000) {
} else {
// Return predicted values minus the current stockpiles along with a base rater for all resources
return {
"food" : Math.max(needs.food - current.food, 0) + 150,
"wood" : Math.max(needs.wood + 15*needs.population - current.wood, 0) + 150, //TODO: read the house cost in case it changes in the future
"stone" : Math.max(needs.stone - current.stone, 0) + 50,
"metal" : Math.max(needs.metal - current.metal, 0) + 100
};
} else {
return {
"food" : Math.max(needs.food - current.food, 0) + 150,
"wood" : Math.max(needs.wood + 15*needs.population - current.wood, 0) + 150, //TODO: read the house cost in case it changes in the future
"stone" : Math.max(needs.stone - current.stone, 0),
"metal" : Math.max(needs.metal - current.metal, 0)
"food" : Math.max(needs.food - current.food, 0) + EcoManager.baseNeed["food"],
"wood" : Math.max(needs.wood + 15*needs.population - current.wood, 0) + EcoManager.baseNeed["wood"], //TODO: read the house cost in case it changes in the future
"stone" : Math.max(needs.stone - current.stone, 0) + EcoManager.baseNeed["stone"],
"metal" : Math.max(needs.metal - current.metal, 0) + EcoManager.baseNeed["metal"]
};
}
};
// runs through the curItemQueue and allocates resources be sending the
// affordable plans to the Out Queues. Returns a list of the unneeded resources
// so they can be used by lower priority plans.
QueueManager.prototype.affordableToOutQueue = function(gameState) {
var availableRes = this.getAvailableResources(gameState);
if (this.curItemQueue.length === 0) {
return availableRes;
}
var resources = this.getAvailableResources(gameState);
// Check everything in the curItemQueue, if it is affordable then mark it
// for execution
for ( var i = 0; i < this.curItemQueue.length; i++) {
availableRes.subtract(this.queues[this.curItemQueue[i]].getNext().getCost());
if (resources.canAfford(this.queues[this.curItemQueue[i]].getNext().getCost())) {
this.account[this.curItemQueue[i]] -= this.queues[this.curItemQueue[i]].getNext().getCost().toInt();
this.queues[this.curItemQueue[i]].nextToOutQueue();
resources = this.getAvailableResources(gameState);
this.curItemQueue[i] = null;
}
}
// Clear the spent items
var tmpQueue = [];
for ( var i = 0; i < this.curItemQueue.length; i++) {
if (this.curItemQueue[i] !== null) {
tmpQueue.push(this.curItemQueue[i]);
}
}
this.curItemQueue = tmpQueue;
return availableRes;
};
QueueManager.prototype.onlyUsesSpareAndUpdateSpare = function(unitCost, spare){
// This allows plans to be given resources if there are >500 spare after all the
// higher priority plan queues have been looked at and there are still enough resources
// We make it >0 so that even if we have no stone available we can still have non stone
// plans being given resources.
var spareNonNegRes = {
food: Math.max(0, spare.food - 500),
wood: Math.max(0, spare.wood - 500),
stone: Math.max(0, spare.stone - 500),
metal: Math.max(0, spare.metal - 500)
};
var spareNonNeg = new Resources(spareNonNegRes);
var ret = false;
if (spareNonNeg.canAfford(unitCost)){
ret = true;
}
// If there are no negative resources then there weren't any higher priority items so we
// definitely want to say that this can be added to the list.
var tmp = true;
for (key in spare.types){
var type = spare.types[key];
if (spare[type] < 0){
tmp = false;
}
}
// If either to the above sections returns true then
ret = ret || tmp;
spare.subtract(unitCost); // take the resources of the current unit from spare since this
// must be higher priority than any which are looked at
// afterwards.
return ret;
};
String.prototype.rpad = function(padString, length) {
var str = this;
while (str.length < length)
str = str + padString;
return str;
};
QueueManager.prototype.printQueues = function(gameState){
debug("OUTQUEUES");
for (var i in this.queues){
var qStr = "";
var q = this.queues[i];
if (q.outQueue.length > 0)
debug((i + ":"));
for (var j in q.outQueue){
qStr += q.outQueue[j].type + " ";
qStr = " " + q.outQueue[j].type + " ";
if (q.outQueue[j].number)
qStr += "x" + q.outQueue[j].number;
}
if (qStr != ""){
debug((i + ":").rpad(" ", 20) + qStr);
debug (qStr);
}
}
@ -198,21 +129,33 @@ QueueManager.prototype.printQueues = function(gameState){
for (var i in this.queues){
var qStr = "";
var q = this.queues[i];
if (q.queue.length > 0)
debug((i + ":"));
for (var j in q.queue){
qStr += q.queue[j].type + " ";
qStr = " " + q.queue[j].type + " ";
if (q.queue[j].number)
qStr += "x" + q.queue[j].number;
qStr += " ";
}
if (qStr != ""){
debug((i + ":").rpad(" ", 20) + qStr);
debug (qStr);
}
}
debug("Accounts: " + uneval(this.account));
debug("Needed Resources:" + uneval(this.futureNeeds(gameState)));
debug ("Accounts");
for (p in this.accounts)
{
debug(p + ": " + uneval(this.accounts[p]));
}
debug("Needed Resources:" + uneval(this.futureNeeds(gameState,false)));
debug ("Current Resources:" + uneval(gameState.getResources()));
debug ("Available Resources:" + uneval(this.getAvailableResources(gameState)));
};
QueueManager.prototype.clear = function(){
this.curItemQueue = [];
for (i in this.queues)
this.queues[i].empty();
};
QueueManager.prototype.update = function(gameState) {
var self = this;
for (var i in this.priorities){
if (!(this.priorities[i] > 0)){
@ -222,70 +165,75 @@ QueueManager.prototype.update = function(gameState) {
}
Engine.ProfileStart("Queue Manager");
//this.printQueues(gameState);
//if (gameState.ai.playedTurn % 10 === 0)
// this.printQueues(gameState);
Engine.ProfileStart("Pick items from queues");
// See if there is a high priority item from last time.
this.affordableToOutQueue(gameState);
do {
// pick out all affordable items, and list the ratios of (needed
// cost)/priority for unaffordable items.
var ratio = {};
var ratioMin = 1000000;
var ratioMinQueue = undefined;
for (var p in this.queues) {
if (this.queues[p].length() > 0 && this.curItemQueue.indexOf(p) === -1) {
var cost = this.queues[p].getNext().getCost().toInt();
if (cost < this.account[p]) {
this.curItemQueue.push(p);
// break;
} else {
ratio[p] = (cost - this.account[p]) / this.priorities[p];
if (ratio[p] < ratioMin) {
ratioMin = ratio[p];
ratioMinQueue = p;
// TODO: this only pushes the first object. SHould probably try to push any possible object to maximize productivity. Perhaps a settinh?
// looking at queues in decreasing priorities and pushing to the current item queues.
for (i in this.queueArrays)
{
var name = this.queueArrays[i][0];
var queue = this.queueArrays[i][1];
if (queue.length() > 0)
{
var item = queue.getNext();
var total = new Resources();
total.add(this.accounts[name]);
total.subtract(queue.outQueueCost());
if (total.canAfford(item.getCost()))
{
queue.nextToOutQueue();
}
} else if (queue.totalLength() === 0) {
this.accounts[name].reset();
}
}
var availableRes = this.getAvailableResources(gameState);
// assign some accounts to queues. This is done by priority, and by need. Note that this currently only looks at the next element.
for (ress in availableRes)
{
if (availableRes[ress] > 0 && ress != "population")
{
var totalPriority = 0;
// Okay so this is where it gets complicated.
// If a queue requires "ress" for the next element (in the queue or the outqueue)
// And the account is not high enough for it (multiplied by queue length... Might be bad, might not be).
// Then we add it to the total priority.
// (sorry about readability... Those big 'ifs' basically check if there is a need in the inqueue/outqueue
for (j in this.queues) {
if ((this.queues[j].length() > 0 && this.queues[j].getNext().getCost()[ress] > 0)
|| (this.queues[j].outQueueLength() > 0 && this.queues[j].outQueueNext().getCost()[ress] > 0))
if ( (this.queues[j].length() && this.accounts[j][ress] < this.queues[j].length() * (this.queues[j].getNext().getCost()[ress]))
|| (this.queues[j].outQueueLength() && this.accounts[j][ress] < this.queues[j].outQueueLength() * (this.queues[j].outQueueNext().getCost()[ress])))
totalPriority += this.priorities[j];
}
// Now we allow resources to the accounts. We can at most allow "priority/totalpriority*available"
// But we'll sometimes allow less if that would overflow.
for (j in this.queues) {
if ((this.queues[j].length() > 0 && this.queues[j].getNext().getCost()[ress] > 0)
|| (this.queues[j].outQueueLength() > 0 && this.queues[j].outQueueNext().getCost()[ress] > 0))
if ( (this.queues[j].length() && this.accounts[j][ress] < this.queues[j].length() * (this.queues[j].getNext().getCost()[ress]))
|| (this.queues[j].outQueueLength() && this.accounts[j][ress] < this.queues[j].outQueueLength() * (this.queues[j].outQueueNext().getCost()[ress])))
{
// we'll add at much what can be allowed to this queue.
var toAdd = Math.floor(this.priorities[j]/totalPriority * availableRes[ress]);
var maxNeed = 0;
if (this.queues[j].length())
maxNeed = this.queues[j].length() * (this.queues[j].getNext().getCost()[ress]);
if (this.queues[j].outQueueLength() && this.queues[j].outQueueLength() * (this.queues[j].outQueueNext().getCost()[ress]) > maxNeed)
maxNeed = this.queues[j].outQueueLength() * (this.queues[j].outQueueNext().getCost()[ress]);
if (toAdd + this.accounts[j][ress] > maxNeed)
toAdd = maxNeed - this.accounts[j][ress]; // always inferior to the original level.
//debug ("Adding " + toAdd + " of " + ress + " to the account of " + j);
this.accounts[j][ress] += toAdd;
}
}
}
}
// Checks to see that there is an item in at least one queue, otherwise
// breaks the loop.
if (this.curItemQueue.length === 0 && ratioMinQueue === undefined) {
break;
}
var availableRes = this.affordableToOutQueue(gameState);
var allSpare = availableRes["food"] > 0 && availableRes["wood"] > 0 && availableRes["stone"] > 0 && availableRes["metal"] > 0;
// if there are no affordable items use any resources which aren't
// wanted by a higher priority item
if ((availableRes["food"] > 0 || availableRes["wood"] > 0 || availableRes["stone"] > 0 || availableRes["metal"] > 0)
&& ratioMinQueue !== undefined) {
while (Object.keys(ratio).length > 0 && (availableRes["food"] > 0 || availableRes["wood"] > 0 || availableRes["stone"] > 0 || availableRes["metal"] > 0)){
ratioMin = Math.min(); //biggest value
for (var key in ratio){
if (ratio[key] < ratioMin){
ratioMin = ratio[key];
ratioMinQueue = key;
}
}
if (this.onlyUsesSpareAndUpdateSpare(this.queues[ratioMinQueue].getNext().getCost(), availableRes)){
if (allSpare){
for (var p in this.queues) {
this.account[p] += ratioMin * this.priorities[p];
}
}
//this.account[ratioMinQueue] -= this.queues[ratioMinQueue].getNext().getCost().toInt();
this.curItemQueue.push(ratioMinQueue);
allSpare = availableRes["food"] > 0 && availableRes["wood"] > 0 && availableRes["stone"] > 0 && availableRes["metal"] > 0;
}
delete ratio[ratioMinQueue];
}
}
this.affordableToOutQueue(gameState);
} while (this.curItemQueue.length === 0);
}
Engine.ProfileStop();
Engine.ProfileStart("Execute items");
@ -296,10 +244,11 @@ QueueManager.prototype.update = function(gameState) {
var next = this.queues[p].outQueueNext();
if (next.category === "building") {
if (gameState.buildingsBuilt == 0) {
if (this.queues[p].outQueueNext().canExecute(gameState)) {
if (next.canExecute(gameState)) {
this.accounts[p].subtract(next.getCost())
//debug ("Starting " + next.type + " substracted " + uneval(next.getCost()))
this.queues[p].executeNext(gameState);
gameState.buildingsBuilt += 1;
} else {
break;
}
@ -308,8 +257,10 @@ QueueManager.prototype.update = function(gameState) {
}
} else {
if (this.queues[p].outQueueNext().canExecute(gameState)){
//debug ("Starting " + next.type + " substracted " + uneval(next.getCost()))
this.accounts[p].subtract(next.getCost())
this.queues[p].executeNext(gameState);
}else{
} else {
break;
}
}
@ -324,6 +275,13 @@ QueueManager.prototype.addQueue = function(queueName, priority) {
this.queues[queueName] = new Queue();
this.priorities[queueName] = priority;
this.account[queueName] = 0;
this.accounts[queueName] = new Resources();
var self = this;
this.queueArrays = [];
for (var p in this.queues)
this.queueArrays.push([p,this.queues[p]]);
this.queueArrays.sort(function (a,b) { return (self.priorities[b[0]] - self.priorities[a[0]]) });
}
}
QueueManager.prototype.removeQueue = function(queueName) {
@ -334,6 +292,13 @@ QueueManager.prototype.removeQueue = function(queueName) {
delete this.queues[queueName];
delete this.priorities[queueName];
delete this.account[queueName];
delete this.accounts[queueName];
var self = this;
this.queueArrays = [];
for (var p in this.queues)
this.queueArrays.push([p,this.queues[p]]);
this.queueArrays.sort(function (a,b) { return (self.priorities[b[0]] - self.priorities[a[0]]) });
}
}

View File

@ -7,7 +7,21 @@ var Queue = function() {
this.outQueue = [];
};
Queue.prototype.empty = function() {
this.queue = [];
this.outQueue = [];
};
Queue.prototype.addItem = function(plan) {
for (var i in this.queue)
{
if (plan.category === "unit" && this.queue[i].type == plan.type && this.queue[i].number + plan.number <= this.queue[i].maxMerge)
{
this.queue[i].addItem(plan.number)
return;
}
}
this.queue.push(plan);
};
@ -37,15 +51,7 @@ Queue.prototype.outQueueCost = function(){
Queue.prototype.nextToOutQueue = function(){
if (this.queue.length > 0){
if (this.outQueue.length > 0 &&
this.getNext().category === "unit" &&
this.outQueue[this.outQueue.length-1].type === this.getNext().type &&
this.outQueue[this.outQueue.length-1].number < 5){
this.queue.shift();
this.outQueue[this.outQueue.length-1].addItem();
}else{
this.outQueue.push(this.queue.shift());
}
this.outQueue.push(this.queue.shift());
}
};

View File

@ -1,7 +0,0 @@
This is an AI for 0 A.D. (http://play0ad.com/) based on the testBot.
Install by placing the files into the data/mods/public/simulation/ai/qbot folder.
If you are developing you might find it helpful to change the debugOn line in qBot.js. This will make it spew random warnings depending on what I have been working on. Use the debug() function to make your own warnings.
Addendum for the experimental version: this is the regular qBot, with improvements here and there. It's "experimental" as it is currently in "test" phase, and if it proves to work and be more efficient than the normal qBot, will replace it in the next alpha. Please report any error to the wildfire games forum ( http://www.wildfiregames.com/forum/index.php?act=idx ), thanks for playing the bot!

View File

@ -1,720 +0,0 @@
/*
* TerrainAnalysis inherits from Map
*
* This creates a suitable passability map for pathfinding units and provides the findClosestPassablePoint() function.
* This is intended to be a base object for the terrain analysis modules to inherit from.
*/
function TerrainAnalysis(gameState){
var passabilityMap = gameState.getMap();
var obstructionMask = gameState.getPassabilityClassMask("pathfinderObstruction");
obstructionMask |= gameState.getPassabilityClassMask("default");
var obstructionTiles = new Uint16Array(passabilityMap.data.length);
for (var i = 0; i < passabilityMap.data.length; ++i)
{
obstructionTiles[i] = (passabilityMap.data[i] & obstructionMask) ? 0 : 65535;
}
this.Map(gameState, obstructionTiles);
};
copyPrototype(TerrainAnalysis, Map);
// Returns the (approximately) closest point which is passable by searching in a spiral pattern
TerrainAnalysis.prototype.findClosestPassablePoint = function(startPoint, quick, limitDistance){
var w = this.width;
var p = startPoint;
var direction = 1;
if (p[0] + w*p[1] > 0 && p[0] + w*p[1] < this.length &&
this.map[p[0] + w*p[1]] != 0){
if (this.countConnected(p, 10) >= 10){
return p;
}
}
var count = 0;
// search in a spiral pattern.
for (var i = 1; i < w; i++){
for (var j = 0; j < 2; j++){
for (var k = 0; k < i; k++){
p[j] += direction;
if (p[0] + w*p[1] > 0 && p[0] + w*p[1] < this.length &&
this.map[p[0] + w*p[1]] != 0){
if (quick || this.countConnected(p, 10) >= 10){
return p;
}
}
if (limitDistance && count > 40){
return undefined;
}
count += 1;
}
}
direction *= -1;
}
return undefined;
};
// Counts how many accessible tiles there are connected to the start Point. If there are >= maxCount then it stops.
// This is inefficient for large areas so maxCount should be kept small for efficiency.
TerrainAnalysis.prototype.countConnected = function(startPoint, maxCount, curCount, checked){
curCount = curCount || 0;
checked = checked || [];
var w = this.width;
var positions = [[0,1], [0,-1], [1,0], [-1,0]];
curCount += 1; // add 1 for the current point
checked.push(startPoint);
if (curCount >= maxCount){
return curCount;
}
for (var i in positions){
var p = [startPoint[0] + positions[i][0], startPoint[1] + positions[i][1]];
if (p[0] + w*p[1] > 0 && p[0] + w*p[1] < this.length &&
this.map[p[0] + w*p[1]] != 0 && !(p in checked)){
curCount += this.countConnected(p, maxCount, curCount, checked);
}
}
return curCount;
};
/*
* PathFinder inherits from TerrainAnalysis
*
* Used to create a list of distinct paths between two points.
*
* Currently it works with a basic implementation which should be improved.
*
* TODO: Make this use territories.
*/
function PathFinder(gameState){
this.TerrainAnalysis(gameState);
}
copyPrototype(PathFinder, TerrainAnalysis);
/*
* Returns a list of distinct paths to the destination. Currently paths are distinct if they are more than
* blockRadius apart at a distance of blockPlacementRadius from the destination. Where blockRadius and
* blockPlacementRadius are defined in walkGradient
*/
PathFinder.prototype.getPaths = function(start, end, mode){
var s = this.findClosestPassablePoint(this.gamePosToMapPos(start));
var e = this.findClosestPassablePoint(this.gamePosToMapPos(end));
if (!s || !e){
return undefined;
}
var paths = [];
var i = 0;
while (true){
i++;
//this.dumpIm("terrainanalysis_"+i+".png", 511);
this.makeGradient(s,e);
var curPath = this.walkGradient(e, mode);
if (curPath !== undefined){
paths.push(curPath);
}else{
break;
}
this.wipeGradient();
}
//this.dumpIm("terrainanalysis.png", 511);
if (paths.length > 0){
return paths;
}else{
return [];
}
};
// Creates a potential gradient with the start point having the lowest potential
PathFinder.prototype.makeGradient = function(start, end){
var w = this.width;
var map = this.map;
// Holds the list of current points to work outwards from
var stack = [];
// We store the next level in its own stack
var newStack = [];
// Relative positions or new cells from the current one. We alternate between the adjacent 4 and 8 cells
// so that there is an average 1.5 distance for diagonals which is close to the actual sqrt(2) ~ 1.41
var positions = [[[0,1], [0,-1], [1,0], [-1,0]],
[[0,1], [0,-1], [1,0], [-1,0], [1,1], [-1,-1], [1,-1], [-1,1]]];
//Set the distance of the start point to be 1 to distinguish it from the impassable areas
map[start[0] + w*(start[1])] = 1;
stack.push(start);
// while there are new points being added to the stack
while (stack.length > 0){
//run through the current stack
while (stack.length > 0){
var cur = stack.pop();
// stop when we reach the end point
if (cur[0] == end[0] && cur[1] == end[1]){
return;
}
var dist = map[cur[0] + w*(cur[1])] + 1;
// Check the positions adjacent to the current cell
for (var i = 0; i < positions[dist % 2].length; i++){
var pos = positions[dist % 2][i];
var cell = cur[0]+pos[0] + w*(cur[1]+pos[1]);
if (cell >= 0 && cell < this.length && map[cell] > dist){
map[cell] = dist;
newStack.push([cur[0]+pos[0], cur[1]+pos[1]]);
}
}
}
// Replace the old empty stack with the newly filled one.
stack = newStack;
newStack = [];
}
};
// Clears the map to just have the obstructions marked on it.
PathFinder.prototype.wipeGradient = function(){
for (var i = 0; i < this.length; i++){
if (this.map[i] > 0){
this.map[i] = 65535;
}
}
};
// Returns the path down a gradient from the start to the bottom of the gradient, returns a point for every 20 cells in normal mode
// in entryPoints mode this returns the point where the path enters the region near the destination, currently defined
// by blockPlacementRadius. Note doesn't return a path when the destination is within the blockpoint radius.
PathFinder.prototype.walkGradient = function(start, mode){
var positions = [[0,1], [0,-1], [1,0], [-1,0], [1,1], [-1,-1], [1,-1], [-1,1]];
var path = [[start[0]*this.cellSize, start[1]*this.cellSize]];
var blockPoint = undefined;
var blockPlacementRadius = 45;
var blockRadius = 23;
var count = 0;
var cur = start;
var w = this.width;
var dist = this.map[cur[0] + w*cur[1]];
var moved = false;
while (this.map[cur[0] + w*cur[1]] !== 0){
for (var i = 0; i < positions.length; i++){
var pos = positions[i];
var cell = cur[0]+pos[0] + w*(cur[1]+pos[1]);
if (cell >= 0 && cell < this.length && this.map[cell] > 0 && this.map[cell] < dist){
dist = this.map[cell];
cur = [cur[0]+pos[0], cur[1]+pos[1]];
moved = true;
count++;
// Mark the point to put an obstruction at before calculating the next path
if (count === blockPlacementRadius){
blockPoint = cur;
}
// Add waypoints to the path, fairly well spaced apart.
if (count % 40 === 0){
path.unshift([cur[0]*this.cellSize, cur[1]*this.cellSize]);
}
break;
}
}
if (!moved){
break;
}
moved = false;
}
if (blockPoint === undefined){
return undefined;
}
// Add an obstruction to the map at the blockpoint so the next path will take a different route.
this.addInfluence(blockPoint[0], blockPoint[1], blockRadius, -1000000, 'constant');
if (mode === 'entryPoints'){
// returns the point where the path enters the blockPlacementRadius
return [blockPoint[0] * this.cellSize, blockPoint[1] * this.cellSize];
}else{
// return a path of points 20 squares apart on the route
return path;
}
};
// Would be used to calculate the width of a chokepoint
// NOTE: Doesn't currently work.
PathFinder.prototype.countAttached = function(pos){
var positions = [[0,1], [0,-1], [1,0], [-1,0]];
var w = this.width;
var val = this.map[pos[0] + w*pos[1]];
var stack = [pos];
var used = {};
while (stack.length > 0){
var cur = stack.pop();
used[cur[0] + " " + cur[1]] = true;
for (var i = 0; i < positions.length; i++){
var p = positions[i];
var cell = cur[0]+p[0] + w*(cur[1]+p[1]);
}
}
};
/*
* Accessibility inherits from TerrainAnalysis
*
* Determines whether there is a path from one point to another. It is initialised with a single point (p1) and then
* can efficiently determine if another point is reachable from p1. Initialising the object is costly so it should be
* cached.
*/
function Accessibility(gameState, location){
this.TerrainAnalysis(gameState);
var start = this.findClosestPassablePoint(this.gamePosToMapPos(location));
// Check that the accessible region is a decent size, otherwise obstacles close to the start point can create
// tiny accessible areas which makes the rest of the map inaceesible.
var iterations = 0;
while (this.floodFill(start) < 20 && iterations < 30){
this.map[start[0] + this.width*(start[1])] = 0;
start = this.findClosestPassablePoint(this.gamePosToMapPos(location));
iterations += 1;
}
//this.dumpIm("accessibility.png");
}
copyPrototype(Accessibility, TerrainAnalysis);
// Return true if the given point is accessible from the point given when initialising the Accessibility object. #
// If the given point is impassable the closest passable point is used.
Accessibility.prototype.isAccessible = function(position){
var s = this.findClosestPassablePoint(this.gamePosToMapPos(position), true, true);
if (!s)
return false;
return this.map[s[0] + this.width * s[1]] === 1;
};
// fill all of the accessible areas with value 1
Accessibility.prototype.floodFill = function(start){
var w = this.width;
var map = this.map;
// Holds the list of current points to work outwards from
var stack = [];
// We store new points to be added to the stack temporarily in here while we run through the current stack
var newStack = [];
// Relative positions or new cells from the current one.
var positions = [[0,1], [0,-1], [1,0], [-1,0]];
// Set the start point to be accessible
map[start[0] + w*(start[1])] = 1;
stack.push(start);
var count = 0;
// while there are new points being added to the stack
while (stack.length > 0){
//run through the current stack
while (stack.length > 0){
var cur = stack.pop();
// Check the positions adjacent to the current cell
for (var i = 0; i < positions.length; i++){
var pos = positions[i];
var cell = cur[0]+pos[0] + w*(cur[1]+pos[1]);
if (cell >= 0 && cell < this.length && map[cell] > 1){
map[cell] = 1;
newStack.push([cur[0]+pos[0], cur[1]+pos[1]]);
count += 1;
}
}
}
// Replace the old empty stack with the newly filled one.
stack = newStack;
newStack = [];
}
return count;
};
// Some different take on the idea of Quantumstate... What I'll do is make a list of any terrain obstruction...
function aStarPath(gameState, onWater){
var self = this;
this.passabilityMap = gameState.getMap();
var obstructionMaskLand = gameState.getPassabilityClassMask("default");
var obstructionMaskWater = gameState.getPassabilityClassMask("ship");
var obstructionTiles = new Uint16Array(this.passabilityMap.data.length);
for (var i = 0; i < this.passabilityMap.data.length; ++i)
{
if (onWater) {
obstructionTiles[i] = (this.passabilityMap.data[i] & obstructionMaskWater) ? 0 : 255;
} else {
obstructionTiles[i] = (this.passabilityMap.data[i] & obstructionMaskLand) ? 0 : 255;
// We allow water, but we set it at a different index.
if (!(this.passabilityMap.data[i] & obstructionMaskWater) && obstructionTiles[i] === 0)
obstructionTiles[i] = 200;
}
}
if (onWater)
this.onWater = true;
else
this.onWater = false;
this.pathRequiresWater = this.onWater;
this.cellSize = gameState.cellSize;
this.Map(gameState, obstructionTiles);
this.passabilityMap = new Map(gameState, obstructionTiles, true);
var type = ["wood","stone", "metal"];
if (onWater) // trees can perhaps be put into water, I'd doubt so about the rest.
type = ["wood"];
for (o in type) {
var entities = gameState.getResourceSupplies(type[o]);
entities.forEach(function (supply) { //}){
var radius = Math.floor(supply.obstructionRadius() / self.cellSize);
if (type[o] === "wood") {
for (var xx = -1; xx <= 1;xx++)
for (var yy = -1; yy <= 1;yy++)
{
var x = self.gamePosToMapPos(supply.position())[0];
var y = self.gamePosToMapPos(supply.position())[1];
if (x+xx >= 0 && x+xx < self.width && y+yy >= 0 && y+yy < self.height)
{
self.map[x+xx + (y+yy)*self.width] = 0;
self.passabilityMap.map[x+xx + (y+yy)*self.width] = 100; // tree
}
}
self.map[x + y*self.width] = 0;
self.passabilityMap.map[x + y*self.width] = 0;
} else {
for (var xx = -radius; xx <= radius;xx++)
for (var yy = -radius; yy <= radius;yy++)
{
var x = self.gamePosToMapPos(supply.position())[0];
var y = self.gamePosToMapPos(supply.position())[1];
if (x+xx >= 0 && x+xx < self.width && y+yy >= 0 && y+yy < self.height)
{
self.map[x+xx + (y+yy)*self.width] = 0;
self.passabilityMap.map[x+xx + (y+yy)*self.width] = 0;
}
}
}
});
}
//this.dumpIm("Non-Expanded Obstructions.png",255);
this.expandInfluences();
//this.dumpIm("Expanded Obstructions.png",10);
//this.BluringRadius = 10;
//this.Blur(this.BluringRadius); // first steop of bluring
}
copyPrototype(aStarPath, TerrainAnalysis);
aStarPath.prototype.getPath = function(start,end,optimized, minSampling, iterationLimit , gamestate)
{
if (minSampling === undefined)
this.minSampling = 2;
else this.minSampling = minSampling;
if (start[0] < 0 || this.gamePosToMapPos(start)[0] >= this.width || start[1] < 0 || this.gamePosToMapPos(start)[1] >= this.height)
return undefined;
var s = this.findClosestPassablePoint(this.gamePosToMapPos(start));
var e = this.findClosestPassablePoint(this.gamePosToMapPos(end));
if (!s || !e){
return undefined;
}
var w = this.width;
var h = this.height;
this.optimized = optimized;
if (this.minSampling < 1)
this.minSampling = 1;
if (gamestate !== undefined)
{
this.TotorMap = new Map(gamestate);
this.TotorMap.addInfluence(s[0],s[1],1,200,'constant');
this.TotorMap.addInfluence(e[0],e[1],1,200,'constant');
}
this.iterationLimit = 65500;
if (iterationLimit !== undefined)
this.iterationLimit = iterationLimit;
this.s = s[0] + w*s[1];
this.e = e[0] + w*e[1];
// I was using incredibly slow associative arrays before…
this.openList = [];
this.parentSquare = new Uint32Array(this.map.length);
this.isOpened = new Boolean(this.map.length);
this.fCostArray = new Uint32Array(this.map.length);
this.gCostArray = new Uint32Array(this.map.length);
this.currentSquare = this.s;
this.totalIteration = 0;
this.isOpened[this.s] = true;
this.openList.push(this.s);
this.fCostArray[this.s] = SquareVectorDistance([this.s%w, Math.floor(this.s/w)], [this.e%w, Math.floor(this.e/w)]);
this.gCostArray[this.s] = 0;
this.parentSquare[this.s] = this.s;
//debug ("Initialized okay");
return this.continuePath(gamestate);
}
// in case it's not over yet, this can carry on the calculation of a path over multiple turn until it's over
aStarPath.prototype.continuePath = function(gamestate)
{
var w = this.width;
var h = this.height;
var positions = [[0,1], [0,-1], [1,0], [-1,0], [1,1], [-1,-1], [1,-1], [-1,1]];
var cost = [100,100,100,100,150,150,150,150];
var invCost = [1,1,1,1,0.8,0.8,0.8,0.8];
//creation of variables used in the loop
var found = false;
var nouveau = false;
var shortcut = false;
var Sampling = this.minSampling;
var closeToEnd = false;
var infinity = Math.min();
var currentDist = infinity;
var e = this.e;
var s = this.s;
var iteration = 0;
// on to A*
while (found === false && this.openList.length !== 0 && iteration < this.iterationLimit){
currentDist = infinity;
if (shortcut === true) {
this.currentSquare = this.openList.shift();
} else {
for (i in this.openList)
{
var sum = this.fCostArray[this.openList[i]] + this.gCostArray[this.openList[i]];
if (sum < currentDist)
{
this.currentSquare = this.openList[i];
currentDist = sum;
}
}
this.openList.splice(this.openList.indexOf(this.currentSquare),1);
}
if (!this.onWater && this.passabilityMap.map[this.currentSquare] === 200) {
this.onWater = true;
this.pathRequiresWater = true;
} else if (this.onWater && this.passabilityMap.map[this.currentSquare] !== 200)
this.onWater = false;
shortcut = false;
this.isOpened[this.currentSquare] = false;
// optimizaiton: can make huge jumps if I know there's nothing in the way
Sampling = this.minSampling;
if (this.optimized === true) {
Sampling = Math.floor( (+this.map[this.currentSquare]-this.minSampling)/Sampling )*Sampling;
if (Sampling < this.minSampling)
Sampling = this.minSampling;
}
/*
var diagSampling = Math.floor(Sampling / 1.5);
if (diagSampling < this.minSampling)
diagSampling = this.minSampling;
*/
var target = [this.e%w, Math.floor(this.e/w)];
closeToEnd = false;
if (SquareVectorDistance([this.currentSquare%w, Math.floor(this.currentSquare/w)], target) <= Sampling*Sampling)
{
closeToEnd = true;
Sampling = 1;
}
if (gamestate !== undefined)
this.TotorMap.addInfluence(this.currentSquare % w, Math.floor(this.currentSquare / w),1,40,'constant');
for (i in positions)
{
//var hereSampling = cost[i] == 1 ? Sampling : diagSampling;
var index = 0 + this.currentSquare +positions[i][0]*Sampling +w*Sampling*positions[i][1];
if (this.map[index] >= Sampling)
{
if(this.isOpened[index] === undefined)
{
this.parentSquare[index] = this.currentSquare;
this.fCostArray[index] = SquareVectorDistance([index%w, Math.floor(index/w)], target);// * cost[i];
this.gCostArray[index] = this.gCostArray[this.currentSquare] + cost[i] * Sampling;// - this.map[index];
if (!this.onWater && this.passabilityMap.map[index] === 200) {
this.gCostArray[index] += this.width*this.width*3;
} else if (this.onWater && this.passabilityMap.map[index] !== 200) {
this.gCostArray[index] += this.fCostArray[index];
} else if (!this.onWater && this.passabilityMap.map[index] === 100) {
this.gCostArray[index] += 100;
}
if (this.openList[0] !== undefined && this.fCostArray[this.openList[0]] + this.gCostArray[this.openList[0]] > this.fCostArray[index] + this.gCostArray[index])
{
this.openList.unshift(index);
shortcut = true;
} else {
this.openList.push(index);
}
this.isOpened[index] = true;
if (closeToEnd === true && (index === e || index - 1 === e || index + 1 === e || index - w === e || index + w === e
|| index + 1 + w === e || index + 1 - w === e || index - 1 + w === e|| index - 1 - w === e)) {
this.parentSquare[this.e] = this.currentSquare;
found = true;
break;
}
} else {
var addCost = 0;
if (!this.onWater && this.passabilityMap.map[index] === 200) {
addCost = this.width*this.width*3;
} else if (this.onWater && this.passabilityMap.map[index] !== 200) {
addCost = this.fCostArray[index];
} else if (!this.onWater && this.passabilityMap.map[index] === 100) {
addCost += 100;
}
//addCost -= this.map[index];
// already on the Open or closed list
if (this.gCostArray[index] > cost[i] * Sampling + addCost + this.gCostArray[this.currentSquare])
{
this.parentSquare[index] = this.currentSquare;
this.gCostArray[index] = cost[i] * Sampling + addCost + this.gCostArray[this.currentSquare];
}
}
}
}
iteration++;
}
this.totalIteration += iteration;
if (iteration === this.iterationLimit && found === false && this.openList.length !== 0)
{
// we've got to assume that we stopped because we reached the upper limit of iterations
return "toBeContinued";
}
//debug (this.totalIteration);
var paths = [];
if (found) {
this.currentSquare = e;
var lastPos = [0,0];
while (this.parentSquare[this.currentSquare] !== s)
{
this.currentSquare = this.parentSquare[this.currentSquare];
if (gamestate !== undefined)
this.TotorMap.addInfluence(this.currentSquare % w, Math.floor(this.currentSquare / w),1,50,'constant');
if (SquareVectorDistance(lastPos,[this.currentSquare % w, Math.floor(this.currentSquare / w)]) > 300)
{
lastPos = [ (this.currentSquare % w) * this.cellSize, Math.floor(this.currentSquare / w) * this.cellSize];
paths.push(lastPos);
if (gamestate !== undefined)
this.TotorMap.addInfluence(this.currentSquare % w, Math.floor(this.currentSquare / w),1,100,'constant');
}
}
} else {
// we have not found a path.
// what do we do then?
}
if (gamestate !== undefined)
this.TotorMap.dumpIm("Path From " +s +" to " +e +".png",255);
delete this.parentSquare;
delete this.isOpened;
delete this.fCostArray;
delete this.gCostArray;
if (paths.length > 0) {
return [paths, this.pathRequiresWater];
} else {
return undefined;
}
}
/**
* Make each cell's 8-bit value at least one greater than each of its
* neighbours' values. (If the grid is initialised with 0s and things high enough (> 100 on most maps), the
* result of each cell is its Manhattan distance to the nearest 0.)
*/
aStarPath.prototype.expandInfluences = function() {
var w = this.width;
var h = this.height;
var grid = this.map;
for ( var y = 0; y < h; ++y) {
var min = 8;
for ( var x = 0; x < w; ++x) {
var g = grid[x + y * w];
if (g > min)
grid[x + y * w] = min;
else if (g < min)
min = g;
++min;
if (min > 8)
min = 8;
}
for ( var x = w - 2; x >= 0; --x) {
var g = grid[x + y * w];
if (g > min)
grid[x + y * w] = min;
else if (g < min)
min = g;
++min;
if (min > 8)
min = 8;
}
}
for ( var x = 0; x < w; ++x) {
var min = 8;
for ( var y = 0; y < h; ++y) {
var g = grid[x + y * w];
if (g > min)
grid[x + y * w] = min;
else if (g < min)
min = g;
++min;
if (min > 8)
min = 8;
}
for ( var y = h - 2; y >= 0; --y) {
var g = grid[x + y * w];
if (g > min)
grid[x + y * w] = min;
else if (g < min)
min = g;
++min;
if (min > 8)
min = 8;
}
}
};

View File

@ -7,6 +7,8 @@
//timer.activateTimer : Sets the status of a deactivated timer to active.
//timer.deactivateTimer : Deactivates a timer. Deactivated timers will never give true.
// Currently totally unused, iirc.
//-EmjeR-// Timer class //
var Timer = function() {

View File

@ -1,85 +0,0 @@
var WalkToCC = function(gameState, militaryManager){
this.minAttackSize = 20;
this.maxAttackSize = 60;
this.idList=[];
};
// Returns true if the attack can be executed at the current time
WalkToCC.prototype.canExecute = function(gameState, militaryManager){
var enemyStrength = militaryManager.measureEnemyStrength(gameState);
var enemyCount = militaryManager.measureEnemyCount(gameState);
// We require our army to be >= this strength
var targetStrength = enemyStrength * 1.5;
var availableCount = militaryManager.countAvailableUnits();
var availableStrength = militaryManager.measureAvailableStrength();
debug("Troops needed for attack: " + this.minAttackSize + " Have: " + availableCount);
debug("Troops strength for attack: " + targetStrength + " Have: " + availableStrength);
return ((availableStrength >= targetStrength && availableCount >= this.minAttackSize)
|| availableCount >= this.maxAttackSize);
};
// Executes the attack plan, after this is executed the update function will be run every turn
WalkToCC.prototype.execute = function(gameState, militaryManager){
var availableCount = militaryManager.countAvailableUnits();
this.idList = militaryManager.getAvailableUnits(availableCount);
var pending = EntityCollectionFromIds(gameState, this.idList);
// Find the critical enemy buildings we could attack
var targets = militaryManager.getEnemyBuildings(gameState,"ConquestCritical");
// If there are no critical structures, attack anything else that's critical
if (targets.length == 0) {
targets = gameState.entities.filter(function(ent) {
return (gameState.isEntityEnemy(ent) && ent.hasClass("ConquestCritical") && ent.owner() !== 0);
});
}
// If there's nothing, attack anything else that's less critical
if (targets.length == 0) {
targets = militaryManager.getEnemyBuildings(gameState,"Town");
}
if (targets.length == 0) {
targets = militaryManager.getEnemyBuildings(gameState,"Village");
}
// If we have a target, move to it
if (targets.length) {
// Remove the pending role
pending.forEach(function(ent) {
ent.setMetadata("role", "attack");
});
var target = targets.toEntityArray()[0];
var targetPos = target.position();
// TODO: this should be an attack-move command
pending.move(targetPos[0], targetPos[1]);
} else if (targets.length == 0 ) {
gameState.ai.gameFinished = true;
}
};
// Runs every turn after the attack is executed
// This removes idle units from the attack
WalkToCC.prototype.update = function(gameState, militaryManager){
var removeList = [];
for (var idKey in this.idList){
var id = this.idList[idKey];
var ent = militaryManager.entity(id);
if(ent)
{
if(ent.isIdle()) {
militaryManager.unassignUnit(id);
removeList.push(id);
}
} else {
removeList.push(id);
}
}
for (var i in removeList){
this.idList.splice(this.idList.indexOf(removeList[i]),1);
}
};

View File

@ -4,11 +4,14 @@
var Worker = function(ent) {
this.ent = ent;
this.approachCount = 0;
this.maxApproachTime = 45000;
this.unsatisfactoryResource = false; // if true we'll reguarly check if we can't have better now.
};
Worker.prototype.update = function(gameState) {
var subrole = this.ent.getMetadata("subrole");
var subrole = this.ent.getMetadata(PlayerID, "subrole");
if (!this.ent.position()){
// If the worker has no position then no work can be done
@ -16,56 +19,81 @@ Worker.prototype.update = function(gameState) {
}
if (subrole === "gatherer"){
if (!(this.ent.unitAIState().split(".")[1] === "GATHER" && this.ent.unitAIOrderData()[0].type
&& this.getResourceType(this.ent.unitAIOrderData()[0].type) === this.ent.getMetadata("gather-type"))
&& !(this.ent.unitAIState().split(".")[1] === "RETURNRESOURCE")){
if (this.ent.unitAIState().split(".")[1] !== "GATHER" && this.ent.unitAIState().split(".")[1] !== "COMBAT" && this.ent.unitAIState().split(".")[1] !== "RETURNRESOURCE"){
// TODO: handle combat for hunting animals
if (!this.ent.resourceCarrying() || this.ent.resourceCarrying().length === 0 ||
this.ent.resourceCarrying()[0].type === this.ent.getMetadata("gather-type")){
this.ent.resourceCarrying()[0].type === this.ent.getMetadata(PlayerID, "gather-type")){
Engine.ProfileStart("Start Gathering");
this.startGathering(gameState);
Engine.ProfileStop();
} else if (this.ent.unitAIState().split(".")[1] !== "RETURNRESOURCE") {
// Should deposit resources
Engine.ProfileStart("Return Resources");
this.returnResources(gameState);
if (!this.returnResources(gameState))
{
// no dropsite, abandon cargo.
// if we have a new order
if (this.ent.resourceCarrying()[0].type !== this.ent.getMetadata(PlayerID, "gather-type"))
this.startGathering(gameState);
else {
this.ent.setMetadata(PlayerID, "gather-type",undefined);
this.ent.setMetadata(PlayerID, "subrole", "idle");
this.ent.stopMoving();
}
}
Engine.ProfileStop();
}
this.startApproachingResourceTime = gameState.getTimeElapsed();
//Engine.PostCommand({"type": "set-shading-color", "entities": [this.ent.id()], "rgb": [10,0,0]});
}else{
// If we haven't reached the resource in 1 minutes twice in a row and none of the resource has been
// gathered then mark it as inaccessible.
if (gameState.getTimeElapsed() - this.startApproachingResourceTime > 60000){
if (this.gatheringFrom){
var ent = gameState.getEntityById(this.gatheringFrom);
if (ent && ent.resourceSupplyAmount() == ent.resourceSupplyMax()){
// if someone gathers from it, it's only that the pathfinder sucks.
if (this.approachCount > 0 && ent.getMetadata("gatherer-count") <= 2){
ent.setMetadata("inaccessible", true);
this.ent.setMetadata("subrole", "idle");
this.ent.flee(ent);
}
this.approachCount++;
}else{
this.approachCount = 0;
}
this.startApproachingResourceTime = gameState.getTimeElapsed();
}
} else if (this.ent.unitAIState().split(".")[1] === "GATHER") {
if (this.unsatisfactoryResource && (this.ent.id() + gameState.ai.playedTurn) % 20 === 0)
{
Engine.ProfileStart("Start Gathering");
this.startGathering(gameState);
Engine.ProfileStop();
}
/*if (gameState.getTimeElapsed() - this.startApproachingResourceTime > this.maxApproachTime) {
if (this.gatheringFrom) {
var ent = gameState.getEntityById(this.gatheringFrom);
if ((ent && ent.resourceSupplyAmount() == ent.resourceSupplyMax())) {
// if someone gathers from it, it's only that the pathfinder sucks.
debug (ent.toString() + " is inaccessible");
ent.setMetadata(PlayerID, "inaccessible", true);
this.ent.flee(ent);
this.ent.setMetadata(PlayerID, "subrole", "idle");
this.gatheringFrom = undefined;
}
}
}*/
} else if (this.ent.unitAIState().split(".")[1] === "COMBAT") {
/*if (gameState.getTimeElapsed() - this.startApproachingResourceTime > this.maxApproachTime) {
var ent = gameState.getEntityById(this.ent.unitAIOrderData()[0].target);
if (ent && !ent.isHurt()) {
// if someone gathers from it, it's only that the pathfinder sucks.
debug (ent.toString() + " is inaccessible from Combat");
ent.setMetadata(PlayerID, "inaccessible", true);
this.ent.flee(ent);
this.ent.setMetadata(PlayerID, "subrole", "idle");
this.gatheringFrom = undefined;
}
}*/
} else {
this.startApproachingResourceTime = gameState.getTimeElapsed();
}
} else if(subrole === "builder") {
if (this.ent.unitAIState().split(".")[1] !== "REPAIR"){
var target = this.ent.getMetadata("target-foundation");
var target = this.ent.getMetadata(PlayerID, "target-foundation");
if (target.foundationProgress() === undefined && target.needsRepair() == false)
this.ent.setMetadata("subrole", "idle");
this.ent.setMetadata(PlayerID, "subrole", "idle");
else
this.ent.repair(target);
}
this.startApproachingResourceTime = gameState.getTimeElapsed();
//Engine.PostCommand({"type": "set-shading-color", "entities": [this.ent.id()], "rgb": [0,10,0]});
} else {
this.startApproachingResourceTime = gameState.getTimeElapsed();
}
Engine.ProfileStart("Update Gatherer Counts");
@ -75,12 +103,12 @@ Worker.prototype.update = function(gameState) {
Worker.prototype.updateGathererCounts = function(gameState, dead){
// update gatherer counts for the resources
if (this.ent.unitAIState().split(".")[2] === "GATHERING" && !dead){
if (this.ent.unitAIState().split(".")[1] === "GATHER" && !dead){
if (this.gatheringFrom !== this.ent.unitAIOrderData()[0].target){
if (this.gatheringFrom){
var ent = gameState.getEntityById(this.gatheringFrom);
if (ent && ent.resourceSupplyType()){
ent.setMetadata("gatherer-count", ent.getMetadata("gatherer-count") - 1);
ent.setMetadata(PlayerID, "gatherer-count", ent.getMetadata(PlayerID, "gatherer-count") - 1);
this.markFull(gameState,ent);
}
}
@ -88,26 +116,26 @@ Worker.prototype.updateGathererCounts = function(gameState, dead){
if (this.gatheringFrom){
var ent = gameState.getEntityById(this.gatheringFrom);
if (ent && ent.resourceSupplyType()){
ent.setMetadata("gatherer-count", (ent.getMetadata("gatherer-count") || 0) + 1);
ent.setMetadata(PlayerID, "gatherer-count", (ent.getMetadata(PlayerID, "gatherer-count") || 0) + 1);
this.markFull(gameState,ent);
}
}
}
} else if (this.ent.unitAIState().split(".")[2] === "RETURNRESOURCE" && !dead) {
this.startApproachingResourceTime = gameState.getTimeElapsed();
}
} else if (this.ent.unitAIState().split(".")[1] === "RETURNRESOURCE" && !dead) {
// We remove us from the counting is we have no following order or its not "return to collected resource".
if (this.ent.unitAIOrderData().length === 1) {
var ent = gameState.getEntityById(this.gatheringFrom);
if (ent && ent.resourceSupplyType()){
ent.setMetadata("gatherer-count", ent.getMetadata("gatherer-count") - 1);
ent.setMetadata(PlayerID, "gatherer-count", ent.getMetadata(PlayerID, "gatherer-count") - 1);
this.markFull(gameState,ent);
}
this.gatheringFrom = undefined;
}
if (!this.ent.unitAIOrderData()[1].target || this.gatheringFrom !== this.ent.unitAIOrderData()[1].target){
} else if (!this.ent.unitAIOrderData()[1].target || this.gatheringFrom !== this.ent.unitAIOrderData()[1].target){
if (this.gatheringFrom){
var ent = gameState.getEntityById(this.gatheringFrom);
if (ent && ent.resourceSupplyType()){
ent.setMetadata("gatherer-count", ent.getMetadata("gatherer-count") - 1);
ent.setMetadata(PlayerID, "gatherer-count", ent.getMetadata(PlayerID, "gatherer-count") - 1);
this.markFull(gameState,ent);
}
}
@ -117,7 +145,7 @@ Worker.prototype.updateGathererCounts = function(gameState, dead){
if (this.gatheringFrom){
var ent = gameState.getEntityById(this.gatheringFrom);
if (ent && ent.resourceSupplyType()){
ent.setMetadata("gatherer-count", ent.getMetadata("gatherer-count") - 1);
ent.setMetadata(PlayerID, "gatherer-count", ent.getMetadata(PlayerID, "gatherer-count") - 1);
this.markFull(gameState,ent);
}
this.gatheringFrom = undefined;
@ -128,43 +156,43 @@ Worker.prototype.updateGathererCounts = function(gameState, dead){
Worker.prototype.markFull = function(gameState,ent){
var maxCounts = {"food": 15, "wood": 6, "metal": 15, "stone": 15, "treasure": 1};
var resource = ent.resourceSupplyType().generic;
if (ent.resourceSupplyType() && ent.getMetadata("gatherer-count") >= maxCounts[resource]){
if (!ent.getMetadata("full")){
ent.setMetadata("full", true);
if (ent.resourceSupplyType() && ent.getMetadata(PlayerID, "gatherer-count") >= maxCounts[resource]){
if (!ent.getMetadata(PlayerID, "full")){
ent.setMetadata(PlayerID, "full", true);
// update the dropsite
var dropsite = gameState.getEntityById(ent.getMetadata("linked-dropsite"));
if (dropsite == undefined || dropsite.getMetadata("linked-resources-" + resource) === undefined)
var dropsite = gameState.getEntityById(ent.getMetadata(PlayerID, "linked-dropsite"));
if (dropsite == undefined || dropsite.getMetadata(PlayerID, "linked-resources-" + resource) === undefined)
return;
if (ent.getMetadata("linked-dropsite-nearby") == true) {
dropsite.setMetadata("resource-quantity-" + resource, +dropsite.getMetadata("resource-quantity-" + resource) - (+ent.getMetadata("dp-update-value")));
dropsite.getMetadata("linked-resources-" + resource).updateEnt(ent);
dropsite.getMetadata("nearby-resources-" + resource).updateEnt(ent);
if (ent.getMetadata(PlayerID, "linked-dropsite-nearby") == true) {
dropsite.setMetadata(PlayerID, "resource-quantity-" + resource, +dropsite.getMetadata(PlayerID, "resource-quantity-" + resource) - (+ent.getMetadata(PlayerID, "dp-update-value")));
dropsite.getMetadata(PlayerID, "linked-resources-" + resource).updateEnt(ent);
dropsite.getMetadata(PlayerID, "nearby-resources-" + resource).updateEnt(ent);
} else {
dropsite.setMetadata("resource-quantity-far-" + resource, +dropsite.getMetadata("resource-quantity-" + resource) - (+ent.getMetadata("dp-update-value")));
dropsite.getMetadata("linked-resources-" + resource).updateEnt(ent);
dropsite.setMetadata(PlayerID, "resource-quantity-far-" + resource, +dropsite.getMetadata(PlayerID, "resource-quantity-" + resource) - (+ent.getMetadata(PlayerID, "dp-update-value")));
dropsite.getMetadata(PlayerID, "linked-resources-" + resource).updateEnt(ent);
}
}
}else{
if (ent.getMetadata("full")){
ent.setMetadata("full", false);
if (ent.getMetadata(PlayerID, "full")){
ent.setMetadata(PlayerID, "full", false);
// update the dropsite
var dropsite = gameState.getEntityById(ent.getMetadata("linked-dropsite"));
if (dropsite == undefined || dropsite.getMetadata("linked-resources-" + resource) === undefined)
var dropsite = gameState.getEntityById(ent.getMetadata(PlayerID, "linked-dropsite"));
if (dropsite == undefined || dropsite.getMetadata(PlayerID, "linked-resources-" + resource) === undefined)
return;
if (ent.getMetadata("linked-dropsite-nearby") == true) {
dropsite.setMetadata("resource-quantity-" + resource, +dropsite.getMetadata("resource-quantity-" + resource) + ent.resourceSupplyAmount());
dropsite.getMetadata("linked-resources-" + resource).updateEnt(ent);
dropsite.getMetadata("nearby-resources-" + resource).updateEnt(ent);
if (ent.getMetadata(PlayerID, "linked-dropsite-nearby") == true) {
dropsite.setMetadata(PlayerID, "resource-quantity-" + resource, +dropsite.getMetadata(PlayerID, "resource-quantity-" + resource) + ent.resourceSupplyAmount());
dropsite.getMetadata(PlayerID, "linked-resources-" + resource).updateEnt(ent);
dropsite.getMetadata(PlayerID, "nearby-resources-" + resource).updateEnt(ent);
} else {
dropsite.setMetadata("resource-quantity-far-" + resource, +dropsite.getMetadata("resource-quantity-" + resource) + ent.resourceSupplyAmount());
dropsite.getMetadata("linked-resources-" + resource).updateEnt(ent);
dropsite.setMetadata(PlayerID, "resource-quantity-far-" + resource, +dropsite.getMetadata(PlayerID, "resource-quantity-" + resource) + ent.resourceSupplyAmount());
dropsite.getMetadata(PlayerID, "linked-resources-" + resource).updateEnt(ent);
}
}
}
};
Worker.prototype.startGathering = function(gameState){
var resource = this.ent.getMetadata("gather-type");
var resource = this.ent.getMetadata(PlayerID, "gather-type");
var ent = this.ent;
if (!ent.position()){
@ -172,6 +200,8 @@ Worker.prototype.startGathering = function(gameState){
return;
}
this.unsatisfactoryResource = false;
// TODO: this is not necessarily optimal.
// find closest dropsite which has nearby resources of the correct type
@ -179,79 +209,148 @@ Worker.prototype.startGathering = function(gameState){
var nearestResources = undefined;
var nearestDropsite = undefined;
// first, look for nearby resources.
// first step: count how many dropsites we have that have enough resources "close" to them.
// TODO: this is a huge part of multi-base support. Count only those in the same base as the worker.
var number = 0;
gameState.getOwnDropsites(resource).forEach(function (dropsite){ if (dropsite.getMetadata("linked-resources-" +resource) !== undefined
&& dropsite.getMetadata("linked-resources-" +resource).length > 3) { number++; } });
gameState.getOwnDropsites(resource).forEach(function (dropsite){ //}){
if (dropsite.getMetadata("resource-quantity-" +resource) == undefined)
return;
if (dropsite.position() && (dropsite.getMetadata("resource-quantity-" +resource) > 10 || number <= 1) ) {
var dist = SquareVectorDistance(ent.position(), dropsite.position());
if (dist < minDropsiteDist){
minDropsiteDist = dist;
nearestResources = dropsite.getMetadata("linked-resources-" + resource);
nearestDropsite = dropsite;
}
var ourDropsites = gameState.getOwnDropsites(resource);
if (ourDropsites.length === 0)
{
debug ("We do not have a dropsite for " + resource + ", aborting");
return;
}
ourDropsites.forEach(function (dropsite) {
if (dropsite.getMetadata(PlayerID, "linked-resources-" +resource) !== undefined
&& dropsite.getMetadata(PlayerID, "resource-quantity-" +resource) !== undefined && dropsite.getMetadata(PlayerID, "resource-quantity-" +resource) > 200) {
number++;
}
});
// none, check even low level of resources and far away
if (!nearestResources || nearestResources.length === 0){
gameState.getOwnDropsites(resource).forEach(function (dropsite){ //}){
if (dropsite.position() &&
(dropsite.getMetadata("resource-quantity-" +resource)+dropsite.getMetadata("resource-quantity-far-" +resource) > 10 || number <= 1)) {
//debug ("Available " +resource + " dropsites: " +ourDropsites.length);
// Allright second step, if there are any such dropsites, we pick the closest.
// we pick one with a lot of resource, or we pick the only one available (if it's high enough, otherwise we'll see with "far" below).
if (number > 0)
{
ourDropsites.forEach(function (dropsite) { //}){
if (dropsite.getMetadata(PlayerID, "resource-quantity-" +resource) == undefined)
return;
if (dropsite.position() && (dropsite.getMetadata(PlayerID, "resource-quantity-" +resource) > 700 || (number === 1 && dropsite.getMetadata(PlayerID, "resource-quantity-" +resource) > 200) ) ) {
var dist = SquareVectorDistance(ent.position(), dropsite.position());
if (dist < minDropsiteDist){
minDropsiteDist = dist;
nearestResources = dropsite.getMetadata("linked-resources-" + resource);
nearestResources = dropsite.getMetadata(PlayerID, "linked-resources-" + resource);
nearestDropsite = dropsite;
}
}
});
}
// else, just get the closest to our closest dropsite.
if (!nearestResources || nearestResources.length === 0){
nearestResources = gameState.getResourceSupplies(resource);
gameState.getOwnDropsites(resource).forEach(function (dropsite){
if (dropsite.position()){
//debug ("Nearest dropsite: " +nearestDropsite);
// Now if we have no dropsites, we repeat the process with resources "far" from dropsites but still linked with them.
// I add the "close" value for code sanity.
// Again, we choose a dropsite with a lot of resources left, or we pick the only one available (in this case whatever happens).
if (!nearestResources || nearestResources.length === 0) {
//debug ("here(1)");
gameState.getOwnDropsites(resource).forEach(function (dropsite){ //}){
var quantity = dropsite.getMetadata(PlayerID, "resource-quantity-" +resource)+dropsite.getMetadata(PlayerID, "resource-quantity-far-" +resource);
if (dropsite.position() && (quantity) > 700 || number === 1) {
var dist = SquareVectorDistance(ent.position(), dropsite.position());
if (dist < minDropsiteDist){
minDropsiteDist = dist;
nearestResources = dropsite.getMetadata(PlayerID, "linked-resources-" + resource);
nearestDropsite = dropsite;
}
}
});
this.unsatisfactoryResource = true;
//debug ("Nearest dropsite: " +nearestDropsite);
}
// If we still haven't found any fitting dropsite...
// Then we'll just pick any resource, and we'll check for the closest dropsite to that one
if (!nearestResources || nearestResources.length === 0){
//debug ("No fitting dropsite for " + resource + " found, iterating the map.");
nearestResources = gameState.getResourceSupplies(resource);
this.unsatisfactoryResource = true;
}
if (nearestResources.length === 0){
if (resource === "food" && !this.buildAnyField(gameState)) // try to go build a farm
if (resource === "food")
{
if (this.buildAnyField(gameState))
return;
debug("No " + resource + " found! (1)");
}
else
debug("No " + resource + " found! (1)");
return;
}
//debug("Found " + nearestResources.length + "spots for " + resource);
if (!nearestDropsite) {
/*if (!nearestDropsite) {
debug ("No dropsite for " +resource);
return;
}
}*/
var supplies = [];
var nearestSupplyDist = Math.min();
var nearestSupply = undefined;
// filter resources
// TODo: add a bonus for resources with a lot of resources left, perhaps, to spread gathering?
nearestResources.forEach(function(supply) { //}){
// TODO: handle enemy territories
if (!supply.position()){
// sanity check, perhaps sheep could be garrisoned?
if (!supply.position()) {
//debug ("noposition");
return;
}
if (supply.getMetadata(PlayerID, "inaccessible") === true) {
//debug ("inaccessible");
return;
}
// too many workers trying to gather from this resource
if (supply.getMetadata(PlayerID, "full") === true) {
//debug ("full");
return;
}
// Don't gather enemy farms
if (!supply.isOwn(PlayerID) && supply.owner() !== 0) {
//debug ("enemy");
return;
}
var territoryOwner = Map.createTerritoryMap(gameState).getOwner(supply.position());
if (territoryOwner != PlayerID && territoryOwner != 0) {
//debug ("enemy territory");
return;
}
// quickscope accessbility check.
if (!gameState.ai.accessibility.pathAvailable(gameState, ent.position(), supply.position(), true)) {
//debug ("nopath");
return;
}
// some simple check for chickens: if they're in a square that's inaccessible, we won't gather from them.
if (supply.footprintRadius() < 1)
{
var fakeMap = new Map(gameState,gameState.getMap().data);
var id = fakeMap.gamePosToMapPos(supply.position())[0] + fakeMap.width*fakeMap.gamePosToMapPos(supply.position())[1];
if ( (gameState.sharedScript.passabilityClasses["pathfinderObstruction"] & gameState.getMap().data[id]) )
{
supply.setMetadata(PlayerID, "inaccessible", true)
return;
}
}
// measure the distance to the resource (largely irrelevant)
var dist = SquareVectorDistance(supply.position(), ent.position());
// Add on a factor for the nearest dropsite if one exists
if (supply.getMetadata("linked-dropsite") !== undefined){
if (nearestDropsite !== undefined ){
dist += 4*SquareVectorDistance(supply.position(), nearestDropsite.position());
dist /= 5.0;
}
@ -260,8 +359,8 @@ Worker.prototype.startGathering = function(gameState){
if (dist < 40000 && supply.resourceSupplyType().generic == "treasure"){
dist /= 1000;
}
if (dist < nearestSupplyDist){
if (dist < nearestSupplyDist) {
nearestSupplyDist = dist;
nearestSupply = supply;
}
@ -270,38 +369,64 @@ Worker.prototype.startGathering = function(gameState){
if (nearestSupply) {
var pos = nearestSupply.position();
// find a fitting dropsites in case we haven't already.
if (!nearestDropsite) {
ourDropsites.forEach(function (dropsite){ //}){
if (dropsite.position()){
var dist = SquareVectorDistance(pos, dropsite.position());
if (dist < minDropsiteDist){
minDropsiteDist = dist;
nearestDropsite = dropsite;
}
}
});
if (!nearestDropsite)
{
debug ("No dropsite for " +resource);
return;
}
}
// if the resource is far away, try to build a farm instead.
var tried = false;
if (resource === "food" && SquareVectorDistance(pos,this.ent.position()) > 22500)
{
tried = this.buildAnyField(gameState);
if (!tried && SquareVectorDistance(pos,this.ent.position()) > 62500) {
return; // wait. a farm should appear.
}
if (!tried) {
var territoryOwner = gameState.getTerritoryMap().getOwner(pos);
if (!gameState.ai.accessibility.isAccessible(pos) ||
(territoryOwner != gameState.getPlayerID() && territoryOwner != 0)){
nearestSupply.setMetadata("inaccessible", true);
}else{
ent.gather(nearestSupply);
if (!tried && SquareVectorDistance(pos,this.ent.position()) > 62500) {
return; // wait. a farm should appear.
}
}
}else{
if (!tried) {
this.maxApproachTime = Math.max(25000, VectorDistance(pos,this.ent.position()) * 1000);
ent.gather(nearestSupply);
}
} else {
if (resource === "food" && this.buildAnyField(gameState))
return;
debug("No " + resource + " found! (2)");
// If we had a fitting closest dropsite with a lot of resources, mark it as not good. It means it's probably full. Then retry.
// it'll be resetted next time it's counted anyway.
if (nearestDropsite && nearestDropsite.getMetadata(PlayerID, "resource-quantity-" +resource)+nearestDropsite.getMetadata(PlayerID, "resource-quantity-far-" +resource) > 400)
{
nearestDropsite.setMetadata(PlayerID, "resource-quantity-" +resource, 0);
nearestDropsite.setMetadata(PlayerID, "resource-quantity-far-" +resource, 0);
this.startGathering(gameState);
}
}
};
// Makes the worker deposit the currently carried resources at the closest dropsite
Worker.prototype.returnResources = function(gameState){
if (!this.ent.resourceCarrying() || this.ent.resourceCarrying().length === 0){
return;
return true; // assume we're OK.
}
var resource = this.ent.resourceCarrying()[0].type;
var self = this;
if (!this.ent.position()){
// TODO: work out what to do when entity has no position
return;
return true;
}
var closestDropsite = undefined;
@ -317,11 +442,12 @@ Worker.prototype.returnResources = function(gameState){
});
if (!closestDropsite){
debug("No dropsite found for " + resource);
return;
debug("No dropsite found to deposit " + resource);
return false;
}
this.ent.returnResources(closestDropsite);
return true;
};
Worker.prototype.getResourceType = function(type){

View File

@ -1,5 +1,6 @@
{
"name": "qBot",
"description": "Quantumstate's improved version of the Test Bot",
"constructor": "QBotAI"
"constructor": "QBotAI",
"useShared" : false
}

View File

@ -14,7 +14,7 @@ AIInterface.prototype.GetRepresentation = function()
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
// Return the same game state as the GUI uses
var state = cmpGuiInterface.GetSimulationState(-1);
var state = cmpGuiInterface.GetExtendedSimulationState(-1);
// Add some extra AI-specific data
state.events = this.events;
@ -36,6 +36,31 @@ AIInterface.prototype.GetRepresentation = function()
return state;
};
// Intended to be called first, during the map initialization: no caching
AIInterface.prototype.GetFullRepresentation = function()
{
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
// Return the same game state as the GUI uses
var state = cmpGuiInterface.GetSimulationState(-1);
// Add some extra AI-specific data
state.events = this.events;
// Add entity representations
Engine.ProfileStart("proxy representations");
state.entities = {};
// all entities are changed in the initial state.
for (var id in this.changedEntities)
{
var aiProxy = Engine.QueryInterface(+id, IID_AIProxy);
if (aiProxy)
state.entities[id] = aiProxy.GetFullRepresentation();
}
Engine.ProfileStop();
return state;
};
AIInterface.prototype.ChangedEntity = function(ent)
{

View File

@ -90,7 +90,12 @@ GuiInterface.prototype.GetSimulationState = function(player)
"isEnemy": enemies,
"entityLimits": cmpPlayerEntityLimits.GetLimits(),
"entityCounts": cmpPlayerEntityLimits.GetCounts(),
"techModifications": cmpTechnologyManager.GetTechModifications()
"techModifications": cmpTechnologyManager.GetTechModifications(),
"researchQueued": cmpTechnologyManager.GetQueuedResearch(),
"researchStarted": cmpTechnologyManager.GetStartedResearch(),
"researchedTechs": cmpTechnologyManager.GetResearchedTechs(),
"classCounts": cmpTechnologyManager.GetClassCounts(),
"typeCountsByClass": cmpTechnologyManager.GetTypeCountsByClass()
};
ret.players.push(playerData);
}
@ -1451,6 +1456,7 @@ GuiInterface.prototype.GetFoundationSnapData = function(player, data)
if (template.BuildRestrictions.Category == "Dock")
{
// warning: copied almost identically in helpers/command.js , "GetDockAngle".
var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
var cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager);
if (!cmpTerrain || !cmpWaterManager)

View File

@ -422,4 +422,26 @@ TechnologyManager.prototype.GetTechModifications = function()
return this.modifications;
};
// called by GUIInterface for PlayerData. AI use.
TechnologyManager.prototype.GetQueuedResearch = function()
{
return this.researchQueued;
};
TechnologyManager.prototype.GetStartedResearch = function()
{
return this.researchStarted;
};
TechnologyManager.prototype.GetResearchedTechs = function()
{
return this.researchedTechs;
};
TechnologyManager.prototype.GetClassCounts = function()
{
return this.classCounts;
};
TechnologyManager.prototype.GetTypeCountsByClass = function()
{
return this.typeCountsByClass;
};
Engine.RegisterComponentType(IID_TechnologyManager, "TechnologyManager", TechnologyManager);

View File

@ -1 +0,0 @@
Engine.RegisterInterface("TechnologyTemplateManager");

View File

@ -532,6 +532,99 @@ function ExtractFormations(ents)
return { "entities": entities, "members": members, "ids": ids };
}
/**
* Tries to find the best angle to put a dock at a given position
* Taken from GuiInterface.js
*/
function GetDockAngle(templateName,x,y)
{
var cmpTemplateMgr = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateMgr.GetTemplate(templateName);
if (template.BuildRestrictions.Category !== "Dock")
return undefined;
var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
var cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager);
if (!cmpTerrain || !cmpWaterManager)
{
return undefined;
}
// Get footprint size
var halfSize = 0;
if (template.Footprint.Square)
{
halfSize = Math.max(template.Footprint.Square["@depth"], template.Footprint.Square["@width"])/2;
}
else if (template.Footprint.Circle)
{
halfSize = template.Footprint.Circle["@radius"];
}
/* Find direction of most open water, algorithm:
* 1. Pick points in a circle around dock
* 2. If point is in water, add to array
* 3. Scan array looking for consecutive points
* 4. Find longest sequence of consecutive points
* 5. If sequence equals all points, no direction can be determined,
* expand search outward and try (1) again
* 6. Calculate angle using average of sequence
*/
const numPoints = 16;
for (var dist = 0; dist < 4; ++dist)
{
var waterPoints = [];
for (var i = 0; i < numPoints; ++i)
{
var angle = (i/numPoints)*2*Math.PI;
var d = halfSize*(dist+1);
var nx = x - d*Math.sin(angle);
var nz = y + d*Math.cos(angle);
if (cmpTerrain.GetGroundLevel(nx, nz) < cmpWaterManager.GetWaterLevel(nx, nz))
{
waterPoints.push(i);
}
}
var consec = [];
var length = waterPoints.length;
for (var i = 0; i < length; ++i)
{
var count = 0;
for (var j = 0; j < (length-1); ++j)
{
if (((waterPoints[(i + j) % length]+1) % numPoints) == waterPoints[(i + j + 1) % length])
{
++count;
}
else
{
break;
}
}
consec[i] = count;
}
var start = 0;
var count = 0;
for (var c in consec)
{
if (consec[c] > count)
{
start = c;
count = consec[c];
}
}
// If we've found a shoreline, stop searching
if (count != numPoints-1)
{
return -(((waterPoints[start] + consec[start]/2) % numPoints)/numPoints*2*Math.PI);
}
}
return undefined;
}
/**
* Attempts to construct a building using the specified parameters.
* Returns true on success, false on failure.
@ -581,6 +674,11 @@ function TryConstructBuilding(player, cmpPlayer, controlAllUnits, cmd)
return false;
}
// If it's a dock, get the right angle.
var angle = GetDockAngle(cmd.template,cmd.x,cmd.z);
if (angle !== undefined)
cmd.angle = angle;
// Move the foundation to the right place
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
cmpPosition.JumpTo(cmd.x, cmd.z);

View File

@ -32,6 +32,8 @@ function InitGame(settings)
newResourceCounts[resouces] = settings.StartingResources;
cmpPlayer.SetResourceCounts(newResourceCounts);
}
cmpAIManager.TryLoadSharedComponent();
cmpAIManager.RunGamestateInit();
}
Engine.RegisterGlobal("InitGame", InitGame);

View File

@ -111,11 +111,6 @@ public:
componentManager.AddComponent(SYSTEM_ENTITY, CID_TerritoryManager, noParam);
componentManager.AddComponent(SYSTEM_ENTITY, CID_WaterManager, noParam);
if (!skipAI)
{
componentManager.AddComponent(SYSTEM_ENTITY, CID_AIManager, noParam);
}
// Add scripted system components:
if (!skipScriptedComponents)
{
@ -134,6 +129,12 @@ public:
LOAD_SCRIPTED_COMPONENT("Timer");
#undef LOAD_SCRIPTED_COMPONENT
if (!skipAI)
{
componentManager.AddComponent(SYSTEM_ENTITY, CID_AIManager, noParam);
}
}
}

View File

@ -141,6 +141,9 @@ COMPONENT(SoundManager)
INTERFACE(TechnologyManager)
COMPONENT(TechnologyManagerScripted)
INTERFACE(TechnologyTemplateManager)
COMPONENT(TechnologyTemplateManagerScripted)
INTERFACE(Terrain)
COMPONENT(Terrain)

View File

@ -34,6 +34,7 @@
#include "simulation2/components/ICmpObstructionManager.h"
#include "simulation2/components/ICmpRangeManager.h"
#include "simulation2/components/ICmpTemplateManager.h"
#include "simulation2/components/ICmpTechnologyTemplateManager.h"
#include "simulation2/components/ICmpTerritoryManager.h"
#include "simulation2/helpers/Grid.h"
#include "simulation2/serialization/DebugSerializer.h"
@ -86,6 +87,8 @@ private:
m_ScriptInterface.LoadGlobalScripts();
m_ScriptInterface.RegisterFunction<void, std::wstring, CAIPlayer::IncludeModule>("IncludeModule");
m_ScriptInterface.RegisterFunction<void, CAIPlayer::DumpHeap>("DumpHeap");
m_ScriptInterface.RegisterFunction<void, CAIPlayer::ForceGC>("ForceGC");
m_ScriptInterface.RegisterFunction<void, CScriptValRooted, CAIPlayer::PostCommand>("PostCommand");
m_ScriptInterface.RegisterFunction<void, std::wstring, std::vector<u32>, u32, u32, u32, CAIPlayer::DumpImage>("DumpImage");
@ -104,7 +107,19 @@ private:
self->LoadScripts(name);
}
static void DumpHeap(void* cbdata)
{
CAIPlayer* self = static_cast<CAIPlayer*> (cbdata);
//std::cout << JS_GetGCParameter(self->m_ScriptInterface.GetRuntime(), JSGC_BYTES) << std::endl;
self->m_ScriptInterface.DumpHeap();
}
static void ForceGC(void* cbdata)
{
CAIPlayer* self = static_cast<CAIPlayer*> (cbdata);
JS_GC(self->m_ScriptInterface.GetContext());
}
static void PostCommand(void* cbdata, CScriptValRooted cmd)
{
CAIPlayer* self = static_cast<CAIPlayer*> (cbdata);
@ -206,6 +221,8 @@ private:
return false;
}
m_ScriptInterface.GetProperty(metadata.get(), "useShared", m_UseSharedComponent);
CScriptVal obj;
if (callConstructor)
@ -242,11 +259,24 @@ private:
m_Commands.clear();
m_ScriptInterface.CallFunctionVoid(m_Obj.get(), "HandleMessage", state);
}
// overloaded with a sharedAI part.
// javascript can handle both natively on the same function.
void Run(CScriptVal state, CScriptValRooted SharedAI)
{
m_Commands.clear();
m_ScriptInterface.CallFunctionVoid(m_Obj.get(), "HandleMessage", state, SharedAI);
}
void InitWithSharedScript(CScriptVal state, CScriptValRooted SharedAI)
{
m_Commands.clear();
m_ScriptInterface.CallFunctionVoid(m_Obj.get(), "InitWithSharedScript", state, SharedAI);
}
CAIWorker& m_Worker;
std::wstring m_AIName;
player_id_t m_Player;
bool m_UseSharedComponent;
ScriptInterface m_ScriptInterface;
CScriptValRooted m_Obj;
std::vector<shared_ptr<ScriptInterface::StructuredClone> > m_Commands;
@ -261,20 +291,28 @@ public:
};
CAIWorker() :
// TODO: Passing a 32 MB argument to CreateRuntime() is a temporary fix
// TODO: Passing a 24 MB argument to CreateRuntime() is a temporary fix
// to prevent frequent AI out-of-memory crashes. The argument should be
// removed as soon as AI data-sharing has been implemented. See #1650.
m_ScriptRuntime(ScriptInterface::CreateRuntime(33554432)),
// removed as soon whenever the new pathfinder is committed
// And the AIs can stop relying on their own little hands.
m_ScriptRuntime(ScriptInterface::CreateRuntime(25165824)),
m_ScriptInterface("Engine", "AI", m_ScriptRuntime),
m_TurnNum(0),
m_CommandsComputed(true),
m_HasLoadedEntityTemplates(false)
m_HasLoadedEntityTemplates(false),
m_HasSharedComponent(false)
{
m_ScriptInterface.SetCallbackData(static_cast<void*> (this));
// TODO: ought to seed the RNG (in a network-synchronised way) before we use it
m_ScriptInterface.ReplaceNondeterministicRNG(m_RNG);
m_ScriptInterface.LoadGlobalScripts();
m_ScriptInterface.SetCallbackData(NULL);
m_ScriptInterface.RegisterFunction<void, CScriptValRooted, CAIWorker::PostCommand>("PostCommand");
m_ScriptInterface.RegisterFunction<void, CAIWorker::DumpHeap>("DumpHeap");
m_ScriptInterface.RegisterFunction<void, CAIWorker::ForceGC>("ForceGC");
}
~CAIWorker()
@ -288,17 +326,161 @@ public:
m_TerritoryMapVal = CScriptValRooted();
}
// This is called by AIs if they use the v3 API.
// If the AIs originate the call, cbdata is not NULL.
// If the shared component does, it is, so it must not be taken into account.
static void PostCommand(void* cbdata, CScriptValRooted cmd)
{
if (cbdata == NULL) {
debug_warn(L"Warning: the shared component has tried to push an engine command. Ignoring.");
return;
}
CAIPlayer* self = static_cast<CAIPlayer*> (cbdata);
self->m_Commands.push_back(self->m_ScriptInterface.WriteStructuredClone(cmd.get()));
}
// The next two ought to be implmeneted someday but for now as it returns "null" it can't
static void DumpHeap(void* cbdata)
{
if (cbdata == NULL) {
debug_warn(L"Warning: the shared component has asked for DumpHeap. Ignoring.");
return;
}
CAIWorker* self = static_cast<CAIWorker*> (cbdata);
self->m_ScriptInterface.DumpHeap();
}
static void ForceGC(void* cbdata)
{
if (cbdata == NULL) {
debug_warn(L"Warning: the shared component has asked for ForceGC. Ignoring.");
return;
}
CAIWorker* self = static_cast<CAIWorker*> (cbdata);
PROFILE3("AI compute GC");
JS_GC(self->m_ScriptInterface.GetContext());
}
bool TryLoadSharedComponent(bool callConstructor)
{
// only load if there are AI players.
if (m_Players.size() == 0)
return false;
// we don't need to load it.
if (!m_HasSharedComponent)
return false;
// reset the value so it can be used to determine if we actually initialized it.
m_HasSharedComponent = false;
VfsPaths sharedPathnames;
// Check for "shared" module.
vfs::GetPathnames(g_VFS, L"simulation/ai/common-api-v3/", L"*.js", sharedPathnames);
for (VfsPaths::iterator it = sharedPathnames.begin(); it != sharedPathnames.end(); ++it)
{
if (!m_ScriptInterface.LoadGlobalScriptFile(*it))
{
LOGERROR(L"Failed to load shared script %ls", it->string().c_str());
return false;
}
m_HasSharedComponent = true;
}
if (!m_HasSharedComponent)
return false;
// mainly here for the error messages
OsPath path = L"simulation/ai/common-api-v2/";
// Constructor name is SharedScript
CScriptVal ctor;
if (!m_ScriptInterface.GetProperty(m_ScriptInterface.GetGlobalObject(), "SharedScript", ctor)
|| ctor.undefined())
{
LOGERROR(L"Failed to create shared AI component: %ls: can't find constructor '%hs'", path.string().c_str(), "SharedScript");
return false;
}
if (callConstructor)
{
// Set up the data to pass as the constructor argument
CScriptVal settings;
m_ScriptInterface.Eval(L"({})", settings);
CScriptVal playersID;
m_ScriptInterface.Eval(L"({})", playersID);
for (size_t i = 0; i < m_Players.size(); ++i)
{
jsval val = m_ScriptInterface.ToJSVal(m_ScriptInterface.GetContext(), m_Players[i]->m_Player);
m_ScriptInterface.SetPropertyInt(playersID.get(), i, CScriptVal(val), true);
}
m_ScriptInterface.SetProperty(settings.get(), "players", playersID);
ENSURE(m_HasLoadedEntityTemplates);
m_ScriptInterface.SetProperty(settings.get(), "templates", m_EntityTemplates, false);
m_ScriptInterface.SetProperty(settings.get(), "techTemplates", m_TechTemplates, false);
m_SharedAIObj = CScriptValRooted(m_ScriptInterface.GetContext(),m_ScriptInterface.CallConstructor(ctor.get(), settings.get()));
}
else
{
// For deserialization, we want to create the object with the correct prototype
// but don't want to actually run the constructor again
// XXX: actually we don't currently use this path for deserialization - maybe delete it?
m_SharedAIObj = CScriptValRooted(m_ScriptInterface.GetContext(),m_ScriptInterface.NewObjectFromConstructor(ctor.get()));
}
if (m_SharedAIObj.undefined())
{
LOGERROR(L"Failed to create shared AI component: %ls: error calling constructor '%hs'", path.string().c_str(), "SharedScript");
return false;
}
return true;
}
bool AddPlayer(const std::wstring& aiName, player_id_t player, bool callConstructor)
{
shared_ptr<CAIPlayer> ai(new CAIPlayer(*this, aiName, player, m_ScriptRuntime, m_RNG));
if (!ai->Initialise(callConstructor))
return false;
// this will be set to true if we need to load the shared Component.
if (!m_HasSharedComponent)
m_HasSharedComponent = ai->m_UseSharedComponent;
m_ScriptInterface.MaybeGC();
m_Players.push_back(ai);
return true;
}
bool RunGamestateInit(const shared_ptr<ScriptInterface::StructuredClone>& gameState, const Grid<u16>& passabilityMap, const Grid<u8>& territoryMap)
{
// this will be run last by InitGame.Js, passing the full game representation.
// For now it will run for the shared Component.
CScriptVal state = m_ScriptInterface.ReadStructuredClone(gameState);
JSContext* cx = m_ScriptInterface.GetContext();
m_PassabilityMapVal = CScriptValRooted(cx, ScriptInterface::ToJSVal(cx, passabilityMap));
m_TerritoryMapVal = CScriptValRooted(cx, ScriptInterface::ToJSVal(cx, territoryMap));
if (m_HasSharedComponent)
{
m_ScriptInterface.SetProperty(state.get(), "passabilityMap", m_PassabilityMapVal, true);
m_ScriptInterface.SetProperty(state.get(), "territoryMap", m_TerritoryMapVal, true);
m_ScriptInterface.CallFunctionVoid(m_SharedAIObj.get(), "initWithState", state);
m_ScriptInterface.MaybeGC();
for (size_t i = 0; i < m_Players.size(); ++i)
{
if (m_HasSharedComponent && m_Players[i]->m_UseSharedComponent)
m_Players[i]->InitWithSharedScript(state,m_SharedAIObj);
}
}
return true;
}
void StartComputation(const shared_ptr<ScriptInterface::StructuredClone>& gameState, const Grid<u16>& passabilityMap, const Grid<u8>& territoryMap, bool territoryMapDirty)
{
ENSURE(m_CommandsComputed);
@ -335,8 +517,8 @@ public:
void GetCommands(std::vector<SCommandSets>& commands)
{
WaitToFinishComputation();
WaitToFinishComputation();
commands.clear();
commands.resize(m_Players.size());
for (size_t i = 0; i < m_Players.size(); ++i)
@ -346,6 +528,11 @@ public:
}
}
void RegisterTechTemplates(const shared_ptr<ScriptInterface::StructuredClone>& techTemplates) {
JSContext* cx = m_ScriptInterface.GetContext();
m_TechTemplates = CScriptValRooted(cx, m_ScriptInterface.ReadStructuredClone(techTemplates));
}
void LoadEntityTemplates(const std::vector<std::pair<std::string, const CParamNode*> >& templates)
{
m_HasLoadedEntityTemplates = true;
@ -454,6 +641,11 @@ public:
LOGERROR(L"AI script Deserialize call failed");
}
}
int getPlayerSize()
{
return m_Players.size();
}
private:
CScriptValRooted LoadMetadata(const VfsPath& path)
@ -469,6 +661,19 @@ private:
void PerformComputation()
{
if (m_Players.size() == 0)
{
// Run the GC every so often.
// (This isn't particularly necessary, but it makes profiling clearer
// since it avoids random GC delays while running other scripts)
if (m_TurnNum++ % 50 == 0)
{
PROFILE3("AI compute GC");
m_ScriptInterface.MaybeGC();
}
return;
}
// Deserialize the game state, to pass to the AI's HandleMessage
CScriptVal state;
{
@ -483,23 +688,41 @@ private:
// to prevent AI scripts accidentally modifying the state and
// affecting other AI scripts they share it with. But the performance
// cost is far too high, so we won't do that.
// If there is a shared component, run it
if (m_HasSharedComponent)
{
PROFILE3("AI run shared component");
m_ScriptInterface.CallFunctionVoid(m_SharedAIObj.get(), "onUpdate", state);
}
for (size_t i = 0; i < m_Players.size(); ++i)
{
PROFILE3("AI script");
PROFILE2_ATTR("player: %d", m_Players[i]->m_Player);
PROFILE2_ATTR("script: %ls", m_Players[i]->m_AIName.c_str());
m_Players[i]->Run(state);
if (m_HasSharedComponent && m_Players[i]->m_UseSharedComponent)
m_Players[i]->Run(state,m_SharedAIObj);
else
m_Players[i]->Run(state);
}
// Run GC if we are about to overflow
if (JS_GetGCParameter(m_ScriptInterface.GetRuntime(), JSGC_BYTES) > 24000000)
{
PROFILE3("AI compute GC");
JS_GC(m_ScriptInterface.GetContext());
}
// Run the GC every so often.
// (This isn't particularly necessary, but it makes profiling clearer
// since it avoids random GC delays while running other scripts)
if (m_TurnNum++ % 25 == 0)
/*if (m_TurnNum++ % 20 == 0)
{
PROFILE3("AI compute GC");
m_ScriptInterface.MaybeGC();
}
}*/
}
shared_ptr<ScriptRuntime> m_ScriptRuntime;
@ -509,10 +732,15 @@ private:
CScriptValRooted m_EntityTemplates;
bool m_HasLoadedEntityTemplates;
CScriptValRooted m_TechTemplates;
std::map<VfsPath, CScriptValRooted> m_PlayerMetadata;
std::vector<shared_ptr<CAIPlayer> > m_Players; // use shared_ptr just to avoid copying
bool m_HasSharedComponent;
CScriptValRooted m_SharedAIObj;
std::vector<SCommandSets> m_Commands;
shared_ptr<ScriptInterface::StructuredClone> m_GameState;
Grid<u16> m_PassabilityMap;
CScriptValRooted m_PassabilityMapVal;
@ -546,6 +774,17 @@ public:
m_TerritoriesDirtyID = 0;
StartLoadEntityTemplates();
// loading the technology template
ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface();
CmpPtr<ICmpTechnologyTemplateManager> cmpTechTemplateManager(GetSimContext(), SYSTEM_ENTITY);
ENSURE(cmpTechTemplateManager);
// Get the game state from AIInterface
CScriptVal techTemplates = cmpTechTemplateManager->GetAllTechs();
m_Worker.RegisterTechTemplates(scriptInterface.WriteStructuredClone(techTemplates.get()));
}
virtual void Deinit()
@ -605,6 +844,45 @@ public:
if (cmpRangeManager)
cmpRangeManager->SetLosRevealAll(player, true);
}
virtual void TryLoadSharedComponent()
{
m_Worker.TryLoadSharedComponent(true);
}
virtual void RunGamestateInit()
{
ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface();
CmpPtr<ICmpAIInterface> cmpAIInterface(GetSimContext(), SYSTEM_ENTITY);
ENSURE(cmpAIInterface);
// Get the game state from AIInterface
CScriptVal state = cmpAIInterface->GetFullRepresentation();
// Get the passability data
Grid<u16> dummyGrid;
const Grid<u16>* passabilityMap = &dummyGrid;
CmpPtr<ICmpPathfinder> cmpPathfinder(GetSimContext(), SYSTEM_ENTITY);
if (cmpPathfinder)
passabilityMap = &cmpPathfinder->GetPassabilityGrid();
// Get the territory data
// Since getting the territory grid can trigger a recalculation, we check NeedUpdate first
bool territoryMapDirty = false;
Grid<u8> dummyGrid2;
const Grid<u8>* territoryMap = &dummyGrid2;
CmpPtr<ICmpTerritoryManager> cmpTerritoryManager(GetSimContext(), SYSTEM_ENTITY);
if (cmpTerritoryManager && cmpTerritoryManager->NeedUpdate(&m_TerritoriesDirtyID))
{
territoryMap = &cmpTerritoryManager->GetTerritoryGrid();
territoryMapDirty = true;
}
LoadPathfinderClasses(state);
m_Worker.RunGamestateInit(scriptInterface.WriteStructuredClone(state.get()), *passabilityMap, *territoryMap);
}
virtual void StartComputation()
{
@ -614,6 +892,9 @@ public:
ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface();
if (m_Worker.getPlayerSize() == 0)
return;
CmpPtr<ICmpAIInterface> cmpAIInterface(GetSimContext(), SYSTEM_ENTITY);
ENSURE(cmpAIInterface);

View File

@ -34,6 +34,11 @@ public:
{
return m_Script.Call<CScriptVal> ("GetRepresentation");
}
virtual CScriptVal GetFullRepresentation()
{
return m_Script.Call<CScriptVal> ("GetFullRepresentation");
}
};
REGISTER_COMPONENT_SCRIPT_WRAPPER(AIInterfaceScripted)

View File

@ -28,6 +28,11 @@ public:
* to be passed to AI scripts.
*/
virtual CScriptVal GetRepresentation() = 0;
/**
* Returns a script object that represents the current world state,
* to be passed to AI scripts. No caching for initialization
*/
virtual CScriptVal GetFullRepresentation() = 0;
DECLARE_INTERFACE_TYPE(AIInterface)
};

View File

@ -26,6 +26,8 @@
BEGIN_INTERFACE_WRAPPER(AIManager)
DEFINE_INTERFACE_METHOD_2("AddPlayer", void, ICmpAIManager, AddPlayer, std::wstring, player_id_t)
DEFINE_INTERFACE_METHOD_0("TryLoadSharedComponent", void, ICmpAIManager, TryLoadSharedComponent)
DEFINE_INTERFACE_METHOD_0("RunGamestateInit", void, ICmpAIManager, RunGamestateInit)
END_INTERFACE_WRAPPER(AIManager)
// Implement the static method that finds all AI scripts

View File

@ -31,6 +31,8 @@ public:
* to control player @p player.
*/
virtual void AddPlayer(std::wstring id, player_id_t player) = 0;
virtual void TryLoadSharedComponent() = 0;
virtual void RunGamestateInit() = 0;
/**
* Call this at the end of a turn, to trigger AI computation which will be

View File

@ -0,0 +1,39 @@
/* Copyright (C) 2012 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#include "precompiled.h"
#include "ICmpTechnologyTemplateManager.h"
#include "simulation2/system/InterfaceScripted.h"
#include "simulation2/scripting/ScriptComponent.h"
BEGIN_INTERFACE_WRAPPER(TechnologyTemplateManager)
END_INTERFACE_WRAPPER(TechnologyTemplateManager)
class CCmpTechnologyTemplateManagerScripted : public ICmpTechnologyTemplateManager
{
public:
DEFAULT_SCRIPT_WRAPPER(TechnologyTemplateManagerScripted)
virtual CScriptVal GetAllTechs()
{
return m_Script.Call<CScriptVal>("GetAllTechs");
}
};
REGISTER_COMPONENT_SCRIPT_WRAPPER(TechnologyTemplateManagerScripted)

View File

@ -0,0 +1,38 @@
/* Copyright (C) 2012 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef INCLUDED_ICMPTECHNOLOGYTEMPLATEMANAGER
#define INCLUDED_ICMPTECHNOLOGYTEMPLATEMANAGER
#include "simulation2/system/Interface.h"
#include "maths/Fixed.h"
/**
* Technology template manager interface.
* (This interface only includes the functions needed by native code for accessing
* technology template data, the associated logic is handled in scripts)
*/
class ICmpTechnologyTemplateManager : public IComponent
{
public:
virtual CScriptVal GetAllTechs() = 0;
DECLARE_INTERFACE_TYPE(TechnologyTemplateManager)
};
#endif // INCLUDED_ICMPTECHNOLOGYTEMPLATEMANAGER