1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-01-12 02:28:11 +02:00

NKAI: initial implementation of object graph

This commit is contained in:
Andrii Danylchenko 2024-01-20 22:54:30 +02:00
parent a373ec6743
commit 376a17409f
17 changed files with 476 additions and 50 deletions

View File

@ -185,8 +185,8 @@ void foreach_tile_pos(const Func & foo)
}
}
template<class Func>
void foreach_tile_pos(CCallback * cbp, const Func & foo) // avoid costly retrieval of thread-specific pointer
template<class Func, class TCallback>
void foreach_tile_pos(TCallback * cbp, const Func & foo) // avoid costly retrieval of thread-specific pointer
{
int3 mapSize = cbp->getMapSize();
for(int z = 0; z < mapSize.z; z++)

View File

@ -75,8 +75,7 @@ void DangerHitMapAnalyzer::updateHitMap()
PathfinderSettings ps;
ps.mainTurnDistanceLimit = 10;
ps.scoutTurnDistanceLimit = 10;
ps.scoutTurnDistanceLimit = ps.mainTurnDistanceLimit = MAIN_TURN_DISTANCE_LIMIT;
ps.useHeroChain = false;
ai->pathfinder->updatePaths(pair.second, ps);
@ -160,9 +159,6 @@ void DangerHitMapAnalyzer::calculateTileOwners()
std::map<const CGHeroInstance *, HeroRole> townHeroes;
std::map<const CGHeroInstance *, const CGTownInstance *> heroTownMap;
PathfinderSettings pathfinderSettings;
pathfinderSettings.mainTurnDistanceLimit = 5;
auto addTownHero = [&](const CGTownInstance * town)
{
@ -192,7 +188,10 @@ void DangerHitMapAnalyzer::calculateTileOwners()
addTownHero(town);
}
ai->pathfinder->updatePaths(townHeroes, PathfinderSettings());
PathfinderSettings ps;
ps.mainTurnDistanceLimit = ps.scoutTurnDistanceLimit = MAIN_TURN_DISTANCE_LIMIT;
ai->pathfinder->updatePaths(townHeroes, ps);
pforeachTilePos(mapSize, [&](const int3 & pos)
{
@ -239,6 +238,11 @@ void DangerHitMapAnalyzer::calculateTileOwners()
hitMap[pos.x][pos.y][pos.z].closestTown = enemyTown;
}
});
for(auto h : townHeroes)
{
delete h.first;
}
}
const std::vector<HitMapInfo> & DangerHitMapAnalyzer::getTownThreats(const CGTownInstance * town) const

View File

@ -244,7 +244,7 @@ void ObjectClusterizer::clusterize()
logAi->trace("Check object %s%s.", obj->getObjectName(), obj->visitablePos().toString());
#endif
auto paths = ai->pathfinder->getPathInfo(obj->visitablePos());
auto paths = ai->pathfinder->getPathInfo(obj->visitablePos(), true);
if(paths.empty())
{

View File

@ -179,7 +179,7 @@ Goals::TGoalVec CaptureObjectsBehavior::decompose() const
const int3 pos = objToVisit->visitablePos();
auto paths = ai->nullkiller->pathfinder->getPathInfo(pos);
auto paths = ai->nullkiller->pathfinder->getPathInfo(pos, true);
std::vector<std::shared_ptr<ExecuteHeroChain>> waysToVisitObj;
std::shared_ptr<ExecuteHeroChain> closestWay;
@ -210,7 +210,7 @@ Goals::TGoalVec CaptureObjectsBehavior::decompose() const
{
captureObjects(ai->nullkiller->objectClusterizer->getNearbyObjects());
if(tasks.empty() || ai->nullkiller->getScanDepth() != ScanDepth::SMALL)
if(tasks.empty())
captureObjects(ai->nullkiller->objectClusterizer->getFarObjects());
}

View File

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

View File

@ -14,6 +14,7 @@ set(Nullkiller_SRCS
Pathfinding/Rules/AIMovementAfterDestinationRule.cpp
Pathfinding/Rules/AIMovementToDestinationRule.cpp
Pathfinding/Rules/AIPreviousNodeRule.cpp
Pathfinding/ObjectGraph.cpp
AIUtility.cpp
Analyzers/ArmyManager.cpp
Analyzers/HeroManager.cpp
@ -78,6 +79,7 @@ set(Nullkiller_HEADERS
Pathfinding/Rules/AIMovementAfterDestinationRule.h
Pathfinding/Rules/AIMovementToDestinationRule.h
Pathfinding/Rules/AIPreviousNodeRule.h
Pathfinding/ObjectGraph.h
AIUtility.h
Analyzers/ArmyManager.h
Analyzers/HeroManager.h

View File

@ -27,6 +27,9 @@ namespace NKAI
using namespace Goals;
// while we play vcmieagles graph can be shared
std::unique_ptr<ObjectGraph> Nullkiller::baseGraph;
Nullkiller::Nullkiller()
:activeHero(nullptr), scanDepth(ScanDepth::MAIN_FULL), useHeroChain(true)
{
@ -119,6 +122,12 @@ void Nullkiller::resetAiState()
lockedHeroes.clear();
dangerHitMap->reset();
useHeroChain = true;
if(!baseGraph)
{
baseGraph = std::make_unique<ObjectGraph>();
baseGraph->updateGraph(this);
}
}
void Nullkiller::updateAiState(int pass, bool fast)
@ -173,6 +182,7 @@ void Nullkiller::updateAiState(int pass, bool fast)
boost::this_thread::interruption_point();
pathfinder->updatePaths(activeHeroes, cfg);
pathfinder->updateGraphs(activeHeroes);
boost::this_thread::interruption_point();

View File

@ -25,7 +25,6 @@ namespace NKAI
{
const float MIN_PRIORITY = 0.01f;
const float SMALL_SCAN_MIN_PRIORITY = 0.4f;
enum class HeroLockedReason
{
@ -38,15 +37,6 @@ enum class HeroLockedReason
HERO_CHAIN = 3
};
enum class ScanDepth
{
MAIN_FULL = 0,
SMALL = 1,
ALL_FULL = 2
};
class Nullkiller
{
private:
@ -54,11 +44,12 @@ private:
int3 targetTile;
ObjectInstanceID targetObject;
std::map<const CGHeroInstance *, HeroLockedReason> lockedHeroes;
ScanDepth scanDepth;
TResources lockedResources;
bool useHeroChain;
public:
static std::unique_ptr<ObjectGraph> baseGraph;
std::unique_ptr<DangerHitMapAnalyzer> dangerHitMap;
std::unique_ptr<BuildAnalyzer> buildAnalyzer;
std::unique_ptr<ObjectClusterizer> objectClusterizer;
@ -94,7 +85,6 @@ public:
int32_t getFreeGold() const { return getFreeResources()[EGameResID::GOLD]; }
void lockResources(const TResources & res);
const TResources & getLockedResources() const { return lockedResources; }
ScanDepth getScanDepth() const { return scanDepth; }
private:
void resetAiState();

View File

@ -26,13 +26,9 @@ namespace NKAI
{
namespace AIPathfinding
{
#ifdef ENVIRONMENT64
const int BUCKET_COUNT = 7;
#else
const int BUCKET_COUNT = 5;
#endif // ENVIRONMENT64
const int BUCKET_SIZE = 5;
const int BUCKET_COUNT = 5;
const int BUCKET_SIZE = 3;
const int NUM_CHAINS = BUCKET_COUNT * BUCKET_SIZE;
const int THREAD_COUNT = 8;
const int CHAIN_MAX_DEPTH = 4;

View File

@ -33,7 +33,7 @@ bool AIPathfinder::isTileAccessible(const HeroPtr & hero, const int3 & tile) con
|| storage->isTileAccessible(hero, tile, EPathfindingLayer::SAIL);
}
std::vector<AIPath> AIPathfinder::getPathInfo(const int3 & tile) const
std::vector<AIPath> AIPathfinder::getPathInfo(const int3 & tile, bool includeGraph) const
{
const TerrainTile * tileInfo = cb->getTile(tile, false);
@ -42,7 +42,19 @@ std::vector<AIPath> AIPathfinder::getPathInfo(const int3 & tile) const
return std::vector<AIPath>();
}
return storage->getChainInfo(tile, !tileInfo->isWater());
auto info = storage->getChainInfo(tile, !tileInfo->isWater());
if(includeGraph)
{
for(auto hero : cb->getHeroesInfo())
{
auto & graph = heroGraphs.at(hero->id);
graph.addChainInfo(info, tile, hero, ai);
}
}
return info;
}
void AIPathfinder::updatePaths(std::map<const CGHeroInstance *, HeroRole> heroes, PathfinderSettings pathfinderSettings)
@ -71,7 +83,7 @@ void AIPathfinder::updatePaths(std::map<const CGHeroInstance *, HeroRole> heroes
storage->setTownsAndDwellings(cb->getTownsInfo(), ai->memory->visitableObjs);
}
auto config = std::make_shared<AIPathfinding::AIPathfinderConfig>(cb, ai, storage);
auto config = std::make_shared<AIPathfinding::AIPathfinderConfig>(cb, ai, storage, pathfinderSettings.allowBypassObjects);
logAi->trace("Recalculate paths pass %d", pass++);
cb->calculatePaths(config);
@ -112,4 +124,33 @@ void AIPathfinder::updatePaths(std::map<const CGHeroInstance *, HeroRole> heroes
logAi->trace("Recalculated paths in %ld", timeElapsed(start));
}
void AIPathfinder::updateGraphs(std::map<const CGHeroInstance *, HeroRole> heroes)
{
auto start = std::chrono::high_resolution_clock::now();
std::vector<const CGHeroInstance *> heroesVector;
heroGraphs.clear();
for(auto hero : heroes)
{
heroGraphs.emplace(hero.first->id, GraphPaths());
heroesVector.push_back(hero.first);
}
parallel_for(blocked_range<size_t>(0, heroesVector.size()), [&](const blocked_range<size_t> & r)
{
for(auto i = r.begin(); i != r.end(); i++)
heroGraphs[heroesVector[i]->id].calculatePaths(heroesVector[i], ai);
});
#if NKAI_TRACE_LEVEL >= 2
for(auto hero : heroes)
{
heroGraphs[hero.first->id].dumpToLog();
}
#endif
logAi->trace("Graph paths updated in %lld", timeElapsed(start));
}
}

View File

@ -11,6 +11,7 @@
#pragma once
#include "AINodeStorage.h"
#include "ObjectGraph.h"
#include "../AIUtility.h"
namespace NKAI
@ -23,11 +24,13 @@ struct PathfinderSettings
bool useHeroChain;
uint8_t scoutTurnDistanceLimit;
uint8_t mainTurnDistanceLimit;
bool allowBypassObjects;
PathfinderSettings()
:useHeroChain(false),
scoutTurnDistanceLimit(255),
mainTurnDistanceLimit(255)
mainTurnDistanceLimit(255),
allowBypassObjects(true)
{ }
};
@ -37,12 +40,14 @@ private:
std::shared_ptr<AINodeStorage> storage;
CPlayerSpecificInfoCallback * cb;
Nullkiller * ai;
std::map<ObjectInstanceID, GraphPaths> heroGraphs;
public:
AIPathfinder(CPlayerSpecificInfoCallback * cb, Nullkiller * ai);
std::vector<AIPath> getPathInfo(const int3 & tile) const;
std::vector<AIPath> getPathInfo(const int3 & tile, bool includeGraph = false) const;
bool isTileAccessible(const HeroPtr & hero, const int3 & tile) const;
void updatePaths(std::map<const CGHeroInstance *, HeroRole> heroes, PathfinderSettings pathfinderSettings);
void updateGraphs(std::map<const CGHeroInstance *, HeroRole> heroes);
void init();
};

View File

@ -24,16 +24,17 @@ namespace AIPathfinding
std::vector<std::shared_ptr<IPathfindingRule>> makeRuleset(
CPlayerSpecificInfoCallback * cb,
Nullkiller * ai,
std::shared_ptr<AINodeStorage> nodeStorage)
std::shared_ptr<AINodeStorage> nodeStorage,
bool allowBypassObjects)
{
std::vector<std::shared_ptr<IPathfindingRule>> rules = {
std::make_shared<AILayerTransitionRule>(cb, ai, nodeStorage),
std::make_shared<DestinationActionRule>(),
std::make_shared<AIMovementToDestinationRule>(nodeStorage),
std::make_shared<MovementCostRule>(),
std::make_shared<AIPreviousNodeRule>(nodeStorage),
std::make_shared<AIMovementAfterDestinationRule>(cb, nodeStorage)
};
std::vector<std::shared_ptr<IPathfindingRule>> rules = {
std::make_shared<AILayerTransitionRule>(cb, ai, nodeStorage),
std::make_shared<DestinationActionRule>(),
std::make_shared<AIMovementToDestinationRule>(nodeStorage),
std::make_shared<MovementCostRule>(),
std::make_shared<AIPreviousNodeRule>(nodeStorage),
std::make_shared<AIMovementAfterDestinationRule>(cb, nodeStorage, allowBypassObjects)
};
return rules;
}
@ -41,8 +42,9 @@ namespace AIPathfinding
AIPathfinderConfig::AIPathfinderConfig(
CPlayerSpecificInfoCallback * cb,
Nullkiller * ai,
std::shared_ptr<AINodeStorage> nodeStorage)
:PathfinderConfig(nodeStorage, makeRuleset(cb, ai, nodeStorage)), aiNodeStorage(nodeStorage)
std::shared_ptr<AINodeStorage> nodeStorage,
bool allowBypassObjects)
:PathfinderConfig(nodeStorage, makeRuleset(cb, ai, nodeStorage, allowBypassObjects)), aiNodeStorage(nodeStorage)
{
options.canUseCast = true;
}

View File

@ -30,7 +30,8 @@ namespace AIPathfinding
AIPathfinderConfig(
CPlayerSpecificInfoCallback * cb,
Nullkiller * ai,
std::shared_ptr<AINodeStorage> nodeStorage);
std::shared_ptr<AINodeStorage> nodeStorage,
bool allowBypassObjects);
~AIPathfinderConfig();

View File

@ -0,0 +1,237 @@
/*
* ObjectGraph.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 "ObjectGraph.h"
#include "AIPathfinderConfig.h"
#include "../../../CCallback.h"
#include "../../../lib/mapping/CMap.h"
#include "../Engine/Nullkiller.h"
namespace NKAI
{
void ObjectGraph::updateGraph(const Nullkiller * ai)
{
auto cb = ai->cb;
auto mapSize = cb->getMapSize();
std::map<const CGHeroInstance *, HeroRole> actors;
std::map<const CGHeroInstance *, const CGObjectInstance *> actorObjectMap;
auto addObjectActor = [&](const CGObjectInstance * obj)
{
auto objectActor = new CGHeroInstance(obj->cb);
CRandomGenerator rng;
auto visitablePos = obj->visitablePos();
objectActor->setOwner(ai->playerID); // lets avoid having multiple colors
objectActor->initHero(rng, static_cast<HeroTypeID>(0));
objectActor->pos = objectActor->convertFromVisitablePos(visitablePos);
objectActor->initObj(rng);
actorObjectMap[objectActor] = obj;
actors[objectActor] = obj->ID == Obj::TOWN ? HeroRole::MAIN : HeroRole::SCOUT;
addObject(obj);
};
for(auto obj : ai->memory->visitableObjs)
{
if(obj && obj->isVisitable() && obj->ID != Obj::HERO)
{
addObjectActor(obj);
}
}
for(auto town : cb->getTownsInfo())
{
addObjectActor(town);
}
PathfinderSettings ps;
ps.mainTurnDistanceLimit = 5;
ps.scoutTurnDistanceLimit = 1;
ps.allowBypassObjects = false;
ai->pathfinder->updatePaths(actors, ps);
foreach_tile_pos(cb.get(), [&](const CPlayerSpecificInfoCallback * cb, const int3 & pos)
{
auto paths = ai->pathfinder->getPathInfo(pos);
for(AIPath & path1 : paths)
{
for(AIPath & path2 : paths)
{
if(path1.targetHero == path2.targetHero)
continue;
auto obj1 = actorObjectMap[path1.targetHero];
auto obj2 = actorObjectMap[path2.targetHero];
nodes[obj1->visitablePos()].connections[obj2->visitablePos()].update(
path1.movementCost() + path2.movementCost(),
path1.getPathDanger() + path2.getPathDanger());
}
}
});
for(auto h : actors)
{
delete h.first;
}
}
void ObjectGraph::addObject(const CGObjectInstance * obj)
{
nodes[obj->visitablePos()].init(obj);
}
void ObjectGraph::connectHeroes(const Nullkiller * ai)
{
for(auto obj : ai->memory->visitableObjs)
{
if(obj && obj->ID == Obj::HERO)
{
addObject(obj);
}
}
for(auto node : nodes)
{
auto pos = node.first;
auto paths = ai->pathfinder->getPathInfo(pos);
for(AIPath & path : paths)
{
if(path.turn() == 0)
continue;
auto heroPos = path.targetHero->visitablePos();
nodes[pos].connections[heroPos].update(
path.movementCost(),
path.getPathDanger());
nodes[heroPos].connections[pos].update(
path.movementCost(),
path.getPathDanger());
}
}
}
bool GraphNodeComparer::operator()(int3 lhs, int3 rhs) const
{
return pathNodes.at(lhs).cost > pathNodes.at(rhs).cost;
}
void GraphPaths::calculatePaths(const CGHeroInstance * targetHero, const Nullkiller * ai)
{
graph = *ai->baseGraph;
graph.connectHeroes(ai);
pathNodes.clear();
GraphNodeComparer cmp(pathNodes);
GraphPathNode::TFibHeap pq(cmp);
pathNodes[targetHero->visitablePos()].cost = 0;
pq.emplace(targetHero->visitablePos());
while(!pq.empty())
{
int3 pos = pq.top();
pq.pop();
auto node = pathNodes[pos];
node.isInQueue = false;
graph.iterateConnections(pos, [&](int3 target, ObjectLink o)
{
auto & targetNode = pathNodes[target];
if(targetNode.tryUpdate(pos, node, o))
{
if(targetNode.isInQueue)
{
pq.increase(targetNode.handle);
}
else
{
targetNode.handle = pq.emplace(target);
targetNode.isInQueue = true;
}
}
});
}
}
void GraphPaths::dumpToLog() const
{
for(auto & node : pathNodes)
{
logAi->trace(
"%s -> %s: %f !%d",
node.second.previous.toString(),
node.first.toString(),
node.second.cost,
node.second.danger);
}
}
void GraphPaths::addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const
{
auto node = pathNodes.find(tile);
if(node == pathNodes.end() || !node->second.reachable())
return;
std::vector<int3> tilesToPass;
uint64_t danger = node->second.danger;
float cost = node->second.cost;;
while(node != pathNodes.end() && node->second.cost > 1)
{
vstd::amax(danger, node->second.danger);
tilesToPass.push_back(node->first);
node = pathNodes.find(node->second.previous);
}
if(tilesToPass.empty())
return;
auto entryPaths = ai->pathfinder->getPathInfo(tilesToPass.back());
for(auto & path : entryPaths)
{
if(path.targetHero != hero)
continue;
for(auto graphTile : tilesToPass)
{
AIPathNodeInfo n;
n.coord = graphTile;
n.cost = cost;
n.turns = static_cast<ui8>(cost) + 1; // just in case lets select worst scenario
n.danger = danger;
n.targetHero = hero;
path.nodes.insert(path.nodes.begin(), n);
}
paths.push_back(path);
}
}
}

View File

@ -0,0 +1,129 @@
/*
* ObjectGraph.h, 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
*
*/
#pragma once
#include "AINodeStorage.h"
#include "../AIUtility.h"
namespace NKAI
{
class Nullkiller;
struct ObjectLink
{
float cost = 100000; // some big number
uint64_t danger = 0;
void update(float newCost, uint64_t newDanger)
{
if(cost > newCost)
{
cost = newCost;
danger = newDanger;
}
}
};
struct ObjectNode
{
ObjectInstanceID objID;
bool objectExists;
std::unordered_map<int3, ObjectLink> connections;
void init(const CGObjectInstance * obj)
{
objectExists = true;
objID = obj->id;
}
};
class ObjectGraph
{
std::unordered_map<int3, ObjectNode> nodes;
public:
void updateGraph(const Nullkiller * ai);
void addObject(const CGObjectInstance * obj);
void connectHeroes(const Nullkiller * ai);
template<typename Func>
void iterateConnections(const int3 & pos, Func fn)
{
for(auto connection : nodes.at(pos).connections)
{
fn(connection.first, connection.second);
}
}
};
struct GraphPathNode;
class GraphNodeComparer
{
const std::unordered_map<int3, GraphPathNode> & pathNodes;
public:
GraphNodeComparer(const std::unordered_map<int3, GraphPathNode> & pathNodes)
:pathNodes(pathNodes)
{
}
bool operator()(int3 lhs, int3 rhs) const;
};
struct GraphPathNode
{
const float BAD_COST = 100000;
int3 previous = int3(-1);
float cost = BAD_COST;
uint64_t danger = 0;
using TFibHeap = boost::heap::fibonacci_heap<int3, boost::heap::compare<GraphNodeComparer>>;
TFibHeap::handle_type handle;
bool isInQueue = false;
bool reachable() const
{
return cost < BAD_COST;
}
bool tryUpdate(const int3 & pos, const GraphPathNode & prev, const ObjectLink & link)
{
auto newCost = prev.cost + link.cost;
if(newCost < cost)
{
previous = pos;
danger = prev.danger + link.danger;
cost = newCost;
return true;
}
return false;
}
};
class GraphPaths
{
ObjectGraph graph;
std::unordered_map<int3, GraphPathNode> pathNodes;
public:
void calculatePaths(const CGHeroInstance * targetHero, const Nullkiller * ai);
void addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const;
void dumpToLog() const;
};
}

