2
0

BAI.cpp 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741
  1. /*
  2. * BAI.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/BattleAction.h"
  12. #include "battle/BattleStateInfoForRetreat.h"
  13. #include "battle/CBattleInfoEssentials.h"
  14. #include "BAI/base.h"
  15. #include "BAI/v13/BAI.h"
  16. #include "BAI/v13/action.h"
  17. #include "BAI/v13/hexaction.h"
  18. #include "BAI/v13/hexactmask.h"
  19. #include "BAI/v13/render.h"
  20. #include "BAI/v13/supplementary_data.h"
  21. #include "common.h"
  22. #include "schema/v13/types.h"
  23. #include "AI/BattleAI/BattleEvaluator.h"
  24. #include <algorithm>
  25. #include <optional>
  26. namespace MMAI::BAI::V13
  27. {
  28. using ErrorCode = Schema::V13::ErrorCode;
  29. using PA = Schema::V13::PlayerAttribute;
  30. Schema::Action BAI::getNonRenderAction()
  31. {
  32. // info("getNonRenderAciton called with result type: " + std::to_string(result->type));
  33. const auto * s = state.get();
  34. auto action = model->getAction(s);
  35. debug("Got action: " + std::to_string(action));
  36. while(action == Schema::ACTION_RENDER_ANSI)
  37. {
  38. if(state->supdata->ansiRender.empty())
  39. {
  40. state->supdata->ansiRender = renderANSI();
  41. state->supdata->type = Schema::V13::ISupplementaryData::Type::ANSI_RENDER;
  42. }
  43. // info("getNonRenderAciton (loop) called with result type: " + std::to_string(res.type));
  44. action = model->getAction(state.get());
  45. }
  46. state->supdata->ansiRender.clear();
  47. state->supdata->type = Schema::V13::ISupplementaryData::Type::REGULAR;
  48. return action;
  49. }
  50. std::unique_ptr<State> BAI::initState(const CPlayerBattleCallback * b)
  51. {
  52. return std::make_unique<State>(version, colorname, b);
  53. }
  54. void BAI::battleStart(
  55. const BattleID & bid,
  56. const CCreatureSet * army1,
  57. const CCreatureSet * army2,
  58. int3 tile,
  59. const CGHeroInstance * hero1,
  60. const CGHeroInstance * hero2,
  61. BattleSide side,
  62. bool replayAllowed
  63. )
  64. {
  65. Base::battleStart(bid, army1, army2, tile, hero1, hero2, side, replayAllowed);
  66. battle = cb->getBattle(bid);
  67. state = initState(battle.get());
  68. getActionTotalMs = 0;
  69. getActionTotalCalls = 0;
  70. }
  71. // XXX: battleEnd() is NOT called by CPlayerInterface (i.e. GUI)
  72. // However, it's called by AAI (i.e. headless) and that's all we want
  73. // since the terminal result is needed only during training.
  74. void BAI::battleEnd(const BattleID & bid, const BattleResult * br, QueryID queryID)
  75. {
  76. Base::battleEnd(bid, br, queryID);
  77. state->onBattleEnd(br);
  78. debug("MMAI %s this battle.", (br->winner == battle->battleGetMySide() ? "won" : "lost"));
  79. // Check if battle ended normally or was forced via a RETREAT action
  80. if(state->action == nullptr)
  81. {
  82. // no previous action means battle ended without giving us a turn (OK)
  83. // Happens if the enemy immediately retreats (we won)
  84. // or if the enemy one-shots us (we lost)
  85. info("Battle ended without giving us a turn: nothing to do");
  86. }
  87. else if(state->action->action == Schema::ACTION_RETREAT)
  88. {
  89. if(resetting)
  90. {
  91. // this is an intended restart (i.e. converted ACTION_RESTART)
  92. info("Battle ended due to ACTION_RESET: nothing to do");
  93. }
  94. else
  95. {
  96. // this is real retreat
  97. info("Battle ended due to ACTION_RETREAT: reporting terminal state, expecting ACTION_RESET");
  98. auto a = getNonRenderAction();
  99. ASSERT(a == Schema::ACTION_RESET, "expected ACTION_RESET, got: " + std::to_string(EI(a)));
  100. }
  101. }
  102. else
  103. {
  104. debug("Battle ended normally: reporting terminal state, expecting ACTION_RESET");
  105. auto a = getNonRenderAction();
  106. ASSERT(a == Schema::ACTION_RESET, "expected ACTION_RESET, got: " + std::to_string(EI(a)));
  107. }
  108. if(getActionTotalCalls > 0)
  109. info("MMAI stats after battle end: %d predictions, %d ms per prediction", getActionTotalCalls, getActionTotalMs / getActionTotalCalls);
  110. else
  111. info("MMAI stats after battle end: 0 predictions");
  112. // BAI is destroyed after this call
  113. debug("Leaving battleEnd, embracing death");
  114. }
  115. void BAI::battleStacksAttacked(const BattleID & bid, const std::vector<BattleStackAttacked> & bsa, bool ranged)
  116. {
  117. Base::battleStacksAttacked(bid, bsa, ranged);
  118. state->onBattleStacksAttacked(bsa);
  119. }
  120. void BAI::battleTriggerEffect(const BattleID & bid, const BattleTriggerEffect & bte)
  121. {
  122. Base::battleTriggerEffect(bid, bte);
  123. state->onBattleTriggerEffect(bte);
  124. }
  125. void BAI::yourTacticPhase(const BattleID & bid, int distance)
  126. {
  127. Base::yourTacticPhase(bid, distance);
  128. cb->battleMakeTacticAction(bid, BattleAction::makeEndOFTacticPhase(battle->battleGetTacticsSide()));
  129. }
  130. bool BAI::maybeCastSpell(const CStack * astack, const BattleID & bid)
  131. {
  132. if(!enableSpellsUsage)
  133. return false;
  134. const auto * hero = battle->battleGetMyHero();
  135. if(!hero)
  136. return false;
  137. if(battle->battleCanCastSpell(hero, spells::Mode::HERO) != ESpellCastProblem::OK)
  138. return false;
  139. auto lv = state->lpstats->getAttr(PA::ARMY_VALUE_NOW_ABS);
  140. auto rv = state->rpstats->getAttr(PA::ARMY_VALUE_NOW_ABS);
  141. auto vratio = static_cast<float>(lv) / rv;
  142. if(battle->battleGetMySide() == BattleSide::RIGHT_SIDE)
  143. vratio = 1 / vratio;
  144. logAi->debug("Attempting a BattleAI spellcast");
  145. auto evaluator = BattleEvaluator(env, cb, astack, *cb->getPlayerID(), bid, battle->battleGetMySide(), vratio, 2);
  146. return evaluator.attemptCastingSpell(astack);
  147. }
  148. std::shared_ptr<BattleAction> BAI::maybeBuildAutoAction(const CStack * astack, const BattleID & bid) const
  149. {
  150. if(astack->creatureId() == CreatureID::FIRST_AID_TENT)
  151. {
  152. const CStack * target = nullptr;
  153. auto maxdmg = 0;
  154. for(const auto * stack : battle->battleGetStacks(CBattleInfoEssentials::ONLY_MINE))
  155. {
  156. auto dmg = stack->getMaxHealth() - stack->getFirstHPleft();
  157. if(dmg <= maxdmg)
  158. continue;
  159. maxdmg = dmg;
  160. target = stack;
  161. }
  162. if(target)
  163. {
  164. return std::make_shared<BattleAction>(BattleAction::makeHeal(astack, target));
  165. }
  166. }
  167. else if(astack->creatureId() == CreatureID::CATAPULT)
  168. {
  169. if(!astack->canShoot())
  170. // out of ammo
  171. return std::make_shared<BattleAction>(BattleAction::makeDefend(astack)); // out of ammo (arrow towers have 99 shots)
  172. auto ba = std::make_shared<BattleAction>();
  173. ba->side = astack->unitSide();
  174. ba->stackNumber = astack->unitId();
  175. ba->actionType = EActionType::CATAPULT;
  176. if(battle->battleGetGateState() == EGateState::CLOSED)
  177. {
  178. ba->aimToHex(battle->wallPartToBattleHex(EWallPart::GATE));
  179. return ba;
  180. }
  181. using WP = EWallPart;
  182. auto wallparts = {WP::KEEP, WP::BOTTOM_TOWER, WP::UPPER_TOWER, WP::BELOW_GATE, WP::OVER_GATE, WP::BOTTOM_WALL, WP::UPPER_WALL};
  183. for(auto wp : wallparts)
  184. {
  185. using WS = EWallState;
  186. auto ws = battle->battleGetWallState(wp);
  187. if(ws == WS::REINFORCED || ws == WS::INTACT || ws == WS::DAMAGED)
  188. {
  189. ba->aimToHex(battle->wallPartToBattleHex(wp));
  190. return ba;
  191. }
  192. }
  193. // no walls left
  194. return std::make_shared<BattleAction>(BattleAction::makeDefend(astack)); // out of ammo (arrow towers have 99 shots)
  195. }
  196. else if(astack->creatureId() == CreatureID::ARROW_TOWERS)
  197. {
  198. if(!astack->canShoot())
  199. // out of ammo (arrow towers have 99 shots)
  200. return std::make_shared<BattleAction>(BattleAction::makeDefend(astack));
  201. auto allstacks = battle->battleGetStacks(CBattleInfoEssentials::ONLY_ENEMY);
  202. auto target = std::ranges::max_element(
  203. allstacks,
  204. [](const CStack * a, const CStack * b)
  205. {
  206. return Stack::GetValue(a->unitType()) < Stack::GetValue(b->unitType());
  207. }
  208. );
  209. ASSERT(target != allstacks.end(), "Could not find an enemy stack to attack. Falling back to a defend.");
  210. return std::make_shared<BattleAction>(BattleAction::makeShotAttack(astack, *target));
  211. }
  212. return nullptr;
  213. }
  214. std::optional<BattleAction> BAI::maybeFleeOrSurrender(const BattleID & bid)
  215. {
  216. BattleStateInfoForRetreat bs;
  217. bs.canFlee = battle->battleCanFlee();
  218. bs.canSurrender = battle->battleCanSurrender(*cb->getPlayerID());
  219. if(!bs.canFlee && !bs.canSurrender)
  220. {
  221. logAi->debug("Can't flee or surrender.");
  222. return std::nullopt;
  223. }
  224. bs.ourSide = battle->battleGetMySide();
  225. bs.ourHero = battle->battleGetMyHero();
  226. bs.enemyHero = nullptr;
  227. for(const auto * stack : battle->battleGetAllStacks(false))
  228. {
  229. if(stack->alive())
  230. {
  231. if(stack->unitSide() == bs.ourSide)
  232. {
  233. bs.ourStacks.push_back(stack);
  234. }
  235. else
  236. {
  237. bs.enemyStacks.push_back(stack);
  238. bs.enemyHero = battle->battleGetOwnerHero(stack);
  239. }
  240. }
  241. }
  242. logAi->info("Making surrender/retreat decision...");
  243. return cb->makeSurrenderRetreatDecision(bid, bs);
  244. }
  245. void BAI::activeStack(const BattleID & bid, const CStack * astack)
  246. {
  247. Base::activeStack(bid, astack);
  248. try
  249. {
  250. _activeStack(bid, astack);
  251. }
  252. catch(const std::exception & e)
  253. {
  254. error("Falling back to BattleAI due to MMAI error: " + std::string(e.what()));
  255. auto evaluator = BattleEvaluator(env, cb, astack, *cb->getPlayerID(), bid, battle->battleGetMySide(), 1.0f, 2);
  256. cb->battleMakeUnitAction(bid, evaluator.selectStackAction(astack));
  257. return;
  258. }
  259. }
  260. void BAI::_activeStack(const BattleID & bid, const CStack * astack)
  261. {
  262. auto ba = maybeBuildAutoAction(astack, bid);
  263. if(ba)
  264. {
  265. info("Making automatic action with %s", astack->getDescription());
  266. cb->battleMakeUnitAction(bid, *ba);
  267. return;
  268. }
  269. // Guard against infinite battles
  270. // (print warning once, make only fallback actions from there on)
  271. if(!inFallback && getActionTotalCalls >= 100)
  272. {
  273. warn("Reached 100 predictions, will fall back to BattleAI until this combat ends");
  274. inFallback = true;
  275. }
  276. if(inFallback)
  277. {
  278. auto evaluator = BattleEvaluator(env, cb, astack, *cb->getPlayerID(), bid, battle->battleGetMySide(), 1.0f, 2);
  279. cb->battleMakeUnitAction(bid, evaluator.selectStackAction(astack));
  280. return;
  281. }
  282. state->onActiveStack(astack);
  283. if(maybeCastSpell(astack, bid))
  284. return;
  285. if(state->battlefield->astack == nullptr)
  286. {
  287. error(
  288. "The current stack is not part of the state. "
  289. "This should NOT happen. "
  290. "Falling back to a wait/defend action."
  291. );
  292. auto fa = astack->waitedThisTurn ? BattleAction::makeDefend(astack) : BattleAction::makeWait(astack);
  293. cb->battleMakeUnitAction(bid, fa);
  294. return;
  295. }
  296. auto concede = maybeFleeOrSurrender(bid);
  297. if(concede)
  298. {
  299. info("Making retreat/surrender action.");
  300. cb->battleMakeUnitAction(bid, *concede);
  301. return;
  302. }
  303. logAi->debug("Not conceding.");
  304. while(true)
  305. {
  306. auto t0 = std::chrono::steady_clock::now();
  307. auto a = getNonRenderAction();
  308. auto dt = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - t0).count();
  309. getActionTotalMs += dt;
  310. getActionTotalCalls += 1;
  311. allactions.push_back(a);
  312. if(a == Schema::ACTION_RESET)
  313. {
  314. // XXX: retreat is always allowed for ML, limited by action mask only
  315. debug("Received ACTION_RESET, converting to ACTION_RETREAT in order to reset battle");
  316. a = Schema::ACTION_RETREAT;
  317. resetting = true;
  318. }
  319. state->action = std::make_unique<Action>(a, state->battlefield.get(), colorname);
  320. debug("(%lld ms) Got action: %d: %s ", dt, a, state->action->name());
  321. ba = buildBattleAction();
  322. if(ba)
  323. {
  324. debug("Action is VALID: " + state->action->name());
  325. errcounter = 0;
  326. cb->battleMakeUnitAction(bid, *ba);
  327. break;
  328. }
  329. else
  330. {
  331. ++errcounter;
  332. error("Action is INVALID: " + state->action->name());
  333. if(errcounter > 10)
  334. {
  335. warn("Got 10 consecutive errors, will fall back to BattleAI until this combat ends");
  336. auto evaluator = BattleEvaluator(env, cb, astack, *cb->getPlayerID(), bid, battle->battleGetMySide(), 1.0f, 2);
  337. cb->battleMakeUnitAction(bid, evaluator.selectStackAction(astack));
  338. inFallback = true;
  339. break;
  340. }
  341. }
  342. }
  343. }
  344. std::shared_ptr<BattleAction> BAI::buildBattleAction()
  345. {
  346. ASSERT(state->battlefield, "Cannot build battle action if state->battlefield is missing");
  347. auto * action = state->action.get();
  348. const auto * bf = state->battlefield.get();
  349. const auto * acstack = bf->astack->cstack;
  350. auto [x, y] = Hex::CalcXY(acstack->getPosition());
  351. auto & hex = bf->hexes->at(y).at(x);
  352. std::shared_ptr<BattleAction> res = nullptr;
  353. if(!state->action->hex)
  354. {
  355. switch(static_cast<GlobalAction>(state->action->action))
  356. {
  357. case GlobalAction::RETREAT:
  358. res = std::make_shared<BattleAction>(BattleAction::makeRetreat(battle->battleGetMySide()));
  359. break;
  360. case GlobalAction::WAIT:
  361. if(acstack->waitedThisTurn)
  362. {
  363. ASSERT(!state->actmask.at(EI(GlobalAction::WAIT)), "mask allowed wait when stack has already waited");
  364. state->supdata->errcode = ErrorCode::ALREADY_WAITED;
  365. error("Action error: %s (%d): ALREADY_WAITED", action->name(), EI(action->action));
  366. return res;
  367. }
  368. res = std::make_shared<BattleAction>(BattleAction::makeWait(acstack));
  369. break;
  370. default:
  371. THROW_FORMAT("Unexpected non-hex action: %d", state->action->action);
  372. }
  373. return res;
  374. }
  375. // With action masking, invalid actions should never occur
  376. // However, for manual playing/testing, it's bad to raise exceptions
  377. // => return errcode (Gym env will raise an exception if errcode > 0)
  378. const auto & bhex = action->hex->bhex;
  379. auto & stack = action->hex->stack; // may be null
  380. auto mask = HexActMask(action->hex->attr(HexAttribute::ACTION_MASK));
  381. if(mask.test(EI(action->hexaction)))
  382. {
  383. // Action is VALID
  384. // XXX: Do minimal asserts to prevent bugs with nullptr deref
  385. // Server will log any attempted invalid actions otherwise
  386. switch(action->hexaction)
  387. {
  388. case HexAction::MOVE:
  389. {
  390. auto ba = (bhex == acstack->getPosition()) ? BattleAction::makeDefend(acstack) : BattleAction::makeMove(acstack, bhex);
  391. res = std::make_shared<BattleAction>(ba);
  392. }
  393. break;
  394. case HexAction::SHOOT:
  395. ASSERT(stack, "no target to shoot");
  396. res = std::make_shared<BattleAction>(BattleAction::makeShotAttack(acstack, stack->cstack));
  397. break;
  398. case HexAction::AMOVE_TR:
  399. case HexAction::AMOVE_R:
  400. case HexAction::AMOVE_BR:
  401. case HexAction::AMOVE_BL:
  402. case HexAction::AMOVE_L:
  403. case HexAction::AMOVE_TL:
  404. {
  405. const auto & edir = AMOVE_TO_EDIR.at(EI(action->hexaction));
  406. auto nbh = bhex.cloneInDirection(edir, false); // neighbouring bhex
  407. ASSERT(nbh.isAvailable(), "mask allowed attack to an unavailable hex #" + std::to_string(nbh.toInt()));
  408. const auto * estack = battle->battleGetStackByPos(nbh);
  409. ASSERT(estack, "no enemy stack for melee attack");
  410. res = std::make_shared<BattleAction>(BattleAction::makeMeleeAttack(acstack, nbh, bhex));
  411. }
  412. break;
  413. case HexAction::AMOVE_2TR:
  414. case HexAction::AMOVE_2R:
  415. case HexAction::AMOVE_2BR:
  416. case HexAction::AMOVE_2BL:
  417. case HexAction::AMOVE_2L:
  418. case HexAction::AMOVE_2TL:
  419. {
  420. ASSERT(acstack->doubleWide(), "got AMOVE_2 action for a single-hex stack");
  421. const auto & edir = AMOVE_TO_EDIR.at(EI(action->hexaction));
  422. auto obh = acstack->occupiedHex(bhex);
  423. auto nbh = obh.cloneInDirection(edir, false); // neighbouring bhex
  424. ASSERT(nbh.isAvailable(), "mask allowed attack to an unavailable hex #" + std::to_string(nbh.toInt()));
  425. const auto * estack = battle->battleGetStackByPos(nbh);
  426. ASSERT(estack, "no enemy stack for melee attack");
  427. res = std::make_shared<BattleAction>(BattleAction::makeMeleeAttack(acstack, nbh, bhex));
  428. }
  429. break;
  430. default:
  431. THROW_FORMAT("Unexpected hexaction: %d", EI(action->hexaction));
  432. }
  433. return res;
  434. }
  435. // Action is INVALID
  436. // XXX:
  437. // mask prevents certain actions, but during TESTING
  438. // those actions may be taken anyway.
  439. //
  440. // IF we are here, it means the mask disallows that action
  441. //
  442. // => *throw* errors here only if the mask SHOULD HAVE ALLOWED it
  443. // and *set* regular, non-throw errors otherwise
  444. //
  445. handleUnexpectedAction(acstack, hex, action);
  446. ASSERT(state->supdata->errcode != ErrorCode::OK, "Could not identify why the action is invalid" + debugInfo(action, acstack, nullptr));
  447. return res;
  448. }
  449. void BAI::handleUnexpectedAction(const CStack * acstack, std::unique_ptr<Hex> & hex, Action * action)
  450. {
  451. const auto & bhex = action->hex->bhex;
  452. auto & stack = action->hex->stack; // may be null
  453. auto rinfo = battle->getReachability(acstack);
  454. auto ainfo = battle->getAccessibility();
  455. switch(state->action->hexaction)
  456. {
  457. case HexAction::AMOVE_TR:
  458. case HexAction::AMOVE_R:
  459. case HexAction::AMOVE_BR:
  460. case HexAction::AMOVE_BL:
  461. case HexAction::AMOVE_L:
  462. case HexAction::AMOVE_TL:
  463. case HexAction::AMOVE_2TR:
  464. case HexAction::AMOVE_2R:
  465. case HexAction::AMOVE_2BR:
  466. case HexAction::AMOVE_2BL:
  467. case HexAction::AMOVE_2L:
  468. case HexAction::AMOVE_2TL:
  469. case HexAction::MOVE:
  470. {
  471. auto a = ainfo.at(action->hex->bhex.toInt());
  472. if(a == EAccessibility::OBSTACLE)
  473. {
  474. auto statemask = HexStateMask(hex->attr(HexAttribute::STATE_MASK));
  475. ASSERT(
  476. !statemask.test(EI(HexState::PASSABLE)),
  477. "accessibility is OBSTACLE, but hex state mask has PASSABLE set: " + statemask.to_string() + debugInfo(action, acstack, nullptr)
  478. );
  479. state->supdata->errcode = ErrorCode::HEX_BLOCKED;
  480. error("Action error: %s (%d): HEX_BLOCKED", action->name(), EI(action->action));
  481. break;
  482. }
  483. else if(a == EAccessibility::ALIVE_STACK)
  484. {
  485. auto bh = action->hex->bhex;
  486. if(bh.toInt() == acstack->getPosition().toInt())
  487. {
  488. // means we want to defend (moving to self)
  489. // or attack from same hex we're currently at
  490. // this should always be allowed
  491. ASSERT(false, "mask prevented (A)MOVE to own hex" + debugInfo(action, acstack, nullptr));
  492. }
  493. else if(bh.toInt() == acstack->occupiedHex().toInt())
  494. {
  495. ASSERT(
  496. rinfo.distances.at(bh.toInt()) == ReachabilityInfo::INFINITE_DIST,
  497. "mask prevented (A)MOVE to self-occupied hex" + debugInfo(action, acstack, nullptr)
  498. );
  499. // means we can't fit on our own back hex
  500. }
  501. // means we try to move onto another stack
  502. state->supdata->errcode = ErrorCode::HEX_BLOCKED;
  503. error("Action error: %s (%d): HEX_BLOCKED", action->name(), EI(action->action));
  504. break;
  505. }
  506. // only remaining is ACCESSIBLE
  507. ASSERT(a == EAccessibility::ACCESSIBLE, "accessibility should've been ACCESSIBLE, was: " = std::to_string(EI(a)));
  508. auto nbh = BattleHex{};
  509. if(action->hexaction < HexAction::AMOVE_2TR)
  510. {
  511. auto edir = AMOVE_TO_EDIR.at(EI(action->hexaction));
  512. nbh = bhex.cloneInDirection(edir, false);
  513. }
  514. else
  515. {
  516. if(!acstack->doubleWide())
  517. {
  518. state->supdata->errcode = ErrorCode::INVALID_DIR;
  519. error("Action error: %s (%d): INVALID_DIR", action->name(), EI(action->action));
  520. break;
  521. }
  522. auto edir = AMOVE_TO_EDIR.at(EI(action->hexaction));
  523. nbh = acstack->occupiedHex().cloneInDirection(edir, false);
  524. }
  525. if(!nbh.isAvailable())
  526. {
  527. state->supdata->errcode = ErrorCode::HEX_MELEE_NA;
  528. error("Action error: %s (%d): HEX_MELEE_NA", action->name(), EI(action->action));
  529. break;
  530. }
  531. const auto * estack = battle->battleGetStackByPos(nbh);
  532. if(!estack)
  533. {
  534. state->supdata->errcode = ErrorCode::STACK_NA;
  535. error("Action error: %s (%d): STACK_NA", action->name(), EI(action->action));
  536. break;
  537. }
  538. if(estack->unitSide() == acstack->unitSide())
  539. {
  540. state->supdata->errcode = ErrorCode::FRIENDLY_FIRE;
  541. error("Action error: %s (%d): FRIENDLY_FIRE", action->name(), EI(action->action));
  542. break;
  543. }
  544. }
  545. break;
  546. case HexAction::SHOOT:
  547. if(!stack)
  548. {
  549. state->supdata->errcode = ErrorCode::STACK_NA;
  550. error("Action error: %s (%d): STACK_NA", action->name(), EI(action->action));
  551. break;
  552. }
  553. else if(stack->cstack->unitSide() == acstack->unitSide())
  554. {
  555. state->supdata->errcode = ErrorCode::FRIENDLY_FIRE;
  556. error("Action error: %s (%d): FRIENDLY_FIRE", action->name(), EI(action->action));
  557. break;
  558. }
  559. else
  560. {
  561. ASSERT(!battle->battleCanShoot(acstack, bhex), "mask prevented SHOOT at a shootable bhex " + action->hex->name());
  562. state->supdata->errcode = ErrorCode::CANNOT_SHOOT;
  563. error("Action error: %s (%d): CANNOT_SHOOT", action->name(), EI(action->action));
  564. break;
  565. }
  566. break;
  567. default:
  568. THROW_FORMAT("Unexpected hexaction: %d", EI(action->hexaction));
  569. }
  570. }
  571. std::string BAI::debugInfo(Action * action, const CStack * astack, const BattleHex * const nbh)
  572. {
  573. auto info = std::stringstream();
  574. info << "\n*** DEBUG INFO ***\n";
  575. info << "action: " << action->name() << " [" << action->action << "]\n";
  576. info << "action->hex->bhex.toInt() = " << action->hex->bhex.toInt() << "\n";
  577. auto ainfo = battle->getAccessibility();
  578. auto rinfo = battle->getReachability(astack);
  579. info << "ainfo[bhex]=" << EI(ainfo.at(action->hex->bhex.toInt())) << "\n";
  580. info << "rinfo.distances[bhex] <= astack->getMovementRange(): " << (rinfo.distances[action->hex->bhex.toInt()] <= astack->getMovementRange()) << "\n";
  581. info << "action->hex->name = " << action->hex->name() << "\n";
  582. for(int i = 0; i < action->hex->attrs.size(); i++)
  583. info << "action->hex->attrs[" << i << "] = " << EI(action->hex->attrs[i]) << "\n";
  584. info << "action->hex->hexactmask = ";
  585. info << HexActMask(action->hex->attr(HexAttribute::ACTION_MASK)).to_string();
  586. info << "\n";
  587. auto stack = action->hex->stack;
  588. if(stack)
  589. {
  590. info << "stack->cstack->getPosition().toInt()=" << stack->cstack->getPosition().toInt() << "\n";
  591. info << "stack->cstack->slot=" << stack->cstack->unitSlot() << "\n";
  592. info << "stack->cstack->doubleWide=" << stack->cstack->doubleWide() << "\n";
  593. info << "cb->battleCanShoot(stack->cstack)=" << battle->battleCanShoot(stack->cstack) << "\n";
  594. }
  595. else
  596. {
  597. info << "cstack: (nullptr)\n";
  598. }
  599. info << "astack->getPosition().toInt()=" << astack->getPosition().toInt() << "\n";
  600. info << "astack->slot=" << astack->unitSlot() << "\n";
  601. info << "astack->doubleWide=" << astack->doubleWide() << "\n";
  602. info << "cb->battleCanShoot(astack)=" << battle->battleCanShoot(astack) << "\n";
  603. if(nbh)
  604. {
  605. info << "nbh->toInt()=" << nbh->toInt() << "\n";
  606. info << "ainfo[nbh]=" << EI(ainfo.at(nbh->toInt())) << "\n";
  607. info << "rinfo.distances[nbh] <= astack->getMovementRange(): " << (rinfo.distances[nbh->toInt()] <= astack->getMovementRange()) << "\n";
  608. if(stack)
  609. info << "astack->isMeleeAttackPossible(...)=" << astack->isMeleeAttackPossible(astack, stack->cstack, *nbh) << "\n";
  610. }
  611. info << "\nACTION TRACE:\n";
  612. for(const auto & a : allactions)
  613. info << a << ",";
  614. info << "\nRENDER:\n";
  615. info << renderANSI();
  616. return info.str();
  617. }
  618. std::string BAI::renderANSI() const
  619. {
  620. try
  621. {
  622. Verify(state.get());
  623. }
  624. catch(const std::exception & e)
  625. {
  626. try
  627. {
  628. std::cout << e.what();
  629. std::cout << "Disaster render:\n";
  630. std::cout << Render(state.get(), state->action.get()) << "\n";
  631. }
  632. catch(std::exception & e2)
  633. {
  634. std::cerr << "(failed: " << e2.what() << ")\n";
  635. }
  636. throw;
  637. }
  638. return Render(state.get(), state->action.get());
  639. }
  640. void BAI::actionStarted(const BattleID & bid, const BattleAction & action)
  641. {
  642. Base::actionStarted(bid, action);
  643. state->onActionStarted(action);
  644. };
  645. void BAI::actionFinished(const BattleID & bid, const BattleAction & action)
  646. {
  647. Base::actionFinished(bid, action);
  648. state->onActionFinished(action);
  649. };
  650. }