commit first version of new ai bot (named Petra)

This was SVN commit r14865.
This commit is contained in:
mimo 2014-03-24 22:33:50 +00:00
parent b03e3644ac
commit 97afd25171
26 changed files with 8671 additions and 0 deletions

View File

@ -0,0 +1,284 @@
Engine.IncludeModule("common-api");
var PETRA = (function() {
var m = {};
// "local" global variables for stuffs that will need a unique ID
// Note that since order of loading is alphabetic, this means this file must go before any other file using them.
m.playerGlobals = [];
m.PetraBot = function PetraBot(settings)
{
API3.BaseAI.call(this, settings);
this.turn = 0;
this.playedTurn = 0;
this.Config = new m.Config();
this.Config.updateDifficulty(settings.difficulty);
//this.Config.personality = settings.personality;
this.savedEvents = {};
this.defcon = 5;
this.defconChangeTime = -10000000;
};
m.PetraBot.prototype = new API3.BaseAI();
m.PetraBot.prototype.CustomInit = function(gameState, sharedScript)
{
this.initPersonality();
this.priorities = this.Config.priorities;
// this.queues can only be modified by the queue manager or things will go awry.
this.queues = {};
for (var i in this.priorities)
this.queues[i] = new m.Queue();
this.queueManager = new m.QueueManager(this.Config, this.queues, this.priorities);
this.HQ = new m.HQ(this.Config);
gameState.Config = this.Config;
m.playerGlobals[PlayerID] = {};
m.playerGlobals[PlayerID].uniqueIDBOPlans = 0; // training/building/research plans
m.playerGlobals[PlayerID].uniqueIDBases = 1; // base manager ID. Starts at one because "0" means "no base" on the map
m.playerGlobals[PlayerID].uniqueIDTPlans = 1; // transport plans. starts at 1 because 0 might be used as none.
m.playerGlobals[PlayerID].uniqueIDArmy = 0;
this.HQ.init(gameState,this.queues);
var filter = API3.Filters.byClass("CivCentre");
var myKeyEntities = gameState.getOwnEntities().filter(filter);
if (myKeyEntities.length == 0)
myKeyEntities = gameState.getOwnEntities();
var enemyKeyEntities = gameState.getEnemyEntities().filter(filter);
if (enemyKeyEntities.length == 0)
enemyKeyEntities = gameState.getEnemyEntities();
this.myIndex = this.accessibility.getAccessValue(myKeyEntities.toEntityArray()[0].position());
this.pathFinder = new API3.aStarPath(gameState, false, true);
this.pathsToMe = [];
this.pathInfo = { "angle" : 0, "needboat" : true, "mkeyPos" : myKeyEntities.toEntityArray()[0].position(), "ekeyPos" : enemyKeyEntities.toEntityArray()[0].position() };
// First path has a sampling of 3, which ensures we'll get at least one path even on Acropolis. The others are 6 so might fail.
var pos = [this.pathInfo.mkeyPos[0] + 150*Math.cos(this.pathInfo.angle),this.pathInfo.mkeyPos[1] + 150*Math.sin(this.pathInfo.angle)];
var path = this.pathFinder.getPath(this.pathInfo.ekeyPos, pos, 2, 2);// uncomment for debug:*/, 300000, gameState);
//Engine.DumpImage("initialPath" + this.player + ".png", this.pathFinder.TotorMap.map, this.pathFinder.TotorMap.width,this.pathFinder.TotorMap.height,255);
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;
};
m.PetraBot.prototype.OnUpdate = function(sharedScript)
{
if (this.gameFinished)
return;
for (var i in this.events)
{
if(this.savedEvents[i] !== undefined)
this.savedEvents[i] = this.savedEvents[i].concat(this.events[i]);
else
this.savedEvents[i] = this.events[i];
}
// Run the update every n turns, offset depending on player ID to balance the load
if ((this.turn + this.player) % 8 == 5)
{
Engine.ProfileStart("PetraBot bot (player " + this.player +")");
this.playedTurn++;
if (this.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] + 150*Math.cos(this.pathInfo.angle),this.pathInfo.mkeyPos[1] + 150*Math.sin(this.pathInfo.angle)];
var path = this.pathFinder.getPath(this.pathInfo.ekeyPos, pos, 6, 5);// uncomment for debug:*/, 300000, this.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)
{
m.debug ("Assuming this is a water map");
this.HQ.waterMap = true;
}
delete this.pathFinder;
delete this.pathInfo;
}
}
var townPhase = this.gameState.townPhase();
var cityPhase = this.gameState.cityPhase();
// try going up phases.
// TODO: softcode this more
if (this.gameState.canResearch(townPhase,true) && this.gameState.getPopulation() >= this.Config.Economy.villagePopCap - 10
&& this.gameState.findResearchers(townPhase,true).length != 0 && this.queues.majorTech.length() === 0)
{
var plan = new m.ResearchPlan(this.gameState, townPhase, true);
plan.lastIsGo = false;
plan.onStart = function (gameState) { gameState.ai.HQ.econState = "growth"; gameState.ai.HQ.OnTownPhase(gameState) };
plan.isGo = function (gameState) {
var ret = gameState.getPopulation() >= gameState.Config.Economy.villagePopCap
if (ret && !this.lastIsGo)
this.onGo(gameState);
else if (!ret && this.lastIsGo)
this.onNotGo(gameState);
this.lastIsGo = ret;
return ret;
};
plan.onGo = function (gameState) { gameState.ai.HQ.econState = "townPhasing"; m.debug ("Trying to reach TownPhase"); };
plan.onNotGo = function (gameState) { gameState.ai.HQ.econState = "growth"; };
this.queues.majorTech.addItem(plan);
}
else if (this.gameState.canResearch(cityPhase,true) && this.gameState.getTimeElapsed() > (this.Config.Economy.cityPhase*1000)
&& this.gameState.getOwnEntitiesByRole("worker", true).length > 85
&& this.gameState.findResearchers(cityPhase, true).length != 0 && this.queues.majorTech.length() === 0
&& this.queues.civilCentre.length() === 0)
{
var plan = new m.ResearchPlan(this.gameState, cityPhase, true);
plan.onStart = function (gameState) { gameState.ai.HQ.OnCityPhase(gameState) };
this.queues.majorTech.addItem(plan);
}
// defcon cooldown
if (this.defcon < 5 && this.gameState.timeSinceDefconChange() > 20000)
{
this.defcon++;
m.debug ("updefconing to " +this.defcon);
if (this.defcon >= 4 && this.HQ.hasGarrisonedFemales)
this.HQ.ungarrisonAll(this.gameState);
}
this.HQ.update(this.gameState, this.queues, this.savedEvents);
this.queueManager.update(this.gameState);
/*
// Use this to debug informations about the metadata.
if (this.playedTurn % 10 === 0)
{
// some debug informations about units.
var units = this.gameState.getOwnEntities();
for (var i in units._entities)
{
var ent = units._entities[i];
if (!ent.isIdle())
continue;
warn ("Unit " + ent.id() + " is a " + ent._templateName);
if (sharedScript._entityMetadata[PlayerID][ent.id()])
{
var metadata = sharedScript._entityMetadata[PlayerID][ent.id()];
for (var j in metadata)
{
warn ("Metadata " + j);
if (typeof(metadata[j]) == "object")
warn ("Object");
else if (typeof(metadata[j]) == undefined)
warn ("Undefined");
else
warn(uneval(metadata[j]));
}
}
}
}*/
//if (this.playedTurn % 5 === 0)
// this.queueManager.printQueues(this.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["Create"].length % 29;
for (var i = 0; i < n; i++)
Math.random();
for (var i in this.savedEvents)
this.savedEvents[i] = [];
Engine.ProfileStop();
}
this.turn++;
};
// defines our core components strategy-wise.
// TODO: the sky's the limit here.
m.PetraBot.prototype.initPersonality = function()
{
if (this.Config.difficulty >= 2)
{
this.Config.personality.aggressive = Math.random();
this.Config.personality.cooperative = Math.random();
}
if (this.Config.personality.aggressive > 0.7)
{
this.Config.Military.popForBarracks1 = 0;
this.Config.Economy.villagePopCap = 75;
this.Config.Economy.cityPhase = 900;
this.Config.Economy.popForMarket = 80;
this.Config.Economy.targetNumBuilders = 2;
this.Config.Economy.femaleRatio = 0.3;
this.Config.Defense.prudence = 0.5;
this.Config.priorities.defenseBuilding = 60;
}
if (this.Config.debug == 0)
return;
warn(" >>> Petra bot: personality = " + uneval(this.Config.personality));
};
/*m.PetraBot.prototype.Deserialize = function(data, sharedScript)
{
};
// Override the default serializer
PetraBot.prototype.Serialize = function()
{
return {};
};*/
// For the moment we just use the debugging flag and the debugging function from the API.
// Maybe it will make sense in the future to separate them.
m.DebugEnabled = function()
{
return API3.DebugEnabled;
}
m.debug = function(output)
{
API3.debug(output);
}
return m;
}());

View File

@ -0,0 +1,377 @@
var PETRA = function(m)
{
/* Defines an army
* An army is a collection of own entities and enemy entities.
* This doesn't use entity collections are they aren't really useful
* and it would probably slow the rest of the system down too much.
* All entities are therefore lists of ID
* Inherited by the defense manager and several of the attack manager's attack plan.
*/
m.Army = function(gameState, owner, ownEntities, foeEntities)
{
this.ID = m.playerGlobals[PlayerID].uniqueIDArmy++;
this.Config = owner.Config;
this.defenseRatio = this.Config.Defense.defenseRatio;
this.compactSize = this.Config.Defense.armyCompactSize;
this.breakawaySize = this.Config.Defense.armyBreakawaySize;
// average
this.foePosition = [0,0];
this.ownPosition = [0,0];
this.positionLastUpdate = gameState.getTimeElapsed();
// Some caching
// A list of our defenders that were tasked with attacking a particular unit
// This doesn't mean that they actually are since they could move on to something else on their own.
this.assignedAgainst = {};
// who we assigned against, for quick removal.
this.assignedTo = {};
// For substrengths, format is "name": [classes]
this.foeEntities = [];
this.foeStrength = 0;
this.foeSubStrength = {};
this.ownEntities = [];
this.ownStrength = 0;
this.ownSubStrength = {};
// actually add units
for (var i in foeEntities)
this.addFoe(gameState,foeEntities[i], true);
for (var i in ownEntities)
this.addOwn(gameState,ownEntities[i]);
this.recalculatePosition(gameState, true);
return true;
}
// if not forced, will only recalculate if on a different turn.
m.Army.prototype.recalculatePosition = function(gameState, force)
{
if (!force && this.positionLastUpdate === gameState.getTimeElapsed())
return;
var pos = [0,0];
if (this.foeEntities.length !== 0)
{
for each (var id in this.foeEntities)
{
var ent = gameState.getEntityById(id);
var epos = ent.position();
pos[0] += epos[0];
pos[1] += epos[1];
}
this.foePosition[0] = pos[0]/this.foeEntities.length;
this.foePosition[1] = pos[1]/this.foeEntities.length;
} else
this.foePosition = [0,0];
pos = [0,0];
if (this.ownEntities.length !== 0)
{
for each (var id in this.ownEntities)
{
var ent = gameState.getEntityById(id);
var epos = ent.position();
pos[0] += epos[0];
pos[1] += epos[1];
}
this.ownPosition[0] = pos[0]/this.ownEntities.length;
this.ownPosition[1] = pos[1]/this.ownEntities.length;
} else
this.ownPosition = [0,0];
this.positionLastUpdate = gameState.getTimeElapsed();
}
// helper
m.Army.prototype.recalculateStrengths = function (gameState)
{
this.ownStrength = 0;
this.foeStrength = 0;
// todo: deal with specifics.
for each (var id in this.foeEntities)
this.evaluateStrength(gameState.getEntityById(id));
for each (var id in this.ownEntities)
this.evaluateStrength(gameState.getEntityById(id), true);
}
// adds or remove the strength of the entity either to the enemy or to our units.
m.Army.prototype.evaluateStrength = function (ent, isOwn, remove)
{
var entStrength = m.getMaxStrength(ent);
if (remove)
entStrength *= -1;
if (isOwn)
this.ownStrength += entStrength;
else
this.foeStrength += entStrength;
// todo: deal with specifics.
}
// add an entity to the enemy army
// Will return true if the entity was added and false otherwise.
// won't recalculate our position but will dirty it.
m.Army.prototype.addFoe = function (gameState, enemyID, force)
{
if (this.foeEntities.indexOf(enemyID) !== -1)
return false;
var ent = gameState.getEntityById(enemyID);
if (ent === undefined || ent.position() === undefined)
return false;
// check distance
if (!force && API3.SquareVectorDistance(ent.position(), this.foePosition) > this.compactSize)
return false;
this.foeEntities.push(enemyID);
this.assignedAgainst[enemyID] = [];
this.positionLastUpdate = 0;
this.evaluateStrength(ent);
ent.setMetadata(PlayerID, "PartOfArmy", this.ID);
return true;
}
// returns true if the entity was removed and false otherwise.
// TODO: when there is a technology update, we should probably recompute the strengths, or weird stuffs will happen.
m.Army.prototype.removeFoe = function (gameState, enemyID, enemyEntity)
{
var idx = this.foeEntities.indexOf(enemyID);
if (idx === -1)
return false;
var ent = enemyEntity === undefined ? gameState.getEntityById(enemyID) : enemyEntity;
if (ent === undefined)
{
warn("Trying to remove a non-existing enemy entity, crashing for stacktrace");
xgzrg();
}
this.foeEntities.splice(idx, 1);
this.evaluateStrength(ent, false, true);
ent.setMetadata(PlayerID, "PartOfArmy", undefined);
this.assignedAgainst[enemyID] = undefined;
for (var to in this.assignedTo)
if (this.assignedTo[to] == enemyID)
this.assignedTo[to] = undefined;
return true;
}
// adds a defender but doesn't assign him yet.
m.Army.prototype.addOwn = function (gameState, ID)
{
if (this.ownEntities.indexOf(ID) !== -1)
return false;
var ent = gameState.getEntityById(ID);
if (ent === undefined || ent.position() === undefined)
return false;
this.ownEntities.push(ID);
this.evaluateStrength(ent, true);
ent.setMetadata(PlayerID, "PartOfArmy", this.ID);
this.assignedTo[ID] = 0;
var formerSubrole = ent.getMetadata(PlayerID, "subrole");
if (formerSubrole && formerSubrole === "defender") // can happen when armies are merged for example
return true;
if (formerSubrole !== undefined)
ent.setMetadata(PlayerID, "formerSubrole", formerSubrole);
ent.setMetadata(PlayerID, "subrole", "defender");
return true;
}
m.Army.prototype.removeOwn = function (gameState, ID, Entity)
{
var idx = this.ownEntities.indexOf(ID);
if (idx === -1)
return false;
var ent = Entity === undefined ? gameState.getEntityById(ID) : Entity;
if (ent === undefined)
{
warn( ID);
warn("Trying to remove a non-existing entity, crashing for stacktrace");
xgzrg();
}
this.ownEntities.splice(idx, 1);
this.evaluateStrength(ent, true, true);
ent.setMetadata(PlayerID, "PartOfArmy", undefined);
if (this.assignedTo[ID] !== 0)
{
var temp = this.assignedAgainst[this.assignedTo[ID]];
if (temp)
temp.splice(temp.indexOf(ID), 1);
}
this.assignedTo[ID] = undefined;
var formerSubrole = ent.getMetadata(PlayerID, "formerSubrole");
if (formerSubrole !== undefined)
ent.setMetadata(PlayerID, "subrole", formerSubrole);
else
ent.setMetadata(PlayerID, "subrole", undefined);
return true;
}
// this one is "undefined entity" proof because it's called at odd times.
// Orders a unit to attack an enemy.
// overridden by specific army classes.
m.Army.prototype.assignUnit = function (gameState, entID)
{
}
// resets the army properly.
// assumes we already cleared dead units.
m.Army.prototype.clear = function (gameState, events)
{
while(this.foeEntities.length > 0)
this.removeFoe(gameState,this.foeEntities[0]);
while(this.ownEntities.length > 0)
this.removeOwn(gameState,this.ownEntities[0]);
this.assignedAgainst = {};
this.assignedTo = {};
this.recalculateStrengths(gameState);
this.recalculatePosition(gameState);
}
// merge this army with another properly.
// assumes units are in only one army.
// also assumes that all have been properly cleaned up (no dead units).
m.Army.prototype.merge = function (gameState, otherArmy)
{
// copy over all parameters.
for (var i in otherArmy.assignedAgainst)
{
if (this.assignedAgainst[i] === undefined)
this.assignedAgainst[i] = otherArmy.assignedAgainst[i];
else
this.assignedAgainst[i] = this.assignedAgainst[i].concat(otherArmy.assignedAgainst[i]);
}
for (var i in otherArmy.assignedTo)
this.assignedTo[i] = otherArmy.assignedTo[i];
for each (var id in otherArmy.foeEntities)
this.addFoe(gameState, id);
// TODO: reassign those ?
for each (var id in otherArmy.ownEntities)
this.addOwn(gameState, id);
this.recalculatePosition(gameState, true);
this.recalculateStrengths(gameState);
return true;
}
// TODO: when there is a technology update, we should probably recompute the strengths, or weird stuffs might happen.
m.Army.prototype.checkEvents = function (gameState, events)
{
var renameEvents = events["EntityRenamed"]; // take care of promoted and packed units
var destroyEvents = events["Destroy"];
var convEvents = events["OwnershipChanged"];
var garriEvents = events["Garrison"];
// Warning the metadata is already cloned in shared.js. Futhermore, changes should be done before destroyEvents
// otherwise it would remove the old entity from this army list
// TODO we should may-be reevaluate the strength
for each (var msg in renameEvents)
{
if (this.foeEntities.indexOf(msg.entity) !== -1)
{
var idx = this.foeEntities.indexOf(msg.entity);
this.foeEntities[idx] = msg.newentity;
this.assignedAgainst[msg.newentity] = this.assignedAgainst[msg.entity];
this.assignedAgainst[msg.entity] = undefined;
for (var to in this.assignedTo)
if (this.assignedTo[to] == msg.entity)
this.assignedTo[to] = msg.newentity;
}
else if (this.ownEntities.indexOf(msg.entity) !== -1)
{
var idx = this.ownEntities.indexOf(msg.entity);
this.ownEntities[idx] = msg.newentity;
this.assignedTo[msg.newentity] = this.assignedTo[msg.entity];
this.assignedTo[msg.entity] = undefined;
for (var against in this.assignedAgainst)
{
if (!this.assignedAgainst[against])
continue;
if (this.assignedAgainst[against].indexOf(msg.entity) !== -1)
this.assignedAgainst[against][this.assignedAgainst[against].indexOf(msg.entity)] = msg.newentity;
}
}
}
for each (var msg in destroyEvents)
{
if (msg.entityObj === undefined)
continue;
if (msg.entityObj._entity.owner === PlayerID)
this.removeOwn(gameState, msg.entity, msg.entityObj);
else
this.removeFoe(gameState, msg.entity, msg.entityObj);
}
for each (var msg in garriEvents)
this.removeFoe(gameState, msg.entity);
for each (var msg in convEvents)
{
if (msg.to === PlayerID)
{
// we have converted an enemy, let's assign it as a defender
if (this.removeFoe(gameState, msg.entity))
this.addOwn(gameState, msg.entity);
} else if (msg.from === PlayerID)
this.removeOwn(gameState, msg.entity); // TODO: add allies
}
}
// assumes cleaned army.
// this only checks for breakaways.
m.Army.prototype.onUpdate = function (gameState)
{
var breakaways = [];
// TODO: assign unassigned defenders, cleanup of a few things.
// perhaps occasional strength recomputation
// occasional update or breakaways, positions…
if (gameState.getTimeElapsed() - this.positionLastUpdate > 5000)
{
this.recalculatePosition(gameState);
this.positionLastUpdate = gameState.getTimeElapsed();
// Check for breakaways.
for (var i = 0; i < this.foeEntities.length; ++i)
{
var id = this.foeEntities[i];
var ent = gameState.getEntityById(id);
if (API3.SquareVectorDistance(ent.position(), this.foePosition) > this.breakawaySize)
{
breakaways.push(id);
if(this.removeFoe(gameState, id))
i--;
}
}
this.recalculatePosition(gameState);
}
return breakaways;
}
return m;
}(PETRA);

View File

