battlefield.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. /*
  2. * battlefield.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 "battle/BattleHex.h"
  12. #include "battle/IBattleInfoCallback.h"
  13. #include "battle/ReachabilityInfo.h"
  14. #include "BAI/v13/battlefield.h"
  15. #include "BAI/v13/hex.h"
  16. #include "common.h"
  17. #include <algorithm>
  18. #include <memory>
  19. #include <ranges>
  20. namespace MMAI::BAI::V13
  21. {
  22. using HA = HexAttribute;
  23. using SA = StackAttribute;
  24. using LT = LinkType;
  25. // A custom hash function must be provided for the adjmap
  26. struct PairHash
  27. {
  28. std::size_t operator()(const std::pair<si16, si16> & t) const
  29. {
  30. auto h0 = std::hash<int>{}(std::get<0>(t));
  31. auto h1 = std::hash<int>{}(std::get<1>(t));
  32. return h0 ^ (h1 << 1);
  33. }
  34. };
  35. namespace
  36. {
  37. std::unordered_map<std::pair<si16, si16>, bool, PairHash> InitAdjMap()
  38. {
  39. auto res = std::unordered_map<std::pair<si16, si16>, bool, PairHash>{};
  40. for(int id1 = 0; id1 < GameConstants::BFIELD_SIZE; id1++)
  41. {
  42. auto hex1 = BattleHex(id1);
  43. for(auto dir : BattleHex::hexagonalDirections())
  44. {
  45. auto hex2 = hex1.cloneInDirection(dir, false);
  46. res[{hex1.toInt(), hex2.toInt()}] = true;
  47. }
  48. }
  49. return res;
  50. }
  51. }
  52. Battlefield::Battlefield(const std::shared_ptr<Hexes> & hexes_, const Stacks & stacks_, const AllLinks & allLinks_, const Stack * astack_)
  53. : hexes(hexes_), stacks(stacks_), allLinks(allLinks_), astack(astack_) {};
  54. // static
  55. std::shared_ptr<const Battlefield> Battlefield::Create(
  56. const CPlayerBattleCallback * battle,
  57. const CStack * acstack,
  58. const GlobalStats * oldgstats,
  59. const GlobalStats * gstats,
  60. std::map<const CStack *, Stack::Stats> & stacksStats,
  61. bool isMorale
  62. )
  63. {
  64. auto [stacks, queue] = InitStacks(battle, acstack, oldgstats, gstats, stacksStats, isMorale);
  65. auto [hexes, astack] = InitHexes(battle, acstack, stacks);
  66. auto links = InitAllLinks(battle, stacks, queue, hexes);
  67. return std::make_shared<const Battlefield>(hexes, stacks, links, astack);
  68. }
  69. // static
  70. // result is a vector<UnitID>
  71. // XXX: there is a bug in VCMI when high morale occurs:
  72. // - the stack acts as if it's already the next unit's turn
  73. // - as a result, QueuePos for the ACTIVE stack is non-0
  74. // while the QueuePos for the next (non-active) stack is 0
  75. // (this applies only to good morale; bad morale simply skips turn)
  76. // As a workaround, a "isMorale" flag is passed whenever the astack is
  77. // acting because of high morale and queue is "shifted" accordingly.
  78. Queue Battlefield::GetQueue(const CPlayerBattleCallback * battle, const CStack * astack, bool isMorale)
  79. {
  80. auto res = Queue{};
  81. auto tmp = std::vector<battle::Units>{};
  82. battle->battleGetTurnOrder(tmp, S13::STACK_QUEUE_SIZE, 0);
  83. for(auto & units : tmp)
  84. {
  85. for(auto & unit : units)
  86. {
  87. if(res.size() < S13::STACK_QUEUE_SIZE)
  88. res.push_back(unit->unitId());
  89. else
  90. break;
  91. }
  92. }
  93. // XXX: after morale, battleGetTurnOrder() returns wrong order
  94. // (where a non-active stack is first)
  95. // The active stack *must* be first-in-queue
  96. if(isMorale && astack && res.at(0) != astack->unitId())
  97. {
  98. // logAi->debug("Morale triggered -- will rearrange stack queue");
  99. std::rotate(res.rbegin(), res.rbegin() + 1, res.rend());
  100. res.at(0) = astack->unitId();
  101. }
  102. else
  103. {
  104. // the only scenario where the active stack is not first in queue
  105. // is at battle end (i.e. no active stack)
  106. // assert(astack == nullptr || res.at(0) == astack->unitId());
  107. ASSERT(astack == nullptr || res.at(0) == astack->unitId(), "queue[0] is not the currently active stack!");
  108. }
  109. return res;
  110. }
  111. // static
  112. std::tuple<std::shared_ptr<Hexes>, Stack *> Battlefield::InitHexes(const CPlayerBattleCallback * battle, const CStack * acstack, const Stacks & stacks)
  113. {
  114. auto res = std::make_shared<Hexes>();
  115. auto ainfo = battle->getAccessibility();
  116. auto hexstacks = std::map<BattleHex, std::shared_ptr<Stack>>{};
  117. auto hexobstacles = std::array<std::vector<std::shared_ptr<const CObstacleInstance>>, 165>{};
  118. std::shared_ptr<ActiveStackInfo> astackinfo = nullptr;
  119. Stack * astack = nullptr;
  120. for(const auto & stack : stacks)
  121. {
  122. for(const auto & bh : stack->cstack->getHexes())
  123. if(bh.isAvailable())
  124. hexstacks.try_emplace(bh, stack);
  125. // XXX: at battle_end, stack->cstack != acstack even if qpos=0
  126. if((stack->attr(SA::QUEUE) & 1) && acstack)
  127. astack = stack.get();
  128. }
  129. for(const auto & obstacle : battle->battleGetAllObstacles())
  130. for(const auto & bh : obstacle->getAffectedTiles())
  131. if(bh.isAvailable())
  132. hexobstacles.at(Hex::CalcId(bh)).push_back(obstacle);
  133. if(astack)
  134. {
  135. // astack can be nullptr if battle just begun (no turns yet)
  136. astackinfo = std::make_shared<ActiveStackInfo>(astack, battle->battleCanShoot(astack->cstack), std::make_shared<ReachabilityInfo>(astack->rinfo));
  137. }
  138. auto gatestate = battle->battleGetGateState();
  139. for(int y = 0; y < 11; ++y)
  140. {
  141. for(int x = 0; x < 15; ++x)
  142. {
  143. auto i = (y * 15) + x;
  144. auto bh = BattleHex(x + 1, y);
  145. res->at(y).at(x) = std::make_unique<Hex>(bh, ainfo.at(bh.toInt()), gatestate, hexobstacles.at(i), hexstacks, astackinfo);
  146. }
  147. }
  148. // XXX: astack can be nullptr (even if acstack is not) -- see above
  149. return {res, astack};
  150. };
  151. // static
  152. std::tuple<Stacks, Queue> Battlefield::InitStacks(
  153. const CPlayerBattleCallback * battle,
  154. const CStack * astack,
  155. const GlobalStats * oldgstats,
  156. const GlobalStats * gstats,
  157. std::map<const CStack *, Stack::Stats> & stacksStats,
  158. bool isMorale
  159. )
  160. {
  161. auto stacks = Stacks{};
  162. auto cstacks = battle->battleGetStacks();
  163. // Sorting needed to ensure ordered insertion of summons/machines
  164. std::ranges::sort(
  165. cstacks,
  166. [](const CStack * a, const CStack * b)
  167. {
  168. return a->unitId() < b->unitId();
  169. }
  170. );
  171. /*
  172. * Units for each side are indexed as follows:
  173. *
  174. * 1. The 7 "regular" army stacks use indexes 0..6 (index=slot)
  175. * 2. Up to N* summoned units will use indexes 7+ (ordered by unit ID)
  176. * 3. Up to N* war machines will use FREE indexes 7+, if any (ordered by unit ID).
  177. * 4. Remaining units from 2. and 3. will use FREE indexes from 1, if any (ordered by unit ID).
  178. * 5. Remaining units from 4. will be ignored.
  179. */
  180. auto queue = GetQueue(battle, astack, isMorale);
  181. auto summons = std::array<std::deque<const CStack *>, 2>{};
  182. auto machines = std::array<std::deque<const CStack *>, 2>{};
  183. auto blocking = std::map<const CStack *, bool>{};
  184. auto blocked = std::map<const CStack *, bool>{};
  185. auto setBlockedBlocking = [&battle, &blocked, &blocking](const CStack * cstack)
  186. {
  187. blocked.emplace(cstack, false);
  188. blocking.emplace(cstack, false);
  189. for(const auto * adjacent : battle->battleAdjacentUnits(cstack))
  190. {
  191. if(adjacent->unitOwner() == cstack->unitOwner())
  192. continue;
  193. if(!blocked[cstack] && cstack->canShoot() && !cstack->hasBonusOfType(BonusType::FREE_SHOOTING) && !cstack->hasBonusOfType(BonusType::SIEGE_WEAPON))
  194. {
  195. blocked[cstack] = true;
  196. }
  197. if(!blocking[cstack] && adjacent->canShoot() && !adjacent->hasBonusOfType(BonusType::FREE_SHOOTING)
  198. && !adjacent->hasBonusOfType(BonusType::SIEGE_WEAPON))
  199. {
  200. blocking[cstack] = true;
  201. }
  202. }
  203. };
  204. // estimated dmg by active stack
  205. // values are for ranged attack if unit is an unblocked shooter
  206. // otherwise for melee attack
  207. auto estdmg = std::map<const CStack *, DamageEstimation>{};
  208. auto estimateDamage = [&battle, &astack, &estdmg, &blocked](const CStack * cstack)
  209. {
  210. if(!astack)
  211. {
  212. // no active stack (e.g. called during battleStart or battleEnd)
  213. estdmg.try_emplace(cstack);
  214. }
  215. else if(astack->unitSide() == cstack->unitSide())
  216. {
  217. // no damage to friendly units
  218. estdmg.try_emplace(cstack);
  219. }
  220. else
  221. {
  222. const auto attinfo = BattleAttackInfo(astack, cstack, 0, astack->canShoot() && !blocked[astack]);
  223. estdmg.try_emplace(cstack, battle->calculateDmgRange(attinfo));
  224. }
  225. };
  226. // This must be pre-set as dmg estimation depends on it
  227. if(astack)
  228. setBlockedBlocking(astack);
  229. for(auto & cstack : cstacks)
  230. {
  231. if(cstack != astack)
  232. setBlockedBlocking(cstack);
  233. estimateDamage(cstack);
  234. auto stack = std::make_shared<Stack>(
  235. cstack,
  236. queue,
  237. // a blank stackStats entry is created if missing
  238. Stack::StatsContainer{.oldgstats = oldgstats, .gstats = gstats, .stackStats = stacksStats[cstack]},
  239. battle->getReachability(cstack),
  240. blocked[cstack],
  241. blocking[cstack],
  242. estdmg[cstack]
  243. );
  244. stacks.push_back(stack);
  245. }
  246. return {stacks, queue};
  247. }
  248. // static
  249. AllLinks Battlefield::InitAllLinks(const CPlayerBattleCallback * battle, const Stacks & stacks, const Queue & queue, std::shared_ptr<Hexes> & hexes)
  250. {
  251. auto allLinks = AllLinks();
  252. for(auto i = 0; i < EI(LT::_count); ++i)
  253. allLinks[static_cast<LT>(i)] = std::make_shared<Links>();
  254. for(auto & srcrow : *hexes)
  255. {
  256. for(auto & srchex : srcrow)
  257. {
  258. for(auto & dstrow : *hexes)
  259. {
  260. for(auto & dsthex : dstrow)
  261. {
  262. LinkTwoHexes(allLinks, battle, stacks, queue, srchex.get(), dsthex.get());
  263. }
  264. }
  265. }
  266. }
  267. return allLinks;
  268. }
  269. namespace
  270. {
  271. float calculateRangeMod(const CPlayerBattleCallback * battle, const CStack * cstack, const BattleHex & src, const BattleHex & dst)
  272. {
  273. float rangemod = 1;
  274. if(battle->battleHasDistancePenalty(cstack, src, dst))
  275. rangemod *= 0.5;
  276. if(battle->battleHasWallPenalty(cstack, src, dst))
  277. rangemod *= 0.5;
  278. return rangemod;
  279. }
  280. }
  281. void Battlefield::LinkTwoHexes(
  282. AllLinks & allLinks,
  283. const CPlayerBattleCallback * battle,
  284. const Stacks & stacks,
  285. const Queue & queue,
  286. const Hex * src,
  287. const Hex * dst
  288. )
  289. {
  290. static const auto adjmap = InitAdjMap();
  291. bool neighbour = adjmap.contains({src->bhex.toInt(), dst->bhex.toInt()});
  292. bool reachable = false;
  293. float rangemod = 0;
  294. float rangedDmgFrac = 0;
  295. float meleeDmgFrac = 0;
  296. float retalDmgFrac = 0;
  297. int actsBefore = 0;
  298. if(src->stack && !src->getAttr(HA::IS_REAR) && !src->stack->flag(StackFlag1::SLEEPING))
  299. {
  300. reachable = src->stack->rinfo.distances.at(dst->bhex.toInt()) <= src->stack->attr(SA::SPEED);
  301. // rangemod is set even if dst is free
  302. if(src->stack->cstack->canShoot() && !src->stack->cstack->coversPos(dst->bhex) && !src->stack->flag(StackFlag1::BLOCKED) && !neighbour)
  303. {
  304. rangemod = calculateRangeMod(battle, src->stack->cstack, src->bhex, dst->bhex);
  305. }
  306. // *dmgFracs are set only between opposing stacks
  307. if(dst->stack && (dst->stack->cstack->unitSide() != src->stack->cstack->unitSide()))
  308. {
  309. if(rangemod > 0)
  310. {
  311. auto estdmg = battle->calculateDmgRange(BattleAttackInfo(src->stack->cstack, dst->stack->cstack, 0, true));
  312. auto avgdmg = 0.5 * (estdmg.damage.max + estdmg.damage.min);
  313. // negate the rangemod in the dmg calc (i.e. report the "base" dmg)
  314. avgdmg *= 1 / rangemod;
  315. rangedDmgFrac = avgdmg / dst->stack->cstack->getAvailableHealth();
  316. }
  317. auto bai = BattleAttackInfo(src->stack->cstack, dst->stack->cstack, 0, false);
  318. auto retdmg = DamageEstimation{};
  319. auto estdmg = battle->battleEstimateDamage(bai, &retdmg);
  320. auto avgdmg = 0.5 * (estdmg.damage.max + estdmg.damage.min);
  321. meleeDmgFrac = avgdmg / dst->stack->cstack->getAvailableHealth();
  322. if(retdmg.damage.max > 0)
  323. {
  324. auto avgret = 0.5 * (retdmg.damage.max + retdmg.damage.min);
  325. retalDmgFrac = avgret / src->stack->cstack->getAvailableHealth();
  326. }
  327. }
  328. }
  329. if(src->stack && dst->stack && src->id != dst->id)
  330. {
  331. auto srcpos = src->stack->qposFirst;
  332. auto dstpos = dst->stack->qposFirst;
  333. if(srcpos < dstpos)
  334. {
  335. ASSERT(dstpos <= queue.size(), "dstpos exceeds queue size");
  336. actsBefore = true;
  337. }
  338. }
  339. //
  340. // Build links
  341. //
  342. if(neighbour)
  343. allLinks[LT::ADJACENT]->add(src->id, dst->id, 1);
  344. if(reachable)
  345. allLinks[LT::REACH]->add(src->id, dst->id, 1);
  346. if(actsBefore)
  347. allLinks[LT::ACTS_BEFORE]->add(src->id, dst->id, std::min<int>(2, actsBefore));
  348. if(rangemod)
  349. allLinks[LT::RANGED_MOD]->add(src->id, dst->id, std::min<float>(2, rangemod));
  350. if(rangedDmgFrac)
  351. allLinks[LT::RANGED_DMG_REL]->add(src->id, dst->id, std::min<float>(2, rangedDmgFrac));
  352. if(meleeDmgFrac)
  353. allLinks[LT::MELEE_DMG_REL]->add(src->id, dst->id, std::min<float>(2, meleeDmgFrac));
  354. if(retalDmgFrac)
  355. allLinks[LT::RETAL_DMG_REL]->add(src->id, dst->id, std::min<float>(2, retalDmgFrac));
  356. }
  357. }