mirror of
https://github.com/vcmi/vcmi.git
synced 2025-11-27 22:49:25 +02:00
feature: improve trading logic within makeTurn pass to avoid thousands of microtransactions; to focus on now's requirements but with long term in mind; to get some gold for the army as well (compared to 0 as before)
This commit is contained in:
@@ -18,16 +18,29 @@
|
||||
namespace NK2AI
|
||||
{
|
||||
|
||||
TResources BuildAnalyzer::getResourcesRequiredNow() const
|
||||
TResources BuildAnalyzer::getMissingResourcesNow(const float armyGoldRatio) const
|
||||
{
|
||||
auto result = withoutGold(armyCost) + requiredResources - aiNk->getFreeResources();
|
||||
auto armyGold = goldOnly(armyCost);
|
||||
armyGold[GameResID::GOLD] = armyGoldRatio * armyGold[GameResID::GOLD];
|
||||
auto result = requiredResources + goldRemove(armyCost) + armyGold - aiNk->getFreeResources();
|
||||
result.positive();
|
||||
return result;
|
||||
}
|
||||
|
||||
TResources BuildAnalyzer::getTotalResourcesRequired() const
|
||||
TResources BuildAnalyzer::getMissingResourcesInTotal(const float armyGoldRatio) const
|
||||
{
|
||||
auto result = totalDevelopmentCost + withoutGold(armyCost) - aiNk->getFreeResources();
|
||||
auto armyGold = goldOnly(armyCost);
|
||||
armyGold[GameResID::GOLD] = armyGoldRatio * armyGold[GameResID::GOLD];
|
||||
auto result = totalDevelopmentCost + goldRemove(armyCost) + armyGold - aiNk->getFreeResources();
|
||||
result.positive();
|
||||
return result;
|
||||
}
|
||||
|
||||
TResources BuildAnalyzer::getFreeResourcesAfterMissingTotal(const float armyGoldRatio) const
|
||||
{
|
||||
auto armyGold = goldOnly(armyCost);
|
||||
armyGold[GameResID::GOLD] = armyGoldRatio * armyGold[GameResID::GOLD];
|
||||
auto result = aiNk->getFreeResources() - totalDevelopmentCost - goldRemove(armyCost) - armyGold;
|
||||
result.positive();
|
||||
return result;
|
||||
}
|
||||
@@ -77,8 +90,8 @@ void BuildAnalyzer::update()
|
||||
|
||||
boost::range::sort(developmentInfos, [](const TownDevelopmentInfo & tdi1, const TownDevelopmentInfo & tdi2) -> bool
|
||||
{
|
||||
auto val1 = approximateInGold(tdi1.armyCost) - approximateInGold(tdi1.townDevelopmentCost);
|
||||
auto val2 = approximateInGold(tdi2.armyCost) - approximateInGold(tdi2.townDevelopmentCost);
|
||||
auto val1 = goldApproximate(tdi1.armyCost) - goldApproximate(tdi1.townDevelopmentCost);
|
||||
auto val2 = goldApproximate(tdi2.armyCost) - goldApproximate(tdi2.townDevelopmentCost);
|
||||
return val1 > val2;
|
||||
});
|
||||
|
||||
@@ -119,7 +132,7 @@ void TownDevelopmentInfo::addBuildingBuilt(const BuildingInfo & bi)
|
||||
void TownDevelopmentInfo::addBuildingToBuild(const BuildingInfo & bi)
|
||||
{
|
||||
townDevelopmentCost += bi.buildCostWithPrerequisites;
|
||||
townDevelopmentCost += BuildAnalyzer::withoutGold(bi.armyCost);
|
||||
townDevelopmentCost += BuildAnalyzer::goldRemove(bi.armyCost);
|
||||
|
||||
if (bi.isBuildable)
|
||||
{
|
||||
@@ -434,19 +447,29 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
|
||||
return info;
|
||||
}
|
||||
|
||||
int32_t BuildAnalyzer::approximateInGold(const TResources & res)
|
||||
TResource BuildAnalyzer::goldApproximate(const TResources & res)
|
||||
{
|
||||
// TODO: Would it make sense to use the marketplace rate of the player?
|
||||
// TODO: Would it make sense to use the marketplace rate of the player? See Nullkiller::handleTrading()
|
||||
return res[EGameResID::GOLD]
|
||||
+ 75 * (res[EGameResID::WOOD] + res[EGameResID::ORE])
|
||||
+ 125 * (res[EGameResID::GEMS] + res[EGameResID::CRYSTAL] + res[EGameResID::MERCURY] + res[EGameResID::SULFUR]);
|
||||
}
|
||||
|
||||
TResources BuildAnalyzer::withoutGold(TResources other)
|
||||
TResources BuildAnalyzer::goldRemove(TResources other)
|
||||
{
|
||||
// TODO: Mircea: Potential issue modifying the input directly? To inspect
|
||||
other[GameResID::GOLD] = 0;
|
||||
return other;
|
||||
TResources copy;
|
||||
for(int i = 0; i < GameResID::COUNT; ++i)
|
||||
copy[i] = other[i];
|
||||
|
||||
copy[GameResID::GOLD] = 0;
|
||||
return copy;
|
||||
}
|
||||
|
||||
TResources BuildAnalyzer::goldOnly(TResources other)
|
||||
{
|
||||
TResources copy;
|
||||
copy[GameResID::GOLD] = other[GameResID::GOLD];
|
||||
return copy;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -82,8 +82,9 @@ public:
|
||||
explicit BuildAnalyzer(Nullkiller * aiNk) : aiNk(aiNk) {}
|
||||
void update();
|
||||
|
||||
TResources getResourcesRequiredNow() const;
|
||||
TResources getTotalResourcesRequired() const;
|
||||
TResources getMissingResourcesNow(float armyGoldRatio = 0) const;
|
||||
TResources getMissingResourcesInTotal(float armyGoldRatio = 0) const;
|
||||
TResources getFreeResourcesAfterMissingTotal(float armyGoldRatio = 0) const;
|
||||
const std::vector<TownDevelopmentInfo> & getDevelopmentInfo() const { return developmentInfos; }
|
||||
TResources getDailyIncome() const { return dailyIncome; }
|
||||
float getGoldPressure() const { return goldPressure; }
|
||||
@@ -102,8 +103,9 @@ public:
|
||||
std::unique_ptr<ArmyManager> & armyManager,
|
||||
std::shared_ptr<CCallback> & cc,
|
||||
bool excludeDwellingDependencies = true);
|
||||
static int32_t approximateInGold(const TResources & res);
|
||||
static TResources withoutGold(TResources other);
|
||||
static TResource goldApproximate(const TResources & res);
|
||||
static TResources goldRemove(TResources other);
|
||||
static TResources goldOnly(TResources other);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -31,8 +31,8 @@ Goals::TGoalVec BuildingBehavior::decompose(const Nullkiller * aiNk) const
|
||||
{
|
||||
Goals::TGoalVec tasks;
|
||||
|
||||
TResources resourcesRequired = aiNk->buildAnalyzer->getResourcesRequiredNow();
|
||||
TResources totalDevelopmentCost = aiNk->buildAnalyzer->getTotalResourcesRequired();
|
||||
TResources resourcesRequired = aiNk->buildAnalyzer->getMissingResourcesNow();
|
||||
TResources totalDevelopmentCost = aiNk->buildAnalyzer->getMissingResourcesInTotal();
|
||||
TResources availableResources = aiNk->getFreeResources();
|
||||
TResources dailyIncome = aiNk->buildAnalyzer->getDailyIncome();
|
||||
|
||||
|
||||
@@ -597,9 +597,7 @@ bool Nullkiller::executeTask(const Goals::TTask & task) const
|
||||
TResources Nullkiller::getFreeResources() const
|
||||
{
|
||||
auto freeRes = cc->getResourceAmount() - lockedResources;
|
||||
|
||||
freeRes.positive();
|
||||
|
||||
return freeRes;
|
||||
}
|
||||
|
||||
@@ -610,98 +608,154 @@ void Nullkiller::lockResources(const TResources & res)
|
||||
|
||||
bool Nullkiller::handleTrading()
|
||||
{
|
||||
// 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.25f;
|
||||
bool haveTraded = false;
|
||||
bool shouldTryToTrade = true;
|
||||
ObjectInstanceID marketId;
|
||||
for (const auto town : cc->getTownsInfo())
|
||||
|
||||
// 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;
|
||||
if (const CGObjectInstance* obj = cc->getObj(marketId, 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)
|
||||
{
|
||||
if (const auto* m = dynamic_cast<const IMarket*>(obj))
|
||||
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("Nullkiller::handleTrading Free %s. FreeAfterMissingTotal %s. Required %s", getFreeResources().toString(), freeAfterMissingTotal.toString(), missingNow.toString());
|
||||
#endif
|
||||
|
||||
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)
|
||||
{
|
||||
while (shouldTryToTrade)
|
||||
if(missingNow[i] == 0)
|
||||
continue;
|
||||
|
||||
const TResource score = income[i] - missingNow[i];
|
||||
if(score < mostWantedScoreNeg)
|
||||
{
|
||||
shouldTryToTrade = false;
|
||||
buildAnalyzer->update();
|
||||
TResources required = buildAnalyzer->getTotalResourcesRequired();
|
||||
TResources income = buildAnalyzer->getDailyIncome();
|
||||
TResources available = cc->getResourceAmount();
|
||||
#if NK2AI_TRACE_LEVEL >= 2
|
||||
logAi->debug("Available %s", available.toString());
|
||||
logAi->debug("Required %s", required.toString());
|
||||
#endif
|
||||
int mostWanted = -1;
|
||||
int mostExpendable = -1;
|
||||
float minRatio = std::numeric_limits<float>::max();
|
||||
float maxRatio = std::numeric_limits<float>::min();
|
||||
|
||||
for (int i = 0; i < required.size(); ++i)
|
||||
{
|
||||
if (required[i] <= 0)
|
||||
continue;
|
||||
float ratio = static_cast<float>(available[i]) / required[i];
|
||||
|
||||
if (ratio < minRatio) {
|
||||
minRatio = ratio;
|
||||
mostWanted = i;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < required.size(); ++i)
|
||||
{
|
||||
float ratio;
|
||||
if (required[i] > 0)
|
||||
ratio = static_cast<float>(available[i]) / required[i];
|
||||
else
|
||||
ratio = available[i];
|
||||
|
||||
bool okToSell = false;
|
||||
|
||||
if (i == GameResID::GOLD)
|
||||
{
|
||||
if (income[i] > 0 && !buildAnalyzer->isGoldPressureOverMax())
|
||||
okToSell = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (required[i] <= 0 && income[i] > 0)
|
||||
okToSell = true;
|
||||
}
|
||||
|
||||
if (ratio > maxRatio && okToSell) {
|
||||
maxRatio = ratio;
|
||||
mostExpendable = i;
|
||||
}
|
||||
}
|
||||
#if NK2AI_TRACE_LEVEL >= 2
|
||||
logAi->debug("mostExpendable: %d mostWanted: %d", mostExpendable, mostWanted);
|
||||
#endif
|
||||
if (mostExpendable == mostWanted || mostWanted == -1 || mostExpendable == -1)
|
||||
return false;
|
||||
|
||||
int toGive;
|
||||
int toGet;
|
||||
m->getOffer(mostExpendable, mostWanted, toGive, toGet, EMarketMode::RESOURCE_RESOURCE);
|
||||
//logAi->info("Offer is: I get %d of %s for %d of %s at %s", toGet, mostWanted, toGive, mostExpendable, obj->getObjectName());
|
||||
//TODO trade only as much as needed
|
||||
if (toGive && toGive <= available[mostExpendable]) //don't try to sell 0 resources
|
||||
{
|
||||
cc->trade(m->getObjInstanceID(), EMarketMode::RESOURCE_RESOURCE, GameResID(mostExpendable), GameResID(mostWanted), toGive);
|
||||
#if NK2AI_TRACE_LEVEL >= 2
|
||||
logAi->info("Traded %d of %s for %d of %s at %s", toGive, mostExpendable, toGet, mostWanted, obj->getObjectName());
|
||||
#endif
|
||||
haveTraded = true;
|
||||
shouldTryToTrade = true;
|
||||
}
|
||||
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(
|
||||
"Nullkiller::handleTrading mostWanted: %d, mostWantedScoreNeg %d, mostExpendable: %d, mostExpendableAmountPos %d",
|
||||
mostWanted,
|
||||
mostWantedScoreNeg,
|
||||
mostExpendable,
|
||||
mostExpendableAmountPos
|
||||
);
|
||||
#endif
|
||||
|
||||
if(mostExpendable == mostWanted || mostWanted == EMPTY || mostExpendable == EMPTY)
|
||||
break;
|
||||
|
||||
int givenPerUnit;
|
||||
int receivedUnits;
|
||||
market->getOffer(mostExpendable, mostWanted, givenPerUnit, receivedUnits, EMarketMode::RESOURCE_RESOURCE);
|
||||
if (!givenPerUnit || !receivedUnits)
|
||||
{
|
||||
logGlobal->error(
|
||||
"Nullkiller::handleTrading No offer for %d of %d, given %d, received %d. Should never happen",
|
||||
mostExpendable,
|
||||
mostWanted,
|
||||
givenPerUnit,
|
||||
receivedUnits
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
// TODO: Mircea: if 15 wood and 14 gems, gems can be used a lot more for buying other things
|
||||
if (givenPerUnit > mostExpendableAmountPos)
|
||||
break;
|
||||
|
||||
TResource multiplier = std::min(static_cast<int>(mostExpendableAmountPos * EXPENDABLE_BULK_RATIO / givenPerUnit),
|
||||
missingNow[mostWanted] / receivedUnits); // 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(
|
||||
"Nullkiller::handleTrading Something went wrong with the multiplier %d",
|
||||
multiplier
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
cc->trade(market->getObjInstanceID(), EMarketMode::RESOURCE_RESOURCE, GameResID(mostExpendable), GameResID(mostWanted), givenMultiplied);
|
||||
#if NK2AI_TRACE_LEVEL >= 2
|
||||
logAi->info("Nullkiller::handleTrading Traded %d of %s for %d of %s at %s", givenMultiplied, mostExpendable, receivedUnits, mostWanted, obj->getObjectName());
|
||||
#endif
|
||||
haveTraded = true;
|
||||
shouldTryToTrade = true;
|
||||
}
|
||||
return haveTraded;
|
||||
}
|
||||
|
||||
@@ -380,7 +380,7 @@ float RewardEvaluator::getEnemyHeroStrategicalValue(const CGHeroInstance * enemy
|
||||
/// @return between 0-1.0f
|
||||
float RewardEvaluator::getNowResourceRequirementStrength(GameResID resType) const
|
||||
{
|
||||
TResources requiredResources = aiNk->buildAnalyzer->getResourcesRequiredNow();
|
||||
TResources requiredResources = aiNk->buildAnalyzer->getMissingResourcesNow();
|
||||
TResources dailyIncome = aiNk->buildAnalyzer->getDailyIncome();
|
||||
|
||||
if(requiredResources[resType] == 0)
|
||||
@@ -395,7 +395,7 @@ float RewardEvaluator::getNowResourceRequirementStrength(GameResID resType) cons
|
||||
/// @return between 0-1.0f
|
||||
float RewardEvaluator::getTotalResourceRequirementStrength(GameResID resType) const
|
||||
{
|
||||
TResources requiredResources = aiNk->buildAnalyzer->getTotalResourcesRequired();
|
||||
TResources requiredResources = aiNk->buildAnalyzer->getMissingResourcesInTotal();
|
||||
TResources dailyIncome = aiNk->buildAnalyzer->getDailyIncome();
|
||||
|
||||
if(requiredResources[resType] == 0)
|
||||
|
||||
@@ -25,9 +25,7 @@ bool SaveResources::operator==(const SaveResources & other) const
|
||||
void SaveResources::accept(AIGateway * aiGw)
|
||||
{
|
||||
aiGw->nullkiller->lockResources(resources);
|
||||
|
||||
logAi->debug("Locked %s resources", resources.toString());
|
||||
|
||||
logAi->debug("Locked resources %s", resources.toString());
|
||||
throw goalFulfilledException(sptr(*this));
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace NK2AI
|
||||
{
|
||||
namespace Goals
|
||||
{
|
||||
// TODO: Mircea: Inspect if it's really in use. See aiNk->getLockedResources()
|
||||
class DLL_EXPORT SaveResources : public ElementarGoal<SaveResources>
|
||||
{
|
||||
private:
|
||||
|
||||
Reference in New Issue
Block a user