@ -0,0 +1,286 @@
var PETRA = function(m)
{
/* Attack Manager
*/
m.AttackManager = function(Config)
{
this.Config = Config;
this.totalNumber = 0;
this.attackNumber = 0;
this.rushNumber = 0;
this.raidNumber = 0;
this.upcomingAttacks = { "CityAttack": [], "Rush": [], "Raid": [] };
this.startedAttacks = { "CityAttack": [], "Rush": [], "Raid": [] };
this.debugTime = 0;
};
// More initialisation for stuff that needs the gameState
m.AttackManager.prototype.init = function(gameState, queues)
{
this.maxRushes = 0;
if (this.Config.personality.aggressive > 0.9)
this.maxRushes = 2
else if (this.Config.personality.aggressive > 0.7)
this.maxRushes = 1;
else
this.maxRushes = 0;
};
// Some functions are run every turn
// Others once in a while
m.AttackManager.prototype.update = function(gameState, queues, events)
{
if (this.Config.debug == 2 && gameState.getTimeElapsed() > this.debugTime + 60000)
{
this.debugTime = gameState.getTimeElapsed();
warn(" upcoming attacks =================");
for (var attackType in this.upcomingAttacks)
{
for (var i = 0; i < this.upcomingAttacks[attackType].length; ++i)
{
var attack = this.upcomingAttacks[attackType][i];
warn(" type " + attackType + " state " + attack.state + " paused " + attack.isPaused());
}
}
warn(" started attacks ==================");
for (var attackType in this.startedAttacks)
{
for (var i = 0; i < this.startedAttacks[attackType].length; ++i)
{
var attack = this.startedAttacks[attackType][i];
warn(" type " + attackType + " state " + attack.state + " paused " + attack.isPaused());
}
}
warn(" ==================================");
}
for (var attackType in this.upcomingAttacks)
{
for (var i = 0; i < this.upcomingAttacks[attackType].length; ++i)
{
var attack = this.upcomingAttacks[attackType][i];
attack.checkEvents(gameState, events, queues);
// okay so we'll get the support plan
if (!attack.isStarted())
{
var updateStep = attack.updatePreparation(gameState, this,events);
// now we're gonna check if the preparation time is over
if (updateStep === 1 || attack.isPaused() )
{
// just chillin'
}
else if (updateStep === 0 || updateStep === 3)
{
if (this.Config.debug)
warn("Attack Manager: " + attack.getType() + " plan " + attack.getName() + " aborted.");
if (updateStep === 3)
this.attackPlansEncounteredWater = true;
attack.Abort(gameState, this);
this.upcomingAttacks[attackType].splice(i--,1);
}
else if (updateStep === 2)
{
if (attack.StartAttack(gameState,this))
{
var targetName = gameState.sharedScript.playersData[attack.targetPlayer].name;
var proba = Math.random();
if (proba < 0.2)
var chatText = "I am launching an attack against " + targetName + ".";
else if (proba < 0.4)
var chatText = "Attacking " + targetName + ".";
else if (proba < 0.7)
var chatText = "I have sent an army against " + targetName + ".";
else
var chatText = "I'm starting an attack against " + targetName + ".";
gameState.ai.chatTeam(chatText);
if (this.Config.debug)
warn("Attack Manager: Starting " + attack.getType() + " plan " + attack.getName());
this.startedAttacks[attackType].push(attack);
}
else
attack.Abort(gameState, this);
this.upcomingAttacks[attackType].splice(i--,1);
}
}
else
{
var targetName = gameState.sharedScript.playersData[attack.targetPlayer].name;
var proba = Math.random();
if (proba < 0.2)
var chatText = "I am launching an attack against " + targetName + ".";
else if (proba < 0.4)
var chatText = "Attacking " + targetName + ".";
else if (proba < 0.7)
var chatText = "I have sent an army against " + targetName + ".";
else
var chatText = "I'm starting an attack against " + targetName + ".";
gameState.ai.chatTeam(chatText);
if (this.Config.debug)
warn("Attack Manager: Starting " + attack.getType() + " plan " + attack.getName());
this.startedAttacks[attackType].push(attack);
this.upcomingAttacks[attackType].splice(i--,1);
}
}
}
for (var attackType in this.startedAttacks)
{
for (var i = 0; i < this.startedAttacks[attackType].length; ++i)
{
var attack = this.startedAttacks[attackType][i];
attack.checkEvents(gameState, events, queues);
// okay so then we'll update the attack.
if (attack.isPaused())
continue;
var remaining = attack.update(gameState,this,events);
if (!remaining)
{
if (this.Config.debug)
warn("Military Manager: " + attack.getType() + " plan " + attack.getName() + " is finished with remaining " + remaining);
attack.Abort(gameState);
this.startedAttacks[attackType].splice(i--,1);
}
}
}
// creating plans after updating because an aborted plan might be reused in that case.
// TODO: remove the limitation to attacks when on water maps.
if (!gameState.ai.HQ.waterMap && !this.attackPlansEncounteredWater)
{
if (this.rushNumber < this.maxRushes && gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_barracks"), true) >= 1)
{
if (this.upcomingAttacks["Rush"].length === 0)
{
// we have a barracks and we want to rush, rush.
var attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, -1, "Rush");
if (this.Config.debug)
warn("Headquarters: Rushing plan " + this.totalNumber + " with maxRushes " + this.maxRushes);
this.rushNumber++;
this.totalNumber++;
this.upcomingAttacks["Rush"].push(attackPlan);
}
}
// if we have a barracks, there's no water, we're at age >= 1 and we've decided to attack.
else if (gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_barracks"), true) >= 1
&& (gameState.currentPhase() > 1 || gameState.isResearching(gameState.townPhase())))
{
if (gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_dock"), true) === 0 && gameState.ai.HQ.waterMap)
{
// wait till we get a dock.
}
else if (this.upcomingAttacks["CityAttack"].length === 0)
{
if (this.attackNumber < 2)
var attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, -1);
else
var attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, -1, "superSized");
if (attackPlan.failed)
this.attackPlansEncounteredWater = true; // hack
else
{
if (this.Config.debug)
warn("Military Manager: Creating the plan " + this.totalNumber);
this.attackNumber++;
this.totalNumber++;
this.upcomingAttacks["CityAttack"].push(attackPlan);
}
}
}
if (this.upcomingAttacks["Raid"].length === 999)
{
var enemyCC = gameState.getEnemyStructures().filter(API3.Filters.and(API3.Filters.byClass("CivCentre"), API3.Filters.isFoundation()));
if (enemyCC.length > 0)
{
// prepare some raid on this CC
var enemy = enemyCC.toEntityArray()[0].owner();
var attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, enemy, "Raid");
if (this.Config.debug)
warn("Headquarters: Raiding plan " + this.totalNumber);
this.raidNumber++;
this.totalNumber++;
this.upcomingAttacks["Raid"].push(attackPlan);
}
}
}
};
m.AttackManager.prototype.pausePlan = function(gameState, planName)
{
for (var attackType in this.upcomingAttacks)
{
for (var i in this.upcomingAttacks[attackType])
{
var attack = this.upcomingAttacks[attackType][i];
if (attack.getName() == planName)
attack.setPaused(true);
}
}
for (var attackType in this.startedAttacks)
{
for (var i in this.startedAttacks[attackType])
{
var attack = this.startedAttacks[attackType][i];
if (attack.getName() == planName)
attack.setPaused(true);
}
}
};
m.AttackManager.prototype.unpausePlan = function(gameState, planName)
{
for (var attackType in this.upcomingAttacks)
{
for (var i in this.upcomingAttacks[attackType])
{
var attack = this.upcomingAttacks[attackType][i];
if (attack.getName() == planName)
attack.setPaused(false);
}
}
for (var attackType in this.startedAttacks)
{
for (var i in this.startedAttacks[attackType])
{
var attack = this.startedAttacks[attackType][i];
if (attack.getName() == planName)
attack.setPaused(false);
}
}
};
m.AttackManager.prototype.pauseAllPlans = function(gameState)
{
for (var attackType in this.upcomingAttacks)
for (var i in this.upcomingAttacks[attackType])
this.upcomingAttacks[attackType][i].setPaused(true);
for (var attackType in this.startedAttacks)
for (var i in this.startedAttacks[attackType])
this.startedAttacks[attackType][i].setPaused(true);
};
m.AttackManager.prototype.unpauseAllPlans = function(gameState)
{
for (var attackType in this.upcomingAttacks)
for (var i in this.upcomingAttacks[attackType])
this.upcomingAttacks[attackType][i].setPaused(false);
for (var attackType in this.startedAttacks)
for (var i in this.startedAttacks[attackType])
this.startedAttacks[attackType][i].setPaused(false);
};
return m;
}(PETRA);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,840 @@
var PETRA = function(m)
{
/* Base Manager
* Handles lower level economic stuffs.
* Some tasks:
-tasking workers: gathering/hunting/building/repairing?/scouting/plans.
-giving feedback/estimates on GR
-achieving building stuff plans (scouting/getting ressource/building) or other long-staying plans, if I ever get any.
-getting good spots for dropsites
-managing dropsite use in the base
> warning HQ if we'll need more space
-updating whatever needs updating, keeping track of stuffs (rebuilding needs)
*/
m.BaseManager = function(Config)
{
this.Config = Config;
this.ID = m.playerGlobals[PlayerID].uniqueIDBases++;
// anchor building: seen as the main building of the base. Needs to have territorial influence
this.anchor = undefined;
this.accessIndex = undefined;
// Maximum distance (from any dropsite) to look for resources
// 3 areas are used: from 0 to max/4, from max/4 to max/2 and from max/2 to max
this.maxDistResourceSquare = 360*360;
this.constructing = false;
// vector for iterating, to check one use the HQ map.
this.territoryIndices = [];
};
m.BaseManager.prototype.init = function(gameState, unconstructed)
{
this.constructing = unconstructed;
// entitycollections
this.units = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "base", this.ID));
this.workers = this.units.filter(API3.Filters.byMetadata(PlayerID,"role","worker"));
this.buildings = gameState.getOwnStructures().filter(API3.Filters.byMetadata(PlayerID, "base", this.ID));
this.units.allowQuickIter();
this.workers.allowQuickIter();
this.buildings.allowQuickIter();
this.units.registerUpdates();
this.workers.registerUpdates();
this.buildings.registerUpdates();
// array of entity IDs, with each being
this.dropsites = {};
this.dropsiteSupplies = {};
this.gatherers = {};
for each (var type in this.Config.resources)
{
this.dropsiteSupplies[type] = {"nearby": [], "medium": [], "faraway": []};
this.gatherers[type] = {"nextCheck": 0, "used": 0, "lost": 0};
}
};
m.BaseManager.prototype.assignEntity = function(unit)
{
unit.setMetadata(PlayerID, "base", this.ID);
this.units.updateEnt(unit);
this.workers.updateEnt(unit);
this.buildings.updateEnt(unit);
};
m.BaseManager.prototype.setAnchor = function(gameState, anchorEntity)
{
if (!anchorEntity.hasClass("Structure") || !anchorEntity.hasTerritoryInfluence())
{
warn("Error: Petra base " + this.ID + " has been assigned an anchor building that has no territorial influence. Please report this on the forum.")
return false;
}
this.anchor = anchorEntity;
this.anchor.setMetadata(PlayerID, "base", this.ID);
this.anchor.setMetadata(PlayerID, "baseAnchor", true);
this.buildings.updateEnt(this.anchor);
this.accessIndex = gameState.ai.accessibility.getAccessValue(this.anchor.position());
return true;
};
m.BaseManager.prototype.checkEvents = function (gameState, events, queues)
{
var renameEvents = events["EntityRenamed"];
var destEvents = events["Destroy"];
var createEvents = events["Create"];
var cFinishedEvents = events["ConstructionFinished"];
for (var i in renameEvents)
{
var ent = gameState.getEntityById(renameEvents[i].newentity);
var workerObject = ent.getMetadata(PlayerID, "worker-object");
if (workerObject)
workerObject.ent = ent;
}
for (var i in destEvents)
{
var evt = destEvents[i];
// let's check we haven't lost an important building here.
if (evt != undefined && !evt.SuccessfulFoundation && evt.entityObj != undefined && evt.metadata !== undefined && evt.metadata[PlayerID] &&
evt.metadata[PlayerID]["base"] !== undefined && evt.metadata[PlayerID]["base"] == this.ID)
{
var ent = evt.entityObj;
if (ent.resourceDropsiteTypes() && !ent.hasClass("Elephant"))
this.removeDropsite(gameState, ent);
if (evt.metadata[PlayerID]["baseAnchor"] && evt.metadata[PlayerID]["baseAnchor"] == true)
{
// sounds like we lost our anchor. Let's try rebuilding it.
// TODO: currently the HQ manager sets us as initgathering, we probably ouht to do it
this.anchor = undefined;
this.constructing = true; // let's switch mode.
this.workers.forEach( function (worker) { worker.stopMoving(); });
queues.civilCentre.addItem(new m.ConstructionPlan(gameState, gameState.ai.HQ.bBase[0], { "base": this.ID, "baseAnchor": true }, ent.position()));
}
}
}
for (var i in cFinishedEvents)
{
var evt = cFinishedEvents[i];
if (evt && evt.newentity)
{
var ent = gameState.getEntityById(evt.newentity);
if (ent === undefined)
continue;
if (ent.getMetadata(PlayerID,"base") == this.ID)
{
if (ent.resourceDropsiteTypes() && !ent.hasClass("Elephant"))
this.assignResourceToDropsite(gameState, ent);
if (ent.resourceDropsiteTypes() && !ent.hasClass("Elephant") && this.Config.debug)
for each (var ress in ent.resourceDropsiteTypes())
warn(" DPresource " + ress + " = " + this.getResourceLevel(gameState, ress));
}
}
}
for (var i in createEvents)
{
var evt = createEvents[i];
if (evt && evt.entity)
{
var ent = gameState.getEntityById(evt.entity);
if (ent === undefined)
continue;
// do necessary stuff here
}
}
};
/**
* Assign the resources around the dropsites of this basis in three areas according to distance, and sort them in each area.
* Moving resources (animals) and buildable resources (fields) are treated elsewhere.
*/
m.BaseManager.prototype.assignResourceToDropsite = function (gameState, dropsite)
{
if (this.dropsites[dropsite.id()])
{
if (this.Config.debug)
warn("assignResourceToDropsite: dropsite already in the list. Should never happen");
return;
}
this.dropsites[dropsite.id()] = true;
var self = this;
for each (var type in dropsite.resourceDropsiteTypes())
{
var resources = gameState.getResourceSupplies(type);
if (resources.length === 0)
continue;
var nearby = this.dropsiteSupplies[type]["nearby"];
var medium = this.dropsiteSupplies[type]["medium"];
var faraway = this.dropsiteSupplies[type]["faraway"];
resources.forEach(function(supply)
{
if (!supply.position())
return;
if (supply.getMetadata(PlayerID, "inaccessible") === true)
return;
if (supply.hasClass("Animal")) // moving resources are treated differently TODO
return;
if (supply.hasClass("Field")) // fields are treated separately
return;
// quickscope accessibility check
if (!gameState.ai.accessibility.pathAvailable(gameState, dropsite.position(), supply.position(),false, true))
return;
var dist = API3.SquareVectorDistance(supply.position(), dropsite.position());
if (dist < self.maxDistResourceSquare)
{
if (supply.resourceSupplyType()["generic"] == "treasure")
{
if (dist < self.maxDistResourceSquare/4)
dist = 0;
else
dist = self.maxDistResourceSquare/16;
}
if (dist < self.maxDistResourceSquare/16) // distmax/4
nearby.push({ "dropsite": dropsite.id(), "id": supply.id(), "ent": supply, "dist": dist });
else if (dist < self.maxDistResourceSquare/4) // distmax/2
medium.push({ "dropsite": dropsite.id(), "id": supply.id(), "ent": supply, "dist": dist });
else
faraway.push({ "dropsite": dropsite.id(), "id": supply.id(), "ent": supply, "dist": dist });
}
});
nearby.sort(function(r1, r2) { return (r1.dist - r2.dist);});
medium.sort(function(r1, r2) { return (r1.dist - r2.dist);});
faraway.sort(function(r1, r2) { return (r1.dist - r2.dist);});
/* var debug = false;
if (debug)
{
faraway.forEach(function(res){
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [2,0,0]});
});
medium.forEach(function(res){
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [0,2,0]});
});
nearby.forEach(function(res){
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [0,0,2]});
});
} */
}
};
// completely remove the dropsite resources from our list.
m.BaseManager.prototype.removeDropsite = function (gameState, ent)
{
if (!ent.id())
return;
var removeSupply = function(entId, supply){
for (var i = 0; i < supply.length; ++i)
{
// exhausted resource, remove it from this list
if (!supply[i].ent || !gameState.getEntityById(supply[i].id))
supply.splice(i--, 1);
// resource assigned to the removed dropsite, remove it
else if (supply["dropsite"] === entId)
supply.splice(i--, 1);
}
};
for (var type in this.dropsiteSupplies)
{
removeSupply(ent.id(), this.dropsiteSupplies[type]["nearby"]);
removeSupply(ent.id(), this.dropsiteSupplies[type]["medium"]);
removeSupply(ent.id(), this.dropsiteSupplies[type]["faraway"]);
}
this.dropsites[ent.id()] = undefined;
return;
};
// Returns the position of the best place to build a new dropsite for the specified resource
m.BaseManager.prototype.findBestDropsiteLocation = function(gameState, resource)
{
var storeHousePlate = gameState.getTemplate(gameState.applyCiv("structures/{civ}_storehouse"));
// This builds a map. The procedure is fairly simple. It adds the resource maps
// (which are dynamically updated and are made so that they will facilitate DP placement)
// Then checks for a good spot in the territory. If none, and town/city phase, checks outside
// The AI will currently not build a CC if it wouldn't connect with an existing CC.
var obstructions = m.createObstructionMap(gameState, this.accessIndex, storeHousePlate);
obstructions.expandInfluences();
// copy the resource map as initialization.
var locateMap = new API3.Map(gameState.sharedScript, gameState.sharedScript.resourceMaps[resource].map, true);
var DPFoundations = gameState.getOwnFoundations().filter(API3.Filters.byType(gameState.applyCiv("foundation|structures/{civ}_storehouse")));
var ccEnts = gameState.getOwnEntities().filter(API3.Filters.byClass("CivCentre")).toEntityArray();
// TODO: might be better to check dropsites someplace else.
// loop over this in this.terrytoryindices. It's usually a little too much, but it's always enough.
for (var p = 0; p < this.territoryIndices.length; ++p)
{
var j = this.territoryIndices[p];
locateMap.map[j] *= 2;
// only add where the map is currently not null, ie in our territory and some "Resource" would be close.
// This makes the placement go from "OK" to "human-like".
for (var i in gameState.sharedScript.resourceMaps)
if (locateMap.map[j] !== 0 && i !== "food")
locateMap.map[j] += gameState.sharedScript.resourceMaps[i].map[j];
locateMap.map[j] *= 0.7; // Just a normalisation factor as the max is 255
var pos = [j%locateMap.width+0.5, Math.floor(j/locateMap.width)+0.5];
pos = [gameState.cellSize*pos[0], gameState.cellSize*pos[1]];
for (var i in this.dropsites)
{
if (!gameState.getEntityById(i))
continue;
var dpPos = gameState.getEntityById(i).position();
if (!dpPos)
continue;
var dist = API3.SquareVectorDistance(dpPos, pos);
if (dist < 3600)
{
locateMap.map[j] = 0;
break;
}
else if (dist < 6400)
locateMap.map[j] /= 2;
}
if (locateMap.map[j] == 0)
continue;
for (var i in DPFoundations._entities)
{
var dpPos = gameState.getEntityById(i).position();
if (!dpPos)
continue;
var dist = API3.SquareVectorDistance(dpPos, pos);
if (dist < 3600)
{
locateMap.map[j] = 0;
break;
}
else if (dist < 6400)
locateMap.map[j] /= 2;
}
if (locateMap.map[j] == 0)
continue;
for each (var cc in ccEnts)
{
var ccPos = cc.position();
if (!ccPos)
continue;
var dist = API3.SquareVectorDistance(ccPos, pos);
if (dist < 3600)
{
locateMap.map[j] = 0;
break;
}
else if (dist < 6400)
locateMap.map[j] /= 2;
}
}
var best = locateMap.findBestTile(2, obstructions); // try to find a spot to place a DP.
var bestIdx = best[0];
if (this.Config.debug == 2)
warn("for dropsite best is " + best[1] + " at " + gameState.getTimeElapsed());
// tell the dropsite builder we haven't found anything satisfactory.
// var cutbest = 60;
// // being less demanding for first dropsite
// if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_storehouse")) == 0)
// var cutbest = 50;
// if (best[1] < cutbest)
// return false;
var quality = best[1];
if (quality <= 0)
return {"quality": quality, "pos": [0, 0]};
var x = ((bestIdx % locateMap.width) + 0.5) * gameState.cellSize;
var z = (Math.floor(bestIdx / locateMap.width) + 0.5) * gameState.cellSize;
return {"quality": quality, "pos": [x, z]};
};
m.BaseManager.prototype.getResourceLevel = function (gameState, type)
{
var count = 0;
var check = {};
var nearby = this.dropsiteSupplies[type]["nearby"];
for each (var supply in nearby)
{
if (check[supply.id]) // avoid double counting as same resource can appear several time
continue;
check[supply.id] = true;
count += supply.ent.resourceSupplyAmount();
}
var medium = this.dropsiteSupplies[type]["medium"];
for each (var supply in medium)
{
if (check[supply.id])
continue;
check[supply.id] = true;
count += 0.6*supply.ent.resourceSupplyAmount();
}
return count;
};
// check our resource levels and react accordingly
m.BaseManager.prototype.checkResourceLevels = function (gameState, queues)
{
for each (var type in this.Config.resources)
{
if (type == "food")
{
var count = this.getResourceLevel(gameState, type); // TODO animals are not accounted, may-be we should
var numFarms = gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_field"), true);
var numFound = gameState.countEntitiesByType(gameState.applyCiv("foundation|structures/{civ}_field"), true);
var numQueue = queues.field.countQueuedUnits();
// TODO if not yet farms, add a check on time used/lost and build farmstead if needed
if (count < 1200 && numFarms + numFound + numQueue === 0) // tell the queue manager we'll be trying to build fields shortly.
{
for (var i = 0; i < this.Config.Economy.initialFields; ++i)
{
var plan = new m.ConstructionPlan(gameState, "structures/{civ}_field", { "base" : this.ID });
plan.isGo = function() { return false; }; // don't start right away.
queues.field.addItem(plan);
}
}
else if (count < 400 && numFarms + numFound === 0)
{
for (var i in queues.field.queue)
queues.field.queue[i].isGo = function() { return true; }; // start them
}
else if(gameState.ai.HQ.canBuild(gameState, "structures/{civ}_field")) // let's see if we need to add new farms.
{
if (numFound < 2 && numFound + numQueue < 3)
queues.field.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_field", { "base" : this.ID }));
}
}
else if (queues.dropsites.length() === 0 && gameState.countFoundationsByType(gameState.applyCiv("structures/{civ}_storehouse"), true) === 0)
{
if (gameState.ai.playedTurn > this.gatherers[type].nextCheck)
{
var self = this;
this.gatherersByType(gameState, type).forEach(function (ent) {
if (ent.unitAIState() === "INDIVIDUAL.GATHER.GATHERING")
++self.gatherers[type].used;
else if (ent.unitAIState() === "INDIVIDUAL.RETURNRESOURCE.APPROACHING")
++self.gatherers[type].lost;
});
// TODO add also a test on remaining resources
var total = this.gatherers[type].used + this.gatherers[type].lost;
if (total > 150 || (total > 60 && type !== "wood"))
{
var ratio = this.gatherers[type].lost / total;
if (ratio > 0.15)
{
var newDP = this.findBestDropsiteLocation(gameState, type);
if (newDP.quality > 50 && gameState.ai.HQ.canBuild(gameState, "structures/{civ}_storehouse"))
{
queues.dropsites.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_storehouse", { "base": this.ID }, newDP.pos));
if (!gameState.isResearched("gather_capacity_wheelbarrow") && !gameState.isResearching("gather_capacity_wheelbarrow"))
queues.minorTech.addItem(new m.ResearchPlan(gameState, "gather_capacity_wheelbarrow"));
}
else
gameState.ai.HQ.buildNewBase(gameState, queues, type);
}
this.gatherers[type].nextCheck = gameState.ai.playedTurn + 20;
this.gatherers[type].used = 0;
this.gatherers[type].lost = 0;
}
else if (total === 0)
this.gatherers[type].nextCheck = gameState.ai.playedTurn + 10;
}
}
else
{
this.gatherers[type].nextCheck = gameState.ai.playedTurn;
this.gatherers[type].used = 0;
this.gatherers[type].lost = 0;
}
}
};
// let's return the estimated gather rates.
m.BaseManager.prototype.getGatherRates = function(gameState, currentRates)
{
for (var i in currentRates)
{
// I calculate the exact gathering rate for each unit.
// I must then lower that to account for travel time.
// Given that the faster you gather, the more travel time matters,
// I use some logarithms.
// TODO: this should take into account for unit speed and/or distance to target
var units = this.gatherersByType(gameState, i);
units.forEach(function (ent) {
var gRate = ent.currentGatherRate();
if (gRate !== undefined)
currentRates[i] += Math.log(1+gRate)/1.1;
});
if (i === "food")
{
units = this.workers.filter(API3.Filters.byMetadata(PlayerID, "subrole", "hunter"));
units.forEach(function (ent) {
var gRate = ent.currentGatherRate()
if (gRate !== undefined)
currentRates[i] += Math.log(1+gRate)/1.1;
});
}
currentRates[i] += 0.5*m.GetTCRessGatherer(gameState, i);
}
};
m.BaseManager.prototype.assignRolelessUnits = function(gameState)
{
// TODO: make this cleverer.
var roleless = this.units.filter(API3.Filters.not(API3.Filters.byHasMetadata(PlayerID, "role")));
var self = this;
roleless.forEach(function(ent) {
if (ent.hasClass("Worker") || ent.hasClass("CitizenSoldier"))
ent.setMetadata(PlayerID, "role", "worker");
else if (ent.hasClass("Support") && ent.hasClass("Elephant"))
ent.setMetadata(PlayerID, "role", "worker");
});
};
// If the numbers of workers on the resources is unbalanced then set some of workers to idle so
// they can be reassigned by reassignIdleWorkers.
// TODO: actually this probably should be in the HQ.
m.BaseManager.prototype.setWorkersIdleByPriority = function(gameState)
{
if (gameState.currentPhase() < 2)
return;
var resources = gameState.ai.queueManager.getAvailableResources(gameState);
var avgOverdraft = 0;
for each (var type in resources.types)
avgOverdraft += resources[type];
avgOverdraft /= 4;
var mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState);
for each (var type in resources.types)
{
if (type === mostNeeded[0])
continue;
if (resources[type] > avgOverdraft + 200 || (resources[type] > avgOverdraft && avgOverdraft > 200))
{
if (this.gatherersByType(gameState, type).length === 0)
continue;
// TODO: perhaps change this?
var nb = 2;
this.gatherersByType(gameState, type).forEach( function (ent) {
if (nb == 0)
return;
nb--;
// TODO: might want to direct assign.
ent.stopMoving();
ent.setMetadata(PlayerID, "subrole","idle");
});
}
}
};
// TODO: work on this.
m.BaseManager.prototype.reassignIdleWorkers = function(gameState)
{
// Search for idle workers, and tell them to gather resources based on demand
var filter = API3.Filters.or(API3.Filters.byMetadata(PlayerID,"subrole","idle"), API3.Filters.not(API3.Filters.byHasMetadata(PlayerID,"subrole")));
var idleWorkers = gameState.updatingCollection("idle-workers-base-" + this.ID, filter, this.workers);
var self = this;
if (idleWorkers.length) {
idleWorkers.forEach(function(ent)
{
// Check that the worker isn't garrisoned
if (ent.position() === undefined)
return;
// Support elephant can only be builders
if (ent.hasClass("Support") && ent.hasClass("Elephant")) {
ent.setMetadata(PlayerID, "subrole", "idle");
return;
}
if (ent.hasClass("Worker"))
{
if (self.anchor && self.anchor.needsRepair() === true)
ent.repair(self.anchor);
else
{
var types = gameState.ai.HQ.pickMostNeededResources(gameState);
ent.setMetadata(PlayerID, "subrole", "gatherer");
ent.setMetadata(PlayerID, "gather-type", types[0]);
m.AddTCRessGatherer(gameState, types[0]);
}
}
else if (ent.hasClass("Cavalry"))
ent.setMetadata(PlayerID, "subrole", "hunter");
});
}
};
m.BaseManager.prototype.workersBySubrole = function(gameState, subrole)
{
return gameState.updatingCollection("subrole-" + subrole +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "subrole", subrole), this.workers, true);
};
m.BaseManager.prototype.gatherersByType = function(gameState, type)
{
return gameState.updatingCollection("workers-gathering-" + type +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "gather-type", type), this.workersBySubrole(gameState, "gatherer"));
};
// returns an entity collection of workers.
// They are idled immediatly and their subrole set to idle.
m.BaseManager.prototype.pickBuilders = function(gameState, workers, number)
{
var availableWorkers = this.workers.filter(API3.Filters.not(API3.Filters.byClass("Cavalry"))).toEntityArray();
availableWorkers.sort(function (a,b) {
var vala = 0, valb = 0;
if (a.getMetadata(PlayerID, "subrole") == "builder")
vala = 100;
if (b.getMetadata(PlayerID, "subrole") == "builder")
valb = 100;
if (a.getMetadata(PlayerID, "subrole") == "idle")
vala = -20;
if (b.getMetadata(PlayerID, "subrole") == "idle")
valb = -20;
if (a.getMetadata(PlayerID, "plan") != undefined)
vala = -100;
if (b.getMetadata(PlayerID, "plan") != undefined)
valb = -100;
return (vala - valb);
});
var needed = Math.min(number, availableWorkers.length);
for (var i = 0; i < needed; ++i)
{
availableWorkers[i].stopMoving();
availableWorkers[i].setMetadata(PlayerID, "subrole", "idle");
workers.addEnt(availableWorkers[i]);
}
return;
};
m.BaseManager.prototype.assignToFoundations = function(gameState, noRepair)
{
// If we have some foundations, and we don't have enough builder-workers,
// try reassigning some other workers who are nearby
// AI tries to use builders sensibly, not completely stopping its econ.
var self = this;
// TODO: this is not perfect performance-wise.
var foundations = this.buildings.filter(API3.Filters.and(API3.Filters.isFoundation(),API3.Filters.not(API3.Filters.byClass("Field")))).toEntityArray();
var damagedBuildings = this.buildings.filter(function (ent) {
if (ent.foundationProgress() === undefined && ent.needsRepair())
return true;
return false;
}).toEntityArray();
// Check if nothing to build
if (!foundations.length && !damagedBuildings.length){
return;
}
var workers = this.workers.filter(API3.Filters.not(API3.Filters.byClass("Cavalry")));
var builderWorkers = this.workersBySubrole(gameState, "builder");
var idleBuilderWorkers = this.workersBySubrole(gameState, "builder").filter(API3.Filters.isIdle());
// if we're constructing and we have the foundations to our base anchor, only try building that.
if (this.constructing == true && this.buildings.filter(API3.Filters.and(API3.Filters.isFoundation(), API3.Filters.byMetadata(PlayerID, "baseAnchor", true))).length !== 0)
{
foundations = this.buildings.filter(API3.Filters.byMetadata(PlayerID, "baseAnchor", true)).toEntityArray();
var tID = foundations[0].id();
workers.forEach(function (ent) { //}){
var target = ent.getMetadata(PlayerID, "target-foundation");
if (target && target != tID)
{
ent.stopMoving();
ent.setMetadata(PlayerID, "target-foundation", tID);
}
});
}
if (workers.length < 2)
{
var noobs = gameState.ai.HQ.bulkPickWorkers(gameState, this.ID, 2);
if(noobs)
{
noobs.forEach(function (worker) { //}){
worker.setMetadata(PlayerID,"base", self.ID);
worker.setMetadata(PlayerID,"subrole", "builder");
workers.updateEnt(worker);
builderWorkers.updateEnt(worker);
idleBuilderWorkers.updateEnt(worker);
});
}
}
var addedWorkers = 0;
var maxTotalBuilders = Math.ceil(workers.length * 0.2);
if (this.constructing == true && maxTotalBuilders < 15)
maxTotalBuilders = 15;
for (var i in foundations) {
var target = foundations[i];
if (target.hasClass("Field"))
continue; // we do not build fields
var assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length;
var targetNB = this.Config.Economy.targetNumBuilders; // TODO: dynamic that.
if (target.hasClass("House"))
targetNB *= 2;
else if (target.hasClass("Barracks") || target.hasClass("Tower"))
targetNB = 4;
else if (target.hasClass("Fortress"))
targetNB = 7;
if (target.getMetadata(PlayerID, "baseAnchor") == true)
targetNB = 15;
if (assigned < targetNB) {
if (builderWorkers.length - idleBuilderWorkers.length + addedWorkers < maxTotalBuilders) {
var addedToThis = 0;
idleBuilderWorkers.forEach(function(ent) {
if (ent.position() && API3.SquareVectorDistance(ent.position(), target.position()) < 10000 && assigned + addedToThis < targetNB)
{
addedWorkers++;
addedToThis++;
ent.setMetadata(PlayerID, "target-foundation", target.id());
}
});
if (assigned + addedToThis < targetNB)
{
var nonBuilderWorkers = workers.filter(function(ent) { return (ent.getMetadata(PlayerID, "subrole") !== "builder" && ent.position() !== undefined); }).toEntityArray();
var time = target.buildTime();
nonBuilderWorkers.sort(function (workerA,workerB)
{
var coeffA = API3.SquareVectorDistance(target.position(),workerA.position());
// elephant moves slowly, so when far away they are only useful if build time is long
if (workerA.hasClass("Elephant"))
coeffA *= 0.5 * (1 + (Math.sqrt(coeffA)/150)*(30/time));
else if (workerA.getMetadata(PlayerID, "gather-type") === "food")
coeffA *= 3;
var coeffB = API3.SquareVectorDistance(target.position(),workerB.position());
if (workerB.hasClass("Elephant"))
coeffB *= 0.5 * (1 + (Math.sqrt(coeffB)/150)*(30/time));
else if (workerB.getMetadata(PlayerID, "gather-type") === "food")
coeffB *= 3;
return (coeffA - coeffB);
});
var current = 0;
while (assigned + addedToThis < targetNB && current < nonBuilderWorkers.length)
{
addedWorkers++;
addedToThis++;
var ent = nonBuilderWorkers[current++];
ent.stopMoving();
ent.setMetadata(PlayerID, "subrole", "builder");
ent.setMetadata(PlayerID, "target-foundation", target.id());
};
}
}
}
}
// don't repair if we're still under attack, unless it's like a vital (civcentre or wall) building that's getting destroyed.
for (var i in damagedBuildings) {
var target = damagedBuildings[i];
if (gameState.defcon() < 5) {
if (target.healthLevel() > 0.5 || !target.hasClass("CivCentre") || !target.hasClass("StoneWall")) {
continue;
}
} else if (noRepair && !target.hasClass("CivCentre"))
continue;
var territory = m.createTerritoryMap(gameState);
if (territory.getOwner(target.position()) !== PlayerID || territory.getOwner([target.position()[0] + 5, target.position()[1]]) !== PlayerID)
continue;
var assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length;
if (assigned < targetNB/3) {
if (builderWorkers.length + addedWorkers < targetNB*2) {
var nonBuilderWorkers = workers.filter(function(ent) { return (ent.getMetadata(PlayerID, "subrole") !== "builder" && ent.position() !== undefined); });
if (gameState.defcon() < 5)
nonBuilderWorkers = workers.filter(function(ent) { return (ent.getMetadata(PlayerID, "subrole") !== "builder" && ent.hasClass("Female") && ent.position() !== undefined); });
var nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), targetNB/3 - assigned);
nearestNonBuilders.forEach(function(ent) {
ent.stopMoving();
addedWorkers++;
ent.setMetadata(PlayerID, "subrole", "builder");
ent.setMetadata(PlayerID, "target-foundation", target.id());
});
}
}
}
};
m.BaseManager.prototype.update = function(gameState, queues, events)
{
Engine.ProfileStart("Base update - base " + this.ID);
var self = this;
this.checkResourceLevels(gameState, queues);
this.assignToFoundations(gameState);
if (this.constructing && this.anchor)
{
var owner = m.createTerritoryMap(gameState).getOwner(this.anchor.position());
if(owner !== 0 && !gameState.isPlayerAlly(owner))
{
// we're in enemy territory. If we're too close from the enemy, destroy us.
var eEnts = gameState.getEnemyStructures().filter(API3.Filters.byClass("CivCentre")).toEntityArray();
for (var i in eEnts)
{
var entPos = eEnts[i].position();
if (API3.SquareVectorDistance(entPos, this.anchor.position()) < 8000)
this.anchor.destroy();
}
}
}
if (gameState.ai.playedTurn % 2 === 0)
this.setWorkersIdleByPriority(gameState);
this.assignRolelessUnits(gameState);
// should probably be last to avoid reallocations of units that would have done stuffs otherwise.
this.reassignIdleWorkers(gameState);
// TODO: do this incrementally a la defense.js
this.workers.forEach(function(ent) {
if (!ent.getMetadata(PlayerID, "worker-object"))
ent.setMetadata(PlayerID, "worker-object", new m.Worker(ent));
ent.getMetadata(PlayerID, "worker-object").update(self, gameState);
});
Engine.ProfileStop();
};
return m;
}(PETRA);

