mirror of
https://github.com/vcmi/vcmi.git
synced 2025-01-18 03:21:27 +02:00
NKAI: port exploration from VCAI
This commit is contained in:
parent
721b15d9de
commit
1388fb1493
@ -77,7 +77,6 @@ AIGateway::AIGateway()
|
|||||||
destinationTeleport = ObjectInstanceID();
|
destinationTeleport = ObjectInstanceID();
|
||||||
destinationTeleportPos = int3(-1);
|
destinationTeleportPos = int3(-1);
|
||||||
nullkiller.reset(new Nullkiller());
|
nullkiller.reset(new Nullkiller());
|
||||||
announcedCheatingProblem = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AIGateway::~AIGateway()
|
AIGateway::~AIGateway()
|
||||||
@ -378,7 +377,7 @@ void AIGateway::objectRemoved(const CGObjectInstance * obj, const PlayerColor &
|
|||||||
nullkiller->memory->removeFromMemory(obj);
|
nullkiller->memory->removeFromMemory(obj);
|
||||||
nullkiller->objectClusterizer->onObjectRemoved(obj->id);
|
nullkiller->objectClusterizer->onObjectRemoved(obj->id);
|
||||||
|
|
||||||
if(nullkiller->baseGraph && nullkiller->settings->isObjectGraphAllowed())
|
if(nullkiller->baseGraph && nullkiller->isObjectGraphAllowed())
|
||||||
{
|
{
|
||||||
nullkiller->baseGraph->removeObject(obj);
|
nullkiller->baseGraph->removeObject(obj);
|
||||||
}
|
}
|
||||||
@ -829,13 +828,9 @@ void AIGateway::makeTurn()
|
|||||||
boost::shared_lock<boost::shared_mutex> gsLock(CGameState::mutex);
|
boost::shared_lock<boost::shared_mutex> gsLock(CGameState::mutex);
|
||||||
setThreadName("AIGateway::makeTurn");
|
setThreadName("AIGateway::makeTurn");
|
||||||
|
|
||||||
if(cb->getStartInfo()->extraOptionsInfo.cheatsAllowed)
|
if(nullkiller->isOpenMap())
|
||||||
cb->sendMessage("vcmieagles");
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
if(!announcedCheatingProblem)
|
cb->sendMessage("vcmieagles");
|
||||||
cb->sendMessage("Nullkiller AI currently requires the ability to cheat in order to function correctly! Please enable!");
|
|
||||||
announcedCheatingProblem = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
retrieveVisitableObjs();
|
retrieveVisitableObjs();
|
||||||
|
@ -96,7 +96,7 @@ public:
|
|||||||
std::unique_ptr<boost::thread> makingTurn;
|
std::unique_ptr<boost::thread> makingTurn;
|
||||||
private:
|
private:
|
||||||
boost::mutex turnInterruptionMutex;
|
boost::mutex turnInterruptionMutex;
|
||||||
bool announcedCheatingProblem;
|
|
||||||
public:
|
public:
|
||||||
ObjectInstanceID selectedObject;
|
ObjectInstanceID selectedObject;
|
||||||
|
|
||||||
|
@ -381,7 +381,7 @@ void ObjectClusterizer::clusterizeObject(
|
|||||||
logAi->trace("Check object %s%s.", obj->getObjectName(), obj->visitablePos().toString());
|
logAi->trace("Check object %s%s.", obj->getObjectName(), obj->visitablePos().toString());
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if(ai->settings->isObjectGraphAllowed())
|
if(ai->isObjectGraphAllowed())
|
||||||
{
|
{
|
||||||
ai->pathfinder->calculateQuickPathsWithBlocker(pathCache, heroes, obj->visitablePos());
|
ai->pathfinder->calculateQuickPathsWithBlocker(pathCache, heroes, obj->visitablePos());
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,11 @@ bool CaptureObjectsBehavior::operator==(const CaptureObjectsBehavior & other) co
|
|||||||
&& vectorEquals(objectSubTypes, other.objectSubTypes);
|
&& vectorEquals(objectSubTypes, other.objectSubTypes);
|
||||||
}
|
}
|
||||||
|
|
||||||
Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath> & paths, const Nullkiller * nullkiller, const CGObjectInstance * objToVisit)
|
Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(
|
||||||
|
const std::vector<AIPath> & paths,
|
||||||
|
const Nullkiller * nullkiller,
|
||||||
|
const CGObjectInstance * objToVisit,
|
||||||
|
bool force)
|
||||||
{
|
{
|
||||||
Goals::TGoalVec tasks;
|
Goals::TGoalVec tasks;
|
||||||
|
|
||||||
@ -72,7 +76,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(objToVisit && !shouldVisit(nullkiller, path.targetHero, objToVisit))
|
if(objToVisit && !force && !shouldVisit(nullkiller, path.targetHero, objToVisit))
|
||||||
{
|
{
|
||||||
#if NKAI_TRACE_LEVEL >= 2
|
#if NKAI_TRACE_LEVEL >= 2
|
||||||
logAi->trace("Ignore path. Hero %s should not visit obj %s", path.targetHero->getNameTranslated(), objToVisit->getObjectName());
|
logAi->trace("Ignore path. Hero %s should not visit obj %s", path.targetHero->getNameTranslated(), objToVisit->getObjectName());
|
||||||
@ -200,12 +204,12 @@ void CaptureObjectsBehavior::decomposeObjects(
|
|||||||
logAi->trace("Checking object %s, %s", objToVisit->getObjectName(), objToVisit->visitablePos().toString());
|
logAi->trace("Checking object %s, %s", objToVisit->getObjectName(), objToVisit->visitablePos().toString());
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
nullkiller->pathfinder->calculatePathInfo(paths, objToVisit->visitablePos(), nullkiller->settings->isObjectGraphAllowed());
|
nullkiller->pathfinder->calculatePathInfo(paths, objToVisit->visitablePos(), nullkiller->isObjectGraphAllowed());
|
||||||
|
|
||||||
#if NKAI_TRACE_LEVEL >= 1
|
#if NKAI_TRACE_LEVEL >= 1
|
||||||
logAi->trace("Found %d paths", paths.size());
|
logAi->trace("Found %d paths", paths.size());
|
||||||
#endif
|
#endif
|
||||||
vstd::concatenate(tasksLocal, getVisitGoals(paths, nullkiller, objToVisit));
|
vstd::concatenate(tasksLocal, getVisitGoals(paths, nullkiller, objToVisit, specificObjects));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lock(sync);
|
std::lock_guard<std::mutex> lock(sync);
|
||||||
|
@ -68,7 +68,11 @@ namespace Goals
|
|||||||
|
|
||||||
bool operator==(const CaptureObjectsBehavior & other) const override;
|
bool operator==(const CaptureObjectsBehavior & other) const override;
|
||||||
|
|
||||||
static Goals::TGoalVec getVisitGoals(const std::vector<AIPath> & paths, const Nullkiller * nullkiller, const CGObjectInstance * objToVisit = nullptr);
|
static Goals::TGoalVec getVisitGoals(
|
||||||
|
const std::vector<AIPath> & paths,
|
||||||
|
const Nullkiller * nullkiller,
|
||||||
|
const CGObjectInstance * objToVisit = nullptr,
|
||||||
|
bool force = false);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool objectMatchesFilter(const CGObjectInstance * obj) const;
|
bool objectMatchesFilter(const CGObjectInstance * obj) const;
|
||||||
|
@ -42,7 +42,7 @@ Goals::TGoalVec ClusterBehavior::decompose(const Nullkiller * ai) const
|
|||||||
Goals::TGoalVec ClusterBehavior::decomposeCluster(const Nullkiller * ai, std::shared_ptr<ObjectCluster> cluster) const
|
Goals::TGoalVec ClusterBehavior::decomposeCluster(const Nullkiller * ai, std::shared_ptr<ObjectCluster> cluster) const
|
||||||
{
|
{
|
||||||
auto center = cluster->calculateCenter(ai->cb.get());
|
auto center = cluster->calculateCenter(ai->cb.get());
|
||||||
auto paths = ai->pathfinder->getPathInfo(center->visitablePos(), ai->settings->isObjectGraphAllowed());
|
auto paths = ai->pathfinder->getPathInfo(center->visitablePos(), ai->isObjectGraphAllowed());
|
||||||
|
|
||||||
auto blockerPos = cluster->blocker->visitablePos();
|
auto blockerPos = cluster->blocker->visitablePos();
|
||||||
std::vector<AIPath> blockerPaths;
|
std::vector<AIPath> blockerPaths;
|
||||||
|
334
AI/Nullkiller/Behaviors/ExplorationBehavior.cpp
Normal file
334
AI/Nullkiller/Behaviors/ExplorationBehavior.cpp
Normal file
@ -0,0 +1,334 @@
|
|||||||
|
/*
|
||||||
|
* ExplorationBehavior.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 "ExplorationBehavior.h"
|
||||||
|
#include "../AIGateway.h"
|
||||||
|
#include "../AIUtility.h"
|
||||||
|
#include "../Goals/Invalid.h"
|
||||||
|
#include "../Goals/Composition.h"
|
||||||
|
#include "../Goals/ExecuteHeroChain.h"
|
||||||
|
#include "../Markers/ExplorationPoint.h"
|
||||||
|
#include "CaptureObjectsBehavior.h"
|
||||||
|
#include "../Goals/CaptureObject.h"
|
||||||
|
#include "../../../lib/CPlayerState.h"
|
||||||
|
|
||||||
|
namespace NKAI
|
||||||
|
{
|
||||||
|
|
||||||
|
using namespace Goals;
|
||||||
|
|
||||||
|
struct ExplorationHelper
|
||||||
|
{
|
||||||
|
const CGHeroInstance * hero;
|
||||||
|
int sightRadius;
|
||||||
|
float bestValue;
|
||||||
|
TSubgoal bestGoal;
|
||||||
|
int3 bestTile;
|
||||||
|
int bestTilesDiscovered;
|
||||||
|
const Nullkiller * ai;
|
||||||
|
CCallback * cbp;
|
||||||
|
const TeamState * ts;
|
||||||
|
int3 ourPos;
|
||||||
|
bool allowDeadEndCancellation;
|
||||||
|
|
||||||
|
ExplorationHelper(const CGHeroInstance * hero, const Nullkiller * ai)
|
||||||
|
:ai(ai), cbp(ai->cb.get()), hero(hero)
|
||||||
|
{
|
||||||
|
ts = cbp->getPlayerTeam(ai->playerID);
|
||||||
|
sightRadius = hero->getSightRadius();
|
||||||
|
bestGoal = sptr(Goals::Invalid());
|
||||||
|
bestValue = 0;
|
||||||
|
bestTilesDiscovered = 0;
|
||||||
|
ourPos = hero->visitablePos();
|
||||||
|
allowDeadEndCancellation = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
TSubgoal makeComposition() const
|
||||||
|
{
|
||||||
|
Composition c;
|
||||||
|
c.addNext(ExplorationPoint(bestTile, bestTilesDiscovered));
|
||||||
|
c.addNext(bestGoal);
|
||||||
|
return sptr(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
void scanSector(int scanRadius)
|
||||||
|
{
|
||||||
|
int3 tile = int3(0, 0, ourPos.z);
|
||||||
|
|
||||||
|
const auto & slice = (*(ts->fogOfWarMap))[ourPos.z];
|
||||||
|
|
||||||
|
for(tile.x = ourPos.x - scanRadius; tile.x <= ourPos.x + scanRadius; tile.x++)
|
||||||
|
{
|
||||||
|
for(tile.y = ourPos.y - scanRadius; tile.y <= ourPos.y + scanRadius; tile.y++)
|
||||||
|
{
|
||||||
|
|
||||||
|
if(cbp->isInTheMap(tile) && slice[tile.x][tile.y])
|
||||||
|
{
|
||||||
|
scanTile(tile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void scanMap()
|
||||||
|
{
|
||||||
|
int3 mapSize = cbp->getMapSize();
|
||||||
|
int perimeter = 2 * sightRadius * (mapSize.x + mapSize.y);
|
||||||
|
|
||||||
|
std::vector<int3> from;
|
||||||
|
std::vector<int3> to;
|
||||||
|
|
||||||
|
from.reserve(perimeter);
|
||||||
|
to.reserve(perimeter);
|
||||||
|
|
||||||
|
foreach_tile_pos([&](const int3 & pos)
|
||||||
|
{
|
||||||
|
if((*(ts->fogOfWarMap))[pos.z][pos.x][pos.y])
|
||||||
|
{
|
||||||
|
bool hasInvisibleNeighbor = false;
|
||||||
|
|
||||||
|
foreach_neighbour(cbp, pos, [&](CCallback * cbp, int3 neighbour)
|
||||||
|
{
|
||||||
|
if(!(*(ts->fogOfWarMap))[neighbour.z][neighbour.x][neighbour.y])
|
||||||
|
{
|
||||||
|
hasInvisibleNeighbor = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if(hasInvisibleNeighbor)
|
||||||
|
from.push_back(pos);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logAi->debug("Exploration scan visible area perimeter for hero %s", hero->getNameTranslated());
|
||||||
|
|
||||||
|
for(const int3 & tile : from)
|
||||||
|
{
|
||||||
|
scanTile(tile);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!bestGoal->invalid())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
allowDeadEndCancellation = false;
|
||||||
|
|
||||||
|
for(int i = 0; i < sightRadius; i++)
|
||||||
|
{
|
||||||
|
getVisibleNeighbours(from, to);
|
||||||
|
vstd::concatenate(from, to);
|
||||||
|
vstd::removeDuplicates(from);
|
||||||
|
}
|
||||||
|
|
||||||
|
logAi->debug("Exploration scan all possible tiles for hero %s", hero->getNameTranslated());
|
||||||
|
|
||||||
|
for(const int3 & tile : from)
|
||||||
|
{
|
||||||
|
scanTile(tile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void scanTile(const int3 & tile)
|
||||||
|
{
|
||||||
|
if(tile == ourPos
|
||||||
|
|| !ai->pathfinder->isTileAccessible(hero, tile)) //shouldn't happen, but it does
|
||||||
|
return;
|
||||||
|
|
||||||
|
int tilesDiscovered = howManyTilesWillBeDiscovered(tile);
|
||||||
|
if(!tilesDiscovered)
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto paths = ai->pathfinder->getPathInfo(tile);
|
||||||
|
auto waysToVisit = CaptureObjectsBehavior::getVisitGoals(paths, ai, ai->cb->getTopObj(tile));
|
||||||
|
|
||||||
|
for(int i = 0; i != paths.size(); i++)
|
||||||
|
{
|
||||||
|
auto & path = paths[i];
|
||||||
|
auto goal = waysToVisit[i];
|
||||||
|
|
||||||
|
if(path.exchangeCount > 1 || path.targetHero != hero || path.movementCost() <= 0.0 || goal->invalid())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
float ourValue = (float)tilesDiscovered * tilesDiscovered / path.movementCost();
|
||||||
|
|
||||||
|
if(ourValue > bestValue) //avoid costly checks of tiles that don't reveal much
|
||||||
|
{
|
||||||
|
auto obj = cb->getTopObj(tile);
|
||||||
|
|
||||||
|
// picking up resources does not yield any exploration at all.
|
||||||
|
// if it blocks the way to some explorable tile AIPathfinder will take care of it
|
||||||
|
if(obj && obj->isBlockedVisitable())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isSafeToVisit(hero, path.heroArmy, path.getTotalDanger()))
|
||||||
|
{
|
||||||
|
bestGoal = goal;
|
||||||
|
bestValue = ourValue;
|
||||||
|
bestTile = tile;
|
||||||
|
bestTilesDiscovered = tilesDiscovered;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void getVisibleNeighbours(const std::vector<int3> & tiles, std::vector<int3> & out) const
|
||||||
|
{
|
||||||
|
for(const int3 & tile : tiles)
|
||||||
|
{
|
||||||
|
foreach_neighbour(cbp, tile, [&](CCallback * cbp, int3 neighbour)
|
||||||
|
{
|
||||||
|
if((*(ts->fogOfWarMap))[neighbour.z][neighbour.x][neighbour.y])
|
||||||
|
{
|
||||||
|
out.push_back(neighbour);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int howManyTilesWillBeDiscovered(const int3 & pos) const
|
||||||
|
{
|
||||||
|
int ret = 0;
|
||||||
|
int3 npos = int3(0, 0, pos.z);
|
||||||
|
|
||||||
|
const auto & slice = (*(ts->fogOfWarMap))[pos.z];
|
||||||
|
|
||||||
|
for(npos.x = pos.x - sightRadius; npos.x <= pos.x + sightRadius; npos.x++)
|
||||||
|
{
|
||||||
|
for(npos.y = pos.y - sightRadius; npos.y <= pos.y + sightRadius; npos.y++)
|
||||||
|
{
|
||||||
|
if(cbp->isInTheMap(npos)
|
||||||
|
&& pos.dist2d(npos) - 0.5 < sightRadius
|
||||||
|
&& !slice[npos.x][npos.y])
|
||||||
|
{
|
||||||
|
if(allowDeadEndCancellation
|
||||||
|
&& !hasReachableNeighbor(npos))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasReachableNeighbor(const int3 & pos) const
|
||||||
|
{
|
||||||
|
for(const int3 & dir : int3::getDirs())
|
||||||
|
{
|
||||||
|
int3 tile = pos + dir;
|
||||||
|
if(cbp->isInTheMap(tile))
|
||||||
|
{
|
||||||
|
auto isAccessible = ai->pathfinder->isTileAccessible(hero, tile);
|
||||||
|
|
||||||
|
if(isAccessible)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string ExplorationBehavior::toString() const
|
||||||
|
{
|
||||||
|
return "Explore";
|
||||||
|
}
|
||||||
|
|
||||||
|
Goals::TGoalVec ExplorationBehavior::decompose(const Nullkiller * ai) const
|
||||||
|
{
|
||||||
|
Goals::TGoalVec tasks;
|
||||||
|
|
||||||
|
for(auto obj : ai->memory->visitableObjs)
|
||||||
|
{
|
||||||
|
if(!vstd::contains(ai->memory->alreadyVisited, obj))
|
||||||
|
{
|
||||||
|
switch(obj->ID.num)
|
||||||
|
{
|
||||||
|
case Obj::REDWOOD_OBSERVATORY:
|
||||||
|
case Obj::PILLAR_OF_FIRE:
|
||||||
|
tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 200)).addNext(CaptureObject(obj))));
|
||||||
|
break;
|
||||||
|
case Obj::MONOLITH_ONE_WAY_ENTRANCE:
|
||||||
|
case Obj::MONOLITH_TWO_WAY:
|
||||||
|
case Obj::SUBTERRANEAN_GATE:
|
||||||
|
auto tObj = dynamic_cast<const CGTeleport *>(obj);
|
||||||
|
if(TeleportChannel::IMPASSABLE != ai->memory->knownTeleportChannels[tObj->channel]->passability)
|
||||||
|
{
|
||||||
|
tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj))));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
switch(obj->ID.num)
|
||||||
|
{
|
||||||
|
case Obj::MONOLITH_TWO_WAY:
|
||||||
|
case Obj::SUBTERRANEAN_GATE:
|
||||||
|
auto tObj = dynamic_cast<const CGTeleport *>(obj);
|
||||||
|
if(TeleportChannel::IMPASSABLE == ai->memory->knownTeleportChannels[tObj->channel]->passability)
|
||||||
|
break;
|
||||||
|
for(auto exit : ai->memory->knownTeleportChannels[tObj->channel]->exits)
|
||||||
|
{
|
||||||
|
if(!cb->getObj(exit))
|
||||||
|
{
|
||||||
|
// Always attempt to visit two-way teleports if one of channel exits is not visible
|
||||||
|
tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj))));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto heroes = ai->cb->getHeroesInfo();
|
||||||
|
|
||||||
|
for(const CGHeroInstance * hero : heroes)
|
||||||
|
{
|
||||||
|
ExplorationHelper scanResult(hero, ai);
|
||||||
|
|
||||||
|
scanResult.scanSector(1);
|
||||||
|
|
||||||
|
if(!scanResult.bestGoal->invalid())
|
||||||
|
{
|
||||||
|
tasks.push_back(scanResult.makeComposition());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
scanResult.scanSector(15);
|
||||||
|
|
||||||
|
if(!scanResult.bestGoal->invalid())
|
||||||
|
{
|
||||||
|
tasks.push_back(scanResult.makeComposition());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(ai->getScanDepth() == ScanDepth::ALL_FULL)
|
||||||
|
{
|
||||||
|
scanResult.scanMap();
|
||||||
|
|
||||||
|
if(!scanResult.bestGoal->invalid())
|
||||||
|
{
|
||||||
|
tasks.push_back(scanResult.makeComposition());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
38
AI/Nullkiller/Behaviors/ExplorationBehavior.h
Normal file
38
AI/Nullkiller/Behaviors/ExplorationBehavior.h
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* ExplorationBehavior.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 "lib/VCMI_Lib.h"
|
||||||
|
#include "../Goals/CGoal.h"
|
||||||
|
#include "../AIUtility.h"
|
||||||
|
|
||||||
|
namespace NKAI
|
||||||
|
{
|
||||||
|
namespace Goals
|
||||||
|
{
|
||||||
|
class ExplorationBehavior : public CGoal<ExplorationBehavior>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ExplorationBehavior()
|
||||||
|
:CGoal(EXPLORATION_BEHAVIOR)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
TGoalVec decompose(const Nullkiller * ai) const override;
|
||||||
|
std::string toString() const override;
|
||||||
|
|
||||||
|
bool operator==(const ExplorationBehavior & other) const override
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -69,7 +69,7 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con
|
|||||||
logAi->trace("Checking ways to gaher army for hero %s, %s", hero->getObjectName(), pos.toString());
|
logAi->trace("Checking ways to gaher army for hero %s, %s", hero->getObjectName(), pos.toString());
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
auto paths = ai->pathfinder->getPathInfo(pos, ai->settings->isObjectGraphAllowed());
|
auto paths = ai->pathfinder->getPathInfo(pos, ai->isObjectGraphAllowed());
|
||||||
|
|
||||||
#if NKAI_TRACE_LEVEL >= 1
|
#if NKAI_TRACE_LEVEL >= 1
|
||||||
logAi->trace("Gather army found %d paths", paths.size());
|
logAi->trace("Gather army found %d paths", paths.size());
|
||||||
@ -231,7 +231,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
|
|||||||
logAi->trace("Checking ways to upgrade army in town %s, %s", upgrader->getObjectName(), pos.toString());
|
logAi->trace("Checking ways to upgrade army in town %s, %s", upgrader->getObjectName(), pos.toString());
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
auto paths = ai->pathfinder->getPathInfo(pos, ai->settings->isObjectGraphAllowed());
|
auto paths = ai->pathfinder->getPathInfo(pos, ai->isObjectGraphAllowed());
|
||||||
auto goals = CaptureObjectsBehavior::getVisitGoals(paths, ai);
|
auto goals = CaptureObjectsBehavior::getVisitGoals(paths, ai);
|
||||||
|
|
||||||
std::vector<std::shared_ptr<ExecuteHeroChain>> waysToVisitObj;
|
std::vector<std::shared_ptr<ExecuteHeroChain>> waysToVisitObj;
|
||||||
|
@ -15,6 +15,8 @@ set(Nullkiller_SRCS
|
|||||||
Pathfinding/Rules/AIMovementToDestinationRule.cpp
|
Pathfinding/Rules/AIMovementToDestinationRule.cpp
|
||||||
Pathfinding/Rules/AIPreviousNodeRule.cpp
|
Pathfinding/Rules/AIPreviousNodeRule.cpp
|
||||||
Pathfinding/ObjectGraph.cpp
|
Pathfinding/ObjectGraph.cpp
|
||||||
|
Pathfinding/GraphPaths.cpp
|
||||||
|
Pathfinding/ObjectGraphCalculator.cpp
|
||||||
AIUtility.cpp
|
AIUtility.cpp
|
||||||
Analyzers/ArmyManager.cpp
|
Analyzers/ArmyManager.cpp
|
||||||
Analyzers/HeroManager.cpp
|
Analyzers/HeroManager.cpp
|
||||||
@ -42,6 +44,7 @@ set(Nullkiller_SRCS
|
|||||||
Markers/HeroExchange.cpp
|
Markers/HeroExchange.cpp
|
||||||
Markers/UnlockCluster.cpp
|
Markers/UnlockCluster.cpp
|
||||||
Markers/DefendTown.cpp
|
Markers/DefendTown.cpp
|
||||||
|
Markers/ExplorationPoint.cpp
|
||||||
Engine/Nullkiller.cpp
|
Engine/Nullkiller.cpp
|
||||||
Engine/DeepDecomposer.cpp
|
Engine/DeepDecomposer.cpp
|
||||||
Engine/PriorityEvaluator.cpp
|
Engine/PriorityEvaluator.cpp
|
||||||
@ -57,6 +60,7 @@ set(Nullkiller_SRCS
|
|||||||
Behaviors/GatherArmyBehavior.cpp
|
Behaviors/GatherArmyBehavior.cpp
|
||||||
Behaviors/ClusterBehavior.cpp
|
Behaviors/ClusterBehavior.cpp
|
||||||
Behaviors/StayAtTownBehavior.cpp
|
Behaviors/StayAtTownBehavior.cpp
|
||||||
|
Behaviors/ExplorationBehavior.cpp
|
||||||
Helpers/ArmyFormation.cpp
|
Helpers/ArmyFormation.cpp
|
||||||
AIGateway.cpp
|
AIGateway.cpp
|
||||||
)
|
)
|
||||||
@ -80,6 +84,8 @@ set(Nullkiller_HEADERS
|
|||||||
Pathfinding/Rules/AIMovementToDestinationRule.h
|
Pathfinding/Rules/AIMovementToDestinationRule.h
|
||||||
Pathfinding/Rules/AIPreviousNodeRule.h
|
Pathfinding/Rules/AIPreviousNodeRule.h
|
||||||
Pathfinding/ObjectGraph.h
|
Pathfinding/ObjectGraph.h
|
||||||
|
Pathfinding/GraphPaths.h
|
||||||
|
Pathfinding/ObjectGraphCalculator.h
|
||||||
AIUtility.h
|
AIUtility.h
|
||||||
pforeach.h
|
pforeach.h
|
||||||
Analyzers/ArmyManager.h
|
Analyzers/ArmyManager.h
|
||||||
@ -111,6 +117,7 @@ set(Nullkiller_HEADERS
|
|||||||
Markers/HeroExchange.h
|
Markers/HeroExchange.h
|
||||||
Markers/UnlockCluster.h
|
Markers/UnlockCluster.h
|
||||||
Markers/DefendTown.h
|
Markers/DefendTown.h
|
||||||
|
Markers/ExplorationPoint.h
|
||||||
Engine/Nullkiller.h
|
Engine/Nullkiller.h
|
||||||
Engine/DeepDecomposer.h
|
Engine/DeepDecomposer.h
|
||||||
Engine/PriorityEvaluator.h
|
Engine/PriorityEvaluator.h
|
||||||
@ -126,6 +133,7 @@ set(Nullkiller_HEADERS
|
|||||||
Behaviors/GatherArmyBehavior.h
|
Behaviors/GatherArmyBehavior.h
|
||||||
Behaviors/ClusterBehavior.h
|
Behaviors/ClusterBehavior.h
|
||||||
Behaviors/StayAtTownBehavior.h
|
Behaviors/StayAtTownBehavior.h
|
||||||
|
Behaviors/ExplorationBehavior.h
|
||||||
Helpers/ArmyFormation.h
|
Helpers/ArmyFormation.h
|
||||||
AIGateway.h
|
AIGateway.h
|
||||||
)
|
)
|
||||||
|
@ -19,8 +19,11 @@
|
|||||||
#include "../Behaviors/GatherArmyBehavior.h"
|
#include "../Behaviors/GatherArmyBehavior.h"
|
||||||
#include "../Behaviors/ClusterBehavior.h"
|
#include "../Behaviors/ClusterBehavior.h"
|
||||||
#include "../Behaviors/StayAtTownBehavior.h"
|
#include "../Behaviors/StayAtTownBehavior.h"
|
||||||
|
#include "../Behaviors/ExplorationBehavior.h"
|
||||||
#include "../Goals/Invalid.h"
|
#include "../Goals/Invalid.h"
|
||||||
#include "../Goals/Composition.h"
|
#include "../Goals/Composition.h"
|
||||||
|
#include "../../../lib/CPlayerState.h"
|
||||||
|
#include "../../lib/StartInfo.h"
|
||||||
|
|
||||||
namespace NKAI
|
namespace NKAI
|
||||||
{
|
{
|
||||||
@ -35,13 +38,45 @@ Nullkiller::Nullkiller()
|
|||||||
{
|
{
|
||||||
memory = std::make_unique<AIMemory>();
|
memory = std::make_unique<AIMemory>();
|
||||||
settings = std::make_unique<Settings>();
|
settings = std::make_unique<Settings>();
|
||||||
|
|
||||||
|
useObjectGraph = settings->isObjectGraphAllowed();
|
||||||
|
openMap = settings->isOpenMap() || useObjectGraph;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool canUseOpenMap(std::shared_ptr<CCallback> cb, PlayerColor playerID)
|
||||||
|
{
|
||||||
|
if(!cb->getStartInfo()->extraOptionsInfo.cheatsAllowed)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TeamState * team = cb->getPlayerTeam(playerID);
|
||||||
|
|
||||||
|
auto hasHumanInTeam = vstd::contains_if(team->players, [cb](PlayerColor teamMateID) -> bool
|
||||||
|
{
|
||||||
|
return cb->getPlayerState(teamMateID)->isHuman();
|
||||||
|
});
|
||||||
|
|
||||||
|
if(hasHumanInTeam)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cb->getStartInfo()->difficulty >= 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Nullkiller::init(std::shared_ptr<CCallback> cb, AIGateway * gateway)
|
void Nullkiller::init(std::shared_ptr<CCallback> cb, AIGateway * gateway)
|
||||||
{
|
{
|
||||||
this->cb = cb;
|
this->cb = cb;
|
||||||
this->gateway = gateway;
|
this->gateway = gateway;
|
||||||
this->playerID = gateway->playerID;
|
|
||||||
|
playerID = gateway->playerID;
|
||||||
|
|
||||||
|
if(openMap && !canUseOpenMap(cb, playerID))
|
||||||
|
{
|
||||||
|
useObjectGraph = false;
|
||||||
|
openMap = false;
|
||||||
|
}
|
||||||
|
|
||||||
baseGraph.reset();
|
baseGraph.reset();
|
||||||
|
|
||||||
@ -190,7 +225,7 @@ void Nullkiller::resetAiState()
|
|||||||
useHeroChain = true;
|
useHeroChain = true;
|
||||||
objectClusterizer->reset();
|
objectClusterizer->reset();
|
||||||
|
|
||||||
if(!baseGraph && settings->isObjectGraphAllowed())
|
if(!baseGraph && isObjectGraphAllowed())
|
||||||
{
|
{
|
||||||
baseGraph = std::make_unique<ObjectGraph>();
|
baseGraph = std::make_unique<ObjectGraph>();
|
||||||
baseGraph->updateGraph(this);
|
baseGraph->updateGraph(this);
|
||||||
@ -237,12 +272,12 @@ void Nullkiller::updateAiState(int pass, bool fast)
|
|||||||
cfg.useHeroChain = useHeroChain;
|
cfg.useHeroChain = useHeroChain;
|
||||||
cfg.allowBypassObjects = true;
|
cfg.allowBypassObjects = true;
|
||||||
|
|
||||||
if(scanDepth == ScanDepth::SMALL || settings->isObjectGraphAllowed())
|
if(scanDepth == ScanDepth::SMALL || isObjectGraphAllowed())
|
||||||
{
|
{
|
||||||
cfg.mainTurnDistanceLimit = settings->getMainHeroTurnDistanceLimit();
|
cfg.mainTurnDistanceLimit = settings->getMainHeroTurnDistanceLimit();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(scanDepth != ScanDepth::ALL_FULL || settings->isObjectGraphAllowed())
|
if(scanDepth != ScanDepth::ALL_FULL || isObjectGraphAllowed())
|
||||||
{
|
{
|
||||||
cfg.scoutTurnDistanceLimit =settings->getScoutHeroTurnDistanceLimit();
|
cfg.scoutTurnDistanceLimit =settings->getScoutHeroTurnDistanceLimit();
|
||||||
}
|
}
|
||||||
@ -251,7 +286,7 @@ void Nullkiller::updateAiState(int pass, bool fast)
|
|||||||
|
|
||||||
pathfinder->updatePaths(activeHeroes, cfg);
|
pathfinder->updatePaths(activeHeroes, cfg);
|
||||||
|
|
||||||
if(settings->isObjectGraphAllowed())
|
if(isObjectGraphAllowed())
|
||||||
{
|
{
|
||||||
pathfinder->updateGraphs(
|
pathfinder->updateGraphs(
|
||||||
activeHeroes,
|
activeHeroes,
|
||||||
@ -354,6 +389,9 @@ void Nullkiller::makeTurn()
|
|||||||
decompose(bestTasks, sptr(GatherArmyBehavior()), MAX_DEPTH);
|
decompose(bestTasks, sptr(GatherArmyBehavior()), MAX_DEPTH);
|
||||||
decompose(bestTasks, sptr(StayAtTownBehavior()), MAX_DEPTH);
|
decompose(bestTasks, sptr(StayAtTownBehavior()), MAX_DEPTH);
|
||||||
|
|
||||||
|
if(!isOpenMap())
|
||||||
|
decompose(bestTasks, sptr(ExplorationBehavior()), MAX_DEPTH);
|
||||||
|
|
||||||
if(cb->getDate(Date::DAY) == 1 || heroManager->getHeroRoles().empty())
|
if(cb->getDate(Date::DAY) == 1 || heroManager->getHeroRoles().empty())
|
||||||
{
|
{
|
||||||
decompose(bestTasks, sptr(StartupBehavior()), 1);
|
decompose(bestTasks, sptr(StartupBehavior()), 1);
|
||||||
|
@ -76,6 +76,8 @@ private:
|
|||||||
TResources lockedResources;
|
TResources lockedResources;
|
||||||
bool useHeroChain;
|
bool useHeroChain;
|
||||||
AIGateway * gateway;
|
AIGateway * gateway;
|
||||||
|
bool openMap;
|
||||||
|
bool useObjectGraph;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
static std::unique_ptr<ObjectGraph> baseGraph;
|
static std::unique_ptr<ObjectGraph> baseGraph;
|
||||||
@ -116,6 +118,8 @@ public:
|
|||||||
void lockResources(const TResources & res);
|
void lockResources(const TResources & res);
|
||||||
const TResources & getLockedResources() const { return lockedResources; }
|
const TResources & getLockedResources() const { return lockedResources; }
|
||||||
ScanDepth getScanDepth() const { return scanDepth; }
|
ScanDepth getScanDepth() const { return scanDepth; }
|
||||||
|
bool isOpenMap() const { return openMap; }
|
||||||
|
bool isObjectGraphAllowed() const { return useObjectGraph; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void resetAiState();
|
void resetAiState();
|
||||||
|
@ -735,6 +735,20 @@ public:
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class ExplorePointEvaluator : public IEvaluationContextBuilder
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
|
||||||
|
{
|
||||||
|
if(task->goalType != Goals::EXPLORATION_POINT)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int tilesDiscovered = task->value;
|
||||||
|
|
||||||
|
evaluationContext.addNonCriticalStrategicalValue(0.03f * tilesDiscovered);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
class StayAtTownManaRecoveryEvaluator : public IEvaluationContextBuilder
|
class StayAtTownManaRecoveryEvaluator : public IEvaluationContextBuilder
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@ -1056,6 +1070,7 @@ PriorityEvaluator::PriorityEvaluator(const Nullkiller * ai)
|
|||||||
evaluationContextBuilders.push_back(std::make_shared<ExchangeSwapTownHeroesContextBuilder>());
|
evaluationContextBuilders.push_back(std::make_shared<ExchangeSwapTownHeroesContextBuilder>());
|
||||||
evaluationContextBuilders.push_back(std::make_shared<DismissHeroContextBuilder>(ai));
|
evaluationContextBuilders.push_back(std::make_shared<DismissHeroContextBuilder>(ai));
|
||||||
evaluationContextBuilders.push_back(std::make_shared<StayAtTownManaRecoveryEvaluator>());
|
evaluationContextBuilders.push_back(std::make_shared<StayAtTownManaRecoveryEvaluator>());
|
||||||
|
evaluationContextBuilders.push_back(std::make_shared<ExplorePointEvaluator>());
|
||||||
}
|
}
|
||||||
|
|
||||||
EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal) const
|
EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal) const
|
||||||
|
@ -28,8 +28,9 @@ namespace NKAI
|
|||||||
scoutHeroTurnDistanceLimit(5),
|
scoutHeroTurnDistanceLimit(5),
|
||||||
maxGoldPressure(0.3f),
|
maxGoldPressure(0.3f),
|
||||||
maxpass(10),
|
maxpass(10),
|
||||||
allowObjectGraph(false),
|
allowObjectGraph(true),
|
||||||
useTroopsFromGarrisons(false)
|
useTroopsFromGarrisons(false),
|
||||||
|
openMap(true)
|
||||||
{
|
{
|
||||||
JsonNode node = JsonUtils::assembleFromFiles("config/ai/nkai/nkai-settings");
|
JsonNode node = JsonUtils::assembleFromFiles("config/ai/nkai/nkai-settings");
|
||||||
|
|
||||||
@ -63,6 +64,11 @@ namespace NKAI
|
|||||||
allowObjectGraph = node.Struct()["allowObjectGraph"].Bool();
|
allowObjectGraph = node.Struct()["allowObjectGraph"].Bool();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!node.Struct()["openMap"].isNull())
|
||||||
|
{
|
||||||
|
openMap = node.Struct()["openMap"].Bool();
|
||||||
|
}
|
||||||
|
|
||||||
if(!node.Struct()["useTroopsFromGarrisons"].isNull())
|
if(!node.Struct()["useTroopsFromGarrisons"].isNull())
|
||||||
{
|
{
|
||||||
useTroopsFromGarrisons = node.Struct()["useTroopsFromGarrisons"].Bool();
|
useTroopsFromGarrisons = node.Struct()["useTroopsFromGarrisons"].Bool();
|
||||||
|
@ -28,6 +28,7 @@ namespace NKAI
|
|||||||
float maxGoldPressure;
|
float maxGoldPressure;
|
||||||
bool allowObjectGraph;
|
bool allowObjectGraph;
|
||||||
bool useTroopsFromGarrisons;
|
bool useTroopsFromGarrisons;
|
||||||
|
bool openMap;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
Settings();
|
Settings();
|
||||||
@ -39,5 +40,6 @@ namespace NKAI
|
|||||||
int getScoutHeroTurnDistanceLimit() const { return scoutHeroTurnDistanceLimit; }
|
int getScoutHeroTurnDistanceLimit() const { return scoutHeroTurnDistanceLimit; }
|
||||||
bool isObjectGraphAllowed() const { return allowObjectGraph; }
|
bool isObjectGraphAllowed() const { return allowObjectGraph; }
|
||||||
bool isGarrisonTroopsUsageAllowed() const { return useTroopsFromGarrisons; }
|
bool isGarrisonTroopsUsageAllowed() const { return useTroopsFromGarrisons; }
|
||||||
|
bool isOpenMap() const { return openMap; }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -73,7 +73,9 @@ namespace Goals
|
|||||||
CAPTURE_OBJECT,
|
CAPTURE_OBJECT,
|
||||||
SAVE_RESOURCES,
|
SAVE_RESOURCES,
|
||||||
STAY_AT_TOWN_BEHAVIOR,
|
STAY_AT_TOWN_BEHAVIOR,
|
||||||
STAY_AT_TOWN
|
STAY_AT_TOWN,
|
||||||
|
EXPLORATION_BEHAVIOR,
|
||||||
|
EXPLORATION_POINT
|
||||||
};
|
};
|
||||||
|
|
||||||
class DLL_EXPORT TSubgoal : public std::shared_ptr<AbstractGoal>
|
class DLL_EXPORT TSubgoal : public std::shared_ptr<AbstractGoal>
|
||||||
|
@ -148,7 +148,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(i > 0 && ai->nullkiller->settings->isObjectGraphAllowed())
|
else if(i > 0 && ai->nullkiller->isObjectGraphAllowed())
|
||||||
{
|
{
|
||||||
auto chainMask = i < chainPath.nodes.size() - 1 ? chainPath.nodes[i + 1].chainMask : node->chainMask;
|
auto chainMask = i < chainPath.nodes.size() - 1 ? chainPath.nodes[i + 1].chainMask : node->chainMask;
|
||||||
|
|
||||||
|
32
AI/Nullkiller/Markers/ExplorationPoint.cpp
Normal file
32
AI/Nullkiller/Markers/ExplorationPoint.cpp
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* HeroExchange.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 "ExplorationPoint.h"
|
||||||
|
#include "../AIGateway.h"
|
||||||
|
#include "../Engine/Nullkiller.h"
|
||||||
|
#include "../AIUtility.h"
|
||||||
|
#include "../Analyzers/ArmyManager.h"
|
||||||
|
|
||||||
|
namespace NKAI
|
||||||
|
{
|
||||||
|
|
||||||
|
using namespace Goals;
|
||||||
|
|
||||||
|
bool ExplorationPoint::operator==(const ExplorationPoint & other) const
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ExplorationPoint::toString() const
|
||||||
|
{
|
||||||
|
return "Explore " +tile.toString() + " for " + std::to_string(value) + " tiles";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
35
AI/Nullkiller/Markers/ExplorationPoint.h
Normal file
35
AI/Nullkiller/Markers/ExplorationPoint.h
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* ExplorationPoint.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 "../Goals/CGoal.h"
|
||||||
|
#include "../Pathfinding/AINodeStorage.h"
|
||||||
|
|
||||||
|
namespace NKAI
|
||||||
|
{
|
||||||
|
namespace Goals
|
||||||
|
{
|
||||||
|
class DLL_EXPORT ExplorationPoint : public CGoal<ExplorationPoint>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ExplorationPoint(int3 tile, int tilesToReviel)
|
||||||
|
: CGoal(Goals::EXPLORATION_POINT)
|
||||||
|
{
|
||||||
|
settile(tile);
|
||||||
|
setvalue(tilesToReviel);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator==(const ExplorationPoint & other) const override;
|
||||||
|
|
||||||
|
std::string toString() const override;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
#include "AINodeStorage.h"
|
#include "AINodeStorage.h"
|
||||||
#include "ObjectGraph.h"
|
#include "ObjectGraph.h"
|
||||||
|
#include "GraphPaths.h"
|
||||||
#include "../AIUtility.h"
|
#include "../AIUtility.h"
|
||||||
|
|
||||||
namespace NKAI
|
namespace NKAI
|
||||||
|
393
AI/Nullkiller/Pathfinding/GraphPaths.cpp
Normal file
393
AI/Nullkiller/Pathfinding/GraphPaths.cpp
Normal file
@ -0,0 +1,393 @@
|
|||||||
|
/*
|
||||||
|
* GraphPaths.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 "GraphPaths.h"
|
||||||
|
#include "AIPathfinderConfig.h"
|
||||||
|
#include "../../../lib/CRandomGenerator.h"
|
||||||
|
#include "../../../CCallback.h"
|
||||||
|
#include "../../../lib/mapping/CMap.h"
|
||||||
|
#include "../Engine/Nullkiller.h"
|
||||||
|
#include "../../../lib/logging/VisualLogger.h"
|
||||||
|
#include "Actions/QuestAction.h"
|
||||||
|
#include "../pforeach.h"
|
||||||
|
#include "Actions/BoatActions.h"
|
||||||
|
|
||||||
|
namespace NKAI
|
||||||
|
{
|
||||||
|
|
||||||
|
bool GraphNodeComparer::operator()(const GraphPathNodePointer & lhs, const GraphPathNodePointer & rhs) const
|
||||||
|
{
|
||||||
|
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, uint8_t scanDepth)
|
||||||
|
{
|
||||||
|
graph.copyFrom(*ai->baseGraph);
|
||||||
|
graph.connectHeroes(ai);
|
||||||
|
|
||||||
|
visualKey = std::to_string(ai->playerID) + ":" + targetHero->getNameTranslated();
|
||||||
|
pathNodes.clear();
|
||||||
|
|
||||||
|
GraphNodeComparer cmp(pathNodes);
|
||||||
|
GraphPathNode::TFibHeap pq(cmp);
|
||||||
|
|
||||||
|
pathNodes[targetHero->visitablePos()][GrapthPathNodeType::NORMAL].cost = 0;
|
||||||
|
pq.emplace(GraphPathNodePointer(targetHero->visitablePos(), GrapthPathNodeType::NORMAL));
|
||||||
|
|
||||||
|
while(!pq.empty())
|
||||||
|
{
|
||||||
|
GraphPathNodePointer pos = pq.top();
|
||||||
|
pq.pop();
|
||||||
|
|
||||||
|
auto & node = getOrCreateNode(pos);
|
||||||
|
std::shared_ptr<SpecialAction> transitionAction;
|
||||||
|
|
||||||
|
if(node.obj)
|
||||||
|
{
|
||||||
|
if(node.obj->ID == Obj::QUEST_GUARD
|
||||||
|
|| node.obj->ID == Obj::BORDERGUARD
|
||||||
|
|| node.obj->ID == Obj::BORDER_GATE)
|
||||||
|
{
|
||||||
|
auto questObj = dynamic_cast<const IQuestObject *>(node.obj);
|
||||||
|
auto questInfo = QuestInfo(questObj->quest, node.obj, pos.coord);
|
||||||
|
|
||||||
|
if(node.obj->ID == Obj::QUEST_GUARD
|
||||||
|
&& questObj->quest->mission == Rewardable::Limiter{}
|
||||||
|
&& questObj->quest->killTarget == ObjectInstanceID::NONE)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto questAction = std::make_shared<AIPathfinding::QuestAction>(questInfo);
|
||||||
|
|
||||||
|
if(!questAction->canAct(ai, targetHero))
|
||||||
|
{
|
||||||
|
transitionAction = questAction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node.isInQueue = false;
|
||||||
|
|
||||||
|
graph.iterateConnections(pos.coord, [this, ai, &pos, &node, &transitionAction, &pq, scanDepth](int3 target, const ObjectLink & o)
|
||||||
|
{
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
if(targetNode.cost > scanDepth)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
targetNode.specialAction = compositeAction;
|
||||||
|
|
||||||
|
auto targetGraphNode = graph.getNode(target);
|
||||||
|
|
||||||
|
if(targetGraphNode.objID.hasValue())
|
||||||
|
{
|
||||||
|
targetNode.obj = ai->cb->getObj(targetGraphNode.objID, false);
|
||||||
|
|
||||||
|
if(targetNode.obj && targetNode.obj->ID == Obj::HERO)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(targetNode.isInQueue)
|
||||||
|
{
|
||||||
|
pq.increase(targetNode.handle);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
targetNode.handle = pq.emplace(targetPointer);
|
||||||
|
targetNode.isInQueue = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraphPaths::dumpToLog() const
|
||||||
|
{
|
||||||
|
logVisual->updateWithLock(visualKey, [&](IVisualLogBuilder & logBuilder)
|
||||||
|
{
|
||||||
|
for(auto & tile : pathNodes)
|
||||||
|
{
|
||||||
|
for(auto & node : tile.second)
|
||||||
|
{
|
||||||
|
if(!node.previous.valid())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if(NKAI_GRAPH_TRACE_LEVEL >= 2)
|
||||||
|
{
|
||||||
|
logAi->trace(
|
||||||
|
"%s -> %s: %f !%d",
|
||||||
|
node.previous.coord.toString(),
|
||||||
|
tile.first.toString(),
|
||||||
|
node.cost,
|
||||||
|
node.danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
logBuilder.addLine(node.previous.coord, tile.first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GraphPathNode::tryUpdate(const GraphPathNodePointer & 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraphPaths::addChainInfo(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 & node : nodes->second)
|
||||||
|
{
|
||||||
|
if(!node.reachable())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
std::vector<GraphPathNodePointer> tilesToPass;
|
||||||
|
|
||||||
|
uint64_t danger = node.danger;
|
||||||
|
float cost = node.cost;
|
||||||
|
bool allowBattle = false;
|
||||||
|
|
||||||
|
auto current = GraphPathNodePointer(nodes->first, node.nodeType);
|
||||||
|
|
||||||
|
while(true)
|
||||||
|
{
|
||||||
|
auto currentTile = pathNodes.find(current.coord);
|
||||||
|
|
||||||
|
if(currentTile == pathNodes.end())
|
||||||
|
break;
|
||||||
|
|
||||||
|
auto currentNode = currentTile->second[current.nodeType];
|
||||||
|
|
||||||
|
if(!currentNode.previous.valid())
|
||||||
|
break;
|
||||||
|
|
||||||
|
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 & path : entryPaths)
|
||||||
|
{
|
||||||
|
if(path.targetHero != hero)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
for(auto graphTile = tilesToPass.rbegin(); graphTile != tilesToPass.rend(); graphTile++)
|
||||||
|
{
|
||||||
|
AIPathNodeInfo n;
|
||||||
|
|
||||||
|
n.coord = graphTile->coord;
|
||||||
|
n.cost = cost;
|
||||||
|
n.turns = static_cast<ui8>(cost) + 1; // just in case lets select worst scenario
|
||||||
|
n.danger = danger;
|
||||||
|
n.targetHero = hero;
|
||||||
|
n.parentIndex = -1;
|
||||||
|
n.specialAction = getNode(*graphTile).specialAction;
|
||||||
|
|
||||||
|
if(n.specialAction)
|
||||||
|
{
|
||||||
|
n.actionIsBlocked = !n.specialAction->canAct(ai, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
for(auto & node : path.nodes)
|
||||||
|
{
|
||||||
|
node.parentIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
path.nodes.insert(path.nodes.begin(), n);
|
||||||
|
}
|
||||||
|
|
||||||
|
path.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);
|
||||||
|
|
||||||
|
paths.push_back(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(ai, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto blocker = ai->objectClusterizer->getBlocker(n);
|
||||||
|
|
||||||
|
if(!blocker)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// blocker node
|
||||||
|
path.nodes.push_back(n);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
118
AI/Nullkiller/Pathfinding/GraphPaths.h
Normal file
118
AI/Nullkiller/Pathfinding/GraphPaths.h
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
* GraphPaths.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 "ObjectGraph.h"
|
||||||
|
|
||||||
|
namespace NKAI
|
||||||
|
{
|
||||||
|
|
||||||
|
class Nullkiller;
|
||||||
|
|
||||||
|
struct GraphPathNode;
|
||||||
|
|
||||||
|
enum GrapthPathNodeType
|
||||||
|
{
|
||||||
|
NORMAL,
|
||||||
|
|
||||||
|
BATTLE,
|
||||||
|
|
||||||
|
LAST
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GraphPathNodePointer
|
||||||
|
{
|
||||||
|
int3 coord = int3(-1);
|
||||||
|
GrapthPathNodeType nodeType = GrapthPathNodeType::NORMAL;
|
||||||
|
|
||||||
|
GraphPathNodePointer() = default;
|
||||||
|
|
||||||
|
GraphPathNodePointer(int3 coord, GrapthPathNodeType type)
|
||||||
|
:coord(coord), nodeType(type)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
bool valid() const
|
||||||
|
{
|
||||||
|
return coord.valid();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
using GraphNodeStorage = std::unordered_map<int3, GraphPathNode[GrapthPathNodeType::LAST]>;
|
||||||
|
|
||||||
|
class GraphNodeComparer
|
||||||
|
{
|
||||||
|
const GraphNodeStorage & pathNodes;
|
||||||
|
|
||||||
|
public:
|
||||||
|
GraphNodeComparer(const GraphNodeStorage & pathNodes)
|
||||||
|
:pathNodes(pathNodes)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator()(const GraphPathNodePointer & lhs, const GraphPathNodePointer & rhs) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GraphPathNode
|
||||||
|
{
|
||||||
|
const float BAD_COST = 100000;
|
||||||
|
|
||||||
|
GrapthPathNodeType nodeType = GrapthPathNodeType::NORMAL;
|
||||||
|
GraphPathNodePointer previous;
|
||||||
|
float cost = BAD_COST;
|
||||||
|
uint64_t danger = 0;
|
||||||
|
const CGObjectInstance * obj = nullptr;
|
||||||
|
std::shared_ptr<SpecialAction> specialAction;
|
||||||
|
|
||||||
|
using TFibHeap = boost::heap::fibonacci_heap<GraphPathNodePointer, boost::heap::compare<GraphNodeComparer>>;
|
||||||
|
|
||||||
|
TFibHeap::handle_type handle;
|
||||||
|
bool isInQueue = false;
|
||||||
|
|
||||||
|
bool reachable() const
|
||||||
|
{
|
||||||
|
return cost < BAD_COST;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool tryUpdate(const GraphPathNodePointer & pos, const GraphPathNode & prev, const ObjectLink & link);
|
||||||
|
};
|
||||||
|
|
||||||
|
class GraphPaths
|
||||||
|
{
|
||||||
|
ObjectGraph graph;
|
||||||
|
GraphNodeStorage pathNodes;
|
||||||
|
std::string visualKey;
|
||||||
|
|
||||||
|
public:
|
||||||
|
GraphPaths();
|
||||||
|
void calculatePaths(const CGHeroInstance * targetHero, const Nullkiller * ai, uint8_t scanDepth);
|
||||||
|
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:
|
||||||
|
GraphPathNode & getOrCreateNode(const GraphPathNodePointer & pos)
|
||||||
|
{
|
||||||
|
auto & node = pathNodes[pos.coord][pos.nodeType];
|
||||||
|
|
||||||
|
node.nodeType = pos.nodeType;
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GraphPathNode & getNode(const GraphPathNodePointer & pos) const
|
||||||
|
{
|
||||||
|
auto & node = pathNodes.at(pos.coord)[pos.nodeType];
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
@ -9,6 +9,7 @@
|
|||||||
*/
|
*/
|
||||||
#include "StdInc.h"
|
#include "StdInc.h"
|
||||||
#include "ObjectGraph.h"
|
#include "ObjectGraph.h"
|
||||||
|
#include "ObjectGraphCalculator.h"
|
||||||
#include "AIPathfinderConfig.h"
|
#include "AIPathfinderConfig.h"
|
||||||
#include "../../../lib/CRandomGenerator.h"
|
#include "../../../lib/CRandomGenerator.h"
|
||||||
#include "../../../CCallback.h"
|
#include "../../../CCallback.h"
|
||||||
@ -22,392 +23,6 @@
|
|||||||
namespace NKAI
|
namespace NKAI
|
||||||
{
|
{
|
||||||
|
|
||||||
struct ConnectionCostInfo
|
|
||||||
{
|
|
||||||
float totalCost = 0;
|
|
||||||
float avg = 0;
|
|
||||||
int connectionsCount = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
class ObjectGraphCalculator
|
|
||||||
{
|
|
||||||
private:
|
|
||||||
ObjectGraph * target;
|
|
||||||
const Nullkiller * ai;
|
|
||||||
std::mutex syncLock;
|
|
||||||
|
|
||||||
std::map<const CGHeroInstance *, HeroRole> actors;
|
|
||||||
std::map<const CGHeroInstance *, const CGObjectInstance *> actorObjectMap;
|
|
||||||
|
|
||||||
std::vector<std::unique_ptr<CGBoat>> temporaryBoats;
|
|
||||||
std::vector<std::unique_ptr<CGHeroInstance>> temporaryActorHeroes;
|
|
||||||
|
|
||||||
public:
|
|
||||||
ObjectGraphCalculator(ObjectGraph * target, const Nullkiller * ai)
|
|
||||||
:ai(ai), target(target), syncLock()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
void setGraphObjects()
|
|
||||||
{
|
|
||||||
for(auto obj : ai->memory->visitableObjs)
|
|
||||||
{
|
|
||||||
if(obj && obj->isVisitable() && obj->ID != Obj::HERO && obj->ID != Obj::EVENT)
|
|
||||||
{
|
|
||||||
addObjectActor(obj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for(auto town : ai->cb->getTownsInfo())
|
|
||||||
{
|
|
||||||
addObjectActor(town);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void calculateConnections()
|
|
||||||
{
|
|
||||||
updatePaths();
|
|
||||||
|
|
||||||
std::vector<AIPath> pathCache;
|
|
||||||
|
|
||||||
foreach_tile_pos(ai->cb.get(), [this, &pathCache](const CPlayerSpecificInfoCallback * cb, const int3 & 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()
|
|
||||||
{
|
|
||||||
tbb::concurrent_unordered_set<int3, std::hash<int3>> junctions;
|
|
||||||
|
|
||||||
pforeachTilePaths(ai->cb->getMapSize(), ai, [this, &junctions](const int3 & pos, std::vector<AIPath> & paths)
|
|
||||||
{
|
|
||||||
if(target->hasNodeAt(pos))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if(ai->cb->getGuardingCreaturePosition(pos).valid())
|
|
||||||
return;
|
|
||||||
|
|
||||||
ConnectionCostInfo currentCost = getConnectionsCost(paths);
|
|
||||||
|
|
||||||
if(currentCost.connectionsCount <= 2)
|
|
||||||
return;
|
|
||||||
|
|
||||||
float neighborCost = getNeighborConnectionsCost(pos, paths);
|
|
||||||
|
|
||||||
if(currentCost.avg < neighborCost)
|
|
||||||
{
|
|
||||||
junctions.insert(pos);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
for(auto pos : junctions)
|
|
||||||
{
|
|
||||||
addJunctionActor(pos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
void updatePaths()
|
|
||||||
{
|
|
||||||
PathfinderSettings ps;
|
|
||||||
|
|
||||||
ps.mainTurnDistanceLimit = 5;
|
|
||||||
ps.scoutTurnDistanceLimit = 1;
|
|
||||||
ps.allowBypassObjects = false;
|
|
||||||
|
|
||||||
ai->pathfinder->updatePaths(actors, ps);
|
|
||||||
}
|
|
||||||
|
|
||||||
void calculateConnections(const int3 & pos, std::vector<AIPath> & pathCache)
|
|
||||||
{
|
|
||||||
if(target->hasNodeAt(pos))
|
|
||||||
{
|
|
||||||
foreach_neighbour(
|
|
||||||
ai->cb.get(),
|
|
||||||
pos,
|
|
||||||
[this, &pos, &pathCache](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor)
|
|
||||||
{
|
|
||||||
if(target->hasNodeAt(neighbor))
|
|
||||||
{
|
|
||||||
ai->pathfinder->calculatePathInfo(pathCache, neighbor);
|
|
||||||
|
|
||||||
for(auto & path : pathCache)
|
|
||||||
{
|
|
||||||
if(pos == path.targetHero->visitablePos())
|
|
||||||
{
|
|
||||||
target->tryAddConnection(pos, neighbor, path.movementCost(), path.getTotalDanger());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
ai->pathfinder->calculatePathInfo(pathCache, pos);
|
|
||||||
|
|
||||||
for(AIPath & path1 : pathCache)
|
|
||||||
{
|
|
||||||
for(AIPath & path2 : pathCache)
|
|
||||||
{
|
|
||||||
if(path1.targetHero == path2.targetHero)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
auto pos1 = path1.targetHero->visitablePos();
|
|
||||||
auto pos2 = path2.targetHero->visitablePos();
|
|
||||||
|
|
||||||
if(guardPos.valid() && guardPos != pos1 && guardPos != pos2)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
auto obj1 = actorObjectMap[path1.targetHero];
|
|
||||||
auto obj2 = actorObjectMap[path2.targetHero];
|
|
||||||
|
|
||||||
auto tile1 = cb->getTile(pos1);
|
|
||||||
auto tile2 = cb->getTile(pos2);
|
|
||||||
|
|
||||||
if(tile2->isWater() && !tile1->isWater())
|
|
||||||
{
|
|
||||||
if(!cb->getTile(pos)->isWater())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
auto startingObjIsBoat = (obj1 && obj1->ID == Obj::BOAT) || target->isVirtualBoat(pos1);
|
|
||||||
|
|
||||||
if(!startingObjIsBoat)
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto danger = ai->pathfinder->getStorage()->evaluateDanger(pos2, path1.targetHero, true);
|
|
||||||
|
|
||||||
auto updated = target->tryAddConnection(
|
|
||||||
pos1,
|
|
||||||
pos2,
|
|
||||||
path1.movementCost() + path2.movementCost(),
|
|
||||||
danger);
|
|
||||||
|
|
||||||
if(NKAI_GRAPH_TRACE_LEVEL >= 2 && updated)
|
|
||||||
{
|
|
||||||
logAi->trace(
|
|
||||||
"Connected %s[%s] -> %s[%s] through [%s], cost %2f",
|
|
||||||
obj1 ? obj1->getObjectName() : "J", pos1.toString(),
|
|
||||||
obj2 ? obj2->getObjectName() : "J", pos2.toString(),
|
|
||||||
pos.toString(),
|
|
||||||
path1.movementCost() + path2.movementCost());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isExtraConnection(float direct, float side1, float side2) const
|
|
||||||
{
|
|
||||||
float sideRatio = (side1 + side2) / direct;
|
|
||||||
|
|
||||||
return sideRatio < 1.25f && direct > side1 && direct > side2;
|
|
||||||
}
|
|
||||||
|
|
||||||
void removeExtraConnections()
|
|
||||||
{
|
|
||||||
std::vector<std::pair<int3, int3>> connectionsToRemove;
|
|
||||||
|
|
||||||
for(auto & actor : temporaryActorHeroes)
|
|
||||||
{
|
|
||||||
auto pos = actor->visitablePos();
|
|
||||||
auto & currentNode = target->getNode(pos);
|
|
||||||
|
|
||||||
target->iterateConnections(pos, [this, &pos, &connectionsToRemove, ¤tNode](int3 n1, ObjectLink o1)
|
|
||||||
{
|
|
||||||
target->iterateConnections(n1, [&pos, &o1, ¤tNode, &connectionsToRemove, this](int3 n2, ObjectLink o2)
|
|
||||||
{
|
|
||||||
auto direct = currentNode.connections.find(n2);
|
|
||||||
|
|
||||||
if(direct != currentNode.connections.end() && isExtraConnection(direct->second.cost, o1.cost, o2.cost))
|
|
||||||
{
|
|
||||||
connectionsToRemove.push_back({pos, n2});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
vstd::removeDuplicates(connectionsToRemove);
|
|
||||||
|
|
||||||
for(auto & c : connectionsToRemove)
|
|
||||||
{
|
|
||||||
target->removeConnection(c.first, c.second);
|
|
||||||
|
|
||||||
if(NKAI_GRAPH_TRACE_LEVEL >= 2)
|
|
||||||
{
|
|
||||||
logAi->trace("Remove ineffective connection %s->%s", c.first.toString(), c.second.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void addObjectActor(const CGObjectInstance * obj)
|
|
||||||
{
|
|
||||||
auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique<CGHeroInstance>(obj->cb)).get();
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
if(cb->getTile(visitablePos)->isWater())
|
|
||||||
{
|
|
||||||
objectActor->boat = temporaryBoats.emplace_back(std::make_unique<CGBoat>(objectActor->cb)).get();
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(objectActor->visitablePos() == visitablePos);
|
|
||||||
|
|
||||||
actorObjectMap[objectActor] = obj;
|
|
||||||
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, 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();
|
|
||||||
|
|
||||||
CRandomGenerator rng;
|
|
||||||
|
|
||||||
objectActor->setOwner(ai->playerID); // lets avoid having multiple colors
|
|
||||||
objectActor->initHero(rng, static_cast<HeroTypeID>(0));
|
|
||||||
objectActor->pos = objectActor->convertFromVisitablePos(visitablePos);
|
|
||||||
objectActor->initObj(rng);
|
|
||||||
|
|
||||||
if(isVirtualBoat || ai->cb->getTile(visitablePos)->isWater())
|
|
||||||
{
|
|
||||||
objectActor->boat = temporaryBoats.emplace_back(std::make_unique<CGBoat>(objectActor->cb)).get();
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(objectActor->visitablePos() == visitablePos);
|
|
||||||
|
|
||||||
actorObjectMap[objectActor] = nullptr;
|
|
||||||
actors[objectActor] = isVirtualBoat ? HeroRole::MAIN : HeroRole::SCOUT;
|
|
||||||
|
|
||||||
target->registerJunction(visitablePos);
|
|
||||||
}
|
|
||||||
|
|
||||||
ConnectionCostInfo getConnectionsCost(std::vector<AIPath> & paths) const
|
|
||||||
{
|
|
||||||
std::map<int3, float> costs;
|
|
||||||
|
|
||||||
for(auto & path : paths)
|
|
||||||
{
|
|
||||||
auto fromPos = path.targetHero->visitablePos();
|
|
||||||
auto cost = costs.find(fromPos);
|
|
||||||
|
|
||||||
if(cost == costs.end())
|
|
||||||
{
|
|
||||||
costs.emplace(fromPos, path.movementCost());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if(path.movementCost() < cost->second)
|
|
||||||
{
|
|
||||||
costs[fromPos] = path.movementCost();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ConnectionCostInfo result;
|
|
||||||
|
|
||||||
for(auto & cost : costs)
|
|
||||||
{
|
|
||||||
result.totalCost += cost.second;
|
|
||||||
result.connectionsCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(result.connectionsCount)
|
|
||||||
{
|
|
||||||
result.avg = result.totalCost / result.connectionsCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
bool ObjectGraph::tryAddConnection(
|
bool ObjectGraph::tryAddConnection(
|
||||||
const int3 & from,
|
const int3 & from,
|
||||||
const int3 & to,
|
const int3 & to,
|
||||||
@ -538,372 +153,4 @@ void ObjectGraph::dumpToLog(std::string visualKey) const
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bool GraphNodeComparer::operator()(const GraphPathNodePointer & lhs, const GraphPathNodePointer & rhs) const
|
|
||||||
{
|
|
||||||
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, uint8_t scanDepth)
|
|
||||||
{
|
|
||||||
graph.copyFrom(*ai->baseGraph);
|
|
||||||
graph.connectHeroes(ai);
|
|
||||||
|
|
||||||
visualKey = std::to_string(ai->playerID) + ":" + targetHero->getNameTranslated();
|
|
||||||
pathNodes.clear();
|
|
||||||
|
|
||||||
GraphNodeComparer cmp(pathNodes);
|
|
||||||
GraphPathNode::TFibHeap pq(cmp);
|
|
||||||
|
|
||||||
pathNodes[targetHero->visitablePos()][GrapthPathNodeType::NORMAL].cost = 0;
|
|
||||||
pq.emplace(GraphPathNodePointer(targetHero->visitablePos(), GrapthPathNodeType::NORMAL));
|
|
||||||
|
|
||||||
while(!pq.empty())
|
|
||||||
{
|
|
||||||
GraphPathNodePointer pos = pq.top();
|
|
||||||
pq.pop();
|
|
||||||
|
|
||||||
auto & node = getOrCreateNode(pos);
|
|
||||||
std::shared_ptr<SpecialAction> transitionAction;
|
|
||||||
|
|
||||||
if(node.obj)
|
|
||||||
{
|
|
||||||
if(node.obj->ID == Obj::QUEST_GUARD
|
|
||||||
|| node.obj->ID == Obj::BORDERGUARD
|
|
||||||
|| node.obj->ID == Obj::BORDER_GATE)
|
|
||||||
{
|
|
||||||
auto questObj = dynamic_cast<const IQuestObject *>(node.obj);
|
|
||||||
auto questInfo = QuestInfo(questObj->quest, node.obj, pos.coord);
|
|
||||||
|
|
||||||
if(node.obj->ID == Obj::QUEST_GUARD
|
|
||||||
&& questObj->quest->mission == Rewardable::Limiter{}
|
|
||||||
&& questObj->quest->killTarget == ObjectInstanceID::NONE)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto questAction = std::make_shared<AIPathfinding::QuestAction>(questInfo);
|
|
||||||
|
|
||||||
if(!questAction->canAct(ai, targetHero))
|
|
||||||
{
|
|
||||||
transitionAction = questAction;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
node.isInQueue = false;
|
|
||||||
|
|
||||||
graph.iterateConnections(pos.coord, [this, ai, &pos, &node, &transitionAction, &pq, scanDepth](int3 target, const ObjectLink & o)
|
|
||||||
{
|
|
||||||
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))
|
|
||||||
{
|
|
||||||
if(targetNode.cost > scanDepth)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
targetNode.specialAction = compositeAction;
|
|
||||||
|
|
||||||
auto targetGraphNode = graph.getNode(target);
|
|
||||||
|
|
||||||
if(targetGraphNode.objID.hasValue())
|
|
||||||
{
|
|
||||||
targetNode.obj = ai->cb->getObj(targetGraphNode.objID, false);
|
|
||||||
|
|
||||||
if(targetNode.obj && targetNode.obj->ID == Obj::HERO)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(targetNode.isInQueue)
|
|
||||||
{
|
|
||||||
pq.increase(targetNode.handle);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
targetNode.handle = pq.emplace(targetPointer);
|
|
||||||
targetNode.isInQueue = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void GraphPaths::dumpToLog() const
|
|
||||||
{
|
|
||||||
logVisual->updateWithLock(visualKey, [&](IVisualLogBuilder & logBuilder)
|
|
||||||
{
|
|
||||||
for(auto & tile : pathNodes)
|
|
||||||
{
|
|
||||||
for(auto & node : tile.second)
|
|
||||||
{
|
|
||||||
if(!node.previous.valid())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if(NKAI_GRAPH_TRACE_LEVEL >= 2)
|
|
||||||
{
|
|
||||||
logAi->trace(
|
|
||||||
"%s -> %s: %f !%d",
|
|
||||||
node.previous.coord.toString(),
|
|
||||||
tile.first.toString(),
|
|
||||||
node.cost,
|
|
||||||
node.danger);
|
|
||||||
}
|
|
||||||
|
|
||||||
logBuilder.addLine(node.previous.coord, tile.first);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
bool GraphPathNode::tryUpdate(const GraphPathNodePointer & 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
void GraphPaths::addChainInfo(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 & node : nodes->second)
|
|
||||||
{
|
|
||||||
if(!node.reachable())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
std::vector<GraphPathNodePointer> tilesToPass;
|
|
||||||
|
|
||||||
uint64_t danger = node.danger;
|
|
||||||
float cost = node.cost;
|
|
||||||
bool allowBattle = false;
|
|
||||||
|
|
||||||
auto current = GraphPathNodePointer(nodes->first, node.nodeType);
|
|
||||||
|
|
||||||
while(true)
|
|
||||||
{
|
|
||||||
auto currentTile = pathNodes.find(current.coord);
|
|
||||||
|
|
||||||
if(currentTile == pathNodes.end())
|
|
||||||
break;
|
|
||||||
|
|
||||||
auto currentNode = currentTile->second[current.nodeType];
|
|
||||||
|
|
||||||
if(!currentNode.previous.valid())
|
|
||||||
break;
|
|
||||||
|
|
||||||
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 & path : entryPaths)
|
|
||||||
{
|
|
||||||
if(path.targetHero != hero)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
for(auto graphTile = tilesToPass.rbegin(); graphTile != tilesToPass.rend(); graphTile++)
|
|
||||||
{
|
|
||||||
AIPathNodeInfo n;
|
|
||||||
|
|
||||||
n.coord = graphTile->coord;
|
|
||||||
n.cost = cost;
|
|
||||||
n.turns = static_cast<ui8>(cost) + 1; // just in case lets select worst scenario
|
|
||||||
n.danger = danger;
|
|
||||||
n.targetHero = hero;
|
|
||||||
n.parentIndex = -1;
|
|
||||||
n.specialAction = getNode(*graphTile).specialAction;
|
|
||||||
|
|
||||||
if(n.specialAction)
|
|
||||||
{
|
|
||||||
n.actionIsBlocked = !n.specialAction->canAct(ai, n);
|
|
||||||
}
|
|
||||||
|
|
||||||
for(auto & node : path.nodes)
|
|
||||||
{
|
|
||||||
node.parentIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
path.nodes.insert(path.nodes.begin(), n);
|
|
||||||
}
|
|
||||||
|
|
||||||
path.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);
|
|
||||||
|
|
||||||
paths.push_back(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(ai, n);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto blocker = ai->objectClusterizer->getBlocker(n);
|
|
||||||
|
|
||||||
if(!blocker)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// blocker node
|
|
||||||
path.nodes.push_back(n);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -112,102 +112,4 @@ public:
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct GraphPathNode;
|
|
||||||
|
|
||||||
enum GrapthPathNodeType
|
|
||||||
{
|
|
||||||
NORMAL,
|
|
||||||
|
|
||||||
BATTLE,
|
|
||||||
|
|
||||||
LAST
|
|
||||||
};
|
|
||||||
|
|
||||||
struct GraphPathNodePointer
|
|
||||||
{
|
|
||||||
int3 coord = int3(-1);
|
|
||||||
GrapthPathNodeType nodeType = GrapthPathNodeType::NORMAL;
|
|
||||||
|
|
||||||
GraphPathNodePointer() = default;
|
|
||||||
|
|
||||||
GraphPathNodePointer(int3 coord, GrapthPathNodeType type)
|
|
||||||
:coord(coord), nodeType(type)
|
|
||||||
{ }
|
|
||||||
|
|
||||||
bool valid() const
|
|
||||||
{
|
|
||||||
return coord.valid();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
using GraphNodeStorage = std::unordered_map<int3, GraphPathNode[GrapthPathNodeType::LAST]>;
|
|
||||||
|
|
||||||
class GraphNodeComparer
|
|
||||||
{
|
|
||||||
const GraphNodeStorage & pathNodes;
|
|
||||||
|
|
||||||
public:
|
|
||||||
GraphNodeComparer(const GraphNodeStorage & pathNodes)
|
|
||||||
:pathNodes(pathNodes)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
bool operator()(const GraphPathNodePointer & lhs, const GraphPathNodePointer & rhs) const;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct GraphPathNode
|
|
||||||
{
|
|
||||||
const float BAD_COST = 100000;
|
|
||||||
|
|
||||||
GrapthPathNodeType nodeType = GrapthPathNodeType::NORMAL;
|
|
||||||
GraphPathNodePointer previous;
|
|
||||||
float cost = BAD_COST;
|
|
||||||
uint64_t danger = 0;
|
|
||||||
const CGObjectInstance * obj = nullptr;
|
|
||||||
std::shared_ptr<SpecialAction> specialAction;
|
|
||||||
|
|
||||||
using TFibHeap = boost::heap::fibonacci_heap<GraphPathNodePointer, boost::heap::compare<GraphNodeComparer>>;
|
|
||||||
|
|
||||||
TFibHeap::handle_type handle;
|
|
||||||
bool isInQueue = false;
|
|
||||||
|
|
||||||
bool reachable() const
|
|
||||||
{
|
|
||||||
return cost < BAD_COST;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool tryUpdate(const GraphPathNodePointer & pos, const GraphPathNode & prev, const ObjectLink & link);
|
|
||||||
};
|
|
||||||
|
|
||||||
class GraphPaths
|
|
||||||
{
|
|
||||||
ObjectGraph graph;
|
|
||||||
GraphNodeStorage pathNodes;
|
|
||||||
std::string visualKey;
|
|
||||||
|
|
||||||
public:
|
|
||||||
GraphPaths();
|
|
||||||
void calculatePaths(const CGHeroInstance * targetHero, const Nullkiller * ai, uint8_t scanDepth);
|
|
||||||
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:
|
|
||||||
GraphPathNode & getOrCreateNode(const GraphPathNodePointer & pos)
|
|
||||||
{
|
|
||||||
auto & node = pathNodes[pos.coord][pos.nodeType];
|
|
||||||
|
|
||||||
node.nodeType = pos.nodeType;
|
|
||||||
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
const GraphPathNode & getNode(const GraphPathNodePointer & pos) const
|
|
||||||
{
|
|
||||||
auto & node = pathNodes.at(pos.coord)[pos.nodeType];
|
|
||||||
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
387
AI/Nullkiller/Pathfinding/ObjectGraphCalculator.cpp
Normal file
387
AI/Nullkiller/Pathfinding/ObjectGraphCalculator.cpp
Normal file
@ -0,0 +1,387 @@
|
|||||||
|
/*
|
||||||
|
* ObjectGraphCalculator.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 "ObjectGraphCalculator.h"
|
||||||
|
#include "AIPathfinderConfig.h"
|
||||||
|
#include "../../../lib/CRandomGenerator.h"
|
||||||
|
#include "../../../CCallback.h"
|
||||||
|
#include "../../../lib/mapping/CMap.h"
|
||||||
|
#include "../Engine/Nullkiller.h"
|
||||||
|
#include "../../../lib/logging/VisualLogger.h"
|
||||||
|
#include "Actions/QuestAction.h"
|
||||||
|
#include "../pforeach.h"
|
||||||
|
|
||||||
|
namespace NKAI
|
||||||
|
{
|
||||||
|
|
||||||
|
ObjectGraphCalculator::ObjectGraphCalculator(ObjectGraph * target, const Nullkiller * ai)
|
||||||
|
:ai(ai), target(target), syncLock()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void ObjectGraphCalculator::setGraphObjects()
|
||||||
|
{
|
||||||
|
for(auto obj : ai->memory->visitableObjs)
|
||||||
|
{
|
||||||
|
if(obj && obj->isVisitable() && obj->ID != Obj::HERO && obj->ID != Obj::EVENT)
|
||||||
|
{
|
||||||
|
addObjectActor(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(auto town : ai->cb->getTownsInfo())
|
||||||
|
{
|
||||||
|
addObjectActor(town);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ObjectGraphCalculator::calculateConnections()
|
||||||
|
{
|
||||||
|
updatePaths();
|
||||||
|
|
||||||
|
std::vector<AIPath> pathCache;
|
||||||
|
|
||||||
|
foreach_tile_pos(ai->cb.get(), [this, &pathCache](const CPlayerSpecificInfoCallback * cb, const int3 & pos)
|
||||||
|
{
|
||||||
|
calculateConnections(pos, pathCache);
|
||||||
|
});
|
||||||
|
|
||||||
|
removeExtraConnections();
|
||||||
|
}
|
||||||
|
|
||||||
|
float ObjectGraphCalculator::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 ObjectGraphCalculator::addMinimalDistanceJunctions()
|
||||||
|
{
|
||||||
|
tbb::concurrent_unordered_set<int3, std::hash<int3>> junctions;
|
||||||
|
|
||||||
|
pforeachTilePaths(ai->cb->getMapSize(), ai, [this, &junctions](const int3 & pos, std::vector<AIPath> & paths)
|
||||||
|
{
|
||||||
|
if(target->hasNodeAt(pos))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if(ai->cb->getGuardingCreaturePosition(pos).valid())
|
||||||
|
return;
|
||||||
|
|
||||||
|
ConnectionCostInfo currentCost = getConnectionsCost(paths);
|
||||||
|
|
||||||
|
if(currentCost.connectionsCount <= 2)
|
||||||
|
return;
|
||||||
|
|
||||||
|
float neighborCost = getNeighborConnectionsCost(pos, paths);
|
||||||
|
|
||||||
|
if(currentCost.avg < neighborCost)
|
||||||
|
{
|
||||||
|
junctions.insert(pos);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for(auto pos : junctions)
|
||||||
|
{
|
||||||
|
addJunctionActor(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ObjectGraphCalculator::updatePaths()
|
||||||
|
{
|
||||||
|
PathfinderSettings ps;
|
||||||
|
|
||||||
|
ps.mainTurnDistanceLimit = 5;
|
||||||
|
ps.scoutTurnDistanceLimit = 1;
|
||||||
|
ps.allowBypassObjects = false;
|
||||||
|
|
||||||
|
ai->pathfinder->updatePaths(actors, ps);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ObjectGraphCalculator::calculateConnections(const int3 & pos, std::vector<AIPath> & pathCache)
|
||||||
|
{
|
||||||
|
if(target->hasNodeAt(pos))
|
||||||
|
{
|
||||||
|
foreach_neighbour(
|
||||||
|
ai->cb.get(),
|
||||||
|
pos,
|
||||||
|
[this, &pos, &pathCache](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor)
|
||||||
|
{
|
||||||
|
if(target->hasNodeAt(neighbor))
|
||||||
|
{
|
||||||
|
ai->pathfinder->calculatePathInfo(pathCache, neighbor);
|
||||||
|
|
||||||
|
for(auto & path : pathCache)
|
||||||
|
{
|
||||||
|
if(pos == path.targetHero->visitablePos())
|
||||||
|
{
|
||||||
|
target->tryAddConnection(pos, neighbor, path.movementCost(), path.getTotalDanger());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
ai->pathfinder->calculatePathInfo(pathCache, pos);
|
||||||
|
|
||||||
|
for(AIPath & path1 : pathCache)
|
||||||
|
{
|
||||||
|
for(AIPath & path2 : pathCache)
|
||||||
|
{
|
||||||
|
if(path1.targetHero == path2.targetHero)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
auto pos1 = path1.targetHero->visitablePos();
|
||||||
|
auto pos2 = path2.targetHero->visitablePos();
|
||||||
|
|
||||||
|
if(guardPos.valid() && guardPos != pos1 && guardPos != pos2)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
auto obj1 = actorObjectMap[path1.targetHero];
|
||||||
|
auto obj2 = actorObjectMap[path2.targetHero];
|
||||||
|
|
||||||
|
auto tile1 = cb->getTile(pos1);
|
||||||
|
auto tile2 = cb->getTile(pos2);
|
||||||
|
|
||||||
|
if(tile2->isWater() && !tile1->isWater())
|
||||||
|
{
|
||||||
|
if(!cb->getTile(pos)->isWater())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
auto startingObjIsBoat = (obj1 && obj1->ID == Obj::BOAT) || target->isVirtualBoat(pos1);
|
||||||
|
|
||||||
|
if(!startingObjIsBoat)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto danger = ai->pathfinder->getStorage()->evaluateDanger(pos2, path1.targetHero, true);
|
||||||
|
|
||||||
|
auto updated = target->tryAddConnection(
|
||||||
|
pos1,
|
||||||
|
pos2,
|
||||||
|
path1.movementCost() + path2.movementCost(),
|
||||||
|
danger);
|
||||||
|
|
||||||
|
if(NKAI_GRAPH_TRACE_LEVEL >= 2 && updated)
|
||||||
|
{
|
||||||
|
logAi->trace(
|
||||||
|
"Connected %s[%s] -> %s[%s] through [%s], cost %2f",
|
||||||
|
obj1 ? obj1->getObjectName() : "J", pos1.toString(),
|
||||||
|
obj2 ? obj2->getObjectName() : "J", pos2.toString(),
|
||||||
|
pos.toString(),
|
||||||
|
path1.movementCost() + path2.movementCost());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ObjectGraphCalculator::isExtraConnection(float direct, float side1, float side2) const
|
||||||
|
{
|
||||||
|
float sideRatio = (side1 + side2) / direct;
|
||||||
|
|
||||||
|
return sideRatio < 1.25f && direct > side1 && direct > side2;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ObjectGraphCalculator::removeExtraConnections()
|
||||||
|
{
|
||||||
|
std::vector<std::pair<int3, int3>> connectionsToRemove;
|
||||||
|
|
||||||
|
for(auto & actor : temporaryActorHeroes)
|
||||||
|
{
|
||||||
|
auto pos = actor->visitablePos();
|
||||||
|
auto & currentNode = target->getNode(pos);
|
||||||
|
|
||||||
|
target->iterateConnections(pos, [this, &pos, &connectionsToRemove, ¤tNode](int3 n1, ObjectLink o1)
|
||||||
|
{
|
||||||
|
target->iterateConnections(n1, [&pos, &o1, ¤tNode, &connectionsToRemove, this](int3 n2, ObjectLink o2)
|
||||||
|
{
|
||||||
|
auto direct = currentNode.connections.find(n2);
|
||||||
|
|
||||||
|
if(direct != currentNode.connections.end() && isExtraConnection(direct->second.cost, o1.cost, o2.cost))
|
||||||
|
{
|
||||||
|
connectionsToRemove.push_back({pos, n2});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
vstd::removeDuplicates(connectionsToRemove);
|
||||||
|
|
||||||
|
for(auto & c : connectionsToRemove)
|
||||||
|
{
|
||||||
|
target->removeConnection(c.first, c.second);
|
||||||
|
|
||||||
|
if(NKAI_GRAPH_TRACE_LEVEL >= 2)
|
||||||
|
{
|
||||||
|
logAi->trace("Remove ineffective connection %s->%s", c.first.toString(), c.second.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ObjectGraphCalculator::addObjectActor(const CGObjectInstance * obj)
|
||||||
|
{
|
||||||
|
auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique<CGHeroInstance>(obj->cb)).get();
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
if(cb->getTile(visitablePos)->isWater())
|
||||||
|
{
|
||||||
|
objectActor->boat = temporaryBoats.emplace_back(std::make_unique<CGBoat>(objectActor->cb)).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(objectActor->visitablePos() == visitablePos);
|
||||||
|
|
||||||
|
actorObjectMap[objectActor] = obj;
|
||||||
|
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 ObjectGraphCalculator::addJunctionActor(const int3 & visitablePos, bool isVirtualBoat)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(syncLock);
|
||||||
|
|
||||||
|
auto internalCb = temporaryActorHeroes.front()->cb;
|
||||||
|
auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique<CGHeroInstance>(internalCb)).get();
|
||||||
|
|
||||||
|
CRandomGenerator rng;
|
||||||
|
|
||||||
|
objectActor->setOwner(ai->playerID); // lets avoid having multiple colors
|
||||||
|
objectActor->initHero(rng, static_cast<HeroTypeID>(0));
|
||||||
|
objectActor->pos = objectActor->convertFromVisitablePos(visitablePos);
|
||||||
|
objectActor->initObj(rng);
|
||||||
|
|
||||||
|
if(isVirtualBoat || ai->cb->getTile(visitablePos)->isWater())
|
||||||
|
{
|
||||||
|
objectActor->boat = temporaryBoats.emplace_back(std::make_unique<CGBoat>(objectActor->cb)).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(objectActor->visitablePos() == visitablePos);
|
||||||
|
|
||||||
|
actorObjectMap[objectActor] = nullptr;
|
||||||
|
actors[objectActor] = isVirtualBoat ? HeroRole::MAIN : HeroRole::SCOUT;
|
||||||
|
|
||||||
|
target->registerJunction(visitablePos);
|
||||||
|
}
|
||||||
|
|
||||||
|
ConnectionCostInfo ObjectGraphCalculator::getConnectionsCost(std::vector<AIPath> & paths) const
|
||||||
|
{
|
||||||
|
std::map<int3, float> costs;
|
||||||
|
|
||||||
|
for(auto & path : paths)
|
||||||
|
{
|
||||||
|
auto fromPos = path.targetHero->visitablePos();
|
||||||
|
auto cost = costs.find(fromPos);
|
||||||
|
|
||||||
|
if(cost == costs.end())
|
||||||
|
{
|
||||||
|
costs.emplace(fromPos, path.movementCost());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if(path.movementCost() < cost->second)
|
||||||
|
{
|
||||||
|
costs[fromPos] = path.movementCost();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ConnectionCostInfo result;
|
||||||
|
|
||||||
|
for(auto & cost : costs)
|
||||||
|
{
|
||||||
|
result.totalCost += cost.second;
|
||||||
|
result.connectionsCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(result.connectionsCount)
|
||||||
|
{
|
||||||
|
result.avg = result.totalCost / result.connectionsCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
56
AI/Nullkiller/Pathfinding/ObjectGraphCalculator.h
Normal file
56
AI/Nullkiller/Pathfinding/ObjectGraphCalculator.h
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* ObjectGraphCalculator.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 "ObjectGraph.h"
|
||||||
|
#include "../AIUtility.h"
|
||||||
|
|
||||||
|
namespace NKAI
|
||||||
|
{
|
||||||
|
|
||||||
|
struct ConnectionCostInfo
|
||||||
|
{
|
||||||
|
float totalCost = 0;
|
||||||
|
float avg = 0;
|
||||||
|
int connectionsCount = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ObjectGraphCalculator
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
ObjectGraph * target;
|
||||||
|
const Nullkiller * ai;
|
||||||
|
std::mutex syncLock;
|
||||||
|
|
||||||
|
std::map<const CGHeroInstance *, HeroRole> actors;
|
||||||
|
std::map<const CGHeroInstance *, const CGObjectInstance *> actorObjectMap;
|
||||||
|
|
||||||
|
std::vector<std::unique_ptr<CGBoat>> temporaryBoats;
|
||||||
|
std::vector<std::unique_ptr<CGHeroInstance>> temporaryActorHeroes;
|
||||||
|
|
||||||
|
public:
|
||||||
|
ObjectGraphCalculator(ObjectGraph * target, const Nullkiller * ai);
|
||||||
|
void setGraphObjects();
|
||||||
|
void calculateConnections();
|
||||||
|
float getNeighborConnectionsCost(const int3 & pos, std::vector<AIPath> & pathCache);
|
||||||
|
void addMinimalDistanceJunctions();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void updatePaths();
|
||||||
|
void calculateConnections(const int3 & pos, std::vector<AIPath> & pathCache);
|
||||||
|
bool isExtraConnection(float direct, float side1, float side2) const;
|
||||||
|
void removeExtraConnections();
|
||||||
|
void addObjectActor(const CGObjectInstance * obj);
|
||||||
|
void addJunctionActor(const int3 & visitablePos, bool isVirtualBoat = false);
|
||||||
|
ConnectionCostInfo getConnectionsCost(std::vector<AIPath> & paths) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
@ -4,5 +4,7 @@
|
|||||||
"mainHeroTurnDistanceLimit" : 10,
|
"mainHeroTurnDistanceLimit" : 10,
|
||||||
"scoutHeroTurnDistanceLimit" : 5,
|
"scoutHeroTurnDistanceLimit" : 5,
|
||||||
"maxGoldPressure" : 0.3,
|
"maxGoldPressure" : 0.3,
|
||||||
"useTroopsFromGarrisons" : true
|
"useTroopsFromGarrisons" : true,
|
||||||
|
"openMap": true,
|
||||||
|
"allowObjectGraph": true
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user