View File

@ -13,6 +13,7 @@
#include "../Actions/QuestAction.h"
#include "../../Goals/Invalid.h"
#include "AIPreviousNodeRule.h"
#include "../../../../lib/pathfinder/PathfinderOptions.h"
namespace NKAI
{
@ -20,8 +21,9 @@ namespace AIPathfinding
{
AIMovementAfterDestinationRule::AIMovementAfterDestinationRule(
CPlayerSpecificInfoCallback * cb,
std::shared_ptr<AINodeStorage> nodeStorage)
:cb(cb), nodeStorage(nodeStorage)
std::shared_ptr<AINodeStorage> nodeStorage,
bool allowBypassObjects)
:cb(cb), nodeStorage(nodeStorage), allowBypassObjects(allowBypassObjects)
{
}
@ -47,6 +49,9 @@ namespace AIPathfinding
return;
}
if(!allowBypassObjects)
return;
#if NKAI_PATHFINDER_TRACE_LEVEL >= 2
logAi->trace(
"Movement from tile %s is blocked. Try to bypass. Action: %d, blocker: %d",

View File

@ -25,9 +25,13 @@ namespace AIPathfinding
private:
CPlayerSpecificInfoCallback * cb;
std::shared_ptr<AINodeStorage> nodeStorage;
bool allowBypassObjects;
public:
AIMovementAfterDestinationRule(CPlayerSpecificInfoCallback * cb, std::shared_ptr<AINodeStorage> nodeStorage);
AIMovementAfterDestinationRule(
CPlayerSpecificInfoCallback * cb,
std::shared_ptr<AINodeStorage> nodeStorage,
bool allowBypassObjects);
virtual void process(
const PathNodeInfo & source,