View File

@ -0,0 +1,124 @@
var PETRA = function(m)
{
// this defines the medium difficulty
m.Config = function() {
this.difficulty = 2; // 0 is sandbox, 1 is easy, 2 is medium, 3 is hard, 4 is very hard.
this.debug = 0;
this.Military = {
"towerLapseTime" : 90, // Time to wait between building 2 towers
"fortressLapseTime" : 420, // Time to wait between building 2 fortresses
"popForBarracks1" : 25,
"popForBarracks2" : 95,
"timeForBlacksmith" : 900,
};
this.Economy = {
"villagePopCap" : 40, // How many units we want before aging to town.
"cityPhase" : 840, // time to start trying to reach city phase
"popForMarket" : 50,
"dockStartTime" : 240, // Time to wait before building the dock
"targetNumBuilders" : 1.5, // Base number of builders per foundation.
"targetNumTraders" : 4, // Target number of traders
"femaleRatio" : 0.5, // percent of females among the workforce.
"initialFields" : 5
};
// Note: attack settings are set directly in attack_plan.js
// defense
this.Defense =
{
"defenseRatio" : 2, // see defense.js for more info.
"armyCompactSize" : 2000, // squared. Half-diameter of an army.
"armyBreakawaySize" : 3500, // squared.
"armyMergeSize" : 1400, // squared.
"armyStrengthWariness" : 2, // Representation of how important army strength is for its "watch level".
"prudence" : 1 // Representation of how quickly we'll forget about a dangerous army.
};
// military
this.buildings =
{
"base" : {
"default" : [ "structures/{civ}_civil_centre" ],
"ptol" : [ "structures/{civ}_military_colony" ],
"sele" : [ "structures/{civ}_military_colony" ]
},
"advanced" : {
"default" : [],
"hele" : [ "structures/{civ}_gymnasion" ],
"athen" : [ "structures/{civ}_gymnasion" ],
"spart" : [ "structures/{civ}_syssiton" ],
"cart" : [ "structures/{civ}_embassy_celtic",
"structures/{civ}_embassy_iberian", "structures/{civ}_embassy_italiote" ],
"celt" : [ "structures/{civ}_kennel" ],
"pers" : [ "structures/{civ}_fortress", "structures/{civ}_stables", "structures/{civ}_apadana" ],
"rome" : [ "structures/{civ}_army_camp" ],
"mace" : [ "structures/{civ}_siege_workshop"],
"maur" : [ "structures/{civ}_elephant_stables"]
},
"fort" : {
"default" : [ "structures/{civ}_fortress" ],
"celt" : [ "structures/{civ}_fortress_b", "structures/{civ}_fortress_g" ]
}
};
this.priorities =
{
"villager" : 30, // should be slightly lower than the citizen soldier one because otherwise they get all the food
"citizenSoldier" : 60,
"trader" : 50,
"ships" : 70,
"house" : 350,
"dropsites" : 120,
"field" : 500,
"economicBuilding" : 90,
"militaryBuilding" : 240, // set to something lower after the first barracks.
"defenseBuilding" : 70,
"civilCentre" : 950,
"majorTech" : 700,
"minorTech" : 40
};
this.personality =
{
"aggressive": 0.5,
"cooperative": 0.5
};
this.resources = ["food", "wood", "stone", "metal"];
};
//Config.prototype = new BaseConfig();
m.Config.prototype.updateDifficulty = function(difficulty)
{
this.difficulty = difficulty;
// changing settings based on difficulty.
this.targetNumTraders = 2 * this.difficulty;
if (this.difficulty === 1)
{
this.Military.popForBarracks1 = 35;
this.Military.popForBarracks2 = 150; // shouldn't reach it
this.Military.popForBlacksmith = 150; // shouldn't reach it
this.Economy.cityPhase = 1800;
this.Economy.popForMarket = 80;
this.Economy.femaleRatio = 0.6;
this.Economy.initialFields = 2;
}
else if (this.difficulty === 0)
{
this.Military.popForBarracks1 = 60;
this.Military.popForBarracks2 = 150; // shouldn't reach it
this.Military.popForBlacksmith = 150; // shouldn't reach it
this.Economy.cityPhase = 240000;
this.Economy.popForMarket = 200;
this.Economy.femaleRatio = 0.7;
this.Economy.initialFields = 1;
}
};
return m;
}(PETRA);

View File

@ -0,0 +1,7 @@
{
"name": "Petra Bot",
"description": "Based on Aegis, but heavily modified and expected to be more robust",
"moduleName" : "PETRA",
"constructor": "PetraBot",
"useShared": true
}

View File

@ -0,0 +1,180 @@
var PETRA = function(m)
{
// Specialization of Armies used by the defense manager.
m.DefenseArmy = function(gameState, defManager, ownEntities, foeEntities)
{
if (!m.Army.call(this, gameState, defManager, ownEntities, foeEntities))
return false;
this.watchTSMultiplicator = this.Config.Defense.armyStrengthWariness;
this.watchDecrement = this.Config.Defense.prudence;
this.foeSubStrength = {
"spear" : ["Infantry", "Spear"], //also pikemen
"sword" : ["Infantry", "Sword"],
"ranged" : ["Infantry", "Ranged"],
"meleeCav" : ["Cavalry", "Melee"],
"rangedCav" : ["Cavalry", "Ranged"],
"Elephant" : ["Elephant"],
"meleeSiege" : ["Siege", "Melee"],
"rangedSiege" : ["Siege", "Ranged"]
};
this.ownSubStrength = {
"spear" : ["Infantry", "Spear"], //also pikemen
"sword" : ["Infantry", "Sword"],
"ranged" : ["Infantry", "Ranged"],
"meleeCav" : ["Cavalry", "Melee"],
"rangedCav" : ["Cavalry", "Ranged"],
"Elephant" : ["Elephant"],
"meleeSiege" : ["Siege", "Melee"],
"rangedSiege" : ["Siege", "Ranged"]
};
this.checkDangerosity(gameState); // might push us to 1.
this.watchLevel = this.foeStrength * this.watchTSMultiplicator;
return true;
}
m.DefenseArmy.prototype = Object.create(m.Army.prototype);
m.DefenseArmy.prototype.assignUnit = function (gameState, entID)
{
// we'll assume this defender is ours already.
// we'll also override any previous assignment
var ent = gameState.getEntityById(entID);
if (!ent || !ent.position())
return false;
var idMin = undefined;
var distMin = undefined;
var idMinAll = undefined;
var distMinAll = undefined;
for each (var id in this.foeEntities)
{
var eEnt = gameState.getEntityById(id);
if (!eEnt || !eEnt.position()) // probably can't happen.
continue;
if (eEnt.unitAIOrderData().length && eEnt.unitAIOrderData()[0]["target"] &&
eEnt.unitAIOrderData()[0]["target"] === entID)
{ // being attacked >>> target the unit
idMin = id;
break;
}
var dist = API3.SquareVectorDistance(ent.position(), eEnt.position());
if (idMinAll === undefined || dist < distMin)
{
idMinAll = id;
distMinAll = dist;
}
if (this.assignedAgainst[id].length > 2) // already enough units against it
continue;
var dist = API3.SquareVectorDistance(ent.position(), eEnt.position());
if (idMin === undefined || dist < distMin)
{
idMin = id;
distMin = dist;
}
}
if (idMin !== undefined)
{
this.assignedTo[entID] = idMin;
this.assignedAgainst[idMin].push(entID);
ent.attack(idMin);
return true;
}
else if (idMinAll !== undefined)
{
this.assignedTo[entID] = idMinAll;
this.assignedAgainst[idMinAll].push(entID);
ent.attack(idMinAll);
return true;
}
this.recalculatePosition(gameState);
ent.attackMove(this.foePosition[0], this.foePosition[1]);
return false;
}
// TODO: this should return cleverer results ("needs anti-elephant"…)
m.DefenseArmy.prototype.needsDefenders = function (gameState, events)
{
// some preliminary checks because we don't update for tech
if (this.foeStrength < 0 || this.ownStrength < 0)
this.recalculateStrengths(gameState);
if (this.foeStrength * this.defenseRatio <= this.ownStrength)
return false;
return this.foeStrength * this.defenseRatio - this.ownStrength;
}
m.DefenseArmy.prototype.getState = function (gameState)
{
if (this.foeEntities.length === 0)
return 0;
if (this.state === 2)
return 2;
if (this.watchLevel > 0)
return 1;
return 0;
}
// check if we should remain at state 2 or drift away
m.DefenseArmy.prototype.checkDangerosity = function (gameState)
{
this.territoryMap = m.createTerritoryMap(gameState);
// right now we'll check if our position is "enemy" or not.
if (this.territoryMap.getOwner(this.ownPosition) !== PlayerID)
this.state = 1;
else if (this.state === 1)
this.state = 2;
}
m.DefenseArmy.prototype.update = function (gameState)
{
var breakaways = this.onUpdate(gameState);
this.checkDangerosity(gameState);
var normalWatch = this.foeStrength * this.watchTSMultiplicator;
if (this.state === 2)
this.watchLevel = normalWatch;
else if (this.watchLevel > normalWatch)
this.watchLevel = normalWatch;
else
this.watchLevel -= this.watchDecrement;
// TODO: deal with watchLevel?
return breakaways;
}
m.DefenseArmy.prototype.debug = function (gameState)
{
m.debug(" ");
m.debug ("Army " + this.ID)
// m.debug ("state " + this.state);
// m.debug ("WatchLevel " + this.watchLevel);
// m.debug ("Entities " + this.foeEntities.length);
// m.debug ("Strength " + this.foeStrength);
// debug (gameState.getEntityById(ent)._templateName + ", ID " + ent);
//debug ("Defenders " + this.ownEntities.length);
for each (ent in this.foeEntities)
{
if (gameState.getEntityById(ent) !== undefined)
{
m.debug (gameState.getEntityById(ent)._templateName + ", ID " + ent);
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent], "rgb": [0.5,0,0]});
} else
m.debug("ent " + ent);
}
m.debug ("");
}
return m;
}(PETRA);

