diff --git a/AI/Nullkiller2/Analyzers/BuildAnalyzer.cpp b/AI/Nullkiller2/Analyzers/BuildAnalyzer.cpp index f1df65d3b..afa8fda49 100644 --- a/AI/Nullkiller2/Analyzers/BuildAnalyzer.cpp +++ b/AI/Nullkiller2/Analyzers/BuildAnalyzer.cpp @@ -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; } } diff --git a/AI/Nullkiller2/Analyzers/BuildAnalyzer.h b/AI/Nullkiller2/Analyzers/BuildAnalyzer.h index 97847d9cc..3e5351775 100644 --- a/AI/Nullkiller2/Analyzers/BuildAnalyzer.h +++ b/AI/Nullkiller2/Analyzers/BuildAnalyzer.h @@ -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 & getDevelopmentInfo() const { return developmentInfos; } TResources getDailyIncome() const { return dailyIncome; } float getGoldPressure() const { return goldPressure; } @@ -102,8 +103,9 @@ public: std::unique_ptr & armyManager, std::shared_ptr & 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); }; } diff --git a/AI/Nullkiller2/Behaviors/BuildingBehavior.cpp b/AI/Nullkiller2/Behaviors/BuildingBehavior.cpp index 4b6030b05..61680b3c3 100644 --- a/AI/Nullkiller2/Behaviors/BuildingBehavior.cpp +++ b/AI/Nullkiller2/Behaviors/BuildingBehavior.cpp @@ -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(); diff --git a/AI/Nullkiller2/Engine/Nullkiller.cpp b/AI/Nullkiller2/Engine/Nullkiller.cpp index 7da5a4fa4..b37f8b621 100644 --- a/AI/Nullkiller2/Engine/Nullkiller.cpp +++ b/AI/Nullkiller2/Engine/Nullkiller.cpp @@ -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(obj); + assert(market); + // if (!market) + // return false; + + bool shouldTryToTrade = true; + while(shouldTryToTrade) { - if (const auto* m = dynamic_cast(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::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::max(); - float maxRatio = std::numeric_limits::min(); - - for (int i = 0; i < required.size(); ++i) - { - if (required[i] <= 0) - continue; - float ratio = static_cast(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(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(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; } diff --git a/AI/Nullkiller2/Engine/PriorityEvaluator.cpp b/AI/Nullkiller2/Engine/PriorityEvaluator.cpp index c1ed96598..232917186 100644 --- a/AI/Nullkiller2/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller2/Engine/PriorityEvaluator.cpp @@ -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) diff --git a/AI/Nullkiller2/Goals/SaveResources.cpp b/AI/Nullkiller2/Goals/SaveResources.cpp index ac8a0a25a..5c94ef115 100644 --- a/AI/Nullkiller2/Goals/SaveResources.cpp +++ b/AI/Nullkiller2/Goals/SaveResources.cpp @@ -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)); } diff --git a/AI/Nullkiller2/Goals/SaveResources.h b/AI/Nullkiller2/Goals/SaveResources.h index 6e25fd90b..a479e8a7d 100644 --- a/AI/Nullkiller2/Goals/SaveResources.h +++ b/AI/Nullkiller2/Goals/SaveResources.h @@ -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 { private: