2
0

DefenceBehavior.cpp 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. /*
  2. * DefenceBehavior.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 "DefenceBehavior.h"
  12. #include "../AIGateway.h"
  13. #include "../AIUtility.h"
  14. #include "../Behaviors/CaptureObjectsBehavior.h"
  15. #include "../Engine/Nullkiller.h"
  16. #include "../Goals/BuyArmy.h"
  17. #include "../Goals/Composition.h"
  18. #include "../Goals/DismissHero.h"
  19. #include "../Goals/ExchangeSwapTownHeroes.h"
  20. #include "../Goals/ExecuteHeroChain.h"
  21. #include "../Goals/RecruitHero.h"
  22. #include "../Markers/DefendTown.h"
  23. namespace NK2AI
  24. {
  25. const float THREAT_IGNORE_RATIO = 2;
  26. using namespace Goals;
  27. std::string DefenceBehavior::toString() const
  28. {
  29. return "Defend towns";
  30. }
  31. Goals::TGoalVec DefenceBehavior::decompose(const Nullkiller * aiNk) const
  32. {
  33. Goals::TGoalVec tasks;
  34. for(const auto town : aiNk->cc->getTownsInfo())
  35. {
  36. evaluateDefence(tasks, town, aiNk);
  37. }
  38. return tasks;
  39. }
  40. bool isThreatUnderControl(const CGTownInstance * town, const HitMapInfo & threat, const Nullkiller * aiNk, const std::vector<AIPath> & paths)
  41. {
  42. int dayOfWeek = aiNk->cc->getDate(Date::DAY_OF_WEEK);
  43. for(const AIPath & path : paths)
  44. {
  45. bool threatIsWeak = path.getHeroStrength() / (float)threat.danger > THREAT_IGNORE_RATIO;
  46. bool needToSaveGrowth = threat.turn == 0 && dayOfWeek == 7;
  47. if(threatIsWeak && !needToSaveGrowth)
  48. {
  49. if((path.exchangeCount == 1 && path.turn() < threat.turn) || path.turn() < threat.turn - 1 || (path.turn() < threat.turn && threat.turn >= 2))
  50. {
  51. #if NK2AI_TRACE_LEVEL >= 1
  52. logAi->trace(
  53. "Hero %s can eliminate danger for town %s using path %s.", path.targetHero->getObjectName(), town->getObjectName(), path.toString()
  54. );
  55. #endif
  56. return true;
  57. }
  58. }
  59. }
  60. return false;
  61. }
  62. void handleCounterAttack(
  63. const CGTownInstance * town,
  64. const HitMapInfo & threat,
  65. const HitMapInfo & maximumDanger,
  66. const Nullkiller * aiNk,
  67. Goals::TGoalVec & tasks
  68. )
  69. {
  70. if(threat.heroPtr.isVerified() && threat.turn <= 1 && (threat.danger == maximumDanger.danger || threat.turn < maximumDanger.turn))
  71. {
  72. auto heroCapturingPaths = aiNk->pathfinder->getPathInfo(threat.heroPtr->visitablePos());
  73. auto goals = CaptureObjectsBehavior::getVisitGoals(heroCapturingPaths, aiNk, threat.heroPtr.get());
  74. for(int i = 0; i < heroCapturingPaths.size(); i++)
  75. {
  76. AIPath & path = heroCapturingPaths[i];
  77. TSubgoal goal = goals[i];
  78. if(!goal || goal->invalid() || !goal->isElementar())
  79. continue;
  80. Composition composition;
  81. composition.addNext(DefendTown(town, threat, path, true)).addNext(goal);
  82. tasks.push_back(Goals::sptr(composition));
  83. }
  84. }
  85. }
  86. bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoalVec & tasks, const Nullkiller * aiNk)
  87. {
  88. if(aiNk->isHeroLocked(town->getGarrisonHero()))
  89. {
  90. logAi->trace("Hero %s in garrison of town %s is supposed to defend the town", town->getGarrisonHero()->getNameTranslated(), town->getNameTranslated());
  91. return true;
  92. }
  93. if(!town->getVisitingHero())
  94. {
  95. if(aiNk->cc->getHeroCount(aiNk->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER)
  96. {
  97. logAi->trace("Extracting hero %s from garrison of town %s", town->getGarrisonHero()->getNameTranslated(), town->getNameTranslated());
  98. tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5)));
  99. return false;
  100. }
  101. if(aiNk->heroManager->getHeroRoleOrDefaultInefficient(town->getGarrisonHero()) == HeroRole::MAIN)
  102. {
  103. auto armyDismissLimit = 1000;
  104. auto heroToDismiss = aiNk->heroManager->findWeakHeroToDismiss(armyDismissLimit);
  105. if(heroToDismiss)
  106. {
  107. tasks.push_back(Goals::sptr(Goals::DismissHero(heroToDismiss).setpriority(5)));
  108. return false;
  109. }
  110. }
  111. }
  112. return false;
  113. }
  114. void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town, const Nullkiller * aiNk) const
  115. {
  116. #if NK2AI_TRACE_LEVEL >= 1
  117. logAi->trace("Evaluating defence for %s", town->getNameTranslated());
  118. #endif
  119. auto threatNode = aiNk->dangerHitMap->getObjectThreat(town);
  120. std::vector<HitMapInfo> threats = aiNk->dangerHitMap->getTownThreats(town);
  121. // TODO: Mircea: Why don't we check if there's any danger in threadNode? Maybe map is still unexplored and no danger
  122. // or simply no one is around
  123. threats.push_back(threatNode.fastestDanger); // no guarantee that fastest danger will be there
  124. if(town->getGarrisonHero() && handleGarrisonHeroFromPreviousTurn(town, tasks, aiNk))
  125. return;
  126. if(!threatNode.fastestDanger.heroPtr.isVerified())
  127. {
  128. #if NK2AI_TRACE_LEVEL >= 1
  129. logAi->trace("No threat found for town %s", town->getNameTranslated());
  130. #endif
  131. return;
  132. }
  133. const uint64_t reinforcement = aiNk->armyManager->howManyReinforcementsCanBuy(town->getUpperArmy(), town);
  134. if(reinforcement)
  135. {
  136. #if NK2AI_TRACE_LEVEL >= 1
  137. logAi->trace("Town %s can buy defence army %lld", town->getNameTranslated(), reinforcement);
  138. #endif
  139. // TODO: Mircea: This won't have any money left because BuyArmyBehavior runs first and could have used all resources by now
  140. tasks.push_back(Goals::sptr(Goals::BuyArmy(town, reinforcement).setpriority(0.5f)));
  141. }
  142. auto paths = aiNk->pathfinder->getPathInfo(town->visitablePos());
  143. for(auto & threat : threats)
  144. {
  145. #if NK2AI_TRACE_LEVEL >= 1
  146. logAi->trace(
  147. "Town %s has threat %lld in %s turns, hero: %s",
  148. town->getNameTranslated(),
  149. threat.danger,
  150. std::to_string(threat.turn),
  151. threat.heroPtr.nameOrDefault()
  152. );
  153. #endif
  154. handleCounterAttack(town, threat, threatNode.maximumDanger, aiNk, tasks);
  155. if(isThreatUnderControl(town, threat, aiNk, paths))
  156. continue;
  157. evaluateRecruitingHero(tasks, threat, town, aiNk);
  158. if(paths.empty())
  159. {
  160. #if NK2AI_TRACE_LEVEL >= 1
  161. logAi->trace("No ways to defend town %s", town->getNameTranslated());
  162. #endif
  163. continue;
  164. }
  165. std::vector<int> pathsToDefend;
  166. std::map<const CGHeroInstance *, std::vector<int>> defferedPaths;
  167. AIPath * closestWay = nullptr;
  168. for(int i = 0; i < paths.size(); i++)
  169. {
  170. auto & path = paths[i];
  171. if(!closestWay || path.movementCost() < closestWay->movementCost())
  172. closestWay = &path;
  173. #if NK2AI_TRACE_LEVEL >= 1
  174. logAi->trace(
  175. "Hero %s can defend town with force %lld in %s turns, cost: %f, path: %s",
  176. path.targetHero->getObjectName(),
  177. path.getHeroStrength(),
  178. std::to_string(path.turn()),
  179. path.movementCost(),
  180. path.toString()
  181. );
  182. #endif
  183. auto townDefenseStrength = town->getGarrisonHero()
  184. ? town->getGarrisonHero()->getTotalStrength()
  185. : (town->getVisitingHero() ? town->getVisitingHero()->getTotalStrength() : town->getUpperArmy()->getArmyStrength());
  186. if(town->getVisitingHero() && path.targetHero == town->getVisitingHero())
  187. {
  188. if(path.getHeroStrength() < townDefenseStrength)
  189. continue;
  190. }
  191. else if(town->getGarrisonHero() && path.targetHero == town->getGarrisonHero())
  192. {
  193. if(path.getHeroStrength() < townDefenseStrength)
  194. continue;
  195. }
  196. if(path.turn() <= threat.turn - 2)
  197. {
  198. #if NK2AI_TRACE_LEVEL >= 1
  199. logAi->trace(
  200. "Defer defence of %s by %s because he has enough time to reach the town next turn", town->getObjectName(), path.targetHero->getObjectName()
  201. );
  202. #endif
  203. defferedPaths[path.targetHero].push_back(i);
  204. continue;
  205. }
  206. if(!path.targetHero->canBeMergedWith(*town))
  207. {
  208. #if NK2AI_TRACE_LEVEL >= 1
  209. logAi->trace("Can't merge armies of hero %s and town %s", path.targetHero->getObjectName(), town->getObjectName());
  210. #endif
  211. continue;
  212. }
  213. if(path.targetHero == town->getVisitingHero() && path.exchangeCount == 1)
  214. {
  215. #if NK2AI_TRACE_LEVEL >= 1
  216. logAi->trace("Put %s to garrison of town %s", path.targetHero->getObjectName(), town->getObjectName());
  217. #endif
  218. // dismiss creatures we are not able to pick to be able to hide in garrison
  219. if(town->getGarrisonHero() || town->getUpperArmy()->stacksCount() == 0 || path.targetHero->canBeMergedWith(*town)
  220. || (town->getUpperArmy()->getArmyStrength() < 500 && town->fortLevel() >= CGTownInstance::CITADEL))
  221. {
  222. tasks.push_back(
  223. Goals::sptr(
  224. Composition()
  225. .addNext(DefendTown(town, threat, path.targetHero))
  226. .addNext(ExchangeSwapTownHeroes(town, town->getVisitingHero(), HeroLockedReason::DEFENCE))
  227. )
  228. );
  229. }
  230. continue;
  231. }
  232. // main without army and visiting scout with army, very specific case
  233. if(town->getVisitingHero() && town->getUpperArmy()->stacksCount() == 0 && path.targetHero != town->getVisitingHero() && path.exchangeCount == 1
  234. && path.turn() == 0 && aiNk->heroManager->evaluateHero(path.targetHero) > aiNk->heroManager->evaluateHero(town->getVisitingHero())
  235. && 10 * path.targetHero->getTotalStrength() < town->getVisitingHero()->getTotalStrength())
  236. {
  237. path.heroArmy = town->getVisitingHero();
  238. tasks.push_back(
  239. Goals::sptr(
  240. Composition()
  241. .addNext(DefendTown(town, threat, path))
  242. .addNextSequence(
  243. {sptr(ExchangeSwapTownHeroes(town, town->getVisitingHero())),
  244. sptr(ExecuteHeroChain(path, town)),
  245. sptr(ExchangeSwapTownHeroes(town, path.targetHero, HeroLockedReason::DEFENCE))}
  246. )
  247. )
  248. );
  249. continue;
  250. }
  251. if(threat.turn == 0 || (path.turn() <= threat.turn && path.getHeroStrength() * aiNk->settings->getSafeAttackRatio() >= threat.danger))
  252. {
  253. if(aiNk->arePathHeroesLocked(path))
  254. {
  255. #if NK2AI_TRACE_LEVEL >= 1
  256. logAi->trace("Can not move %s to defend town %s. Path is locked.", path.targetHero->getObjectName(), town->getObjectName());
  257. #endif
  258. continue;
  259. }
  260. pathsToDefend.push_back(i);
  261. }
  262. }
  263. for(int i : pathsToDefend)
  264. {
  265. AIPath & path = paths[i];
  266. for(int j : defferedPaths[path.targetHero])
  267. {
  268. AIPath & defferedPath = paths[j];
  269. if(defferedPath.getHeroStrength() >= path.getHeroStrength() && defferedPath.turn() <= path.turn())
  270. {
  271. continue; // TODO: Mircea: Should it be break instead? Or continue for the outside loop?
  272. }
  273. }
  274. Composition composition;
  275. composition.addNext(DefendTown(town, threat, path));
  276. TGoalVec sequence;
  277. if(town->getGarrisonHero() && path.targetHero == town->getGarrisonHero() && path.exchangeCount == 1)
  278. {
  279. composition.addNext(ExchangeSwapTownHeroes(town, town->getGarrisonHero(), HeroLockedReason::DEFENCE));
  280. tasks.push_back(Goals::sptr(composition));
  281. #if NK2AI_TRACE_LEVEL >= 1
  282. logAi->trace("Locking hero %s in garrison of %s", town->getGarrisonHero()->getObjectName(), town->getObjectName());
  283. #endif
  284. continue;
  285. }
  286. if(town->getVisitingHero() && path.targetHero != town->getVisitingHero() && !path.containsHero(town->getVisitingHero()))
  287. {
  288. if(town->getGarrisonHero() && town->getGarrisonHero() != path.targetHero)
  289. {
  290. #if NK2AI_TRACE_LEVEL >= 1
  291. logAi->trace("Cancel moving %s to defend town %s as the town has garrison hero", path.targetHero->getObjectName(), town->getObjectName());
  292. #endif
  293. continue;
  294. }
  295. if(path.turn() == 0)
  296. {
  297. sequence.push_back(sptr(ExchangeSwapTownHeroes(town, town->getVisitingHero())));
  298. }
  299. }
  300. #if NK2AI_TRACE_LEVEL >= 1
  301. logAi->trace("Move %s to defend town %s", path.targetHero->getObjectName(), town->getObjectName());
  302. #endif
  303. ExecuteHeroChain heroChain(path, town);
  304. if(closestWay)
  305. {
  306. heroChain.closestWayRatio = closestWay->movementCost() / heroChain.getPath().movementCost();
  307. }
  308. sequence.push_back(sptr(heroChain));
  309. composition.addNextSequence(sequence);
  310. const auto firstBlockedAction = path.getFirstBlockedAction();
  311. if(firstBlockedAction)
  312. {
  313. auto subGoal = firstBlockedAction->decompose(aiNk, path.targetHero);
  314. #if NK2AI_TRACE_LEVEL >= 2
  315. logAi->trace("Decomposing special action %s returns %s", firstBlockedAction->toString(), subGoal->toString());
  316. #endif
  317. if(subGoal->invalid())
  318. {
  319. #if NK2AI_TRACE_LEVEL >= 1
  320. logAi->trace("Path is invalid. Skipping");
  321. #endif
  322. continue;
  323. }
  324. composition.addNext(subGoal);
  325. }
  326. tasks.push_back(Goals::sptr(composition));
  327. }
  328. }
  329. logAi->debug("Found %d tasks", tasks.size());
  330. }
  331. void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & threat, const CGTownInstance * town, const Nullkiller * aiNk)
  332. {
  333. // TODO: Mircea: Shouldn't it be threat.turn < 1? How does the current one make sense?
  334. if(threat.turn > 0 || town->getGarrisonHero() || town->getVisitingHero())
  335. return;
  336. // TODO: Mircea: Replace with aiNk->heroManager->canRecruitHero(town) but skip limit?
  337. if(town->hasBuilt(BuildingID::TAVERN) && aiNk->cc->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST)
  338. {
  339. const auto heroesInTavern = aiNk->cc->getAvailableHeroes(town);
  340. for(auto hero : heroesInTavern)
  341. {
  342. // TODO: Mircea: Investigate if this logic might be off, as the attacker will most probably be more powerful than a tavern hero
  343. // A new hero improves the defence strength of town's army if it has defence > 0 in primary skills
  344. if(hero->getTotalStrength() < threat.danger)
  345. continue;
  346. bool heroAlreadyHiredInOtherTown = false;
  347. for(const auto & task : tasks)
  348. {
  349. if(auto * const recruitGoal = dynamic_cast<Goals::RecruitHero *>(task.get()))
  350. {
  351. if(recruitGoal->getHero() == hero)
  352. {
  353. heroAlreadyHiredInOtherTown = true;
  354. break;
  355. }
  356. }
  357. }
  358. if(heroAlreadyHiredInOtherTown)
  359. continue;
  360. auto myHeroes = aiNk->cc->getHeroesInfo();
  361. #if NK2AI_TRACE_LEVEL >= 1
  362. logAi->trace("Hero %s can be recruited to defend %s", hero->getObjectName(), town->getObjectName());
  363. #endif
  364. bool needSwap = false;
  365. const CGHeroInstance * heroToDismiss = nullptr;
  366. if(town->getVisitingHero())
  367. {
  368. if(!town->getGarrisonHero())
  369. needSwap = true;
  370. else
  371. {
  372. if(town->getVisitingHero()->getArmyStrength() < town->getGarrisonHero()->getArmyStrength())
  373. {
  374. if(town->getVisitingHero()->getArmyStrength() >= hero->getArmyStrength())
  375. continue;
  376. heroToDismiss = town->getVisitingHero();
  377. }
  378. else if(town->getGarrisonHero()->getArmyStrength() >= hero->getArmyStrength())
  379. continue;
  380. else
  381. {
  382. needSwap = true;
  383. heroToDismiss = town->getGarrisonHero();
  384. }
  385. }
  386. // avoid dismissing one weak hero in order to recruit another.
  387. // TODO: Mircea: Move to constant
  388. if(heroToDismiss && heroToDismiss->getArmyStrength() + 500 > hero->getArmyStrength())
  389. continue;
  390. }
  391. // TODO: Mircea: Check if it immediately dismisses after losing a castle, though that implies losing a hero too if present in the castle
  392. else if(aiNk->heroManager->heroCapReached())
  393. {
  394. heroToDismiss = aiNk->heroManager->findWeakHeroToDismiss(hero->getArmyStrength(), town);
  395. if(!heroToDismiss)
  396. continue;
  397. }
  398. TGoalVec sequence;
  399. Goals::Composition recruitHeroComposition;
  400. if(needSwap)
  401. sequence.push_back(sptr(ExchangeSwapTownHeroes(town, town->getVisitingHero())));
  402. if(heroToDismiss)
  403. sequence.push_back(sptr(DismissHero(heroToDismiss)));
  404. sequence.push_back(sptr(Goals::RecruitHero(town, hero)));
  405. tasks.push_back(sptr(Goals::Composition().addNext(DefendTown(town, threat, hero)).addNextSequence(sequence)));
  406. }
  407. }
  408. }
  409. }