BattleExchangeVariant.cpp 25 KB

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