1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-12-16 10:19:47 +02:00
vcmi/AI/Nullkiller/Behaviors/DefenceBehavior.cpp

445 lines
12 KiB
C++
Raw Normal View History

2021-05-16 12:53:32 +02:00
/*
* DefenceBehavior.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 "DefenceBehavior.h"
2021-05-16 14:39:38 +02:00
#include "../AIGateway.h"
2021-05-16 12:53:32 +02:00
#include "../Engine/Nullkiller.h"
#include "../AIUtility.h"
#include "../Goals/BuyArmy.h"
#include "../Goals/ExecuteHeroChain.h"
2021-05-16 14:00:24 +02:00
#include "../Behaviors/CaptureObjectsBehavior.h"
#include "../Goals/RecruitHero.h"
#include "../Goals/DismissHero.h"
2021-05-16 14:00:24 +02:00
#include "../Goals/Composition.h"
2023-02-28 09:07:59 +02:00
#include "../Goals/CaptureObject.h"
2021-05-16 14:00:24 +02:00
#include "../Markers/DefendTown.h"
2021-05-16 12:53:32 +02:00
#include "../Goals/ExchangeSwapTownHeroes.h"
2022-09-26 20:01:07 +02:00
namespace NKAI
{
2021-05-16 12:53:32 +02:00
extern boost::thread_specific_ptr<CCallback> cb;
2021-05-16 14:39:38 +02:00
extern boost::thread_specific_ptr<AIGateway> ai;
2021-05-16 12:53:32 +02:00
const float TREAT_IGNORE_RATIO = 2;
2023-02-28 09:07:59 +02:00
2021-05-16 12:53:32 +02:00
using namespace Goals;
std::string DefenceBehavior::toString() const
{
return "Defend towns";
}
Goals::TGoalVec DefenceBehavior::decompose() const
2021-05-16 12:53:32 +02:00
{
Goals::TGoalVec tasks;
for(auto town : cb->getTownsInfo())
2021-05-16 12:53:32 +02:00
{
evaluateDefence(tasks, town);
2021-05-16 12:53:32 +02:00
}
return tasks;
}
void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town) const
2021-05-16 12:53:32 +02:00
{
logAi->trace("Evaluating defence for %s", town->getNameTranslated());
2021-05-16 12:53:32 +02:00
auto treatNode = ai->nullkiller->dangerHitMap->getObjectTreat(town);
2023-06-11 18:21:50 +02:00
std::vector<HitMapInfo> treats = ai->nullkiller->dangerHitMap->getTownTreats(town);
treats.push_back(treatNode.fastestDanger); // no guarantee that fastest danger will be there
2021-05-16 12:53:32 +02:00
2021-05-16 13:09:49 +02:00
int dayOfWeek = cb->getDate(Date::DAY_OF_WEEK);
2021-05-16 12:53:32 +02:00
if(town->garrisonHero)
{
if(ai->nullkiller->isHeroLocked(town->garrisonHero.get()))
2021-05-16 12:53:32 +02:00
{
logAi->trace(
"Hero %s in garrison of town %s is suposed to defend the town",
town->garrisonHero->getNameTranslated(),
town->getNameTranslated());
2021-05-16 13:22:37 +02:00
return;
2021-05-16 12:53:32 +02:00
}
if(!town->visitingHero && cb->getHeroCount(ai->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER)
{
logAi->trace(
"Extracting hero %s from garrison of town %s",
town->garrisonHero->getNameTranslated(),
town->getNameTranslated());
2021-05-16 12:53:32 +02:00
tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5)));
return;
}
2021-05-16 12:53:32 +02:00
}
if(!treatNode.fastestDanger.hero)
{
logAi->trace("No treat found for town %s", town->getNameTranslated());
return;
}
2021-05-16 13:09:49 +02:00
uint64_t reinforcement = ai->nullkiller->armyManager->howManyReinforcementsCanBuy(town->getUpperArmy(), town);
2021-05-16 12:53:32 +02:00
if(reinforcement)
{
logAi->trace("Town %s can buy defence army %lld", town->getNameTranslated(), reinforcement);
2021-05-16 12:53:32 +02:00
tasks.push_back(Goals::sptr(Goals::BuyArmy(town, reinforcement).setpriority(0.5f)));
}
auto paths = ai->nullkiller->pathfinder->getPathInfo(town->visitablePos());
2021-05-16 12:53:32 +02:00
for(auto & treat : treats)
{
2022-09-06 20:14:22 +02:00
logAi->trace(
2021-05-16 13:09:49 +02:00
"Town %s has treat %lld in %s turns, hero: %s",
town->getNameTranslated(),
2021-05-16 12:53:32 +02:00
treat.danger,
std::to_string(treat.turn),
treat.hero->getNameTranslated());
2021-05-16 12:53:32 +02:00
2021-05-16 13:09:49 +02:00
bool treatIsUnderControl = false;
for(AIPath & path : paths)
{
if(town->visitingHero && path.targetHero == town->visitingHero.get())
{
if(path.getHeroStrength() < town->visitingHero->getHeroStrength())
continue;
}
else if(town->garrisonHero && path.targetHero == town->garrisonHero.get())
{
if(path.getHeroStrength() < town->visitingHero->getHeroStrength())
continue;
}
else
{
if(town->visitingHero)
continue;
}
2022-09-06 20:14:22 +02:00
2023-02-28 09:07:59 +02:00
if(treat.hero.validAndSet()
&& treat.turn <= 1
2023-06-11 18:21:50 +02:00
&& (treat.danger == treatNode.maximumDanger.danger || treat.turn < treatNode.maximumDanger.turn))
2021-05-16 13:09:49 +02:00
{
2023-06-11 18:21:50 +02:00
auto heroCapturingPaths = ai->nullkiller->pathfinder->getPathInfo(treat.hero->visitablePos());
auto goals = CaptureObjectsBehavior::getVisitGoals(heroCapturingPaths, treat.hero.get());
for(int i = 0; i < heroCapturingPaths.size(); i++)
{
AIPath & path = heroCapturingPaths[i];
TSubgoal goal = goals[i];
2023-02-28 09:07:59 +02:00
2023-06-11 18:21:50 +02:00
if(!goal || goal->invalid() || !goal->isElementar()) continue;
2023-02-28 09:07:59 +02:00
2023-06-11 18:21:50 +02:00
Composition composition;
composition.addNext(DefendTown(town, treat, path, true)).addNext(goal);
tasks.push_back(Goals::sptr(composition));
}
2023-02-28 09:07:59 +02:00
}
bool treatIsWeak = path.getHeroStrength() / (float)treat.danger > TREAT_IGNORE_RATIO;
2023-02-28 09:07:59 +02:00
bool needToSaveGrowth = treat.turn == 0 && dayOfWeek == 7;
if(treatIsWeak && !needToSaveGrowth)
{
if((path.exchangeCount == 1 && path.turn() < treat.turn)
|| path.turn() < treat.turn - 1
|| (path.turn() < treat.turn && treat.turn >= 2))
2021-05-16 13:09:49 +02:00
{
2022-09-26 20:01:07 +02:00
#if NKAI_TRACE_LEVEL >= 1
2022-09-06 20:14:22 +02:00
logAi->trace(
2021-05-16 13:09:49 +02:00
"Hero %s can eliminate danger for town %s using path %s.",
2023-02-28 09:07:59 +02:00
path.targetHero->getObjectName(),
town->getObjectName(),
2021-05-16 13:09:49 +02:00
path.toString());
2022-09-06 20:14:22 +02:00
#endif
2021-05-16 13:09:49 +02:00
treatIsUnderControl = true;
2023-02-28 09:07:59 +02:00
2021-05-16 13:09:49 +02:00
break;
}
}
}
if(treatIsUnderControl)
continue;
evaluateRecruitingHero(tasks, treat, town);
2021-05-16 13:09:49 +02:00
if(paths.empty())
{
logAi->trace("No ways to defend town %s", town->getNameTranslated());
continue;
}
2021-05-16 14:00:24 +02:00
std::vector<int> pathsToDefend;
std::map<const CGHeroInstance *, std::vector<int>> defferedPaths;
2021-05-16 14:00:24 +02:00
for(int i = 0; i < paths.size(); i++)
2021-05-16 12:53:32 +02:00
{
2021-05-16 14:00:24 +02:00
auto & path = paths[i];
2022-09-26 20:01:07 +02:00
#if NKAI_TRACE_LEVEL >= 1
2021-05-16 12:53:32 +02:00
logAi->trace(
2021-05-16 13:09:49 +02:00
"Hero %s can defend town with force %lld in %s turns, cost: %f, path: %s",
2023-02-28 09:07:59 +02:00
path.targetHero->getObjectName(),
2021-05-16 12:53:32 +02:00
path.getHeroStrength(),
std::to_string(path.turn()),
2021-05-16 13:09:49 +02:00
path.movementCost(),
2021-05-16 12:53:32 +02:00
path.toString());
#endif
if(path.turn() <= treat.turn - 2)
{
2022-09-26 20:01:07 +02:00
#if NKAI_TRACE_LEVEL >= 1
2023-03-06 13:19:49 +02:00
logAi->trace("Defer defence of %s by %s because he has enough time to reach the town next trun",
2023-02-28 09:07:59 +02:00
town->getObjectName(),
path.targetHero->getObjectName());
2022-09-06 20:14:22 +02:00
#endif
2021-05-16 14:00:24 +02:00
defferedPaths[path.targetHero].push_back(i);
continue;
}
2021-05-16 12:53:32 +02:00
if(path.targetHero == town->visitingHero.get() && path.exchangeCount == 1)
2021-05-16 12:53:32 +02:00
{
2022-09-26 20:01:07 +02:00
#if NKAI_TRACE_LEVEL >= 1
2021-05-16 14:00:24 +02:00
logAi->trace("Put %s to garrison of town %s",
2023-02-28 09:07:59 +02:00
path.targetHero->getObjectName(),
town->getObjectName());
2021-05-16 12:53:32 +02:00
#endif
2021-05-16 14:00:24 +02:00
// dismiss creatures we are not able to pick to be able to hide in garrison
if(town->garrisonHero
|| town->getUpperArmy()->stacksCount() == 0
|| (town->getUpperArmy()->getArmyStrength() < 500 && town->fortLevel() >= CGTownInstance::CITADEL))
2021-05-16 14:00:24 +02:00
{
tasks.push_back(
Goals::sptr(Composition()
.addNext(DefendTown(town, treat, path.targetHero))
.addNext(ExchangeSwapTownHeroes(town, town->visitingHero.get(), HeroLockedReason::DEFENCE))));
}
2021-05-16 13:07:54 +02:00
continue;
2021-05-16 12:53:32 +02:00
}
// main without army and visiting scout with army, very specific case
if(town->visitingHero && town->getUpperArmy()->stacksCount() == 0
&& path.targetHero != town->visitingHero.get() && path.exchangeCount == 1 && path.turn() == 0
&& ai->nullkiller->heroManager->evaluateHero(path.targetHero) > ai->nullkiller->heroManager->evaluateHero(town->visitingHero.get())
&& 10 * path.targetHero->getTotalStrength() < town->visitingHero->getTotalStrength())
{
path.heroArmy = town->visitingHero.get();
tasks.push_back(
Goals::sptr(Composition()
.addNext(DefendTown(town, treat, path))
.addNextSequence({
sptr(ExchangeSwapTownHeroes(town, town->visitingHero.get())),
sptr(ExecuteHeroChain(path, town)),
sptr(ExchangeSwapTownHeroes(town, path.targetHero, HeroLockedReason::DEFENCE))
})));
continue;
}
2021-05-16 13:07:54 +02:00
if(treat.turn == 0 || (path.turn() <= treat.turn && path.getHeroStrength() * SAFE_ATTACK_CONSTANT >= treat.danger))
2021-05-16 12:53:32 +02:00
{
if(ai->nullkiller->arePathHeroesLocked(path))
{
2022-09-26 20:01:07 +02:00
#if NKAI_TRACE_LEVEL >= 1
2021-05-16 14:00:24 +02:00
logAi->trace("Can not move %s to defend town %s. Path is locked.",
2023-02-28 09:07:59 +02:00
path.targetHero->getObjectName(),
town->getObjectName());
#endif
continue;
}
2021-05-16 14:00:24 +02:00
pathsToDefend.push_back(i);
}
}
2021-05-16 14:00:24 +02:00
for(int i : pathsToDefend)
{
2021-05-16 14:00:24 +02:00
AIPath & path = paths[i];
2021-05-16 14:00:24 +02:00
for(int j : defferedPaths[path.targetHero])
{
2021-05-16 14:00:24 +02:00
AIPath & defferedPath = paths[j];
if(defferedPath.getHeroStrength() >= path.getHeroStrength()
&& defferedPath.turn() <= path.turn())
{
continue;
}
}
Composition composition;
composition.addNext(DefendTown(town, treat, path));
TGoalVec sequence;
if(town->visitingHero && path.targetHero != town->visitingHero && !path.containsHero(town->visitingHero))
{
if(town->garrisonHero)
{
if(ai->nullkiller->heroManager->getHeroRole(town->visitingHero.get()) == HeroRole::SCOUT
&& town->visitingHero->getArmyStrength() < path.heroArmy->getArmyStrength() / 20)
{
if(path.turn() == 0)
sequence.push_back(sptr(DismissHero(town->visitingHero.get())));
}
else
{
2022-09-26 20:01:07 +02:00
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Cancel moving %s to defend town %s as the town has garrison hero",
path.targetHero->getObjectName(),
town->getObjectName());
2021-05-16 14:00:24 +02:00
#endif
continue;
}
}
else if(path.turn() == 0)
{
sequence.push_back(sptr(ExchangeSwapTownHeroes(town, town->visitingHero.get())));
}
}
2021-05-16 14:00:24 +02:00
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Move %s to defend town %s",
path.targetHero->getObjectName(),
town->getObjectName());
#endif
sequence.push_back(sptr(ExecuteHeroChain(path, town)));
composition.addNextSequence(sequence);
2021-05-16 14:00:24 +02:00
auto firstBlockedAction = path.getFirstBlockedAction();
if(firstBlockedAction)
{
auto subGoal = firstBlockedAction->decompose(path.targetHero);
2022-09-26 20:01:07 +02:00
#if NKAI_TRACE_LEVEL >= 2
2021-05-16 14:00:24 +02:00
logAi->trace("Decomposing special action %s returns %s", firstBlockedAction->toString(), subGoal->toString());
2021-05-16 12:53:32 +02:00
#endif
2021-05-16 14:00:24 +02:00
if(subGoal->invalid())
{
2022-09-26 20:01:07 +02:00
#if NKAI_TRACE_LEVEL >= 1
2021-05-16 14:00:24 +02:00
logAi->trace("Path is invalid. Skipping");
#endif
continue;
}
composition.addNext(subGoal);
}
tasks.push_back(Goals::sptr(composition));
2021-05-16 12:53:32 +02:00
}
}
logAi->debug("Found %d tasks", tasks.size());
}
2022-09-26 20:01:07 +02:00
void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & treat, const CGTownInstance * town) const
{
if(town->hasBuilt(BuildingID::TAVERN)
&& cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST)
{
auto heroesInTavern = cb->getAvailableHeroes(town);
for(auto hero : heroesInTavern)
{
if(hero->getTotalStrength() < treat.danger)
continue;
auto myHeroes = cb->getHeroesInfo();
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Hero %s can be recruited to defend %s", hero->getObjectName(), town->getObjectName());
#endif
bool needSwap = false;
const CGHeroInstance * heroToDismiss = nullptr;
if(town->visitingHero)
{
if(!town->garrisonHero)
needSwap = true;
else
{
if(town->visitingHero->getArmyStrength() < town->garrisonHero->getArmyStrength())
{
if(town->visitingHero->getArmyStrength() >= hero->getArmyStrength())
continue;
heroToDismiss = town->visitingHero.get();
}
else if(town->garrisonHero->getArmyStrength() >= hero->getArmyStrength())
continue;
else
{
needSwap = true;
heroToDismiss = town->garrisonHero.get();
}
}
}
else if(ai->nullkiller->heroManager->heroCapReached())
{
const CGHeroInstance * weakestHero = nullptr;
for(auto existingHero : myHeroes)
{
if(ai->nullkiller->isHeroLocked(existingHero)
|| existingHero->getArmyStrength() > hero->getArmyStrength()
|| ai->nullkiller->heroManager->getHeroRole(existingHero) == HeroRole::MAIN
|| existingHero->movementPointsRemaining()
|| existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1))
continue;
if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength())
{
weakestHero = existingHero;
}
}
if(!weakestHero)
continue;
heroToDismiss = weakestHero;
}
TGoalVec sequence;
Goals::Composition recruitHeroComposition;
if(needSwap)
sequence.push_back(sptr(ExchangeSwapTownHeroes(town, town->visitingHero.get())));
if(heroToDismiss)
sequence.push_back(sptr(DismissHero(heroToDismiss)));
sequence.push_back(sptr(Goals::RecruitHero(town, hero)));
tasks.push_back(sptr(Goals::Composition().addNext(DefendTown(town, treat, hero)).addNextSequence(sequence)));
}
}
}
2022-09-26 20:01:07 +02:00
}