View File

@ -0,0 +1,445 @@
var PETRA = function(m)
{
m.DefenseManager = function(Config)
{
this.armies = []; // array of "army" Objects
this.Config = Config;
this.targetList = [];
}
m.DefenseManager.prototype.init = function(gameState)
{
this.armyMergeSize = this.Config.Defense.armyMergeSize;
};
m.DefenseManager.prototype.update = function(gameState, events)
{
this.territoryMap = m.createTerritoryMap(gameState);
this.checkEnemyArmies(gameState, events);
this.checkEnemyUnits(gameState);
this.assignDefenders(gameState);
this.MessageProcess(gameState,events);
};
m.DefenseManager.prototype.makeIntoArmy = function(gameState, entityID)
{
// Try to add it to an existing army.
for (var o in this.armies)
if (this.armies[o].addFoe(gameState,entityID))
return; // over
// Create a new army for it.
var army = new m.DefenseArmy(gameState, this, [], [entityID]);
this.armies.push(army);
};
// TODO: this algorithm needs to be improved, sorta.
m.DefenseManager.prototype.isDangerous = function(gameState, entity)
{
if (!entity.position())
return false;
if (this.territoryMap.getOwner(entity.position()) === entity.owner())
return false;
// check if the entity is trying to build a new base near our buildings, and if yes, add this base in our target list
if (entity.unitAIState() == "INDIVIDUAL.REPAIR.REPAIRING")
{
var targetId = entity.unitAIOrderData()[0]["target"];
if (this.targetList.indexOf(targetId) !== -1)
return true;
var target = gameState.getEntityById(targetId);
if (target && target.hasClass("CivCentre"))
{
var myBuildings = gameState.getOwnStructures();
for (var i in myBuildings._entities)
{
if (API3.SquareVectorDistance(myBuildings._entities[i].position(), entity.position()) > 30000)
continue;
this.targetList.push(targetId);
return true;
}
}
}
if (entity.attackTypes() === undefined || entity.hasClass("Support"))
return false;
for (var i = 0; i < this.targetList.length; ++i)
{
var target = gameState.getEntityById(this.targetList[i]);
if (!target || !target.position()) // the enemy base is either destroyed or built
this.targetList.splice(i--, 1);
else if (API3.SquareVectorDistance(target.position(), entity.position()) < 6000)
return true;
}
var myCCFoundations = gameState.getOwnFoundations().filter(API3.Filters.byClass("CivCentre"));
for (var i in myCCFoundations._entities)
{
if (!myCCFoundations._entities[i].getBuildersNb())
continue;
if (API3.SquareVectorDistance(myCCFoundations._entities[i].position(), entity.position()) < 6000)
return true;
}
if (this.Config.personality.cooperative > 0.3)
{
var allyCC = gameState.getAllyEntities().filter(API3.Filters.byClass("CivCentre"));
for (var i in allyCC._entities)
{
if (this.Config.personality.cooperative < 0.6 && allyCC._entities[i].foundationProgress() !== undefined)
continue;
if (API3.SquareVectorDistance(allyCC._entities[i].position(), entity.position()) < 6000)
return true;
}
}
var myBuildings = gameState.getOwnStructures();
for (var i in myBuildings._entities)
if (API3.SquareVectorDistance(myBuildings._entities[i].position(), entity.position()) < 6000)
return true;
return false;
};
m.DefenseManager.prototype.checkEnemyUnits = function(gameState)
{
var self = this;
// loop through enemy units
var nbPlayers = gameState.sharedScript.playersData.length - 1;
var i = 1 + gameState.ai.playedTurn % nbPlayers;
if (i === PlayerID || gameState.isPlayerAlly(i))
return;
var filter = API3.Filters.and(API3.Filters.byClass("Unit"), API3.Filters.byOwner(i));
var enemyUnits = gameState.updatingGlobalCollection("player-" +i + "-units", filter);
enemyUnits.forEach( function (ent) {
// first check: is this unit already part of an army.
if (ent.getMetadata(PlayerID, "PartOfArmy") !== undefined)
return;
// TODO what to do for ships ?
if (ent.hasClass("Ship") || ent.hasClass("Trader"))
return;
// check if unit is dangerous "a priori"
if (self.isDangerous(gameState, ent))
self.makeIntoArmy(gameState, ent.id());
});
};
m.DefenseManager.prototype.checkEnemyArmies = function(gameState, events)
{
var self = this;
for (var o = 0; o < this.armies.length; ++o)
{
var army = this.armies[o];
army.checkEvents(gameState, events); // must be called every turn for all armies
// this returns a list of IDs: the units that broke away from the army for being too far.
var breakaways = army.update(gameState);
for (var u in breakaways)
{
// assume dangerosity
this.makeIntoArmy(gameState,breakaways[u]);
}
if (army.getState(gameState) === 0)
{
army.clear(gameState);
this.armies.splice(o--,1);
continue;
}
}
// Check if we can't merge it with another.
for (var o = 0; o < this.armies.length; ++o)
{
var army = this.armies[o];
for (var p = o+1; p < this.armies.length; ++p)
{
var otherArmy = this.armies[p];
if (otherArmy.state !== army.state)
continue;
if (API3.SquareVectorDistance(army.foePosition, otherArmy.foePosition) < this.armyMergeSize)
{
// no need to clear here.
army.merge(gameState, otherArmy);
this.armies.splice(p--,1);
}
}
}
if (gameState.ai.playedTurn % 5 !== 0)
return;
// Check if any army is no more dangerous (possibly because it has defeated us and destroyed our base)
for (var o = 0; o < this.armies.length; ++o)
{
var army = this.armies[o];
army.recalculatePosition(gameState);
var owner = this.territoryMap.getOwner(army.foePosition);
if (gameState.isPlayerAlly(owner))
continue;
else if (owner !== 0) // enemy army back in its territory
{
army.clear(gameState);
this.armies.splice(o--,1);
continue;
}
// army in neutral territory // TODO check smaller distance with all our buildings instead of only ccs with big distance
var stillDangerous = false;
var bases = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).toEntityArray();
if (this.Config.personality.cooperative > 0.3)
{
var allyCC = gameState.getAllyEntities().filter(API3.Filters.byClass("CivCentre")).toEntityArray();
bases = bases.concat(allyCC);
}
for (var i in bases)
{
if (API3.SquareVectorDistance(bases[i].position(), army.foePosition) < 40000)
{
if(this.Config.debug > 0)
warn("but still near one of our CC");
stillDangerous = true;
break;
}
}
if (stillDangerous)
continue;
army.clear(gameState);
this.armies.splice(o--,1);
}
};
m.DefenseManager.prototype.assignDefenders = function(gameState, events)
{
if (this.armies.length === 0)
return;
var armiesNeeding = [];
// Okay, let's add defenders
// TODO: this is dumb.
for (var i in this.armies)
{
var army = this.armies[i];
var needsDef = army.needsDefenders(gameState);
if (needsDef === false)
continue;
// Okay for now needsDef is the total needed strength.
// we're dumb so we don't choose if we have a defender shortage.
armiesNeeding.push( {"army": army, "need": needsDef} );
}
if (armiesNeeding.length === 0)
return;
// let's get our potential units
var potentialDefenders = [];
gameState.getOwnUnits().forEach(function(ent) {
if (ent.getMetadata(PlayerID, "PartOfArmy") !== undefined)
return;
if (ent.getMetadata(PlayerID, "plan") !== undefined)
{
var subrole = ent.getMetadata(PlayerID, "subrole");
if (subrole && (subrole === "completing" || subrole === "walking" || subrole === "attacking"))
return;
if (ent.hasClass("Siege"))
return;
}
else
{
if (!ent.hasClass("Infantry") && !ent.hasClass("Cavalry"))
return;
}
potentialDefenders.push(ent.id());
});
for (var a = 0; a < armiesNeeding.length; ++a)
armiesNeeding[a]["army"].recalculatePosition(gameState);
for (var i = 0; i < potentialDefenders.length; ++i)
{
var ent = gameState.getEntityById(potentialDefenders[i]);
if (!ent.position())
continue;
var aMin = undefined;
var distMin = undefined;
for (var a = 0; a < armiesNeeding.length; ++a)
{
var dist = API3.SquareVectorDistance(ent.position(), armiesNeeding[a]["army"].foePosition);
if (aMin !== undefined && dist > distMin)
continue;
aMin = a;
distMin = dist;
}
if (aMin === undefined)
{
for (var a = 0; a < armiesNeeding.length; ++a)
warn(" defense/armiesNeeding " + uneval(armiesNeeding[a]["need"]));
}
var str = m.getMaxStrength(ent);
armiesNeeding[aMin]["need"] -= str;
armiesNeeding[aMin]["army"].addOwn(gameState, potentialDefenders[i]);
armiesNeeding[aMin]["army"].assignUnit(gameState, potentialDefenders[i]);
if (armiesNeeding[aMin]["need"] <= 0)
armiesNeeding.splice(aMin, 1);
if (armiesNeeding.length === 0)
break;
}
// If shortage of defenders: increase the priority of soldiers queues
if (armiesNeeding.length !== 0)
gameState.ai.HQ.boostSoldiers(gameState, 10000, true);
else
gameState.ai.HQ.unboostSoldiers(gameState);
};
// this processes the attackmessages
// So that a unit that gets attacked will not be completely dumb.
// warning: big levels of indentation coming.
m.DefenseManager.prototype.MessageProcess = function(gameState,events) {
/* var self = this;
var attackedEvents = events["Attacked"];
for (var key in attackedEvents){
var e = attackedEvents[key];
if (gameState.isEntityOwn(gameState.getEntityById(e.target))) {
var attacker = gameState.getEntityById(e.attacker);
var ourUnit = gameState.getEntityById(e.target);
// the attacker must not be already dead, and it must not be me (think catapults that miss).
if (attacker === undefined || attacker.owner() === PlayerID || attacker.position() === undefined)
continue;
var mapPos = this.dangerMap.gamePosToMapPos(attacker.position());
this.dangerMap.addInfluence(mapPos[0], mapPos[1], 4, 1, 'constant');
// disregard units from attack plans and defense.
if (ourUnit.getMetadata(PlayerID, "role") == "defense" || ourUnit.getMetadata(PlayerID, "role") == "attack")
continue;
var territory = this.territoryMap.getOwner(attacker.position());
if (attacker.owner() == 0)
{
if (ourUnit !== undefined && ourUnit.hasClass("Unit") && !ourUnit.hasClass("Support"))
ourUnit.attack(e.attacker);
else
{
ourUnit.flee(attacker);
ourUnit.setMetadata(PlayerID,"fleeing", gameState.getTimeElapsed());
}
if (territory === PlayerID)
{
// anyway we'll register the animal as dangerous, and attack it (note: only on our territory. Don't care otherwise).
this.listOfWantedUnits[attacker.id()] = new EntityCollection(gameState.sharedScript);
this.listOfWantedUnits[attacker.id()].addEnt(attacker);
this.listOfWantedUnits[attacker.id()].freeze();
this.listOfWantedUnits[attacker.id()].registerUpdates();
var filter = Filters.byTargetedEntity(attacker.id());
this.WantedUnitsAttacker[attacker.id()] = this.myUnits.filter(filter);
this.WantedUnitsAttacker[attacker.id()].registerUpdates();
}
} // Disregard military units except in our territory. Disregard all calls in enemy territory.
else if (territory == PlayerID || (territory != attacker.owner() && ourUnit.hasClass("Support")))
{
// 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.
if (attacker.hasClass("Structure")) {
// todo: we ultimately have to check wether it's a danger point or an isolated area, and if it's a danger point, mark it as so.
// Right now, to make the AI less gameable, we'll mark any surrounding resource as inaccessible.
// usual tower range is 80. Be on the safe side.
var close = gameState.getResourceSupplies("wood").filter(Filters.byDistance(attacker.position(), 90));
close.forEach(function (supply) { //}){
supply.setMetadata(PlayerID, "inaccessible", true);
});
} else {
// TODO: right now a soldier always retaliate... Perhaps it should be set in "Defense" mode.
// TODO: handle the ship case
if (attacker.hasClass("Ship"))
continue;
// 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]) {
if (this.reevaluateEntity(gameState, attacker))
{
var position = attacker.position();
var close = HQ.enemyWatchers[attacker.owner()].enemySoldiers.filter(Filters.byDistance(position, self.armyCompactSize));
if (close.length > 2 || ourUnit.hasClass("Support") || attacker.hasClass("Siege"))
{
// armify it, then armify units close to him.
this.armify(gameState,attacker);
armyID = attacker.getMetadata(PlayerID, "inArmy");
close.forEach(function (ent) { //}){
if (API3.SquareVectorDistance(position, ent.position()) < self.armyCompactSize)
{
ent.setMetadata(PlayerID, "inArmy", armyID);
self.enemyArmy[ent.owner()][armyID].addEnt(ent);
}
});
return; // don't use too much processing power. If there are other cases, they'll be processed soon enough.
}
}
// Defensemanager will deal with them in the next turn.
}
if (ourUnit !== undefined && ourUnit.hasClass("Unit")) {
if (ourUnit.hasClass("Support")) {
// let's try to garrison this support unit.
if (ourUnit.position())
{
var buildings = gameState.getOwnEntities().filter(Filters.byCanGarrison()).filterNearest(ourUnit.position(),4).toEntityArray();
var garrisoned = false;
for (var i in buildings)
{
var struct = buildings[i];
if (struct.garrisoned() && struct.garrisonMax() - struct.garrisoned().length > 0)
{
garrisoned = true;
ourUnit.garrison(struct);
break;
}
}
if (!garrisoned) {
ourUnit.flee(attacker);
ourUnit.setMetadata(PlayerID,"fleeing", gameState.getTimeElapsed());
}
}
} else {
// It's a soldier. Right now we'll retaliate
// TODO: check for stronger units against this type, check for fleeing options, etc.
// Check also for neighboring towers and garrison there perhaps?
ourUnit.attack(e.attacker);
}
}
}
}
}
}
*/
}; // nice sets of closing brackets, isn't it?
return m;
}(PETRA);

View File

@ -0,0 +1,69 @@
var PETRA = function(m)
{
// returns some sort of DPS * health factor. If you specify a class, it'll use the modifiers against that class too.
m.getMaxStrength = function(ent, againstClass)
{
var strength = 0.0;
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];
if (type == "Slaughter" || type == "Charged")
continue;
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;
break;
case "hack":
strength += (val * 0.075) / 3;
break;
case "pierce":
strength += (val * 0.065) / 3;
break;
}
}
if (attackRange){
strength += (attackRange.max * 0.0125) ;
}
for (var str in attackTimes) {
var val = parseFloat(attackTimes[str]);
switch (str){
case "repeat":
strength += (val / 100000);
break;
case "prepare":
strength -= (val / 100000);
break;
}
}
}
for (var str in armourStrength) {
var val = parseFloat(armourStrength[str]);
switch (str) {
case "crush":
strength += (val * 0.085) / 3;
break;
case "hack":
strength += (val * 0.075) / 3;
break;
case "pierce":
strength += (val * 0.065) / 3;
break;
}
}
return strength * hp;
};
return m;
}(PETRA);

View File

@ -0,0 +1,16 @@
var PETRA = function(m)
{
m.EntityCollectionFromIds = function(gameState, idList){
var ents = {};
for (var i in idList){
var id = idList[i];
if (gameState.entities._entities[id]) {
ents[id] = gameState.entities._entities[id];
}
}
return new API3.EntityCollection(gameState.sharedScript, ents);
}
return m;
}(PETRA);

View File

