/* * 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, std::shared_ptr 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(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, std::shared_ptr cc ) { 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) { 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(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; } }