ResourceTrader.cpp 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. /*
  2. * ResourceTrader.cpp, part of VCMI engine
  3. *
  4. * Authors: listed in file AUTHORS in main folder
  5. *
  6. * License: GNU General Public License v2.0 or later
  7. * Full text of license available in license.txt file, in main folder
  8. */
  9. #include "StdInc.h"
  10. #include "ResourceTrader.h"
  11. namespace NK2AI
  12. {
  13. bool ResourceTrader::trade(const std::unique_ptr<BuildAnalyzer> & buildAnalyzer, std::shared_ptr<CCallback> cc, TResources freeResources)
  14. {
  15. // TODO: Mircea: Maybe include based on how close danger is: X as default + proportion of close danger or something around that
  16. constexpr float ARMY_GOLD_RATIO_PER_MAKE_TURN_PASS = 0.1f;
  17. constexpr float EXPENDABLE_BULK_RATIO = 0.3f;
  18. bool haveTraded = false;
  19. ObjectInstanceID marketId;
  20. // TODO: Mircea: What about outside town markets that have better rates than a single town for example?
  21. // Are those used anywhere? To inspect.
  22. for (const auto * const town : cc->getTownsInfo())
  23. {
  24. if (town->hasBuiltSomeTradeBuilding())
  25. {
  26. marketId = town->id;
  27. break;
  28. }
  29. }
  30. if (!marketId.hasValue())
  31. return false;
  32. const CGObjectInstance * obj = cc->getObj(marketId, false);
  33. assert(obj);
  34. // if (!obj)
  35. // return false;
  36. const auto * market = dynamic_cast<const IMarket *>(obj);
  37. assert(market);
  38. // if (!market)
  39. // return false;
  40. bool shouldTryToTrade = true;
  41. while(shouldTryToTrade)
  42. {
  43. shouldTryToTrade = false;
  44. buildAnalyzer->update();
  45. // if we favor getResourcesRequiredNow is better on short term, if we favor getTotalResourcesRequired is better on long term
  46. TResources missingNow = buildAnalyzer->getMissingResourcesNow(ARMY_GOLD_RATIO_PER_MAKE_TURN_PASS);
  47. if(missingNow.empty())
  48. break;
  49. TResources income = buildAnalyzer->getDailyIncome();
  50. // We don't want to sell something that's necessary later on, though that could make short term a bit harder sometimes
  51. TResources freeAfterMissingTotal = buildAnalyzer->getFreeResourcesAfterMissingTotal(ARMY_GOLD_RATIO_PER_MAKE_TURN_PASS);
  52. #if NK2AI_TRACE_LEVEL >= 2
  53. logAi->info("ResourceTrader: Free %s. FreeAfterMissingTotal %s. MissingNow %s", freeResources.toString(), freeAfterMissingTotal.toString(), missingNow.toString());
  54. #endif
  55. if(ResourceTrader::tradeHelper(EXPENDABLE_BULK_RATIO, market, missingNow, income, freeAfterMissingTotal, buildAnalyzer, cc))
  56. {
  57. haveTraded = true;
  58. shouldTryToTrade = true;
  59. }
  60. }
  61. return haveTraded;
  62. }
  63. bool ResourceTrader::tradeHelper(
  64. float EXPENDABLE_BULK_RATIO,
  65. const IMarket * market,
  66. TResources missingNow,
  67. TResources income,
  68. TResources freeAfterMissingTotal,
  69. const std::unique_ptr<BuildAnalyzer> & buildAnalyzer,
  70. std::shared_ptr<CCallback> cc
  71. )
  72. {
  73. constexpr int EMPTY = -1;
  74. int mostWanted = EMPTY;
  75. TResource mostWantedScoreNeg = std::numeric_limits<TResource>::max();
  76. int mostExpendable = EMPTY;
  77. TResource mostExpendableAmountPos = 0;
  78. // Find the most wanted resource
  79. for(int i = 0; i < missingNow.size(); ++i)
  80. {
  81. if(missingNow[i] == 0)
  82. continue;
  83. const TResource score = income[i] - missingNow[i];
  84. if(score < mostWantedScoreNeg)
  85. {
  86. mostWanted = i;
  87. mostWantedScoreNeg = score;
  88. }
  89. }
  90. // Find the most expendable resource
  91. for(int i = 0; i < missingNow.size(); ++i)
  92. {
  93. const TResource amountToSell = freeAfterMissingTotal[i];
  94. if(amountToSell == 0)
  95. continue;
  96. bool okToSell = false;
  97. if(i == GameResID::GOLD)
  98. {
  99. // TODO: Mircea: Check if we should negate isGoldPressureOverMax() instead
  100. if(income[GameResID::GOLD] > 0 && !buildAnalyzer->isGoldPressureOverMax())
  101. okToSell = true;
  102. }
  103. else
  104. {
  105. okToSell = true;
  106. }
  107. if(amountToSell > mostExpendableAmountPos && okToSell)
  108. {
  109. mostExpendable = i;
  110. mostExpendableAmountPos = amountToSell;
  111. }
  112. }
  113. #if NK2AI_TRACE_LEVEL >= 2
  114. logAi->trace(
  115. "ResourceTrader: mostWanted: %d, mostWantedScoreNeg %d, mostExpendable: %d, mostExpendableAmountPos %d",
  116. mostWanted,
  117. mostWantedScoreNeg,
  118. mostExpendable,
  119. mostExpendableAmountPos
  120. );
  121. #endif
  122. if(mostExpendable == mostWanted || mostWanted == EMPTY || mostExpendable == EMPTY)
  123. return false;
  124. int givenPerUnit;
  125. int receivedPerUnit;
  126. market->getOffer(mostExpendable, mostWanted, givenPerUnit, receivedPerUnit, EMarketMode::RESOURCE_RESOURCE);
  127. if(!givenPerUnit || !receivedPerUnit)
  128. {
  129. logGlobal->error(
  130. "ResourceTrader: No offer for %d of %d, given %d, received %d. Should never happen",
  131. mostExpendable,
  132. mostWanted,
  133. givenPerUnit,
  134. receivedPerUnit
  135. );
  136. return false;
  137. }
  138. // TODO: Mircea: if 15 wood and 14 gems, gems can be used a lot more for buying other things
  139. if(givenPerUnit > mostExpendableAmountPos)
  140. return false;
  141. TResource multiplier = std::min(
  142. static_cast<int>(mostExpendableAmountPos * EXPENDABLE_BULK_RATIO / givenPerUnit),
  143. missingNow[mostWanted] / receivedPerUnit
  144. ); // for gold we have to / receivedUnits, because 1 ore gives many gold units
  145. if(multiplier == 0) // could happen for very small values due to EXPENDABLE_BULK_RATIO
  146. multiplier = 1;
  147. const TResource givenMultiplied = givenPerUnit * multiplier;
  148. if(givenMultiplied > freeAfterMissingTotal[mostExpendable])
  149. {
  150. logGlobal->error("ResourceTrader: Something went wrong with the multiplier %d", multiplier);
  151. return false;
  152. }
  153. cc->trade(market->getObjInstanceID(), EMarketMode::RESOURCE_RESOURCE, GameResID(mostExpendable), GameResID(mostWanted), givenMultiplied);
  154. #if NK2AI_TRACE_LEVEL >= 2
  155. logAi->info("ResourceTrader: Traded %d of %s for %d receivedPerUnit of %s", givenMultiplied, mostExpendable, receivedPerUnit, mostWanted);
  156. #endif
  157. return true;
  158. }
  159. }