PriorityEvaluator.cpp 62 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776
  1. /*
  2. * PriorityEvaluator.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. */
  10. #include "StdInc.h"
  11. #include <limits>
  12. #include "Nullkiller.h"
  13. #include "../../../lib/entities/artifact/CArtifact.h"
  14. #include "../../../lib/entities/ResourceTypeHandler.h"
  15. #include "../../../lib/mapObjects/CGResource.h"
  16. #include "../../../lib/mapping/TerrainTile.h"
  17. #include "../../../lib/CPlayerState.h"
  18. #include "../../../lib/RoadHandler.h"
  19. #include "../../../lib/CCreatureHandler.h"
  20. #include "../../../lib/GameLibrary.h"
  21. #include "../../../lib/StartInfo.h"
  22. #include "../../../lib/GameSettings.h"
  23. #include "../../../lib/filesystem/Filesystem.h"
  24. #include "../Goals/ExecuteHeroChain.h"
  25. #include "../Goals/BuildThis.h"
  26. #include "../Goals/StayAtTown.h"
  27. #include "../Goals/ExchangeSwapTownHeroes.h"
  28. #include "../Goals/DismissHero.h"
  29. #include "../Markers/UnlockCluster.h"
  30. #include "../Markers/HeroExchange.h"
  31. #include "../Markers/ArmyUpgrade.h"
  32. #include "../Markers/DefendTown.h"
  33. namespace NK2AI
  34. {
  35. constexpr float MAX_CRITICAL_VALUE = 2.0f;
  36. EvaluationContext::EvaluationContext(const Nullkiller* aiNk)
  37. : movementCost(0.0),
  38. manaCost(0),
  39. danger(0),
  40. closestWayRatio(1),
  41. movementCostByRole(),
  42. skillReward(0),
  43. goldReward(0),
  44. goldCost(0),
  45. armyReward(0),
  46. armyLossRatio(0),
  47. heroRole(HeroRole::SCOUT),
  48. turn(0),
  49. strategicalValue(0),
  50. conquestValue(0),
  51. evaluator(aiNk),
  52. enemyHeroDangerRatio(0),
  53. threat(0),
  54. armyGrowth(0),
  55. armyInvolvement(0),
  56. defenseValue(0),
  57. isDefend(false),
  58. threatTurns(INT_MAX),
  59. involvesSailing(false),
  60. isTradeBuilding(false),
  61. isExchange(false),
  62. isArmyUpgrade(false),
  63. isHero(false),
  64. isEnemy(false),
  65. explorePriority(0),
  66. powerRatio(0)
  67. {
  68. }
  69. void EvaluationContext::addNonCriticalStrategicalValue(float value)
  70. {
  71. vstd::amax(strategicalValue, std::min(value, MAX_CRITICAL_VALUE));
  72. }
  73. PriorityEvaluator::~PriorityEvaluator()
  74. {
  75. delete engine;
  76. }
  77. void PriorityEvaluator::initVisitTile()
  78. {
  79. auto file = CResourceHandler::get()->load(ResourcePath("config/ai/nk2ai/object-priorities.txt"))->readAll();
  80. std::string str = std::string((char *)file.first.get(), file.second);
  81. engine = fl::FllImporter().fromString(str);
  82. armyLossRatioVariable = engine->getInputVariable("armyLoss");
  83. armyGrowthVariable = engine->getInputVariable("armyGrowth");
  84. heroRoleVariable = engine->getInputVariable("heroRole");
  85. dangerVariable = engine->getInputVariable("danger");
  86. turnVariable = engine->getInputVariable("turn");
  87. mainTurnDistanceVariable = engine->getInputVariable("mainTurnDistance");
  88. scoutTurnDistanceVariable = engine->getInputVariable("scoutTurnDistance");
  89. goldRewardVsMovementVariable = engine->getInputVariable("goldReward");
  90. armyRewardVariable = engine->getInputVariable("armyReward");
  91. skillRewardVariable = engine->getInputVariable("skillReward");
  92. rewardTypeVariable = engine->getInputVariable("rewardType");
  93. closestHeroRatioVariable = engine->getInputVariable("closestHeroRatio");
  94. strategicalValueVariable = engine->getInputVariable("strategicalValue");
  95. goldPressureVariable = engine->getInputVariable("goldPressure");
  96. goldCostVariable = engine->getInputVariable("goldCost");
  97. fearVariable = engine->getInputVariable("fear");
  98. value = engine->getOutputVariable("Value");
  99. }
  100. bool isAnotherAi(const CGObjectInstance * obj, const CPlayerSpecificInfoCallback & cb)
  101. {
  102. return obj->getOwner().isValidPlayer()
  103. && cb.getStartInfo()->getIthPlayersSettings(obj->getOwner()).isControlledByAI();
  104. }
  105. int32_t estimateTownIncome(CCallback * cb, const CGObjectInstance * target, const CGHeroInstance * hero)
  106. {
  107. auto relations = cb->getPlayerRelations(hero->tempOwner, target->tempOwner);
  108. if(relations != PlayerRelations::ENEMIES)
  109. return 0; // if we already own it, no additional reward will be received by just visiting it
  110. auto booster = isAnotherAi(target, *cb) ? 1 : 2;
  111. auto town = cb->getTown(target->id);
  112. auto fortLevel = town->fortLevel();
  113. if(town->hasCapitol())
  114. return booster * 2000;
  115. // probably well developed town will have city hall
  116. if(fortLevel == CGTownInstance::CASTLE) return booster * 750;
  117. return booster * (town->hasFort() && town->tempOwner != PlayerColor::NEUTRAL ? booster * 500 : 250);
  118. }
  119. int32_t getResourcesGoldReward(const TResources & res)
  120. {
  121. int32_t result = 0;
  122. for(auto r : LIBRARY->resourceTypeHandler->getAllObjects())
  123. {
  124. if(res[r] > 0)
  125. result += r == EGameResID::GOLD ? res[r] : res[r] * 100;
  126. }
  127. return result;
  128. }
  129. uint64_t getDwellingArmyValue(CCallback * cb, const CGObjectInstance * target, bool checkGold)
  130. {
  131. auto dwelling = dynamic_cast<const CGDwelling *>(target);
  132. uint64_t score = 0;
  133. for(auto & creLevel : dwelling->creatures)
  134. {
  135. if(creLevel.first && creLevel.second.size())
  136. {
  137. auto creature = creLevel.second.back().toCreature();
  138. auto creaturesAreFree = creature->getLevel() == 1;
  139. if(!creaturesAreFree && checkGold && !cb->getResourceAmount().canAfford(creature->getFullRecruitCost() * creLevel.first))
  140. continue;
  141. score += creature->getAIValue() * creLevel.first;
  142. }
  143. }
  144. return score;
  145. }
  146. uint64_t getDwellingArmyGrowth(CCallback * cb, const CGObjectInstance * target, PlayerColor myColor)
  147. {
  148. auto dwelling = dynamic_cast<const CGDwelling *>(target);
  149. uint64_t score = 0;
  150. if(dwelling->getOwner() == myColor)
  151. return 0;
  152. for(auto & creLevel : dwelling->creatures)
  153. {
  154. if(creLevel.second.size())
  155. {
  156. auto creature = creLevel.second.back().toCreature();
  157. score += creature->getAIValue() * creature->getGrowth();
  158. // Increase priority towards the end of the week if units are lost afterwards
  159. if(!cb->getSettings().getBoolean(EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED))
  160. {
  161. const auto dayOfWeek = cb->getDate(Date::DAY_OF_WEEK);
  162. score *= dayOfWeek;
  163. }
  164. }
  165. }
  166. return score;
  167. }
  168. int getDwellingArmyCost(const CGObjectInstance * target)
  169. {
  170. auto dwelling = dynamic_cast<const CGDwelling *>(target);
  171. int cost = 0;
  172. for(auto & creLevel : dwelling->creatures)
  173. {
  174. if(creLevel.first && creLevel.second.size())
  175. {
  176. auto creature = creLevel.second.back().toCreature();
  177. auto creaturesAreFree = creature->getLevel() == 1;
  178. if(!creaturesAreFree)
  179. cost += creature->getFullRecruitCost().marketValue() * creLevel.first;
  180. }
  181. }
  182. return cost;
  183. }
  184. static uint64_t evaluateSpellScrollArmyValue(const SpellID &)
  185. {
  186. return 1500;
  187. }
  188. static uint64_t evaluateArtifactArmyValue(const CArtifact * art)
  189. {
  190. if(art->getId() == ArtifactID::SPELL_SCROLL)
  191. return 1500;
  192. return getPotentialArtifactScore(art);
  193. }
  194. uint64_t RewardEvaluator::getArmyReward(
  195. const CGObjectInstance * target,
  196. const CGHeroInstance * hero,
  197. const CCreatureSet * army,
  198. bool checkGold) const
  199. {
  200. const float enemyArmyEliminationRewardRatio = 0.5f;
  201. auto relations = aiNk->cc->getPlayerRelations(target->tempOwner, aiNk->playerID);
  202. if(!target) // TODO: Mircea: Shouldn't it be relations instead of target in the if? See getArmyGrowth below
  203. return 0;
  204. switch(target->ID)
  205. {
  206. case Obj::HILL_FORT:
  207. return aiNk->armyManager->calculateCreaturesUpgrade(army, target, aiNk->cc->getResourceAmount()).upgradeValue;
  208. case Obj::CREATURE_GENERATOR1:
  209. case Obj::CREATURE_GENERATOR2:
  210. case Obj::CREATURE_GENERATOR3:
  211. case Obj::CREATURE_GENERATOR4:
  212. return getDwellingArmyValue(aiNk->cc.get(), target, checkGold);
  213. case Obj::SPELL_SCROLL:
  214. return evaluateSpellScrollArmyValue(dynamic_cast<const CGArtifact *>(target)->getArtifactInstance()->getScrollSpellID());
  215. case Obj::ARTIFACT:
  216. return evaluateArtifactArmyValue(dynamic_cast<const CGArtifact *>(target)->getArtifactInstance()->getType());
  217. case Obj::HERO:
  218. return relations == PlayerRelations::ENEMIES
  219. ? enemyArmyEliminationRewardRatio * dynamic_cast<const CGHeroInstance *>(target)->getArmyStrength()
  220. : 0;
  221. case Obj::PANDORAS_BOX:
  222. return 5000;
  223. case Obj::MAGIC_WELL:
  224. case Obj::MAGIC_SPRING:
  225. return getManaRecoveryArmyReward(hero);
  226. default:
  227. break;
  228. }
  229. auto rewardable = dynamic_cast<const Rewardable::Interface *>(target);
  230. if(rewardable)
  231. {
  232. auto totalValue = 0;
  233. for(int index : rewardable->getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT))
  234. {
  235. auto & info = rewardable->configuration.info[index];
  236. auto rewardValue = 0;
  237. for(auto artID : info.reward.grantedArtifacts)
  238. rewardValue += evaluateArtifactArmyValue(artID.toArtifact());
  239. for(auto scroll : info.reward.grantedScrolls)
  240. rewardValue += evaluateSpellScrollArmyValue(scroll);
  241. for(const auto & stackInfo : info.reward.creatures)
  242. rewardValue += stackInfo.getType()->getAIValue() * stackInfo.getCount();
  243. const auto combined_size = std::min(static_cast<size_t>(1),
  244. info.reward.grantedArtifacts.size() + info.reward.creatures.size() +
  245. info.reward.grantedScrolls.size());
  246. totalValue += rewardValue > 0 ? rewardValue / combined_size : 0;
  247. }
  248. return totalValue;
  249. }
  250. return 0;
  251. }
  252. uint64_t RewardEvaluator::getArmyGrowth(
  253. const CGObjectInstance * target,
  254. const CGHeroInstance * hero,
  255. const CCreatureSet * army) const
  256. {
  257. if(!target)
  258. return 0;
  259. auto relations = aiNk->cc->getPlayerRelations(target->tempOwner, hero->tempOwner);
  260. if(relations != PlayerRelations::ENEMIES)
  261. return 0;
  262. switch(target->ID)
  263. {
  264. case Obj::TOWN:
  265. {
  266. auto town = dynamic_cast<const CGTownInstance *>(target);
  267. auto fortLevel = town->fortLevel();
  268. auto neutral = !town->getOwner().isValidPlayer();
  269. auto booster = isAnotherAi(town, *aiNk->cc) || neutral ? 1 : 2;
  270. if(fortLevel < CGTownInstance::CITADEL)
  271. return town->hasFort() ? booster * 500 : 0;
  272. else
  273. return booster * (fortLevel == CGTownInstance::CASTLE ? 5000 : 2000);
  274. }
  275. case Obj::CREATURE_GENERATOR1:
  276. case Obj::CREATURE_GENERATOR2:
  277. case Obj::CREATURE_GENERATOR3:
  278. case Obj::CREATURE_GENERATOR4:
  279. return getDwellingArmyGrowth(aiNk->cc.get(), target, hero->getOwner());
  280. case Obj::ARTIFACT:
  281. // it is not supported now because hero will not sit in town on 7th day but later parts of legion may be counted as army growth as well.
  282. return 0;
  283. default:
  284. return 0;
  285. }
  286. }
  287. int RewardEvaluator::getGoldCost(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army) const
  288. {
  289. if(!target)
  290. return 0;
  291. if(auto * m = dynamic_cast<const IMarket *>(target))
  292. {
  293. if(m->allowsTrade(EMarketMode::RESOURCE_SKILL))
  294. return aiNk->cc->getSettings().getInteger(EGameSettings::MARKETS_UNIVERSITY_GOLD_COST);
  295. }
  296. switch(target->ID)
  297. {
  298. case Obj::HILL_FORT:
  299. return aiNk->armyManager->calculateCreaturesUpgrade(army, target, aiNk->cc->getResourceAmount()).upgradeCost[EGameResID::GOLD];
  300. case Obj::SCHOOL_OF_MAGIC:
  301. case Obj::SCHOOL_OF_WAR:
  302. return 1000;
  303. case Obj::CREATURE_GENERATOR1:
  304. case Obj::CREATURE_GENERATOR2:
  305. case Obj::CREATURE_GENERATOR3:
  306. case Obj::CREATURE_GENERATOR4:
  307. return getDwellingArmyCost(target);
  308. default:
  309. return 0;
  310. }
  311. }
  312. float RewardEvaluator::getEnemyHeroStrategicalValue(const CGHeroInstance * enemy) const
  313. {
  314. auto objectsUnderThreat = aiNk->dangerHitMap->getOneTurnAccessibleObjects(enemy);
  315. float objectValue = 0;
  316. for(auto obj : objectsUnderThreat)
  317. {
  318. vstd::amax(objectValue, getStrategicalValue(obj));
  319. }
  320. /*
  321. 1. If an enemy hero can attack nearby object, it's not useful to capture the object on our own.
  322. Killing the hero is almost as important (0.9) as capturing the object itself.
  323. 2. The formula quickly approaches 1.0 as hero level increases,
  324. but higher level always means higher value and the minimal value for level 1 hero is 0.5
  325. */
  326. return std::min(1.5f, objectValue * 0.9f + (1.5f - (1.5f / (1 + enemy->level))));
  327. }
  328. /// @return between 0-1.0f
  329. float RewardEvaluator::getNowResourceRequirementStrength(GameResID resType) const
  330. {
  331. TResources requiredResources = aiNk->buildAnalyzer->getMissingResourcesNow();
  332. TResources dailyIncome = aiNk->buildAnalyzer->getDailyIncome();
  333. if(requiredResources[resType] == 0)
  334. return 0;
  335. if(dailyIncome[resType] == 0)
  336. return 1.0f;
  337. return 0.8f;
  338. }
  339. /// @return between 0-1.0f
  340. float RewardEvaluator::getTotalResourceRequirementStrength(GameResID resType) const
  341. {
  342. TResources requiredResources = aiNk->buildAnalyzer->getMissingResourcesInTotal();
  343. TResources dailyIncome = aiNk->buildAnalyzer->getDailyIncome();
  344. if(requiredResources[resType] == 0)
  345. return 0;
  346. if(dailyIncome[resType] == 0)
  347. return 1.0f;
  348. return 0.8f;
  349. }
  350. uint64_t RewardEvaluator::townArmyGrowth(const CGTownInstance * town) const
  351. {
  352. uint64_t result = 0;
  353. for(auto creatureInfo : town->creatures)
  354. {
  355. if(creatureInfo.second.empty())
  356. continue;
  357. auto creature = creatureInfo.second.back().toCreature();
  358. result += creature->getAIValue() * town->getGrowthInfo(creature->getLevel() - 1).totalGrowth();
  359. }
  360. return result;
  361. }
  362. float RewardEvaluator::getManaRecoveryArmyReward(const CGHeroInstance * hero) const
  363. {
  364. return aiNk->heroManager->getMagicStrength(hero) * 10000 * (1.0f - std::sqrt(static_cast<float>(hero->mana) / hero->manaLimit()));
  365. }
  366. /// @return between 0-1.0f
  367. float RewardEvaluator::getCombinedResourceRequirementStrength(const TResources & res) const
  368. {
  369. float sum = 0.0f;
  370. for(TResources::nziterator it(res); it.valid(); it++)
  371. {
  372. auto calculation = 0.5f * getNowResourceRequirementStrength(it->resType)
  373. + 0.5f * getTotalResourceRequirementStrength(it->resType);
  374. // Even not required resources should be valuable because they shouldn't be left for the enemies to collect
  375. sum += std::min(MINIMUM_STRATEGICAL_VALUE_NON_TOWN, calculation);
  376. }
  377. return sum;
  378. }
  379. float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target, const CGHeroInstance * hero) const
  380. {
  381. if(!target)
  382. return 0;
  383. switch(target->ID)
  384. {
  385. case Obj::MINE:
  386. {
  387. auto mine = dynamic_cast<const CGMine *>(target);
  388. TResources res;
  389. res[mine->producedResource] = mine->producedQuantity;
  390. // Mines should have higher priority than resources
  391. return 1.0f + getCombinedResourceRequirementStrength(res);
  392. }
  393. case Obj::RESOURCE:
  394. {
  395. auto resource = dynamic_cast<const CGResource *>(target);
  396. TResources res;
  397. res[resource->resourceID()] = resource->getAmount();
  398. return getCombinedResourceRequirementStrength(res);
  399. }
  400. case Obj::TOWN:
  401. {
  402. if(aiNk->buildAnalyzer->getDevelopmentInfo().empty())
  403. return 10.0f;
  404. auto town = dynamic_cast<const CGTownInstance *>(target);
  405. if(town->getOwner() == aiNk->playerID)
  406. {
  407. auto armyIncome = townArmyGrowth(town);
  408. auto dailyIncome = town->dailyIncome()[EGameResID::GOLD];
  409. return std::min(1.0f, std::sqrt(armyIncome / 40000.0f)) + std::min(0.3f, dailyIncome / 10000.0f);
  410. }
  411. auto fortLevel = town->fortLevel();
  412. auto booster = isAnotherAi(town, *aiNk->cc) ? 0.4f : 1.0f;
  413. if(town->hasCapitol())
  414. return booster * 1.5;
  415. if(fortLevel < CGTownInstance::CITADEL)
  416. return booster * (town->hasFort() ? 1.0 : 0.8);
  417. return booster * (fortLevel == CGTownInstance::CASTLE ? 1.4 : 1.2);
  418. }
  419. case Obj::HERO:
  420. return aiNk->cc->getPlayerRelations(target->tempOwner, aiNk->playerID) == PlayerRelations::ENEMIES
  421. ? getEnemyHeroStrategicalValue(dynamic_cast<const CGHeroInstance *>(target))
  422. : 0;
  423. case Obj::KEYMASTER:
  424. return 0.6f;
  425. default:
  426. break;
  427. }
  428. auto rewardable = dynamic_cast<const Rewardable::Interface *>(target);
  429. if(rewardable && hero)
  430. {
  431. auto resourceReward = 0.0f;
  432. for(int index : rewardable->getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT))
  433. {
  434. resourceReward += getCombinedResourceRequirementStrength(rewardable->configuration.info[index].reward.resources);
  435. }
  436. return resourceReward;
  437. }
  438. return 0;
  439. }
  440. float RewardEvaluator::getConquestValue(const CGObjectInstance* target) const
  441. {
  442. if (!target)
  443. return 0;
  444. if (target->getOwner() == aiNk->playerID)
  445. return 0;
  446. switch (target->ID)
  447. {
  448. case Obj::TOWN:
  449. {
  450. if (aiNk->buildAnalyzer->getDevelopmentInfo().empty())
  451. return 10.0f;
  452. auto town = dynamic_cast<const CGTownInstance*>(target);
  453. if (town->getOwner() == aiNk->playerID)
  454. {
  455. auto armyIncome = townArmyGrowth(town);
  456. auto dailyIncome = town->dailyIncome()[EGameResID::GOLD];
  457. return std::min(1.0f, std::sqrt(armyIncome / 40000.0f)) + std::min(0.3f, dailyIncome / 10000.0f);
  458. }
  459. auto fortLevel = town->fortLevel();
  460. auto booster = 1.0f;
  461. if (town->hasCapitol())
  462. return booster * 1.5;
  463. if (fortLevel < CGTownInstance::CITADEL)
  464. return booster * (town->hasFort() ? 1.0 : 0.8);
  465. else
  466. return booster * (fortLevel == CGTownInstance::CASTLE ? 1.4 : 1.2);
  467. }
  468. case Obj::HERO:
  469. return aiNk->cc->getPlayerRelations(target->tempOwner, aiNk->playerID) == PlayerRelations::ENEMIES
  470. ? getEnemyHeroStrategicalValue(dynamic_cast<const CGHeroInstance*>(target))
  471. : 0;
  472. default:
  473. return 0;
  474. }
  475. }
  476. float RewardEvaluator::evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const
  477. {
  478. auto rewardable = dynamic_cast<const CRewardableObject *>(hut);
  479. assert(rewardable);
  480. auto skill = SecondarySkill(*rewardable->configuration.getVariable("secondarySkill", "gainedSkill"));
  481. // TODO: Mircea: Move to constants
  482. if(!hut->wasVisited(hero->tempOwner))
  483. return role == HeroRole::SCOUT ? 2 : 0;
  484. if(hero->getSecSkillLevel(skill) != MasteryLevel::NONE
  485. || static_cast<int>(hero->secSkills.size()) >= aiNk->cc->getSettings().getInteger(EGameSettings::HEROES_SKILL_PER_HERO))
  486. return 0;
  487. auto score = aiNk->heroManager->evaluateSecSkill(skill, hero);
  488. // TODO: Mircea: Move to constants
  489. return score >= 2 ? (role == HeroRole::MAIN ? 10 : 4) : score;
  490. }
  491. float RewardEvaluator::getSkillReward(const CGObjectInstance * target, const CGHeroInstance * hero, HeroRole role) const
  492. {
  493. const float enemyHeroEliminationSkillRewardRatio = 0.5f;
  494. if(!target)
  495. return 0;
  496. switch(target->ID)
  497. {
  498. case Obj::STAR_AXIS:
  499. case Obj::SCHOLAR:
  500. case Obj::SCHOOL_OF_MAGIC:
  501. case Obj::SCHOOL_OF_WAR:
  502. case Obj::GARDEN_OF_REVELATION:
  503. case Obj::MARLETTO_TOWER:
  504. case Obj::MERCENARY_CAMP:
  505. case Obj::TREE_OF_KNOWLEDGE:
  506. return 1;
  507. case Obj::LEARNING_STONE:
  508. return 1.0f / std::sqrt(hero->level);
  509. case Obj::ARENA:
  510. return 2;
  511. case Obj::SHRINE_OF_MAGIC_INCANTATION:
  512. return 0.25f;
  513. case Obj::SHRINE_OF_MAGIC_GESTURE:
  514. return 1.0f;
  515. case Obj::SHRINE_OF_MAGIC_THOUGHT:
  516. return 2.0f;
  517. case Obj::LIBRARY_OF_ENLIGHTENMENT:
  518. return 8;
  519. case Obj::WITCH_HUT:
  520. return evaluateWitchHutSkillScore(target, hero, role);
  521. case Obj::PANDORAS_BOX:
  522. //Can contains experience, spells, or skills (only on custom maps)
  523. return 2.5f;
  524. case Obj::HERO:
  525. return aiNk->cc->getPlayerRelations(target->tempOwner, aiNk->playerID) == PlayerRelations::ENEMIES
  526. ? enemyHeroEliminationSkillRewardRatio * dynamic_cast<const CGHeroInstance *>(target)->level
  527. : 0;
  528. default:
  529. break;
  530. }
  531. auto rewardable = dynamic_cast<const Rewardable::Interface *>(target);
  532. if(rewardable)
  533. {
  534. auto totalValue = 0.0f;
  535. for(int index : rewardable->getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT))
  536. {
  537. auto & info = rewardable->configuration.info[index];
  538. auto rewardValue = 0.0f;
  539. if(!info.reward.spells.empty())
  540. {
  541. for(auto spellID : info.reward.spells)
  542. {
  543. const spells::Spell * spell = LIBRARY->spells()->getById(spellID);
  544. if(hero->canLearnSpell(spell) && !hero->spellbookContainsSpell(spellID))
  545. {
  546. rewardValue += std::sqrt(spell->getLevel()) / 4.0f;
  547. }
  548. }
  549. totalValue += rewardValue / info.reward.spells.size();
  550. }
  551. if(!info.reward.primary.empty())
  552. {
  553. for(auto value : info.reward.primary)
  554. {
  555. totalValue += value;
  556. }
  557. }
  558. }
  559. return totalValue;
  560. }
  561. return 0;
  562. }
  563. const HitMapInfo & RewardEvaluator::getEnemyHeroDanger(const int3 & tile, uint8_t turn) const
  564. {
  565. auto & threatNode = aiNk->dangerHitMap->getTileThreat(tile);
  566. if(threatNode.maximumDanger.danger == 0)
  567. return HitMapInfo::NoThreat;
  568. if(threatNode.maximumDanger.turn <= turn)
  569. return threatNode.maximumDanger;
  570. return threatNode.fastestDanger.turn <= turn ? threatNode.fastestDanger : HitMapInfo::NoThreat;
  571. }
  572. int32_t getArmyCost(const CArmedInstance * army)
  573. {
  574. int32_t value = 0;
  575. for(const auto & stack : army->Slots())
  576. {
  577. value += stack.second->getCreatureID().toCreature()->getFullRecruitCost().marketValue() * stack.second->getCount();
  578. }
  579. return value;
  580. }
  581. int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CGHeroInstance * hero) const
  582. {
  583. if(!target)
  584. return 0;
  585. auto relations = aiNk->cc->getPlayerRelations(target->tempOwner, hero->tempOwner);
  586. const int dailyIncomeMultiplier = 5;
  587. const float enemyArmyEliminationGoldRewardRatio = 0.2f;
  588. const int32_t heroEliminationBonus = GameConstants::HERO_GOLD_COST / 2;
  589. switch(target->ID)
  590. {
  591. case Obj::RESOURCE:
  592. {
  593. auto * res = dynamic_cast<const CGResource*>(target);
  594. return res && res->resourceID() == GameResID::GOLD ? 600 : 100;
  595. }
  596. case Obj::TREASURE_CHEST:
  597. return 1500;
  598. case Obj::WATER_WHEEL:
  599. return 1000;
  600. case Obj::TOWN:
  601. return dailyIncomeMultiplier * estimateTownIncome(aiNk->cc.get(), target, hero);
  602. case Obj::MINE:
  603. case Obj::ABANDONED_MINE:
  604. {
  605. auto * mine = dynamic_cast<const CGMine*>(target);
  606. return dailyIncomeMultiplier * (mine->producedResource == GameResID::GOLD ? 1000 : 75);
  607. }
  608. case Obj::PANDORAS_BOX:
  609. return 2500;
  610. case Obj::PRISON:
  611. //Objectively saves us 2500 to hire hero
  612. return GameConstants::HERO_GOLD_COST;
  613. case Obj::HERO:
  614. return relations == PlayerRelations::ENEMIES
  615. ? heroEliminationBonus + enemyArmyEliminationGoldRewardRatio * getArmyCost(dynamic_cast<const CGHeroInstance *>(target))
  616. : 0;
  617. default:
  618. break;
  619. }
  620. auto rewardable = dynamic_cast<const Rewardable::Interface *>(target);
  621. if(rewardable)
  622. {
  623. auto goldReward = 0;
  624. for(int index : rewardable->getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT))
  625. {
  626. auto & info = rewardable->configuration.info[index];
  627. goldReward += getResourcesGoldReward(info.reward.resources);
  628. }
  629. return goldReward;
  630. }
  631. return 0;
  632. }
  633. class HeroExchangeEvaluator : public IEvaluationContextBuilder
  634. {
  635. public:
  636. void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
  637. {
  638. if(task->goalType != Goals::HERO_EXCHANGE)
  639. return;
  640. const auto & heroExchange = dynamic_cast<Goals::HeroExchange &>(*task);
  641. // const auto receiverHeroRole = evaluationContext.evaluator.aiNk->heroManager->getHeroRoleOrDefaultInefficient(heroExchange.hero);
  642. const auto giverHeroRole = evaluationContext.evaluator.aiNk->heroManager->getHeroRoleOrDefaultInefficient(heroExchange.exchangePath.targetHero);
  643. // It's allowed for SCOUTs to receive from other SCOUTs, so all army gets in one place before delivery to main
  644. // TODO: Mircea: See how we can get some kind of balance between MAINs in terms of army delivery
  645. // See: GatherArmyBehavior::deliverArmyToHero
  646. const uint64_t additionalArmyStrength = heroExchange.getReinforcementArmyStrength(evaluationContext.evaluator.aiNk);
  647. const float additionalArmyRatio = additionalArmyStrength / heroExchange.hero->getArmyStrength();
  648. evaluationContext.addNonCriticalStrategicalValue(additionalArmyRatio);
  649. evaluationContext.armyGrowth = additionalArmyStrength;
  650. evaluationContext.movementCost = heroExchange.exchangePath.movementCost();
  651. evaluationContext.danger = heroExchange.exchangePath.getTotalDanger();
  652. evaluationContext.heroRole = giverHeroRole;
  653. evaluationContext.isExchange = true;
  654. }
  655. };
  656. class ArmyUpgradeEvaluator : public IEvaluationContextBuilder
  657. {
  658. public:
  659. void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
  660. {
  661. if(task->goalType != Goals::ARMY_UPGRADE)
  662. return;
  663. const auto & armyUpgrade = dynamic_cast<Goals::ArmyUpgrade &>(*task);
  664. const uint64_t additionalArmyStrength = armyUpgrade.getUpgradeValue();
  665. // No strategical value and reduce the strength a bit to don't explode the final score so nothing else gets done by no one
  666. evaluationContext.armyGrowth = additionalArmyStrength / 5.0f;
  667. evaluationContext.isArmyUpgrade = true;
  668. }
  669. };
  670. class ExplorePointEvaluator : public IEvaluationContextBuilder
  671. {
  672. public:
  673. void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
  674. {
  675. if(task->goalType != Goals::EXPLORATION_POINT)
  676. return;
  677. int tilesDiscovered = task->value;
  678. evaluationContext.addNonCriticalStrategicalValue(0.03f * tilesDiscovered);
  679. for(const auto obj : evaluationContext.evaluator.aiNk->cc->getVisitableObjs(task->tile))
  680. {
  681. switch(obj->ID.num)
  682. {
  683. case Obj::MONOLITH_ONE_WAY_ENTRANCE:
  684. case Obj::MONOLITH_TWO_WAY:
  685. case Obj::SUBTERRANEAN_GATE:
  686. evaluationContext.explorePriority = 1;
  687. break;
  688. case Obj::REDWOOD_OBSERVATORY:
  689. case Obj::PILLAR_OF_FIRE:
  690. evaluationContext.explorePriority = 2;
  691. break;
  692. default:
  693. logAi->warn("ExplorePointEvaluator buildEvaluationContext unknown exploration point %d", obj->ID.num);
  694. }
  695. }
  696. if(evaluationContext.evaluator.aiNk->cc->getTile(task->tile)->roadType != RoadId::NO_ROAD)
  697. evaluationContext.explorePriority = 1;
  698. if(evaluationContext.explorePriority == 0)
  699. evaluationContext.explorePriority = 3;
  700. }
  701. };
  702. class StayAtTownManaRecoveryEvaluator : public IEvaluationContextBuilder
  703. {
  704. public:
  705. void buildEvaluationContext(EvaluationContext& evaluationContext, Goals::TSubgoal task) const override
  706. {
  707. if (task->goalType != Goals::STAY_AT_TOWN)
  708. return;
  709. Goals::StayAtTown& stayAtTown = dynamic_cast<Goals::StayAtTown&>(*task);
  710. if (stayAtTown.getHero() != nullptr && stayAtTown.getHero()->movementPointsRemaining() < 100)
  711. {
  712. return;
  713. }
  714. if(stayAtTown.town->mageGuildLevel() > 0)
  715. evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero());
  716. if (vstd::isAlmostZero(evaluationContext.armyReward))
  717. evaluationContext.isDefend = true;
  718. else
  719. {
  720. evaluationContext.movementCost += stayAtTown.getMovementWasted();
  721. evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted();
  722. }
  723. }
  724. };
  725. void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uint8_t turn, uint64_t ourStrength)
  726. {
  727. HitMapInfo enemyDanger = evaluationContext.evaluator.getEnemyHeroDanger(tile, turn);
  728. if(enemyDanger.danger)
  729. {
  730. auto dangerRatio = enemyDanger.danger / (double)ourStrength;
  731. vstd::amax(evaluationContext.enemyHeroDangerRatio, dangerRatio);
  732. vstd::amax(evaluationContext.threat, enemyDanger.threat);
  733. }
  734. }
  735. class DefendTownEvaluator : public IEvaluationContextBuilder
  736. {
  737. public:
  738. void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
  739. {
  740. if(task->goalType != Goals::DEFEND_TOWN)
  741. return;
  742. Goals::DefendTown & defendTown = dynamic_cast<Goals::DefendTown &>(*task);
  743. const CGTownInstance * town = defendTown.town;
  744. auto & threat = defendTown.getThreat();
  745. auto strategicalValue = evaluationContext.evaluator.getStrategicalValue(town);
  746. float multiplier = 1;
  747. if(threat.turn < defendTown.getTurn())
  748. multiplier /= 1 + (defendTown.getTurn() - threat.turn);
  749. multiplier /= 1.0f + threat.turn / 5.0f;
  750. if(defendTown.getTurn() > 0 && defendTown.isCounterAttack())
  751. {
  752. auto ourSpeed = defendTown.hero->movementPointsLimit(true);
  753. auto enemySpeed = threat.heroPtr.get()->movementPointsLimit(true);
  754. if(enemySpeed > ourSpeed) multiplier *= 0.7f;
  755. }
  756. auto dailyIncome = town->dailyIncome()[EGameResID::GOLD];
  757. auto armyGrowth = evaluationContext.evaluator.townArmyGrowth(town);
  758. evaluationContext.armyGrowth += armyGrowth * multiplier;
  759. evaluationContext.goldReward += dailyIncome * 5 * multiplier;
  760. if(evaluationContext.evaluator.aiNk->buildAnalyzer->getDevelopmentInfo().size() == 1)
  761. vstd::amax(evaluationContext.strategicalValue, 2.5f * multiplier * strategicalValue);
  762. else
  763. evaluationContext.addNonCriticalStrategicalValue(1.7f * multiplier * strategicalValue);
  764. evaluationContext.defenseValue = town->fortLevel();
  765. evaluationContext.isDefend = true;
  766. evaluationContext.threatTurns = threat.turn;
  767. vstd::amax(evaluationContext.danger, defendTown.getThreat().danger);
  768. addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength());
  769. }
  770. };
  771. class ExecuteHeroChainEvaluationContextBuilder : public IEvaluationContextBuilder
  772. {
  773. private:
  774. const Nullkiller * aiNk;
  775. public:
  776. ExecuteHeroChainEvaluationContextBuilder(const Nullkiller * aiNk) : aiNk(aiNk) {}
  777. void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
  778. {
  779. if(task->goalType != Goals::EXECUTE_HERO_CHAIN)
  780. return;
  781. Goals::ExecuteHeroChain & chain = dynamic_cast<Goals::ExecuteHeroChain &>(*task);
  782. const AIPath & path = chain.getPath();
  783. if (vstd::isAlmostZero(path.movementCost()))
  784. return;
  785. vstd::amax(evaluationContext.danger, path.getTotalDanger());
  786. evaluationContext.movementCost += path.movementCost();
  787. evaluationContext.closestWayRatio = chain.closestWayRatio;
  788. std::map<const CGHeroInstance *, float> costsPerHero;
  789. for(auto & node : path.nodes)
  790. {
  791. vstd::amax(costsPerHero[node.targetHero], node.cost);
  792. if (node.layer == EPathfindingLayer::SAIL)
  793. evaluationContext.involvesSailing = true;
  794. }
  795. float highestCostForSingleHero = 0;
  796. for(auto pair : costsPerHero)
  797. {
  798. auto role = evaluationContext.evaluator.aiNk->heroManager->getHeroRoleOrDefaultInefficient(pair.first);
  799. evaluationContext.movementCostByRole[role] += pair.second;
  800. if (pair.second > highestCostForSingleHero)
  801. highestCostForSingleHero = pair.second;
  802. }
  803. if (highestCostForSingleHero > 1 && costsPerHero.size() > 1)
  804. {
  805. //Chains that involve more than 1 hero doing something for more than a turn are too expensive in my book. They often involved heroes doing nothing just standing there waiting to fulfill their part of the chain.
  806. return;
  807. }
  808. evaluationContext.movementCost *= costsPerHero.size(); //further deincentivise chaining as it often involves bringing back the army afterwards
  809. auto hero = task->hero;
  810. bool checkGold = evaluationContext.danger == 0;
  811. auto army = path.heroArmy;
  812. const CGObjectInstance * target = aiNk->cc->getObj((ObjectInstanceID)task->objid, false);
  813. auto heroRole = evaluationContext.evaluator.aiNk->heroManager->getHeroRoleOrDefaultInefficient(hero);
  814. if(heroRole == HeroRole::MAIN)
  815. evaluationContext.heroRole = heroRole;
  816. // Assuming Slots() returns a collection of slots with slot.second->getCreatureID() and slot.second->getPower()
  817. float heroPower = 0;
  818. float totalPower = 0;
  819. // Map to store the aggregated power of creatures by CreatureID
  820. std::map<CreatureID, float> totalPowerByCreatureID;
  821. // Calculate hero power and total power by CreatureID
  822. for (const auto & slot : hero->Slots())
  823. {
  824. CreatureID creatureID = slot.second->getCreatureID();
  825. float slotPower = slot.second->getPower();
  826. // Add the power of this slot to the heroPower
  827. heroPower += slotPower;
  828. // Accumulate the total power for the specific CreatureID
  829. if (totalPowerByCreatureID.find(creatureID) == totalPowerByCreatureID.end())
  830. {
  831. // First time encountering this CreatureID, retrieve total creatures' power
  832. totalPowerByCreatureID[creatureID] = aiNk->armyManager->getTotalCreaturesAvailable(creatureID).power;
  833. }
  834. }
  835. // Calculate total power based on unique CreatureIDs
  836. for (const auto& entry : totalPowerByCreatureID)
  837. {
  838. totalPower += entry.second;
  839. }
  840. // Compute the power ratio if total power is greater than zero
  841. if (totalPower > 0)
  842. {
  843. evaluationContext.powerRatio = heroPower / totalPower;
  844. }
  845. if (target)
  846. {
  847. evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero);
  848. evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold);
  849. evaluationContext.armyGrowth += evaluationContext.evaluator.getArmyGrowth(target, hero, army);
  850. evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, heroRole);
  851. evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target));
  852. evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target);
  853. if (target->ID == Obj::HERO)
  854. evaluationContext.isHero = true;
  855. if (target->getOwner().isValidPlayer() && aiNk->cc->getPlayerRelations(aiNk->playerID, target->getOwner()) == PlayerRelations::ENEMIES)
  856. evaluationContext.isEnemy = true;
  857. if (target->ID == Obj::TOWN)
  858. evaluationContext.defenseValue = dynamic_cast<const CGTownInstance*>(target)->fortLevel();
  859. evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army);
  860. if(evaluationContext.danger > 0)
  861. evaluationContext.skillReward += (float)evaluationContext.danger / (float)hero->getArmyStrength();
  862. }
  863. evaluationContext.armyInvolvement += army->getArmyCost();
  864. vstd::amax(evaluationContext.armyLossRatio, (float)path.getTotalArmyLoss() / (float)army->getArmyStrength());
  865. addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength());
  866. vstd::amax(evaluationContext.turn, path.turn());
  867. }
  868. };
  869. class ClusterEvaluationContextBuilder : public IEvaluationContextBuilder
  870. {
  871. public:
  872. ClusterEvaluationContextBuilder(const Nullkiller * aiNk) {}
  873. void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
  874. {
  875. if(task->goalType != Goals::UNLOCK_CLUSTER)
  876. return;
  877. Goals::UnlockCluster & clusterGoal = dynamic_cast<Goals::UnlockCluster &>(*task);
  878. std::shared_ptr<ObjectCluster> cluster = clusterGoal.getCluster();
  879. auto hero = clusterGoal.hero;
  880. auto role = evaluationContext.evaluator.aiNk->heroManager->getHeroRoleOrDefaultInefficient(hero);
  881. std::vector<std::pair<ObjectInstanceID, ClusterObjectInfo>> objects;
  882. objects.reserve(cluster->objects.size());
  883. for (const auto& obj : cluster->objects)
  884. objects.emplace_back(obj.first, obj.second);
  885. std::sort(objects.begin(), objects.end(), [](std::pair<ObjectInstanceID, ClusterObjectInfo> o1, std::pair<ObjectInstanceID, ClusterObjectInfo> o2) -> bool
  886. {
  887. return o1.second.priority > o2.second.priority;
  888. });
  889. int boost = 1;
  890. for(auto & objInfo : objects)
  891. {
  892. auto target = evaluationContext.evaluator.aiNk->cc->getObj(objInfo.first);
  893. bool checkGold = objInfo.second.danger == 0;
  894. auto army = hero;
  895. evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero) / boost;
  896. evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold) / boost;
  897. evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, role) / boost;
  898. evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target) / boost);
  899. evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target);
  900. evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army) / boost;
  901. evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost;
  902. evaluationContext.movementCost += objInfo.second.movementCost / boost;
  903. vstd::amax(evaluationContext.turn, objInfo.second.turn / boost);
  904. boost <<= 1;
  905. if(boost > 8)
  906. break;
  907. }
  908. }
  909. };
  910. class ExchangeSwapTownHeroesContextBuilder : public IEvaluationContextBuilder
  911. {
  912. public:
  913. void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
  914. {
  915. if(task->goalType != Goals::EXCHANGE_SWAP_TOWN_HEROES)
  916. return;
  917. Goals::ExchangeSwapTownHeroes & swapCommand = dynamic_cast<Goals::ExchangeSwapTownHeroes &>(*task);
  918. const CGHeroInstance * garrisonHero = swapCommand.getGarrisonHero();
  919. logAi->trace("buildEvaluationContext ExchangeSwapTownHeroesContextBuilder %s affected objects: %d", swapCommand.toString(), swapCommand.getAffectedObjects().size());
  920. for (auto obj : swapCommand.getAffectedObjects())
  921. {
  922. logAi->trace("affected object: %s", evaluationContext.evaluator.aiNk->cc->getObj(obj)->getObjectName());
  923. }
  924. if (garrisonHero)
  925. logAi->debug("with %s and %d", garrisonHero->getNameTranslated(), int(swapCommand.getLockingReason()));
  926. if(garrisonHero && swapCommand.getLockingReason() == HeroLockedReason::DEFENCE)
  927. {
  928. auto defenderRole = evaluationContext.evaluator.aiNk->heroManager->getHeroRoleOrDefaultInefficient(garrisonHero);
  929. auto mpLeft = garrisonHero->movementPointsRemaining() / (float)garrisonHero->movementPointsLimit(true);
  930. evaluationContext.movementCost += mpLeft;
  931. evaluationContext.movementCostByRole[defenderRole] += mpLeft;
  932. evaluationContext.heroRole = defenderRole;
  933. evaluationContext.isDefend = true;
  934. evaluationContext.armyInvolvement = garrisonHero->getArmyStrength();
  935. logAi->debug("evaluationContext.isDefend: %d", evaluationContext.isDefend);
  936. }
  937. }
  938. };
  939. class DismissHeroContextBuilder : public IEvaluationContextBuilder
  940. {
  941. private:
  942. const Nullkiller * aiNk;
  943. public:
  944. DismissHeroContextBuilder(const Nullkiller * aiNk) : aiNk(aiNk) {}
  945. void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
  946. {
  947. if(task->goalType != Goals::DISMISS_HERO)
  948. return;
  949. Goals::DismissHero & dismissCommand = dynamic_cast<Goals::DismissHero &>(*task);
  950. const CGHeroInstance * dismissedHero = dismissCommand.getHero();
  951. auto role = aiNk->heroManager->getHeroRoleOrDefaultInefficient(dismissedHero);
  952. auto mpLeft = dismissedHero->movementPointsRemaining();
  953. evaluationContext.movementCost += mpLeft;
  954. evaluationContext.movementCostByRole[role] += mpLeft;
  955. evaluationContext.goldCost += GameConstants::HERO_GOLD_COST + getArmyCost(dismissedHero);
  956. }
  957. };
  958. class BuildThisEvaluationContextBuilder : public IEvaluationContextBuilder
  959. {
  960. public:
  961. void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
  962. {
  963. if(task->goalType != Goals::BUILD_STRUCTURE)
  964. return;
  965. Goals::BuildThis & buildThis = dynamic_cast<Goals::BuildThis &>(*task);
  966. auto & bi = buildThis.buildingInfo;
  967. constexpr int dailyIncomeValueFactor = 7;
  968. evaluationContext.goldReward += dailyIncomeValueFactor * bi.dailyIncome.marketValue() / 2; // 7 day income but half we already have
  969. evaluationContext.heroRole = HeroRole::MAIN;
  970. evaluationContext.movementCostByRole[evaluationContext.heroRole] += bi.prerequisitesCount;
  971. int32_t cost = bi.buildCost[EGameResID::GOLD];
  972. evaluationContext.goldCost += cost;
  973. evaluationContext.closestWayRatio = 1;
  974. evaluationContext.buildingCost += bi.buildCostWithPrerequisites;
  975. bool alreadyOwn = false;
  976. int highestMageGuildPossible = BuildingID::MAGES_GUILD_3;
  977. for (auto town : evaluationContext.evaluator.aiNk->cc->getTownsInfo())
  978. {
  979. if (town->hasBuilt(bi.id))
  980. alreadyOwn = true;
  981. if (evaluationContext.evaluator.aiNk->cc->canBuildStructure(town, BuildingID::MAGES_GUILD_5) != EBuildingState::FORBIDDEN)
  982. highestMageGuildPossible = BuildingID::MAGES_GUILD_5;
  983. else if (evaluationContext.evaluator.aiNk->cc->canBuildStructure(town, BuildingID::MAGES_GUILD_4) != EBuildingState::FORBIDDEN)
  984. highestMageGuildPossible = BuildingID::MAGES_GUILD_4;
  985. }
  986. if (bi.id == BuildingID::MARKETPLACE || bi.dailyIncome[EGameResID::WOOD] > 0)
  987. evaluationContext.isTradeBuilding = true;
  988. #if NK2AI_TRACE_LEVEL >= 1
  989. logAi->trace("Building costs for %s : %s MarketValue: %d",bi.toString(), evaluationContext.buildingCost.toString(), evaluationContext.buildingCost.marketValue());
  990. #endif
  991. if(bi.creatureID != CreatureID::NONE)
  992. {
  993. evaluationContext.addNonCriticalStrategicalValue(buildThis.townInfo.armyStrength / 50000.0);
  994. if(bi.baseCreatureID == bi.creatureID)
  995. {
  996. evaluationContext.addNonCriticalStrategicalValue((0.5f + 0.1f * bi.creatureLevel) / (float)bi.prerequisitesCount);
  997. evaluationContext.armyReward += bi.armyStrength * 1.5;
  998. }
  999. else
  1000. {
  1001. auto potentialUpgradeValue = evaluationContext.evaluator.getUpgradeArmyReward(buildThis.town, bi);
  1002. evaluationContext.addNonCriticalStrategicalValue(potentialUpgradeValue / 10000.0f / (float)bi.prerequisitesCount);
  1003. if(bi.id.isDwelling())
  1004. evaluationContext.armyReward += bi.armyStrength - evaluationContext.evaluator.aiNk->armyManager->evaluateStackPower(bi.baseCreatureID.toCreature(), bi.creatureGrowth);
  1005. else //This is for prerequisite-buildings
  1006. evaluationContext.armyReward += evaluationContext.evaluator.aiNk->armyManager->evaluateStackPower(bi.baseCreatureID.toCreature(), bi.creatureGrowth);
  1007. if(alreadyOwn)
  1008. evaluationContext.armyReward /= bi.buildCostWithPrerequisites.marketValue();
  1009. }
  1010. }
  1011. else if(bi.id == BuildingID::CITADEL || bi.id == BuildingID::CASTLE)
  1012. {
  1013. evaluationContext.addNonCriticalStrategicalValue(buildThis.town->creatures.size() * 0.2f);
  1014. evaluationContext.armyReward += buildThis.townInfo.armyStrength / 2;
  1015. }
  1016. else if(bi.id >= BuildingID::MAGES_GUILD_1 && bi.id <= BuildingID::MAGES_GUILD_5)
  1017. {
  1018. evaluationContext.skillReward += 2 * bi.id.getMagesGuildLevel();
  1019. if (!alreadyOwn && evaluationContext.evaluator.aiNk->cc->canBuildStructure(buildThis.town, highestMageGuildPossible) != EBuildingState::FORBIDDEN)
  1020. {
  1021. for (auto hero : evaluationContext.evaluator.aiNk->cc->getHeroesInfo())
  1022. {
  1023. if(hero->getPrimSkillLevel(PrimarySkill::SPELL_POWER) + hero->getPrimSkillLevel(PrimarySkill::KNOWLEDGE) > hero->getPrimSkillLevel(PrimarySkill::ATTACK) + hero->getPrimSkillLevel(PrimarySkill::DEFENSE)
  1024. && hero->manaLimit() > 30)
  1025. evaluationContext.armyReward += hero->getArmyCost();
  1026. }
  1027. }
  1028. }
  1029. int sameTownBonus = 0;
  1030. for (auto town : evaluationContext.evaluator.aiNk->cc->getTownsInfo())
  1031. {
  1032. if (buildThis.town->getFaction() == town->getFaction())
  1033. sameTownBonus += town->getTownLevel();
  1034. }
  1035. evaluationContext.armyReward *= sameTownBonus;
  1036. if(evaluationContext.goldReward)
  1037. {
  1038. auto goldPressure = evaluationContext.evaluator.aiNk->buildAnalyzer->getGoldPressure();
  1039. evaluationContext.addNonCriticalStrategicalValue(evaluationContext.goldReward * goldPressure / 3500.0f / bi.prerequisitesCount);
  1040. }
  1041. if(bi.isMissingResources && bi.prerequisitesCount == 1)
  1042. {
  1043. evaluationContext.strategicalValue /= 3;
  1044. evaluationContext.movementCostByRole[evaluationContext.heroRole] += 5;
  1045. evaluationContext.turn += 5;
  1046. }
  1047. }
  1048. };
  1049. uint64_t RewardEvaluator::getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const
  1050. {
  1051. if(aiNk->buildAnalyzer->isBuilt(town->getFactionID(), bi.id))
  1052. return 0;
  1053. auto creaturesToUpgrade = aiNk->armyManager->getTotalCreaturesAvailable(bi.baseCreatureID);
  1054. auto upgradedPower = aiNk->armyManager->evaluateStackPower(bi.creatureID.toCreature(), creaturesToUpgrade.count);
  1055. return upgradedPower - creaturesToUpgrade.power;
  1056. }
  1057. PriorityEvaluator::PriorityEvaluator(const Nullkiller * aiNk) : aiNk(aiNk)
  1058. {
  1059. initVisitTile();
  1060. evaluationContextBuilders.push_back(std::make_shared<ExecuteHeroChainEvaluationContextBuilder>(aiNk));
  1061. evaluationContextBuilders.push_back(std::make_shared<BuildThisEvaluationContextBuilder>());
  1062. evaluationContextBuilders.push_back(std::make_shared<ClusterEvaluationContextBuilder>(aiNk));
  1063. evaluationContextBuilders.push_back(std::make_shared<HeroExchangeEvaluator>());
  1064. evaluationContextBuilders.push_back(std::make_shared<ArmyUpgradeEvaluator>());
  1065. evaluationContextBuilders.push_back(std::make_shared<DefendTownEvaluator>());
  1066. evaluationContextBuilders.push_back(std::make_shared<ExchangeSwapTownHeroesContextBuilder>());
  1067. evaluationContextBuilders.push_back(std::make_shared<DismissHeroContextBuilder>(aiNk));
  1068. evaluationContextBuilders.push_back(std::make_shared<StayAtTownManaRecoveryEvaluator>());
  1069. evaluationContextBuilders.push_back(std::make_shared<ExplorePointEvaluator>());
  1070. }
  1071. EvaluationContext PriorityEvaluator::buildEvaluationContext(const Goals::TSubgoal & goal) const
  1072. {
  1073. Goals::TGoalVec parts;
  1074. EvaluationContext context(aiNk);
  1075. if(goal->goalType == Goals::COMPOSITION)
  1076. {
  1077. parts = goal->decompose(aiNk);
  1078. }
  1079. else
  1080. {
  1081. parts.push_back(goal);
  1082. }
  1083. for(auto subgoal : parts)
  1084. {
  1085. context.goldCost += subgoal->goldCost;
  1086. context.buildingCost += subgoal->buildingCost;
  1087. for(auto builder : evaluationContextBuilders)
  1088. {
  1089. builder->buildEvaluationContext(context, subgoal);
  1090. }
  1091. }
  1092. return context;
  1093. }
  1094. float PriorityEvaluator::evaluateMovement(float score, const float movementCost)
  1095. {
  1096. if(movementCost > 0)
  1097. {
  1098. if(movementCost < 1)
  1099. // Reduce bonus if it's too close, otherwise the score is amplified too much
  1100. score /= std::pow(movementCost, 0.6f);
  1101. else
  1102. // Penalize distance, including when 1.0
  1103. score /= 0.75 + std::pow(movementCost, 1.3f);
  1104. }
  1105. return score;
  1106. }
  1107. float PriorityEvaluator::evaluateArmyLossRatio(float score, const float armyLossRatio, const HeroRole heroRole)
  1108. {
  1109. if(armyLossRatio > 0)
  1110. {
  1111. score -= score * armyLossRatio;
  1112. // Encourage battles for MAIN to gather more XP in the same heroes, not in scouts
  1113. if(heroRole != MAIN)
  1114. score /= 5.0f;
  1115. }
  1116. return score;
  1117. }
  1118. float PriorityEvaluator::evaluateSkillReward(float score, const float skillReward, const float armyInvolvement, const float armyLossRatio)
  1119. {
  1120. // Encourage stronger heroes
  1121. return score + skillReward * armyInvolvement * (1 - armyLossRatio) * 0.05;
  1122. }
  1123. float PriorityEvaluator::evaluateConquestValue(float score, const float conquestValue, const float armyInvolvement)
  1124. {
  1125. if(conquestValue > 0)
  1126. score = armyInvolvement * conquestValue;
  1127. return score;
  1128. }
  1129. float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
  1130. {
  1131. auto evaluationContext = buildEvaluationContext(task);
  1132. int rewardType = (evaluationContext.goldReward > 0 ? 1 : 0)
  1133. + (evaluationContext.armyReward > 0 ? 1 : 0)
  1134. + (evaluationContext.skillReward > 0 ? 1 : 0)
  1135. + (evaluationContext.strategicalValue > 0 ? 1 : 0);
  1136. float goldRewardVsMovement = evaluationContext.goldReward / std::log2f(2 + evaluationContext.movementCost * 10);
  1137. const bool amIWithoutCastle = aiNk->cc->getPlayerState(aiNk->playerID)->daysWithoutCastle.has_value();
  1138. double result = 0;
  1139. if (aiNk->settings->isUseFuzzy())
  1140. {
  1141. float fuzzyResult = 0;
  1142. try
  1143. {
  1144. armyLossRatioVariable->setValue(evaluationContext.armyLossRatio);
  1145. heroRoleVariable->setValue(evaluationContext.heroRole);
  1146. mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]);
  1147. scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]);
  1148. goldRewardVsMovementVariable->setValue(goldRewardVsMovement);
  1149. armyRewardVariable->setValue(evaluationContext.armyReward);
  1150. armyGrowthVariable->setValue(evaluationContext.armyGrowth);
  1151. skillRewardVariable->setValue(evaluationContext.skillReward);
  1152. dangerVariable->setValue(evaluationContext.danger);
  1153. rewardTypeVariable->setValue(rewardType);
  1154. closestHeroRatioVariable->setValue(evaluationContext.closestWayRatio);
  1155. strategicalValueVariable->setValue(evaluationContext.strategicalValue);
  1156. goldPressureVariable->setValue(aiNk->buildAnalyzer->getGoldPressure());
  1157. goldCostVariable->setValue(evaluationContext.goldCost / ((float)aiNk->getFreeResources()[EGameResID::GOLD] + (float)aiNk->buildAnalyzer->getDailyIncome()[EGameResID::GOLD] + 1.0f));
  1158. turnVariable->setValue(evaluationContext.turn);
  1159. fearVariable->setValue(evaluationContext.enemyHeroDangerRatio);
  1160. engine->process();
  1161. fuzzyResult = value->getValue();
  1162. }
  1163. catch (fl::Exception& fe)
  1164. {
  1165. logAi->error("evaluate VisitTile: %s", fe.getWhat());
  1166. }
  1167. result = fuzzyResult;
  1168. }
  1169. else
  1170. {
  1171. float score = 0;
  1172. // TODO: Mircea: Shouldn't it default to 0 instead of 1.0 in the end?
  1173. const float maxWillingToLose = amIWithoutCastle ? 1
  1174. : aiNk->settings->getMaxArmyLossTarget() * evaluationContext.powerRatio > 0
  1175. ? aiNk->settings->getMaxArmyLossTarget() * evaluationContext.powerRatio
  1176. : 1.0;
  1177. const float maxEnemyDangerRatio = evaluationContext.powerRatio > 0 ? evaluationContext.powerRatio : 1.0;
  1178. const bool arriveNextWeek = aiNk->cc->getDate(Date::DAY_OF_WEEK) + evaluationContext.turn > LIBRARY->engineSettings()->getInteger(EGameSettings::GENERAL_DAYS_PER_WEEK);
  1179. #if NK2AI_TRACE_LEVEL >= 2
  1180. logAi->trace(
  1181. "BEFORE: priorityTier %d, Evaluated %s, armyLossRatio: %f, maxWillingToLose: %f, turn: %d, turns main: %f, scout: %f, armyInvolvement: %f, "
  1182. "goldReward: %f, goldRewardVsMovement: %f, goldCost: %d, armyReward: %f, armyGrowth: %f, skillReward: %f, danger: %d, threatTurns: %d, threat: %d, "
  1183. "heroRole: %s, strategicalValue: %f, conquestValue: %f, buildingCost.marketValue: %f, closestWayRatio: %f, enemyHeroDangerRatio: %f, "
  1184. "maxEnemyDangerRatio: %f, explorePriority: %d, isDefend: %d, isEnemy: %d, arriveNextWeek: %d, powerRatio: %f",
  1185. priorityTier,
  1186. task->toString(),
  1187. evaluationContext.armyLossRatio,
  1188. maxWillingToLose,
  1189. static_cast<int>(evaluationContext.turn),
  1190. evaluationContext.movementCostByRole[HeroRole::MAIN],
  1191. evaluationContext.movementCostByRole[HeroRole::SCOUT],
  1192. evaluationContext.armyInvolvement,
  1193. evaluationContext.goldReward,
  1194. goldRewardVsMovement,
  1195. evaluationContext.goldCost,
  1196. evaluationContext.armyReward,
  1197. evaluationContext.armyGrowth,
  1198. evaluationContext.skillReward,
  1199. evaluationContext.danger,
  1200. evaluationContext.threatTurns,
  1201. evaluationContext.threat,
  1202. evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout",
  1203. evaluationContext.strategicalValue,
  1204. evaluationContext.conquestValue,
  1205. evaluationContext.buildingCost.marketValue(),
  1206. evaluationContext.closestWayRatio,
  1207. evaluationContext.enemyHeroDangerRatio,
  1208. maxEnemyDangerRatio,
  1209. evaluationContext.explorePriority,
  1210. evaluationContext.isDefend,
  1211. evaluationContext.isEnemy,
  1212. arriveNextWeek,
  1213. evaluationContext.powerRatio);
  1214. #endif
  1215. switch (priorityTier)
  1216. {
  1217. case INSTAKILL: //Take towns / kill heroes in immediate reach
  1218. {
  1219. if(evaluationContext.turn > 0 || evaluationContext.isExchange || evaluationContext.isDefend)
  1220. return 0;
  1221. if(evaluationContext.movementCost >= 1)
  1222. return 0;
  1223. // TODO: Mircea: Ensure defenseValue is taken into account. See AINodeStorage::evaluateArmyLoss and CCreatureSet::getArmyStrength
  1224. // TODO: Mircea: make it dynamic, allow higher risk for killing a higher risk hero if it leads to killing an entire player. See conquestValue
  1225. if(maxWillingToLose - evaluationContext.armyLossRatio < 0)
  1226. return 0;
  1227. score = evaluateConquestValue(score, evaluationContext.conquestValue, evaluationContext.armyInvolvement);
  1228. score = evaluateArmyLossRatio(score, evaluationContext.armyLossRatio, evaluationContext.heroRole);
  1229. if(vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > maxEnemyDangerRatio && !amIWithoutCastle))
  1230. return 0;
  1231. score *= evaluationContext.closestWayRatio;
  1232. score = evaluateMovement(score, evaluationContext.movementCost);
  1233. break;
  1234. }
  1235. case INSTADEFEND: //Defend immediately threatened towns
  1236. {
  1237. if(!evaluationContext.isDefend)
  1238. return 0;
  1239. // TODO: Mircea: Often is better to die as long as you're almost destroying the opponent. To revisit
  1240. if(maxWillingToLose - evaluationContext.armyLossRatio < 0)
  1241. return 0;
  1242. if(evaluationContext.isEnemy && evaluationContext.turn > 0)
  1243. return 0;
  1244. if(evaluationContext.threatTurns <= evaluationContext.turn)
  1245. {
  1246. // TODO: Mircea: Too many heroes are rushing for INSTADEFEND.
  1247. // We need some kind of smart selection of who to go, not everyone qualified
  1248. // Probably apply normal fight calculation as in others + filter out unnecessary ones in makeTurn buildPlan
  1249. const float OPTIMAL_PERCENTAGE = 0.75f; // We want army to be 75% of the threat
  1250. float optimalStrength = evaluationContext.threat * OPTIMAL_PERCENTAGE;
  1251. // Calculate how far the army is from optimal strength
  1252. float deviation = std::abs(evaluationContext.armyInvolvement - optimalStrength);
  1253. float deviationPercentage = deviation / evaluationContext.threat;
  1254. // Calculate score: 1.0 is perfect, decreasing as deviation increases
  1255. score = 1.0f / (1.0f + deviationPercentage);
  1256. score *= evaluationContext.closestWayRatio;
  1257. score = evaluateMovement(score, evaluationContext.movementCost);
  1258. }
  1259. break;
  1260. }
  1261. case KILL: //Take towns / kill heroes that are further away
  1262. {
  1263. if(evaluationContext.isDefend)
  1264. return 0;
  1265. // TODO: Mircea: Ensure defenseValue is taken into account. See AINodeStorage::evaluateArmyLoss and CCreatureSet::getArmyStrength
  1266. // if (evaluationContext.defenseValue < 2 && evaluationContext.enemyHeroDangerRatio > involvedStrengthOutOfTotalRatio)
  1267. // return 0;
  1268. if (evaluationContext.turn > 0 && evaluationContext.isHero)
  1269. return 0;
  1270. if (arriveNextWeek && evaluationContext.isEnemy)
  1271. return 0;
  1272. score = evaluateConquestValue(score, evaluationContext.conquestValue, evaluationContext.armyInvolvement);
  1273. // TODO: Mircea: Last part of the if looks strange, to revisit
  1274. if(vstd::isAlmostZero(score)
  1275. || (evaluationContext.enemyHeroDangerRatio > maxEnemyDangerRatio && (evaluationContext.turn > 0 || evaluationContext.isExchange)
  1276. && !amIWithoutCastle))
  1277. return 0;
  1278. if (maxWillingToLose - evaluationContext.armyLossRatio < 0)
  1279. return 0;
  1280. score = evaluateArmyLossRatio(score, evaluationContext.armyLossRatio, evaluationContext.heroRole);
  1281. score *= evaluationContext.closestWayRatio;
  1282. score = evaluateMovement(score, evaluationContext.movementCost);
  1283. break;
  1284. }
  1285. case EXPLORE_AND_GATHER:
  1286. case ESCAPE:
  1287. // TODO: Mircea: Should not go to something that gives army if no slots available in the hero, but probably not in the evaluator, but in the finder
  1288. // task.get()->hero->getSlotFor(creature, 7) == false (not sure I get to know which creature is there in Orc Tower building)
  1289. // /// so I can't know for sure if it fits my stacks or not, but at least we can avoid going there with all 7 stacks occupied by other units
  1290. // task.get()->hero->getFreeSlots(7) == 7
  1291. // getDuplicatingSlots(task.get()->hero) == false
  1292. {
  1293. if(evaluationContext.conquestValue > 0)
  1294. return 0;
  1295. if(evaluationContext.isDefend)
  1296. return 0;
  1297. if(evaluationContext.buildingCost.marketValue() > 0)
  1298. return 0;
  1299. if(maxWillingToLose - evaluationContext.armyLossRatio < 0)
  1300. return 0;
  1301. if(priorityTier == EXPLORE_AND_GATHER && evaluationContext.enemyHeroDangerRatio > maxEnemyDangerRatio)
  1302. return 0;
  1303. if(priorityTier == ESCAPE && task->hero)
  1304. {
  1305. const auto currentTileThreat = aiNk->dangerHitMap->getTileThreat(task->hero->visitablePos());
  1306. if(currentTileThreat.fastestDanger.turn < 1 && currentTileThreat.fastestDanger.danger > task->hero->getTotalStrength())
  1307. {
  1308. // Encourage routes which go away of the threat
  1309. const auto currentTileThreatVal = currentTileThreat.fastestDanger.threat;
  1310. const auto destTileThreatVal = aiNk->dangerHitMap->getTileThreat(task->tile).fastestDanger.threat;
  1311. const auto delta = currentTileThreatVal - destTileThreatVal;
  1312. if(delta > 0)
  1313. {
  1314. logAi->trace("priorityTier %d, Encouraging route with less threat delta: %f", priorityTier, delta);
  1315. score += delta;
  1316. }
  1317. else
  1318. logAi->trace("priorityTier %d, Cannot encourage route because it has a negative threat delta: %f. Hoping hero will live", priorityTier, delta);
  1319. }
  1320. }
  1321. // TODO: Mircea: Not sure this makes sense anymore, deactivating for now, to test more and delete in the end
  1322. // if(evaluationContext.enemyHeroDangerRatio > involvedStrengthOutOfTotalRatio && !evaluationContext.isDefend && priorityTier != FAR_HUNTER_GATHER)
  1323. // return 0;
  1324. // TODO: Mircea: Not sure these make sense anymore, deactivating for now, to test more and delete in the end
  1325. // if(priorityTier != FAR_HUNTER_GATHER && evaluationContext.isDefend
  1326. // && (evaluationContext.enemyHeroDangerRatio > involvedStrengthOutOfTotalRatio || evaluationContext.threatTurns > 0 || evaluationContext.turn > 0))
  1327. // return 0;
  1328. // TODO: Mircea: Candidate to re-include arriveNextWeek with !isExploration or > 0, but might prevent fights far away, maybe just discourage
  1329. // if(priorityTier != FAR_HUNTER_GATHER
  1330. // && ((evaluationContext.enemyHeroDangerRatio > 0 && arriveNextWeek) || evaluationContext.enemyHeroDangerRatio > involvedStrengthOutOfTotalRatio))
  1331. // return 0;
  1332. const auto requiresBattle = evaluationContext.armyLossRatio > 0 || evaluationContext.danger > 0;
  1333. score += evaluationContext.strategicalValue * 1000;
  1334. if(evaluationContext.explorePriority > 0)
  1335. {
  1336. score = 600.0f / evaluationContext.explorePriority;
  1337. // Encourage exploration for MAIN that requires battles, so SCOUTs can continue exploring
  1338. if(evaluationContext.heroRole == MAIN && requiresBattle)
  1339. score *= 2;
  1340. }
  1341. if(evaluationContext.goldReward > 0)
  1342. {
  1343. // try to balance other resources vs gold, especially 2500 gold treasures
  1344. score += evaluationContext.goldReward > 500 ? evaluationContext.goldReward / 2.0f : evaluationContext.goldReward * 2.0f;
  1345. if(evaluationContext.heroRole == MAIN)
  1346. {
  1347. if(requiresBattle)
  1348. // Encourage MAIN to fight for crypts and similar
  1349. score *= 2;
  1350. else
  1351. // Discourage MAIN to waste time picking resources if they don't require a fight
  1352. score *= 0.33;
  1353. }
  1354. }
  1355. if(evaluationContext.skillReward > 0)
  1356. {
  1357. if(evaluationContext.heroRole == MAIN)
  1358. {
  1359. score = 1000 + evaluateSkillReward(score, evaluationContext.skillReward, evaluationContext.armyInvolvement, evaluationContext.armyLossRatio);
  1360. // Encourage skill increases before battles
  1361. if(!requiresBattle)
  1362. score *= 3;
  1363. }
  1364. else
  1365. // TODO: Mircea: Improve logic so that skill reward should be 0 for SCOUTs for one time things like a scholar, but allowed for buildings that give to all visiting heroes
  1366. // TODO: Mircea: Ease the restriction after 1 month or a bit more, because MAINs had enough time to grow, avoiding a SPAM of role shifts for each upgrade a SCOUT gets
  1367. // Discourage SCOUTs to pick-up skills/artifacts, otherwise it creates a mess with shifting MAIN responsibility.
  1368. // MAINs grow and fight, SCOUTs do the groundwork.
  1369. score = std::max(1.0f, score / 1000.0f);
  1370. }
  1371. score += evaluationContext.heroRole == MAIN ? evaluationContext.armyReward : evaluationContext.armyReward / 10.0f;
  1372. // workshop (free lvl 1 units for Tower) and similar dwellings receive both armyReward and armyGrowth in evaluationContext
  1373. // For that reason only getDwellingArmyGrowth gets amplified towards day 7 if units are lost after
  1374. // Hero exchange and army upgrade are using this too
  1375. score += evaluationContext.armyGrowth;
  1376. if(evaluationContext.goldCost > 0)
  1377. // Will be outside the if, just temporary for debugging
  1378. score -= evaluationContext.goldCost / 4.0f; // don't include the full cost of School of Magic or others because those locations are beneficial
  1379. score = evaluateArmyLossRatio(score, evaluationContext.armyLossRatio, evaluationContext.heroRole);
  1380. score *= evaluationContext.closestWayRatio;
  1381. score = evaluateMovement(score, evaluationContext.movementCost);
  1382. break;
  1383. }
  1384. case DEFEND: //Defend whatever if nothing else is to do
  1385. {
  1386. if (evaluationContext.enemyHeroDangerRatio > maxEnemyDangerRatio)
  1387. return 0;
  1388. if (evaluationContext.isDefend || evaluationContext.isArmyUpgrade)
  1389. score = evaluationContext.armyInvolvement;
  1390. score *= evaluationContext.closestWayRatio;
  1391. score = evaluateMovement(score, evaluationContext.movementCost);
  1392. break;
  1393. }
  1394. case BUILDINGS: //For buildings and buying army
  1395. {
  1396. // TODO: Mircea: What's the point of this check for ::BUILDINGS? Isn't the priority itself just for buildings? To test
  1397. if(maxWillingToLose - evaluationContext.armyLossRatio < 0)
  1398. return 0;
  1399. //If we already have locked resources, we don't look at other buildings
  1400. if(aiNk->getLockedResources().marketValue() > 0)
  1401. return 0;
  1402. // TODO: Mircea: See if evaluateConquestValue can be reused here as well, to test, don't want to disturb building logic
  1403. score += evaluationContext.conquestValue * 1000;
  1404. score += evaluationContext.strategicalValue * 1000;
  1405. score += evaluationContext.goldReward;
  1406. score = evaluateSkillReward(score, evaluationContext.skillReward, evaluationContext.armyInvolvement, evaluationContext.armyLossRatio);
  1407. score += evaluationContext.armyReward;
  1408. score += evaluationContext.armyGrowth;
  1409. if(evaluationContext.buildingCost.marketValue() > 0)
  1410. {
  1411. if(!evaluationContext.isTradeBuilding && aiNk->getFreeResources()[EGameResID::WOOD] - evaluationContext.buildingCost[EGameResID::WOOD] < 5
  1412. && aiNk->buildAnalyzer->getDailyIncome()[EGameResID::WOOD] < 1)
  1413. {
  1414. logAi->trace("priorityTier %d, Should make sure to build marketplace instead of %s", priorityTier, task->toString());
  1415. for(auto town : aiNk->cc->getTownsInfo())
  1416. {
  1417. if(!town->hasBuiltResourceMarketplace())
  1418. return 0;
  1419. }
  1420. }
  1421. score += 1000;
  1422. auto resourcesAvailable = evaluationContext.evaluator.aiNk->getFreeResources();
  1423. auto income = aiNk->buildAnalyzer->getDailyIncome();
  1424. // TODO: Mircea: Might want to use isGoldPressureOverMax or canAfford inside hunter gather as well if it's not already applied before
  1425. if(aiNk->buildAnalyzer->isGoldPressureOverMax())
  1426. score /= evaluationContext.buildingCost.marketValue();
  1427. if(!resourcesAvailable.canAfford(evaluationContext.buildingCost))
  1428. {
  1429. TResources needed = evaluationContext.buildingCost - resourcesAvailable;
  1430. needed.positive();
  1431. int turnsTo = needed.maxPurchasableCount(income);
  1432. bool haveEverythingButGold = true;
  1433. for(const GameResID & i : LIBRARY->resourceTypeHandler->getAllObjects())
  1434. {
  1435. if(i != GameResID::GOLD && resourcesAvailable[i] < evaluationContext.buildingCost[i])
  1436. haveEverythingButGold = false;
  1437. }
  1438. if(turnsTo == INT_MAX)
  1439. return 0;
  1440. if(!haveEverythingButGold)
  1441. score /= turnsTo;
  1442. }
  1443. }
  1444. else
  1445. {
  1446. if(evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend && vstd::isAlmostZero(evaluationContext.conquestValue))
  1447. return 0;
  1448. }
  1449. break;
  1450. }
  1451. default:
  1452. throw std::runtime_error("PriorityEvaluator::evaluate Unsupported priority: " + std::to_string(priorityTier));
  1453. }
  1454. result = score;
  1455. //TODO: Figure out the root cause for why evaluationContext.closestWayRatio has become -nan(ind).
  1456. if (std::isnan(result))
  1457. return 0;
  1458. }
  1459. #if NK2AI_TRACE_LEVEL >= 2
  1460. logAi->trace(
  1461. "priorityTier %d, Evaluated %s, armyLossRatio: %f, turn: %d, turns main: %f, turns scout: %f, armyInvolvement: %f, "
  1462. "goldReward: %f, goldRewardVsMovement: %f, goldCost: %d, armyReward: %f, armyGrowth: %f, skillReward: %f, danger: %d, threatTurns: %d, threat: %d, "
  1463. "heroRole: %s, strategicalValue: %f, conquestValue: %f, buildingCost.marketValue: %f, closestWayRatio: %f, enemyHeroDangerRatio: %f, "
  1464. "explorePriority: %d, isDefend: %d, isEnemy: %d, powerRatio: %f, result %f",
  1465. priorityTier,
  1466. task->toString(),
  1467. evaluationContext.armyLossRatio,
  1468. static_cast<int>(evaluationContext.turn),
  1469. evaluationContext.movementCostByRole[HeroRole::MAIN],
  1470. evaluationContext.movementCostByRole[HeroRole::SCOUT],
  1471. evaluationContext.armyInvolvement,
  1472. evaluationContext.goldReward,
  1473. goldRewardVsMovement,
  1474. evaluationContext.goldCost,
  1475. evaluationContext.armyReward,
  1476. evaluationContext.armyGrowth,
  1477. evaluationContext.skillReward,
  1478. evaluationContext.danger,
  1479. evaluationContext.threatTurns,
  1480. evaluationContext.threat,
  1481. evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout",
  1482. evaluationContext.strategicalValue,
  1483. evaluationContext.conquestValue,
  1484. evaluationContext.buildingCost.marketValue(),
  1485. evaluationContext.closestWayRatio,
  1486. evaluationContext.enemyHeroDangerRatio,
  1487. evaluationContext.explorePriority,
  1488. evaluationContext.isDefend,
  1489. evaluationContext.isEnemy,
  1490. evaluationContext.powerRatio,
  1491. result
  1492. );
  1493. #endif
  1494. return result;
  1495. }
  1496. }