1
0
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:
Mircea TheHonestCTO
2025-09-07 15:08:20 +02:00
parent 9df90a25ba
commit 6f33bfe302
7 changed files with 181 additions and 103 deletions

View File

@@ -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;
}
}

View File

@@ -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);
};
}

View File

@@ -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();

View File

@@ -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;
}

View File

@@ -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)

View File

@@ -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));
}

View File

@@ -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: