1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-11-24 08:32:34 +02:00
vcmi/AI/BattleAI/BattleAI.cpp

293 lines
8.1 KiB
C++
Raw Normal View History

/*
* 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
*
*/
#include "StdInc.h"
#include "BattleAI.h"
#include "BattleEvaluator.h"
#include "BattleExchangeVariant.h"
Spells configuration version 2 (effect-based) * Indirect spell effects loading * Json serializer improvements * spell->canBeCastAt do not allow useless cast for any spell * Added proxy caster class for spell-created obstacles * Handle damage from spell-created obstacles inside mechanics * Experimental GameState integration/regression tests * Ignore mod settings and load only "vcmi" mod when running tests * fixed https://bugs.vcmi.eu/view.php?id=2765 (with tests) * Huge improvements of BattleAI regarding spell casts * AI can cast almost any combat spell except TELEPORT, SACRIFICE and obstacle placement spells. * Possible fix for https://bugs.vcmi.eu/view.php?id=1811 * CStack factored out to several classes * [Battle] Allowed RETURN_AFTER_STRIKE effect on server side to be optional * [Battle] Allowed BattleAction have multiple destinations * [Spells] Converted limit|immunity to target condition * [Spells] Use partial configuration reload for backward compatibility handling * [Tests] Started tests for CUnitState * Partial fixes of fire shield effect * [Battle] Do HP calculations in 64 bits * [BattleAI] Use threading for spell cast evaluation * [BattleAI] Made AI be able to evaluate modified turn order (on hypothetical battle state) * Implemented https://bugs.vcmi.eu/view.php?id=2811 * plug rare freeze when hypnotized unit shots vertically * Correctly apply ONLY_MELEE_FIGHT / ONLY_DISTANCE_FIGHT for unit damage, attack & defense * [BattleAI] Try to not waste a cast if battle is actually won already * Extended JsonSerializeFormat API * fixed https://bugs.vcmi.eu/view.php?id=2847 * Any unit effect can be now chained (not only damage like Chain Lightning) ** only damage effect for now actually uses "chainFactor" * Possible quick fix for https://bugs.vcmi.eu/view.php?id=2860
2017-07-20 06:08:49 +02:00
#include "StackWithBonuses.h"
#include "EnemyInfo.h"
2023-08-13 12:56:04 +02:00
#include "tbb/parallel_for.h"
Spells configuration version 2 (effect-based) * Indirect spell effects loading * Json serializer improvements * spell->canBeCastAt do not allow useless cast for any spell * Added proxy caster class for spell-created obstacles * Handle damage from spell-created obstacles inside mechanics * Experimental GameState integration/regression tests * Ignore mod settings and load only "vcmi" mod when running tests * fixed https://bugs.vcmi.eu/view.php?id=2765 (with tests) * Huge improvements of BattleAI regarding spell casts * AI can cast almost any combat spell except TELEPORT, SACRIFICE and obstacle placement spells. * Possible fix for https://bugs.vcmi.eu/view.php?id=1811 * CStack factored out to several classes * [Battle] Allowed RETURN_AFTER_STRIKE effect on server side to be optional * [Battle] Allowed BattleAction have multiple destinations * [Spells] Converted limit|immunity to target condition * [Spells] Use partial configuration reload for backward compatibility handling * [Tests] Started tests for CUnitState * Partial fixes of fire shield effect * [Battle] Do HP calculations in 64 bits * [BattleAI] Use threading for spell cast evaluation * [BattleAI] Made AI be able to evaluate modified turn order (on hypothetical battle state) * Implemented https://bugs.vcmi.eu/view.php?id=2811 * plug rare freeze when hypnotized unit shots vertically * Correctly apply ONLY_MELEE_FIGHT / ONLY_DISTANCE_FIGHT for unit damage, attack & defense * [BattleAI] Try to not waste a cast if battle is actually won already * Extended JsonSerializeFormat API * fixed https://bugs.vcmi.eu/view.php?id=2847 * Any unit effect can be now chained (not only damage like Chain Lightning) ** only damage effect for now actually uses "chainFactor" * Possible quick fix for https://bugs.vcmi.eu/view.php?id=2860
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"
#include "../../lib/spells/CSpellHandler.h"
Spells configuration version 2 (effect-based) * Indirect spell effects loading * Json serializer improvements * spell->canBeCastAt do not allow useless cast for any spell * Added proxy caster class for spell-created obstacles * Handle damage from spell-created obstacles inside mechanics * Experimental GameState integration/regression tests * Ignore mod settings and load only "vcmi" mod when running tests * fixed https://bugs.vcmi.eu/view.php?id=2765 (with tests) * Huge improvements of BattleAI regarding spell casts * AI can cast almost any combat spell except TELEPORT, SACRIFICE and obstacle placement spells. * Possible fix for https://bugs.vcmi.eu/view.php?id=1811 * CStack factored out to several classes * [Battle] Allowed RETURN_AFTER_STRIKE effect on server side to be optional * [Battle] Allowed BattleAction have multiple destinations * [Spells] Converted limit|immunity to target condition * [Spells] Use partial configuration reload for backward compatibility handling * [Tests] Started tests for CUnitState * Partial fixes of fire shield effect * [Battle] Do HP calculations in 64 bits * [BattleAI] Use threading for spell cast evaluation * [BattleAI] Made AI be able to evaluate modified turn order (on hypothetical battle state) * Implemented https://bugs.vcmi.eu/view.php?id=2811 * plug rare freeze when hypnotized unit shots vertically * Correctly apply ONLY_MELEE_FIGHT / ONLY_DISTANCE_FIGHT for unit damage, attack & defense * [BattleAI] Try to not waste a cast if battle is actually won already * Extended JsonSerializeFormat API * fixed https://bugs.vcmi.eu/view.php?id=2847 * Any unit effect can be now chained (not only damage like Chain Lightning) ** only damage effect for now actually uses "chainFactor" * Possible quick fix for https://bugs.vcmi.eu/view.php?id=2860
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"
#include "../../lib/battle/CObstacleInstance.h"
#include "../../lib/CStack.h" // TODO: remove
2021-02-15 14:03:32 +02:00
// Eventually only IBattleInfoCallback and battle::Unit should be used,
// CUnitState should be private and CStack should be removed completely
#define LOGL(text) print(text)
#define LOGFL(text, formattingEl) print(boost::str(boost::format(text) % formattingEl))
CBattleAI::CBattleAI()
: side(-1),
wasWaitingForRealize(false),
wasUnlockingGs(false)
{
}
CBattleAI::~CBattleAI()
{
if(cb)
{
//Restore previous state of CB - it may be shared with the main AI (like VCAI)
cb->waitTillRealize = wasWaitingForRealize;
cb->unlockGsWhenWaiting = wasUnlockingGs;
}
}
void CBattleAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB)
{
setCbc(CB);
env = ENV;
cb = CB;
2023-08-31 18:09:28 +02:00
playerID = *CB->getPlayerID();
wasWaitingForRealize = CB->waitTillRealize;
wasUnlockingGs = CB->unlockGsWhenWaiting;
CB->waitTillRealize = false;
CB->unlockGsWhenWaiting = false;
2023-04-08 13:05:47 +02:00
movesSkippedByDefense = 0;
}
void CBattleAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB, AutocombatPreferences autocombatPreferences)
{
initBattleInterface(ENV, CB);
autobattlePreferences = autocombatPreferences;
}
2023-08-28 17:59:12 +02:00
BattleAction CBattleAI::useHealingTent(const BattleID & battleID, const CStack *stack)
{
2023-08-28 17:59:12 +02:00
auto healingTargets = cb->getBattle(battleID)->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;
}
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
}
2023-08-28 17:59:12 +02:00
void CBattleAI::yourTacticPhase(const BattleID & battleID, int distance)
2023-07-18 20:43:53 +02:00
{
2023-08-28 17:59:12 +02:00
cb->battleMakeTacticAction(battleID, BattleAction::makeEndOFTacticPhase(cb->getBattle(battleID)->battleGetTacticsSide()));
2023-07-18 20:43:53 +02:00
}
2023-08-28 17:59:12 +02:00
static float getStrengthRatio(std::shared_ptr<CBattleInfoCallback> 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;
}
2023-08-28 17:59:12 +02:00
void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack )
{
LOG_TRACE_PARAMS(logAi, "stack: %s", stack->nodeName());
2023-04-08 13:05:47 +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();
};
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();
try
{
if(stack->creatureId() == CreatureID::CATAPULT)
{
2023-08-28 17:59:12 +02:00
cb->battleMakeUnitAction(battleID, useCatapult(battleID, stack));
return;
}
if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON) && stack->hasBonusOfType(BonusType::HEALER))
{
2023-08-28 17:59:12 +02:00
cb->battleMakeUnitAction(battleID, useHealingTent(battleID, stack));
return;
}
#if BATTLE_TRACE_LEVEL>=1
logAi->trace("Build evaluator and targets");
#endif
2023-08-28 17:59:12 +02:00
BattleEvaluator evaluator(env, cb, stack, playerID, battleID, side, getStrengthRatio(cb->getBattle(battleID), side));
result = evaluator.selectStackAction(stack);
if(!skipCastUntilNextBattle && evaluator.canCastSpell())
{
auto spelCasted = evaluator.attemptCastingSpell(stack);
if(spelCasted)
return;
skipCastUntilNextBattle = true;
}
2023-08-08 17:54:37 +02:00
logAi->trace("Spellcast attempt completed in %lld", timeElapsed(start));
2023-08-28 17:59:12 +02:00
if(auto action = considerFleeingOrSurrendering(battleID))
{
2023-08-28 17:59:12 +02:00
cb->battleMakeUnitAction(battleID, *action);
return;
}
}
2017-11-16 13:15:43 +02:00
catch(boost::thread_interrupted &)
{
throw;
}
catch(std::exception &e)
{
2016-08-13 16:44:37 +02:00
logAi->error("Exception occurred in %s %s",__FUNCTION__, e.what());
}
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-08-28 17:59:12 +02:00
cb->battleMakeUnitAction(battleID, result);
}
2023-08-28 17:59:12 +02:00
BattleAction CBattleAI::useCatapult(const BattleID & battleID, const CStack * stack)
{
2020-11-28 17:11:33 +02:00
BattleAction attack;
BattleHex targetHex = BattleHex::INVALID;
2023-08-28 17:59:12 +02:00
if(cb->getBattle(battleID)->battleGetGateState() == EGateState::CLOSED)
2020-11-28 17:11:33 +02:00
{
2023-08-28 17:59:12 +02:00
targetHex = cb->getBattle(battleID)->wallPartToBattleHex(EWallPart::GATE);
2021-02-15 14:03:32 +02:00
}
2020-11-28 17:11:33 +02:00
else
{
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)
{
2023-08-28 17:59:12 +02:00
auto wallState = cb->getBattle(battleID)->battleGetWallState(wallPart);
2020-11-28 17:11:33 +02:00
if(wallState == EWallState::REINFORCED || wallState == EWallState::INTACT || wallState == EWallState::DAMAGED)
2020-11-28 17:11:33 +02:00
{
2023-08-28 17:59:12 +02:00
targetHex = cb->getBattle(battleID)->wallPartToBattleHex(wallPart);
2020-11-28 17:11:33 +02:00
break;
}
}
}
if(!targetHex.isValid())
{
return BattleAction::makeDefend(stack);
}
attack.aimToHex(targetHex);
attack.actionType = EActionType::CATAPULT;
attack.side = side;
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
}
2023-08-28 17:59:12 +02:00
void CBattleAI::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed)
{
Spells configuration version 2 (effect-based) * Indirect spell effects loading * Json serializer improvements * spell->canBeCastAt do not allow useless cast for any spell * Added proxy caster class for spell-created obstacles * Handle damage from spell-created obstacles inside mechanics * Experimental GameState integration/regression tests * Ignore mod settings and load only "vcmi" mod when running tests * fixed https://bugs.vcmi.eu/view.php?id=2765 (with tests) * Huge improvements of BattleAI regarding spell casts * AI can cast almost any combat spell except TELEPORT, SACRIFICE and obstacle placement spells. * Possible fix for https://bugs.vcmi.eu/view.php?id=1811 * CStack factored out to several classes * [Battle] Allowed RETURN_AFTER_STRIKE effect on server side to be optional * [Battle] Allowed BattleAction have multiple destinations * [Spells] Converted limit|immunity to target condition * [Spells] Use partial configuration reload for backward compatibility handling * [Tests] Started tests for CUnitState * Partial fixes of fire shield effect * [Battle] Do HP calculations in 64 bits * [BattleAI] Use threading for spell cast evaluation * [BattleAI] Made AI be able to evaluate modified turn order (on hypothetical battle state) * Implemented https://bugs.vcmi.eu/view.php?id=2811 * plug rare freeze when hypnotized unit shots vertically * Correctly apply ONLY_MELEE_FIGHT / ONLY_DISTANCE_FIGHT for unit damage, attack & defense * [BattleAI] Try to not waste a cast if battle is actually won already * Extended JsonSerializeFormat API * fixed https://bugs.vcmi.eu/view.php?id=2847 * Any unit effect can be now chained (not only damage like Chain Lightning) ** only damage effect for now actually uses "chainFactor" * Possible quick fix for https://bugs.vcmi.eu/view.php?id=2860
2017-07-20 06:08:49 +02:00
LOG_TRACE(logAi);
side = Side;
skipCastUntilNextBattle = false;
}
void CBattleAI::print(const std::string &text) const
{
logAi->trace("%s Battle AI[%p]: %s", playerID.toString(), this, text);
}
2023-08-28 17:59:12 +02:00
std::optional<BattleAction> CBattleAI::considerFleeingOrSurrendering(const BattleID & battleID)
{
2022-10-14 10:24:29 +02:00
BattleStateInfoForRetreat bs;
2023-08-28 17:59:12 +02:00
bs.canFlee = cb->getBattle(battleID)->battleCanFlee();
bs.canSurrender = cb->getBattle(battleID)->battleCanSurrender(playerID);
bs.ourSide = cb->getBattle(battleID)->battleGetMySide();
bs.ourHero = cb->getBattle(battleID)->battleGetMyHero();
2022-10-15 14:05:20 +02:00
bs.enemyHero = nullptr;
2022-10-14 10:24:29 +02:00
2023-08-28 17:59:12 +02:00
for(auto stack : cb->getBattle(battleID)->battleGetAllStacks(false))
{
2022-10-14 10:24:29 +02:00
if(stack->alive())
{
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);
2023-08-28 17:59:12 +02:00
bs.enemyHero = cb->getBattle(battleID)->battleGetOwnerHero(stack);
2022-10-15 14:05:20 +02:00
}
2022-10-14 10:24:29 +02:00
}
}
2022-10-14 10:24:29 +02:00
2023-04-08 13:05:47 +02:00
bs.turnsSkippedByDefense = movesSkippedByDefense / bs.ourStacks.size();
if(!bs.canFlee && !bs.canSurrender)
{
2023-04-16 19:42:56 +02:00
return std::nullopt;
}
2022-10-14 10:24:29 +02:00
2023-08-28 17:59:12 +02:00
auto result = cb->makeSurrenderRetreatDecision(battleID, bs);
2023-04-08 13:05:47 +02:00
if(!result && bs.canFlee && bs.turnsSkippedByDefense > 30)
{
return BattleAction::makeRetreat(bs.ourSide);
}
return result;
}