Jelajahi Sumber

Nullkiller: object clusterizer

Andrii Danylchenko 4 tahun lalu
induk
melakukan
0705ee595a

+ 256 - 0
AI/Nullkiller/Analyzers/ObjectClusterizer.cpp

@@ -0,0 +1,256 @@
+/*
+* DangerHitMapAnalyzer.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 "ObjectClusterizer.h"
+#include "../Goals/ExecuteHeroChain.h"
+#include "../VCAI.h"
+#include "../Engine/Nullkiller.h"
+#include "lib/mapping/CMap.h" //for victory conditions
+
+extern boost::thread_specific_ptr<CCallback> cb;
+extern boost::thread_specific_ptr<VCAI> ai;
+
+void ObjectCluster::addObject(const CGObjectInstance * obj, const AIPath & path, float priority)
+{
+	auto & info = objects[obj];
+
+	if(info.priority < priority)
+	{
+		info.priority = priority;
+		info.movementCost = path.movementCost() - path.firstNode().cost;
+		info.danger = path.targetObjectDanger;
+		info.turn = path.turn();
+	}
+}
+
+const CGObjectInstance * ObjectCluster::calculateCenter() const
+{
+	auto v = getObjects();
+	auto tile = int3(0);
+	float priority = 0;
+
+	for(auto pair : objects)
+	{
+		auto newPoint = pair.first->visitablePos();
+		float newPriority = std::pow(pair.second.priority, 4); // lets make high priority targets more weghtful
+		int3 direction = newPoint - tile;
+		float priorityRatio = newPriority / (priority + newPriority);
+
+		tile += direction * priorityRatio;
+		priority += newPriority;
+	}
+
+	auto closestPair = *vstd::minElementByFun(objects, [&](const std::pair<const CGObjectInstance *, ObjectInfo> & pair) -> int
+	{
+		return pair.first->visitablePos().dist2dSQ(tile);
+	});
+
+	return closestPair.first;
+}
+
+std::vector<const CGObjectInstance *> ObjectCluster::getObjects() const
+{
+	std::vector<const CGObjectInstance *> result;
+
+	for(auto pair : objects)
+	{
+		result.push_back(pair.first);
+	}
+
+	return result;
+}
+
+std::vector<const CGObjectInstance *> ObjectClusterizer::getNearbyObjects() const
+{
+	return nearObjects.getObjects();
+}
+
+std::vector<std::shared_ptr<ObjectCluster>> ObjectClusterizer::getLockedClusters() const
+{
+	std::vector<std::shared_ptr<ObjectCluster>> result;
+
+	for(auto pair : blockedObjects)
+	{
+		result.push_back(pair.second);
+	}
+
+	return result;
+}
+
+const CGObjectInstance * ObjectClusterizer::getBlocker(const AIPath & path) const
+{
+	for(auto node = path.nodes.rbegin(); node != path.nodes.rend(); node++)
+	{
+		auto guardPos = cb->getGuardingCreaturePosition(node->coord);
+		auto blockers = cb->getVisitableObjs(node->coord);
+
+		if(guardPos.valid())
+		{
+			auto guard = cb->getTopObj(cb->getGuardingCreaturePosition(node->coord));
+
+			if(guard)
+			{
+				blockers.insert(blockers.begin(), guard);
+			}
+		}
+
+		if(blockers.empty())
+			continue;
+
+		auto blocker = blockers.front();
+
+		if(blocker->ID == Obj::GARRISON
+			|| blocker->ID == Obj::MONSTER
+			|| blocker->ID == Obj::GARRISON2
+			|| blocker->ID == Obj::BORDERGUARD
+			|| blocker->ID == Obj::QUEST_GUARD
+			|| blocker->ID == Obj::BORDER_GATE)
+		{
+			return blocker;
+		}
+	}
+
+	return nullptr;
+}
+
+bool shouldVisitObject(const CGObjectInstance * obj)
+{
+	if(isObjectRemovable(obj))
+	{
+		return true;
+	}
+
+	const int3 pos = obj->visitablePos();
+
+	if(obj->ID != Obj::CREATURE_GENERATOR1 && vstd::contains(ai->alreadyVisited, obj)
+		|| obj->wasVisited(ai->playerID))
+	{
+		return false;
+	}
+
+	auto playerRelations = cb->getPlayerRelations(ai->playerID, obj->tempOwner);
+
+	if(playerRelations != PlayerRelations::ENEMIES && !isWeeklyRevisitable(obj))
+	{
+		return false;
+	}
+
+	//it may be hero visiting this obj
+	//we don't try visiting object on which allied or owned hero stands
+	// -> it will just trigger exchange windows and AI will be confused that obj behind doesn't get visited
+	const CGObjectInstance * topObj = cb->getTopObj(pos);
+
+	if(!topObj)
+		return false; // partly visible obj but its visitable pos is not visible.
+
+	if(topObj->ID == Obj::HERO && cb->getPlayerRelations(ai->playerID, topObj->tempOwner) != PlayerRelations::ENEMIES)
+		return false;
+	else
+		return true; //all of the following is met
+}
+
+void ObjectClusterizer::clusterize()
+{
+	nearObjects.reset();
+	farObjects.reset();
+	blockedObjects.clear();
+
+	Obj ignoreObjects[] = {
+		Obj::MONSTER,
+		Obj::SIGN,
+		Obj::REDWOOD_OBSERVATORY,
+		Obj::MONOLITH_TWO_WAY,
+		Obj::MONOLITH_ONE_WAY_ENTRANCE,
+		Obj::MONOLITH_ONE_WAY_EXIT,
+		Obj::BUOY
+	};
+
+	logAi->debug("Begin object clusterization");
+
+	for(const CGObjectInstance * obj : ai->visitableObjs)
+	{
+		if(!shouldVisitObject(obj))
+			continue;
+
+		auto paths = ai->ah->getPathsToTile(obj->visitablePos());
+
+		if(paths.empty())
+			continue;
+
+		std::sort(paths.begin(), paths.end(), [](const AIPath & p1, const AIPath & p2) -> bool
+		{
+			return p1.movementCost() < p2.movementCost();
+		});
+
+		if(vstd::contains(ignoreObjects, obj->ID))
+		{
+			farObjects.addObject(obj, paths.front(), 0);
+
+			continue;
+		}
+		
+		bool added = false;
+		bool directlyAccessible = false;
+
+		for(auto & path : paths)
+		{
+			if(path.nodes.size() > 1)
+			{
+				auto blocker = getBlocker(path);
+
+				if(blocker)
+				{
+					auto cluster = blockedObjects[blocker];
+
+					if(!cluster)
+					{
+						cluster.reset(new ObjectCluster(blocker));
+						blockedObjects[blocker] = cluster;
+					}
+
+					if(!vstd::contains(cluster->objects, obj))
+					{
+						float priority = ai->nullkiller->priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)));
+
+						cluster->addObject(obj, path, priority);
+
+						added = true;
+					}
+				}
+			}
+			else
+			{
+				directlyAccessible = true;
+			}
+		}
+
+		if(!added || directlyAccessible)
+		{
+			if(paths.front().turn() <= 2)
+				nearObjects.addObject(obj, paths.front(), 0);
+			else
+				farObjects.addObject(obj, paths.front(), 0);
+		}
+	}
+
+	logAi->trace("Near objects count: %i", nearObjects.objects.size());
+	logAi->trace("Far objects count: %i", farObjects.objects.size());
+	for(auto pair : blockedObjects)
+	{
+		logAi->trace("Cluster %s %s count: %i", pair.first->getObjectName(), pair.first->visitablePos().toString(), pair.second->objects.size());
+
+#if AI_TRACE_LEVEL >= 1
+		for(auto obj : pair.second->getObjects())
+		{
+			logAi->trace("Object %s %s", obj->getObjectName(), obj->visitablePos().toString());
+		}
+#endif
+	}
+}

+ 65 - 0
AI/Nullkiller/Analyzers/ObjectClusterizer.h

@@ -0,0 +1,65 @@
+/*
+* DangerHitMapAnalyzer.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 "../Pathfinding/AINodeStorage.h"
+
+struct ObjectInfo
+{
+	float priority;
+	float movementCost;
+	uint64_t danger;
+	uint8_t turn;
+};
+
+struct ObjectCluster
+{
+public:
+	std::map<const CGObjectInstance *, ObjectInfo> objects;
+	const CGObjectInstance * blocker;
+
+	void reset()
+	{
+		objects.clear();
+	}
+
+	void addObject(const CGObjectInstance * object, const AIPath & path, float priority);
+	
+	ObjectCluster(const CGObjectInstance * blocker)
+		:objects(), blocker(blocker)
+	{
+	}
+
+	ObjectCluster() : ObjectCluster(nullptr)
+	{
+	}
+
+	std::vector<const CGObjectInstance *> getObjects() const;
+	const CGObjectInstance * calculateCenter() const;
+};
+
+class ObjectClusterizer
+{
+private:
+	ObjectCluster nearObjects;
+	ObjectCluster farObjects;
+	std::map<const CGObjectInstance *, std::shared_ptr<ObjectCluster>> blockedObjects;
+
+public:
+	void clusterize();
+	std::vector<const CGObjectInstance *> getNearbyObjects() const;
+	std::vector<std::shared_ptr<ObjectCluster>> getLockedClusters() const;
+	const CGObjectInstance * getBlocker(const AIPath & path) const;
+
+	ObjectClusterizer()
+		:nearObjects(), farObjects(), blockedObjects()
+	{
+	}
+};

+ 104 - 119
AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp

@@ -50,179 +50,164 @@ bool CaptureObjectsBehavior::operator==(const CaptureObjectsBehavior & other) co
 		&& vectorEquals(objectSubTypes, other.objectSubTypes);
 }
 
-Goals::TGoalVec CaptureObjectsBehavior::decompose() const
+Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath> & paths, const CGObjectInstance * objToVisit)
 {
 	Goals::TGoalVec tasks;
 
-	auto captureObjects = [&](const std::vector<const CGObjectInstance*> & objs) -> void{
-		if(objs.empty())
-		{
-			return;
-		}
-
-		logAi->debug("Scanning objects, count %d", objs.size());
-
-		for(auto objToVisit : objs)
-		{			
-#if AI_TRACE_LEVEL >= 1
-			logAi->trace("Checking object %s, %s", objToVisit->getObjectName(), objToVisit->visitablePos().toString());
-#endif
-
-			if(!shouldVisitObject(objToVisit))
-				continue;
+	tasks.reserve(paths.size());
 
-			const int3 pos = objToVisit->visitablePos();
+	const AIPath * closestWay = nullptr;
+	std::vector<ExecuteHeroChain *> waysToVisitObj;
 
-			auto paths = ai->ah->getPathsToTile(pos);
-			std::vector<std::shared_ptr<ExecuteHeroChain>> waysToVisitObj;
-			std::shared_ptr<ExecuteHeroChain> closestWay;
-					
-#if AI_TRACE_LEVEL >= 1
-			logAi->trace("Found %d paths", paths.size());
-#endif
+	for(auto & path : paths)
+	{
+		tasks.push_back(sptr(Goals::Invalid()));
 
-			for(auto & path : paths)
-			{
 #if AI_TRACE_LEVEL >= 2
-				logAi->trace("Path found %s", path.toString());
+		logAi->trace("Path found %s", path.toString());
 #endif
 
-				if(ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
-				{
+		if(ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
+		{
 #if AI_TRACE_LEVEL >= 2
-					logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength());
+			logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength());
 #endif
-					continue;
-				}
+			continue;
+		}
 
-				if(!shouldVisit(path.targetHero, objToVisit))
-					continue;
+		if(objToVisit && !shouldVisit(path.targetHero, objToVisit))
+			continue;
 
-				auto hero = path.targetHero;
-				auto danger = path.getTotalDanger();
+		auto hero = path.targetHero;
+		auto danger = path.getTotalDanger();
 
-				if(ai->ah->getHeroRole(hero) == HeroRole::SCOUT && danger == 0 && path.exchangeCount > 1)
-					continue;
+		if(ai->ah->getHeroRole(hero) == HeroRole::SCOUT && danger == 0 && path.exchangeCount > 1)
+			continue;
 
-				auto firstBlockedAction = path.getFirstBlockedAction();
-				if(firstBlockedAction)
-				{
-					auto subGoal = firstBlockedAction->decompose(path.targetHero);
+		auto firstBlockedAction = path.getFirstBlockedAction();
+		if(firstBlockedAction)
+		{
+			auto subGoal = firstBlockedAction->decompose(path.targetHero);
 
 #if AI_TRACE_LEVEL >= 2
-					logAi->trace("Decomposing special action %s returns %s", firstBlockedAction->toString(), subGoal->toString());
+			logAi->trace("Decomposing special action %s returns %s", firstBlockedAction->toString(), subGoal->toString());
 #endif
 
-					if(!subGoal->invalid())
-					{
-						Composition composition;
+			if(!subGoal->invalid())
+			{
+				Composition composition;
+
+				composition.addNext(ExecuteHeroChain(path, objToVisit));
+				composition.addNext(subGoal);
 
-						composition.addNext(ExecuteHeroChain(path, objToVisit));
-						composition.addNext(subGoal);
+				tasks[tasks.size() - 1] = sptr(composition);
+			}
 
-						tasks.push_back(sptr(composition));
-					}
+			continue;
+		}
 
-					continue;
-				}
+		auto isSafe = isSafeToVisit(hero, path.heroArmy, danger);
 
-				auto isSafe = isSafeToVisit(hero, path.heroArmy, danger);
-				
 #if AI_TRACE_LEVEL >= 2
-				logAi->trace(
-					"It is %s to visit %s by %s with army %lld, danger %lld and army loss %lld", 
-					isSafe ? "safe" : "not safe",
-					objToVisit->getObjectName(), 
-					hero->name,
-					path.getHeroStrength(),
-					danger,
-					path.getTotalArmyLoss());
+		logAi->trace(
+			"It is %s to visit %s by %s with army %lld, danger %lld and army loss %lld",
+			isSafe ? "safe" : "not safe",
+			objToVisit ? objToVisit->getObjectName() : path.targetTile().toString(),
+			hero->name,
+			path.getHeroStrength(),
+			danger,
+			path.getTotalArmyLoss());
 #endif
 
-				if(isSafe)
-				{
-					auto newWay = std::make_shared<ExecuteHeroChain>(path, objToVisit);
-
-					waysToVisitObj.push_back(newWay);
+		if(isSafe)
+		{
+			auto newWay = new ExecuteHeroChain(path, objToVisit);
+			TSubgoal sharedPtr;
 
-					if(!closestWay || closestWay->getPath().movementCost() > newWay->getPath().movementCost())
-						closestWay = newWay;
-				}
-			}
+			sharedPtr.reset(newWay);
 
-			if(waysToVisitObj.empty())
-				continue;
+			if(!closestWay || closestWay->movementCost() > path.movementCost())
+				closestWay = &path;
 
-			for(auto way : waysToVisitObj)
+			if(!ai->nullkiller->arePathHeroesLocked(path))
 			{
-				if(ai->nullkiller->arePathHeroesLocked(way->getPath()))
-					continue;
-
-				way->closestWayRatio
-					= closestWay->getPath().movementCost() / way->getPath().movementCost();
-
-				tasks.push_back(sptr(*way));
+				waysToVisitObj.push_back(newWay);
+				tasks[tasks.size() - 1] = sharedPtr;
 			}
 		}
-	};
-
-	if(specificObjects)
-	{
-		captureObjects(objectsToCapture);
 	}
-	else
+
+	for(auto way : waysToVisitObj)
 	{
-		captureObjects(std::vector<const CGObjectInstance*>(ai->visitableObjs.begin(), ai->visitableObjs.end()));
+		way->closestWayRatio
+			= closestWay->movementCost() / way->getPath().movementCost();
 	}
 
 	return tasks;
 }
 
-bool CaptureObjectsBehavior::shouldVisitObject(ObjectIdRef obj) const
+Goals::TGoalVec CaptureObjectsBehavior::decompose() const
 {
-	const CGObjectInstance* objInstance = obj;
-	if(!objInstance)
-		return false;
+	Goals::TGoalVec tasks;
 
-	if(objectTypes.size() && !vstd::contains(objectTypes, objInstance->ID.num))
-	{
-		return false;
-	}
+	auto captureObjects = [&](const std::vector<const CGObjectInstance*> & objs) -> void{
+		if(objs.empty())
+		{
+			return;
+		}
+
+		logAi->debug("Scanning objects, count %d", objs.size());
 
-	if(objectSubTypes.size() && !vstd::contains(objectSubTypes, objInstance->subID))
+		for(auto objToVisit : objs)
+		{			
+#if AI_TRACE_LEVEL >= 1
+			logAi->trace("Checking object %s, %s", objToVisit->getObjectName(), objToVisit->visitablePos().toString());
+#endif
+
+			if(!shouldVisitObject(objToVisit))
+				continue;
+
+			const int3 pos = objToVisit->visitablePos();
+
+			auto paths = ai->ah->getPathsToTile(pos);
+			std::vector<std::shared_ptr<ExecuteHeroChain>> waysToVisitObj;
+			std::shared_ptr<ExecuteHeroChain> closestWay;
+					
+#if AI_TRACE_LEVEL >= 1
+			logAi->trace("Found %d paths", paths.size());
+#endif
+			vstd::concatenate(tasks, getVisitGoals(paths, objToVisit));
+		}
+	};
+
+	if(specificObjects)
 	{
-		return false;
+		captureObjects(objectsToCapture);
 	}
-	
-	if(isObjectRemovable(obj))
+	else
 	{
-		return true;
+		captureObjects(ai->nullkiller->objectClusterizer->getNearbyObjects());
 	}
 
-	const int3 pos = objInstance->visitablePos();
+	vstd::erase_if(tasks, [](TSubgoal task) -> bool
+	{
+		return task->invalid();
+	});
+
+	return tasks;
+}
 
-	if(objInstance->ID != Obj::CREATURE_GENERATOR1 && vstd::contains(ai->alreadyVisited, objInstance)
-		|| obj->wasVisited(ai->playerID))
+bool CaptureObjectsBehavior::shouldVisitObject(const CGObjectInstance * obj) const
+{
+	if(objectTypes.size() && !vstd::contains(objectTypes, obj->ID.num))
 	{
 		return false;
 	}
 
-	auto playerRelations = cb->getPlayerRelations(ai->playerID, objInstance->tempOwner);
-	if(playerRelations != PlayerRelations::ENEMIES && !isWeeklyRevisitable(objInstance))
+	if(objectSubTypes.size() && !vstd::contains(objectSubTypes, obj->subID))
 	{
 		return false;
 	}
 
-	//it may be hero visiting this obj
-	//we don't try visiting object on which allied or owned hero stands
-	// -> it will just trigger exchange windows and AI will be confused that obj behind doesn't get visited
-	const CGObjectInstance * topObj = cb->getTopObj(pos);
-
-	if(!topObj)
-		return false; // partly visible obj but its visitable pos is not visible.
-
-	if(topObj->ID == Obj::HERO && cb->getPlayerRelations(ai->playerID, topObj->tempOwner) != PlayerRelations::ENEMIES)
-		return false;
-	else
-		return true; //all of the following is met
+	return true;
 }

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

@@ -64,8 +64,10 @@ namespace Goals
 
 		virtual bool operator==(const CaptureObjectsBehavior & other) const override;
 
+		static Goals::TGoalVec getVisitGoals(const std::vector<AIPath> & paths, const CGObjectInstance * objToVisit = nullptr);
+
 	private:
-		bool shouldVisitObject(ObjectIdRef obj) const;
+		bool shouldVisitObject(const CGObjectInstance * obj) const;
 	};
 }
 

+ 113 - 0
AI/Nullkiller/Behaviors/ClusterBehavior.cpp

@@ -0,0 +1,113 @@
+/*
+* RecruitHeroBehavior.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 "ClusterBehavior.h"
+#include "../VCAI.h"
+#include "../Engine/Nullkiller.h"
+#include "../AIhelper.h"
+#include "../AIUtility.h"
+#include "../Goals/UnlockCluster.h"
+#include "../Goals/Composition.h"
+#include "../Behaviors/CaptureObjectsBehavior.h"
+
+extern boost::thread_specific_ptr<CCallback> cb;
+extern boost::thread_specific_ptr<VCAI> ai;
+extern FuzzyHelper * fh;
+
+using namespace Goals;
+
+std::string ClusterBehavior::toString() const
+{
+	return "Unlock Clusters";
+}
+
+Goals::TGoalVec ClusterBehavior::decompose() const
+{
+	Goals::TGoalVec tasks;
+	auto clusters = ai->nullkiller->objectClusterizer->getLockedClusters();
+
+	for(auto cluster : clusters)
+	{
+		vstd::concatenate(tasks, decomposeCluster(cluster));
+	}
+
+	return tasks;
+}
+
+Goals::TGoalVec ClusterBehavior::decomposeCluster(std::shared_ptr<ObjectCluster> cluster) const
+{
+	auto center = cluster->calculateCenter();
+	auto paths = ai->ah->getPathsToTile(center->visitablePos());
+	auto blockerPos = cluster->blocker->visitablePos();
+	std::vector<AIPath> blockerPaths;
+
+	blockerPaths.reserve(paths.size());
+
+	TGoalVec goals;
+
+#if AI_TRACE_LEVEL >= 2
+	logAi->trace(
+		"Checking cluster %s, found %d paths",
+		cluster->blocker->getObjectName(),
+		cluster->blocker->visitablePos().toString(),
+		paths.size());
+#endif
+
+	for(auto path = paths.begin(); path != paths.end();)
+	{
+#if AI_TRACE_LEVEL >= 2
+		logAi->trace("Checking path %s", path->toString());
+#endif
+
+		auto blocker = ai->nullkiller->objectClusterizer->getBlocker(*path);
+
+		if(blocker != cluster->blocker)
+		{
+			path = paths.erase(path);
+			continue;
+		}
+
+		blockerPaths.push_back(*path);
+
+		blockerPaths.back().nodes.clear();
+
+		for(auto node = path->nodes.rbegin(); node != path->nodes.rend(); node++)
+		{
+			blockerPaths.back().nodes.insert(blockerPaths.back().nodes.begin(), *node);
+
+			if(node->coord == blockerPos)
+				break;
+		}
+
+#if AI_TRACE_LEVEL >= 2
+		logAi->trace("Unlock path found %s", blockerPaths.back().toString());
+#endif
+		path++;
+	}
+
+#if AI_TRACE_LEVEL >= 2
+	logAi->trace("Decompose unlock paths");
+#endif
+
+	auto unlockTasks = CaptureObjectsBehavior::getVisitGoals(blockerPaths);
+
+	for(int i = 0; i < paths.size(); i++)
+	{
+		if(unlockTasks[i]->invalid())
+			continue;
+
+		auto path = paths[i];
+		auto elementarUnlock = sptr(UnlockCluster(cluster, path));
+
+		goals.push_back(sptr(Composition().addNext(elementarUnlock).addNext(unlockTasks[i])));
+	}
+
+	return goals;
+}

+ 39 - 0
AI/Nullkiller/Behaviors/ClusterBehavior.h

@@ -0,0 +1,39 @@
+/*
+* RecruitHeroBehavior.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"
+
+struct ObjectCluster;
+
+namespace Goals
+{
+	class ClusterBehavior : public CGoal<ClusterBehavior>
+	{
+	public:
+		ClusterBehavior()
+			:CGoal(CLUSTER_BEHAVIOR)
+		{
+		}
+
+		virtual TGoalVec decompose() const override;
+		virtual std::string toString() const override;
+
+		virtual bool operator==(const ClusterBehavior & other) const override
+		{
+			return true;
+		}
+
+	private:
+		Goals::TGoalVec decomposeCluster(std::shared_ptr<ObjectCluster> cluster) const;
+	};
+}

+ 6 - 0
AI/Nullkiller/CMakeLists.txt

@@ -32,6 +32,7 @@ set(VCAI_SRCS
 		Goals/CollectRes.cpp
 		Goals/Trade.cpp
 		Goals/RecruitHero.cpp
+		Goals/UnlockCluster.cpp
 		Goals/DigAtTile.cpp
 		Goals/GetArtOfType.cpp
 		Goals/ExecuteHeroChain.cpp
@@ -40,6 +41,7 @@ set(VCAI_SRCS
 		Engine/PriorityEvaluator.cpp
 		Analyzers/DangerHitMapAnalyzer.cpp
 		Analyzers/BuildAnalyzer.cpp
+		Analyzers/ObjectClusterizer.cpp
 		Behaviors/CaptureObjectsBehavior.cpp
 		Behaviors/RecruitHeroBehavior.cpp
 		Behaviors/BuyArmyBehavior.cpp
@@ -48,6 +50,7 @@ set(VCAI_SRCS
 		Behaviors/BuildingBehavior.cpp
 		Behaviors/GatherArmyBehavior.cpp
 		Behaviors/CompleteQuestBehavior.cpp
+		Behaviors/ClusterBehavior.cpp
 		main.cpp
 		VCAI.cpp
 )
@@ -87,6 +90,7 @@ set(VCAI_HEADERS
 		Goals/AdventureSpellCast.h
 		Goals/CollectRes.h
 		Goals/Trade.h
+		Goals/UnlockCluster.h
 		Goals/RecruitHero.h
 		Goals/DigAtTile.h
 		Goals/GetArtOfType.h
@@ -97,6 +101,7 @@ set(VCAI_HEADERS
 		Engine/PriorityEvaluator.h
 		Analyzers/DangerHitMapAnalyzer.h
 		Analyzers/BuildAnalyzer.h
+		Analyzers/ObjectClusterizer.h
 		Behaviors/CaptureObjectsBehavior.h
 		Behaviors/RecruitHeroBehavior.h
 		Behaviors/BuyArmyBehavior.h
@@ -105,6 +110,7 @@ set(VCAI_HEADERS
 		Behaviors/BuildingBehavior.h
 		Behaviors/GatherArmyBehavior.h
 		Behaviors/CompleteQuestBehavior.h
+		Behaviors/ClusterBehavior.h
 		VCAI.h
 )
 

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

@@ -18,6 +18,7 @@
 #include "../Behaviors/DefenceBehavior.h"
 #include "../Behaviors/BuildingBehavior.h"
 #include "../Behaviors/GatherArmyBehavior.h"
+#include "../Behaviors/ClusterBehavior.h"
 #include "../Goals/Invalid.h"
 
 extern boost::thread_specific_ptr<CCallback> cb;
@@ -30,6 +31,7 @@ Nullkiller::Nullkiller()
 	priorityEvaluator.reset(new PriorityEvaluator());
 	dangerHitMap.reset(new DangerHitMapAnalyzer());
 	buildAnalyzer.reset(new BuildAnalyzer());
+	objectClusterizer.reset(new ObjectClusterizer());
 }
 
 Goals::TTask Nullkiller::choseBestTask(Goals::TTaskVec & tasks) const
@@ -45,7 +47,7 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TSubgoal behavior) const
 {
 	logAi->debug("Checking behavior %s", behavior->toString());
 
-	const int MAX_DEPTH = 0;
+	const int MAX_DEPTH = 10;
 	Goals::TGoalVec goals[MAX_DEPTH + 1];
 	Goals::TTaskVec tasks;
 	std::map<Goals::TSubgoal, Goals::TSubgoal> decompositionMap;
@@ -152,6 +154,7 @@ void Nullkiller::updateAiState()
 	ai->ah->updatePaths(activeHeroes, true);
 	ai->ah->update();
 
+	objectClusterizer->clusterize();
 	buildAnalyzer->update();
 }
 
@@ -204,6 +207,7 @@ void Nullkiller::makeTurn()
 		Goals::TTaskVec bestTasks = {
 			choseBestTask(sptr(BuyArmyBehavior())),
 			choseBestTask(sptr(CaptureObjectsBehavior())),
+			choseBestTask(sptr(ClusterBehavior())),
 			choseBestTask(sptr(RecruitHeroBehavior())),
 			choseBestTask(sptr(DefenceBehavior())),
 			choseBestTask(sptr(BuildingBehavior())),

+ 3 - 1
AI/Nullkiller/Engine/Nullkiller.h

@@ -12,6 +12,7 @@
 #include "PriorityEvaluator.h"
 #include "../Analyzers/DangerHitMapAnalyzer.h"
 #include "../Analyzers/BuildAnalyzer.h"
+#include "../Analyzers/ObjectClusterizer.h"
 #include "../Goals/AbstractGoal.h"
 
 const float MAX_GOLD_PEASURE = 0.3f;
@@ -31,7 +32,6 @@ enum class HeroLockedReason
 class Nullkiller
 {
 private:
-	std::unique_ptr<PriorityEvaluator> priorityEvaluator;
 	const CGHeroInstance * activeHero;
 	int3 targetTile;
 	std::map<const CGHeroInstance *, HeroLockedReason> lockedHeroes;
@@ -39,6 +39,8 @@ private:
 public:
 	std::unique_ptr<DangerHitMapAnalyzer> dangerHitMap;
 	std::unique_ptr<BuildAnalyzer> buildAnalyzer;
+	std::unique_ptr<ObjectClusterizer> objectClusterizer;
+	std::unique_ptr<PriorityEvaluator> priorityEvaluator;
 
 	Nullkiller();
 	void makeTurn();

+ 65 - 6
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -23,6 +23,7 @@
 #include "../AIhelper.h"
 #include "../Engine/Nullkiller.h"
 #include "../Goals/ExecuteHeroChain.h"
+#include "../Goals/UnlockCluster.h"
 
 #define MIN_AI_STRENGHT (0.5f) //lower when combat AI gets smarter
 #define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us
@@ -459,11 +460,18 @@ public:
 		evaluationContext.movementCost += path.movementCost();
 		evaluationContext.closestWayRatio = chain.closestWayRatio;
 
+		std::map<const CGHeroInstance *, float> costsPerHero;
+
 		for(auto & node : path.nodes)
 		{
-			auto role = ai->ah->getHeroRole(node.targetHero);
+			vstd::amax(costsPerHero[node.targetHero], node.cost);
+		}
+
+		for(auto pair : costsPerHero)
+		{
+			auto role = ai->ah->getHeroRole(pair.first);
 
-			evaluationContext.movementCostByRole[role] += node.cost;
+			evaluationContext.movementCostByRole[role] += pair.second;
 		}
 
 		auto heroPtr = task->hero;
@@ -484,6 +492,56 @@ public:
 	}
 };
 
+class ClusterEvaluationContextBuilder : public IEvaluationContextBuilder
+{
+public:
+	virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
+	{
+		if(task->goalType != Goals::UNLOCK_CLUSTER)
+			return;
+
+		Goals::UnlockCluster & clusterGoal = dynamic_cast<Goals::UnlockCluster &>(*task);
+		std::shared_ptr<ObjectCluster> cluster = clusterGoal.getCluster();
+
+		auto hero = clusterGoal.hero.get();
+		auto role = ai->ah->getHeroRole(clusterGoal.hero);
+
+		std::vector<std::pair<const CGObjectInstance *, ObjectInfo>> objects(cluster->objects.begin(), cluster->objects.end());
+
+		std::sort(objects.begin(), objects.end(), [](std::pair<const CGObjectInstance *, ObjectInfo> o1, std::pair<const CGObjectInstance *, ObjectInfo> o2) -> bool
+		{
+			return o1.second.priority > o2.second.priority;
+		});
+
+		int boost = 1;
+
+		for(auto objInfo : objects)
+		{
+			auto target = objInfo.first;
+			auto day = cb->getDate(Date::DAY);
+			bool checkGold = objInfo.second.danger == 0;
+			auto army = hero;
+
+			evaluationContext.goldReward += getGoldReward(target, hero) / boost;
+			evaluationContext.armyReward += getArmyReward(target, hero, army, checkGold) / boost;
+			evaluationContext.skillReward += getSkillReward(target, hero, role) / boost;
+			evaluationContext.strategicalValue += getStrategicalValue(target) / boost;
+			evaluationContext.goldCost += getGoldCost(target, hero, army) / boost;
+			evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost;
+			evaluationContext.movementCost += objInfo.second.movementCost / boost;
+
+			boost <<= 1;
+
+			if(boost > 8)
+				break;
+		}
+
+		const AIPath & pathToCenter = clusterGoal.getPathToCenter();
+
+		vstd::amax(evaluationContext.turn, pathToCenter.turn());
+	}
+};
+
 class BuildThisEvaluationContextBuilder : public IEvaluationContextBuilder
 {
 public:
@@ -529,6 +587,7 @@ PriorityEvaluator::PriorityEvaluator()
 	initVisitTile();
 	evaluationContextBuilders.push_back(std::make_shared<ExecuteHeroChainEvaluationContextBuilder>());
 	evaluationContextBuilders.push_back(std::make_shared<BuildThisEvaluationContextBuilder>());
+	evaluationContextBuilders.push_back(std::make_shared<ClusterEvaluationContextBuilder>());
 }
 
 EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal) const
@@ -545,14 +604,14 @@ EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal
 		parts.push_back(goal);
 	}
 
-	for(auto goal : parts)
+	for(auto subgoal : parts)
 	{
-		context.strategicalValue += goal->strategicalValue;
-		context.goldCost += goal->goldCost;
+		context.strategicalValue += subgoal->strategicalValue;
+		context.goldCost += subgoal->goldCost;
 
 		for(auto builder : evaluationContextBuilders)
 		{
-			builder->buildEvaluationContext(context, goal);
+			builder->buildEvaluationContext(context, subgoal);
 		}
 	}
 

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

@@ -61,7 +61,9 @@ namespace Goals
 		EXECUTE_HERO_CHAIN,
 		EXCHANGE_SWAP_TOWN_HEROES,
 		DISMISS_HERO,
-		COMPOSITION
+		COMPOSITION,
+		CLUSTER_BEHAVIOR,
+		UNLOCK_CLUSTER
 	};
 
 	class DLL_EXPORT TSubgoal : public std::shared_ptr<AbstractGoal>

+ 8 - 3
AI/Nullkiller/Goals/Composition.cpp

@@ -70,16 +70,16 @@ TGoalVec Composition::decompose() const
 			Composition & other = dynamic_cast<Composition &>(*goal);
 			bool cancel = false;
 
-			for(auto goal : other.subtasks)
+			for(auto subgoal : other.subtasks)
 			{
-				if(goal == last || vstd::contains(tasks, goal))
+				if(subgoal == last || vstd::contains(tasks, subgoal))
 				{
 					cancel = true;
 
 					break;
 				}
 
-				newComposition.addNext(goal);
+				newComposition.addNext(subgoal);
 			}
 
 			if(cancel)
@@ -90,6 +90,11 @@ TGoalVec Composition::decompose() const
 			newComposition.addNext(goal);
 		}
 
+		if(newComposition.isElementar() && !newComposition.hero.validAndSet())
+		{
+			logAi->error("Bad");
+		}
+
 		result.push_back(sptr(newComposition));
 	}
 

+ 0 - 53
AI/Nullkiller/Goals/FindObj.cpp

@@ -1,53 +0,0 @@
-/*
-* FindObj.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 "FindObj.h"
-#include "../VCAI.h"
-#include "../AIUtility.h"
-
-extern boost::thread_specific_ptr<CCallback> cb;
-extern boost::thread_specific_ptr<VCAI> ai;
-extern FuzzyHelper * fh;
-
-using namespace Goals;
-
-bool FindObj::operator==(const FindObj & other) const
-{
-	return other.hero.h == hero.h && other.objid == objid;
-}
-//
-//TSubgoal FindObj::whatToDoToAchieve()
-//{
-//	const CGObjectInstance * o = nullptr;
-//	if(resID > -1) //specified
-//	{
-//		for(const CGObjectInstance * obj : ai->visitableObjs)
-//		{
-//			if(obj->ID == objid && obj->subID == resID)
-//			{
-//				o = obj;
-//				break; //TODO: consider multiple objects and choose best
-//			}
-//		}
-//	}
-//	else
-//	{
-//		for(const CGObjectInstance * obj : ai->visitableObjs)
-//		{
-//			if(obj->ID == objid)
-//			{
-//				o = obj;
-//				break; //TODO: consider multiple objects and choose best
-//			}
-//		}
-//	}
-//	if(o && ai->isAccessible(o->pos)) //we don't use isAccessibleForHero as we don't know which hero it is
-//		return sptr(VisitObj(o->id.getNum()));
-//}

+ 0 - 14
AI/Nullkiller/Goals/FindObj.h

@@ -1,14 +0,0 @@
-/*
-* FindObj.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 "CGoal.h"
-
-#error not supported

+ 30 - 0
AI/Nullkiller/Goals/UnlockCluster.cpp

@@ -0,0 +1,30 @@
+/*
+* FindObj.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 "UnlockCluster.h"
+#include "../VCAI.h"
+#include "../Engine/Nullkiller.h"
+#include "../AIUtility.h"
+
+extern boost::thread_specific_ptr<CCallback> cb;
+extern boost::thread_specific_ptr<VCAI> ai;
+extern FuzzyHelper * fh;
+
+using namespace Goals;
+
+bool UnlockCluster::operator==(const UnlockCluster & other) const
+{
+	return other.tile == tile;
+}
+
+std::string UnlockCluster::toString() const
+{
+	return "Unlock Cluster " + cluster->blocker->getObjectName() + tile.toString();
+}

+ 41 - 0
AI/Nullkiller/Goals/UnlockCluster.h

@@ -0,0 +1,41 @@
+/*
+* FindObj.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 "CGoal.h"
+#include "../Analyzers/ObjectClusterizer.h"
+
+
+struct HeroPtr;
+class VCAI;
+class FuzzyHelper;
+
+namespace Goals
+{
+	class DLL_EXPORT UnlockCluster : public CGoal<UnlockCluster>
+	{
+	private:
+		std::shared_ptr<ObjectCluster> cluster;
+		AIPath pathToCenter;
+
+	public:
+		UnlockCluster(std::shared_ptr<ObjectCluster> cluster, const AIPath & pathToCenter)
+			: CGoal(Goals::UNLOCK_CLUSTER), cluster(cluster), pathToCenter(pathToCenter)
+		{
+			tile = cluster->blocker->visitablePos();
+			sethero(pathToCenter.targetHero);
+		}
+
+		virtual bool operator==(const UnlockCluster & other) const override;
+		virtual std::string toString() const override;
+		std::shared_ptr<ObjectCluster> getCluster() const { return cluster; }
+		const AIPath & getPathToCenter() { return pathToCenter; }
+	};
+}