@ -0,0 +1,70 @@
var PETRA = function(m)
{
// Some functions that could be part of the gamestate but are Aegis specific.
// The next three are to register that we assigned a gatherer to a resource this turn.
// expects an entity
m.IsSupplyFull = function(gamestate, supply)
{
if (supply.isFull() === true)
return true;
var count = supply.resourceSupplyGatherers().length;
if (gamestate.turnCache["ressourceGatherer"] && gamestate.turnCache["ressourceGatherer"][supply.id()])
count += gamestate.turnCache["ressourceGatherer"][supply.id()];
if (count >= supply.maxGatherers())
return true;
return false;
}
// add a gatherer to the turn cache for this supply.
m.AddTCGatherer = function(gamestate, supplyID)
{
if (gamestate.turnCache["ressourceGatherer"] && gamestate.turnCache["ressourceGatherer"][supplyID] !== undefined)
++gamestate.turnCache["ressourceGatherer"][supplyID];
else
{
if (!gamestate.turnCache["ressourceGatherer"])
gamestate.turnCache["ressourceGatherer"] = {};
gamestate.turnCache["ressourceGatherer"][supplyID] = 1;
}
}
// remove a gatherer to the turn cache for this supply.
m.RemoveTCGatherer = function(gamestate, supplyID)
{
if (gamestate.turnCache["ressourceGatherer"] && gamestate.turnCache["ressourceGatherer"][supplyID])
--gamestate.turnCache["ressourceGatherer"][supplyID];
else
if (!gamestate.turnCache["ressourceGatherer"])
gamestate.turnCache["ressourceGatherer"] = {};
gamestate.turnCache["ressourceGatherer"][supplyID] = -1;
}
m.GetTCGatherer = function(gamestate, supplyID)
{
if (gamestate.turnCache["ressourceGatherer"] && gamestate.turnCache["ressourceGatherer"][supplyID])
return gamestate.turnCache["ressourceGatherer"][supplyID];
else
return 0;
}
// The next two are to register that we assigned a gatherer to a resource this turn.
m.AddTCRessGatherer = function(gamestate, resource)
{
if (gamestate.turnCache["ressourceGatherer-" + resource])
++gamestate.turnCache["ressourceGatherer-" + resource];
else
gamestate.turnCache["ressourceGatherer-" + resource] = 1;
}
m.GetTCRessGatherer = function(gamestate, resource)
{
if (gamestate.turnCache["ressourceGatherer-" + resource])
return gamestate.turnCache["ressourceGatherer-" + resource];
else
return 0;
}
return m;
}(PETRA);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,237 @@
var PETRA = function(m)
{
// other map functions
m.TERRITORY_PLAYER_MASK = 0x3F;
m.createObstructionMap = function(gameState, accessIndex, template){
var passabilityMap = gameState.getMap();
var territoryMap = gameState.ai.territoryMap;
// default values
var placementType = "land";
var buildOwn = true;
var buildAlly = true;
var buildNeutral = true;
var buildEnemy = false;
// If there is a template then replace the defaults
if (template)
{
placementType = template.buildPlacementType();
buildOwn = template.hasBuildTerritory("own");
buildAlly = template.hasBuildTerritory("ally");
buildNeutral = template.hasBuildTerritory("neutral");
buildEnemy = template.hasBuildTerritory("enemy");
}
var obstructionMask = gameState.getPassabilityClassMask("foundationObstruction") | gameState.getPassabilityClassMask("building-land");
if (placementType == "shore")
{
// TODO: this won't change much, should be cached, it's slow.
var obstructionTiles = new Uint8Array(passabilityMap.data.length);
var okay = false;
for (var x = 0; x < passabilityMap.width; ++x)
{
for (var y = 0; y < passabilityMap.height; ++y)
{
var i = x + y*passabilityMap.width;
var tilePlayer = (territoryMap.data[i] & m.TERRITORY_PLAYER_MASK);
if (gameState.ai.myIndex !== gameState.ai.accessibility.landPassMap[i])
{
obstructionTiles[i] = 0;
continue;
}
if (gameState.isPlayerEnemy(tilePlayer) && tilePlayer !== 0)
{
obstructionTiles[i] = 0;
continue;
}
if ((passabilityMap.data[i] & (gameState.getPassabilityClassMask("building-shore") | gameState.getPassabilityClassMask("default"))))
{
obstructionTiles[i] = 0;
continue;
}
okay = false;
var positions = [[0,1], [1,1], [1,0], [1,-1], [0,-1], [-1,-1], [-1,0], [-1,1]];
var available = 0;
for each (var 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,true) > 500)
if ((passabilityMap.data[index2] & gameState.getPassabilityClassMask("default")) && gameState.ai.accessibility.getRegionSizei(index2,true) > 500)
if ((passabilityMap.data[index3] & gameState.getPassabilityClassMask("default")) && gameState.ai.accessibility.getRegionSizei(index3,true) > 500)
if ((passabilityMap.data[index4] & gameState.getPassabilityClassMask("default")) && gameState.ai.accessibility.getRegionSizei(index4,true) > 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;
}
obstructionTiles[i] = okay ? 255 : 0;
}
}
}
else
{
var playerID = PlayerID;
var obstructionTiles = new Uint8Array(passabilityMap.data.length);
for (var i = 0; i < passabilityMap.data.length; ++i)
{
var tilePlayer = (territoryMap.data[i] & m.TERRITORY_PLAYER_MASK);
var invalidTerritory = (
(!buildOwn && tilePlayer == playerID) ||
(!buildAlly && gameState.isPlayerAlly(tilePlayer) && tilePlayer != playerID) ||
(!buildNeutral && tilePlayer == 0) ||
(!buildEnemy && gameState.isPlayerEnemy(tilePlayer) && tilePlayer != 0)
);
if (accessIndex)
var tileAccessible = (accessIndex === gameState.ai.accessibility.landPassMap[i]);
else
var tileAccessible = true;
if (placementType === "shore")
tileAccessible = true;
obstructionTiles[i] = (!tileAccessible || invalidTerritory || (passabilityMap.data[i] & obstructionMask)) ? 0 : 255;
}
}
var map = new API3.Map(gameState.sharedScript, obstructionTiles);
map.setMaxVal(255);
if (template && template.buildDistance()) {
var minDist = template.buildDistance().MinDistance;
var category = template.buildDistance().FromCategory;
if (minDist !== undefined && category !== undefined){
gameState.getOwnStructures().forEach(function(ent) {
if (ent.buildCategory() === category && ent.position()){
var pos = ent.position();
var x = Math.round(pos[0] / gameState.cellSize);
var z = Math.round(pos[1] / gameState.cellSize);
map.addInfluence(x, z, minDist/gameState.cellSize, -255, 'constant');
}
});
}
}
return map;
};
m.createTerritoryMap = function(gameState) {
var map = gameState.ai.territoryMap;
var ret = new API3.Map(gameState.sharedScript, map.data);
ret.getOwner = function(p) { return this.point(p) & m.TERRITORY_PLAYER_MASK; };
ret.getOwnerIndex = function(p) { return this.map[p] & m.TERRITORY_PLAYER_MASK; };
return ret;
};
m.createFrontierMap = function(gameState, borderMap)
{
var territory = m.createTerritoryMap(gameState);
var around = [ [-0.7,0.7], [0,1], [0.7,0.7], [1,0], [0.7,-0.7], [0,-1], [-0.7,-0.7], [-1,0] ];
var map = new API3.Map(gameState.sharedScript);
var width = map.width;
var insideSmall = 10;
var insideLarge = 15;
for (var j = 0; j < territory.length; ++j)
{
if (territory.getOwnerIndex(j) !== PlayerID || borderMap.map[j] === 2)
continue;
var ix = j%width;
var iz = Math.floor(j/width);
for each (var a in around)
{
var jx = ix + Math.round(insideSmall*a[0]);
if (jx < 0 || jx >= width)
continue;
var jz = iz + Math.round(insideSmall*a[1]);
if (jz < 0 || jz >= width)
continue;
if (borderMap && borderMap.map[jx+width*jz] > 1)
continue;
if (!gameState.isPlayerAlly(territory.getOwnerIndex(jx+width*jz)))
{
map.map[j] = 2;
break;
}
jx = ix + Math.round(insideLarge*a[0]);
if (jx < 0 || jx >= width)
continue;
jz = iz + Math.round(insideLarge*a[1]);
if (jz < 0 || jz >= width)
continue;
if (borderMap && borderMap.map[jx+width*jz] > 1)
continue;
if (!gameState.isPlayerAlly(territory.getOwnerIndex(jx+width*jz)))
map.map[j] = 1;
}
}
// m.debugMap(gameState, map);
return map;
};
// TODO foresee the case of square maps
m.createBorderMap = function(gameState)
{
var map = new API3.Map(gameState.sharedScript);
var width = map.width;
var ic = (width - 1) / 2;
var radmax = (ic-2)*(ic-2); // we assume two inaccessible cells all around
for (var j = 0; j < map.length; ++j)
{
var dx = j%width - ic;
var dy = Math.floor(j/width) - ic;
var radius = dx*dx + dy*dy;
if (radius > radmax)
map.map[j] = 2;
else if (radius > (ic - 18)*(ic - 18))
map.map[j] = 1;
}
return map;
};
m.debugMap = function(gameState, map)
{
var width = map.width;
var cell = map.cellSize;
gameState.getEntities().forEach( function (ent) {
var pos = ent.position();
if (!pos)
return;
var x = Math.round(pos[0] / cell);
var z = Math.round(pos[1] / cell);
var id = x + width*z;
if (map.map[id] == 1)
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [2,0,0]});
else if (map.map[id] == 2)
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,2,0]});
else if (map.map[id] == 3)
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,0,2]});
});
};
return m;
}(PETRA);

View File

@ -0,0 +1,290 @@
var PETRA = function(m)
{
/* Naval Manager
Will deal with anything ships.
-Basically trade over water (with fleets and goals commissioned by the economy manager)
-Defense over water (commissioned by the defense manager)
-subtask being patrols, escort, naval superiority.
-Transport of units over water (a few units).
-Scouting, ultimately.
Also deals with handling docks, making sure we have access and stuffs like that.
Does not build them though, that's for the base manager to handle.
*/
m.NavalManager = function() {
// accessibility zones for which we have a dock.
// Connexion is described as [landindex] = [seaIndexes];
// technically they also exist for sea zones but I don't care.
this.landZoneDocked = [];
// list of seas I have a dock on.
this.accessibleSeas = [];
// ship subCollections. Also exist for land zones, idem, not caring.
this.seaShips = [];
this.seaTpShips = [];
this.seaWarships = [];
// wanted NB per zone.
this.wantedTpShips = [];
this.wantedWarships = [];
this.transportPlans = [];
this.askedPlans = [];
};
// More initialisation for stuff that needs the gameState
m.NavalManager.prototype.init = function(gameState, queues) {
// finished docks
this.docks = gameState.getOwnStructures().filter(API3.Filters.and(API3.Filters.byClass("Dock"), API3.Filters.not(API3.Filters.isFoundation())));
this.docks.allowQuickIter();
this.docks.registerUpdates();
this.ships = gameState.getOwnEntities().filter(API3.Filters.byClass("Ship"));
// note: those two can overlap (some transport ships are warships too and vice-versa).
this.tpShips = this.ships.filter(API3.Filters.byCanGarrison());
this.warships = this.ships.filter(API3.Filters.byClass("Warship"));
this.ships.registerUpdates();
this.tpShips.registerUpdates();
this.warships.registerUpdates();
for (var i = 0; i < gameState.ai.accessibility.regionSize.length; ++i)
{
if (gameState.ai.accessibility.regionType[i] !== "water")
{
// push dummies
this.seaShips.push(new API3.EntityCollection(gameState.sharedScript));
this.seaTpShips.push(new API3.EntityCollection(gameState.sharedScript));
this.seaWarships.push(new API3.EntityCollection(gameState.sharedScript));
this.wantedTpShips.push(0);
this.wantedWarships.push(0);
} else {
var collec = this.ships.filter(API3.Filters.byStaticMetadata(PlayerID, "sea", i));
collec.registerUpdates();
this.seaShips.push(collec);
collec = this.tpShips.filter(API3.Filters.byStaticMetadata(PlayerID, "sea", i));
collec.registerUpdates();
this.seaTpShips.push(collec);
var collec = this.warships.filter(API3.Filters.byStaticMetadata(PlayerID, "sea", i));
collec.registerUpdates();
this.seaWarships.push(collec);
this.wantedTpShips.push(1);
this.wantedWarships.push(1);
}
this.landZoneDocked.push([]);
}
};
m.NavalManager.prototype.getUnconnectedSeas = function (gameState, region) {
var seas = gameState.ai.accessibility.regionLinks[region]
if (seas.length === 0)
return [];
for (var i = 0; i < seas.length; ++i)
{
if (this.landZoneDocked[region].indexOf(seas[i]) !== -1)
seas.splice(i--,1);
}
return seas;
};
// returns true if there is a path from A to B and we have docks.
m.NavalManager.prototype.canReach = function (gameState, regionA, regionB) {
var path = gameState.ai.accessibility.getTrajectToIndex(regionA, regionB);
if (!path)
{
return false;
}
for (var i = 0; i < path.length - 1; ++i)
{
if (gameState.ai.accessibility.regionType[path[i]] == "land")
if (this.accessibleSeas.indexOf(path[i+1]) === -1)
{
m.debug ("cannot reach because of " + path[i+1]);
return false; // we wn't be able to board on that sea
}
}
return true;
};
m.NavalManager.prototype.checkEvents = function (gameState, queues, events) {
var evts = events["ConstructionFinished"];
// TODO: probably check stuffs like a base destruction.
for (var i in evts)
{
var evt = evts[i];
if (evt && evt.newentity)
{
var entity = gameState.getEntityById(evt.newentity);
if (entity && entity.hasClass("Dock") && entity.isOwn(PlayerID))
{
// okay we have a dock whose construction is finished.
// let's assign it to us.
var pos = entity.position();
var li = gameState.ai.accessibility.getAccessValue(pos);
var ni = entity.getMetadata(PlayerID, "sea");
if (this.landZoneDocked[li].indexOf(ni) === -1)
this.landZoneDocked[li].push(ni);
if (this.accessibleSeas.indexOf(ni) === -1)
this.accessibleSeas.push(ni);
}
}
}
};
m.NavalManager.prototype.addPlan = function(plan) {
this.transportPlans.push(plan);
};
// will create a plan at the end of the turn.
// many units can call this separately and end up in the same plan
// which can be useful.
m.NavalManager.prototype.askForTransport = function(entity, startPos, endPos) {
this.askedPlans.push([entity, startPos, endPos]);
};
// creates aforementionned plans
m.NavalManager.prototype.createPlans = function(gameState) {
var startID = {};
for (var i in this.askedPlans)
{
var plan = this.askedPlans[i];
var startIndex = gameState.ai.accessibility.getAccessValue(plan[1]);
var endIndex = gameState.ai.accessibility.getAccessValue(plan[2]);
if (startIndex === 1 || endIndex === -1)
continue;
if (!startID[startIndex])
{
startID[startIndex] = {};
startID[startIndex][endIndex] = { "dest" : plan[2], "units": [plan[0]]};
}
else if (!startID[startIndex][endIndex])
startID[startIndex][endIndex] = { "dest" : plan[2], "units": [plan[0]]};
else
startID[startIndex][endIndex].units.push(plan[0]);
}
for (var i in startID)
for (var k in startID[i])
{
var tpPlan = new m.TransportPlan(gameState, startID[i][k].units, startID[i][k].dest, false)
this.transportPlans.push (tpPlan);
}
};
// TODO: work on this.
m.NavalManager.prototype.maintainFleet = function(gameState, queues, events) {
// check if we have enough transport ships.
// check per region.
for (var i = 0; i < this.seaShips.length; ++i)
{
var tpNb = gameState.countOwnQueuedEntitiesWithMetadata("sea", i);
if (this.accessibleSeas.indexOf(i) !== -1 && this.seaTpShips[i].length < this.wantedTpShips[i]
&& tpNb + queues.ships.length() === 0 && gameState.getTemplate(gameState.applyCiv("units/{civ}_ship_bireme")).available(gameState))
{
// TODO: check our dock can build the wanted ship types, for Carthage.
queues.ships.addItem(new m.TrainingPlan(gameState, "units/{civ}_ship_bireme", { "sea" : i }, 1, 1 ));
}
}
};
// bumps up the number of ships we want if we need more.
m.NavalManager.prototype.checkLevels = function(gameState, queues) {
if (queues.ships.length() !== 0)
return;
for (var i = 0; i < this.transportPlans.length; ++i)
{
var plan = this.transportPlans[i];
if (plan.needTpShips())
{
var zone = plan.neededShipsZone();
if (zone && gameState.countOwnQueuedEntitiesWithMetadata("sea", zone) > 0)
continue;
if (zone && this.wantedTpShips[i] === 0)
this.wantedTpShips[i]++;
else if (zone && plan.allAtOnce)
this.wantedTpShips[i]++;
}
}
};
// assigns free ships to plans that need some
m.NavalManager.prototype.assignToPlans = function(gameState, queues, events) {
for (var i = 0; i < this.transportPlans.length; ++i)
{
var plan = this.transportPlans[i];
if (plan.needTpShips())
{
// assign one per go.
var zone = plan.neededShipsZone();
if (zone)
{
for each (var ship in this.seaTpShips[zone]._entities)
{
if (!ship.getMetadata(PlayerID, "tpplan"))
{
m.debug ("Assigning ship " + ship.id() + " to plan" + plan.ID);
plan.assignShip(gameState, ship);
return true;
}
}
}
}
}
return false;
};
m.NavalManager.prototype.checkActivePlan = function(ID) {
for (var i = 0; i < this.transportPlans.length; ++i)
if (this.transportPlans[i].ID === ID)
return true;
return false;
};
// Some functions are run every turn
// Others once in a while
m.NavalManager.prototype.update = function(gameState, queues, events) {
Engine.ProfileStart("Naval Manager update");
this.checkEvents(gameState, queues, events);
if (gameState.ai.playedTurn % 10 === 0)
{
this.maintainFleet(gameState, queues, events);
this.checkLevels(gameState, queues);
}
for (var i = 0; i < this.transportPlans.length; ++i)
if (!this.transportPlans[i].carryOn(gameState, this))
{
// whatever the reason, this plan needs to be ended
// it could be that it's finished though.
var seaZone = this.transportPlans[i].neededShipsZone();
var rallyPos = [];
this.docks.forEach(function (dock) {
if (dock.getMetadata(PlayerID,"sea") == seaZone)
rallyPos = dock.position();
});
this.transportPlans[i].ships.move(rallyPos);
this.transportPlans[i].releaseAll(gameState);
this.transportPlans.splice(i,1);
--i;
}
this.assignToPlans(gameState, queues, events);
if (gameState.ai.playedTurn % 10 === 2)
{
this.createPlans(gameState);
this.askedPlans = [];
}
Engine.ProfileStop();
};
return m;
}(PETRA);

View File

