0ad/binaries/data/mods/public/simulation/ai/petra/tradeManager.js

342 lines
11 KiB
JavaScript
Raw Normal View History

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)
{
if (this.tradeRoute.source.hasClass("NavalMarket") && this.tradeRoute.target.hasClass("NavalMarket"))
return; // TODO naval trade not yet implemented
if (!this.tradeRoute.source.hasClass("NavalMarket"))
var base = this.tradeRoute.source.getMetadata(PlayerID, "base");
else
var base = this.tradeRoute.target.getMetadata(PlayerID, "base");
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": base }, 1, 1));
}
};
// 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);
};
// Try to setup trade routes TODO complete it
// TODO use also docks (should be counted in Class("Market"), but may be build one when necessary
m.TradeManager.prototype.buildTradeRoute = function(gameState, queues)
{
var filter = API3.Filters.and(API3.Filters.byClass("Market"), API3.Filters.not(API3.Filters.isFoundation()));
var market1 = gameState.getOwnStructures().filter(filter).toEntityArray();
var market2 = gameState.getExclusiveAllyEntities().filter(filter).toEntityArray();
if (market1.length + market2.length < 1) // We have to wait ... a first market will be built soon
return false;
var needed = 2;
if (market2.length > 0)
var needed = 1;
if (market1.length < needed)
{
// TODO what to do if market1 is invalid ??? should not happen
// and when checking the number >= needed, be sure that they are accessible => We should count the pairs
if (!market1[0] || !market1[0].position())
return false;
if (queues.economicBuilding.countQueuedUnitsWithClass("Market") > 0 ||
gameState.getOwnStructures().filter(API3.Filters.byClass("Market")).length >= needed)
return false;
if (!gameState.ai.HQ.canBuild(gameState, "structures/{civ}_market"))
return false;
// We have to build a second market ... try to put it as far as possible from the first one
// we affect it temporarily to the farthest base, but the real placement will be done
// in queueplan-building.js
var marketBase = market1[0].getMetadata(PlayerID, "base");
var distmax = -1;
var base = -1;
for (var i in gameState.ai.HQ.baseManagers)
{
var baseManager = gameState.ai.HQ.baseManagers[i];
if (!baseManager.anchor || !baseManager.anchor.position())
continue;
var dist = API3.SquareVectorDistance(market1[0].position(), baseManager.anchor.position());
if (dist < distmax)
continue;
distmax = dist;
base = +i;
}
if (distmax > 0)
{
if (this.Config.debug > 2)
API3.warn(" a second market will be built in base " + base);
// TODO build also docks when better
queues.economicBuilding.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_market", { "base": base }));
}
return false;
}
if (market2.length === 0)
market2 = market1;
var distmax = -1;
var imax = -1;
var jmax = -1;
for (var m1 of market1)
{
if (!m1.position())
continue;
var index1 = gameState.ai.accessibility.getAccessValue(m1.position());
for (var m2 of market2)
{
if (m1.id() === m2.id())
continue;
if (!m2.position())
continue;
var index2 = gameState.ai.accessibility.getAccessValue(m2.position());
if (m2.hasClass("Dock") && m2.getMetadata(PlayerID, "sea") === undefined)
{
// m2 may-be an allied dock, without sea already affected to it
var sea = gameState.ai.HQ.navalManager.getDockIndex(gameState, m2, true);
m2.setMetadata(PlayerID, "sea", sea);
}
if (index1 !== index2 && !(m1.hasClass("Dock") && m2.hasClass("Dock") && m1.getMetadata(PlayerID, "sea") === m2.getMetadata(PlayerID, "sea")))
continue;
var dist = API3.SquareVectorDistance(m1.position(), m2.position());
if (dist < distmax)
continue;
distmax = dist;
this.setTradeRoute(m1, m2, Math.round(distmax / this.Config.distUnitGain));
}
}
if (distmax < 0)
{
if (this.Config.debug > 2)
API3.warn("no trade route possible");
return false;
}
if (this.Config.debug > 1)
API3.warn("one trade route set with gain " + Math.round(distmax / this.Config.distUnitGain));
return true;
};
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 += 3;
}
else if (stocks[type] < 500)
{
tradingGoods[type] = 15;
this.targetNumTraders += 2;
}
else if (stocks[type] < 1000)
{
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].type] += mainNeed;
if (mostNeeded[1].wanted > 0)
tradingGoods[mostNeeded[1].type] += nextNeed;
else
tradingGoods[mostNeeded[0].type] += nextNeed;
Engine.PostCommand(PlayerID, {"type": "set-trading-goods", "tradingGoods": tradingGoods});
if (this.Config.debug > 2)
API3.warn(" trading goods set to " + uneval(tradingGoods));
};
// Try to barter unneeded resources for needed resources.
// only once per turn because the info doesn't update between a turn and fixing isn't worth it.
m.TradeManager.prototype.performBarter = function(gameState)
{
var barterers = gameState.getOwnStructures().filter(API3.Filters.and(API3.Filters.byClass("BarterMarket"), API3.Filters.not(API3.Filters.isFoundation()))).toEntityArray();
if (barterers.length === 0)
return false;
// Available resources after account substraction
var available = gameState.ai.queueManager.getAvailableResources(gameState);
var needs = gameState.ai.queueManager.currentNeeds(gameState);
var rates = gameState.ai.HQ.GetCurrentGatherRates(gameState);
var prices = gameState.getBarterPrices();
// calculates conversion rates
var getBarterRate = function (prices,buy,sell) { return Math.round(100 * prices["sell"][sell] / prices["buy"][buy]); };
// loop through each missing resource checking if we could barter and help finishing a queue quickly.
for (var buy of needs.types)
{
if (needs[buy] == 0 || needs[buy] < rates[buy]*30) // check if our rate allows to gather it fast enough
continue;
// pick the best resource to barter.
var bestToSell = undefined;
var bestRate = 0;
for (var sell of needs.types)
{
if (sell === buy)
continue;
if (needs[sell] > 0 || available[sell] < 500) // do not sell if we need it or do not have enough buffer
continue;
if (sell === "food")
{
var barterRateMin = 30;
if (available[sell] > 40000)
barterRateMin = 0;
else if (available[sell] > 15000)
barterRateMin = 5;
else if (available[sell] > 1000)
barterRateMin = 10;
}
else
{
var barterRateMin = 70;
if (available[sell] > 1000)
barterRateMin = 50;
if (buy === "food")
barterRateMin += 20;
}
var barterRate = getBarterRate(prices, buy, sell);
if (barterRate > bestRate && barterRate > barterRateMin)
{
bestRate = barterRate;
bestToSell = sell;
}
}
if (bestToSell !== undefined)
{
barterers[0].barter(buy, bestToSell, 100);
if (this.Config.debug > 2)
API3.warn("Necessity bartering: sold " + bestToSell +" for " + buy + " >> need sell " + needs[bestToSell]
+ " need buy " + needs[buy] + " rate buy " + rates[buy] + " available sell " + available[bestToSell]
+ " available buy " + available[buy] + " barterRate " + bestRate);
return true;
}
}
// now do contingency bartering, selling food to buy finite resources (and annoy our ennemies by increasing prices)
if (available["food"] < 1000 || needs["food"] > 0)
return false;
var bestToBuy = undefined;
var bestChoice = 0;
for (var buy of needs.types)
{
if (buy === "food")
continue;
var barterRateMin = 80;
if (available[buy] < 5000 && available["food"] > 5000)
barterRateMin -= (20 - Math.floor(available[buy]/250));
var barterRate = getBarterRate(prices, buy, "food");
if (barterRate < barterRateMin)
continue;
var choice = barterRate / (100 + available[buy]);
if (choice > bestChoice)
{
bestChoice = choice;
bestToBuy = buy;
}
}
if (bestToBuy !== undefined)
{
barterers[0].barter(bestToBuy, "food", 100);
if (this.Config.debug > 2)
API3.warn("Contingency bartering: sold food for " + bestToBuy + " available sell " + available["food"]
+ " available buy " + available[bestToBuy] + " barterRate " + getBarterRate(prices, bestToBuy, "food"));
return true;
}
return false;
};
m.TradeManager.prototype.update = function(gameState, queues)
{
this.performBarter(gameState);
if (!this.tradeRoute && (gameState.ai.playedTurn % 5 !== 2 || !this.buildTradeRoute(gameState, queues)))
return;
var source = this.tradeRoute.source;
var target = this.tradeRoute.target;
if (!source || !target || !gameState.getEntityById(source.id()) || !gameState.getEntityById(target.id()))
{
if (this.Config.debug > 2)
API3.warn("We have lost our trade route");
this.tradeRoute = undefined;
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) });
if (!this.tradeRoute || gameState.ai.playedTurn % 20 !== 10)
return;
gameState.ai.HQ.researchManager.researchTradeBonus(gameState, queues)
};
return m;
}(PETRA);