فهرست منبع

NKAI: port exploration from VCAI

Andrii Danylchenko 1 سال پیش
والد
کامیت
820f0e0c1a

+ 3 - 8
AI/Nullkiller/AIGateway.cpp

@@ -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();

+ 1 - 1
AI/Nullkiller/AIGateway.h

@@ -95,7 +95,7 @@ public:
 	std::unique_ptr<boost::thread> makingTurn;
 private:
 	boost::mutex turnInterruptionMutex;
-	bool announcedCheatingProblem;
+
 public:
 	ObjectInstanceID selectedObject;
 

+ 1 - 1
AI/Nullkiller/Analyzers/ObjectClusterizer.cpp

@@ -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());
 	}

+ 8 - 4
AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp

@@ -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);

+ 5 - 1
AI/Nullkiller/Behaviors/CaptureObjectsBehavior.h

@@ -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;

+ 1 - 1
AI/Nullkiller/Behaviors/ClusterBehavior.cpp

@@ -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 - 0
AI/Nullkiller/Behaviors/ExplorationBehavior.cpp

@@ -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 - 0
AI/Nullkiller/Behaviors/ExplorationBehavior.h

@@ -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;
+		}
+	};
+}
+
+}

+ 2 - 2
AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp

@@ -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;

+ 8 - 0
AI/Nullkiller/CMakeLists.txt

@@ -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
 )

+ 43 - 5
AI/Nullkiller/Engine/Nullkiller.cpp

@@ -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);

+ 4 - 0
AI/Nullkiller/Engine/Nullkiller.h

@@ -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();

+ 15 - 0
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -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

+ 8 - 2
AI/Nullkiller/Engine/Settings.cpp

@@ -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();

+ 2 - 0
AI/Nullkiller/Engine/Settings.h

@@ -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; }
 	};
 }

+ 3 - 1
AI/Nullkiller/Goals/AbstractGoal.h

@@ -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>

+ 1 - 1
AI/Nullkiller/Goals/ExecuteHeroChain.cpp

@@ -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 - 0
AI/Nullkiller/Markers/ExplorationPoint.cpp

@@ -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 - 0
AI/Nullkiller/Markers/ExplorationPoint.h

@@ -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;
+	};
+}
+
+}

+ 1 - 0
AI/Nullkiller/Pathfinding/AIPathfinder.h

@@ -12,6 +12,7 @@
 
 #include "AINodeStorage.h"
 #include "ObjectGraph.h"
+#include "GraphPaths.h"
 #include "../AIUtility.h"
 
 namespace NKAI

+ 393 - 0
AI/Nullkiller/Pathfinding/GraphPaths.cpp

@@ -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 - 0
AI/Nullkiller/Pathfinding/GraphPaths.h

@@ -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;
+	}
+};
+
+}

+ 1 - 754
AI/Nullkiller/Pathfinding/ObjectGraph.cpp

@@ -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, &currentNode](int3 n1, ObjectLink o1)
-				{
-					target->iterateConnections(n1, [&pos, &o1, &currentNode, &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;
-			}
-		}
-	}
-}
-
 }

+ 0 - 98
AI/Nullkiller/Pathfinding/ObjectGraph.h

@@ -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();
-	}
-};
-
-typedef std::unordered_map<int3, GraphPathNode[GrapthPathNodeType::LAST]> GraphNodeStorage;
-
-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 - 0
AI/Nullkiller/Pathfinding/ObjectGraphCalculator.cpp

@@ -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, &currentNode](int3 n1, ObjectLink o1)
+			{
+				target->iterateConnections(n1, [&pos, &o1, &currentNode, &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 - 0
AI/Nullkiller/Pathfinding/ObjectGraphCalculator.h

@@ -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;
+};
+
+}

+ 3 - 1
config/ai/nkai/nkai-settings.json

@@ -4,5 +4,7 @@
 	"mainHeroTurnDistanceLimit" : 10,
 	"scoutHeroTurnDistanceLimit" : 5,
 	"maxGoldPressure" : 0.3,
-	"useTroopsFromGarrisons" : true
+	"useTroopsFromGarrisons" : true,
+	"openMap": true,
+	"allowObjectGraph": true
 }