1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-09-16 09:26:28 +02:00

Merge pull request #3677 from vcmi/object-graph

Object graph
This commit is contained in:
Andrii Danylchenko
2024-03-29 20:39:56 +02:00
committed by GitHub
36 changed files with 1050 additions and 494 deletions

View File

@@ -50,7 +50,6 @@
#include <chrono>
using namespace tbb;
using dwellingContent = std::pair<ui32, std::vector<CreatureID>>;
@@ -246,26 +245,6 @@ uint64_t timeElapsed(std::chrono::time_point<std::chrono::high_resolution_clock>
// todo: move to obj manager
bool shouldVisit(const Nullkiller * ai, const CGHeroInstance * h, const CGObjectInstance * obj);
template<typename TFunc>
void pforeachTilePos(const int3 & mapSize, TFunc fn)
{
for(int z = 0; z < mapSize.z; ++z)
{
parallel_for(blocked_range<size_t>(0, mapSize.x), [&](const blocked_range<size_t>& r)
{
int3 pos(0, 0, z);
for(pos.x = r.begin(); pos.x != r.end(); ++pos.x)
{
for(pos.y = 0; pos.y < mapSize.y; ++pos.y)
{
fn(pos);
}
}
});
}
}
class CDistanceSorter
{
const CGHeroInstance * hero;

View File

@@ -11,6 +11,7 @@
#include "DangerHitMapAnalyzer.h"
#include "../Engine/Nullkiller.h"
#include "../pforeach.h"
#include "../../../lib/CRandomGenerator.h"
namespace NKAI
@@ -82,9 +83,9 @@ void DangerHitMapAnalyzer::updateHitMap()
boost::this_thread::interruption_point();
pforeachTilePos(mapSize, [&](const int3 & pos)
pforeachTilePaths(mapSize, ai, [&](const int3 & pos, const std::vector<AIPath> & paths)
{
for(AIPath & path : ai->pathfinder->getPathInfo(pos))
for(const AIPath & path : paths)
{
if(path.getFirstBlockedAction())
continue;
@@ -194,14 +195,14 @@ void DangerHitMapAnalyzer::calculateTileOwners()
ai->pathfinder->updatePaths(townHeroes, ps);
pforeachTilePos(mapSize, [&](const int3 & pos)
pforeachTilePaths(mapSize, ai, [&](const int3 & pos, const std::vector<AIPath> & paths)
{
float ourDistance = std::numeric_limits<float>::max();
float enemyDistance = std::numeric_limits<float>::max();
const CGTownInstance * enemyTown = nullptr;
const CGTownInstance * ourTown = nullptr;
for(AIPath & path : ai->pathfinder->getPathInfo(pos))
for(const AIPath & path : paths)
{
if(!path.targetHero || path.getFirstBlockedAction())
continue;

View File

@@ -109,6 +109,7 @@ void HeroManager::update()
for(auto & hero : myHeroes)
{
scores[hero] = evaluateFightingStrength(hero);
knownFightingStrength[hero->id] = hero->getFightingStrength();
}
auto scoreSort = [&](const CGHeroInstance * h1, const CGHeroInstance * h2) -> bool
@@ -192,6 +193,13 @@ bool HeroManager::heroCapReached() const
|| heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP);
}
float HeroManager::getFightingStrengthCached(const CGHeroInstance * hero) const
{
auto cached = knownFightingStrength.find(hero->id);
return cached != knownFightingStrength.end() ? cached->second : hero->getFightingStrength();
}
float HeroManager::getMagicStrength(const CGHeroInstance * hero) const
{
auto hasFly = hero->spellbookContainsSpell(SpellID::FLY);

View File

@@ -20,23 +20,6 @@
namespace NKAI
{
class DLL_EXPORT IHeroManager //: public: IAbstractManager
{
public:
virtual ~IHeroManager() = default;
virtual const std::map<HeroPtr, HeroRole> & getHeroRoles() const = 0;
virtual int selectBestSkill(const HeroPtr & hero, const std::vector<SecondarySkill> & skills) const = 0;
virtual HeroRole getHeroRole(const HeroPtr & hero) const = 0;
virtual void update() = 0;
virtual float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const = 0;
virtual float evaluateHero(const CGHeroInstance * hero) const = 0;
virtual bool canRecruitHero(const CGTownInstance * t = nullptr) const = 0;
virtual bool heroCapReached() const = 0;
virtual const CGHeroInstance * findHeroWithGrail() const = 0;
virtual const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const = 0;
virtual float getMagicStrength(const CGHeroInstance * hero) const = 0;
};
class DLL_EXPORT ISecondarySkillRule
{
public:
@@ -55,7 +38,7 @@ public:
float evaluateSecSkill(const CGHeroInstance * hero, SecondarySkill skill) const;
};
class DLL_EXPORT HeroManager : public IHeroManager
class DLL_EXPORT HeroManager
{
private:
static const SecondarySkillEvaluator wariorSkillsScores;
@@ -64,20 +47,22 @@ private:
CCallback * cb; //this is enough, but we downcast from CCallback
const Nullkiller * ai;
std::map<HeroPtr, HeroRole> heroRoles;
std::map<ObjectInstanceID, float> knownFightingStrength;
public:
HeroManager(CCallback * CB, const Nullkiller * ai) : cb(CB), ai(ai) {}
const std::map<HeroPtr, HeroRole> & getHeroRoles() const override;
HeroRole getHeroRole(const HeroPtr & hero) const override;
int selectBestSkill(const HeroPtr & hero, const std::vector<SecondarySkill> & skills) const override;
void update() override;
float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const override;
float evaluateHero(const CGHeroInstance * hero) const override;
bool canRecruitHero(const CGTownInstance * t = nullptr) const override;
bool heroCapReached() const override;
const CGHeroInstance * findHeroWithGrail() const override;
const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const override;
float getMagicStrength(const CGHeroInstance * hero) const override;
const std::map<HeroPtr, HeroRole> & getHeroRoles() const;
HeroRole getHeroRole(const HeroPtr & hero) const;
int selectBestSkill(const HeroPtr & hero, const std::vector<SecondarySkill> & skills) const;
void update();
float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const;
float evaluateHero(const CGHeroInstance * hero) const;
bool canRecruitHero(const CGTownInstance * t = nullptr) const;
bool heroCapReached() const;
const CGHeroInstance * findHeroWithGrail() const;
const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const;
float getMagicStrength(const CGHeroInstance * hero) const;
float getFightingStrengthCached(const CGHeroInstance * hero) const;
private:
float evaluateFightingStrength(const CGHeroInstance * hero) const;

View File

@@ -90,64 +90,74 @@ std::vector<std::shared_ptr<ObjectCluster>> ObjectClusterizer::getLockedClusters
return result;
}
std::optional<const CGObjectInstance *> ObjectClusterizer::getBlocker(const AIPathNodeInfo & node) const
{
std::vector<const CGObjectInstance *> blockers = {};
if(node.layer == EPathfindingLayer::LAND || node.layer == EPathfindingLayer::SAIL)
{
auto guardPos = ai->cb->getGuardingCreaturePosition(node.coord);
blockers = ai->cb->getVisitableObjs(node.coord);
if(guardPos.valid())
{
auto guard = ai->cb->getTopObj(ai->cb->getGuardingCreaturePosition(node.coord));
if(guard)
{
blockers.insert(blockers.begin(), guard);
}
}
}
if(node.specialAction && node.actionIsBlocked)
{
auto blockerObject = node.specialAction->targetObject();
if(blockerObject)
{
blockers.insert(blockers.begin(), blockerObject);
}
}
if(blockers.empty())
return std::optional< const CGObjectInstance *>();
auto blocker = blockers.front();
if(isObjectPassable(ai, blocker))
return std::optional< const CGObjectInstance *>();
if(blocker->ID == Obj::GARRISON
|| blocker->ID == Obj::GARRISON2)
{
if(dynamic_cast<const CArmedInstance *>(blocker)->getArmyStrength() == 0)
return std::optional< const CGObjectInstance *>();
else
return blocker;
}
if(blocker->ID == Obj::MONSTER
|| blocker->ID == Obj::BORDERGUARD
|| blocker->ID == Obj::BORDER_GATE
|| blocker->ID == Obj::SHIPYARD
|| (blocker->ID == Obj::QUEST_GUARD && node.actionIsBlocked))
{
return blocker;
}
return std::optional< const CGObjectInstance *>();
}
const CGObjectInstance * ObjectClusterizer::getBlocker(const AIPath & path) const
{
for(auto node = path.nodes.rbegin(); node != path.nodes.rend(); node++)
{
std::vector<const CGObjectInstance *> blockers = {};
auto blocker = getBlocker(*node);
if(node->layer == EPathfindingLayer::LAND || node->layer == EPathfindingLayer::SAIL)
{
auto guardPos = ai->cb->getGuardingCreaturePosition(node->coord);
blockers = ai->cb->getVisitableObjs(node->coord);
if(guardPos.valid())
{
auto guard = ai->cb->getTopObj(ai->cb->getGuardingCreaturePosition(node->coord));
if(guard)
{
blockers.insert(blockers.begin(), guard);
}
}
}
if(node->specialAction && node->actionIsBlocked)
{
auto blockerObject = node->specialAction->targetObject();
if(blockerObject)
{
blockers.insert(blockers.begin(), blockerObject);
}
}
if(blockers.empty())
continue;
auto blocker = blockers.front();
if(isObjectPassable(ai, blocker))
continue;
if(blocker->ID == Obj::GARRISON
|| blocker->ID == Obj::GARRISON2)
{
if(dynamic_cast<const CArmedInstance *>(blocker)->getArmyStrength() == 0)
continue;
else
return blocker;
}
if(blocker->ID == Obj::MONSTER
|| blocker->ID == Obj::BORDERGUARD
|| blocker->ID == Obj::BORDER_GATE
|| blocker->ID == Obj::SHIPYARD
|| (blocker->ID == Obj::QUEST_GUARD && node->actionIsBlocked))
{
return blocker;
}
if(blocker)
return *blocker;
}
return nullptr;
@@ -225,15 +235,17 @@ void ObjectClusterizer::clusterize()
ai->memory->visitableObjs.end());
#if NKAI_TRACE_LEVEL == 0
parallel_for(blocked_range<size_t>(0, objs.size()), [&](const blocked_range<size_t> & r) {
tbb::parallel_for(tbb::blocked_range<size_t>(0, objs.size()), [&](const tbb::blocked_range<size_t> & r) {
#else
blocked_range<size_t> r(0, objs.size());
tbb::blocked_range<size_t> r(0, objs.size());
#endif
auto priorityEvaluator = ai->priorityEvaluators->acquire();
auto heroes = ai->cb->getHeroesInfo();
std::vector<AIPath> pathCache;
for(int i = r.begin(); i != r.end(); i++)
{
clusterizeObject(objs[i], priorityEvaluator.get());
clusterizeObject(objs[i], priorityEvaluator.get(), pathCache, heroes);
}
#if NKAI_TRACE_LEVEL == 0
});
@@ -257,7 +269,11 @@ void ObjectClusterizer::clusterize()
logAi->trace("Clusterization complete in %ld", timeElapsed(start));
}
void ObjectClusterizer::clusterizeObject(const CGObjectInstance * obj, PriorityEvaluator * priorityEvaluator)
void ObjectClusterizer::clusterizeObject(
const CGObjectInstance * obj,
PriorityEvaluator * priorityEvaluator,
std::vector<AIPath> & pathCache,
std::vector<const CGHeroInstance *> & heroes)
{
if(!shouldVisitObject(obj))
{
@@ -271,9 +287,14 @@ void ObjectClusterizer::clusterizeObject(const CGObjectInstance * obj, PriorityE
logAi->trace("Check object %s%s.", obj->getObjectName(), obj->visitablePos().toString());
#endif
auto paths = ai->pathfinder->getPathInfo(obj->visitablePos(), true);
if(ai->settings->isObjectGraphAllowed())
{
ai->pathfinder->calculateQuickPathsWithBlocker(pathCache, heroes, obj->visitablePos());
}
else
ai->pathfinder->calculatePathInfo(pathCache, obj->visitablePos(), false);
if(paths.empty())
if(pathCache.empty())
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("No paths found.");
@@ -281,17 +302,17 @@ void ObjectClusterizer::clusterizeObject(const CGObjectInstance * obj, PriorityE
return;
}
std::sort(paths.begin(), paths.end(), [](const AIPath & p1, const AIPath & p2) -> bool
std::sort(pathCache.begin(), pathCache.end(), [](const AIPath & p1, const AIPath & p2) -> bool
{
return p1.movementCost() < p2.movementCost();
});
if(vstd::contains(IgnoredObjectTypes, obj->ID))
{
farObjects.addObject(obj, paths.front(), 0);
farObjects.addObject(obj, pathCache.front(), 0);
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Object ignored. Moved to far objects with path %s", paths.front().toString());
logAi->trace("Object ignored. Moved to far objects with path %s", pathCache.front().toString());
#endif
return;
@@ -299,12 +320,35 @@ void ObjectClusterizer::clusterizeObject(const CGObjectInstance * obj, PriorityE
std::set<const CGHeroInstance *> heroesProcessed;
for(auto & path : paths)
for(auto & path : pathCache)
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Checking path %s", path.toString());
#endif
if(ai->heroManager->getHeroRole(path.targetHero) == HeroRole::SCOUT)
{
if(path.movementCost() > 2.0f)
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Path is too far %f", path.movementCost());
#endif
continue;
}
}
else if(path.movementCost() > 4.0f && obj->ID != Obj::TOWN)
{
auto strategicalValue = valueEvaluator.getStrategicalValue(obj);
if(strategicalValue < 0.3f)
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Object value is too low %f", strategicalValue);
#endif
continue;
}
}
if(!shouldVisit(ai, path.targetHero, obj))
{
#if NKAI_TRACE_LEVEL >= 2

View File

@@ -10,6 +10,7 @@
#pragma once
#include "../Pathfinding/AINodeStorage.h"
#include "../Engine/PriorityEvaluator.h"
namespace NKAI
{
@@ -49,8 +50,6 @@ public:
using ClusterMap = tbb::concurrent_hash_map<const CGObjectInstance *, std::shared_ptr<ObjectCluster>>;
class PriorityEvaluator;
class ObjectClusterizer
{
private:
@@ -60,6 +59,7 @@ private:
ObjectCluster farObjects;
ClusterMap blockedObjects;
const Nullkiller * ai;
RewardEvaluator valueEvaluator;
public:
void clusterize();
@@ -67,12 +67,17 @@ public:
std::vector<const CGObjectInstance *> getFarObjects() const;
std::vector<std::shared_ptr<ObjectCluster>> getLockedClusters() const;
const CGObjectInstance * getBlocker(const AIPath & path) const;
std::optional<const CGObjectInstance *> getBlocker(const AIPathNodeInfo & node) const;
ObjectClusterizer(const Nullkiller * ai): ai(ai) {}
ObjectClusterizer(const Nullkiller * ai): ai(ai), valueEvaluator(ai) {}
private:
bool shouldVisitObject(const CGObjectInstance * obj) const;
void clusterizeObject(const CGObjectInstance * obj, PriorityEvaluator * priorityEvaluator);
void clusterizeObject(
const CGObjectInstance * obj,
PriorityEvaluator * priorityEvaluator,
std::vector<AIPath> & pathCache,
std::vector<const CGHeroInstance *> & heroes);
};
}

View File

@@ -180,6 +180,8 @@ Goals::TGoalVec CaptureObjectsBehavior::decompose() const
logAi->debug("Scanning objects, count %d", objs.size());
std::vector<AIPath> paths;
for(auto objToVisit : objs)
{
if(!objectMatchesFilter(objToVisit))
@@ -193,7 +195,7 @@ Goals::TGoalVec CaptureObjectsBehavior::decompose() const
bool useObjectGraph = ai->nullkiller->settings->isObjectGraphAllowed()
&& ai->nullkiller->getScanDepth() != ScanDepth::SMALL;
auto paths = ai->nullkiller->pathfinder->getPathInfo(pos, useObjectGraph);
ai->nullkiller->pathfinder->calculatePathInfo(paths, pos, useObjectGraph);
std::vector<std::shared_ptr<ExecuteHeroChain>> waysToVisitObj;
std::shared_ptr<ExecuteHeroChain> closestWay;

View File

@@ -43,6 +43,7 @@ Goals::TGoalVec ClusterBehavior::decomposeCluster(std::shared_ptr<ObjectCluster>
{
auto center = cluster->calculateCenter();
auto paths = ai->nullkiller->pathfinder->getPathInfo(center->visitablePos(), ai->nullkiller->settings->isObjectGraphAllowed());
auto blockerPos = cluster->blocker->visitablePos();
std::vector<AIPath> blockerPaths;

View File

@@ -69,7 +69,7 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
logAi->trace("Checking ways to gaher army for hero %s, %s", hero->getObjectName(), pos.toString());
#endif
auto paths = ai->nullkiller->pathfinder->getPathInfo(pos);
auto paths = ai->nullkiller->pathfinder->getPathInfo(pos, ai->nullkiller->settings->isObjectGraphAllowed());
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Gather army found %d paths", paths.size());
@@ -231,7 +231,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
logAi->trace("Checking ways to upgrade army in town %s, %s", upgrader->getObjectName(), pos.toString());
#endif
auto paths = ai->nullkiller->pathfinder->getPathInfo(pos);
auto paths = ai->nullkiller->pathfinder->getPathInfo(pos, ai->nullkiller->settings->isObjectGraphAllowed());
auto goals = CaptureObjectsBehavior::getVisitGoals(paths);
std::vector<std::shared_ptr<ExecuteHeroChain>> waysToVisitObj;

View File

@@ -35,18 +35,23 @@ Goals::TGoalVec StayAtTownBehavior::decompose() const
if(!towns.size())
return tasks;
std::vector<AIPath> paths;
for(auto town : towns)
{
if(!town->hasBuilt(BuildingID::MAGES_GUILD_1))
continue;
auto paths = ai->nullkiller->pathfinder->getPathInfo(town->visitablePos());
ai->nullkiller->pathfinder->calculatePathInfo(paths, town->visitablePos());
for(auto & path : paths)
{
if(town->visitingHero && town->visitingHero.get() != path.targetHero)
continue;
if(!path.targetHero->hasSpellbook() || path.targetHero->mana >= 0.75f * path.targetHero->manaLimit())
continue;
if(path.turn() == 0 && !path.getFirstBlockedAction() && path.exchangeCount <= 1)
{
if(path.targetHero->mana == path.targetHero->manaLimit())

View File

@@ -81,6 +81,7 @@ set(Nullkiller_HEADERS
Pathfinding/Rules/AIPreviousNodeRule.h
Pathfinding/ObjectGraph.h
AIUtility.h
pforeach.h
Analyzers/ArmyManager.h
Analyzers/HeroManager.h
Engine/Settings.h

View File

@@ -67,40 +67,40 @@ void ExecuteHeroChain::accept(AIGateway * ai)
for(int i = chainPath.nodes.size() - 1; i >= 0; i--)
{
auto & node = chainPath.nodes[i];
auto * node = &chainPath.nodes[i];
const CGHeroInstance * hero = node.targetHero;
const CGHeroInstance * hero = node->targetHero;
HeroPtr heroPtr = hero;
if(node.parentIndex >= i)
if(node->parentIndex >= i)
{
logAi->error("Invalid parentIndex while executing node " + node.coord.toString());
logAi->error("Invalid parentIndex while executing node " + node->coord.toString());
}
if(vstd::contains(blockedIndexes, i))
{
blockedIndexes.insert(node.parentIndex);
blockedIndexes.insert(node->parentIndex);
ai->nullkiller->lockHero(hero, HeroLockedReason::HERO_CHAIN);
continue;
}
logAi->debug("Executing chain node %d. Moving hero %s to %s", i, hero->getNameTranslated(), node.coord.toString());
logAi->debug("Executing chain node %d. Moving hero %s to %s", i, hero->getNameTranslated(), node->coord.toString());
try
{
if(hero->movementPointsRemaining() > 0)
{
ai->nullkiller->setActive(hero, node.coord);
ai->nullkiller->setActive(hero, node->coord);
if(node.specialAction)
if(node->specialAction)
{
if(node.actionIsBlocked)
if(node->actionIsBlocked)
{
throw cannotFulfillGoalException("Path is nondeterministic.");
}
node.specialAction->execute(hero);
node->specialAction->execute(hero);
if(!heroPtr.validAndSet())
{
@@ -109,10 +109,34 @@ void ExecuteHeroChain::accept(AIGateway * ai)
return;
}
}
if(node.turns == 0 && node.coord != hero->visitablePos())
else if(i > 0 && ai->nullkiller->settings->isObjectGraphAllowed())
{
auto targetNode = cb->getPathsInfo(hero)->getPathInfo(node.coord);
auto chainMask = i < chainPath.nodes.size() - 1 ? chainPath.nodes[i + 1].chainMask : node->chainMask;
for(auto j = i - 1; j >= 0; j--)
{
auto & nextNode = chainPath.nodes[j];
if(nextNode.specialAction || nextNode.chainMask != chainMask)
break;
auto targetNode = cb->getPathsInfo(hero)->getPathInfo(nextNode.coord);
if(!targetNode->reachable()
|| targetNode->getCost() > nextNode.cost)
break;
i = j;
node = &nextNode;
if(targetNode->action == EPathNodeAction::BATTLE || targetNode->action == EPathNodeAction::TELEPORT_BATTLE)
break;
}
}
if(node->turns == 0 && node->coord != hero->visitablePos())
{
auto targetNode = cb->getPathsInfo(hero)->getPathInfo(node->coord);
if(targetNode->accessible == EPathAccessibility::NOT_SET
|| targetNode->accessible == EPathAccessibility::BLOCKED
@@ -122,7 +146,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
logAi->error(
"Unable to complete chain. Expected hero %s to arrive to %s in 0 turns but he cannot do this",
hero->getNameTranslated(),
node.coord.toString());
node->coord.toString());
return;
}
@@ -132,7 +156,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
{
try
{
if(moveHeroToTile(hero, node.coord))
if(moveHeroToTile(hero, node->coord))
{
continue;
}
@@ -149,11 +173,11 @@ void ExecuteHeroChain::accept(AIGateway * ai)
if(hero->movementPointsRemaining() > 0)
{
CGPath path;
bool isOk = cb->getPathsInfo(hero)->getPath(path, node.coord);
bool isOk = cb->getPathsInfo(hero)->getPath(path, node->coord);
if(isOk && path.nodes.back().turns > 0)
{
logAi->warn("Hero %s has %d mp which is not enough to continue his way towards %s.", hero->getNameTranslated(), hero->movementPointsRemaining(), node.coord.toString());
logAi->warn("Hero %s has %d mp which is not enough to continue his way towards %s.", hero->getNameTranslated(), hero->movementPointsRemaining(), node->coord.toString());
ai->nullkiller->lockHero(hero, HeroLockedReason::HERO_CHAIN);
return;
@@ -165,15 +189,15 @@ void ExecuteHeroChain::accept(AIGateway * ai)
}
}
if(node.coord == hero->visitablePos())
if(node->coord == hero->visitablePos())
continue;
if(node.turns == 0)
if(node->turns == 0)
{
logAi->error(
"Unable to complete chain. Expected hero %s to arive to %s but he is at %s",
hero->getNameTranslated(),
node.coord.toString(),
node->coord.toString(),
hero->visitablePos().toString());
return;
@@ -181,13 +205,13 @@ void ExecuteHeroChain::accept(AIGateway * ai)
// no exception means we were not able to reach the tile
ai->nullkiller->lockHero(hero, HeroLockedReason::HERO_CHAIN);
blockedIndexes.insert(node.parentIndex);
blockedIndexes.insert(node->parentIndex);
}
catch(const goalFulfilledException &)
{
if(!heroPtr.validAndSet())
{
logAi->debug("Hero %s was killed while attempting to reach %s", heroPtr.name, node.coord.toString());
logAi->debug("Hero %s was killed while attempting to reach %s", heroPtr.name, node->coord.toString());
return;
}

View File

@@ -24,7 +24,8 @@
namespace NKAI
{
std::shared_ptr<boost::multi_array<AIPathNode, 5>> AISharedStorage::shared;
std::shared_ptr<boost::multi_array<AIPathNode, 4>> AISharedStorage::shared;
uint64_t AISharedStorage::version = 0;
boost::mutex AISharedStorage::locker;
std::set<int3> commitedTiles;
std::set<int3> commitedTilesInitial;
@@ -40,11 +41,24 @@ const bool DO_NOT_SAVE_TO_COMMITED_TILES = false;
AISharedStorage::AISharedStorage(int3 sizes)
{
if(!shared){
shared.reset(new boost::multi_array<AIPathNode, 5>(
boost::extents[EPathfindingLayer::NUM_LAYERS][sizes.z][sizes.x][sizes.y][AIPathfinding::NUM_CHAINS]));
}
shared.reset(new boost::multi_array<AIPathNode, 4>(
boost::extents[sizes.z][sizes.x][sizes.y][AIPathfinding::NUM_CHAINS]));
nodes = shared;
nodes = shared;
foreach_tile_pos([&](const int3 & pos)
{
for(auto i = 0; i < AIPathfinding::NUM_CHAINS; i++)
{
auto & node = get(pos)[i];
node.version = -1;
node.coord = pos;
}
});
}
else
nodes = shared;
}
AISharedStorage::~AISharedStorage()
@@ -80,6 +94,9 @@ void AIPathNode::addSpecialAction(std::shared_ptr<const SpecialAction> action)
AINodeStorage::AINodeStorage(const Nullkiller * ai, const int3 & Sizes)
: sizes(Sizes), ai(ai), cb(ai->cb.get()), nodes(Sizes)
{
accesibility = std::make_unique<boost::multi_array<EPathAccessibility, 4>>(
boost::extents[sizes.z][sizes.x][sizes.y][EPathfindingLayer::NUM_LAYERS]);
dangerEvaluator.reset(new FuzzyHelper(ai));
}
@@ -90,6 +107,8 @@ void AINodeStorage::initialize(const PathfinderOptions & options, const CGameSta
if(heroChainPass)
return;
AISharedStorage::version++;
//TODO: fix this code duplication with NodeStorage::initialize, problem is to keep `resetTile` inline
const PlayerColor fowPlayer = ai->playerID;
const auto & fow = static_cast<const CGameInfoCallback *>(gs)->getPlayerTeam(fowPlayer)->fogOfWarMap;
@@ -97,7 +116,7 @@ void AINodeStorage::initialize(const PathfinderOptions & options, const CGameSta
//Each thread gets different x, but an array of y located next to each other in memory
parallel_for(blocked_range<size_t>(0, sizes.x), [&](const blocked_range<size_t>& r)
tbb::parallel_for(tbb::blocked_range<size_t>(0, sizes.x), [&](const tbb::blocked_range<size_t>& r)
{
int3 pos;
@@ -152,9 +171,9 @@ std::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
{
int bucketIndex = ((uintptr_t)actor) % AIPathfinding::BUCKET_COUNT;
int bucketOffset = bucketIndex * AIPathfinding::BUCKET_SIZE;
auto chains = nodes.get(pos, layer);
auto chains = nodes.get(pos);
if(chains[0].blocked())
if(blocked(pos, layer))
{
return std::nullopt;
}
@@ -163,15 +182,17 @@ std::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
{
AIPathNode & node = chains[i + bucketOffset];
if(node.actor == actor)
if(node.version != AISharedStorage::version)
{
node.reset(layer, getAccessibility(pos, layer));
node.version = AISharedStorage::version;
node.actor = actor;
return &node;
}
if(!node.actor)
if(node.actor == actor && node.layer == layer)
{
node.actor = actor;
return &node;
}
}
@@ -226,21 +247,6 @@ std::vector<CGPathNode *> AINodeStorage::getInitialNodes()
return initialNodes;
}
void AINodeStorage::resetTile(const int3 & coord, EPathfindingLayer layer, EPathAccessibility accessibility)
{
for(AIPathNode & heroNode : nodes.get(coord, layer))
{
heroNode.actor = nullptr;
heroNode.danger = 0;
heroNode.manaCost = 0;
heroNode.specialAction.reset();
heroNode.armyLoss = 0;
heroNode.chainOther = nullptr;
heroNode.dayFlags = DayFlags::NONE;
heroNode.update(coord, layer, accessibility);
}
}
void AINodeStorage::commit(CDestinationNodeInfo & destination, const PathNodeInfo & source)
{
const AIPathNode * srcNode = getAINode(source.node);
@@ -306,30 +312,31 @@ void AINodeStorage::commit(
}
}
std::vector<CGPathNode *> AINodeStorage::calculateNeighbours(
void AINodeStorage::calculateNeighbours(
std::vector<CGPathNode *> & result,
const PathNodeInfo & source,
EPathfindingLayer layer,
const PathfinderConfig * pathfinderConfig,
const CPathfinderHelper * pathfinderHelper)
{
std::vector<CGPathNode *> neighbours;
neighbours.reserve(16);
std::vector<int3> accessibleNeighbourTiles;
result.clear();
accessibleNeighbourTiles.reserve(8);
pathfinderHelper->calculateNeighbourTiles(accessibleNeighbourTiles, source);
const AIPathNode * srcNode = getAINode(source.node);
auto accessibleNeighbourTiles = pathfinderHelper->getNeighbourTiles(source);
for(auto & neighbour : accessibleNeighbourTiles)
{
for(EPathfindingLayer i = EPathfindingLayer::LAND; i < EPathfindingLayer::NUM_LAYERS; i.advance(1))
{
auto nextNode = getOrCreateNode(neighbour, i, srcNode->actor);
auto nextNode = getOrCreateNode(neighbour, layer, srcNode->actor);
if(!nextNode || nextNode.value()->accessible == EPathAccessibility::NOT_SET)
continue;
if(!nextNode || nextNode.value()->accessible == EPathAccessibility::NOT_SET)
continue;
neighbours.push_back(nextNode.value());
}
result.push_back(nextNode.value());
}
return neighbours;
}
constexpr std::array phisycalLayers = {EPathfindingLayer::LAND, EPathfindingLayer::SAIL};
@@ -346,19 +353,16 @@ bool AINodeStorage::increaseHeroChainTurnLimit()
{
foreach_tile_pos([&](const int3 & pos)
{
auto chains = nodes.get(pos, layer);
if(!chains[0].blocked())
{
for(AIPathNode & node : chains)
iterateValidNodesUntil(pos, layer, [&](AIPathNode & node)
{
if(node.turns <= heroChainTurn && node.action != EPathNodeAction::UNKNOWN)
{
commitedTiles.insert(pos);
break;
return true;
}
}
}
return false;
});
});
}
@@ -374,22 +378,17 @@ bool AINodeStorage::calculateHeroChainFinal()
{
foreach_tile_pos([&](const int3 & pos)
{
auto chains = nodes.get(pos, layer);
if(!chains[0].blocked())
{
for(AIPathNode & node : chains)
iterateValidNodes(pos, layer, [&](AIPathNode & node)
{
if(node.turns > heroChainTurn
&& !node.locked
&& node.action != EPathNodeAction::UNKNOWN
&& node.actor->actorExchangeCount > 1
&& !hasBetterChain(&node, &node, chains))
&& !hasBetterChain(&node, node))
{
heroChain.push_back(&node);
}
}
}
});
});
}
@@ -413,7 +412,6 @@ struct DelayedWork
class HeroChainCalculationTask
{
private:
AISharedStorage & nodes;
AINodeStorage & storage;
std::vector<AIPathNode *> existingChains;
std::vector<ExchangeCandidate> newChains;
@@ -425,14 +423,14 @@ private:
public:
HeroChainCalculationTask(
AINodeStorage & storage, AISharedStorage & nodes, const std::vector<int3> & tiles, uint64_t chainMask, int heroChainTurn)
:existingChains(), newChains(), delayedWork(), nodes(nodes), storage(storage), chainMask(chainMask), heroChainTurn(heroChainTurn), heroChain(), tiles(tiles)
AINodeStorage & storage, const std::vector<int3> & tiles, uint64_t chainMask, int heroChainTurn)
:existingChains(), newChains(), delayedWork(), storage(storage), chainMask(chainMask), heroChainTurn(heroChainTurn), heroChain(), tiles(tiles)
{
existingChains.reserve(AIPathfinding::NUM_CHAINS);
newChains.reserve(AIPathfinding::NUM_CHAINS);
}
void execute(const blocked_range<size_t>& r)
void execute(const tbb::blocked_range<size_t>& r)
{
std::random_device randomDevice;
std::mt19937 randomEngine(randomDevice());
@@ -443,21 +441,19 @@ public:
for(auto layer : phisycalLayers)
{
auto chains = nodes.get(pos, layer);
existingChains.clear();
// fast cut inactive nodes
if(chains[0].blocked())
storage.iterateValidNodes(pos, layer, [this](AIPathNode & node)
{
if(node.turns <= heroChainTurn && node.action != EPathNodeAction::UNKNOWN)
existingChains.push_back(&node);
});
if(existingChains.empty())
continue;
existingChains.clear();
newChains.clear();
for(AIPathNode & node : chains)
{
if(node.turns <= heroChainTurn && node.action != EPathNodeAction::UNKNOWN)
existingChains.push_back(&node);
}
std::shuffle(existingChains.begin(), existingChains.end(), randomEngine);
for(AIPathNode * node : existingChains)
@@ -530,10 +526,10 @@ bool AINodeStorage::calculateHeroChain()
std::shuffle(data.begin(), data.end(), randomEngine);
parallel_for(blocked_range<size_t>(0, data.size()), [&](const blocked_range<size_t>& r)
tbb::parallel_for(tbb::blocked_range<size_t>(0, data.size()), [&](const tbb::blocked_range<size_t>& r)
{
//auto r = blocked_range<size_t>(0, data.size());
HeroChainCalculationTask task(*this, nodes, data, chainMask, heroChainTurn);
HeroChainCalculationTask task(*this, data, chainMask, heroChainTurn);
task.execute(r);
@@ -546,8 +542,8 @@ bool AINodeStorage::calculateHeroChain()
}
else
{
auto r = blocked_range<size_t>(0, data.size());
HeroChainCalculationTask task(*this, nodes, data, chainMask, heroChainTurn);
auto r = tbb::blocked_range<size_t>(0, data.size());
HeroChainCalculationTask task(*this, data, chainMask, heroChainTurn);
task.execute(r);
task.flushResult(heroChain);
@@ -612,14 +608,20 @@ bool AINodeStorage::selectNextActor()
return false;
}
uint64_t AINodeStorage::evaluateArmyLoss(const CGHeroInstance * hero, uint64_t armyValue, uint64_t danger) const
{
float fightingStrength = ai->heroManager->getFightingStrengthCached(hero);
double ratio = (double)danger / (armyValue * fightingStrength);
return (uint64_t)(armyValue * ratio * ratio);
}
void HeroChainCalculationTask::cleanupInefectiveChains(std::vector<ExchangeCandidate> & result) const
{
vstd::erase_if(result, [&](const ExchangeCandidate & chainInfo) -> bool
{
auto pos = chainInfo.coord;
auto chains = nodes.get(pos, EPathfindingLayer::LAND);
auto isNotEffective = storage.hasBetterChain(chainInfo.carrierParent, &chainInfo, chains)
|| storage.hasBetterChain(chainInfo.carrierParent, &chainInfo, result);
auto isNotEffective = storage.hasBetterChain(chainInfo.carrierParent, chainInfo)
|| storage.hasBetterChain(chainInfo.carrierParent, chainInfo, result);
#if NKAI_PATHFINDER_TRACE_LEVEL >= 2
if(isNotEffective)
@@ -645,7 +647,7 @@ void HeroChainCalculationTask::calculateHeroChain(
{
for(AIPathNode * node : variants)
{
if(node == srcNode || !node->actor)
if(node == srcNode || !node->actor || node->version != AISharedStorage::version)
continue;
if((node->actor->chainMask & chainMask) == 0 && (srcNode->actor->chainMask & chainMask) == 0)
@@ -1174,7 +1176,7 @@ void AINodeStorage::calculateTownPortalTeleportations(std::vector<CGPathNode *>
if(actorsVector.size() * initialNodes.size() > 1000)
{
parallel_for(blocked_range<size_t>(0, actorsVector.size()), [&](const blocked_range<size_t> & r)
tbb::parallel_for(tbb::blocked_range<size_t>(0, actorsVector.size()), [&](const tbb::blocked_range<size_t> & r)
{
for(int i = r.begin(); i != r.end(); i++)
{
@@ -1195,95 +1197,116 @@ void AINodeStorage::calculateTownPortalTeleportations(std::vector<CGPathNode *>
bool AINodeStorage::hasBetterChain(const PathNodeInfo & source, CDestinationNodeInfo & destination) const
{
auto pos = destination.coord;
auto chains = nodes.get(pos, EPathfindingLayer::LAND);
auto candidateNode = getAINode(destination.node);
return hasBetterChain(source.node, getAINode(destination.node), chains);
return hasBetterChain(source.node, *candidateNode);
}
bool AINodeStorage::hasBetterChain(
const CGPathNode * source,
const AIPathNode & candidateNode) const
{
return iterateValidNodesUntil(
candidateNode.coord,
candidateNode.layer,
[this, &source, candidateNode](const AIPathNode & node) -> bool
{
return isOtherChainBetter(source, candidateNode, node);
});
}
template<class NodeRange>
bool AINodeStorage::hasBetterChain(
const CGPathNode * source,
const AIPathNode * candidateNode,
const NodeRange & chains) const
const CGPathNode * source,
const AIPathNode & candidateNode,
const NodeRange & nodes) const
{
auto candidateActor = candidateNode->actor;
for(const AIPathNode & node : chains)
for(const AIPathNode & node : nodes)
{
auto sameNode = node.actor == candidateNode->actor;
if(isOtherChainBetter(source, candidateNode, node))
return true;
}
if(sameNode || node.action == EPathNodeAction::UNKNOWN || !node.actor || !node.actor->hero)
{
continue;
}
return false;
}
if(node.danger <= candidateNode->danger && candidateNode->actor == node.actor->battleActor)
{
if(node.getCost() < candidateNode->getCost())
{
#if NKAI_PATHFINDER_TRACE_LEVEL >= 2
logAi->trace(
"Block ineficient battle move %s->%s, hero: %s[%X], army %lld, mp diff: %i",
source->coord.toString(),
candidateNode->coord.toString(),
candidateNode->actor->hero->getNameTranslated(),
candidateNode->actor->chainMask,
candidateNode->actor->armyValue,
node.moveRemains - candidateNode->moveRemains);
#endif
return true;
}
}
bool AINodeStorage::isOtherChainBetter(
const CGPathNode * source,
const AIPathNode & candidateNode,
const AIPathNode & other) const
{
auto sameNode = other.actor == candidateNode.actor;
if(candidateActor->chainMask != node.actor->chainMask && heroChainPass != EHeroChainPass::FINAL)
continue;
if(sameNode || other.action == EPathNodeAction::UNKNOWN || !other.actor || !other.actor->hero)
{
return false;
}
auto nodeActor = node.actor;
auto nodeArmyValue = nodeActor->armyValue - node.armyLoss;
auto candidateArmyValue = candidateActor->armyValue - candidateNode->armyLoss;
if(nodeArmyValue > candidateArmyValue
&& node.getCost() <= candidateNode->getCost())
if(other.danger <= candidateNode.danger && candidateNode.actor == other.actor->battleActor)
{
if(other.getCost() < candidateNode.getCost())
{
#if NKAI_PATHFINDER_TRACE_LEVEL >= 2
logAi->trace(
"Block ineficient move because of stronger army %s->%s, hero: %s[%X], army %lld, mp diff: %i",
"Block ineficient battle move %s->%s, hero: %s[%X], army %lld, mp diff: %i",
source->coord.toString(),
candidateNode->coord.toString(),
candidateNode->actor->hero->getNameTranslated(),
candidateNode->actor->chainMask,
candidateNode->actor->armyValue,
node.moveRemains - candidateNode->moveRemains);
candidateNode.coord.toString(),
candidateNode.actor->hero->getNameTranslated(),
candidateNode.actor->chainMask,
candidateNode.actor->armyValue,
other.moveRemains - candidateNode.moveRemains);
#endif
return true;
}
}
if(heroChainPass == EHeroChainPass::FINAL)
if(candidateNode.actor->chainMask != other.actor->chainMask && heroChainPass != EHeroChainPass::FINAL)
return false;
auto nodeActor = other.actor;
auto nodeArmyValue = nodeActor->armyValue - other.armyLoss;
auto candidateArmyValue = candidateNode.actor->armyValue - candidateNode.armyLoss;
if(nodeArmyValue > candidateArmyValue
&& other.getCost() <= candidateNode.getCost())
{
#if NKAI_PATHFINDER_TRACE_LEVEL >= 2
logAi->trace(
"Block ineficient move because of stronger army %s->%s, hero: %s[%X], army %lld, mp diff: %i",
source->coord.toString(),
candidateNode.coord.toString(),
candidateNode.actor->hero->getNameTranslated(),
candidateNode.actor->chainMask,
candidateNode.actor->armyValue,
other.moveRemains - candidateNode.moveRemains);
#endif
return true;
}
if(heroChainPass == EHeroChainPass::FINAL)
{
if(nodeArmyValue == candidateArmyValue
&& nodeActor->heroFightingStrength >= candidateNode.actor->heroFightingStrength
&& other.getCost() <= candidateNode.getCost())
{
if(nodeArmyValue == candidateArmyValue
&& nodeActor->heroFightingStrength >= candidateActor->heroFightingStrength
&& node.getCost() <= candidateNode->getCost())
if(vstd::isAlmostEqual(nodeActor->heroFightingStrength, candidateNode.actor->heroFightingStrength)
&& vstd::isAlmostEqual(other.getCost(), candidateNode.getCost())
&& &other < &candidateNode)
{
if(vstd::isAlmostEqual(nodeActor->heroFightingStrength, candidateActor->heroFightingStrength)
&& vstd::isAlmostEqual(node.getCost(), candidateNode->getCost())
&& &node < candidateNode)
{
continue;
}
return false;
}
#if NKAI_PATHFINDER_TRACE_LEVEL >= 2
logAi->trace(
"Block ineficient move because of stronger hero %s->%s, hero: %s[%X], army %lld, mp diff: %i",
source->coord.toString(),
candidateNode->coord.toString(),
candidateNode->actor->hero->getNameTranslated(),
candidateNode->actor->chainMask,
candidateNode->actor->armyValue,
node.moveRemains - candidateNode->moveRemains);
logAi->trace(
"Block ineficient move because of stronger hero %s->%s, hero: %s[%X], army %lld, mp diff: %i",
source->coord.toString(),
candidateNode.coord.toString(),
candidateNode.actor->hero->getNameTranslated(),
candidateNode.actor->chainMask,
candidateNode.actor->armyValue,
other.moveRemains - candidateNode.moveRemains);
#endif
return true;
}
return true;
}
}
@@ -1292,12 +1315,15 @@ bool AINodeStorage::hasBetterChain(
bool AINodeStorage::isTileAccessible(const HeroPtr & hero, const int3 & pos, const EPathfindingLayer layer) const
{
auto chains = nodes.get(pos, layer);
auto chains = nodes.get(pos);
for(const AIPathNode & node : chains)
{
if(node.action != EPathNodeAction::UNKNOWN
&& node.actor && node.actor->hero == hero.h)
if(node.version == AISharedStorage::version
&& node.layer == layer
&& node.action != EPathNodeAction::UNKNOWN
&& node.actor
&& node.actor->hero == hero.h)
{
return true;
}
@@ -1306,22 +1332,23 @@ bool AINodeStorage::isTileAccessible(const HeroPtr & hero, const int3 & pos, con
return false;
}
std::vector<AIPath> AINodeStorage::getChainInfo(const int3 & pos, bool isOnLand) const
void AINodeStorage::calculateChainInfo(std::vector<AIPath> & paths, const int3 & pos, bool isOnLand) const
{
std::vector<AIPath> paths;
paths.reserve(AIPathfinding::NUM_CHAINS / 4);
auto chains = nodes.get(pos, isOnLand ? EPathfindingLayer::LAND : EPathfindingLayer::SAIL);
auto layer = isOnLand ? EPathfindingLayer::LAND : EPathfindingLayer::SAIL;
auto chains = nodes.get(pos);
for(const AIPathNode & node : chains)
{
if(node.action == EPathNodeAction::UNKNOWN || !node.actor || !node.actor->hero)
if(node.version != AISharedStorage::version
|| node.layer != layer
|| node.action == EPathNodeAction::UNKNOWN
|| !node.actor
|| !node.actor->hero)
{
continue;
}
AIPath path;
AIPath & path = paths.emplace_back();
path.targetHero = node.actor->hero;
path.heroArmy = node.actor->creatureSet;
@@ -1332,11 +1359,7 @@ std::vector<AIPath> AINodeStorage::getChainInfo(const int3 & pos, bool isOnLand)
path.exchangeCount = node.actor->actorExchangeCount;
fillChainInfo(&node, path, -1);
paths.push_back(path);
}
return paths;
}
void AINodeStorage::fillChainInfo(const AIPathNode * node, AIPath & path, int parentIndex) const
@@ -1349,33 +1372,29 @@ void AINodeStorage::fillChainInfo(const AIPathNode * node, AIPath & path, int pa
if(node->chainOther)
fillChainInfo(node->chainOther, path, parentIndex);
//if(node->actor->hero->visitablePos() != node->coord)
AIPathNodeInfo pathNode;
pathNode.cost = node->getCost();
pathNode.targetHero = node->actor->hero;
pathNode.chainMask = node->actor->chainMask;
pathNode.specialAction = node->specialAction;
pathNode.turns = node->turns;
pathNode.danger = node->danger;
pathNode.coord = node->coord;
pathNode.parentIndex = parentIndex;
pathNode.actionIsBlocked = false;
pathNode.layer = node->layer;
if(pathNode.specialAction)
{
AIPathNodeInfo pathNode;
auto targetNode =node->theNodeBefore ? getAINode(node->theNodeBefore) : node;
pathNode.cost = node->getCost();
pathNode.targetHero = node->actor->hero;
pathNode.chainMask = node->actor->chainMask;
pathNode.specialAction = node->specialAction;
pathNode.turns = node->turns;
pathNode.danger = node->danger;
pathNode.coord = node->coord;
pathNode.parentIndex = parentIndex;
pathNode.actionIsBlocked = false;
pathNode.layer = node->layer;
if(pathNode.specialAction)
{
auto targetNode =node->theNodeBefore ? getAINode(node->theNodeBefore) : node;
pathNode.actionIsBlocked = !pathNode.specialAction->canAct(targetNode);
}
parentIndex = path.nodes.size();
path.nodes.push_back(pathNode);
pathNode.actionIsBlocked = !pathNode.specialAction->canAct(targetNode);
}
parentIndex = path.nodes.size();
path.nodes.push_back(pathNode);
node = getAINode(node->theNodeBefore);
}
}

View File

@@ -28,7 +28,7 @@ namespace NKAI
namespace AIPathfinding
{
const int BUCKET_COUNT = 3;
const int BUCKET_SIZE = 5;
const int BUCKET_SIZE = 7;
const int NUM_CHAINS = BUCKET_COUNT * BUCKET_SIZE;
const int CHAIN_MAX_DEPTH = 4;
}
@@ -49,15 +49,24 @@ struct AIPathNode : public CGPathNode
const AIPathNode * chainOther;
std::shared_ptr<const SpecialAction> specialAction;
const ChainActor * actor;
STRONG_INLINE
bool blocked() const
{
return accessible == EPathAccessibility::NOT_SET
|| accessible == EPathAccessibility::BLOCKED;
}
uint64_t version;
void addSpecialAction(std::shared_ptr<const SpecialAction> action);
inline void reset(EPathfindingLayer layer, EPathAccessibility accessibility)
{
CGPathNode::reset();
actor = nullptr;
danger = 0;
manaCost = 0;
specialAction.reset();
armyLoss = 0;
chainOther = nullptr;
dayFlags = DayFlags::NONE;
this->layer = layer;
accessible = accessibility;
}
};
struct AIPathNodeInfo
@@ -133,21 +142,21 @@ enum EHeroChainPass
class AISharedStorage
{
// 1 - layer (air, water, land)
// 2-4 - position on map[z][x][y]
// 5 - chain (normal, battle, spellcast and combinations)
static std::shared_ptr<boost::multi_array<AIPathNode, 5>> shared;
std::shared_ptr<boost::multi_array<AIPathNode, 5>> nodes;
// 1-3 - position on map[z][x][y]
// 4 - chain + layer (normal, battle, spellcast and combinations, water, air)
static std::shared_ptr<boost::multi_array<AIPathNode, 4>> shared;
std::shared_ptr<boost::multi_array<AIPathNode, 4>> nodes;
public:
static boost::mutex locker;
static uint64_t version;
AISharedStorage(int3 mapSize);
~AISharedStorage();
STRONG_INLINE
boost::detail::multi_array::sub_array<AIPathNode, 1> get(int3 tile, EPathfindingLayer layer) const
boost::detail::multi_array::sub_array<AIPathNode, 1> get(int3 tile) const
{
return (*nodes)[layer][tile.z][tile.x][tile.y];
return (*nodes)[tile.z][tile.x][tile.y];
}
};
@@ -156,6 +165,8 @@ class AINodeStorage : public INodeStorage
private:
int3 sizes;
std::unique_ptr<boost::multi_array<EPathAccessibility, 4>> accesibility;
const CPlayerSpecificInfoCallback * cb;
const Nullkiller * ai;
std::unique_ptr<FuzzyHelper> dangerEvaluator;
@@ -182,8 +193,10 @@ public:
std::vector<CGPathNode *> getInitialNodes() override;
virtual std::vector<CGPathNode *> calculateNeighbours(
virtual void calculateNeighbours(
std::vector<CGPathNode *> & result,
const PathNodeInfo & source,
EPathfindingLayer layer,
const PathfinderConfig * pathfinderConfig,
const CPathfinderHelper * pathfinderHelper) override;
@@ -222,7 +235,27 @@ public:
return aiNode->actor->hero;
}
inline bool blocked(const int3 & tile, EPathfindingLayer layer) const
{
EPathAccessibility accessible = getAccessibility(tile, layer);
return accessible == EPathAccessibility::NOT_SET
|| accessible == EPathAccessibility::BLOCKED;
}
bool hasBetterChain(const PathNodeInfo & source, CDestinationNodeInfo & destination) const;
bool hasBetterChain(const CGPathNode * source, const AIPathNode & candidateNode) const;
template<class NodeRange>
bool hasBetterChain(
const CGPathNode * source,
const AIPathNode & destinationNode,
const NodeRange & chains) const;
bool isOtherChainBetter(
const CGPathNode * source,
const AIPathNode & candidateNode,
const AIPathNode & other) const;
bool isMovementIneficient(const PathNodeInfo & source, CDestinationNodeInfo & destination) const
{
@@ -231,14 +264,8 @@ public:
bool isDistanceLimitReached(const PathNodeInfo & source, CDestinationNodeInfo & destination) const;
template<class NodeRange>
bool hasBetterChain(
const CGPathNode * source,
const AIPathNode * destinationNode,
const NodeRange & chains) const;
std::optional<AIPathNode *> getOrCreateNode(const int3 & coord, const EPathfindingLayer layer, const ChainActor * actor);
std::vector<AIPath> getChainInfo(const int3 & pos, bool isOnLand) const;
void calculateChainInfo(std::vector<AIPath> & result, const int3 & pos, bool isOnLand) const;
bool isTileAccessible(const HeroPtr & hero, const int3 & pos, const EPathfindingLayer layer) const;
void setHeroes(std::map<const CGHeroInstance *, HeroRole> heroes);
void setScoutTurnDistanceLimit(uint8_t distanceLimit) { turnDistanceLimit[HeroRole::SCOUT] = distanceLimit; }
@@ -256,17 +283,19 @@ public:
return dangerEvaluator->evaluateDanger(tile, hero, checkGuards);
}
inline uint64_t evaluateArmyLoss(const CGHeroInstance * hero, uint64_t armyValue, uint64_t danger) const
{
double ratio = (double)danger / (armyValue * hero->getFightingStrength());
uint64_t evaluateArmyLoss(const CGHeroInstance * hero, uint64_t armyValue, uint64_t danger) const;
return (uint64_t)(armyValue * ratio * ratio);
inline EPathAccessibility getAccessibility(const int3 & tile, EPathfindingLayer layer) const
{
return (*this->accesibility)[tile.z][tile.x][tile.y][layer];
}
STRONG_INLINE
void resetTile(const int3 & tile, EPathfindingLayer layer, EPathAccessibility accessibility);
inline void resetTile(const int3 & tile, EPathfindingLayer layer, EPathAccessibility tileAccessibility)
{
(*this->accesibility)[tile.z][tile.x][tile.y][layer] = tileAccessibility;
}
STRONG_INLINE int getBucket(const ChainActor * actor) const
inline int getBucket(const ChainActor * actor) const
{
return ((uintptr_t)actor * 395) % AIPathfinding::BUCKET_COUNT;
}
@@ -274,6 +303,44 @@ public:
void calculateTownPortalTeleportations(std::vector<CGPathNode *> & neighbours);
void fillChainInfo(const AIPathNode * node, AIPath & path, int parentIndex) const;
template<typename Fn>
void iterateValidNodes(const int3 & pos, EPathfindingLayer layer, Fn fn)
{
if(blocked(pos, layer))
return;
auto chains = nodes.get(pos);
for(AIPathNode & node : chains)
{
if(node.version != AISharedStorage::version || node.layer != layer)
continue;
fn(node);
}
}
template<typename Fn>
bool iterateValidNodesUntil(const int3 & pos, EPathfindingLayer layer, Fn predicate) const
{
if(blocked(pos, layer))
return false;
auto chains = nodes.get(pos);
for(AIPathNode & node : chains)
{
if(node.version != AISharedStorage::version || node.layer != layer)
continue;
if(predicate(node))
return true;
}
return false;
}
private:
template<class TVector>
void calculateTownPortal(

View File

@@ -33,16 +33,31 @@ bool AIPathfinder::isTileAccessible(const HeroPtr & hero, const int3 & tile) con
|| storage->isTileAccessible(hero, tile, EPathfindingLayer::SAIL);
}
std::vector<AIPath> AIPathfinder::getPathInfo(const int3 & tile, bool includeGraph) const
void AIPathfinder::calculateQuickPathsWithBlocker(std::vector<AIPath> & result, const std::vector<const CGHeroInstance *> & heroes, const int3 & tile)
{
result.clear();
for(auto hero : heroes)
{
auto graph = heroGraphs.find(hero->id);
if(graph != heroGraphs.end())
graph->second->quickAddChainInfoWithBlocker(result, tile, hero, ai);
}
}
void AIPathfinder::calculatePathInfo(std::vector<AIPath> & result, const int3 & tile, bool includeGraph) const
{
const TerrainTile * tileInfo = cb->getTile(tile, false);
result.clear();
if(!tileInfo)
{
return std::vector<AIPath>();
return;
}
auto info = storage->getChainInfo(tile, !tileInfo->isWater());
storage->calculateChainInfo(result, tile, !tileInfo->isWater());
if(includeGraph)
{
@@ -51,11 +66,9 @@ std::vector<AIPath> AIPathfinder::getPathInfo(const int3 & tile, bool includeGra
auto graph = heroGraphs.find(hero->id);
if(graph != heroGraphs.end())
graph->second.addChainInfo(info, tile, hero, ai);
graph->second->addChainInfo(result, tile, hero, ai);
}
}
return info;
}
void AIPathfinder::updatePaths(const std::map<const CGHeroInstance *, HeroRole> & heroes, PathfinderSettings pathfinderSettings)
@@ -134,21 +147,24 @@ void AIPathfinder::updateGraphs(const std::map<const CGHeroInstance *, HeroRole>
for(auto hero : heroes)
{
if(heroGraphs.try_emplace(hero.first->id, GraphPaths()).second)
if(heroGraphs.try_emplace(hero.first->id).second)
{
heroGraphs[hero.first->id] = std::make_unique<GraphPaths>();
heroesVector.push_back(hero.first);
}
}
parallel_for(blocked_range<size_t>(0, heroesVector.size()), [this, &heroesVector](const blocked_range<size_t> & r)
tbb::parallel_for(tbb::blocked_range<size_t>(0, heroesVector.size()), [this, &heroesVector](const tbb::blocked_range<size_t> & r)
{
for(auto i = r.begin(); i != r.end(); i++)
heroGraphs.at(heroesVector[i]->id).calculatePaths(heroesVector[i], ai);
heroGraphs.at(heroesVector[i]->id)->calculatePaths(heroesVector[i], ai);
});
if(NKAI_GRAPH_TRACE_LEVEL >= 1)
{
for(auto hero : heroes)
{
heroGraphs[hero.first->id].dumpToLog();
heroGraphs[hero.first->id]->dumpToLog();
}
}

View File

@@ -40,20 +40,30 @@ private:
std::shared_ptr<AINodeStorage> storage;
CPlayerSpecificInfoCallback * cb;
Nullkiller * ai;
std::map<ObjectInstanceID, GraphPaths> heroGraphs;
std::map<ObjectInstanceID, std::unique_ptr<GraphPaths>> heroGraphs;
public:
AIPathfinder(CPlayerSpecificInfoCallback * cb, Nullkiller * ai);
std::vector<AIPath> getPathInfo(const int3 & tile, bool includeGraph = false) const;
void calculatePathInfo(std::vector<AIPath> & paths, const int3 & tile, bool includeGraph = false) const;
bool isTileAccessible(const HeroPtr & hero, const int3 & tile) const;
void updatePaths(const std::map<const CGHeroInstance *, HeroRole> & heroes, PathfinderSettings pathfinderSettings);
void updateGraphs(const std::map<const CGHeroInstance *, HeroRole> & heroes);
void calculateQuickPathsWithBlocker(std::vector<AIPath> & result, const std::vector<const CGHeroInstance *> & heroes, const int3 & tile);
void init();
std::shared_ptr<AINodeStorage>getStorage()
{
return storage;
}
std::vector<AIPath> getPathInfo(const int3 & tile, bool includeGraph = false)
{
std::vector<AIPath> result;
calculatePathInfo(result, tile, includeGraph);
return result;
}
};
}

View File

@@ -37,10 +37,8 @@ namespace AIPathfinding
return Goals::sptr(Goals::Invalid());
}
bool BuildBoatAction::canAct(const AIPathNode * source) const
bool BuildBoatAction::canAct(const CGHeroInstance * hero, const TResources & reservedResources) const
{
auto hero = source->actor->hero;
if(cb->getPlayerRelations(hero->tempOwner, shipyard->getObject()->getOwner()) == PlayerRelations::ENEMIES)
{
#if NKAI_TRACE_LEVEL > 1
@@ -53,7 +51,7 @@ namespace AIPathfinding
shipyard->getBoatCost(boatCost);
if(!cb->getResourceAmount().canAfford(source->actor->armyCost + boatCost))
if(!cb->getResourceAmount().canAfford(reservedResources + boatCost))
{
#if NKAI_TRACE_LEVEL > 1
logAi->trace("Can not build a boat. Not enough resources.");
@@ -65,6 +63,18 @@ namespace AIPathfinding
return true;
}
bool BuildBoatAction::canAct(const AIPathNode * source) const
{
return canAct(source->actor->hero, source->actor->armyCost);
}
bool BuildBoatAction::canAct(const AIPathNodeInfo & source) const
{
TResources res;
return canAct(source.targetHero, res);
}
const CGObjectInstance * BuildBoatAction::targetObject() const
{
return dynamic_cast<const CGObjectInstance*>(shipyard);
@@ -75,6 +85,11 @@ namespace AIPathfinding
return sourceActor->resourceActor;
}
std::shared_ptr<SpecialAction> BuildBoatActionFactory::create(const Nullkiller * ai)
{
return std::make_shared<BuildBoatAction>(ai->cb.get(), dynamic_cast<const IShipyard * >(ai->cb->getObj(shipyard)));
}
void SummonBoatAction::execute(const CGHeroInstance * hero) const
{
Goals::AdventureSpellCast(hero, SpellID::SUMMON_BOAT).accept(ai);

View File

@@ -57,6 +57,8 @@ namespace AIPathfinding
}
bool canAct(const AIPathNode * source) const override;
bool canAct(const AIPathNodeInfo & source) const override;
bool canAct(const CGHeroInstance * hero, const TResources & reservedResources) const;
void execute(const CGHeroInstance * hero) const override;
@@ -68,6 +70,19 @@ namespace AIPathfinding
const CGObjectInstance * targetObject() const override;
};
class BuildBoatActionFactory : public ISpecialActionFactory
{
ObjectInstanceID shipyard;
public:
BuildBoatActionFactory(ObjectInstanceID shipyard)
:shipyard(shipyard)
{
}
std::shared_ptr<SpecialAction> create(const Nullkiller * ai) override;
};
}
}

View File

@@ -23,6 +23,11 @@ namespace AIPathfinding
return canAct(node->actor->hero);
}
bool QuestAction::canAct(const AIPathNodeInfo & node) const
{
return canAct(node.targetHero);
}
bool QuestAction::canAct(const CGHeroInstance * hero) const
{
if(questInfo.obj->ID == Obj::BORDER_GATE || questInfo.obj->ID == Obj::BORDERGUARD)

View File

@@ -29,7 +29,7 @@ namespace AIPathfinding
}
bool canAct(const AIPathNode * node) const override;
bool canAct(const AIPathNodeInfo & node) const override;
bool canAct(const CGHeroInstance * hero) const;
Goals::TSubgoal decompose(const CGHeroInstance * hero) const override;

View File

@@ -22,6 +22,7 @@ namespace NKAI
{
struct AIPathNode;
struct AIPathNodeInfo;
class ChainActor;
class SpecialAction
@@ -34,6 +35,11 @@ public:
return true;
}
virtual bool canAct(const AIPathNodeInfo & source) const
{
return true;
}
virtual Goals::TSubgoal decompose(const CGHeroInstance * hero) const;
virtual void execute(const CGHeroInstance * hero) const;
@@ -89,4 +95,11 @@ public:
const AIPathNode * srcNode) const override;
};
class ISpecialActionFactory
{
public:
virtual std::shared_ptr<SpecialAction> create(const Nullkiller * ai) = 0;
virtual ~ISpecialActionFactory() = default;
};
}

View File

@@ -16,6 +16,8 @@
#include "../Engine/Nullkiller.h"
#include "../../../lib/logging/VisualLogger.h"
#include "Actions/QuestAction.h"
#include "../pforeach.h"
#include "Actions/BoatActions.h"
namespace NKAI
{
@@ -32,6 +34,7 @@ class ObjectGraphCalculator
private:
ObjectGraph * target;
const Nullkiller * ai;
std::mutex syncLock;
std::map<const CGHeroInstance *, HeroRole> actors;
std::map<const CGHeroInstance *, const CGObjectInstance *> actorObjectMap;
@@ -41,7 +44,7 @@ private:
public:
ObjectGraphCalculator(ObjectGraph * target, const Nullkiller * ai)
:ai(ai), target(target)
:ai(ai), target(target), syncLock()
{
}
@@ -65,17 +68,51 @@ public:
{
updatePaths();
foreach_tile_pos(ai->cb.get(), [this](const CPlayerSpecificInfoCallback * cb, const int3 & pos)
std::vector<AIPath> pathCache;
foreach_tile_pos(ai->cb.get(), [this, &pathCache](const CPlayerSpecificInfoCallback * cb, const int3 & pos)
{
calculateConnections(pos);
calculateConnections(pos, pathCache);
});
removeExtraConnections();
}
float getNeighborConnectionsCost(const int3 & pos, std::vector<AIPath> & pathCache)
{
float neighborCost = std::numeric_limits<float>::max();
if(NKAI_GRAPH_TRACE_LEVEL >= 2)
{
logAi->trace("Checking junction %s", pos.toString());
}
foreach_neighbour(
ai->cb.get(),
pos,
[this, &neighborCost, &pathCache](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor)
{
ai->pathfinder->calculatePathInfo(pathCache, neighbor);
auto costTotal = this->getConnectionsCost(pathCache);
if(costTotal.connectionsCount > 2 && costTotal.avg < neighborCost)
{
neighborCost = costTotal.avg;
if(NKAI_GRAPH_TRACE_LEVEL >= 2)
{
logAi->trace("Better node found at %s", neighbor.toString());
}
}
});
return neighborCost;
}
void addMinimalDistanceJunctions()
{
foreach_tile_pos(ai->cb.get(), [this](const CPlayerSpecificInfoCallback * cb, const int3 & pos)
pforeachTilePaths(ai->cb->getMapSize(), ai, [this](const int3 & pos, std::vector<AIPath> & paths)
{
if(target->hasNodeAt(pos))
return;
@@ -83,35 +120,12 @@ public:
if(ai->cb->getGuardingCreaturePosition(pos).valid())
return;
ConnectionCostInfo currentCost = getConnectionsCost(pos);
ConnectionCostInfo currentCost = getConnectionsCost(paths);
if(currentCost.connectionsCount <= 2)
return;
float neighborCost = currentCost.avg + 0.001f;
if(NKAI_GRAPH_TRACE_LEVEL >= 2)
{
logAi->trace("Checking junction %s", pos.toString());
}
foreach_neighbour(
ai->cb.get(),
pos,
[this, &neighborCost](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor)
{
auto costTotal = this->getConnectionsCost(neighbor);
if(costTotal.avg < neighborCost)
{
neighborCost = costTotal.avg;
if(NKAI_GRAPH_TRACE_LEVEL >= 2)
{
logAi->trace("Better node found at %s", neighbor.toString());
}
}
});
float neighborCost = getNeighborConnectionsCost(pos, paths);
if(currentCost.avg < neighborCost)
{
@@ -132,20 +146,20 @@ private:
ai->pathfinder->updatePaths(actors, ps);
}
void calculateConnections(const int3 & pos)
void calculateConnections(const int3 & pos, std::vector<AIPath> & pathCache)
{
if(target->hasNodeAt(pos))
{
foreach_neighbour(
ai->cb.get(),
pos,
[this, &pos](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor)
[this, &pos, &pathCache](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor)
{
if(target->hasNodeAt(neighbor))
{
auto paths = ai->pathfinder->getPathInfo(neighbor);
ai->pathfinder->calculatePathInfo(pathCache, neighbor);
for(auto & path : paths)
for(auto & path : pathCache)
{
if(pos == path.targetHero->visitablePos())
{
@@ -155,15 +169,46 @@ private:
}
});
auto obj = ai->cb->getTopObj(pos);
if((obj && obj->ID == Obj::BOAT) || target->isVirtualBoat(pos))
{
ai->pathfinder->calculatePathInfo(pathCache, pos);
for(AIPath & path : pathCache)
{
auto from = path.targetHero->visitablePos();
auto fromObj = actorObjectMap[path.targetHero];
auto danger = ai->pathfinder->getStorage()->evaluateDanger(pos, path.targetHero, true);
auto updated = target->tryAddConnection(
from,
pos,
path.movementCost(),
danger);
if(NKAI_GRAPH_TRACE_LEVEL >= 2 && updated)
{
logAi->trace(
"Connected %s[%s] -> %s[%s] through [%s], cost %2f",
fromObj ? fromObj->getObjectName() : "J", from.toString(),
"Boat", pos.toString(),
pos.toString(),
path.movementCost());
}
}
}
return;
}
auto guardPos = ai->cb->getGuardingCreaturePosition(pos);
auto paths = ai->pathfinder->getPathInfo(pos);
ai->pathfinder->calculatePathInfo(pathCache, pos);
for(AIPath & path1 : paths)
for(AIPath & path1 : pathCache)
{
for(AIPath & path2 : paths)
for(AIPath & path2 : pathCache)
{
if(path1.targetHero == path2.targetHero)
continue;
@@ -185,7 +230,9 @@ private:
if(!cb->getTile(pos)->isWater())
continue;
if(obj1 && (obj1->ID != Obj::BOAT || obj1->ID != Obj::SHIPYARD))
auto startingObjIsBoat = (obj1 && obj1->ID == Obj::BOAT) || target->isVirtualBoat(pos1);
if(!startingObjIsBoat)
continue;
}
@@ -273,13 +320,25 @@ private:
assert(objectActor->visitablePos() == visitablePos);
actorObjectMap[objectActor] = obj;
actors[objectActor] = obj->ID == Obj::TOWN || obj->ID == Obj::SHIPYARD ? HeroRole::MAIN : HeroRole::SCOUT;
actors[objectActor] = obj->ID == Obj::TOWN || obj->ID == Obj::BOAT ? HeroRole::MAIN : HeroRole::SCOUT;
target->addObject(obj);
auto shipyard = dynamic_cast<const IShipyard *>(obj);
if(shipyard && shipyard->bestLocation().valid())
{
int3 virtualBoat = shipyard->bestLocation();
addJunctionActor(virtualBoat, true);
target->addVirtualBoat(virtualBoat, obj);
}
}
void addJunctionActor(const int3 & visitablePos)
void addJunctionActor(const int3 & visitablePos, bool isVirtualBoat = false)
{
std::lock_guard<std::mutex> lock(syncLock);
auto internalCb = temporaryActorHeroes.front()->cb;
auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique<CGHeroInstance>(internalCb)).get();
@@ -290,7 +349,7 @@ private:
objectActor->pos = objectActor->convertFromVisitablePos(visitablePos);
objectActor->initObj(rng);
if(cb->getTile(visitablePos)->isWater())
if(isVirtualBoat || ai->cb->getTile(visitablePos)->isWater())
{
objectActor->boat = temporaryBoats.emplace_back(std::make_unique<CGBoat>(objectActor->cb)).get();
}
@@ -298,14 +357,13 @@ private:
assert(objectActor->visitablePos() == visitablePos);
actorObjectMap[objectActor] = nullptr;
actors[objectActor] = HeroRole::SCOUT;
actors[objectActor] = isVirtualBoat ? HeroRole::MAIN : HeroRole::SCOUT;
target->registerJunction(visitablePos);
}
ConnectionCostInfo getConnectionsCost(const int3 & pos) const
ConnectionCostInfo getConnectionsCost(std::vector<AIPath> & paths) const
{
auto paths = ai->pathfinder->getPathInfo(pos);
std::map<int3, float> costs;
for(auto & path : paths)
@@ -349,7 +407,15 @@ bool ObjectGraph::tryAddConnection(
float cost,
uint64_t danger)
{
return nodes[from].connections[to].update(cost, danger);
auto result = nodes[from].connections[to].update(cost, danger);
auto & connection = nodes[from].connections[to];
if(result && isVirtualBoat(to) && !connection.specialAction)
{
connection.specialAction = std::make_shared<AIPathfinding::BuildBoatActionFactory>(virtualBoats[to]);
}
return result;
}
void ObjectGraph::removeConnection(const int3 & from, const int3 & to)
@@ -374,19 +440,30 @@ void ObjectGraph::updateGraph(const Nullkiller * ai)
void ObjectGraph::addObject(const CGObjectInstance * obj)
{
nodes[obj->visitablePos()].init(obj);
if(!hasNodeAt(obj->visitablePos()))
nodes[obj->visitablePos()].init(obj);
}
void ObjectGraph::addVirtualBoat(const int3 & pos, const CGObjectInstance * shipyard)
{
if(!isVirtualBoat(pos))
{
virtualBoats[pos] = shipyard->id;
}
}
void ObjectGraph::registerJunction(const int3 & pos)
{
nodes[pos].initJunction();
if(!hasNodeAt(pos))
nodes[pos].initJunction();
}
void ObjectGraph::removeObject(const CGObjectInstance * obj)
{
nodes[obj->visitablePos()].objectExists = false;
if(obj->ID == Obj::BOAT)
if(obj->ID == Obj::BOAT && !isVirtualBoat(obj->visitablePos()))
{
vstd::erase_if(nodes[obj->visitablePos()].connections, [&](const std::pair<int3, ObjectLink> & link) -> bool
{
@@ -459,9 +536,35 @@ bool GraphNodeComparer::operator()(const GraphPathNodePointer & lhs, const Graph
return pathNodes.at(lhs.coord)[lhs.nodeType].cost > pathNodes.at(rhs.coord)[rhs.nodeType].cost;
}
GraphPaths::GraphPaths()
: visualKey(""), graph(), pathNodes()
{
}
std::shared_ptr<SpecialAction> getCompositeAction(
const Nullkiller * ai,
std::shared_ptr<ISpecialActionFactory> linkActionFactory,
std::shared_ptr<SpecialAction> transitionAction)
{
if(!linkActionFactory)
return transitionAction;
auto linkAction = linkActionFactory->create(ai);
if(!transitionAction)
return linkAction;
std::vector<std::shared_ptr<const SpecialAction>> actionsArray = {
transitionAction,
linkAction
};
return std::make_shared<CompositeAction>(actionsArray);
}
void GraphPaths::calculatePaths(const CGHeroInstance * targetHero, const Nullkiller * ai)
{
graph = *ai->baseGraph;
graph.copyFrom(*ai->baseGraph);
graph.connectHeroes(ai);
visualKey = std::to_string(ai->playerID) + ":" + targetHero->getNameTranslated();
@@ -508,15 +611,16 @@ void GraphPaths::calculatePaths(const CGHeroInstance * targetHero, const Nullkil
node.isInQueue = false;
graph.iterateConnections(pos.coord, [this, ai, &pos, &node, &transitionAction, &pq](int3 target, ObjectLink o)
graph.iterateConnections(pos.coord, [this, ai, &pos, &node, &transitionAction, &pq](int3 target, const ObjectLink & o)
{
auto targetNodeType = o.danger || transitionAction ? GrapthPathNodeType::BATTLE : pos.nodeType;
auto compositeAction = getCompositeAction(ai, o.specialAction, transitionAction);
auto targetNodeType = o.danger || compositeAction ? GrapthPathNodeType::BATTLE : pos.nodeType;
auto targetPointer = GraphPathNodePointer(target, targetNodeType);
auto & targetNode = getOrCreateNode(targetPointer);
if(targetNode.tryUpdate(pos, node, o))
{
targetNode.specialAction = transitionAction;
targetNode.specialAction = compositeAction;
auto targetGraphNode = graph.getNode(target);
@@ -651,6 +755,11 @@ void GraphPaths::addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHe
n.parentIndex = -1;
n.specialAction = getNode(*graphTile).specialAction;
if(n.specialAction)
{
n.actionIsBlocked = !n.specialAction->canAct(n);
}
for(auto & node : path.nodes)
{
node.parentIndex++;
@@ -668,4 +777,121 @@ void GraphPaths::addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHe
}
}
void GraphPaths::quickAddChainInfoWithBlocker(std::vector<AIPath> & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const
{
auto nodes = pathNodes.find(tile);
if(nodes == pathNodes.end())
return;
for(auto & targetNode : nodes->second)
{
if(!targetNode.reachable())
continue;
std::vector<GraphPathNodePointer> tilesToPass;
uint64_t danger = targetNode.danger;
float cost = targetNode.cost;
bool allowBattle = false;
auto current = GraphPathNodePointer(nodes->first, targetNode.nodeType);
while(true)
{
auto currentTile = pathNodes.find(current.coord);
if(currentTile == pathNodes.end())
break;
auto currentNode = currentTile->second[current.nodeType];
allowBattle = allowBattle || currentNode.nodeType == GrapthPathNodeType::BATTLE;
vstd::amax(danger, currentNode.danger);
vstd::amax(cost, currentNode.cost);
tilesToPass.push_back(current);
if(currentNode.cost < 2.0f)
break;
current = currentNode.previous;
}
if(tilesToPass.empty())
continue;
auto entryPaths = ai->pathfinder->getPathInfo(tilesToPass.back().coord);
for(auto & entryPath : entryPaths)
{
if(entryPath.targetHero != hero)
continue;
auto & path = paths.emplace_back();
path.targetHero = entryPath.targetHero;
path.heroArmy = entryPath.heroArmy;
path.exchangeCount = entryPath.exchangeCount;
path.armyLoss = entryPath.armyLoss + ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), danger);
path.targetObjectDanger = ai->pathfinder->getStorage()->evaluateDanger(tile, path.targetHero, !allowBattle);
path.targetObjectArmyLoss = ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), path.targetObjectDanger);
AIPathNodeInfo n;
n.targetHero = hero;
n.parentIndex = -1;
// final node
n.coord = tile;
n.cost = targetNode.cost;
n.danger = targetNode.danger;
n.parentIndex = path.nodes.size();
path.nodes.push_back(n);
for(auto entryNode = entryPath.nodes.rbegin(); entryNode != entryPath.nodes.rend(); entryNode++)
{
auto blocker = ai->objectClusterizer->getBlocker(*entryNode);
if(blocker)
{
// blocker node
path.nodes.push_back(*entryNode);
path.nodes.back().parentIndex = path.nodes.size() - 1;
break;
}
}
if(path.nodes.size() > 1)
continue;
for(auto graphTile = tilesToPass.rbegin(); graphTile != tilesToPass.rend(); graphTile++)
{
auto & node = getNode(*graphTile);
n.coord = graphTile->coord;
n.cost = node.cost;
n.turns = static_cast<ui8>(node.cost);
n.danger = node.danger;
n.specialAction = node.specialAction;
n.parentIndex = path.nodes.size();
if(n.specialAction)
{
n.actionIsBlocked = !n.specialAction->canAct(n);
}
auto blocker = ai->objectClusterizer->getBlocker(n);
if(!blocker)
continue;
// blocker node
path.nodes.push_back(n);
break;
}
}
}
}
}

View File

@@ -22,6 +22,7 @@ struct ObjectLink
{
float cost = 100000; // some big number
uint64_t danger = 0;
std::shared_ptr<ISpecialActionFactory> specialAction;
bool update(float newCost, uint64_t newDanger)
{
@@ -62,17 +63,35 @@ struct ObjectNode
class ObjectGraph
{
std::unordered_map<int3, ObjectNode> nodes;
std::unordered_map<int3, ObjectInstanceID> virtualBoats;
public:
ObjectGraph()
:nodes(), virtualBoats()
{
}
void updateGraph(const Nullkiller * ai);
void addObject(const CGObjectInstance * obj);
void registerJunction(const int3 & pos);
void addVirtualBoat(const int3 & pos, const CGObjectInstance * shipyard);
void connectHeroes(const Nullkiller * ai);
void removeObject(const CGObjectInstance * obj);
bool tryAddConnection(const int3 & from, const int3 & to, float cost, uint64_t danger);
void removeConnection(const int3 & from, const int3 & to);
void dumpToLog(std::string visualKey) const;
bool isVirtualBoat(const int3 & tile) const
{
return vstd::contains(virtualBoats, tile);
}
void copyFrom(const ObjectGraph & other)
{
nodes = other.nodes;
virtualBoats = other.virtualBoats;
}
template<typename Func>
void iterateConnections(const int3 & pos, Func fn)
{
@@ -167,8 +186,10 @@ class GraphPaths
std::string visualKey;
public:
GraphPaths();
void calculatePaths(const CGHeroInstance * targetHero, const Nullkiller * ai);
void addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const;
void quickAddChainInfoWithBlocker(std::vector<AIPath> & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const;
void dumpToLog() const;
private:

View File

@@ -17,7 +17,10 @@ namespace NKAI
{
namespace AIPathfinding
{
AILayerTransitionRule::AILayerTransitionRule(CPlayerSpecificInfoCallback * cb, Nullkiller * ai, std::shared_ptr<AINodeStorage> nodeStorage)
AILayerTransitionRule::AILayerTransitionRule(
CPlayerSpecificInfoCallback * cb,
Nullkiller * ai,
std::shared_ptr<AINodeStorage> nodeStorage)
:cb(cb), ai(ai), nodeStorage(nodeStorage)
{
setup();

View File

@@ -34,7 +34,10 @@ namespace AIPathfinding
std::map<const CGHeroInstance *, std::shared_ptr<const AirWalkingAction>> airWalkingActions;
public:
AILayerTransitionRule(CPlayerSpecificInfoCallback * cb, Nullkiller * ai, std::shared_ptr<AINodeStorage> nodeStorage);
AILayerTransitionRule(
CPlayerSpecificInfoCallback * cb,
Nullkiller * ai,
std::shared_ptr<AINodeStorage> nodeStorage);
virtual void process(
const PathNodeInfo & source,

View File

@@ -40,6 +40,16 @@ namespace AIPathfinding
return;
}
if(!allowBypassObjects
&& destination.action == EPathNodeAction::EMBARK
&& source.node->layer == EPathfindingLayer::LAND
&& destination.node->layer == EPathfindingLayer::SAIL)
{
destination.blocked = true;
return;
}
auto blocker = getBlockingReason(source, destination, pathfinderConfig, pathfinderHelper);

50
AI/Nullkiller/pforeach.h Normal file
View File

@@ -0,0 +1,50 @@
#pragma once
#include "Engine/Nullkiller.h"
namespace NKAI
{
template<typename TFunc>
void pforeachTilePos(const int3 & mapSize, TFunc fn)
{
for(int z = 0; z < mapSize.z; ++z)
{
tbb::parallel_for(tbb::blocked_range<size_t>(0, mapSize.x), [&](const tbb::blocked_range<size_t> & r)
{
int3 pos(0, 0, z);
for(pos.x = r.begin(); pos.x != r.end(); ++pos.x)
{
for(pos.y = 0; pos.y < mapSize.y; ++pos.y)
{
fn(pos);
}
}
});
}
}
template<typename TFunc>
void pforeachTilePaths(const int3 & mapSize, const Nullkiller * ai, TFunc fn)
{
for(int z = 0; z < mapSize.z; ++z)
{
tbb::parallel_for(tbb::blocked_range<size_t>(0, mapSize.x), [&](const tbb::blocked_range<size_t> & r)
{
int3 pos(0, 0, z);
std::vector<AIPath> paths;
for(pos.x = r.begin(); pos.x != r.end(); ++pos.x)
{
for(pos.y = 0; pos.y < mapSize.y; ++pos.y)
{
ai->pathfinder->calculatePathInfo(paths, pos);
fn(pos, paths);
}
}
});
}
}
}

View File

@@ -155,15 +155,21 @@ void AINodeStorage::commit(CDestinationNodeInfo & destination, const PathNodeInf
});
}
std::vector<CGPathNode *> AINodeStorage::calculateNeighbours(
void AINodeStorage::calculateNeighbours(
std::vector<CGPathNode *> & result,
const PathNodeInfo & source,
EPathfindingLayer layer,
const PathfinderConfig * pathfinderConfig,
const CPathfinderHelper * pathfinderHelper)
{
std::vector<CGPathNode *> neighbours;
neighbours.reserve(16);
std::vector<int3> accessibleNeighbourTiles;
result.clear();
accessibleNeighbourTiles.reserve(8);
pathfinderHelper->calculateNeighbourTiles(accessibleNeighbourTiles, source);
const AIPathNode * srcNode = getAINode(source.node);
auto accessibleNeighbourTiles = pathfinderHelper->getNeighbourTiles(source);
for(auto & neighbour : accessibleNeighbourTiles)
{
@@ -174,11 +180,9 @@ std::vector<CGPathNode *> AINodeStorage::calculateNeighbours(
if(!nextNode || nextNode.value()->accessible == EPathAccessibility::NOT_SET)
continue;
neighbours.push_back(nextNode.value());
result.push_back(nextNode.value());
}
}
return neighbours;
}
void AINodeStorage::setHero(HeroPtr heroPtr, const VCAI * _ai)

View File

@@ -89,8 +89,10 @@ public:
std::vector<CGPathNode *> getInitialNodes() override;
virtual std::vector<CGPathNode *> calculateNeighbours(
virtual void calculateNeighbours(
std::vector<CGPathNode *> & result,
const PathNodeInfo & source,
EPathfindingLayer layer,
const PathfinderConfig * pathfinderConfig,
const CPathfinderHelper * pathfinderHelper) override;

View File

@@ -56,6 +56,7 @@ namespace po = boost::program_options;
namespace po_style = boost::program_options::command_line_style;
static std::atomic<bool> quitRequestedDuringOpeningPlayback = false;
static std::atomic<bool> headlessQuit = false;
#ifndef VCMI_IOS
void processCommand(const std::string &message);
@@ -371,8 +372,10 @@ int main(int argc, char * argv[])
}
else
{
while(true)
while(!headlessQuit)
boost::this_thread::sleep_for(boost::chrono::milliseconds(200));
quitApplication();
}
return 0;
@@ -481,7 +484,15 @@ void handleQuit(bool ask)
// proper solution would be to abort init thread (or wait for it to finish)
if(!ask)
{
quitApplication();
if(settings["session"]["headless"].Bool())
{
headlessQuit = true;
}
else
{
quitApplication();
}
return;
}

View File

@@ -410,7 +410,11 @@ void ApplyClientNetPackVisitor::visitPlayerEndsGame(PlayerEndsGame & pack)
// In auto testing pack.mode we always close client if red pack.player won or lose
if(!settings["session"]["testmap"].isNull() && pack.player == PlayerColor(0))
{
logAi->info("Red player %s. Ending game.", pack.victoryLossCheckResult.victory() ? "won" : "lost");
handleQuit(settings["session"]["spectate"].Bool()); // if spectator is active ask to close client or not
}
}
void ApplyClientNetPackVisitor::visitPlayerReinitInterface(PlayerReinitInterface & pack)

View File

@@ -49,30 +49,27 @@ bool CPathfinderHelper::canMoveFromNode(const PathNodeInfo & source) const
return true;
}
std::vector<int3> CPathfinderHelper::getNeighbourTiles(const PathNodeInfo & source) const
void CPathfinderHelper::calculateNeighbourTiles(std::vector<int3> & result, const PathNodeInfo & source) const
{
std::vector<int3> neighbourTiles;
result.clear();
if (!canMoveFromNode(source))
return neighbourTiles;
return;
neighbourTiles.reserve(8);
getNeighbours(
*source.tile,
source.node->coord,
neighbourTiles,
result,
boost::logic::indeterminate,
source.node->layer == EPathfindingLayer::SAIL);
if(source.isNodeObjectVisitable())
{
vstd::erase_if(neighbourTiles, [&](const int3 & tile) -> bool
vstd::erase_if(result, [&](const int3 & tile) -> bool
{
return !canMoveBetween(tile, source.nodeObject->visitablePos());
});
}
return neighbourTiles;
}
CPathfinder::CPathfinder(CGameState * _gs, std::shared_ptr<PathfinderConfig> config):
@@ -129,6 +126,8 @@ void CPathfinder::calculatePaths()
pq.push(initialNode);
}
std::vector<CGPathNode *> neighbourNodes;
while(!pq.empty())
{
counter++;
@@ -158,43 +157,47 @@ void CPathfinder::calculatePaths()
source.updateInfo(hlp, gamestate);
//add accessible neighbouring nodes to the queue
auto neighbourNodes = config->nodeStorage->calculateNeighbours(source, config.get(), hlp);
for(CGPathNode * neighbour : neighbourNodes)
for(EPathfindingLayer layer = EPathfindingLayer::LAND; layer < EPathfindingLayer::NUM_LAYERS; layer.advance(1))
{
if(neighbour->locked)
if(!hlp->isLayerAvailable(layer))
continue;
if(!hlp->isLayerAvailable(neighbour->layer))
continue;
config->nodeStorage->calculateNeighbours(neighbourNodes, source, layer, config.get(), hlp);
destination.setNode(gamestate, neighbour);
hlp = config->getOrCreatePathfinderHelper(destination, gamestate);
if(!hlp->isPatrolMovementAllowed(neighbour->coord))
continue;
/// Check transition without tile accessability rules
if(source.node->layer != neighbour->layer && !isLayerTransitionPossible())
continue;
destination.turn = turn;
destination.movementLeft = movement;
destination.cost = cost;
destination.updateInfo(hlp, gamestate);
destination.isGuardianTile = destination.guarded && isDestinationGuardian();
for(const auto & rule : config->rules)
for(CGPathNode * neighbour : neighbourNodes)
{
rule->process(source, destination, config.get(), hlp);
if(neighbour->locked)
continue;
if(destination.blocked)
break;
}
destination.setNode(gamestate, neighbour);
hlp = config->getOrCreatePathfinderHelper(destination, gamestate);
if(!destination.blocked)
push(destination.node);
if(!hlp->isPatrolMovementAllowed(neighbour->coord))
continue;
} //neighbours loop
/// Check transition without tile accessability rules
if(source.node->layer != neighbour->layer && !isLayerTransitionPossible())
continue;
destination.turn = turn;
destination.movementLeft = movement;
destination.cost = cost;
destination.updateInfo(hlp, gamestate);
destination.isGuardianTile = destination.guarded && isDestinationGuardian();
for(const auto & rule : config->rules)
{
rule->process(source, destination, config.get(), hlp);
if(destination.blocked)
break;
}
if(!destination.blocked)
push(destination.node);
} //neighbours loop
}
//just add all passable teleport exits
hlp = config->getOrCreatePathfinderHelper(source, gamestate);

View File

@@ -96,7 +96,7 @@ public:
bool addTeleportWhirlpool(const CGWhirlpool * obj) const;
bool canMoveBetween(const int3 & a, const int3 & b) const; //checks only for visitable objects that may make moving between tiles impossible, not other conditions (like tiles itself accessibility)
std::vector<int3> getNeighbourTiles(const PathNodeInfo & source) const;
void calculateNeighbourTiles(std::vector<int3> & result, const PathNodeInfo & source) const;
std::vector<int3> getTeleportExits(const PathNodeInfo & source) const;
void getNeighbours(

View File

@@ -31,8 +31,10 @@ public:
virtual std::vector<CGPathNode *> getInitialNodes() = 0;
virtual std::vector<CGPathNode *> calculateNeighbours(
virtual void calculateNeighbours(
std::vector<CGPathNode *> & result,
const PathNodeInfo & source,
EPathfindingLayer layer,
const PathfinderConfig * pathfinderConfig,
const CPathfinderHelper * pathfinderHelper) = 0;

View File

@@ -60,29 +60,29 @@ void NodeStorage::initialize(const PathfinderOptions & options, const CGameState
}
}
std::vector<CGPathNode *> NodeStorage::calculateNeighbours(
void NodeStorage::calculateNeighbours(
std::vector<CGPathNode *> & result,
const PathNodeInfo & source,
EPathfindingLayer layer,
const PathfinderConfig * pathfinderConfig,
const CPathfinderHelper * pathfinderHelper)
{
std::vector<CGPathNode *> neighbours;
neighbours.reserve(16);
auto accessibleNeighbourTiles = pathfinderHelper->getNeighbourTiles(source);
std::vector<int3> accessibleNeighbourTiles;
result.clear();
accessibleNeighbourTiles.reserve(8);
pathfinderHelper->calculateNeighbourTiles(accessibleNeighbourTiles, source);
for(auto & neighbour : accessibleNeighbourTiles)
{
for(EPathfindingLayer i = EPathfindingLayer::LAND; i < EPathfindingLayer::NUM_LAYERS; i.advance(1))
{
auto * node = getNode(neighbour, i);
auto * node = getNode(neighbour, layer);
if(node->accessible == EPathAccessibility::NOT_SET)
continue;
if(node->accessible == EPathAccessibility::NOT_SET)
continue;
neighbours.push_back(node);
}
result.push_back(node);
}
return neighbours;
}
std::vector<CGPathNode *> NodeStorage::calculateTeleportations(

View File

@@ -36,8 +36,10 @@ public:
std::vector<CGPathNode *> getInitialNodes() override;
virtual std::vector<CGPathNode *> calculateNeighbours(
virtual void calculateNeighbours(
std::vector<CGPathNode *> & result,
const PathNodeInfo & source,
EPathfindingLayer layer,
const PathfinderConfig * pathfinderConfig,
const CPathfinderHelper * pathfinderHelper) override;