1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-04-13 11:40:38 +02:00

Nullkiller AI: further stabilisation, implement staged hero chain (first with limit 0 turns then 1 turn)

This commit is contained in:
Andrii Danylchenko 2021-05-16 14:13:56 +03:00 committed by Andrii Danylchenko
parent 6bebb766a6
commit eea5cb7f0b
14 changed files with 118 additions and 45 deletions

View File

@ -99,7 +99,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getTasks()
auto hero = path.targetHero;
auto danger = path.getTotalDanger();
if(danger == 0 && path.exchangeCount > 1)
if(ai->ah->getHeroRole(hero) == HeroRole::SCOUT && danger == 0 && path.exchangeCount > 1)
continue;
auto isSafe = isSafeToVisit(hero, path.heroArmy, danger);
@ -131,13 +131,13 @@ Goals::TGoalVec CaptureObjectsBehavior::getTasks()
for(auto way : waysToVisitObj)
{
if(ai->nullkiller->arePathHeroesLocked(way->getPath()))
continue;
way->evaluationContext.closestWayRatio
= way->evaluationContext.movementCost / closestWay->evaluationContext.movementCost;
if(way->hero && ai->nullkiller->canMove(way->hero.h))
{
tasks.push_back(sptr(*way));
}
tasks.push_back(sptr(*way));
}
}
};

View File

@ -227,13 +227,25 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
priority);
#endif
tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, town->visitingHero.get()).setpriority(priority)));
tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, town->visitingHero.get(), HeroLockedReason::DEFENCE).setpriority(priority)));
continue;
}
if(path.turn() <= treat.turn && path.getHeroStrength() * SAFE_ATTACK_CONSTANT >= treat.danger)
{
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;
}
#if AI_TRACE_LEVEL >= 1
logAi->trace("Move %s to defend town %s with priority %f",
path.targetHero->name,
@ -242,8 +254,6 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
#endif
tasks.push_back(Goals::sptr(Goals::ExecuteHeroChain(path, town).setpriority(priority)));
continue;
}
}
}

View File

@ -18,6 +18,7 @@
#include "lib/mapping/CMap.h" //for victory conditions
#include "lib/mapObjects/MapObjects.h" //for victory conditions
#include "lib/CPathfinder.h"
#include "../Engine/Nullkiller.h"
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<VCAI> ai;
@ -147,17 +148,17 @@ Goals::TGoalVec StartupBehavior::getTasks()
{
if(canRecruitHero || ai->ah->howManyReinforcementsCanGet(visitingHero, garrisonHero) > 200)
{
tasks.push_back(Goals::sptr(ExchangeSwapTownHeroes(startupTown, visitingHero).setpriority(100)));
tasks.push_back(Goals::sptr(ExchangeSwapTownHeroes(startupTown, visitingHero, HeroLockedReason::STARTUP).setpriority(100)));
}
}
else if(ai->ah->howManyReinforcementsCanGet(garrisonHero, visitingHero) > 200)
{
tasks.push_back(Goals::sptr(ExchangeSwapTownHeroes(startupTown, garrisonHero).setpriority(100)));
tasks.push_back(Goals::sptr(ExchangeSwapTownHeroes(startupTown, garrisonHero, HeroLockedReason::STARTUP).setpriority(100)));
}
}
else if(canRecruitHero)
{
tasks.push_back(Goals::sptr(ExchangeSwapTownHeroes(startupTown, visitingHero).setpriority(100)));
tasks.push_back(Goals::sptr(ExchangeSwapTownHeroes(startupTown, visitingHero, HeroLockedReason::STARTUP).setpriority(100)));
}
}
}
@ -171,7 +172,7 @@ Goals::TGoalVec StartupBehavior::getTasks()
{
for(const CGTownInstance * town : towns)
{
if(town->garrisonHero && town->garrisonHero->movement)
if(town->garrisonHero && town->garrisonHero->movement && ai->nullkiller->getHeroLockedReason(town->garrisonHero) != HeroLockedReason::DEFENCE)
tasks.push_back(Goals::sptr(ExchangeSwapTownHeroes(town, nullptr).setpriority(0.0001f)));
}
}

View File

