mirror of
https://github.com/vcmi/vcmi.git
synced 2024-12-20 20:23:03 +02:00
NKAI: port exploration from VCAI
This commit is contained in:
parent
721b15d9de
commit
1388fb1493
@ -77,7 +77,6 @@ AIGateway::AIGateway()
|
||||
destinationTeleport = ObjectInstanceID();
|
||||
destinationTeleportPos = int3(-1);
|
||||
nullkiller.reset(new Nullkiller());
|
||||
announcedCheatingProblem = false;
|
||||
}
|
||||
|
||||
AIGateway::~AIGateway()
|
||||
@ -378,7 +377,7 @@ void AIGateway::objectRemoved(const CGObjectInstance * obj, const PlayerColor &
|
||||
nullkiller->memory->removeFromMemory(obj);
|
||||
nullkiller->objectClusterizer->onObjectRemoved(obj->id);
|
||||
|
||||
if(nullkiller->baseGraph && nullkiller->settings->isObjectGraphAllowed())
|
||||
if(nullkiller->baseGraph && nullkiller->isObjectGraphAllowed())
|
||||
{
|
||||
nullkiller->baseGraph->removeObject(obj);
|
||||
}
|
||||
@ -829,13 +828,9 @@ void AIGateway::makeTurn()
|
||||
boost::shared_lock<boost::shared_mutex> gsLock(CGameState::mutex);
|
||||
setThreadName("AIGateway::makeTurn");
|
||||
|
||||
if(cb->getStartInfo()->extraOptionsInfo.cheatsAllowed)
|
||||
cb->sendMessage("vcmieagles");
|
||||
else
|
||||
if(nullkiller->isOpenMap())
|
||||
{
|
||||
if(!announcedCheatingProblem)
|
||||
cb->sendMessage("Nullkiller AI currently requires the ability to cheat in order to function correctly! Please enable!");
|
||||
announcedCheatingProblem = true;
|
||||
cb->sendMessage("vcmieagles");
|
||||
}
|
||||
|
||||
retrieveVisitableObjs();
|
||||
|
@ -96,7 +96,7 @@ public:
|
||||
std::unique_ptr<boost::thread> makingTurn;
|
||||
private:
|
||||
boost::mutex turnInterruptionMutex;
|
||||
bool announcedCheatingProblem;
|
||||
|
||||
public:
|
||||
ObjectInstanceID selectedObject;
|
||||
|
||||
|
@ -381,7 +381,7 @@ void ObjectClusterizer::clusterizeObject(
|
||||
logAi->trace("Check object %s%s.", obj->getObjectName(), obj->visitablePos().toString());
|
||||
#endif
|
||||
|
||||
if(ai->settings->isObjectGraphAllowed())
|
||||
if(ai->isObjectGraphAllowed())
|
||||
{
|
||||
ai->pathfinder->calculateQuickPathsWithBlocker(pathCache, heroes, obj->visitablePos());
|
||||
}
|
||||
|
@ -47,7 +47,11 @@ bool CaptureObjectsBehavior::operator==(const CaptureObjectsBehavior & other) co
|
||||
&& 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;
|
||||
|
||||
@ -72,7 +76,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
|
||||
continue;
|
||||
}
|
||||
|
||||
if(objToVisit && !shouldVisit(nullkiller, path.targetHero, objToVisit))
|
||||
if(objToVisit && !force && !shouldVisit(nullkiller, path.targetHero, objToVisit))
|
||||
{
|
||||
#if NKAI_TRACE_LEVEL >= 2
|
||||
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());
|
||||
#endif
|
||||
|
||||
nullkiller->pathfinder->calculatePathInfo(paths, objToVisit->visitablePos(), nullkiller->settings->isObjectGraphAllowed());
|
||||
nullkiller->pathfinder->calculatePathInfo(paths, objToVisit->visitablePos(), nullkiller->isObjectGraphAllowed());
|
||||
|
||||
#if NKAI_TRACE_LEVEL >= 1
|
||||
logAi->trace("Found %d paths", paths.size());
|
||||
#endif
|
||||
vstd::concatenate(tasksLocal, getVisitGoals(paths, nullkiller, objToVisit));
|
||||
vstd::concatenate(tasksLocal, getVisitGoals(paths, nullkiller, objToVisit, specificObjects));
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(sync);
|
||||
|
@ -68,7 +68,11 @@ namespace Goals
|
||||
|
||||
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:
|
||||
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
|
||||
{
|
||||
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();
|
||||
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());
|
||||
#endif
|
||||
|
||||
auto paths = ai->pathfinder->getPathInfo(pos, ai->settings->isObjectGraphAllowed());
|
||||
auto paths = ai->pathfinder->getPathInfo(pos, ai->isObjectGraphAllowed());
|
||||
|
||||
#if NKAI_TRACE_LEVEL >= 1
|
||||
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());
|
||||
#endif
|
||||
|
||||
auto paths = ai->pathfinder->getPathInfo(pos, ai->settings->isObjectGraphAllowed());
|
||||
auto paths = ai->pathfinder->getPathInfo(pos, ai->isObjectGraphAllowed());
|
||||
auto goals = CaptureObjectsBehavior::getVisitGoals(paths, ai);
|
||||
|
||||
std::vector<std::shared_ptr<ExecuteHeroChain>> waysToVisitObj;
|
||||
|
@ -15,6 +15,8 @@ set(Nullkiller_SRCS
|
||||
Pathfinding/Rules/AIMovementToDestinationRule.cpp
|
||||
Pathfinding/Rules/AIPreviousNodeRule.cpp
|
||||
Pathfinding/ObjectGraph.cpp
|
||||
Pathfinding/GraphPaths.cpp
|
||||
Pathfinding/ObjectGraphCalculator.cpp
|
||||
AIUtility.cpp
|
||||
Analyzers/ArmyManager.cpp
|
||||
Analyzers/HeroManager.cpp
|
||||
@ -42,6 +44,7 @@ set(Nullkiller_SRCS
|
||||
Markers/HeroExchange.cpp
|
||||
Markers/UnlockCluster.cpp
|
||||
Markers/DefendTown.cpp
|
||||
Markers/ExplorationPoint.cpp
|
||||
Engine/Nullkiller.cpp
|
||||
Engine/DeepDecomposer.cpp
|
||||
Engine/PriorityEvaluator.cpp
|
||||
@ -57,6 +60,7 @@ set(Nullkiller_SRCS
|
||||
Behaviors/GatherArmyBehavior.cpp
|
||||
Behaviors/ClusterBehavior.cpp
|
||||
Behaviors/StayAtTownBehavior.cpp
|
||||
Behaviors/ExplorationBehavior.cpp
|
||||
Helpers/ArmyFormation.cpp
|
||||
AIGateway.cpp
|
||||
)
|
||||
@ -80,6 +84,8 @@ set(Nullkiller_HEADERS
|
||||
Pathfinding/Rules/AIMovementToDestinationRule.h
|
||||
Pathfinding/Rules/AIPreviousNodeRule.h
|
||||
Pathfinding/ObjectGraph.h
|
||||
Pathfinding/GraphPaths.h
|
||||
Pathfinding/ObjectGraphCalculator.h
|
||||
AIUtility.h
|
||||
pforeach.h
|
||||
Analyzers/ArmyManager.h
|
||||
@ -111,6 +117,7 @@ set(Nullkiller_HEADERS
|
||||
Markers/HeroExchange.h
|
||||
Markers/UnlockCluster.h
|
||||
Markers/DefendTown.h
|
||||
Markers/ExplorationPoint.h
|
||||
Engine/Nullkiller.h
|
||||
Engine/DeepDecomposer.h
|
||||
Engine/PriorityEvaluator.h
|
||||
@ -126,6 +133,7 @@ set(Nullkiller_HEADERS
|
||||
Behaviors/GatherArmyBehavior.h
|
||||
Behaviors/ClusterBehavior.h
|
||||
Behaviors/StayAtTownBehavior.h
|
||||
Behaviors/ExplorationBehavior.h
|
||||
Helpers/ArmyFormation.h
|
||||
AIGateway.h
|
||||
)
|
||||
|
@ -19,8 +19,11 @@
|
||||
#include "../Behaviors/GatherArmyBehavior.h"
|
||||
#include "../Behaviors/ClusterBehavior.h"
|
||||
#include "../Behaviors/StayAtTownBehavior.h"
|
||||
#include "../Behaviors/ExplorationBehavior.h"
|
||||
#include "../Goals/Invalid.h"
|
||||
#include "../Goals/Composition.h"
|
||||
#include "../../../lib/CPlayerState.h"
|
||||
#include "../../lib/StartInfo.h"
|
||||
|
||||
namespace NKAI
|
||||
{
|
||||
@ -35,13 +38,45 @@ Nullkiller::Nullkiller()
|
||||
{
|
||||
memory = std::make_unique<AIMemory>();
|
||||
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)
|
||||
{
|
||||
this->cb = cb;
|
||||
this->gateway = gateway;
|
||||
this->playerID = gateway->playerID;
|
||||
|
||||
playerID = gateway->playerID;
|
||||
|
||||
if(openMap && !canUseOpenMap(cb, playerID))
|
||||
{
|
||||
useObjectGraph = false;
|
||||
openMap = false;
|
||||
}
|
||||
|
||||
baseGraph.reset();
|
||||
|
||||
@ -190,7 +225,7 @@ void Nullkiller::resetAiState()
|
||||
useHeroChain = true;
|
||||
objectClusterizer->reset();
|
||||
|
||||
if(!baseGraph && settings->isObjectGraphAllowed())
|
||||
if(!baseGraph && isObjectGraphAllowed())
|
||||
{
|
||||
baseGraph = std::make_unique<ObjectGraph>();
|
||||
baseGraph->updateGraph(this);
|
||||
@ -237,12 +272,12 @@ void Nullkiller::updateAiState(int pass, bool fast)
|
||||
cfg.useHeroChain = useHeroChain;
|
||||
cfg.allowBypassObjects = true;
|
||||
|
||||
if(scanDepth == ScanDepth::SMALL || settings->isObjectGraphAllowed())
|
||||
if(scanDepth == ScanDepth::SMALL || isObjectGraphAllowed())
|
||||
{
|
||||
cfg.mainTurnDistanceLimit = settings->getMainHeroTurnDistanceLimit();
|
||||
}
|
||||
|
||||
if(scanDepth != ScanDepth::ALL_FULL || settings->isObjectGraphAllowed())
|
||||
if(scanDepth != ScanDepth::ALL_FULL || isObjectGraphAllowed())
|
||||
{
|
||||
cfg.scoutTurnDistanceLimit =settings->getScoutHeroTurnDistanceLimit();
|
||||
}
|
||||
@ -251,7 +286,7 @@ void Nullkiller::updateAiState(int pass, bool fast)
|
||||
|
||||
pathfinder->updatePaths(activeHeroes, cfg);
|
||||
|
||||
if(settings->isObjectGraphAllowed())
|
||||
if(isObjectGraphAllowed())
|
||||
{
|
||||
pathfinder->updateGraphs(
|
||||
activeHeroes,
|
||||
@ -354,6 +389,9 @@ void Nullkiller::makeTurn()
|
||||
decompose(bestTasks, sptr(GatherArmyBehavior()), 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())
|
||||
{
|
||||
decompose(bestTasks, sptr(StartupBehavior()), 1);
|
||||
|
@ -76,6 +76,8 @@ private:
|
||||
TResources lockedResources;
|
||||
bool useHeroChain;
|
||||
AIGateway * gateway;
|
||||
bool openMap;
|
||||
bool useObjectGraph;
|
||||
|
||||
public:
|
||||
static std::unique_ptr<ObjectGraph> baseGraph;
|
||||
@ -116,6 +118,8 @@ public:
|
||||
void lockResources(const TResources & res);
|
||||
const TResources & getLockedResources() const { return lockedResources; }
|
||||
ScanDepth getScanDepth() const { return scanDepth; }
|
||||
bool isOpenMap() const { return openMap; }
|
||||
bool isObjectGraphAllowed() const { return useObjectGraph; }
|
||||
|
||||
private:
|
||||
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
|
||||
{
|
||||
public:
|
||||
@ -1056,6 +1070,7 @@ PriorityEvaluator::PriorityEvaluator(const Nullkiller * ai)
|
||||
evaluationContextBuilders.push_back(std::make_shared<ExchangeSwapTownHeroesContextBuilder>());
|
||||
evaluationContextBuilders.push_back(std::make_shared<DismissHeroContextBuilder>(ai));
|
||||
evaluationContextBuilders.push_back(std::make_shared<StayAtTownManaRecoveryEvaluator>());
|
||||
evaluationContextBuilders.push_back(std::make_shared<ExplorePointEvaluator>());
|
||||
}
|
||||
|
||||
EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal) const
|
||||
|
@ -28,8 +28,9 @@ namespace NKAI
|
||||
scoutHeroTurnDistanceLimit(5),
|
||||
maxGoldPressure(0.3f),
|
||||
maxpass(10),
|
||||
allowObjectGraph(false),
|
||||
useTroopsFromGarrisons(false)
|
||||
allowObjectGraph(true),
|
||||
useTroopsFromGarrisons(false),
|
||||
openMap(true)
|
||||
{
|
||||
JsonNode node = JsonUtils::assembleFromFiles("config/ai/nkai/nkai-settings");
|
||||
|
||||
@ -63,6 +64,11 @@ namespace NKAI
|
||||
allowObjectGraph = node.Struct()["allowObjectGraph"].Bool();
|
||||
}
|
||||
|
||||
if(!node.Struct()["openMap"].isNull())
|
||||
{
|
||||
openMap = node.Struct()["openMap"].Bool();
|
||||
}
|
||||
|
||||
if(!node.Struct()["useTroopsFromGarrisons"].isNull())
|
||||
{
|
||||
useTroopsFromGarrisons = node.Struct()["useTroopsFromGarrisons"].Bool();
|
||||
|
@ -28,6 +28,7 @@ namespace NKAI
|
||||
float maxGoldPressure;
|
||||
bool allowObjectGraph;
|
||||
bool useTroopsFromGarrisons;
|
||||
bool openMap;
|
||||
|
||||
public:
|
||||
Settings();
|
||||
@ -39,5 +40,6 @@ namespace NKAI
|
||||
int getScoutHeroTurnDistanceLimit() const { return scoutHeroTurnDistanceLimit; }
|
||||
bool isObjectGraphAllowed() const { return allowObjectGraph; }
|
||||
bool isGarrisonTroopsUsageAllowed() const { return useTroopsFromGarrisons; }
|
||||
bool isOpenMap() const { return openMap; }
|
||||
};
|
||||
}
|
||||
|
@ -73,7 +73,9 @@ namespace Goals
|
||||
CAPTURE_OBJECT,
|
||||
SAVE_RESOURCES,
|
||||
STAY_AT_TOWN_BEHAVIOR,
|
||||
STAY_AT_TOWN
|
||||
STAY_AT_TOWN,
|
||||
EXPLORATION_BEHAVIOR,
|
||||
EXPLORATION_POINT
|
||||
};
|
||||
|
||||
class DLL_EXPORT TSubgoal : public std::shared_ptr<AbstractGoal>
|
||||
|
@ -148,7 +148,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
|
||||
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;
|
||||
|
||||
|
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 "ObjectGraph.h"
|
||||
#include "GraphPaths.h"
|
||||
#include "../AIUtility.h"
|
||||
|
||||
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 "ObjectGraph.h"
|
||||
#include "ObjectGraphCalculator.h"
|
||||
#include "AIPathfinderConfig.h"
|
||||
#include "../../../lib/CRandomGenerator.h"
|
||||
#include "../../../CCallback.h"
|
||||
@ -22,392 +23,6 @@
|
||||
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(
|
||||
const int3 & from,
|
||||
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,
|
||||
"scoutHeroTurnDistanceLimit" : 5,
|
||||
"maxGoldPressure" : 0.3,
|
||||
"useTroopsFromGarrisons" : true
|
||||
"useTroopsFromGarrisons" : true,
|
||||
"openMap": true,
|
||||
"allowObjectGraph": true
|
||||
}
|
Loading…
Reference in New Issue
Block a user