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"
|
|
|
|
#include "../VCAI.h"
|
|
|
|
#include "../Engine/Nullkiller.h"
|
|
|
|
#include "../AIhelper.h"
|
|
|
|
#include "../AIUtility.h"
|
|
|
|
#include "../Goals/BuyArmy.h"
|
|
|
|
#include "../Goals/VisitTile.h"
|
|
|
|
#include "../Goals/ExecuteHeroChain.h"
|
2021-05-16 13:11:35 +02:00
|
|
|
#include "../Goals/DismissHero.h"
|
2021-05-16 12:53:32 +02:00
|
|
|
#include "../Goals/ExchangeSwapTownHeroes.h"
|
|
|
|
#include "lib/mapping/CMap.h" //for victory conditions
|
|
|
|
#include "lib/CPathfinder.h"
|
|
|
|
|
|
|
|
extern boost::thread_specific_ptr<CCallback> cb;
|
|
|
|
extern boost::thread_specific_ptr<VCAI> ai;
|
|
|
|
extern FuzzyHelper * fh;
|
|
|
|
|
|
|
|
using namespace Goals;
|
|
|
|
|
|
|
|
std::string DefenceBehavior::toString() const
|
|
|
|
{
|
|
|
|
return "Defend towns";
|
|
|
|
}
|
|
|
|
|
|
|
|
Goals::TGoalVec DefenceBehavior::getTasks()
|
|
|
|
{
|
|
|
|
Goals::TGoalVec tasks;
|
|
|
|
|
|
|
|
auto heroes = cb->getHeroesInfo();
|
|
|
|
|
|
|
|
if(heroes.size())
|
|
|
|
{
|
|
|
|
for(auto town : cb->getTownsInfo())
|
|
|
|
{
|
|
|
|
evaluateDefence(tasks, town);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return tasks;
|
|
|
|
}
|
|
|
|
|
2021-05-16 13:09:49 +02:00
|
|
|
uint64_t townArmyIncome(const CGTownInstance * town)
|
|
|
|
{
|
|
|
|
uint64_t result = 0;
|
|
|
|
|
|
|
|
for(auto creatureInfo : town->creatures)
|
|
|
|
{
|
|
|
|
if(creatureInfo.second.empty())
|
|
|
|
continue;
|
|
|
|
|
|
|
|
auto creature = creatureInfo.second.back().toCreature();
|
|
|
|
result += creature->AIValue * town->getGrowthInfo(creature->level).totalGrowth();
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2021-05-16 12:53:32 +02:00
|
|
|
void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town)
|
|
|
|
{
|
2021-05-16 13:09:49 +02:00
|
|
|
auto basicPriority = 0.3f + std::sqrt(townArmyIncome(town) / 20000.0f)
|
|
|
|
+ town->dailyIncome()[Res::GOLD] / 10000.0f;
|
|
|
|
|
|
|
|
logAi->debug("Evaluating defence for %s, basic priority %f", town->name, basicPriority);
|
2021-05-16 12:53:32 +02:00
|
|
|
|
|
|
|
auto treatNode = ai->nullkiller->dangerHitMap->getObjectTreat(town);
|
|
|
|
auto treats = { treatNode.fastestDanger, treatNode.maximumDanger };
|
|
|
|
|
2021-05-16 13:09:49 +02:00
|
|
|
if(!treatNode.fastestDanger.hero)
|
|
|
|
{
|
|
|
|
logAi->debug("No treat found for town %s", town->name);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
int dayOfWeek = cb->getDate(Date::DAY_OF_WEEK);
|
|
|
|
|
2021-05-16 12:53:32 +02:00
|
|
|
if(town->garrisonHero)
|
|
|
|
{
|
2021-05-16 13:07:54 +02:00
|
|
|
if(!ai->nullkiller->isHeroLocked(town->garrisonHero.get()))
|
2021-05-16 12:53:32 +02:00
|
|
|
{
|
2021-05-16 13:22:37 +02:00
|
|
|
if(!town->visitingHero)
|
|
|
|
{
|
|
|
|
tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5)));
|
|
|
|
}
|
|
|
|
|
2021-05-16 12:53:32 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
logAi->debug(
|
|
|
|
"Hero %s in garrison of town %s is suposed to defend the town",
|
2021-05-16 13:07:54 +02:00
|
|
|
town->garrisonHero->name,
|
|
|
|
town->name);
|
2021-05-16 12:53:32 +02:00
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
2021-05-16 13:09:49 +02:00
|
|
|
|
2021-05-16 12:53:32 +02:00
|
|
|
uint64_t reinforcement = ai->ah->howManyReinforcementsCanBuy(town->getUpperArmy(), town);
|
|
|
|
|
|
|
|
if(reinforcement)
|
|
|
|
{
|
|
|
|
logAi->debug("Town %s can buy defence army %lld", town->name, reinforcement);
|
|
|
|
tasks.push_back(Goals::sptr(Goals::BuyArmy(town, reinforcement).setpriority(0.5f)));
|
|
|
|
}
|
|
|
|
|
|
|
|
auto paths = ai->ah->getPathsToTile(town->visitablePos());
|
|
|
|
|
|
|
|
for(auto & treat : treats)
|
|
|
|
{
|
|
|
|
logAi->debug(
|
2021-05-16 13:09:49 +02:00
|
|
|
"Town %s has treat %lld in %s turns, hero: %s",
|
2021-05-16 12:53:32 +02:00
|
|
|
town->name,
|
|
|
|
treat.danger,
|
|
|
|
std::to_string(treat.turn),
|
|
|
|
treat.hero->name);
|
|
|
|
|
2021-05-16 13:09:49 +02:00
|
|
|
bool treatIsUnderControl = false;
|
|
|
|
|
|
|
|
for(AIPath & path : paths)
|
|
|
|
{
|
|
|
|
if(path.getHeroStrength() > treat.danger)
|
|
|
|
{
|
2021-05-16 13:11:35 +02:00
|
|
|
if(path.turn() <= treat.turn && dayOfWeek + treat.turn < 6 && isSafeToVisit(path.targetHero, path.heroArmy, treat.danger)
|
2021-05-16 13:09:49 +02:00
|
|
|
|| path.exchangeCount == 1 && path.turn() < treat.turn
|
|
|
|
|| path.turn() < treat.turn - 1)
|
|
|
|
{
|
|
|
|
logAi->debug(
|
|
|
|
"Hero %s can eliminate danger for town %s using path %s.",
|
|
|
|
path.targetHero->name,
|
|
|
|
town->name,
|
|
|
|
path.toString());
|
|
|
|
|
|
|
|
treatIsUnderControl = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(treatIsUnderControl)
|
|
|
|
continue;
|
|
|
|
|
2019-12-15 15:47:21 +02:00
|
|
|
if(!town->visitingHero
|
|
|
|
&& town->hasBuilt(BuildingID::TAVERN)
|
|
|
|
&& cb->getResourceAmount(Res::GOLD) > GameConstants::HERO_GOLD_COST)
|
2021-05-16 13:09:49 +02:00
|
|
|
{
|
|
|
|
auto heroesInTavern = cb->getAvailableHeroes(town);
|
|
|
|
|
|
|
|
for(auto hero : heroesInTavern)
|
|
|
|
{
|
|
|
|
if(hero->getTotalStrength() > treat.danger)
|
|
|
|
{
|
2021-05-16 13:11:35 +02:00
|
|
|
auto myHeroes = cb->getHeroesInfo();
|
|
|
|
|
|
|
|
if(cb->getHeroesInfo().size() < ALLOWED_ROAMING_HEROES)
|
|
|
|
{
|
|
|
|
logAi->debug("Hero %s can be recruited to defend %s", hero->name, town->name);
|
|
|
|
tasks.push_back(Goals::sptr(Goals::RecruitHero().settown(town).setobjid(hero->id.getNum()).setpriority(1)));
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
const CGHeroInstance * weakestHero = nullptr;
|
|
|
|
|
|
|
|
for(auto existingHero : myHeroes)
|
|
|
|
{
|
|
|
|
if(ai->nullkiller->isHeroLocked(existingHero)
|
|
|
|
|| existingHero->getArmyStrength() > hero->getArmyStrength()
|
|
|
|
|| ai->ah->getHeroRole(existingHero) == HeroRole::MAIN
|
|
|
|
|| existingHero->movement
|
|
|
|
|| existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1))
|
|
|
|
continue;
|
|
|
|
|
|
|
|
if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength())
|
|
|
|
{
|
|
|
|
weakestHero = existingHero;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(weakestHero)
|
|
|
|
{
|
|
|
|
tasks.push_back(Goals::sptr(Goals::DismissHero(weakestHero)));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-05-16 13:09:49 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-16 13:11:35 +02:00
|
|
|
if(paths.empty())
|
|
|
|
{
|
|
|
|
logAi->debug("No ways to defend town %s", town->name);
|
|
|
|
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2021-05-16 12:53:32 +02:00
|
|
|
for(AIPath & path : paths)
|
|
|
|
{
|
|
|
|
#if AI_TRACE_LEVEL >= 1
|
|
|
|
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",
|
2021-05-16 12:53:32 +02:00
|
|
|
path.targetHero->name,
|
|
|
|
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
|
2021-05-16 13:13:40 +02:00
|
|
|
if(path.turn() <= treat.turn - 2)
|
|
|
|
{
|
|
|
|
logAi->trace("Deffer defence of %s by %s because he has enough time to rich the town next trun",
|
|
|
|
town->name,
|
|
|
|
path.targetHero->name);
|
|
|
|
|
|
|
|
continue;
|
|
|
|
}
|
2021-05-16 12:53:32 +02:00
|
|
|
|
2021-05-16 13:09:49 +02:00
|
|
|
float priority = basicPriority
|
|
|
|
+ std::min(SAFE_ATTACK_CONSTANT, (float)path.getHeroStrength() / treat.danger) / (treat.turn + 1);
|
2021-05-16 12:53:32 +02:00
|
|
|
|
|
|
|
if(path.targetHero == town->visitingHero && path.exchangeCount == 1)
|
|
|
|
{
|
|
|
|
#if AI_TRACE_LEVEL >= 1
|
2021-05-16 13:07:54 +02:00
|
|
|
logAi->trace("Put %s to garrison of town %s with priority %f",
|
2021-05-16 12:53:32 +02:00
|
|
|
path.targetHero->name,
|
|
|
|
town->name,
|
|
|
|
priority);
|
|
|
|
#endif
|
|
|
|
|
2021-05-16 13:13:56 +02:00
|
|
|
tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, town->visitingHero.get(), HeroLockedReason::DEFENCE).setpriority(priority)));
|
2021-05-16 13:07:54 +02:00
|
|
|
|
|
|
|
continue;
|
2021-05-16 12:53:32 +02:00
|
|
|
}
|
2021-05-16 13:07:54 +02:00
|
|
|
|
2021-05-16 13:09:49 +02:00
|
|
|
if(path.turn() <= treat.turn && path.getHeroStrength() * SAFE_ATTACK_CONSTANT >= treat.danger)
|
2021-05-16 12:53:32 +02:00
|
|
|
{
|
2021-05-16 13:13:56 +02:00
|
|
|
if(ai->nullkiller->arePathHeroesLocked(path))
|
|
|
|
{
|
|
|
|
#if AI_TRACE_LEVEL >= 1
|
|
|
|
logAi->trace("Can not move %s to defend town %s with priority %f. Path is locked.",
|
|
|
|
path.targetHero->name,
|
|
|
|
town->name,
|
|
|
|
priority);
|
|
|
|
|
|
|
|
#endif
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2021-05-16 12:53:32 +02:00
|
|
|
#if AI_TRACE_LEVEL >= 1
|
|
|
|
logAi->trace("Move %s to defend town %s with priority %f",
|
|
|
|
path.targetHero->name,
|
|
|
|
town->name,
|
|
|
|
priority);
|
|
|
|
#endif
|
|
|
|
|
|
|
|
tasks.push_back(Goals::sptr(Goals::ExecuteHeroChain(path, town).setpriority(priority)));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
logAi->debug("Found %d tasks", tasks.size());
|
|
|
|
|
|
|
|
/*for(auto & treat : treats)
|
|
|
|
{
|
|
|
|
auto paths = ai->ah->getPathsToTile(treat.hero->visitablePos());
|
|
|
|
|
|
|
|
for(AIPath & path : paths)
|
|
|
|
{
|
|
|
|
tasks.push_back(Goals::sptr(Goals::ExecuteHeroChain(path)));
|
|
|
|
}
|
|
|
|
}*/
|
|
|
|
}
|