@ -0,0 +1,478 @@
var PETRA = function(m)
{
/*
Describes a transport plan
Constructor assign units (units is an ID array, or an ID), a destionation (position, ingame), and a wanted escort size.
If "onlyIfOk" is true, then the plan will only start if the wanted escort size is met.
The naval manager will try to deal with it accordingly.
By this I mean that the naval manager will find how to go from access point 1 to access point 2 (relying on in-game pathfinder for mvt)
And then carry units from there.
If units are over multiple accessibility indexes (ie different islands) it will first group them
Note: only assign it units currently over land, or it won't work.
Also: destination should probably be land, otherwise the units will be lost at sea.
*/
// TODO: finish the support of multiple accessibility indexes.
// TODO: this doesn't check we can actually reach in the init, which we might want?
m.TransportPlan = function(gameState, units, destination, allAtOnce, escortSize, onlyIfOK) {
var self = this;
this.ID = m.playerGlobals[PlayerID].uniqueIDTPlans++;
var unitsID = [];
if (units.length !== undefined)
unitsID = units;
else
unitsID = [units];
this.units = m.EntityCollectionFromIds(gameState, unitsID);
this.units.forEach(function (ent) { //}){
ent.setMetadata(PlayerID, "tpplan", self.ID);
ent.setMetadata(PlayerID, "formerRole", ent.getMetadata(PlayerID, "role"));
ent.setMetadata(PlayerID, "role", "transport");
});
this.units.freeze();
this.units.registerUpdates();
m.debug ("Starting a new plan with ID " + this.ID + " to " + destination);
m.debug ("units are " + uneval (units));
this.destination = destination;
this.destinationIndex = gameState.ai.accessibility.getAccessValue(destination);
if (allAtOnce)
this.allAtOnce = allAtOnce;
else
this.allAtOnce = false;
if (escortSize)
this.escortSize = escortSize;
else
this.escortSize = 0;
if (onlyIfOK)
this.onlyIfOK = onlyIfOK;
else
this.onlyIfOK = false;
this.state = "unstarted";
this.ships = gameState.ai.HQ.navalManager.ships.filter(Filters.byMetadata(PlayerID, "tpplan", this.ID));
// note: those two can overlap (some transport ships are warships too and vice-versa).
this.transportShips = gameState.ai.HQ.navalManager.tpShips.filter(Filters.byMetadata(PlayerID, "tpplan", this.ID));
this.escortShips = gameState.ai.HQ.navalManager.warships.filter(Filters.byMetadata(PlayerID, "tpplan", this.ID));
this.ships.registerUpdates();
this.transportShips.registerUpdates();
this.escortShips.registerUpdates();
};
// count available slots
m.TransportPlan.prototype.countFreeSlots = function(onlyTrulyFree)
{
var slots = 0;
this.transportShips.forEach(function (ent) { //}){
slots += ent.garrisonMax();
if (onlyTrulyFree)
slots -= ent.garrisoned().length;
});
}
m.TransportPlan.prototype.assignShip = function(gameState, ship)
{
ship.setMetadata(PlayerID,"tpplan", this.ID);
}
m.TransportPlan.prototype.releaseAll = function(gameState)
{
this.ships.forEach(function (ent) { ent.setMetadata(PlayerID,"tpplan", undefined) });
this.units.forEach(function (ent) {
var fRole = ent.getMetadata(PlayerID, "formerRole");
if (fRole)
ent.setMetadata(PlayerID,"role", fRole);
ent.setMetadata(PlayerID,"tpplan", undefined)
});
}
m.TransportPlan.prototype.releaseAllShips = function(gameState)
{
this.ships.forEach(function (ent) { ent.setMetadata(PlayerID,"tpplan", undefined) });
}
m.TransportPlan.prototype.needTpShips = function()
{
if ((this.allAtOnce && this.countFreeSlots() >= this.units.length) || this.transportShips.length > 0)
return false;
return true;
}
m.TransportPlan.prototype.needEscortShips = function()
{
return !((this.onlyIfOK && this.escortShips.length < this.escortSize) || !this.onlyIfOK);
}
// returns the zone for which we are needing our ships
m.TransportPlan.prototype.neededShipsZone = function()
{
if (!this.seaZone)
return false;
return this.seaZone;
}
// try to move on.
/* several states:
"unstarted" is the initial state, and will determine wether we follow basic or grouping path
Basic path:
- "waitingForBoarding" means we wait 'till we have enough transport ships and escort ships to move stuffs.
- "Boarding" means we're trying to board units onto our ships
- "Moving" means we're moving ships
- "Unboarding" means we're unbording
- Once we're unboarded, we either return to boarding point (if we still have units to board) or we clear.
> there is the possibility that we'll be moving units on land, but that's basically a restart too, with more clearing.
Grouping Path is basically the same with "grouping" and we never unboard (unless there is a need to)
*/
m.TransportPlan.prototype.carryOn = function(gameState, navalManager)
{
if (this.state === "unstarted")
{
// Okay so we can start the plan.
// So what we'll do is check what accessibility indexes our units are.
var unitIndexes = [];
this.units.forEach( function (ent) { //}){
var idx = gameState.ai.accessibility.getAccessValue(ent.position());
if (unitIndexes.indexOf(idx) === -1 && idx !== 1)
unitIndexes.push(idx);
});
// we have indexes. If there is more than 1, we'll try and regroup them.
if (unitIndexes.length > 1)
{
warn("Transport Plan path is too complicated, aborting");
return false;
/*
this.state = "waitingForGrouping";
// get the best index for grouping, ie start by the one farthest away in terms of movement.
var idxLength = {};
for (var i = 0; i < unitIndexes.length; ++i)
idxLength[unitIndexes[i]] = gameState.ai.accessibility.getTrajectToIndex(unitIndexes[i], this.destinationIndex).length;
var sortedArray = unitIndexes.sort(function (a,b) { //}){
return idxLength[b] - idxLength[a];
});
this.startIndex = sortedArray[0];
// okay so we'll board units from this index and we'll try to join them with units of the next index.
// this might not be terribly efficient but it won't be efficient anyhow.
return true;*/
}
this.state = "waitingForBoarding";
// let's get our index this turn.
this.startIndex = unitIndexes[0];
m.debug ("plan " + this.ID + " from " + this.startIndex);
return true;
}
if (this.state === "waitingForBoarding")
{
if (!this.path)
{
this.path = gameState.ai.accessibility.getTrajectToIndex(this.startIndex, this.destinationIndex);
if (!this.path || this.path.length === 0 || this.path.length % 2 === 0)
return false; // TODO: improve error handling
if (this.path[0] !== this.startIndex)
{
warn ("Start point of the path is not the start index, aborting transport plan");
return false;
}
// we have a path, register the first sea zone.
this.seaZone = this.path[1];
m.debug ("Plan " + this.ID + " over seazone " + this.seaZone);
}
// if we currently have no baoarding spot, try and find one.
if (!this.boardingSpot)
{
// TODO: improve on this whenever we have danger maps.
// okay so we have units over an accessibility index.
// we'll get a map going on.
var Xibility = gameState.ai.accessibility;
// custom obstruction map that uses the shore as the obstruction map
// but doesn't really check like for a building.
// created realtime with the other map.
var passabilityMap = gameState.getMap();
var territoryMap = gameState.ai.territoryMap;
var obstructionMask = gameState.getPassabilityClassMask("foundationObstruction") | gameState.getPassabilityClassMask("building-shore");
var obstructions = new API3.Map(gameState.sharedScript);
// wanted map.
var friendlyTiles = new API3.Map(gameState.sharedScript);
for (var j = 0; j < friendlyTiles.length; ++j)
{
// only on the wanted island
if (Xibility.landPassMap[j] !== this.startIndex)
continue;
// setting obstructions
var tilePlayer = (territoryMap.data[j] & TERRITORY_PLAYER_MASK);
// invalid is enemy-controlled or not on the right sea/land (we need a shore for this, we might want to check neighbhs instead).
var invalidTerritory = (gameState.isPlayerEnemy(tilePlayer) && tilePlayer != 0)
|| (Xibility.navalPassMap[j] !== this.path[1]);
obstructions.map[j] = (invalidTerritory || (passabilityMap.data[j] & obstructionMask)) ? 0 : 255;
// currently we'll just like better on our territory
if (tilePlayer == PlayerID)
friendlyTiles.map[j] = 100;
}
obstructions.expandInfluences();
var best = friendlyTiles.findBestTile(4, obstructions);
var bestIdx = best[0];
// not good enough.
if (best[1] <= 0)
{
best = friendlyTiles.findBestTile(1, obstructions);
bestIdx = best[0];
if (best[1] <= 0)
return false; // apparently we won't be able to board.
}
var x = ((bestIdx % friendlyTiles.width) + 0.5) * gameState.cellSize;
var z = (Math.floor(bestIdx / friendlyTiles.width) + 0.5) * gameState.cellSize;
// we have the spot we want to board at.
this.boardingSpot = [x,z];
m.debug ("Plan " + this.ID + " new boarding spot is " + this.boardingSpot);
}
// if all at once we need to be full, else we just need enough escort ships.
if (!this.needTpShips() && !this.needEscortShips())
{
// preparing variables
// TODO: destroy former entity collection.
this.garrisoningUnits = this.units.filter(Filters.not(Filters.isGarrisoned()));
this.garrisoningUnits.registerUpdates();
this.garrisoningUnits.freeze();
this.garrisonShipID = -1;
m.debug ("Boarding");
this.state = "boarding";
}
return true;
} else if (this.state === "waitingForGrouping")
{
// TODO: this.
return true;
}
if (this.state === "boarding" && gameState.ai.playedTurn % 5 === 0)
{
// TODO: improve error recognition.
if (this.units.length === 0)
return false;
if (!this.boardingSpot)
return false;
if (this.needTpShips())
{
this.state = "waitingForBoarding";
return true;
}
if (this.needEscortShips())
{
this.state = "waitingForBoarding";
return true;
}
// check if we aren't actually finished.
if (this.units.getCentrePosition() == undefined || this.countFreeSlots(true) === 0)
{
delete this.boardingSpot;
this.garrisoningUnits.unregister();
this.state = "moving";
return true;
}
// check if we need to move our units and ships closer together
var stillMoving = false;
if (API3.SquareVectorDistance(this.ships.getCentrePosition(),this.boardingSpot) > 1600)
{
this.ships.move(this.boardingSpot[0],this.boardingSpot[1]);
stillMoving = true; // wait till ships are in position
}
if (API3.SquareVectorDistance(this.units.getCentrePosition(),this.boardingSpot) > 1600)
{
this.units.move(this.boardingSpot[0],this.boardingSpot[1]);
stillMoving = true; // wait till units are in position
}
if (stillMoving)
{
return true; // wait.
}
// check if we need to try and board units.
var garrisonShip = gameState.getEntityById(this.garrisonShipID);
var self = this;
// check if ship we're currently garrisoning in is full
if (garrisonShip && garrisonShip.canGarrisonInside())
{
// okay garrison units
var nbStill = garrisonShip.garrisonMax() - garrisonShip.garrisoned().length;
if (this.garrisoningUnits.length < nbStill)
{
Engine.PostCommand({"type": "garrison", "entities": this.garrisoningUnits.toIdArray(), "target": garrisonShip.id(),"queued": false});
}
return true;
} else if (garrisonShip)
{
// full ship, abort
this.garrisonShipID = -1;
garrisonShip = false; // will enter next if.
}
if (!garrisonShip)
{
// could have died or could have be full
// we'll pick a new one, one that isn't full
for (var i in this.transportShips._entities)
{
if (this.transportShips._entities[i].canGarrisonInside())
{
this.garrisonShipID = this.transportShips._entities[i].id();
break;
}
}
return true; // wait.
}
// could I actually get here?
return true;
}
if (this.state === "moving")
{
if (!this.unboardingSpot)
{
// TODO: improve on this whenever we have danger maps.
// okay so we have units over an accessibility index.
// we'll get a map going on.
var Xibility = gameState.ai.accessibility;
// custom obstruction map that uses the shore as the obstruction map
// but doesn't really check like for a building.
// created realtime with the other map.
var passabilityMap = gameState.getMap();
var territoryMap = gameState.ai.territoryMap;
var obstructionMask = gameState.getPassabilityClassMask("foundationObstruction") | gameState.getPassabilityClassMask("building-shore");
var obstructions = new API3.Map(gameState.sharedScript);
// wanted map.
var friendlyTiles = new API3.Map(gameState.sharedScript);
var wantedIndex = -1;
if (this.path.length >= 3)
{
this.path.splice(0,2);
wantedIndex = this.path[0];
} else {
m.debug ("too short at " +uneval(this.path));
return false; // Incomputable
}
for (var j = 0; j < friendlyTiles.length; ++j)
{
// only on the wanted island
if (Xibility.landPassMap[j] !== wantedIndex)
continue;
// setting obstructions
var tilePlayer = (territoryMap.data[j] & TERRITORY_PLAYER_MASK);
// invalid is not on the right land (we need a shore for this, we might want to check neighbhs instead).
var invalidTerritory = (Xibility.landPassMap[j] !== wantedIndex);
obstructions.map[j] = (invalidTerritory || (passabilityMap.data[j] & obstructionMask)) ? 0 : 255;
// currently we'll just like better on our territory
if (tilePlayer == PlayerID)
friendlyTiles.map[j] = 100;
else if (gameState.isPlayerEnemy(tilePlayer) && tilePlayer != 0)
friendlyTiles.map[j] = 4;
else
friendlyTiles.map[j] = 50;
}
obstructions.expandInfluences();
var best = friendlyTiles.findBestTile(4, obstructions);
var bestIdx = best[0];
// not good enough.
if (best[1] <= 0)
{
best = friendlyTiles.findBestTile(1, obstructions);
bestIdx = best[0];
if (best[1] <= 0)
return false; // apparently we won't be able to unboard.
}
var x = ((bestIdx % friendlyTiles.width) + 0.5) * gameState.cellSize;
var z = (Math.floor(bestIdx / friendlyTiles.width) + 0.5) * gameState.cellSize;
// we have the spot we want to board at.
this.unboardingSpot = [x,z];
return true;
}
// TODO: improve error recognition.
if (this.units.length === 0)
return false;
if (!this.unboardingSpot)
return false;
// check if we need to move ships
if (API3.SquareVectorDistance(this.ships.getCentrePosition(),this.unboardingSpot) > 400)
{
this.ships.move(this.unboardingSpot[0],this.unboardingSpot[1]);
} else {
this.state = "unboarding";
return true;
}
return true;
}
if (this.state === "unboarding")
{
// TODO: improve error recognition.
if (this.units.length === 0)
return false;
// check if we need to move ships
if (API3.SquareVectorDistance(this.ships.getCentrePosition(),this.unboardingSpot) > 400)
{
this.ships.move(this.unboardingSpot[0],this.unboardingSpot[1]);
} else {
this.transportShips.forEach( function (ent) { ent.unloadAll() });
// TODO: improve on this.
if (this.path.length > 1)
{
m.debug ("plan " + this.ID + " going back for more");
// basically reset.
delete this.boardingSpot;
delete this.unboardingSpot;
this.state = "unstarted";
this.releaseAllShips();
return true;
}
m.debug ("plan " + this.ID + " is finished");
return false;
}
}
return true;
}
return m;
}(PETRA);

View File

@ -0,0 +1,131 @@
var PETRA = function(m)
{
/*
* Holds a list of wanted items to train or construct
*/
m.Queue = function()
{
this.queue = [];
this.paused = false;
this.switched = 0;
};
m.Queue.prototype.empty = function()
{
this.queue = [];
};
m.Queue.prototype.addItem = function(plan)
{
if (!plan) warn(" essaie d'ajout d un plan vide");
if (!plan)
return;
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);
};
m.Queue.prototype.check= function(gameState)
{
while (this.queue.length > 0)
{
if (!this.queue[0].isInvalid(gameState))
return;
warn(" plan " + this.queue[0].type + " invalid and suppressed");
this.queue.shift();
}
return;
};
m.Queue.prototype.getNext = function()
{
if (this.queue.length > 0)
return this.queue[0];
else
return null;
};
m.Queue.prototype.startNext = function(gameState)
{
if (this.queue.length > 0)
{
this.queue.shift().start(gameState);
return true;
}
else
return false;
};
// returns the maximal account we'll accept for this queue.
// Currently all the cost of the first element and fraction of that of the second
m.Queue.prototype.maxAccountWanted = function(gameState, fraction)
{
var cost = new API3.Resources();
if (this.queue.length > 0 && this.queue[0].isGo(gameState))
cost.add(this.queue[0].getCost());
if (this.queue.length > 1 && this.queue[1].isGo(gameState) && fraction > 0)
{
var costs = this.queue[1].getCost();
costs.multiply(fraction);
cost.add(costs);
}
return cost;
};
m.Queue.prototype.queueCost = function()
{
var cost = new API3.Resources();
for (var key in this.queue)
cost.add(this.queue[key].getCost());
return cost;
};
m.Queue.prototype.length = function()
{
return this.queue.length;
};
m.Queue.prototype.countQueuedUnits = function()
{
var count = 0;
for (var i in this.queue)
count += this.queue[i].number;
return count;
};
m.Queue.prototype.countQueuedUnitsWithClass = function(classe)
{
var count = 0;
for (var i in this.queue)
if (this.queue[i].template && this.queue[i].template.hasClass(classe))
count += this.queue[i].number;
return count;
};
m.Queue.prototype.countQueuedUnitsWithMetadata = function(data,value)
{
var count = 0;
for (var i in this.queue)
if (this.queue[i].metadata[data] && this.queue[i].metadata[data] == value)
count += this.queue[i].number;
return count;
};
m.Queue.prototype.countAllByType = function(t)
{
var count = 0;
for (var i in this.queue)
if (this.queue[i].type === t)
count += this.queue[i].number;
return count;
};
return m;
}(PETRA);

View File

@ -0,0 +1,619 @@
var PETRA = function(m)
{
// This takes the input queues and picks which items to fund with resources until no more resources are left to distribute.
//
// Currently this manager keeps accounts for each queue, split between the 4 main resources
//
// Each time resources are available (ie not in any account), it is split between the different queues
// Mostly based on priority of the queue, and existing needs.
// Each turn, the queue Manager checks if a queue can afford its next item, then it does.
//
// A consequence of the system it's not really revertible. Once a queue has an account of 500 food, it'll keep it
// If for some reason the AI stops getting new food, and this queue lacks, say, wood, no other queues will
// be able to benefit form the 500 food (even if they only needed food).
// This is not to annoying as long as all goes well. If the AI loses many workers, it starts being problematic.
//
// It also has the effect of making the AI more or less always sit on a few hundreds resources since most queues
// get some part of the total, and if all queues have 70% of their needs, nothing gets done
// Particularly noticeable when phasing: the AI often overshoots by a good 200/300 resources before starting.
//
// This system should be improved. It's probably not flexible enough.
m.QueueManager = function(Config, queues, priorities)
{
this.Config = Config;
this.queues = queues;
this.priorities = priorities;
this.accounts = {};
// the sorting is updated on priority change.
var self = this;
this.queueArrays = [];
for (var p in this.queues)
{
this.accounts[p] = new API3.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 = [];
};
m.QueueManager.prototype.getAvailableResources = function(gameState, noAccounts)
{
var resources = gameState.getResources();
if (noAccounts)
return resources;
for (var key in this.queues)
resources.subtract(this.accounts[key]);
return resources;
};
m.QueueManager.prototype.getTotalAccountedResources = function(gameState)
{
var resources = new API3.Resources();
for (var key in this.queues)
resources.add(this.accounts[key]);
return resources;
};
m.QueueManager.prototype.currentNeeds = function(gameState)
{
var needed = new API3.Resources();
//queueArrays because it's faster.
for (var i in this.queueArrays)
{
var name = this.queueArrays[i][0];
var queue = this.queueArrays[i][1];
if (queue.length() == 0 || !queue.queue[0].isGo(gameState))
continue;
// we need resource if the account is smaller than the cost
var costs = queue.queue[0].getCost();
for each (var ress in costs.types)
costs[ress] = Math.max(0, costs[ress] - this.accounts[name][ress]);
needed.add(costs);
}
return needed;
};
// calculate the gather rates we'd want to be able to start all elements in our queues
// TODO: many things.
m.QueueManager.prototype.wantedGatherRates = function(gameState)
{
// get out current resources, not removing accounts.
var current = this.getAvailableResources(gameState, true);
// short queue is the first item of a queue, assumed to be ready in 30s
// medium queue is the second item of a queue, assumed to be ready in 60s
// long queue contains the is the isGo=false items, assumed to be ready in 300s
var totalShort = { "food": 0, "wood": 0, "stone": 0, "metal": 0 };
var totalMedium = { "food": 0, "wood": 0, "stone": 0, "metal": 0 };
var totalLong = { "food": 0, "wood": 0, "stone": 0, "metal": 0 };
var total;
//queueArrays because it's faster.
for (var i in this.queueArrays)
{
var name = this.queueArrays[i][0];
var queue = this.queueArrays[i][1];
if (queue.paused)
continue;
for (var j = 0; j < queue.length(); ++j)
{
if (j > 1)
break;
var cost = queue.queue[j].getCost();
if (queue.queue[j].isGo(gameState))
{
if (j == 0)
total = totalShort;
else
total = totalMedium;
}
else
total = totalLong;
for (var type in total)
total[type] += cost[type];
if (!queue.queue[j].isGo(gameState))
break;
}
}
// global rates
var rates = { "food" : 0, "wood" : 0, "stone" : 0, "metal" : 0 };
var diff;
for (var type in rates)
{
if (current[type] > 0)
{
diff = Math.min(current[type], totalShort[type]);
totalShort[type] -= diff;
current[type] -= diff;
if (current[type] > 0)
{
diff = Math.min(current[type], totalMedium[type]);
totalMedium[type] -= diff;
current[type] -= diff;
if (current[type] > 0)
{
diff = Math.min(current[type], totalLong[type]);
totalLong[type] -= diff;
current[type] -= diff;
}
}
}
rates[type] = totalShort[type]/30 + totalMedium[type]/60 + totalLong[type]/300;
}
// if (this.Config.debug)
// {
// var oldRates = this.oldWantedGatherRates(gameState);
// warn(" old Rates " + uneval(oldRates) + " new rates " + uneval(rates));
// }
return rates;
// return oldRates;
};
m.QueueManager.prototype.oldWantedGatherRates = function(gameState)
{
// global rates
var rates = { "food" : 0, "wood" : 0, "stone" : 0, "metal" : 0 };
// per-queue.
var qTime = gameState.getTimeElapsed();
var time = gameState.getTimeElapsed();
var qCosts = { "food" : 0, "wood" : 0, "stone" : 0, "metal" : 0 };
var currentRess = this.getAvailableResources(gameState);
//queueArrays because it's faster.
for (var i in this.queueArrays)
{
qCosts = { "food" : 0, "wood" : 0, "stone" : 0, "metal" : 0 };
qTime = gameState.getTimeElapsed();
var name = this.queueArrays[i][0];
var queue = this.queueArrays[i][1];
// we'll move temporally along the queue.
for (var j = 0; j < queue.length(); ++j)
{
var elem = queue.queue[j];
var cost = elem.getCost();
var timeMultiplier = Math.max(1,(qTime-time)/25000);
if (!elem.isGo(gameState))
{
// assume we'll be wanted in four minutes.
// TODO: work on this.
for (var type in qCosts)
qCosts[type] += cost[type] / timeMultiplier;
qTime += 240000;
break; // disregard other stuffs.
}
// Assume we want it in 30 seconds from current time.
// Costs are made higher based on priority and lower based on current time.
// TODO: work on this.
for (var type in qCosts)
{
if (cost[type] === 0)
continue;
qCosts[type] += (cost[type] + Math.min(cost[type],this.priorities[name])) / timeMultiplier;
}
qTime += 30000; // TODO: this needs a lot more work.
}
for (var j in qCosts)
{
qCosts[j] -= this.accounts[name][j];
var diff = Math.min(qCosts[j], currentRess[j]);
qCosts[j] -= diff;
currentRess[j] -= diff;
rates[j] += qCosts[j]/(qTime/1000);
}
}
return rates;
};
/*m.QueueManager.prototype.logNeeds = function(gameState) {
if (!this.totor)
{
this.totor = [];
this.currentGathR = [];
this.currentGathRWanted = [];
this.ressLev = [];
}
if (gameState.ai.playedTurn % 10 !== 0)
return;
var array = this.wantedGatherRates(gameState);
this.totor.push( array );
var currentRates = {};
for (var type in array)
currentRates[type] = 0;
for (var i in gameState.ai.HQ.baseManagers)
{
var base = gameState.ai.HQ.baseManagers[i];
for (var type in array)
{
base.gatherersByType(gameState,type).forEach (function (ent) { //}){
var worker = ent.getMetadata(PlayerID, "worker-object");
if (worker)
currentRates[type] += worker.getGatherRate(gameState);
});
}
}
this.currentGathR.push( currentRates );
var types = Object.keys(array);
types.sort(function(a, b) {
var va = (Math.max(0,array[a] - currentRates[a]))/ (currentRates[a]+1);
var vb = (Math.max(0,array[b] - currentRates[b]))/ (currentRates[b]+1);
if (va === vb)
return (array[b]/(currentRates[b]+1)) - (array[a]/(currentRates[a]+1));
return vb-va;
});
this.currentGathRWanted.push( types );
var rss = gameState.getResources();
this.ressLev.push( {"food" : rss["food"],"stone" : rss["stone"],"wood" : rss["wood"],"metal" : rss["metal"]} );
if (gameState.getTimeElapsed() > 20*60*1000 && !this.once)
{
this.once = true;
for (var j in array)
{
log (j + ";");
for (var i = 0; i < this.totor.length; ++i)
{
log (this.totor[i][j] + ";");
}
}
log();
for (var j in array)
{
log (j + ";");
for (var i = 0; i < this.totor.length; ++i)
{
log (this.currentGathR[i][j] + ";");
}
}
log();
for (var j in array)
{
log (j + ";");
for (var i = 0; i < this.totor.length; ++i)
{
log (this.currentGathRWanted[i].indexOf(j) + ";");
}
}
log();
for (var j in array)
{
log (j + ";");
for (var i = 0; i < this.totor.length; ++i)
{
log (this.ressLev[i][j] + ";");
}
}
}
};
*/
m.QueueManager.prototype.printQueues = function(gameState)
{
warn("QUEUES");
for (var i in this.queues)
{
var qStr = "";
var q = this.queues[i];
if (q.queue.length > 0)
{
warn(i + ": ( paused " + q.paused + " with priority " + this.priorities[i] +" and accounts " + uneval(this.accounts[i]) +")");
warn(" while maxAccountWanted(0.6) is " + uneval(q.maxAccountWanted(gameState, 0.6)));
}
for (var j in q.queue)
{
qStr = " " + q.queue[j].type + " ";
if (q.queue[j].number)
qStr += "x" + q.queue[j].number;
qStr += " isGo " + q.queue[j].isGo(gameState);
warn(qStr);
}
}
warn("Accounts");
for (var p in this.accounts)
warn(p + ": " + uneval(this.accounts[p]));
warn("Current Resources:" + uneval(gameState.getResources()));
warn("Available Resources:" + uneval(this.getAvailableResources(gameState)));
warn("Wanted Gather Rates:" + uneval(this.wantedGatherRates(gameState)));
warn("Aegis Wanted Gather Rates:" + uneval(this.oldWantedGatherRates(gameState)));
warn("Current Gather Rates:" + uneval(gameState.ai.HQ.GetCurrentGatherRates(gameState)));
warn(" - - - - - - - -");
for (var i in gameState.ai.HQ.baseManagers)
for (var ress in gameState.ai.HQ.wantedRates)
warn(" DPresource " + i + " ress " + ress + " = " + gameState.ai.HQ.baseManagers[i].getResourceLevel(gameState, ress));
warn(" - - - - - - - -");
};
// nice readable HTML version.
m.QueueManager.prototype.HTMLprintQueues = function(gameState)
{
if (!m.DebugEnabled())
return;
var strToSend = [];
strToSend.push("<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01//EN\"> <html> <head> <title>Aegis Queue Manager</title> <link rel=\"stylesheet\" href=\"table.css\"> </head> <body> <table> <caption>Aegis Build Order</caption> ");
for (var i in this.queues)
{
strToSend.push("<tr>");
var q = this.queues[i];
var str = "<th>" + i + " (" + this.priorities[i] + ")<br><span class=\"ressLevel\">";
for each (var k in this.accounts[i].types)
if(k != "population")
{
str += this.accounts[i][k] + k.substr(0,1).toUpperCase() ;
if (k != "metal") str += " / ";
}
strToSend.push(str + "</span></th>");
for (var j in q.queue) {
if (q.queue[j].isGo(gameState))
strToSend.push("<td>");
else
strToSend.push("<td class=\"NotGo\">");
var qStr = "";
if (q.queue[j].number)
qStr += q.queue[j].number + " ";
qStr += q.queue[j].type;
qStr += "<br><span class=\"ressLevel\">";
var costs = q.queue[j].getCost();
for each (var k in costs.types)
{
qStr += costs[k] + k.substr(0,1).toUpperCase() ;
if (k != "metal")
qStr += " / ";
}
qStr += "</span></td>";
strToSend.push(qStr);
}
strToSend.push("</tr>");
}
strToSend.push("</table>");
/*strToSend.push("<h3>Accounts</h3>");
for (var p in this.accounts)
{
strToSend.push("<p>" + p + ": " + uneval(this.accounts[p]) + " </p>");
}*/
strToSend.push("<p>Wanted Gather Rate:" + uneval(this.wantedGatherRates(gameState)) + "</p>");
strToSend.push("<p>Current Resources:" + uneval(gameState.getResources()) + "</p>");
strToSend.push("<p>Available Resources:" + uneval(this.getAvailableResources(gameState)) + "</p>");
strToSend.push("</body></html>");
for each (var logged in strToSend)
log(logged);
};
m.QueueManager.prototype.clear = function()
{
this.curItemQueue = [];
for (var i in this.queues)
this.queues[i].empty();
};
m.QueueManager.prototype.update = function(gameState)
{
for (var i in this.queues)
{
this.queues[i].check(gameState); // do basic sanity checks on the queue
if (this.priorities[i] > 0)
continue;
warn("QueueManager received bad priorities, please report this error: " + uneval(this.priorities));
this.priorities[i] = 1; // TODO: make the Queue Manager not die when priorities are zero.
}
Engine.ProfileStart("Queue Manager");
// Let's assign resources to plans that need'em
var availableRes = this.getAvailableResources(gameState);
for (var ress in availableRes)
{
if (ress === "population")
continue;
if (availableRes[ress] > 0)
{
var totalPriority = 0;
var tempPrio = {};
var maxNeed = {};
// Okay so this is where it gets complicated.
// If a queue requires "ress" for the next elements (in the queue)
// And the account is not high enough for it.
// Then we add it to the total priority.
// To try and be clever, we don't want a long queue to hog all resources. So two things:
// -if a queue has enough of resource X for the 1st element, its priority is decreased (factor 2).
// -queues accounts are capped at "resources for the first + 60% of the next"
// This avoids getting a high priority queue with many elements hogging all of one resource
// uselessly while it awaits for other resources.
for (var j in this.queues)
{
// returns exactly the correct amount, ie 0 if we're not go.
var queueCost = this.queues[j].maxAccountWanted(gameState, 0.6);
if (this.queues[j].length() > 0 && this.accounts[j][ress] < queueCost[ress] && !this.queues[j].paused)
{
// adding us to the list of queues that need an update.
tempPrio[j] = this.priorities[j];
maxNeed[j] = queueCost[ress] - this.accounts[j][ress];
// if we have enough of that resource for our first item in the queue, diminish our priority.
if (this.accounts[j][ress] >= this.queues[j].getNext().getCost()[ress])
tempPrio[j] /= 2;
if (tempPrio[j])
totalPriority += tempPrio[j];
}
else if (this.accounts[j][ress] > queueCost[ress])
this.accounts[j][ress] = queueCost[ress];
}
// Now we allow resources to the accounts. We can at most allow "TempPriority/totalpriority*available"
// But we'll sometimes allow less if that would overflow.
for (var j in tempPrio)
{
// we'll add at much what can be allowed to this queue.
var toAdd = Math.floor(availableRes[ress] * tempPrio[j]/totalPriority);
var maxAdd = Math.min(maxNeed[j], toAdd);
this.accounts[j][ress] += maxAdd;
}
}
else
{
// We have no available resources, see if we can't "compact" them in one queue.
// compare queues 2 by 2, and if one with a higher priority could be completed by our amount, give it.
// TODO: this isn't perfect compression.
for (var j in this.queues)
{
if (this.queues[j].length() === 0 || this.queues[j].paused)
continue;
var queue = this.queues[j];
var queueCost = queue.maxAccountWanted(gameState, 0);
if (this.accounts[j][ress] >= queueCost[ress])
continue;
for (var i in this.queues)
{
if (i === j)
continue;
var otherQueue = this.queues[i];
if (this.priorities[i] >= this.priorities[j] || otherQueue.switched !== 0)
continue;
if (this.accounts[j][ress] + this.accounts[i][ress] < queueCost[ress])
continue;
var diff = queueCost[ress] - this.accounts[j][ress];
this.accounts[j][ress] += diff;
this.accounts[i][ress] -= diff;
++otherQueue.switched;
if (this.Config.debug)
warn ("switching queue " + ress + " from " + i + " to " + j + " in amount " + diff);
break;
}
}
}
}
// Start the next item in the queue if we can afford it.
for (var i in this.queueArrays)
{
var name = this.queueArrays[i][0];
var queue = this.queueArrays[i][1];
if (queue.length() > 0 && !queue.paused)
{
var item = queue.getNext();
var total = new API3.Resources();
total.add(this.accounts[name]);
if (total.canAfford(item.getCost()))
{
if (item.canStart(gameState))
{
this.accounts[name].subtract(item.getCost());
queue.startNext(gameState);
queue.switched = 0;
}
}
}
else if (queue.length() === 0)
{
this.accounts[name].reset();
queue.switched = 0;
}
}
Engine.ProfileStop();
};
m.QueueManager.prototype.pauseQueue = function(queue, scrapAccounts)
{
if (this.queues[queue])
{
this.queues[queue].paused = true;
if (scrapAccounts)
this.accounts[queue].reset();
}
};
m.QueueManager.prototype.unpauseQueue = function(queue)
{
if (this.queues[queue])
this.queues[queue].paused = false;
};
m.QueueManager.prototype.pauseAll = function(scrapAccounts, but)
{
for (var p in this.queues)
{
if (p != but)
{
if (scrapAccounts)
this.accounts[p].reset();
this.queues[p].paused = true;
}
}
};
m.QueueManager.prototype.unpauseAll = function(but)
{
for (var p in this.queues)
if (p != but)
this.queues[p].paused = false;
};
m.QueueManager.prototype.addQueue = function(queueName, priority)
{
if (this.queues[queueName] == undefined)
{
this.queues[queueName] = new m.Queue();
this.priorities[queueName] = priority;
this.accounts[queueName] = new API3.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]]) });
}
};
m.QueueManager.prototype.removeQueue = function(queueName)
{
if (this.queues[queueName] !== undefined)
{
if (this.curItemQueue.indexOf(queueName) !== -1)
this.curItemQueue.splice(this.curItemQueue.indexOf(queueName),1);
delete this.queues[queueName];
delete this.priorities[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]]) });
}
};
m.QueueManager.prototype.changePriority = function(queueName, newPriority)
{
var self = this;
if (this.queues[queueName] !== undefined)
this.priorities[queueName] = newPriority;
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]]) });
};
return m;
}(PETRA);

