浏览代码

Nullkiller - rewrite decomposition, decomposition cache, morale management

Andrii Danylchenko 4 年之前
父节点
当前提交
e385c83a88

+ 81 - 20
AI/Nullkiller/Analyzers/ArmyManager.cpp

@@ -31,6 +31,11 @@ public:
 	}
 };
 
+uint64_t ArmyManager::howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const
+{
+	return howManyReinforcementsCanGet(hero, hero, source);
+}
+
 std::vector<SlotInfo> ArmyManager::getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const
 {
 	const CCreatureSet * armies[] = { target, source };
@@ -75,15 +80,84 @@ std::vector<SlotInfo>::iterator ArmyManager::getWeakestCreature(std::vector<Slot
 	return weakest;
 }
 
-std::vector<SlotInfo> ArmyManager::getBestArmy(const CCreatureSet * target, const CCreatureSet * source) const
+std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const
 {
-	auto resultingArmy = getSortedSlots(target, source);
+	auto sortedSlots = getSortedSlots(target, source);
+	std::map<TFaction, uint64_t> alignmentMap;
 
-	if(resultingArmy.size() > GameConstants::ARMY_SIZE)
+	for(auto & slot : sortedSlots)
 	{
-		resultingArmy.resize(GameConstants::ARMY_SIZE);
+		alignmentMap[slot.creature->faction] += slot.power;
+	}
+
+	std::set<TFaction> allowedFactions;
+	std::vector<SlotInfo> resultingArmy;
+	uint64_t armyValue = 0;
+
+	CArmedInstance newArmyInstance;
+	auto bonusModifiers = armyCarrier->getBonuses(Selector::type(Bonus::MORALE));
+
+	for(auto bonus : *bonusModifiers)
+	{
+		// army bonuses will change and object bonuses are temporary
+		if(bonus->source != Bonus::ARMY || bonus->source != Bonus::OBJECT)
+		{
+			newArmyInstance.addNewBonus(bonus);
+		}
+	}
+
+	while(allowedFactions.size() < alignmentMap.size())
+	{
+		auto strongestAlignment = vstd::maxElementByFun(alignmentMap, [&](std::pair<TFaction, uint64_t> pair) -> uint64_t
+		{
+			return vstd::contains(allowedFactions, pair.first) ? 0 : pair.second;
+		});
+
+		allowedFactions.insert(strongestAlignment->first);
+
+		std::vector<SlotInfo> newArmy;
+		uint64_t newValue = 0;
+		newArmyInstance.clear();
+
+		for(auto & slot : sortedSlots)
+		{
+			if(vstd::contains(allowedFactions, slot.creature->faction))
+			{
+				auto slotID = newArmyInstance.getSlotFor(slot.creature);
+
+				if(slotID.validSlot())
+				{
+					newArmyInstance.setCreature(slotID, slot.creature->idNumber, slot.count);
+					newArmy.push_back(slot);
+				}
+			}
+		}
+
+		for(auto & slot : newArmyInstance.Slots())
+		{
+			auto morale = slot.second->MoraleVal();
+			auto multiplier = 1.0f;
+
+			if(morale < 0)
+			{
+				multiplier += morale * 0.083f;
+			}
+
+			newValue += multiplier * slot.second->getPower();
+		}
+
+		if(armyValue >= newValue)
+		{
+			break;
+		}
+
+		resultingArmy = newArmy;
+		armyValue = newValue;
 	}
-	else if(source->needsLastStack())
+
+	if(resultingArmy.size() <= GameConstants::ARMY_SIZE
+		&& allowedFactions.size() == alignmentMap.size()
+		&& source->needsLastStack())
 	{
 		auto weakest = getWeakestCreature(resultingArmy);
 
@@ -101,19 +175,6 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const CCreatureSet * target, cons
 	return resultingArmy;
 }
 
-bool ArmyManager::canGetArmy(const CArmedInstance * target, const CArmedInstance * source) const
-{
-	//TODO: merge with pickBestCreatures
-	//if (ai->primaryHero().h == source)
-	if(target->tempOwner != source->tempOwner)
-	{
-		logAi->error("Why are we even considering exchange between heroes from different players?");
-		return false;
-	}
-
-	return 0 < howManyReinforcementsCanGet(target, source);
-}
-
 ui64 ArmyManager::howManyReinforcementsCanBuy(const CCreatureSet * h, const CGDwelling * t) const
 {
 	ui64 aivalue = 0;
@@ -162,9 +223,9 @@ std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(const CCreatureSet * her
 	return creaturesInDwellings;
 }
 
-ui64 ArmyManager::howManyReinforcementsCanGet(const CCreatureSet * target, const CCreatureSet * source) const
+ui64 ArmyManager::howManyReinforcementsCanGet(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const
 {
-	auto bestArmy = getBestArmy(target, source);
+	auto bestArmy = getBestArmy(armyCarrier, target, source);
 	uint64_t newArmy = 0;
 	uint64_t oldArmy = target->getArmyStrength();
 

+ 6 - 6
AI/Nullkiller/Analyzers/ArmyManager.h

@@ -42,10 +42,10 @@ class DLL_EXPORT IArmyManager //: public: IAbstractManager
 {
 public:
 	virtual void update() = 0;
-	virtual bool canGetArmy(const CArmedInstance * target, const CArmedInstance * source) const = 0;
 	virtual ui64 howManyReinforcementsCanBuy(const CCreatureSet * target, const CGDwelling * source) const = 0;
-	virtual ui64 howManyReinforcementsCanGet(const CCreatureSet * target, const CCreatureSet * source) const = 0;
-	virtual std::vector<SlotInfo> getBestArmy(const CCreatureSet * target, const CCreatureSet * source) const = 0;
+	virtual ui64 howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const = 0;
+	virtual ui64 howManyReinforcementsCanGet(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const = 0;
+	virtual std::vector<SlotInfo> getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const = 0;
 	virtual std::vector<SlotInfo>::iterator getWeakestCreature(std::vector<SlotInfo> & army) const = 0;
 	virtual std::vector<SlotInfo> getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const = 0;
 	virtual std::vector<creInfo> getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const = 0;
@@ -69,10 +69,10 @@ private:
 public:
 	ArmyManager(CPlayerSpecificInfoCallback * CB, const Nullkiller * ai): cb(CB), ai(ai) {}
 	void update() override;
-	bool canGetArmy(const CArmedInstance * target, const CArmedInstance * source) const override;
 	ui64 howManyReinforcementsCanBuy(const CCreatureSet * target, const CGDwelling * source) const override;
-	ui64 howManyReinforcementsCanGet(const CCreatureSet * target, const CCreatureSet * source) const override;
-	std::vector<SlotInfo> getBestArmy(const CCreatureSet * target, const CCreatureSet * source) const override;
+	ui64 howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const override;
+	ui64 howManyReinforcementsCanGet(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const override;
+	std::vector<SlotInfo> getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const override;
 	std::vector<SlotInfo>::iterator getWeakestCreature(std::vector<SlotInfo> & army) const override;
 	std::vector<SlotInfo> getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const override;
 	std::vector<creInfo> getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const override;

+ 7 - 0
AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp

@@ -185,6 +185,13 @@ Goals::TGoalVec CaptureObjectsBehavior::decompose() const
 	{
 		captureObjects(objectsToCapture);
 	}
+	else if(objectTypes.size())
+	{
+		captureObjects(
+			std::vector<const CGObjectInstance *>(
+				ai->nullkiller->memory->visitableObjs.begin(),
+				ai->nullkiller->memory->visitableObjs.end()));
+	}
 	else
 	{
 		captureObjects(ai->nullkiller->objectClusterizer->getNearbyObjects());

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

@@ -139,7 +139,7 @@ Goals::TGoalVec StartupBehavior::decompose() const
 	{
 		if(!startupTown->visitingHero)
 		{
-			if(ai->nullkiller->armyManager->howManyReinforcementsCanGet(startupTown->getUpperArmy(), closestHero) > 200)
+			if(ai->nullkiller->armyManager->howManyReinforcementsCanGet(startupTown->getUpperArmy(), startupTown->getUpperArmy(), closestHero) > 200)
 			{
 				auto paths = ai->nullkiller->pathfinder->getPathInfo(startupTown->visitablePos());
 

+ 2 - 0
AI/Nullkiller/CMakeLists.txt

@@ -36,6 +36,7 @@ set(VCAI_SRCS
 		Markers/HeroExchange.cpp
 		Markers/UnlockCluster.cpp
 		Engine/Nullkiller.cpp
+		Engine/DeepDecomposer.cpp
 		Engine/PriorityEvaluator.cpp
 		Analyzers/DangerHitMapAnalyzer.cpp
 		Analyzers/BuildAnalyzer.cpp
@@ -93,6 +94,7 @@ set(VCAI_HEADERS
 		Markers/HeroExchange.h
 		Markers/UnlockCluster.h
 		Engine/Nullkiller.h
+		Engine/DeepDecomposer.h
 		Engine/PriorityEvaluator.h
 		Analyzers/DangerHitMapAnalyzer.h
 		Analyzers/BuildAnalyzer.h

+ 222 - 0
AI/Nullkiller/Engine/DeepDecomposer.cpp

@@ -0,0 +1,222 @@
+/*
+* Nullkiller.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 "DeepDecomposer.h"
+#include "../VCAI.h"
+#include "../Behaviors/CaptureObjectsBehavior.h"
+#include "../Behaviors/RecruitHeroBehavior.h"
+#include "../Behaviors/BuyArmyBehavior.h"
+#include "../Behaviors/StartupBehavior.h"
+#include "../Behaviors/DefenceBehavior.h"
+#include "../Behaviors/BuildingBehavior.h"
+#include "../Behaviors/GatherArmyBehavior.h"
+#include "../Behaviors/ClusterBehavior.h"
+#include "../Goals/Invalid.h"
+#include "../Goals/Composition.h"
+
+extern boost::thread_specific_ptr<CCallback> cb;
+extern boost::thread_specific_ptr<VCAI> ai;
+
+using namespace Goals;
+
+void DeepDecomposer::reset()
+{
+	decompositionCache.clear();
+	goals.clear();
+}
+
+Goals::TGoalVec DeepDecomposer::decompose(TSubgoal behavior, int depthLimit)
+{
+	TGoalVec tasks;
+
+	goals.clear();
+	goals.resize(depthLimit);
+	decompositionCache.resize(depthLimit);
+	depth = 0;
+
+	goals[0] = {behavior};
+
+	while(goals[0].size())
+	{
+		bool fromCache;
+		TSubgoal current = goals[depth].back();
+		TGoalVec subgoals = decomposeCached(unwrapComposition(current), fromCache);
+
+#if AI_TRACE_LEVEL >= 1
+		logAi->trace("Decomposition level %d returned %d goals", depth, subgoals.size());
+#endif
+
+		if(depth < depthLimit - 1)
+		{
+			goals[depth + 1].clear();
+		}
+
+		for(TSubgoal subgoal : subgoals)
+		{
+			if(subgoal->invalid())
+				continue;
+
+			if(subgoal->isElementar())
+			{
+				// need to get rid of priority control in behaviors like Startup to avoid this check.
+				// 0 - goals directly from behavior
+				Goals::TSubgoal task = depth >= 1 ? aggregateGoals(0, subgoal) : subgoal;
+
+#if AI_TRACE_LEVEL >= 1
+				logAi->trace("Found task %s", task->toString());
+#endif
+				if(!isCompositionLoop(subgoal))
+				{
+					tasks.push_back(task);
+
+					if(!fromCache)
+					{
+						addToCache(subgoal);
+					}
+				}
+			}
+			else if(depth < depthLimit - 1)
+			{
+#if AI_TRACE_LEVEL >= 1
+				logAi->trace("Found abstract goal %s", subgoal->toString());
+#endif
+				if(!isCompositionLoop(subgoal))
+				{
+					// depth 0 gives reward, deepest goal - task to execute, all the rest is not important
+					// so avoid details to reduce decomposition complexity but they can give some negative effect so
+					auto goalToAdd = depth >= 1 ? unwrapComposition(subgoal) : subgoal; 
+
+					if(!vstd::contains(goals[depth + 1], goalToAdd))
+					{
+						goals[depth + 1].push_back(subgoal);
+					}
+				}
+			}
+		}
+
+		if(depth < depthLimit - 1 && goals[depth + 1].size())
+		{
+			depth++;
+		}
+		else
+		{
+			goals[depth].pop_back();
+
+			while(depth > 0 && goals[depth].empty())
+			{
+				depth--;
+				goals[depth].pop_back();
+			}
+		}
+	}
+
+	return tasks;
+}
+
+Goals::TSubgoal DeepDecomposer::aggregateGoals(int startDepth, TSubgoal last)
+{
+	Goals::Composition composition;
+
+	for(int i = 0; i <= depth; i++)
+	{
+		composition.addNext(goals[i].back());
+	}
+
+	composition.addNext(last);
+
+	return sptr(composition);
+}
+
+Goals::TSubgoal DeepDecomposer::unwrapComposition(Goals::TSubgoal goal)
+{
+	return goal->goalType == Goals::COMPOSITION ? goal->decompose().back() : goal;
+}
+
+bool DeepDecomposer::isCompositionLoop(TSubgoal goal)
+{
+	auto goalsToTest = goal->goalType == Goals::COMPOSITION ? goal->decompose() : TGoalVec{goal};
+
+	for(auto goalToTest : goalsToTest)
+	{
+		for(int i = depth; i >= 0; i--)
+		{
+			auto parent = unwrapComposition(goals[i].back());
+
+			if(parent == goalToTest)
+			{
+				return true;
+			}
+		}
+	}
+
+	return false;
+}
+
+TGoalVec DeepDecomposer::decomposeCached(TSubgoal goal, bool & fromCache)
+{
+#if AI_TRACE_LEVEL >= 1	
+	logAi->trace("Decomposing %s, level %s", goal->toString(), depth);
+#endif
+
+	if(goal->hasHash())
+	{
+		for(int i = 0; i <= depth; i++)
+		{
+			auto cached = decompositionCache[i].find(goal);
+
+			if(cached != decompositionCache[i].end())
+			{
+#if AI_TRACE_LEVEL >= 1
+				logAi->trace("Use decomposition cache for %s, level: %d", goal->toString(), depth);
+#endif
+				fromCache = true;
+
+				return cached->second;
+			}
+		}
+
+		decompositionCache[depth][goal] = {}; // if goal decomposition yields no goals we still need it in cache to not decompose again
+	}
+
+#if AI_TRACE_LEVEL >= 2	
+	logAi->trace("Calling decompose on %s, level %s", goal->toString(), depth);
+#endif
+
+	fromCache = false;
+
+	return goal->decompose();
+}
+
+void DeepDecomposer::addToCache(TSubgoal goal)
+{
+	bool trusted = true;
+
+	for(int parentDepth = 1; parentDepth <= depth; parentDepth++)
+	{
+		TSubgoal parent = unwrapComposition(goals[parentDepth].back());
+
+		if(parent->hasHash())
+		{
+			auto solution = parentDepth < depth ? aggregateGoals(parentDepth + 1, goal) : goal;
+
+#if AI_TRACE_LEVEL >= 2
+			logAi->trace("Adding %s to decomosition cache of %s at level %d", solution->toString(), parent->toString(), parentDepth);
+#endif
+
+			decompositionCache[parentDepth][parent].push_back(solution);
+
+			if(trusted && parentDepth != 0)
+			{
+				decompositionCache[0][parent].push_back(solution);
+				trusted = false;
+			}
+		}
+	}
+}

+ 41 - 0
AI/Nullkiller/Engine/DeepDecomposer.h

@@ -0,0 +1,41 @@
+/*
+* DeepDecomposer.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/AbstractGoal.h"
+
+struct GoalHash
+{
+	uint64_t operator()(const Goals::TSubgoal & goal) const
+	{
+		return goal->getHash();
+	}
+};
+
+typedef std::unordered_map<Goals::TSubgoal, Goals::TGoalVec, GoalHash> TGoalHashSet;
+
+class DeepDecomposer
+{
+private:
+	std::vector<Goals::TGoalVec> goals;
+	std::vector<TGoalHashSet> decompositionCache;
+	int depth;
+
+public:
+	void reset();
+	Goals::TGoalVec decompose(Goals::TSubgoal behavior, int depthLimit);
+
+private:
+	Goals::TSubgoal aggregateGoals(int startDepth, Goals::TSubgoal last);
+	Goals::TSubgoal unwrapComposition(Goals::TSubgoal goal);
+	bool isCompositionLoop(Goals::TSubgoal goal);
+	Goals::TGoalVec decomposeCached(Goals::TSubgoal goal, bool & fromCache);
+	void addToCache(Goals::TSubgoal goal);
+};

+ 23 - 71
AI/Nullkiller/Engine/Nullkiller.cpp

@@ -19,6 +19,7 @@
 #include "../Behaviors/GatherArmyBehavior.h"
 #include "../Behaviors/ClusterBehavior.h"
 #include "../Goals/Invalid.h"
+#include "../Goals/Composition.h"
 
 extern boost::thread_specific_ptr<CCallback> cb;
 extern boost::thread_specific_ptr<VCAI> ai;
@@ -49,6 +50,7 @@ void Nullkiller::init(std::shared_ptr<CCallback> cb, PlayerColor playerID)
 	pathfinder.reset(new AIPathfinder(cb.get(), this));
 	armyManager.reset(new ArmyManager(cb.get(), this));
 	heroManager.reset(new HeroManager(cb.get(), this));
+	decomposer.reset(new DeepDecomposer());
 }
 
 Goals::TTask Nullkiller::choseBestTask(Goals::TTaskVec & tasks) const
@@ -60,76 +62,23 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TTaskVec & tasks) const
 	return bestTask;
 }
 
-Goals::TTask Nullkiller::choseBestTask(Goals::TSubgoal behavior) const
+Goals::TTask Nullkiller::choseBestTask(Goals::TSubgoal behavior, int decompositionMaxDepth) const
 {
 	logAi->debug("Checking behavior %s", behavior->toString());
 
-	const int MAX_DEPTH = 10;
-	Goals::TGoalVec goals[MAX_DEPTH + 1];
-	Goals::TTaskVec tasks;
-	std::map<Goals::TSubgoal, Goals::TSubgoal> decompositionMap;
 	auto start = boost::chrono::high_resolution_clock::now();
-
-	goals[0] = {behavior};
-
-	int depth = 0;
-	while(goals[0].size())
+	
+	Goals::TGoalVec elementarGoals = decomposer->decompose(behavior, decompositionMaxDepth);
+	Goals::TTaskVec tasks;
+	
+	for(auto goal : elementarGoals)
 	{
-		TSubgoal current = goals[depth].back();
-
-#if AI_TRACE_LEVEL >= 1
-		logAi->trace("Decomposing %s, level: %d", current->toString(), depth);
-#endif
+		Goals::TTask task = Goals::taskptr(*goal);
 
-		TGoalVec subgoals = current->decompose();
-
-#if AI_TRACE_LEVEL >= 1
-		logAi->trace("Found %d goals", subgoals.size());
-#endif
-
-		if(depth < MAX_DEPTH)
-		{
-			goals[depth + 1].clear();
-		}
-
-		for(auto subgoal : subgoals)
-		{
-			if(subgoal->isElementar())
-			{
-				auto task = taskptr(*subgoal);
+		if(task->priority <= 0)
+			task->priority = priorityEvaluator->evaluate(goal);
 
-#if AI_TRACE_LEVEL >= 1
-		logAi->trace("Found task %s", task->toString());
-#endif
-
-				if(task->priority <= 0)
-					task->priority = priorityEvaluator->evaluate(subgoal);
-
-				tasks.push_back(task);
-			}
-			else if(depth < MAX_DEPTH)
-			{
-#if AI_TRACE_LEVEL >= 1
-				logAi->trace("Found abstract goal %s", subgoal->toString());
-#endif
-				goals[depth + 1].push_back(subgoal);
-			}
-		}
-
-		if(depth < MAX_DEPTH && goals[depth + 1].size())
-		{
-			depth++;
-		}
-		else
-		{
-			goals[depth].pop_back();
-
-			while(depth > 0 && goals[depth].empty())
-			{
-				depth--;
-				goals[depth].pop_back();
-			}
-		}
+		tasks.push_back(task);
 	}
 
 	if(tasks.empty())
@@ -190,6 +139,7 @@ void Nullkiller::updateAiState(int pass)
 
 	objectClusterizer->clusterize();
 	buildAnalyzer->update();
+	decomposer->reset();
 
 	logAi->debug("AI state updated in %ld", timeElapsed(start));
 }
@@ -234,6 +184,8 @@ HeroLockedReason Nullkiller::getHeroLockedReason(const CGHeroInstance * hero) co
 
 void Nullkiller::makeTurn()
 {
+	const int MAX_DEPTH = 10;
+
 	resetAiState();
 
 	for(int i = 1; i <= MAXPASS; i++)
@@ -241,18 +193,18 @@ void Nullkiller::makeTurn()
 		updateAiState(i);
 
 		Goals::TTaskVec bestTasks = {
-			choseBestTask(sptr(BuyArmyBehavior())),
-			choseBestTask(sptr(CaptureObjectsBehavior())),
-			choseBestTask(sptr(ClusterBehavior())),
-			choseBestTask(sptr(RecruitHeroBehavior())),
-			choseBestTask(sptr(DefenceBehavior())),
-			choseBestTask(sptr(BuildingBehavior())),
-			choseBestTask(sptr(GatherArmyBehavior()))
+			choseBestTask(sptr(BuyArmyBehavior()), 1),
+			choseBestTask(sptr(CaptureObjectsBehavior()), 1),
+			choseBestTask(sptr(ClusterBehavior()), MAX_DEPTH),
+			choseBestTask(sptr(RecruitHeroBehavior()), 1),
+			choseBestTask(sptr(DefenceBehavior()), MAX_DEPTH),
+			choseBestTask(sptr(BuildingBehavior()), 1),
+			choseBestTask(sptr(GatherArmyBehavior()), MAX_DEPTH)
 		};
 
 		if(cb->getDate(Date::DAY) == 1)
 		{
-			bestTasks.push_back(choseBestTask(sptr(StartupBehavior())));
+			bestTasks.push_back(choseBestTask(sptr(StartupBehavior()), 1));
 		}
 
 		Goals::TTask bestTask = choseBestTask(bestTasks);

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

@@ -12,12 +12,12 @@
 #include "PriorityEvaluator.h"
 #include "FuzzyHelper.h"
 #include "AIMemory.h"
+#include "DeepDecomposer.h"
 #include "../Analyzers/DangerHitMapAnalyzer.h"
 #include "../Analyzers/BuildAnalyzer.h"
 #include "../Analyzers/ArmyManager.h"
 #include "../Analyzers/HeroManager.h"
 #include "../Analyzers/ObjectClusterizer.h"
-#include "../Goals/AbstractGoal.h"
 
 const float MAX_GOLD_PEASURE = 0.3f;
 const float MIN_PRIORITY = 0.01f;
@@ -50,6 +50,7 @@ public:
 	std::unique_ptr<ArmyManager> armyManager;
 	std::unique_ptr<AIMemory> memory;
 	std::unique_ptr<FuzzyHelper> dangerEvaluator;
+	std::unique_ptr<DeepDecomposer> decomposer;
 	PlayerColor playerID;
 	std::shared_ptr<CCallback> cb;
 
@@ -69,6 +70,6 @@ public:
 private:
 	void resetAiState();
 	void updateAiState(int pass);
-	Goals::TTask choseBestTask(Goals::TSubgoal behavior) const;
+	Goals::TTask choseBestTask(Goals::TSubgoal behavior, int decompositionMaxDepth) const;
 	Goals::TTask choseBestTask(Goals::TTaskVec & tasks) const;
 };

+ 4 - 0
AI/Nullkiller/Goals/AbstractGoal.h

@@ -143,6 +143,10 @@ namespace Goals
 		virtual bool operator==(const AbstractGoal & g) const;
 
 		virtual bool isElementar() const { return false; }
+
+		virtual bool hasHash() const { return false; }
+
+		virtual uint64_t getHash() const { return 0; }
 		
 		bool operator!=(const AbstractGoal & g) const
 		{

+ 32 - 1
AI/Nullkiller/Goals/CompleteQuest.cpp

@@ -13,12 +13,19 @@
 #include "../VCAI.h"
 #include "../../../lib/mapping/CMap.h" //for victory conditions
 #include "../../../lib/CPathfinder.h"
+#include "../../../lib/VCMI_Lib.h"
+#include "../../../lib/CGeneralTextHandler.h"
 
 extern boost::thread_specific_ptr<CCallback> cb;
 extern boost::thread_specific_ptr<VCAI> ai;
 
 using namespace Goals;
 
+bool isKeyMaster(const QuestInfo & q)
+{
+	return q.obj && (q.obj->ID == Obj::BORDER_GATE || q.obj->ID == Obj::BORDERGUARD);
+}
+
 std::string CompleteQuest::toString() const
 {
 	return "Complete quest " + questToString();
@@ -26,7 +33,7 @@ std::string CompleteQuest::toString() const
 
 TGoalVec CompleteQuest::decompose() const
 {
-	if(q.obj && (q.obj->ID == Obj::BORDER_GATE || q.obj->ID == Obj::BORDERGUARD))
+	if(isKeyMaster(q))
 	{
 		return missionKeymaster();
 	}
@@ -73,11 +80,35 @@ TGoalVec CompleteQuest::decompose() const
 
 bool CompleteQuest::operator==(const CompleteQuest & other) const
 {
+	if(isKeyMaster(q))
+	{
+		return isKeyMaster(other.q) && q.obj->subID == other.q.obj->subID;
+	}
+	else if(isKeyMaster(other.q))
+	{
+		return false;
+	}
+
 	return q.quest->qid == other.q.quest->qid;
 }
 
+uint64_t CompleteQuest::getHash() const
+{
+	if(isKeyMaster(q))
+	{
+		return q.obj->subID;
+	}
+
+	return q.quest->qid;
+}
+
 std::string CompleteQuest::questToString() const
 {
+	if(isKeyMaster(q))
+	{
+		return "find " + VLC->generaltexth->tentColors[q.obj->subID] + " keymaster tent";
+	}
+
 	if(q.quest->missionType == CQuest::MISSION_NONE)
 		return "inactive quest";
 

+ 2 - 1
AI/Nullkiller/Goals/CompleteQuest.h

@@ -29,11 +29,12 @@ namespace Goals
 
 		virtual Goals::TGoalVec decompose() const override;
 		virtual std::string toString() const override;
+		virtual bool hasHash() const override { return true; }
+		virtual uint64_t getHash() const override;
 
 		virtual bool operator==(const CompleteQuest & other) const override;
 
 	private:
-		TGoalVec getQuestTasks() const;
 		TGoalVec tryCompleteQuest() const;
 		TGoalVec missionArt() const;
 		TGoalVec missionHero() const;

+ 11 - 47
AI/Nullkiller/Goals/Composition.cpp

@@ -45,52 +45,7 @@ void Composition::accept(VCAI * ai)
 
 TGoalVec Composition::decompose() const
 {
-	if(isElementar())
-		return subtasks;
-
-	auto tasks = subtasks;
-	tasks.pop_back();
-
-	TSubgoal last = subtasks.back();
-	auto decomposed = last->decompose();
-	TGoalVec result;
-
-	for(TSubgoal goal : decomposed)
-	{
-		if(goal->invalid() || goal == last || vstd::contains(tasks, goal))
-			continue;
-
-		auto newComposition = Composition(tasks);
-
-		if(goal->goalType == COMPOSITION)
-		{
-			Composition & other = dynamic_cast<Composition &>(*goal);
-			bool cancel = false;
-
-			for(auto subgoal : other.subtasks)
-			{
-				if(subgoal == last || vstd::contains(tasks, subgoal))
-				{
-					cancel = true;
-
-					break;
-				}
-
-				newComposition.addNext(subgoal);
-			}
-
-			if(cancel)
-				continue;
-		}
-		else
-		{
-			newComposition.addNext(goal);
-		}
-
-		result.push_back(sptr(newComposition));
-	}
-
-	return result;
+	return subtasks;
 }
 
 Composition & Composition::addNext(const AbstractGoal & goal)
@@ -100,7 +55,16 @@ Composition & Composition::addNext(const AbstractGoal & goal)
 
 Composition & Composition::addNext(TSubgoal goal)
 {
-	subtasks.push_back(goal);
+	if(goal->goalType == COMPOSITION)
+	{
+		Composition & other = dynamic_cast<Composition &>(*goal);
+		
+		vstd::concatenate(subtasks, other.subtasks);
+	}
+	else
+	{
+		subtasks.push_back(goal);
+	}
 
 	return *this;
 }

+ 1 - 0
AI/Nullkiller/Markers/HeroExchange.cpp

@@ -12,6 +12,7 @@
 #include "../VCAI.h"
 #include "../Engine/Nullkiller.h"
 #include "../AIUtility.h"
+#include "../Analyzers/ArmyManager.h"
 
 extern boost::thread_specific_ptr<CCallback> cb;
 extern boost::thread_specific_ptr<VCAI> ai;

+ 1 - 1
AI/Nullkiller/Pathfinding/AINodeStorage.h

@@ -11,7 +11,7 @@
 #pragma once
 
 #define PATHFINDER_TRACE_LEVEL 0
-#define AI_TRACE_LEVEL 2
+#define AI_TRACE_LEVEL 1
 #define SCOUT_TURN_DISTANCE_LIMIT 3
 
 #include "../../../lib/CPathfinder.h"

+ 2 - 2
AI/Nullkiller/Pathfinding/Actors.cpp

@@ -190,7 +190,7 @@ bool HeroExchangeMap::canExchange(const ChainActor * other)
 			uint64_t reinforcment = upgradeInfo.upgradeValue;
 			
 			if(other->creatureSet->Slots().size())
-				reinforcment += ai->armyManager->howManyReinforcementsCanGet(actor->creatureSet, other->creatureSet);
+				reinforcment += ai->armyManager->howManyReinforcementsCanGet(actor->hero, actor->creatureSet, other->creatureSet);
 
 #if PATHFINDER_TRACE_LEVEL >= 2
 			logAi->trace(
@@ -291,7 +291,7 @@ CCreatureSet * HeroExchangeMap::tryUpgrade(const CCreatureSet * army, const CGOb
 CCreatureSet * HeroExchangeMap::pickBestCreatures(const CCreatureSet * army1, const CCreatureSet * army2) const
 {
 	CCreatureSet * target = new HeroExchangeArmy();
-	auto bestArmy = ai->armyManager->getBestArmy(army1, army2);
+	auto bestArmy = ai->armyManager->getBestArmy(actor->hero, army1, army2);
 
 	for(auto & slotInfo : bestArmy)
 	{

+ 7 - 0
AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp

@@ -90,6 +90,13 @@ namespace AIPathfinding
 			return bypassBattle(source, destination, pathfinderConfig, pathfinderHelper);
 		}
 
+		if(destination.nodeObject
+			&& (destination.nodeObject->ID == Obj::GARRISON || destination.nodeObject->ID == Obj::GARRISON2)
+			&& destination.objectRelations == PlayerRelations::ENEMIES)
+		{
+			return bypassBattle(source, destination, pathfinderConfig, pathfinderHelper);
+		}
+
 		return false;
 	}
 

+ 24 - 2
AI/Nullkiller/VCAI.cpp

@@ -803,11 +803,33 @@ void VCAI::pickBestCreatures(const CArmedInstance * destinationArmy, const CArme
 {
 	const CArmedInstance * armies[] = {destinationArmy, source};
 
-	auto bestArmy = nullkiller->armyManager->getSortedSlots(destinationArmy, source);
+	auto bestArmy = nullkiller->armyManager->getBestArmy(destinationArmy, destinationArmy, source);
 
 	//foreach best type -> iterate over slots in both armies and if it's the appropriate type, send it to the slot where it belongs
-	for(SlotID i = SlotID(0); i.getNum() < bestArmy.size() && i.validSlot(); i.advance(1)) //i-th strongest creature type will go to i-th slot
+	for(SlotID i = SlotID(0); i.validSlot(); i.advance(1)) //i-th strongest creature type will go to i-th slot
 	{
+		if(i.getNum() >= bestArmy.size())
+		{
+			if(destinationArmy->hasStackAtSlot(i))
+			{
+				auto creature = destinationArmy->getCreature(i);
+				auto targetSlot = source->getSlotFor(creature);
+
+				if(targetSlot.validSlot())
+				{
+					// remove unwanted creatures
+					cb->mergeOrSwapStacks(destinationArmy, source, i, targetSlot);
+				}
+				else if(destinationArmy->getStack(i).getPower() < destinationArmy->getArmyStrength() / 100)
+				{
+					// dismiss creatures if the amount is small
+					cb->dismissCreature(destinationArmy, i);
+				}
+			}
+
+			continue;
+		}
+
 		const CCreature * targetCreature = bestArmy[i.getNum()].creature;
 
 		for(auto armyPtr : armies)