@ -79,13 +79,26 @@ void Nullkiller::updateAiState()
auto activeHeroes = ai->getMyHeroes();
vstd::erase_if(activeHeroes, [this](const HeroPtr & hero) -> bool{
return isHeroLocked(hero.h);
auto lockedReason = getHeroLockedReason(hero.h);
return lockedReason == HeroLockedReason::DEFENCE || lockedReason == HeroLockedReason::STARTUP;
});
ai->ah->updatePaths(activeHeroes, true);
ai->ah->updateHeroRoles();
}
bool Nullkiller::arePathHeroesLocked(const AIPath & path) const
{
for(auto & node : path.nodes)
{
if(isHeroLocked(node.targetHero))
return true;
}
return false;
}
void Nullkiller::makeTurn()
{
resetAiState();
@ -97,17 +110,14 @@ void Nullkiller::makeTurn()
Goals::TGoalVec bestTasks = {
choseBestTask(std::make_shared<BuyArmyBehavior>()),
choseBestTask(std::make_shared<CaptureObjectsBehavior>()),
choseBestTask(std::make_shared<RecruitHeroBehavior>())
choseBestTask(std::make_shared<RecruitHeroBehavior>()),
choseBestTask(std::make_shared<DefenceBehavior>())
};
if(cb->getDate(Date::DAY) == 1)
{
bestTasks.push_back(choseBestTask(std::make_shared<StartupBehavior>()));
}
else
{
bestTasks.push_back(choseBestTask(std::make_shared<DefenceBehavior>()));
}
Goals::TSubgoal bestTask = choseBestTask(bestTasks);

View File

@ -14,12 +14,23 @@
#include "../Goals/AbstractGoal.h"
#include "../Behaviors/Behavior.h"
enum class HeroLockedReason
{
NOT_LOCKED = 0,
STARTUP = 1,
DEFENCE = 2,
HERO_CHAIN = 3
};
class Nullkiller
{
private:
std::unique_ptr<PriorityEvaluator> priorityEvaluator;
const CGHeroInstance * activeHero;
std::set<const CGHeroInstance *> lockedHeroes;
std::map<const CGHeroInstance *, HeroLockedReason> lockedHeroes;
public:
std::unique_ptr<DangerHitMapAnalyzer> dangerHitMap;
@ -28,10 +39,11 @@ public:
void makeTurn();
bool isActive(const CGHeroInstance * hero) const { return activeHero == hero; }
bool isHeroLocked(const CGHeroInstance * hero) const { return vstd::contains(lockedHeroes, hero); }
HeroLockedReason getHeroLockedReason(const CGHeroInstance * hero) const { return isHeroLocked(hero) ? lockedHeroes.at(hero) : HeroLockedReason::NOT_LOCKED; }
void setActive(const CGHeroInstance * hero) { activeHero = hero; }
void lockHero(const CGHeroInstance * hero) { lockedHeroes.insert(hero); }
void lockHero(const CGHeroInstance * hero, HeroLockedReason lockReason) { lockedHeroes[hero] = lockReason; }
void unlockHero(const CGHeroInstance * hero) { lockedHeroes.erase(hero); }
bool canMove(const CGHeroInstance * hero) { return hero->movement; }
bool arePathHeroesLocked(const AIPath & path) const;
private:
void resetAiState();

View File

@ -201,10 +201,10 @@ float getEnemyHeroStrategicalValue(const CGHeroInstance * enemy)
for(auto obj : objectsUnderTreat)
{
objectValue += getStrategicalValue(obj);
vstd::amax(objectValue, getStrategicalValue(obj));
}
return objectValue + enemy->level / 15.0f;
return objectValue / 2.0f + enemy->level / 15.0f;
}
float getStrategicalValue(const CGObjectInstance * target)
@ -400,7 +400,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
assert(result >= 0);
#ifdef VCMI_TRACE_PATHFINDER
logAi->trace("Evaluated %s, hero %s, loss: %f, turns: %f, gold: %d, army gain: %d, danger: %d, role: %s, result %f",
logAi->trace("Evaluated %s, hero %s, loss: %f, turns: %f, gold: %d, army gain: %d, danger: %d, role: %s, strategical value: %f, result %f",
task->name(),
hero->name,
armyLossPersentage,
@ -409,6 +409,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
armyReward,
danger,
heroRole ? "scout" : "main",
strategicalValue,
result);
#endif

View File

@ -23,8 +23,11 @@ extern FuzzyHelper * fh;
using namespace Goals;
ExchangeSwapTownHeroes::ExchangeSwapTownHeroes(const CGTownInstance * town, const CGHeroInstance * garrisonHero)
:CGoal(Goals::EXCHANGE_SWAP_TOWN_HEROES), town(town), garrisonHero(garrisonHero)
ExchangeSwapTownHeroes::ExchangeSwapTownHeroes(
const CGTownInstance * town,
const CGHeroInstance * garrisonHero,
HeroLockedReason lockingReason)
:CGoal(Goals::EXCHANGE_SWAP_TOWN_HEROES), town(town), garrisonHero(garrisonHero), lockingReason(lockingReason)
{
}
@ -90,7 +93,7 @@ void ExchangeSwapTownHeroes::accept(VCAI * ai)
cb->swapGarrisonHero(town); // selected hero left in garrison with strongest army
}
ai->nullkiller->lockHero(garrisonHero);
ai->nullkiller->lockHero(garrisonHero, lockingReason);
if(town->visitingHero && town->visitingHero != garrisonHero)
{

View File

@ -10,6 +10,7 @@
#pragma once
#include "CGoal.h"
#include "..\Engine\Nullkiller.h"
namespace Goals
{
@ -18,9 +19,13 @@ namespace Goals
private:
const CGTownInstance * town;
const CGHeroInstance * garrisonHero;
HeroLockedReason lockingReason;
public:
ExchangeSwapTownHeroes(const CGTownInstance * town, const CGHeroInstance * garrisonHero = nullptr);
ExchangeSwapTownHeroes(
const CGTownInstance * town,
const CGHeroInstance * garrisonHero = nullptr,
HeroLockedReason lockingReason = HeroLockedReason::NOT_LOCKED);
TGoalVec getAllPossibleSubgoals() override
{

View File

@ -69,7 +69,7 @@ void ExecuteHeroChain::accept(VCAI * ai)
if(vstd::contains(blockedIndexes, i))
{
blockedIndexes.insert(node.parentIndex);
ai->nullkiller->lockHero(hero.get());
ai->nullkiller->lockHero(hero.get(), HeroLockedReason::HERO_CHAIN);
continue;
}
@ -128,7 +128,7 @@ void ExecuteHeroChain::accept(VCAI * ai)
{
logAi->warn("Hero %s has %d mp which is not enough to continue his way towards %s.", hero.name, hero->movement, node.coord.toString());
ai->nullkiller->lockHero(hero.get());
ai->nullkiller->lockHero(hero.get(), HeroLockedReason::HERO_CHAIN);
return;
}
}
@ -148,13 +148,9 @@ void ExecuteHeroChain::accept(VCAI * ai)
return;
}
// do not lock hero if it is simple one hero chain
if(chainPath.exchangeCount == 1)
return;
// no exception means we were not able to rich the tile
ai->nullkiller->lockHero(hero.get());
ai->nullkiller->lockHero(hero.get(), HeroLockedReason::HERO_CHAIN);
blockedIndexes.insert(node.parentIndex);
}
catch(goalFulfilledException)

View File

@ -32,5 +32,6 @@ namespace Goals
std::string name() const override;
std::string completeMessage() const override;
virtual bool operator==(const ExecuteHeroChain & other) const override;
const AIPath & getPath() const { return chainPath; }
};
}

View File

@ -77,7 +77,8 @@ void AINodeStorage::clear()
{
actors.clear();
heroChainPass = false;
heroChainTurn = 1;
heroChainTurn = 0;
heroChainMaxTurns = 1;
}
const AIPathNode * AINodeStorage::getAINode(const CGPathNode * node) const
@ -251,6 +252,16 @@ std::vector<CGPathNode *> AINodeStorage::calculateNeighbours(
return neighbours;
}
bool AINodeStorage::increaseHeroChainTurnLimit()
{
if(heroChainTurn >= heroChainMaxTurns)
return false;
heroChainTurn++;
return true;
}
bool AINodeStorage::calculateHeroChain()
{
heroChainPass = true;

View File

@ -102,6 +102,7 @@ private:
std::vector<CGPathNode *> heroChain;
bool heroChainPass; // true if we need to calculate hero chain
int heroChainTurn;
int heroChainMaxTurns;
PlayerColor playerID;
public:
@ -113,6 +114,8 @@ public:
void initialize(const PathfinderOptions & options, const CGameState * gs) override;
bool increaseHeroChainTurnLimit();
virtual std::vector<CGPathNode *> getInitialNodes() override;
virtual std::vector<CGPathNode *> calculateNeighbours(

View File

@ -62,12 +62,25 @@ void AIPathfinder::updatePaths(std::vector<HeroPtr> heroes, bool useHeroChain)
}
auto config = std::make_shared<AIPathfinding::AIPathfinderConfig>(cb, ai, storage);
bool continueCalculation = false;
do {
do
{
logAi->trace("Recalculate paths pass %d", pass++);
cb->calculatePaths(config);
logAi->trace("Recalculate chain pass %d", pass);
useHeroChain = useHeroChain && storage->calculateHeroChain();
} while(useHeroChain);
if(useHeroChain)
{
logAi->trace("Recalculate chain pass %d", pass);
continueCalculation = storage->calculateHeroChain();
if(!continueCalculation)
{
logAi->trace("Increase chain turn limit");
continueCalculation = storage->increaseHeroChainTurnLimit() && storage->calculateHeroChain();
}
}
} while(continueCalculation);
}

View File

@ -546,15 +546,22 @@ void VCAI::objectPropertyChanged(const SetObjectProperty * sop)
NET_EVENT_HANDLER;
if(sop->what == ObjProperty::OWNER)
{
if(myCb->getPlayerRelations(playerID, (PlayerColor)sop->val) == PlayerRelations::ENEMIES)
auto relations = myCb->getPlayerRelations(playerID, (PlayerColor)sop->val);
auto obj = myCb->getObj(sop->id, false);
if(obj)
{
//we want to visit objects owned by oppponents
auto obj = myCb->getObj(sop->id, false);
if(obj)
if(relations == PlayerRelations::ENEMIES)
{
//we want to visit objects owned by oppponents
addVisitableObj(obj); // TODO: Remove once save compatability broken. In past owned objects were removed from this set
vstd::erase_if_present(alreadyVisited, obj);
}
else if(relations == PlayerRelations::SAME_PLAYER && obj->ID == Obj::TOWN && nullkiller)
{
// reevaluate defence for a new town
nullkiller->dangerHitMap->reset();
}
}
}
}