View File

@ -0,0 +1,81 @@
var PETRA = function(m)
{
/*
* Common functions and variables to all queue plans.
* has a "--" suffix because it needs to be loaded before the other queueplan files.
*/
m.QueuePlan = function(gameState, type, metadata)
{
this.type = gameState.applyCiv(type);
this.metadata = metadata;
this.template = gameState.getTemplate(this.type);
if (!this.template)
{
warn("Tried to add the inexisting template " + this.type + " to Petra. Please report this on the forums")
return false;
}
this.ID = m.playerGlobals[PlayerID].uniqueIDBOPlans++;
this.cost = new API3.Resources(this.template.cost());
this.number = 1;
this.category = "";
this.lastIsGo = undefined;
return true;
};
// Check the content of this queue
m.QueuePlan.prototype.isInvalid = function(gameState)
{
return false;
};
// if true, the queue manager will begin increasing this plan's account.
m.QueuePlan.prototype.isGo = function(gameState)
{
return true;
};
// can we start this plan immediately?
m.QueuePlan.prototype.canStart = function(gameState)
{
return false;
};
// process the plan.
m.QueuePlan.prototype.start = function(gameState)
{
// should call onStart.
};
m.QueuePlan.prototype.getCost = function()
{
var costs = new API3.Resources();
costs.add(this.cost);
if (this.number !== 1)
costs.multiply(this.number);
return costs;
};
// On Event functions.
// Can be used to do some specific stuffs
// Need to be updated to actually do something if you want them to.
// this is called by "Start" if it succeeds.
m.QueuePlan.prototype.onStart = function(gameState)
{
};
// This is called by "isGo()" if it becomes true while it was false.
m.QueuePlan.prototype.onGo = function(gameState)
{
};
// This is called by "isGo()" if it becomes false while it was true.
m.QueuePlan.prototype.onNotGo = function(gameState)
{
};
return m;
}(PETRA);

View File

@ -0,0 +1,262 @@
var PETRA = function(m)
{
// Defines a construction plan, ie a building.
// We'll try to fing a good position if non has been provided
m.ConstructionPlan = function(gameState, type, metadata, position)
{
if (!m.QueuePlan.call(this, gameState, type, metadata))
return false;
this.position = position ? position : 0;
this.category = "building";
return true;
};
m.ConstructionPlan.prototype = Object.create(m.QueuePlan.prototype);
// checks other than resource ones.
// TODO: change this.
// TODO: if there are specific requirements here, maybe try to do them?
m.ConstructionPlan.prototype.canStart = function(gameState)
{
if (gameState.buildingsBuilt > 0) // do not start another building if already one this turn
return false;
if (!this.isGo(gameState))
return false;
// 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);
};
m.ConstructionPlan.prototype.start = function(gameState)
{
var builders = gameState.findBuilders(this.type).toEntityArray();
// We don't care which builder we assign, since they won't actually
// do the building themselves - all we care about is that there is
// some unit that can start the foundation
var pos = this.findGoodPosition(gameState);
if (!pos){
if (this.template.hasClass("Naval"))
gameState.ai.HQ.dockFailed = true;
gameState.ai.HQ.stopBuilding.push(this.type);
return;
}
this.buildingsBuilt++;
if (this.metadata === undefined)
this.metadata = { "base": pos.base };
else if (this.metadata.base === undefined)
this.metadata.base = pos.base;
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, this.metadata);
}
else
{
// try with the lowest, move towards us unless we're same
if (pos.x == pos.xx && pos.z == pos.zz)
builders[0].construct(this.type, pos.x, pos.z, pos.angle, this.metadata);
else
{
for (var step = 0; step <= 1; step += 0.2)
builders[0].construct(this.type, (step*pos.x + (1-step)*pos.xx), (step*pos.z + (1-step)*pos.zz), pos.angle, this.metadata);
}
}
this.onStart(gameState);
};
m.ConstructionPlan.prototype.findGoodPosition = function(gameState)
{
var template = gameState.getTemplate(this.type);
if (!this.position)
{
if (template.hasClass("CivCentre"))
{
if (this.metadata.type)
var pos = gameState.ai.HQ.findEconomicCCLocation(gameState, this.metadata.type);
else
var pos = gameState.ai.HQ.findStrategicCCLocation(gameState);
if (pos)
return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "xx": pos[0], "zz": pos[1], "base": 0 };
else
return false;
}
else if (template.hasClass("Tower") || template.hasClass("Fortress"))
{
var pos = gameState.ai.HQ.findDefensiveLocation(gameState, template);
if (pos)
return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "xx": pos[0], "zz": pos[1], "base": pos[2] };
else
{
gameState.ai.HQ.stopBuilding.push(this.type);
return false;
}
}
}
var cellSize = gameState.cellSize; // size of each tile
// First, find all tiles that are far enough away from obstructions:
var obstructionMap = m.createObstructionMap(gameState, 0, template);
if (template.buildCategory() !== "Dock")
obstructionMap.expandInfluences();
//obstructionMap.dumpIm(template.buildCategory() + "_obstructions.png");
// Compute each tile's closeness to friendly structures:
var friendlyTiles = new API3.Map(gameState.sharedScript);
var alreadyHasHouses = false;
if (this.position) // If a position was specified then place the building as close to it as possible
{
var x = Math.floor(this.position[0] / cellSize);
var z = Math.floor(this.position[1] / cellSize);
friendlyTiles.addInfluence(x, z, 255);
}
else // No position was specified so try and find a sensible place to build
{
// give a small > 0 level as the result of addInfluence is constrained to be > 0
if (this.metadata && this.metadata.base !== undefined)
{
var base = this.metadata.base;
for (var j = 0; j < friendlyTiles.map.length; ++j)
if (gameState.ai.HQ.basesMap.map[j] === base)
friendlyTiles.map[j] = 30;
}
else
{
for (var j = 0; j < friendlyTiles.map.length; ++j)
if (gameState.ai.HQ.basesMap.map[j] !== 0)
friendlyTiles.map[j] = 30;
}
gameState.getOwnStructures().forEach(function(ent) {
var pos = ent.position();
var x = Math.round(pos[0] / cellSize);
var z = Math.round(pos[1] / cellSize);
if (ent.resourceDropsiteTypes() && ent.resourceDropsiteTypes().indexOf("food") !== -1)
{
if (template.hasClass("Field"))
friendlyTiles.addInfluence(x, z, 20, 50);
else // If this is not a field add a negative influence because we want to leave this area for fields
friendlyTiles.addInfluence(x, z, 20, -20);
}
else if (template.hasClass("House"))
{
if (ent.hasClass("House"))
{
friendlyTiles.addInfluence(x, z, 15, 40); // houses are close to other houses
alreadyHasHouses = true;
}
else
friendlyTiles.addInfluence(x, z, 15, -40); // and further away from other stuffs
}
else if (template.hasClass("Farmstead") && !ent.hasClass("Field"))
friendlyTiles.addInfluence(x, z, 25, -25); // move farmsteads away to make room.
else if (template.hasClass("GarrisonFortress") && ent.genericName() == "House")
friendlyTiles.addInfluence(x, z, 30, -50);
else if (template.hasClass("Military"))
friendlyTiles.addInfluence(x, z, 10, -40);
});
if (template.hasClass("Farmstead"))
{
for (var j = 0; j < friendlyTiles.map.length; ++j)
{
var value = friendlyTiles.map[j] - (gameState.sharedScript.resourceMaps["wood"].map[j])/3;
friendlyTiles.map[j] = value >= 0 ? value : 0;
if (gameState.ai.HQ.borderMap.map[j] > 0)
friendlyTiles.map[j] /= 2; // we need space around farmstead, so disfavor map border
}
}
}
// requires to be inside our territory, and inside our base territory if required
if (this.metadata && this.metadata.base !== undefined)
{
var base = this.metadata.base;
for (var j = 0; j < friendlyTiles.map.length; ++j)
if (gameState.ai.HQ.basesMap.map[j] !== base)
friendlyTiles.map[j] = 0;
}
else
{
for (var j = 0; j < friendlyTiles.map.length; ++j)
if (gameState.ai.HQ.basesMap.map[j] === 0)
friendlyTiles.map[j] = 0;
}
// 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.hasClass("Fortress") || this.type == "structures/{civ}_siege_workshop")
radius = Math.floor(template.obstructionRadius() / cellSize) + 3;
else if (template.buildCategory() === "Dock")
radius = 1;
else if (template.resourceDropsiteTypes() === undefined)
radius = Math.ceil(template.obstructionRadius() / cellSize) + 1;
else
radius = Math.ceil(template.obstructionRadius() / cellSize);
// Find the best non-obstructed
if (template.hasClass("House") && !alreadyHasHouses)
{
// try to get some space first
var bestTile = friendlyTiles.findBestTile(10, obstructionMap);
var bestIdx = bestTile[0];
var bestVal = bestTile[1];
}
if (bestVal === undefined || bestVal === -1)
{
var bestTile = friendlyTiles.findBestTile(radius, obstructionMap);
var bestIdx = bestTile[0];
var bestVal = bestTile[1];
}
if (bestVal <= 0)
{
gameState.ai.HQ.stopBuilding.push(this.type);
return false;
}
var x = ((bestIdx % friendlyTiles.width) + 0.5) * cellSize;
var z = (Math.floor(bestIdx / friendlyTiles.width) + 0.5) * cellSize;
if (template.hasClass("House") || template.hasClass("Field") || template.resourceDropsiteTypes() !== undefined)
var secondBest = obstructionMap.findLowestNeighbor(x,z);
else
var secondBest = [x,z];
// default angle = 3*Math.PI/4;
return { "x": x, "z": z, "angle": 3*Math.PI/4, "xx": secondBest[0], "zz": secondBest[1], "base": gameState.ai.HQ.basesMap.map[bestIdx] };
};
return m;
}(PETRA);

View File

@ -0,0 +1,58 @@
var PETRA = function(m)
{
m.ResearchPlan = function(gameState, type, rush)
{
if (!m.QueuePlan.call(this, gameState, type, {}))
return false;
if (this.template.researchTime === undefined)
return false;
this.category = "technology";
this.rush = rush ? true : false;
return true;
};
m.ResearchPlan.prototype = Object.create(m.QueuePlan.prototype);
m.ResearchPlan.prototype.canStart = function(gameState)
{
// also checks canResearch
return (gameState.findResearchers(this.type).length !== 0);
};
m.ResearchPlan.prototype.isInvalid = function(gameState)
{
return (gameState.isResearched(this.type) || gameState.isResearching(this.type));
};
m.ResearchPlan.prototype.start = function(gameState)
{
var self = this;
//m.debug ("Starting the research plan for " + this.type);
var trainers = gameState.findResearchers(this.type).toEntityArray();
//for (var 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());
});
// drop anything in the queue if we rush it.
if (this.rush)
trainers[0].stopAllProduction(0.45);
trainers[0].research(this.type);
}
this.onStart(gameState);
};
return m;
}(PETRA);

View File

@ -0,0 +1,72 @@
var PETRA = function(m)
{
m.TrainingPlan = function(gameState, type, metadata, number, maxMerge)
{
if (!m.QueuePlan.call(this, gameState, type, metadata))
{
warn(" Plan training " + type + " canceled");
return false;
}
this.category = "unit";
this.cost = new API3.Resources(this.template.cost(), this.template._template.Cost.Population);
this.number = number !== undefined ? number : 1;
this.maxMerge = maxMerge !== undefined ? maxMerge : 5;
return true;
};
m.TrainingPlan.prototype = Object.create(m.QueuePlan.prototype);
m.TrainingPlan.prototype.canStart = function(gameState)
{
if (this.invalidTemplate)
return false;
// TODO: we should probably check pop caps
var trainers = gameState.findTrainers(this.type);
return (trainers.length != 0);
};
m.TrainingPlan.prototype.start = function(gameState)
{
//warn("Executing TrainingPlan " + uneval(this));
var self = this;
var trainers = gameState.findTrainers(this.type).toEntityArray();
// Prefer training buildings with short queues
// (TODO: this should also account for units added to the queue by
// plans that have already been executed this turn)
if (trainers.length > 0)
{
trainers.sort(function(a, b) {
var aa = a.trainingQueueTime();
var bb = b.trainingQueueTime();
if (a.hasClass("Civic") && !self.template.hasClass("Support"))
aa += 0.9;
if (b.hasClass("Civic") && !self.template.hasClass("Support"))
bb += 0.9;
return (aa - bb);
});
if (this.metadata && this.metadata.base !== undefined && this.metadata.base === 0)
this.metadata.base = trainers[0].getMetadata(PlayerID,"base");
trainers[0].train(this.type, this.number, this.metadata);
}
else
warn("pas de trainers for this queue " + this.type);
this.onStart(gameState);
};
m.TrainingPlan.prototype.addItem = function(amount)
{
if (amount === undefined)
amount = 1;
this.number += amount;
};
return m;
}(PETRA);

View File

@ -0,0 +1,123 @@
var PETRA = function(m)
{
/*
* Used to know which templates I have, which templates I know I can train, things like that.
* Mostly unused.
*/
m.TemplateManager = function(gameState) {
var self = this;
this.knownTemplatesList = [];
this.buildingTemplates = [];
this.unitTemplates = [];
this.templateCounters = {};
this.templateCounteredBy = {};
// this will store templates that exist
this.AcknowledgeTemplates(gameState);
this.getBuildableSubtemplates(gameState);
this.getTrainableSubtemplates(gameState);
this.getBuildableSubtemplates(gameState);
this.getTrainableSubtemplates(gameState);
// should be enough in 100% of the cases.
this.getTemplateCounters(gameState);
};
m.TemplateManager.prototype.AcknowledgeTemplates = function(gameState)
{
var self = this;
var myEntities = gameState.getOwnEntities();
myEntities.forEach(function(ent)
{
var template = ent._templateName;
if (self.knownTemplatesList.indexOf(template) === -1) {
self.knownTemplatesList.push(template);
if (ent.hasClass("Unit") && self.unitTemplates.indexOf(template) === -1)
self.unitTemplates.push(template);
else if (self.buildingTemplates.indexOf(template) === -1)
self.buildingTemplates.push(template);
}
});
}
m.TemplateManager.prototype.getBuildableSubtemplates = function(gameState)
{
for each (var templateName in this.knownTemplatesList) {
var template = gameState.getTemplate(templateName);
if (template !== null) {
var buildable = template.buildableEntities();
if (buildable !== undefined)
for each (var subtpname in buildable) {
if (this.knownTemplatesList.indexOf(subtpname) === -1) {
this.knownTemplatesList.push(subtpname);
var subtemplate = gameState.getTemplate(subtpname);
if (subtemplate.hasClass("Unit") && this.unitTemplates.indexOf(subtpname) === -1)
this.unitTemplates.push(subtpname);
else if (this.buildingTemplates.indexOf(subtpname) === -1)
this.buildingTemplates.push(subtpname);
}
}
}
}
}
m.TemplateManager.prototype.getTrainableSubtemplates = function(gameState)
{
for each (var templateName in this.knownTemplatesList) {
var template = gameState.getTemplate(templateName);
if (template !== null) {
var trainables = template.trainableEntities();
if (trainables !== undefined)
for each (var subtpname in trainables) {
if (this.knownTemplatesList.indexOf(subtpname) === -1) {
this.knownTemplatesList.push(subtpname);
var subtemplate = gameState.getTemplate(subtpname);
if (subtemplate.hasClass("Unit") && this.unitTemplates.indexOf(subtpname) === -1)
this.unitTemplates.push(subtpname);
else if (this.buildingTemplates.indexOf(subtpname) === -1)
this.buildingTemplates.push(subtpname);
}
}
}
}
}
m.TemplateManager.prototype.getTemplateCounters = function(gameState)
{
for (var i in this.unitTemplates)
{
var tp = gameState.getTemplate(this.unitTemplates[i]);
var tpname = this.unitTemplates[i];
this.templateCounters[tpname] = tp.getCounteredClasses();
}
}
// features auto-caching
m.TemplateManager.prototype.getCountersToClasses = function(gameState,classes,templateName)
{
if (templateName !== undefined && this.templateCounteredBy[templateName])
return this.templateCounteredBy[templateName];
var templates = [];
for (var i in this.templateCounters) {
var okay = false;
for each (var ticket in this.templateCounters[i]) {
var okaya = true;
for (var a in ticket[0]) {
if (classes.indexOf(ticket[0][a]) === -1)
okaya = false;
}
if (okaya && templates.indexOf(i) === -1)
templates.push([i, ticket[1]]);
}
}
templates.sort (function (a,b) { return -a[1] + b[1]; });
if (templateName !== undefined)
this.templateCounteredBy[templateName] = templates;
return templates;
}
return m;
}(PETRA);

