BattleExchangeVariant.cpp 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023
  1. /*
  2. * BattleAI.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 "BattleExchangeVariant.h"
  12. #include "../../lib/CStack.h"
  13. AttackerValue::AttackerValue()
  14. : value(0),
  15. isRetaliated(false)
  16. {
  17. }
  18. MoveTarget::MoveTarget()
  19. : positions(), cachedAttack(), score(EvaluationResult::INEFFECTIVE_SCORE)
  20. {
  21. turnsToRich = 1;
  22. }
  23. float BattleExchangeVariant::trackAttack(
  24. const AttackPossibility & ap,
  25. std::shared_ptr<HypotheticBattle> hb,
  26. DamageCache & damageCache)
  27. {
  28. if(!ap.attackerState)
  29. {
  30. logAi->trace("Skipping fake ap attack");
  31. return 0;
  32. }
  33. auto attacker = hb->getForUpdate(ap.attack.attacker->unitId());
  34. float attackValue = ap.attackValue();
  35. auto affectedUnits = ap.affectedUnits;
  36. dpsScore.ourDamageReduce += ap.attackerDamageReduce + ap.collateralDamageReduce;
  37. dpsScore.enemyDamageReduce += ap.defenderDamageReduce + ap.shootersBlockedDmg;
  38. attackerValue[attacker->unitId()].value = attackValue;
  39. affectedUnits.push_back(ap.attackerState);
  40. for(auto affectedUnit : affectedUnits)
  41. {
  42. auto unitToUpdate = hb->getForUpdate(affectedUnit->unitId());
  43. auto damageDealt = unitToUpdate->getAvailableHealth() - affectedUnit->getAvailableHealth();
  44. if(damageDealt > 0)
  45. {
  46. unitToUpdate->damage(damageDealt);
  47. }
  48. if(unitToUpdate->unitSide() == attacker->unitSide())
  49. {
  50. if(unitToUpdate->unitId() == attacker->unitId())
  51. {
  52. unitToUpdate->afterAttack(ap.attack.shooting, false);
  53. #if BATTLE_TRACE_LEVEL>=1
  54. logAi->trace(
  55. "%s -> %s, ap retaliation, %s, dps: %lld",
  56. hb->getForUpdate(ap.attack.defender->unitId())->getDescription(),
  57. ap.attack.attacker->getDescription(),
  58. ap.attack.shooting ? "shot" : "mellee",
  59. damageDealt);
  60. #endif
  61. }
  62. else
  63. {
  64. #if BATTLE_TRACE_LEVEL>=1
  65. logAi->trace(
  66. "%s, ap collateral, dps: %lld",
  67. unitToUpdate->getDescription(),
  68. damageDealt);
  69. #endif
  70. }
  71. }
  72. else
  73. {
  74. if(unitToUpdate->unitId() == ap.attack.defender->unitId())
  75. {
  76. if(unitToUpdate->ableToRetaliate() && !affectedUnit->ableToRetaliate())
  77. {
  78. unitToUpdate->afterAttack(ap.attack.shooting, true);
  79. }
  80. #if BATTLE_TRACE_LEVEL>=1
  81. logAi->trace(
  82. "%s -> %s, ap attack, %s, dps: %lld",
  83. attacker->getDescription(),
  84. ap.attack.defender->getDescription(),
  85. ap.attack.shooting ? "shot" : "mellee",
  86. damageDealt);
  87. #endif
  88. }
  89. else
  90. {
  91. #if BATTLE_TRACE_LEVEL>=1
  92. logAi->trace(
  93. "%s, ap enemy collateral, dps: %lld",
  94. unitToUpdate->getDescription(),
  95. damageDealt);
  96. #endif
  97. }
  98. }
  99. }
  100. #if BATTLE_TRACE_LEVEL >= 1
  101. logAi->trace(
  102. "ap score: our: %2f, enemy: %2f, collateral: %2f, blocked: %2f",
  103. ap.attackerDamageReduce,
  104. ap.defenderDamageReduce,
  105. ap.collateralDamageReduce,
  106. ap.shootersBlockedDmg);
  107. #endif
  108. return attackValue;
  109. }
  110. float BattleExchangeVariant::trackAttack(
  111. std::shared_ptr<StackWithBonuses> attacker,
  112. std::shared_ptr<StackWithBonuses> defender,
  113. bool shooting,
  114. bool isOurAttack,
  115. DamageCache & damageCache,
  116. std::shared_ptr<HypotheticBattle> hb,
  117. bool evaluateOnly)
  118. {
  119. const std::string cachingStringBlocksRetaliation = "type_BLOCKS_RETALIATION";
  120. static const auto selectorBlocksRetaliation = Selector::type()(BonusType::BLOCKS_RETALIATION);
  121. const bool counterAttacksBlocked = attacker->hasBonus(selectorBlocksRetaliation, cachingStringBlocksRetaliation);
  122. int64_t attackDamage = damageCache.getDamage(attacker.get(), defender.get(), hb);
  123. float defenderDamageReduce = AttackPossibility::calculateDamageReduce(attacker.get(), defender.get(), attackDamage, damageCache, hb);
  124. float attackerDamageReduce = 0;
  125. if(!evaluateOnly)
  126. {
  127. #if BATTLE_TRACE_LEVEL>=1
  128. logAi->trace(
  129. "%s -> %s, normal attack, %s, dps: %lld, %2f",
  130. attacker->getDescription(),
  131. defender->getDescription(),
  132. shooting ? "shot" : "mellee",
  133. attackDamage,
  134. defenderDamageReduce);
  135. #endif
  136. if(isOurAttack)
  137. {
  138. dpsScore.enemyDamageReduce += defenderDamageReduce;
  139. attackerValue[attacker->unitId()].value += defenderDamageReduce;
  140. }
  141. else
  142. dpsScore.ourDamageReduce += defenderDamageReduce;
  143. defender->damage(attackDamage);
  144. attacker->afterAttack(shooting, false);
  145. }
  146. if(!evaluateOnly && defender->alive() && defender->ableToRetaliate() && !counterAttacksBlocked && !shooting)
  147. {
  148. auto retaliationDamage = damageCache.getDamage(defender.get(), attacker.get(), hb);
  149. attackerDamageReduce = AttackPossibility::calculateDamageReduce(defender.get(), attacker.get(), retaliationDamage, damageCache, hb);
  150. #if BATTLE_TRACE_LEVEL>=1
  151. logAi->trace(
  152. "%s -> %s, retaliation, dps: %lld, %2f",
  153. defender->getDescription(),
  154. attacker->getDescription(),
  155. retaliationDamage,
  156. attackerDamageReduce);
  157. #endif
  158. if(isOurAttack)
  159. {
  160. dpsScore.ourDamageReduce += attackerDamageReduce;
  161. attackerValue[attacker->unitId()].isRetaliated = true;
  162. }
  163. else
  164. {
  165. dpsScore.enemyDamageReduce += attackerDamageReduce;
  166. attackerValue[defender->unitId()].value += attackerDamageReduce;
  167. }
  168. attacker->damage(retaliationDamage);
  169. defender->afterAttack(false, true);
  170. }
  171. auto score = defenderDamageReduce - attackerDamageReduce;
  172. #if BATTLE_TRACE_LEVEL>=1
  173. if(!score)
  174. {
  175. logAi->trace("Attack has zero score def:%2f att:%2f", defenderDamageReduce, attackerDamageReduce);
  176. }
  177. #endif
  178. return score;
  179. }
  180. float BattleExchangeEvaluator::scoreValue(const BattleScore & score) const
  181. {
  182. return score.enemyDamageReduce * getPositiveEffectMultiplier() - score.ourDamageReduce * getNegativeEffectMultiplier();
  183. }
  184. EvaluationResult BattleExchangeEvaluator::findBestTarget(
  185. const battle::Unit * activeStack,
  186. PotentialTargets & targets,
  187. DamageCache & damageCache,
  188. std::shared_ptr<HypotheticBattle> hb)
  189. {
  190. EvaluationResult result(targets.bestAction());
  191. if(!activeStack->waited() && !activeStack->acquireState()->hadMorale)
  192. {
  193. #if BATTLE_TRACE_LEVEL>=1
  194. logAi->trace("Evaluating waited attack for %s", activeStack->getDescription());
  195. #endif
  196. auto hbWaited = std::make_shared<HypotheticBattle>(env.get(), hb);
  197. hbWaited->makeWait(activeStack);
  198. updateReachabilityMap(hbWaited);
  199. for(auto & ap : targets.possibleAttacks)
  200. {
  201. float score = evaluateExchange(ap, 0, targets, damageCache, hbWaited);
  202. if(score > result.score)
  203. {
  204. result.score = score;
  205. result.bestAttack = ap;
  206. result.wait = true;
  207. #if BATTLE_TRACE_LEVEL >= 1
  208. logAi->trace("New high score %2f", result.score);
  209. #endif
  210. }
  211. }
  212. }
  213. #if BATTLE_TRACE_LEVEL>=1
  214. logAi->trace("Evaluating normal attack for %s", activeStack->getDescription());
  215. #endif
  216. updateReachabilityMap(hb);
  217. if(result.bestAttack.attack.shooting
  218. && !result.bestAttack.defenderDead
  219. && !activeStack->waited()
  220. && hb->battleHasShootingPenalty(activeStack, result.bestAttack.dest))
  221. {
  222. if(!canBeHitThisTurn(result.bestAttack))
  223. return result; // lets wait
  224. }
  225. for(auto & ap : targets.possibleAttacks)
  226. {
  227. float score = evaluateExchange(ap, 0, targets, damageCache, hb);
  228. bool sameScoreButWaited = vstd::isAlmostEqual(score, result.score) && result.wait;
  229. if(score > result.score || sameScoreButWaited)
  230. {
  231. result.score = score;
  232. result.bestAttack = ap;
  233. result.wait = false;
  234. #if BATTLE_TRACE_LEVEL >= 1
  235. logAi->trace("New high score %2f", result.score);
  236. #endif
  237. }
  238. }
  239. return result;
  240. }
  241. ReachabilityInfo getReachabilityWithEnemyBypass(
  242. const battle::Unit * activeStack,
  243. DamageCache & damageCache,
  244. std::shared_ptr<HypotheticBattle> state)
  245. {
  246. ReachabilityInfo::Parameters params(activeStack, activeStack->getPosition());
  247. if(!params.flying)
  248. {
  249. for(const auto * unit : state->battleAliveUnits())
  250. {
  251. if(unit->unitSide() == activeStack->unitSide())
  252. continue;
  253. auto dmg = damageCache.getOriginalDamage(activeStack, unit, state);
  254. auto turnsToKill = unit->getAvailableHealth() / dmg;
  255. vstd::amin(turnsToKill, 100);
  256. for(auto & hex : unit->getHexes())
  257. if(hex.isAvailable()) //towers can have <0 pos; we don't also want to overwrite side columns
  258. params.destructibleEnemyTurns[hex] = turnsToKill * unit->getMovementRange();
  259. }
  260. params.bypassEnemyStacks = true;
  261. }
  262. return state->getReachability(params);
  263. }
  264. MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
  265. const battle::Unit * activeStack,
  266. PotentialTargets & targets,
  267. DamageCache & damageCache,
  268. std::shared_ptr<HypotheticBattle> hb)
  269. {
  270. MoveTarget result;
  271. BattleExchangeVariant ev;
  272. logAi->trace("Find move towards unreachable. Enemies count %d", targets.unreachableEnemies.size());
  273. if(targets.unreachableEnemies.empty())
  274. return result;
  275. auto speed = activeStack->getMovementRange();
  276. if(speed == 0)
  277. return result;
  278. updateReachabilityMap(hb);
  279. auto dists = getReachabilityWithEnemyBypass(activeStack, damageCache, hb);
  280. auto flying = activeStack->hasBonusOfType(BonusType::FLYING);
  281. for(const battle::Unit * enemy : targets.unreachableEnemies)
  282. {
  283. logAi->trace(
  284. "Checking movement towards %d of %s",
  285. enemy->getCount(),
  286. enemy->creatureId().toCreature()->getNameSingularTranslated());
  287. auto distance = dists.distToNearestNeighbour(activeStack, enemy);
  288. if(distance >= GameConstants::BFIELD_SIZE)
  289. continue;
  290. if(distance <= speed)
  291. continue;
  292. auto turnsToRich = (distance - 1) / speed + 1;
  293. auto hexes = enemy->getSurroundingHexes();
  294. auto enemySpeed = enemy->getMovementRange();
  295. auto speedRatio = speed / static_cast<float>(enemySpeed);
  296. auto multiplier = speedRatio > 1 ? 1 : speedRatio;
  297. for(auto & hex : hexes)
  298. {
  299. // FIXME: provide distance info for Jousting bonus
  300. auto bai = BattleAttackInfo(activeStack, enemy, 0, cb->battleCanShoot(activeStack));
  301. auto attack = AttackPossibility::evaluate(bai, hex, damageCache, hb);
  302. attack.shootersBlockedDmg = 0; // we do not want to count on it, it is not for sure
  303. auto score = calculateExchange(attack, turnsToRich, targets, damageCache, hb);
  304. score.enemyDamageReduce *= multiplier;
  305. #if BATTLE_TRACE_LEVEL >= 1
  306. logAi->trace("Multiplier: %f, turns: %d, current score %f, new score %f", multiplier, turnsToRich, result.score, scoreValue(score));
  307. #endif
  308. if(result.score < scoreValue(score)
  309. || (result.turnsToRich > turnsToRich && vstd::isAlmostEqual(result.score, scoreValue(score))))
  310. {
  311. result.score = scoreValue(score);
  312. result.positions.clear();
  313. #if BATTLE_TRACE_LEVEL >= 1
  314. logAi->trace("New high score");
  315. #endif
  316. for(const BattleHex & initialEnemyHex : enemy->getAttackableHexes(activeStack))
  317. {
  318. BattleHex enemyHex = initialEnemyHex;
  319. while(!flying && dists.distances[enemyHex] > speed && dists.predecessors.at(enemyHex).isValid())
  320. {
  321. enemyHex = dists.predecessors.at(enemyHex);
  322. if(dists.accessibility[enemyHex] == EAccessibility::ALIVE_STACK)
  323. {
  324. auto defenderToBypass = hb->battleGetUnitByPos(enemyHex);
  325. if(defenderToBypass)
  326. {
  327. #if BATTLE_TRACE_LEVEL >= 1
  328. logAi->trace("Found target to bypass at %d", enemyHex.hex);
  329. #endif
  330. auto attackHex = dists.predecessors[enemyHex];
  331. auto baiBypass = BattleAttackInfo(activeStack, defenderToBypass, 0, cb->battleCanShoot(activeStack));
  332. auto attackBypass = AttackPossibility::evaluate(baiBypass, attackHex, damageCache, hb);
  333. auto adjacentStacks = getAdjacentUnits(enemy);
  334. adjacentStacks.push_back(defenderToBypass);
  335. vstd::removeDuplicates(adjacentStacks);
  336. auto bypassScore = calculateExchange(
  337. attackBypass,
  338. dists.distances[attackHex],
  339. targets,
  340. damageCache,
  341. hb,
  342. adjacentStacks);
  343. if(scoreValue(bypassScore) > result.score)
  344. {
  345. result.score = scoreValue(bypassScore);
  346. #if BATTLE_TRACE_LEVEL >= 1
  347. logAi->trace("New high score after bypass %f", scoreValue(bypassScore));
  348. #endif
  349. }
  350. }
  351. }
  352. }
  353. result.positions.push_back(enemyHex);
  354. }
  355. result.cachedAttack = attack;
  356. result.turnsToRich = turnsToRich;
  357. }
  358. }
  359. }
  360. return result;
  361. }
  362. std::vector<const battle::Unit *> BattleExchangeEvaluator::getAdjacentUnits(const battle::Unit * blockerUnit) const
  363. {
  364. std::queue<const battle::Unit *> queue;
  365. std::vector<const battle::Unit *> checkedStacks;
  366. queue.push(blockerUnit);
  367. while(!queue.empty())
  368. {
  369. auto stack = queue.front();
  370. queue.pop();
  371. checkedStacks.push_back(stack);
  372. auto hexes = stack->getSurroundingHexes();
  373. for(auto hex : hexes)
  374. {
  375. auto neighbor = cb->battleGetUnitByPos(hex);
  376. if(neighbor && neighbor->unitSide() == stack->unitSide() && !vstd::contains(checkedStacks, neighbor))
  377. {
  378. queue.push(neighbor);
  379. checkedStacks.push_back(neighbor);
  380. }
  381. }
  382. }
  383. return checkedStacks;
  384. }
  385. ReachabilityData BattleExchangeEvaluator::getExchangeUnits(
  386. const AttackPossibility & ap,
  387. uint8_t turn,
  388. PotentialTargets & targets,
  389. std::shared_ptr<HypotheticBattle> hb,
  390. std::vector<const battle::Unit *> additionalUnits) const
  391. {
  392. ReachabilityData result;
  393. auto hexes = ap.attack.defender->getSurroundingHexes();
  394. if(!ap.attack.shooting) hexes.push_back(ap.from);
  395. std::vector<const battle::Unit *> allReachableUnits = additionalUnits;
  396. for(auto hex : hexes)
  397. {
  398. vstd::concatenate(allReachableUnits, turn == 0 ? reachabilityMap.at(hex) : getOneTurnReachableUnits(turn, hex));
  399. }
  400. for(auto hex : ap.attack.attacker->getHexes())
  401. {
  402. auto unitsReachingAttacker = turn == 0 ? reachabilityMap.at(hex) : getOneTurnReachableUnits(turn, hex);
  403. for(auto unit : unitsReachingAttacker)
  404. {
  405. if(unit->unitSide() != ap.attack.attacker->unitSide())
  406. {
  407. allReachableUnits.push_back(unit);
  408. result.enemyUnitsReachingAttacker.insert(unit->unitId());
  409. }
  410. }
  411. }
  412. vstd::removeDuplicates(allReachableUnits);
  413. auto copy = allReachableUnits;
  414. for(auto unit : copy)
  415. {
  416. for(auto adjacentUnit : getAdjacentUnits(unit))
  417. {
  418. auto unitWithBonuses = hb->battleGetUnitByID(adjacentUnit->unitId());
  419. if(vstd::contains(targets.unreachableEnemies, adjacentUnit)
  420. && !vstd::contains(allReachableUnits, unitWithBonuses))
  421. {
  422. allReachableUnits.push_back(unitWithBonuses);
  423. }
  424. }
  425. }
  426. vstd::removeDuplicates(allReachableUnits);
  427. if(!vstd::contains(allReachableUnits, ap.attack.attacker))
  428. {
  429. allReachableUnits.push_back(ap.attack.attacker);
  430. }
  431. if(allReachableUnits.size() < 2)
  432. {
  433. #if BATTLE_TRACE_LEVEL>=1
  434. logAi->trace("Reachability map contains only %d stacks", allReachableUnits.size());
  435. #endif
  436. return result;
  437. }
  438. for(auto unit : allReachableUnits)
  439. {
  440. auto accessible = !unit->canShoot() || vstd::contains(additionalUnits, unit);
  441. if(!accessible)
  442. {
  443. for(auto hex : unit->getSurroundingHexes())
  444. {
  445. if(ap.attack.defender->coversPos(hex))
  446. {
  447. accessible = true;
  448. }
  449. }
  450. }
  451. if(accessible)
  452. result.melleeAccessible.push_back(unit);
  453. else
  454. result.shooters.push_back(unit);
  455. }
  456. for(int turn = 0; turn < turnOrder.size(); turn++)
  457. {
  458. for(auto unit : turnOrder[turn])
  459. {
  460. if(vstd::contains(allReachableUnits, unit))
  461. result.units[turn].push_back(unit);
  462. }
  463. vstd::erase_if(result.units[turn], [&](const battle::Unit * u) -> bool
  464. {
  465. return !hb->battleGetUnitByID(u->unitId())->alive();
  466. });
  467. }
  468. return result;
  469. }
  470. float BattleExchangeEvaluator::evaluateExchange(
  471. const AttackPossibility & ap,
  472. uint8_t turn,
  473. PotentialTargets & targets,
  474. DamageCache & damageCache,
  475. std::shared_ptr<HypotheticBattle> hb) const
  476. {
  477. BattleScore score = calculateExchange(ap, turn, targets, damageCache, hb);
  478. #if BATTLE_TRACE_LEVEL >= 1
  479. logAi->trace(
  480. "calculateExchange score +%2f -%2fx%2f = %2f",
  481. score.enemyDamageReduce,
  482. score.ourDamageReduce,
  483. getNegativeEffectMultiplier(),
  484. scoreValue(score));
  485. #endif
  486. return scoreValue(score);
  487. }
  488. BattleScore BattleExchangeEvaluator::calculateExchange(
  489. const AttackPossibility & ap,
  490. uint8_t turn,
  491. PotentialTargets & targets,
  492. DamageCache & damageCache,
  493. std::shared_ptr<HypotheticBattle> hb,
  494. std::vector<const battle::Unit *> additionalUnits) const
  495. {
  496. #if BATTLE_TRACE_LEVEL>=1
  497. logAi->trace("Battle exchange at %d", ap.attack.shooting ? ap.dest.hex : ap.from.hex);
  498. #endif
  499. if(cb->battleGetMySide() == BattleSide::LEFT_SIDE
  500. && cb->battleGetGateState() == EGateState::BLOCKED
  501. && ap.attack.defender->coversPos(BattleHex::GATE_BRIDGE))
  502. {
  503. return BattleScore(EvaluationResult::INEFFECTIVE_SCORE, 0);
  504. }
  505. std::vector<const battle::Unit *> ourStacks;
  506. std::vector<const battle::Unit *> enemyStacks;
  507. if(hb->battleGetUnitByID(ap.attack.defender->unitId())->alive())
  508. enemyStacks.push_back(ap.attack.defender);
  509. ReachabilityData exchangeUnits = getExchangeUnits(ap, turn, targets, hb, additionalUnits);
  510. if(exchangeUnits.units.empty())
  511. {
  512. return BattleScore();
  513. }
  514. auto exchangeBattle = std::make_shared<HypotheticBattle>(env.get(), hb);
  515. BattleExchangeVariant v;
  516. for(int exchangeTurn = 0; exchangeTurn < exchangeUnits.units.size(); exchangeTurn++)
  517. {
  518. for(auto unit : exchangeUnits.units.at(exchangeTurn))
  519. {
  520. if(unit->isTurret())
  521. continue;
  522. bool isOur = exchangeBattle->battleMatchOwner(ap.attack.attacker, unit, true);
  523. auto & attackerQueue = isOur ? ourStacks : enemyStacks;
  524. auto u = exchangeBattle->getForUpdate(unit->unitId());
  525. if(u->alive() && !vstd::contains(attackerQueue, unit))
  526. {
  527. attackerQueue.push_back(unit);
  528. #if BATTLE_TRACE_LEVEL
  529. logAi->trace("Exchanging: %s", u->getDescription());
  530. #endif
  531. }
  532. }
  533. }
  534. auto melleeAttackers = ourStacks;
  535. vstd::removeDuplicates(melleeAttackers);
  536. vstd::erase_if(melleeAttackers, [&](const battle::Unit * u) -> bool
  537. {
  538. return cb->battleCanShoot(u);
  539. });
  540. bool canUseAp = true;
  541. std::set<uint32_t> blockedShooters;
  542. int totalTurnsCount = simulationTurnsCount >= turn + turnOrder.size()
  543. ? simulationTurnsCount
  544. : turn + turnOrder.size();
  545. for(int exchangeTurn = 0; exchangeTurn < simulationTurnsCount; exchangeTurn++)
  546. {
  547. bool isMovingTurm = exchangeTurn < turn;
  548. int queueTurn = exchangeTurn >= exchangeUnits.units.size()
  549. ? exchangeUnits.units.size() - 1
  550. : exchangeTurn;
  551. for(auto activeUnit : exchangeUnits.units.at(queueTurn))
  552. {
  553. bool isOur = exchangeBattle->battleMatchOwner(ap.attack.attacker, activeUnit, true);
  554. battle::Units & attackerQueue = isOur ? ourStacks : enemyStacks;
  555. battle::Units & oppositeQueue = isOur ? enemyStacks : ourStacks;
  556. auto attacker = exchangeBattle->getForUpdate(activeUnit->unitId());
  557. auto shooting = exchangeBattle->battleCanShoot(attacker.get())
  558. && !vstd::contains(blockedShooters, attacker->unitId());
  559. if(!attacker->alive())
  560. {
  561. #if BATTLE_TRACE_LEVEL>=1
  562. logAi->trace("Attacker is dead");
  563. #endif
  564. continue;
  565. }
  566. if(isMovingTurm && !shooting
  567. && !vstd::contains(exchangeUnits.enemyUnitsReachingAttacker, attacker->unitId()))
  568. {
  569. #if BATTLE_TRACE_LEVEL>=1
  570. logAi->trace("Attacker is moving");
  571. #endif
  572. continue;
  573. }
  574. auto targetUnit = ap.attack.defender;
  575. if(!isOur || !exchangeBattle->battleGetUnitByID(targetUnit->unitId())->alive())
  576. {
  577. #if BATTLE_TRACE_LEVEL>=2
  578. logAi->trace("Best target selector for %s", attacker->getDescription());
  579. #endif
  580. auto estimateAttack = [&](const battle::Unit * u) -> float
  581. {
  582. auto stackWithBonuses = exchangeBattle->getForUpdate(u->unitId());
  583. auto score = v.trackAttack(
  584. attacker,
  585. stackWithBonuses,
  586. exchangeBattle->battleCanShoot(stackWithBonuses.get()),
  587. isOur,
  588. damageCache,
  589. hb,
  590. true);
  591. #if BATTLE_TRACE_LEVEL>=2
  592. logAi->trace("Best target selector %s->%s score = %2f", attacker->getDescription(), stackWithBonuses->getDescription(), score);
  593. #endif
  594. return score;
  595. };
  596. auto unitsInOppositeQueueExceptInaccessible = oppositeQueue;
  597. vstd::erase_if(unitsInOppositeQueueExceptInaccessible, [&](const battle::Unit * u)->bool
  598. {
  599. return vstd::contains(exchangeUnits.shooters, u);
  600. });
  601. if(!isOur
  602. && exchangeTurn == 0
  603. && exchangeUnits.units.at(exchangeTurn).at(0)->unitId() != ap.attack.attacker->unitId()
  604. && !vstd::contains(exchangeUnits.enemyUnitsReachingAttacker, attacker->unitId()))
  605. {
  606. vstd::erase_if(unitsInOppositeQueueExceptInaccessible, [&](const battle::Unit * u) -> bool
  607. {
  608. return u->unitId() == ap.attack.attacker->unitId();
  609. });
  610. }
  611. if(!unitsInOppositeQueueExceptInaccessible.empty())
  612. {
  613. targetUnit = *vstd::maxElementByFun(unitsInOppositeQueueExceptInaccessible, estimateAttack);
  614. }
  615. else
  616. {
  617. auto reachable = exchangeBattle->battleGetUnitsIf([this, &exchangeBattle, &attacker](const battle::Unit * u) -> bool
  618. {
  619. if(u->unitSide() == attacker->unitSide())
  620. return false;
  621. if(!exchangeBattle->getForUpdate(u->unitId())->alive())
  622. return false;
  623. if(!u->getPosition().isValid())
  624. return false; // e.g. tower shooters
  625. return vstd::contains_if(reachabilityMap.at(u->getPosition()), [&attacker](const battle::Unit * other) -> bool
  626. {
  627. return attacker->unitId() == other->unitId();
  628. });
  629. });
  630. if(!reachable.empty())
  631. {
  632. targetUnit = *vstd::maxElementByFun(reachable, estimateAttack);
  633. }
  634. else
  635. {
  636. #if BATTLE_TRACE_LEVEL>=1
  637. logAi->trace("Battle queue is empty and no reachable enemy.");
  638. #endif
  639. continue;
  640. }
  641. }
  642. }
  643. auto defender = exchangeBattle->getForUpdate(targetUnit->unitId());
  644. const int totalAttacks = attacker->getTotalAttacks(shooting);
  645. if(canUseAp && activeUnit->unitId() == ap.attack.attacker->unitId()
  646. && targetUnit->unitId() == ap.attack.defender->unitId())
  647. {
  648. v.trackAttack(ap, exchangeBattle, damageCache);
  649. }
  650. else
  651. {
  652. for(int i = 0; i < totalAttacks; i++)
  653. {
  654. v.trackAttack(attacker, defender, shooting, isOur, damageCache, exchangeBattle);
  655. if(!attacker->alive() || !defender->alive())
  656. break;
  657. }
  658. }
  659. if(!shooting)
  660. blockedShooters.insert(defender->unitId());
  661. canUseAp = false;
  662. vstd::erase_if(attackerQueue, [&](const battle::Unit * u) -> bool
  663. {
  664. return !exchangeBattle->battleGetUnitByID(u->unitId())->alive();
  665. });
  666. vstd::erase_if(oppositeQueue, [&](const battle::Unit * u) -> bool
  667. {
  668. return !exchangeBattle->battleGetUnitByID(u->unitId())->alive();
  669. });
  670. }
  671. exchangeBattle->nextRound();
  672. }
  673. // avoid blocking path for stronger stack by weaker stack
  674. // the method checks if all stacks can be placed around enemy
  675. std::map<BattleHex, battle::Units> reachabilityMap;
  676. auto hexes = ap.attack.defender->getSurroundingHexes();
  677. for(auto hex : hexes)
  678. reachabilityMap[hex] = getOneTurnReachableUnits(turn, hex);
  679. auto score = v.getScore();
  680. if(simulationTurnsCount < totalTurnsCount)
  681. {
  682. float scalingRatio = simulationTurnsCount / static_cast<float>(totalTurnsCount);
  683. score.enemyDamageReduce *= scalingRatio;
  684. score.ourDamageReduce *= scalingRatio;
  685. }
  686. if(turn > 0)
  687. {
  688. auto turnMultiplier = 1 - std::min(0.2, 0.05 * turn);
  689. score.enemyDamageReduce *= turnMultiplier;
  690. }
  691. #if BATTLE_TRACE_LEVEL>=1
  692. logAi->trace("Exchange score: enemy: %2f, our -%2f", score.enemyDamageReduce, score.ourDamageReduce);
  693. #endif
  694. return score;
  695. }
  696. bool BattleExchangeEvaluator::canBeHitThisTurn(const AttackPossibility & ap)
  697. {
  698. for(auto pos : ap.attack.attacker->getSurroundingHexes())
  699. {
  700. for(auto u : reachabilityMap[pos])
  701. {
  702. if(u->unitSide() != ap.attack.attacker->unitSide())
  703. {
  704. return true;
  705. }
  706. }
  707. }
  708. return false;
  709. }
  710. void BattleExchangeEvaluator::updateReachabilityMap(std::shared_ptr<HypotheticBattle> hb)
  711. {
  712. const int TURN_DEPTH = 2;
  713. turnOrder.clear();
  714. hb->battleGetTurnOrder(turnOrder, std::numeric_limits<int>::max(), TURN_DEPTH);
  715. for(auto turn : turnOrder)
  716. {
  717. for(auto u : turn)
  718. {
  719. if(!vstd::contains(reachabilityCache, u->unitId()))
  720. {
  721. reachabilityCache[u->unitId()] = hb->getReachability(u);
  722. }
  723. }
  724. }
  725. for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1)
  726. {
  727. reachabilityMap[hex] = getOneTurnReachableUnits(0, hex);
  728. }
  729. }
  730. std::vector<const battle::Unit *> BattleExchangeEvaluator::getOneTurnReachableUnits(uint8_t turn, BattleHex hex) const
  731. {
  732. std::vector<const battle::Unit *> result;
  733. for(int i = 0; i < turnOrder.size(); i++, turn++)
  734. {
  735. auto & turnQueue = turnOrder[i];
  736. HypotheticBattle turnBattle(env.get(), cb);
  737. for(const battle::Unit * unit : turnQueue)
  738. {
  739. if(unit->isTurret())
  740. continue;
  741. if(turnBattle.battleCanShoot(unit))
  742. {
  743. result.push_back(unit);
  744. continue;
  745. }
  746. auto unitSpeed = unit->getMovementRange(turn);
  747. auto radius = unitSpeed * (turn + 1);
  748. auto reachabilityIter = reachabilityCache.find(unit->unitId());
  749. assert(reachabilityIter != reachabilityCache.end()); // missing updateReachabilityMap call?
  750. ReachabilityInfo unitReachability = reachabilityIter != reachabilityCache.end() ? reachabilityIter->second : turnBattle.getReachability(unit);
  751. bool reachable = unitReachability.distances.at(hex) <= radius;
  752. if(!reachable && unitReachability.accessibility[hex] == EAccessibility::ALIVE_STACK)
  753. {
  754. const battle::Unit * hexStack = cb->battleGetUnitByPos(hex);
  755. if(hexStack && cb->battleMatchOwner(unit, hexStack, false))
  756. {
  757. for(BattleHex neighbor : hex.neighbouringTiles())
  758. {
  759. reachable = unitReachability.distances.at(neighbor) <= radius;
  760. if(reachable) break;
  761. }
  762. }
  763. }
  764. if(reachable)
  765. {
  766. result.push_back(unit);
  767. }
  768. }
  769. }
  770. return result;
  771. }
  772. // avoid blocking path for stronger stack by weaker stack
  773. bool BattleExchangeEvaluator::checkPositionBlocksOurStacks(HypotheticBattle & hb, const battle::Unit * activeUnit, BattleHex position)
  774. {
  775. const int BLOCKING_THRESHOLD = 70;
  776. const int BLOCKING_OWN_ATTACK_PENALTY = 100;
  777. const int BLOCKING_OWN_MOVE_PENALTY = 1;
  778. float blockingScore = 0;
  779. auto activeUnitDamage = activeUnit->getMinDamage(hb.battleCanShoot(activeUnit)) * activeUnit->getCount();
  780. for(int turn = 0; turn < turnOrder.size(); turn++)
  781. {
  782. auto & turnQueue = turnOrder[turn];
  783. HypotheticBattle turnBattle(env.get(), cb);
  784. auto unitToUpdate = turnBattle.getForUpdate(activeUnit->unitId());
  785. unitToUpdate->setPosition(position);
  786. for(const battle::Unit * unit : turnQueue)
  787. {
  788. if(unit->unitId() == unitToUpdate->unitId() || cb->battleMatchOwner(unit, activeUnit, false))
  789. continue;
  790. auto blockedUnitDamage = unit->getMinDamage(hb.battleCanShoot(unit)) * unit->getCount();
  791. float ratio = blockedUnitDamage / (float)(blockedUnitDamage + activeUnitDamage + 0.01);
  792. auto unitReachability = turnBattle.getReachability(unit);
  793. auto unitSpeed = unit->getMovementRange(turn); // Cached value, to avoid performance hit
  794. for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1)
  795. {
  796. bool enemyUnit = false;
  797. bool reachable = unitReachability.distances.at(hex) <= unitSpeed;
  798. if(!reachable && unitReachability.accessibility[hex] == EAccessibility::ALIVE_STACK)
  799. {
  800. const battle::Unit * hexStack = turnBattle.battleGetUnitByPos(hex);
  801. if(hexStack && cb->battleMatchOwner(unit, hexStack, false))
  802. {
  803. enemyUnit = true;
  804. for(BattleHex neighbor : hex.neighbouringTiles())
  805. {
  806. reachable = unitReachability.distances.at(neighbor) <= unitSpeed;
  807. if(reachable) break;
  808. }
  809. }
  810. }
  811. if(!reachable && std::count(reachabilityMap[hex].begin(), reachabilityMap[hex].end(), unit) > 1)
  812. {
  813. blockingScore += ratio * (enemyUnit ? BLOCKING_OWN_ATTACK_PENALTY : BLOCKING_OWN_MOVE_PENALTY);
  814. }
  815. }
  816. }
  817. }
  818. #if BATTLE_TRACE_LEVEL>=1
  819. logAi->trace("Position %d, blocking score %f", position.hex, blockingScore);
  820. #endif
  821. return blockingScore > BLOCKING_THRESHOLD;
  822. }