2016-10-28 16:16:46 +02:00
|
|
|
/*
|
2017-07-13 10:26:03 +02:00
|
|
|
* BattleAI.cpp, part of VCMI engine
|
|
|
|
*
|
|
|
|
* Authors: listed in file AUTHORS in main folder
|
|
|
|
*
|
|
|
|
* License: GNU General Public License v2.0 or later
|
|
|
|
* Full text of license available in license.txt file, in main folder
|
|
|
|
*
|
|
|
|
*/
|
2012-09-14 02:42:11 +03:00
|
|
|
#include "StdInc.h"
|
|
|
|
#include "BattleAI.h"
|
2023-08-19 09:22:34 +02:00
|
|
|
#include "BattleEvaluator.h"
|
2022-01-27 09:00:27 +02:00
|
|
|
#include "BattleExchangeVariant.h"
|
2017-07-20 06:08:49 +02:00
|
|
|
|
2016-10-28 16:16:46 +02:00
|
|
|
#include "StackWithBonuses.h"
|
|
|
|
#include "EnemyInfo.h"
|
2023-08-13 12:56:04 +02:00
|
|
|
#include "tbb/parallel_for.h"
|
2017-07-20 06:08:49 +02:00
|
|
|
#include "../../lib/CStopWatch.h"
|
|
|
|
#include "../../lib/CThreadHelper.h"
|
2020-11-28 17:11:33 +02:00
|
|
|
#include "../../lib/mapObjects/CGTownInstance.h"
|
2015-02-02 10:25:26 +02:00
|
|
|
#include "../../lib/spells/CSpellHandler.h"
|
2017-07-20 06:08:49 +02:00
|
|
|
#include "../../lib/spells/ISpellMechanics.h"
|
2023-08-17 18:18:14 +02:00
|
|
|
#include "../../lib/battle/BattleAction.h"
|
2022-10-14 10:24:29 +02:00
|
|
|
#include "../../lib/battle/BattleStateInfoForRetreat.h"
|
2023-03-20 11:41:36 +02:00
|
|
|
#include "../../lib/battle/CObstacleInstance.h"
|
2020-05-09 13:09:32 +02:00
|
|
|
#include "../../lib/CStack.h" // TODO: remove
|
2021-02-15 14:03:32 +02:00
|
|
|
// Eventually only IBattleInfoCallback and battle::Unit should be used,
|
2020-05-09 13:09:32 +02:00
|
|
|
// CUnitState should be private and CStack should be removed completely
|
2012-09-14 02:42:11 +03:00
|
|
|
|
2016-10-28 16:16:46 +02:00
|
|
|
#define LOGL(text) print(text)
|
|
|
|
#define LOGFL(text, formattingEl) print(boost::str(boost::format(text) % formattingEl))
|
2012-09-14 02:42:11 +03:00
|
|
|
|
2018-01-13 10:43:26 +02:00
|
|
|
CBattleAI::CBattleAI()
|
Entities redesign and a few ERM features
* Made most Handlers derived from CHandlerBase and moved service API there.
* Declared existing Entity APIs.
* Added basic script context caching
* Started Lua script module
* Started Lua spell effect API
* Started script state persistence
* Started battle info callback binding
* CommitPackage removed
* Extracted spells::Caster to own header; Expanded Spell API.
* implemented !!MC:S, !!FU:E, !!FU:P, !!MA, !!VR:H, !!VR:C
* !!BU:C, !!BU:E, !!BU:G, !!BU:M implemented
* Allow use of "MC:S@varName@" to declare normal variable (technically v-variable with string key)
* Re-enabled VERM macros.
* !?GM0 added
* !?TM implemented
* Added !!MF:N
* Started !?OB, !!BM, !!HE, !!OW, !!UN
* Added basic support of w-variables
* Added support for ERM indirect variables
* Made !?FU regular trigger
* !!re (ERA loop receiver) implemented
* Fixed ERM receivers with zero args.
2018-03-17 16:58:30 +02:00
|
|
|
: side(-1),
|
|
|
|
wasWaitingForRealize(false),
|
|
|
|
wasUnlockingGs(false)
|
2012-09-24 02:10:56 +03:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2018-01-13 10:43:26 +02:00
|
|
|
CBattleAI::~CBattleAI()
|
2012-09-20 19:55:21 +03:00
|
|
|
{
|
2013-06-21 23:59:32 +03:00
|
|
|
if(cb)
|
2012-09-20 19:55:21 +03:00
|
|
|
{
|
2013-06-21 23:59:32 +03:00
|
|
|
//Restore previous state of CB - it may be shared with the main AI (like VCAI)
|
|
|
|
cb->waitTillRealize = wasWaitingForRealize;
|
|
|
|
cb->unlockGsWhenWaiting = wasUnlockingGs;
|
2012-09-20 19:55:21 +03:00
|
|
|
}
|
2013-06-21 23:59:32 +03:00
|
|
|
}
|
2012-09-20 19:55:21 +03:00
|
|
|
|
2022-12-07 21:50:45 +02:00
|
|
|
void CBattleAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB)
|
2013-06-21 23:59:32 +03:00
|
|
|
{
|
2016-10-28 16:16:46 +02:00
|
|
|
setCbc(CB);
|
Entities redesign and a few ERM features
* Made most Handlers derived from CHandlerBase and moved service API there.
* Declared existing Entity APIs.
* Added basic script context caching
* Started Lua script module
* Started Lua spell effect API
* Started script state persistence
* Started battle info callback binding
* CommitPackage removed
* Extracted spells::Caster to own header; Expanded Spell API.
* implemented !!MC:S, !!FU:E, !!FU:P, !!MA, !!VR:H, !!VR:C
* !!BU:C, !!BU:E, !!BU:G, !!BU:M implemented
* Allow use of "MC:S@varName@" to declare normal variable (technically v-variable with string key)
* Re-enabled VERM macros.
* !?GM0 added
* !?TM implemented
* Added !!MF:N
* Started !?OB, !!BM, !!HE, !!OW, !!UN
* Added basic support of w-variables
* Added support for ERM indirect variables
* Made !?FU regular trigger
* !!re (ERA loop receiver) implemented
* Fixed ERM receivers with zero args.
2018-03-17 16:58:30 +02:00
|
|
|
env = ENV;
|
2016-10-28 16:16:46 +02:00
|
|
|
cb = CB;
|
2017-07-12 21:01:10 +02:00
|
|
|
playerID = *CB->getPlayerID(); //TODO should be sth in callback
|
Entities redesign and a few ERM features
* Made most Handlers derived from CHandlerBase and moved service API there.
* Declared existing Entity APIs.
* Added basic script context caching
* Started Lua script module
* Started Lua spell effect API
* Started script state persistence
* Started battle info callback binding
* CommitPackage removed
* Extracted spells::Caster to own header; Expanded Spell API.
* implemented !!MC:S, !!FU:E, !!FU:P, !!MA, !!VR:H, !!VR:C
* !!BU:C, !!BU:E, !!BU:G, !!BU:M implemented
* Allow use of "MC:S@varName@" to declare normal variable (technically v-variable with string key)
* Re-enabled VERM macros.
* !?GM0 added
* !?TM implemented
* Added !!MF:N
* Started !?OB, !!BM, !!HE, !!OW, !!UN
* Added basic support of w-variables
* Added support for ERM indirect variables
* Made !?FU regular trigger
* !!re (ERA loop receiver) implemented
* Fixed ERM receivers with zero args.
2018-03-17 16:58:30 +02:00
|
|
|
wasWaitingForRealize = CB->waitTillRealize;
|
2013-06-21 23:59:32 +03:00
|
|
|
wasUnlockingGs = CB->unlockGsWhenWaiting;
|
2023-07-18 18:55:59 +02:00
|
|
|
CB->waitTillRealize = false;
|
2013-06-21 23:59:32 +03:00
|
|
|
CB->unlockGsWhenWaiting = false;
|
2023-04-08 13:05:47 +02:00
|
|
|
movesSkippedByDefense = 0;
|
2013-06-21 23:59:32 +03:00
|
|
|
}
|
2012-09-29 17:44:06 +03:00
|
|
|
|
2023-08-19 17:23:55 +02:00
|
|
|
void CBattleAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB, AutocombatPreferences autocombatPreferences)
|
|
|
|
{
|
|
|
|
initBattleInterface(ENV, CB);
|
|
|
|
autobattlePreferences = autocombatPreferences;
|
|
|
|
}
|
|
|
|
|
2023-07-18 15:29:02 +02:00
|
|
|
BattleAction CBattleAI::useHealingTent(const CStack *stack)
|
2012-09-20 19:55:21 +03:00
|
|
|
{
|
2023-07-18 15:29:02 +02:00
|
|
|
auto healingTargets = cb->battleGetStacks(CBattleInfoEssentials::ONLY_MINE);
|
|
|
|
std::map<int, const CStack*> woundHpToStack;
|
|
|
|
for(const auto * stack : healingTargets)
|
|
|
|
{
|
|
|
|
if(auto woundHp = stack->getMaxHealth() - stack->getFirstHPleft())
|
|
|
|
woundHpToStack[woundHp] = stack;
|
|
|
|
}
|
2022-04-11 08:12:41 +02:00
|
|
|
|
2023-07-18 15:29:02 +02:00
|
|
|
if(woundHpToStack.empty())
|
|
|
|
return BattleAction::makeDefend(stack);
|
|
|
|
else
|
|
|
|
return BattleAction::makeHeal(stack, woundHpToStack.rbegin()->second); //last element of the woundHpToStack is the most wounded stack
|
|
|
|
}
|
2022-04-11 08:12:41 +02:00
|
|
|
|
2023-07-18 20:43:53 +02:00
|
|
|
void CBattleAI::yourTacticPhase(int distance)
|
|
|
|
{
|
2023-07-23 12:55:00 +02:00
|
|
|
cb->battleMakeTacticAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide()));
|
2023-07-18 20:43:53 +02:00
|
|
|
}
|
|
|
|
|
2023-08-26 12:06:41 +02:00
|
|
|
float getStrengthRatio(std::shared_ptr<CBattleCallback> cb, int side)
|
|
|
|
{
|
|
|
|
auto stacks = cb->battleGetAllStacks();
|
|
|
|
auto our = 0, enemy = 0;
|
|
|
|
|
|
|
|
for(auto stack : stacks)
|
|
|
|
{
|
|
|
|
auto creature = stack->creatureId().toCreature();
|
|
|
|
|
|
|
|
if(!creature)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
if(stack->unitSide() == side)
|
|
|
|
our += stack->getCount() * creature->getAIValue();
|
|
|
|
else
|
|
|
|
enemy += stack->getCount() * creature->getAIValue();
|
|
|
|
}
|
|
|
|
|
|
|
|
return enemy == 0 ? 1.0f : static_cast<float>(our) / enemy;
|
|
|
|
}
|
|
|
|
|
|
|
|
void CBattleAI::activeStack(const CStack * stack )
|
2023-07-18 15:29:02 +02:00
|
|
|
{
|
|
|
|
LOG_TRACE_PARAMS(logAi, "stack: %s", stack->nodeName());
|
2023-04-08 13:05:47 +02:00
|
|
|
|
2023-08-19 09:22:34 +02:00
|
|
|
auto timeElapsed = [](std::chrono::time_point<std::chrono::high_resolution_clock> start) -> uint64_t
|
|
|
|
{
|
|
|
|
auto end = std::chrono::high_resolution_clock::now();
|
|
|
|
|
|
|
|
return std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
|
|
|
|
};
|
|
|
|
|
2023-07-18 15:29:02 +02:00
|
|
|
BattleAction result = BattleAction::makeDefend(stack);
|
|
|
|
setCbc(cb); //TODO: make solid sure that AIs always use their callbacks (need to take care of event handlers too)
|
|
|
|
|
2023-08-08 17:54:37 +02:00
|
|
|
auto start = std::chrono::high_resolution_clock::now();
|
|
|
|
|
2023-07-18 15:29:02 +02:00
|
|
|
try
|
|
|
|
{
|
|
|
|
if(stack->creatureId() == CreatureID::CATAPULT)
|
2023-07-18 18:55:59 +02:00
|
|
|
{
|
|
|
|
cb->battleMakeUnitAction(useCatapult(stack));
|
|
|
|
return;
|
|
|
|
}
|
2023-07-18 15:29:02 +02:00
|
|
|
if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON) && stack->hasBonusOfType(BonusType::HEALER))
|
2023-07-18 18:55:59 +02:00
|
|
|
{
|
|
|
|
cb->battleMakeUnitAction(useHealingTent(stack));
|
|
|
|
return;
|
|
|
|
}
|
2023-07-18 15:29:02 +02:00
|
|
|
|
2023-08-26 12:06:41 +02:00
|
|
|
#if BATTLE_TRACE_LEVEL>=1
|
|
|
|
logAi->trace("Build evaluator and targets");
|
|
|
|
#endif
|
|
|
|
|
|
|
|
BattleEvaluator evaluator(env, cb, stack, playerID, side, getStrengthRatio(cb, side));
|
2023-08-08 17:37:42 +02:00
|
|
|
|
|
|
|
result = evaluator.selectStackAction(stack);
|
|
|
|
|
2023-08-19 09:22:34 +02:00
|
|
|
if(!skipCastUntilNextBattle && evaluator.canCastSpell())
|
|
|
|
{
|
|
|
|
auto spelCasted = evaluator.attemptCastingSpell(stack);
|
|
|
|
|
|
|
|
if(spelCasted)
|
|
|
|
return;
|
|
|
|
|
|
|
|
skipCastUntilNextBattle = true;
|
|
|
|
}
|
2023-07-18 15:29:02 +02:00
|
|
|
|
2023-08-08 17:54:37 +02:00
|
|
|
logAi->trace("Spellcast attempt completed in %lld", timeElapsed(start));
|
|
|
|
|
2023-07-18 15:29:02 +02:00
|
|
|
if(auto action = considerFleeingOrSurrendering())
|
2023-07-18 18:55:59 +02:00
|
|
|
{
|
|
|
|
cb->battleMakeUnitAction(*action);
|
|
|
|
return;
|
|
|
|
}
|
2012-09-14 02:42:11 +03:00
|
|
|
}
|
2017-11-16 13:15:43 +02:00
|
|
|
catch(boost::thread_interrupted &)
|
|
|
|
{
|
|
|
|
throw;
|
|
|
|
}
|
2012-09-20 19:55:21 +03:00
|
|
|
catch(std::exception &e)
|
|
|
|
{
|
2016-08-13 16:44:37 +02:00
|
|
|
logAi->error("Exception occurred in %s %s",__FUNCTION__, e.what());
|
2012-09-20 19:55:21 +03:00
|
|
|
}
|
2020-11-28 17:11:33 +02:00
|
|
|
|
2023-04-08 13:05:47 +02:00
|
|
|
if(result.actionType == EActionType::DEFEND)
|
|
|
|
{
|
|
|
|
movesSkippedByDefense++;
|
|
|
|
}
|
|
|
|
else if(result.actionType != EActionType::WAIT)
|
|
|
|
{
|
|
|
|
movesSkippedByDefense = 0;
|
|
|
|
}
|
|
|
|
|
2023-08-08 17:54:37 +02:00
|
|
|
logAi->trace("BattleAI decission made in %lld", timeElapsed(start));
|
|
|
|
|
2023-07-18 18:55:59 +02:00
|
|
|
cb->battleMakeUnitAction(result);
|
2012-09-14 02:42:11 +03:00
|
|
|
}
|
|
|
|
|
2012-09-20 19:55:21 +03:00
|
|
|
BattleAction CBattleAI::useCatapult(const CStack * stack)
|
|
|
|
{
|
2020-11-28 17:11:33 +02:00
|
|
|
BattleAction attack;
|
|
|
|
BattleHex targetHex = BattleHex::INVALID;
|
|
|
|
|
|
|
|
if(cb->battleGetGateState() == EGateState::CLOSED)
|
|
|
|
{
|
|
|
|
targetHex = cb->wallPartToBattleHex(EWallPart::GATE);
|
2021-02-15 14:03:32 +02:00
|
|
|
}
|
2020-11-28 17:11:33 +02:00
|
|
|
else
|
|
|
|
{
|
2023-01-13 00:35:58 +02:00
|
|
|
EWallPart wallParts[] = {
|
2020-11-28 17:11:33 +02:00
|
|
|
EWallPart::KEEP,
|
|
|
|
EWallPart::BOTTOM_TOWER,
|
|
|
|
EWallPart::UPPER_TOWER,
|
|
|
|
EWallPart::BELOW_GATE,
|
|
|
|
EWallPart::OVER_GATE,
|
|
|
|
EWallPart::BOTTOM_WALL,
|
|
|
|
EWallPart::UPPER_WALL
|
|
|
|
};
|
|
|
|
|
|
|
|
for(auto wallPart : wallParts)
|
|
|
|
{
|
|
|
|
auto wallState = cb->battleGetWallState(wallPart);
|
|
|
|
|
2023-01-13 01:09:24 +02:00
|
|
|
if(wallState == EWallState::REINFORCED || wallState == EWallState::INTACT || wallState == EWallState::DAMAGED)
|
2020-11-28 17:11:33 +02:00
|
|
|
{
|
|
|
|
targetHex = cb->wallPartToBattleHex(wallPart);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!targetHex.isValid())
|
|
|
|
{
|
|
|
|
return BattleAction::makeDefend(stack);
|
|
|
|
}
|
|
|
|
|
|
|
|
attack.aimToHex(targetHex);
|
|
|
|
attack.actionType = EActionType::CATAPULT;
|
|
|
|
attack.side = side;
|
2023-04-27 19:43:20 +02:00
|
|
|
attack.stackNumber = stack->unitId();
|
2020-11-28 17:11:33 +02:00
|
|
|
|
2023-04-08 13:05:47 +02:00
|
|
|
movesSkippedByDefense = 0;
|
|
|
|
|
2020-11-28 17:11:33 +02:00
|
|
|
return attack;
|
2012-12-16 19:54:20 +03:00
|
|
|
}
|
2012-09-20 19:55:21 +03:00
|
|
|
|
2023-07-27 17:40:47 +02:00
|
|
|
void CBattleAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed)
|
2013-06-21 23:59:32 +03:00
|
|
|
{
|
2017-07-20 06:08:49 +02:00
|
|
|
LOG_TRACE(logAi);
|
2016-10-28 16:16:46 +02:00
|
|
|
side = Side;
|
2023-08-19 09:22:34 +02:00
|
|
|
|
|
|
|
skipCastUntilNextBattle = false;
|
2023-08-08 17:37:42 +02:00
|
|
|
}
|
|
|
|
|
2023-08-19 09:22:34 +02:00
|
|
|
void CBattleAI::print(const std::string &text) const
|
2013-06-21 23:59:32 +03:00
|
|
|
{
|
2017-07-20 06:08:49 +02:00
|
|
|
logAi->trace("%s Battle AI[%p]: %s", playerID.getStr(), this, text);
|
2013-06-21 23:59:32 +03:00
|
|
|
}
|
|
|
|
|
2023-04-16 19:42:56 +02:00
|
|
|
std::optional<BattleAction> CBattleAI::considerFleeingOrSurrendering()
|
2013-06-21 23:59:32 +03:00
|
|
|
{
|
2022-10-14 10:24:29 +02:00
|
|
|
BattleStateInfoForRetreat bs;
|
|
|
|
|
|
|
|
bs.canFlee = cb->battleCanFlee();
|
|
|
|
bs.canSurrender = cb->battleCanSurrender(playerID);
|
|
|
|
bs.ourSide = cb->battleGetMySide();
|
2022-10-15 14:05:20 +02:00
|
|
|
bs.ourHero = cb->battleGetMyHero();
|
|
|
|
bs.enemyHero = nullptr;
|
2022-10-14 10:24:29 +02:00
|
|
|
|
|
|
|
for(auto stack : cb->battleGetAllStacks(false))
|
2013-06-21 23:59:32 +03:00
|
|
|
{
|
2022-10-14 10:24:29 +02:00
|
|
|
if(stack->alive())
|
|
|
|
{
|
2023-04-27 19:43:20 +02:00
|
|
|
if(stack->unitSide() == bs.ourSide)
|
2022-10-14 10:24:29 +02:00
|
|
|
bs.ourStacks.push_back(stack);
|
|
|
|
else
|
2022-10-15 14:05:20 +02:00
|
|
|
{
|
2022-10-14 10:24:29 +02:00
|
|
|
bs.enemyStacks.push_back(stack);
|
2022-10-15 14:05:20 +02:00
|
|
|
bs.enemyHero = cb->battleGetOwnerHero(stack);
|
|
|
|
}
|
2022-10-14 10:24:29 +02:00
|
|
|
}
|
2013-06-21 23:59:32 +03:00
|
|
|
}
|
2022-10-14 10:24:29 +02:00
|
|
|
|
2023-04-08 13:05:47 +02:00
|
|
|
bs.turnsSkippedByDefense = movesSkippedByDefense / bs.ourStacks.size();
|
|
|
|
|
2023-07-30 10:33:52 +02:00
|
|
|
if(!bs.canFlee && !bs.canSurrender)
|
2013-06-21 23:59:32 +03:00
|
|
|
{
|
2023-04-16 19:42:56 +02:00
|
|
|
return std::nullopt;
|
2013-06-21 23:59:32 +03:00
|
|
|
}
|
2022-10-14 10:24:29 +02:00
|
|
|
|
2023-04-08 13:05:47 +02:00
|
|
|
auto result = cb->makeSurrenderRetreatDecision(bs);
|
|
|
|
|
|
|
|
if(!result && bs.canFlee && bs.turnsSkippedByDefense > 30)
|
|
|
|
{
|
|
|
|
return BattleAction::makeRetreat(bs.ourSide);
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
2013-06-21 23:59:32 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|