View File

@ -0,0 +1,109 @@
var PETRA = function(m)
{
/**
* Manage the trade
*/
m.TradeManager = function(Config)
{
this.Config = Config;
this.tradeRoute = undefined;
this.targetNumTraders = this.Config.Economy.targetNumTraders;
};
m.TradeManager.prototype.init = function(gameState)
{
this.traders = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "role", "trader"));
this.traders.allowQuickIter();
this.traders.registerUpdates();
};
m.TradeManager.prototype.setTradeRoute = function(market1, market2)
{
this.tradeRoute = { "source": market1, "target": market2 };
};
m.TradeManager.prototype.hasTradeRoute = function()
{
return (this.tradeRoute !== undefined);
};
m.TradeManager.prototype.assignTrader = function(ent)
{
unit.setMetadata(PlayerID, "role", "trader");
this.traders.updateEnt(unit);
};
// TODO take trader ships into account
m.TradeManager.prototype.trainMoreTraders = function(gameState, queues)
{
var numTraders = gameState.countEntitiesAndQueuedByType(gameState.applyCiv("units/{civ}_support_trader"), true);
if (numTraders < this.targetNumTraders && queues.trader.countQueuedUnits() === 0)
{
var template = gameState.applyCiv("units/{civ}_support_trader")
queues.trader.addItem(new m.TrainingPlan(gameState, template, { "role": "trader", "base": 0 }, 1, 1));
}
};
m.TradeManager.prototype.update = function(gameState, queues)
{
if (!this.tradeRoute)
return;
var self = this;
if (gameState.ai.playedTurn % 100 === 9)
this.setTradingGoods(gameState);
this.trainMoreTraders(gameState, queues);
this.traders.forEach(function(ent) { self.updateTrader(ent) });
};
// TODO deal with garrisoned trader & check if the trade route (i.e. its markets) still exist
m.TradeManager.prototype.updateTrader = function(ent)
{
if (!ent.isIdle() || !ent.position())
return;
if (API3.SquareVectorDistance(this.tradeRoute.target.position(), ent.position()) > API3.SquareVectorDistance(this.tradeRoute.source.position(), ent.position()))
ent.tradeRoute(this.tradeRoute.target, this.tradeRoute.source);
else
ent.tradeRoute(this.tradeRoute.source, this.tradeRoute.target);
};
m.TradeManager.prototype.setTradingGoods = function(gameState)
{
var tradingGoods = { "food": 0, "wood": 0, "stone": 0, "metal": 0 };
// first, try to anticipate future needs
var stocks = gameState.ai.HQ.GetTotalResourceLevel(gameState);
var remaining = 100;
this.targetNumTraders = this.Config.Economy.targetNumTraders;
for (var type in stocks)
{
if (type == "food")
continue;
if (stocks[type] < 200)
{
tradingGoods[type] = 20;
this.targetNumTraders += 2;
}
else if (stocks[type] < 500)
{
tradingGoods[type] = 10;
this.targetNumTraders += 1;
}
remaining -= tradingGoods[type];
}
// then add what is needed now
var mainNeed = Math.floor(remaining * 70 / 100)
var nextNeed = remaining - mainNeed;
var mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState);
tradingGoods[mostNeeded[0]] += mainNeed;
tradingGoods[mostNeeded[1]] += nextNeed;
Engine.PostCommand(PlayerID, {"type": "set-trading-goods", "tradingGoods": tradingGoods});
if (this.Config.debug == 2)
warn(" trading goods set to " + uneval(tradingGoods));
};
return m;
}(PETRA);

View File

@ -0,0 +1,32 @@
var PETRA = function(m)
{
m.AssocArraytoArray = function(assocArray) {
var endArray = [];
for (var i in assocArray)
endArray.push(assocArray[i]);
return endArray;
};
// A is the reference, B must be in "range" of A
// this supposes the range is already squared
m.inRange = function(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.
m.ManhattanDistance = function(a, b)
{
var dx = a[0] - b[0];
var dz = a[1] - b[1];
return Math.abs(dx) + Math.abs(dz);
}
return m;
}(PETRA);

View File

@ -0,0 +1,520 @@
var PETRA = function(m)
{
/**
* This class makes a worker do as instructed by the economy manager
*/
m.Worker = function(ent) {
this.ent = ent;
this.baseID = 0;
this.lastUpdate = undefined;
};
m.Worker.prototype.update = function(baseManager, gameState) {
this.lastUpdate = gameState.ai.playedTurn;
this.baseID = baseManager.ID;
var subrole = this.ent.getMetadata(PlayerID, "subrole");
if (!this.ent.position() || (this.ent.getMetadata(PlayerID,"fleeing") && gameState.getTimeElapsed() - this.ent.getMetadata(PlayerID,"fleeing") < 8000)){
// If the worker has no position then no work can be done
return;
}
if (this.ent.getMetadata(PlayerID,"fleeing"))
this.ent.setMetadata(PlayerID,"fleeing", undefined);
// Okay so we have a few tasks.
// If we're gathering, we'll check that we haven't run idle.
// ANd we'll also check that we're gathering a resource we want to gather.
// If we're fighting or hunting, let's not start gathering, heh?
if (this.ent.unitAIState().split(".")[1] === "COMBAT" || this.ent.getMetadata(PlayerID, "role") === "transport")
return;
/* if (this.ent.unitAIState() == "INDIVIDUAL.IDLE" && subrole != "hunter")
{
var role = this.ent.getMetadata(PlayerID, "role");
var base = this.ent.getMetadata(PlayerID, "base");
var founda = this.ent.getMetadata(PlayerID, "target-foundation");
warn(" unit idle " + this.ent.id() + " role " + role + " subrole " + subrole + " base " + base + " founda " + founda);
} */
if (subrole === "gatherer")
{
if (this.ent.isIdle())
{
// if we aren't storing resources or it's the same type as what we're about to gather,
// let's just pick a new resource.
// TODO if we already carry the max we can -> returnresources
if (!this.ent.resourceCarrying() || this.ent.resourceCarrying().length === 0 ||
this.ent.resourceCarrying()[0].type === this.ent.getMetadata(PlayerID, "gather-type"))
{
this.startGathering(gameState, baseManager);
}
else if (!this.returnResources(gameState)) // try to deposit resources
{
// no dropsite, abandon old resources and start gathering new ones
this.startGathering(gameState, baseManager);
}
}
else if (this.ent.unitAIState().split(".")[1] === "GATHER")
{
// we're already gathering. But let's check from time to time if there is nothing better
// in case UnitAI did something bad
if (this.ent.unitAIOrderData().length)
{
var supplyId = this.ent.unitAIOrderData()[0]["target"];
var supply = gameState.getEntityById(supplyId);
if (supply && !supply.hasClass("Field") && !supply.hasClass("Animal")
&& supplyId !== this.ent.getMetadata(PlayerID, "supply"))
{
var nbGatherers = supply.resourceSupplyGatherers().length
+ m.GetTCGatherer(gameState, supplyId);
if ((nbGatherers > 0 && supply.resourceSupplyAmount()/nbGatherers < 40))
{
m.RemoveTCGatherer(gameState, supplyId);
this.startGathering(gameState, baseManager);
}
else
{
var gatherType = this.ent.getMetadata(PlayerID, "gather-type");
var nearby = baseManager.dropsiteSupplies[gatherType]["nearby"];
var isNearby = nearby.some(function(sup) {
if (sup.id === supplyId)
return true;
return false;
});
if (nearby.length === 0 || isNearby)
this.ent.setMetadata(PlayerID, "supply", supplyId);
else
{
m.RemoveTCGatherer(gameState, supplyId);
this.startGathering(gameState, baseManager);
}
}
}
}
}
}
else if (subrole === "builder")
{
if (this.ent.unitAIState().split(".")[1] === "REPAIR")
return;
// okay so apparently we aren't working.
// Unless we've been explicitely told to keep our role, make us idle.
var target = gameState.getEntityById(this.ent.getMetadata(PlayerID, "target-foundation"));
if (!target || (target.foundationProgress() === undefined && target.needsRepair() === false))
{
this.ent.setMetadata(PlayerID, "subrole", "idle");
this.ent.setMetadata(PlayerID, "target-foundation", undefined);
// If worker elephant, move away to avoid being trapped in between constructions
if (this.ent.hasClass("Elephant"))
this.moveAway(baseManager, gameState);
}
else
{
if (target && target.foundationProgress() === undefined && target.needsRepair() === true)
warn(" target " + target.id() + " needs repair " + target.hitpoints() + " max " + target.maxHitpoints());
this.ent.repair(target);
}
}
else if (subrole === "hunter")
{
if (this.ent.isIdle())
this.startHunting(gameState, baseManager);
}
};
m.Worker.prototype.startGathering = function(gameState, baseManager)
{
if (!this.ent.position()) // TODO: work out what to do when entity has no position
return false;
var resource = this.ent.getMetadata(PlayerID, "gather-type");
// Then if we are gathering food, try to hunt
if (resource === "food" && this.startHunting(gameState, baseManager))
return true;
var foundSupply = function(ent, supply) {
var ret = false;
for (var i = 0; i < supply.length; ++i)
{
// exhausted resource, remove it from this list
if (!supply[i].ent || !gameState.getEntityById(supply[i].id))
{
supply.splice(i--, 1);
continue;
}
if (m.IsSupplyFull(gameState, supply[i].ent) === true)
continue;
// check if available resource is worth one additionnal gatherer (except for farms)
var nbGatherers = supply[i].ent.resourceSupplyGatherers().length
+ m.GetTCGatherer(gameState, supply[i].id);
if (supply[i].ent.resourceSupplyType()["specific"] !== "grain"
&& nbGatherers > 0 && supply[i].ent.resourceSupplyAmount()/(1+nbGatherers) < 40)
continue;
// not in ennemy territory
var territoryOwner = m.createTerritoryMap(gameState).getOwner(supply[i].ent.position());
if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally
continue;
m.AddTCGatherer(gameState, supply[i].id);
ent.gather(supply[i].ent);
ent.setMetadata(PlayerID, "supply", supply[i].id);
ret = true;
break;
}
return ret;
};
var nearby = baseManager.dropsiteSupplies[resource]["nearby"];
if (foundSupply(this.ent, nearby))
return true;
// --> for food, try to gather from fields if any, otherwise build one if any
if (resource === "food" && (this.gatherNearestField(gameState) || this.buildAnyField(gameState)))
return true;
var medium = baseManager.dropsiteSupplies[resource]["medium"];
if (foundSupply(this.ent, medium))
return true;
// So if we're here we have checked our whole base for a proper resource without success
// --> check other bases before going back to faraway resources
for each (var base in gameState.ai.HQ.baseManagers)
{
if (base.ID === this.baseID)
continue;
var nearby = base.dropsiteSupplies[resource]["nearby"];
if (foundSupply(this.ent, nearby))
{
this.ent.setMetadata(PlayerID, "base", base.ID);
return true;
}
}
for each (var base in gameState.ai.HQ.baseManagers)
{
if (base.ID === this.baseID)
continue;
var medium = base.dropsiteSupplies[resource]["medium"];
if (foundSupply(this.ent, medium))
{
this.ent.setMetadata(PlayerID, "base", base.ID);
return true;
}
}
// Okay so we haven't found any appropriate dropsite anywhere.
// Try to help building one if any foundation available in the same base
var self = this;
var foundations = gameState.getOwnFoundations().toEntityArray();
var shouldBuild = foundations.some(function(foundation) {
if (!foundation || foundation.getMetadata(PlayerID, "base") != self.baseID)
return false;
if ((resource !== "food" && foundation.hasClass("Storehouse")) ||
(resource === "food" && foundation.hasClass("DropsiteFood")))
{
self.ent.repair(foundation);
return true;
}
return false;
});
if (shouldBuild)
return true;
// Still nothing, we look now for faraway resources, first in this base, then in others
var faraway = baseManager.dropsiteSupplies[resource]["faraway"];
if (foundSupply(this.ent, faraway))
return true;
for each (var base in gameState.ai.HQ.baseManagers)
{
if (base.ID === this.baseID)
continue;
var faraway = base.dropsiteSupplies[resource]["faraway"];
if (foundSupply(this.ent, faraway))
{
this.ent.setMetadata(PlayerID, "base", base.ID);
return true;
}
}
return false;
};
// Makes the worker deposit the currently carried resources at the closest dropsite
m.Worker.prototype.returnResources = function(gameState)
{
if (!this.ent.resourceCarrying() || this.ent.resourceCarrying().length === 0 || !this.ent.position())
return false;
var resource = this.ent.resourceCarrying()[0].type;
var self = this;
var closestDropsite = undefined;
var dist = Math.min();
gameState.getOwnDropsites(resource).forEach(function(dropsite){
if (dropsite.position())
{
var d = API3.SquareVectorDistance(self.ent.position(), dropsite.position());
if (d < dist)
{
dist = d;
closestDropsite = dropsite;
}
}
});
if (!closestDropsite)
return false;
this.ent.returnResources(closestDropsite);
return true;
};
m.Worker.prototype.startHunting = function(gameState, baseManager)
{
if (!this.ent.position())
return false;
// So here we're doing it basic. We check what we can hunt, we hunt it. No fancies.
var resources = gameState.getHuntableSupplies();
if (resources.length === 0)
return false;
var nearestSupplyDist = Math.min();
var nearestSupply = undefined;
var isCavalry = this.ent.hasClass("Cavalry");
var isRanged = this.ent.hasClass("Ranged");
var entPosition = this.ent.position();
var nearestDropsiteDist = function(supply){
var distMin = 1000000;
var pos = supply.position();
gameState.getOwnDropsites("food").forEach(function (dropsite){
if (!dropsite.position())
return;
var dist = API3.SquareVectorDistance(pos, dropsite.position());
if (dist < distMin)
distMin = dist;
});
return distMin;
};
resources.forEach(function(supply)
{
if (!supply.position())
return;
if (supply.getMetadata(PlayerID, "inaccessible") === true)
return;
if (m.IsSupplyFull(gameState, supply) === true)
return;
// check if available resource is worth one additionnal gatherer (except for farms)
var nbGatherers = supply.resourceSupplyGatherers().length
+ m.GetTCGatherer(gameState, supply.id());
if (nbGatherers > 0 && supply.resourceSupplyAmount()/(1+nbGatherers) < 40)
return;
// Only cavalry and range units should hunt fleeing animals
if (!supply.hasClass("Domestic") && !isCavalry && !isRanged)
return;
// quickscope accessbility check
if (!gameState.ai.accessibility.pathAvailable(gameState, entPosition, supply.position(),false, true))
return;
// measure the distance to the resource
var dist = API3.SquareVectorDistance(entPosition, supply.position());
// Only cavalry should hunt faraway
if (!isCavalry && dist > 25000)
return;
// some simple check for chickens: if they're in a inaccessible square, we won't gather from them.
// TODO: make sure this works with rounding.
if (supply.footprintRadius() < 1)
{
var fakeMap = new API3.Map(gameState.sharedScript, 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;
}
}
// Avoid ennemy territory
var territoryOwner = m.createTerritoryMap(gameState).getOwner(supply.position());
if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally
return;
var dropsiteDist = nearestDropsiteDist(supply);
if (dropsiteDist > 35000)
return;
// Only cavalry should hunt faraway (specially for non domestic animals which flee)
if (!isCavalry && (dropsiteDist > 10000 || ((dropsiteDist > 7000 || territoryOwner == 0 ) && !supply.hasClass("Domestic"))))
return;
if (dist < nearestSupplyDist)
{
nearestSupplyDist = dist;
nearestSupply = supply;
}
});
if (nearestSupply)
{
m.AddTCGatherer(gameState, nearestSupply.id());
this.ent.gather(nearestSupply);
this.ent.setMetadata(PlayerID, "supply", nearestSupply.id());
this.ent.setMetadata(PlayerID, "target-foundation", undefined);
return true;
}
else
{
if (this.ent.getMetadata(PlayerID,"subrole") === "hunter")
this.ent.setMetadata(PlayerID, "subrole", "idle");
return false;
}
};
m.Worker.prototype.getResourceType = function(type){
if (!type || !type.generic)
return undefined;
if (type.generic === "treasure")
return type.specific;
else
return type.generic;
};
m.Worker.prototype.getGatherRate = function(gameState) {
if (this.ent.getMetadata(PlayerID,"subrole") !== "gatherer")
return 0;
var rates = this.ent.resourceGatherRates();
if (this.ent.unitAIOrderData().length && this.ent.unitAIState().split(".")[1] === "GATHER" && this.ent.unitAIOrderData()[0]["target"])
{
var ress = gameState.getEntityById(this.ent.unitAIOrderData()[0]["target"]);
if (!ress)
return 0;
var type = ress.resourceSupplyType();
if (type.generic == "treasure")
return 1000;
var tstring = type.generic + "." + type.specific;
//m.debug (+rates[tstring] + " for " + tstring + " for " + this.ent._templateName);
if (rates[tstring])
return rates[tstring];
return 0;
}
return 0;
};
m.Worker.prototype.gatherNearestField = function(gameState){
if (!this.ent.position())
return false;
var self = this;
var ownFields = gameState.getOwnEntitiesByType(gameState.applyCiv("structures/{civ}_field"), true);
var bestFarmEnt = undefined;
var bestFarmDist = 10000000;
ownFields.forEach(function (field) {
if (m.IsSupplyFull(gameState, field) === true)
return;
var dist = API3.SquareVectorDistance(field.position(), self.ent.position());
if (dist < bestFarmDist)
{
bestFarmEnt = field;
bestFarmDist = dist;
}
});
if (bestFarmEnt !== undefined)
{
this.ent.setMetadata(PlayerID, "base", bestFarmEnt.getMetadata(PlayerID, "base"));
m.AddTCGatherer(gameState, bestFarmEnt.id());
this.ent.gather(bestFarmEnt);
this.ent.setMetadata(PlayerID, "supply", bestFarmEnt.id());
return true;
}
return false;
};
/**
* WARNING with the present options of AI orders, the unit will not gather after building the farm.
* This is done by calling the gatherNearestField function when construction is completed.
*/
m.Worker.prototype.buildAnyField = function(gameState){
var self = this;
var foundations = gameState.getOwnFoundations();
var baseFoundations = foundations.filter(API3.Filters.byMetadata(PlayerID, "base", this.baseID));
var maxGatherers = gameState.getTemplate(gameState.applyCiv("structures/{civ}_field")).maxGatherers();
var bestFarmEnt = undefined;
var bestFarmDist = 10000000;
baseFoundations.forEach(function (found) {
if (found.hasClass("Field")) {
var current = found.getBuildersNb();
if (current === undefined || current >= maxGatherers)
return;
var dist = API3.SquareVectorDistance(found.position(), self.ent.position());
if (dist < bestFarmDist)
{
bestFarmEnt = found;
bestFarmDist = dist;
}
}
});
if (bestFarmEnt !== undefined)
{
this.ent.repair(bestFarmEnt);
return true;
}
// No farms found, search in other bases
foundations.forEach(function (found) {
if (found.hasClass("Field")) {
var current = found.getBuildersNb();
if (current === undefined || current >= maxGatherers)
return;
var dist = API3.SquareVectorDistance(found.position(), self.ent.position());
if (dist < bestFarmDist)
{
bestFarmEnt = found;
bestFarmDist = dist;
}
}
});
if (bestFarmEnt !== undefined)
{
this.ent.repair(bestFarmEnt);
this.ent.setMetadata(PlayerID, "base", bestFarmEnt.getMetadata(PlayerID, "base"));
return true;
}
return false;
};
// Workers elephant should move away from the buildings they've built to avoid being trapped in between constructions
// For the time being, we move towards the nearest gatherer (providing him a dropsite)
m.Worker.prototype.moveAway = function(baseManager, gameState){
var gatherers = baseManager.workersBySubrole(gameState, "gatherer").toEntityArray();
var pos = this.ent.position();
var dist = Math.min();
var destination = pos;
for (var i = 0; i < gatherers.length; ++i)
{
if (gatherers[i].isIdle())
continue;
var distance = API3.SquareVectorDistance(pos, gatherers[i].position());
if (distance > dist)
continue;
dist = distance;
destination = gatherers[i].position();
}
this.ent.move(destination[0], destination[1]);
};
return m;
}(PETRA);