mirror of
https://github.com/vcmi/vcmi.git
synced 2025-11-25 22:42:04 +02:00
181 lines
5.3 KiB
C++
181 lines
5.3 KiB
C++
|
|
/*
|
||
|
|
* ResourceTrader.cpp, part of VCMI engine
|
||
|
|
*
|
||
|
|
* Authors: listed in file AUTHORS in main folder
|
||
|
|
*
|
||
|
|
* License: GNU General Public License v2.0 or later
|
||
|
|
* Full text of license available in license.txt file, in main folder
|
||
|
|
*/
|
||
|
|
#include "ResourceTrader.h"
|
||
|
|
|
||
|
|
namespace NK2AI
|
||
|
|
{
|
||
|
|
bool ResourceTrader::trade(const std::unique_ptr<BuildAnalyzer> & buildAnalyzer, std::shared_ptr<CCallback> cc, TResources freeResources)
|
||
|
|
{
|
||
|
|
// TODO: Mircea: Maybe include based on how close danger is: X as default + proportion of close danger or something around that
|
||
|
|
constexpr float ARMY_GOLD_RATIO_PER_MAKE_TURN_PASS = 0.1f;
|
||
|
|
constexpr float EXPENDABLE_BULK_RATIO = 0.3f;
|
||
|
|
bool haveTraded = false;
|
||
|
|
ObjectInstanceID marketId;
|
||
|
|
|
||
|
|
// TODO: Mircea: What about outside town markets that have better rates than a single town for example?
|
||
|
|
// Are those used anywhere? To inspect.
|
||
|
|
for (const auto * const town : cc->getTownsInfo())
|
||
|
|
{
|
||
|
|
if (town->hasBuiltSomeTradeBuilding())
|
||
|
|
{
|
||
|
|
marketId = town->id;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!marketId.hasValue())
|
||
|
|
return false;
|
||
|
|
|
||
|
|
const CGObjectInstance * obj = cc->getObj(marketId, false);
|
||
|
|
assert(obj);
|
||
|
|
// if (!obj)
|
||
|
|
// return false;
|
||
|
|
|
||
|
|
const auto * market = dynamic_cast<const IMarket *>(obj);
|
||
|
|
assert(market);
|
||
|
|
// if (!market)
|
||
|
|
// return false;
|
||
|
|
|
||
|
|
bool shouldTryToTrade = true;
|
||
|
|
while(shouldTryToTrade)
|
||
|
|
{
|
||
|
|
shouldTryToTrade = false;
|
||
|
|
buildAnalyzer->update();
|
||
|
|
|
||
|
|
// if we favor getResourcesRequiredNow is better on short term, if we favor getTotalResourcesRequired is better on long term
|
||
|
|
TResources missingNow = buildAnalyzer->getMissingResourcesNow(ARMY_GOLD_RATIO_PER_MAKE_TURN_PASS);
|
||
|
|
if(missingNow.empty())
|
||
|
|
break;
|
||
|
|
|
||
|
|
TResources income = buildAnalyzer->getDailyIncome();
|
||
|
|
// We don't want to sell something that's necessary later on, though that could make short term a bit harder sometimes
|
||
|
|
TResources freeAfterMissingTotal = buildAnalyzer->getFreeResourcesAfterMissingTotal(ARMY_GOLD_RATIO_PER_MAKE_TURN_PASS);
|
||
|
|
|
||
|
|
#if NK2AI_TRACE_LEVEL >= 2
|
||
|
|
logAi->info("ResourceTrader: Free %s. FreeAfterMissingTotal %s. MissingNow %s", freeResources.toString(), freeAfterMissingTotal.toString(), missingNow.toString());
|
||
|
|
#endif
|
||
|
|
|
||
|
|
if(ResourceTrader::tradeHelper(EXPENDABLE_BULK_RATIO, market, missingNow, income, freeAfterMissingTotal, buildAnalyzer, cc))
|
||
|
|
{
|
||
|
|
haveTraded = true;
|
||
|
|
shouldTryToTrade = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return haveTraded;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool ResourceTrader::tradeHelper(
|
||
|
|
float EXPENDABLE_BULK_RATIO,
|
||
|
|
const IMarket * market,
|
||
|
|
TResources missingNow,
|
||
|
|
TResources income,
|
||
|
|
TResources freeAfterMissingTotal,
|
||
|
|
const std::unique_ptr<BuildAnalyzer> & buildAnalyzer,
|
||
|
|
std::shared_ptr<CCallback> cc
|
||
|
|
)
|
||
|
|
{
|
||
|
|
constexpr int EMPTY = -1;
|
||
|
|
int mostWanted = EMPTY;
|
||
|
|
TResource mostWantedScoreNeg = std::numeric_limits<TResource>::max();
|
||
|
|
int mostExpendable = EMPTY;
|
||
|
|
TResource mostExpendableAmountPos = 0;
|
||
|
|
|
||
|
|
// Find the most wanted resource
|
||
|
|
for(int i = 0; i < missingNow.size(); ++i)
|
||
|
|
{
|
||
|
|
if(missingNow[i] == 0)
|
||
|
|
continue;
|
||
|
|
|
||
|
|
const TResource score = income[i] - missingNow[i];
|
||
|
|
if(score < mostWantedScoreNeg)
|
||
|
|
{
|
||
|
|
mostWanted = i;
|
||
|
|
mostWantedScoreNeg = score;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Find the most expendable resource
|
||
|
|
for(int i = 0; i < missingNow.size(); ++i)
|
||
|
|
{
|
||
|
|
const TResource amountToSell = freeAfterMissingTotal[i];
|
||
|
|
if(amountToSell == 0)
|
||
|
|
continue;
|
||
|
|
|
||
|
|
bool okToSell = false;
|
||
|
|
if(i == GameResID::GOLD)
|
||
|
|
{
|
||
|
|
// TODO: Mircea: Check if we should negate isGoldPressureOverMax() instead
|
||
|
|
if(income[GameResID::GOLD] > 0 && !buildAnalyzer->isGoldPressureOverMax())
|
||
|
|
okToSell = true;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
okToSell = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
if(amountToSell > mostExpendableAmountPos && okToSell)
|
||
|
|
{
|
||
|
|
mostExpendable = i;
|
||
|
|
mostExpendableAmountPos = amountToSell;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#if NK2AI_TRACE_LEVEL >= 2
|
||
|
|
logAi->trace(
|
||
|
|
"ResourceTrader: mostWanted: %d, mostWantedScoreNeg %d, mostExpendable: %d, mostExpendableAmountPos %d",
|
||
|
|
mostWanted,
|
||
|
|
mostWantedScoreNeg,
|
||
|
|
mostExpendable,
|
||
|
|
mostExpendableAmountPos
|
||
|
|
);
|
||
|
|
#endif
|
||
|
|
|
||
|
|
if(mostExpendable == mostWanted || mostWanted == EMPTY || mostExpendable == EMPTY)
|
||
|
|
return false;
|
||
|
|
|
||
|
|
int givenPerUnit;
|
||
|
|
int receivedPerUnit;
|
||
|
|
market->getOffer(mostExpendable, mostWanted, givenPerUnit, receivedPerUnit, EMarketMode::RESOURCE_RESOURCE);
|
||
|
|
if(!givenPerUnit || !receivedPerUnit)
|
||
|
|
{
|
||
|
|
logGlobal->error(
|
||
|
|
"ResourceTrader: No offer for %d of %d, given %d, received %d. Should never happen",
|
||
|
|
mostExpendable,
|
||
|
|
mostWanted,
|
||
|
|
givenPerUnit,
|
||
|
|
receivedPerUnit
|
||
|
|
);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// TODO: Mircea: if 15 wood and 14 gems, gems can be used a lot more for buying other things
|
||
|
|
if(givenPerUnit > mostExpendableAmountPos)
|
||
|
|
return false;
|
||
|
|
|
||
|
|
TResource multiplier = std::min(
|
||
|
|
static_cast<int>(mostExpendableAmountPos * EXPENDABLE_BULK_RATIO / givenPerUnit),
|
||
|
|
missingNow[mostWanted] / receivedPerUnit
|
||
|
|
); // for gold we have to / receivedUnits, because 1 ore gives many gold units
|
||
|
|
if(multiplier == 0) // could happen for very small values due to EXPENDABLE_BULK_RATIO
|
||
|
|
multiplier = 1;
|
||
|
|
|
||
|
|
const TResource givenMultiplied = givenPerUnit * multiplier;
|
||
|
|
if(givenMultiplied > freeAfterMissingTotal[mostExpendable])
|
||
|
|
{
|
||
|
|
logGlobal->error("ResourceTrader: Something went wrong with the multiplier %d", multiplier);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
cc->trade(market->getObjInstanceID(), EMarketMode::RESOURCE_RESOURCE, GameResID(mostExpendable), GameResID(mostWanted), givenMultiplied);
|
||
|
|
#if NK2AI_TRACE_LEVEL >= 2
|
||
|
|
logAi->info("ResourceTrader: Traded %d of %s for %d receivedPerUnit of %s", givenMultiplied, mostExpendable, receivedPerUnit, mostWanted);
|
||
|
|
#endif
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|