Quellcode durchsuchen

NKAI improve defence and some fixes

Andrii Danylchenko vor 2 Jahren
Ursprung
Commit
8b0c7b6601

+ 23 - 10
AI/Nullkiller/AIGateway.cpp

@@ -359,6 +359,11 @@ void AIGateway::objectRemoved(const CGObjectInstance * obj)
 	{
 		lostHero(cb->getHero(obj->id)); //we can promote, since objectRemoved is called just before actual deletion
 	}
+
+	if(obj->ID == Obj::HERO && cb->getPlayerRelations(obj->tempOwner, playerID) == PlayerRelations::ENEMIES)
+	{
+		nullkiller->dangerHitMap->reset();
+	}
 }
 
 void AIGateway::showHillFortWindow(const CGObjectInstance * object, const CGHeroInstance * visitor)
@@ -580,27 +585,32 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector<C
 		requestActionASAP([=]()
 		{
 			//yes&no -> always answer yes, we are a brave AI :)
-			auto answer = 1;
+			bool answer = true;
 			auto objects = cb->getVisitableObjs(target);
 
 			if(hero.validAndSet() && target.valid() && objects.size())
 			{
-				auto objType = objects.front()->ID;
+				auto objType = objects.back()->ID;
 
-				if(objType == Obj::ARTIFACT || objType == Obj::RESOURCE)
-				{
-					auto ratio = (float)nullkiller->dangerEvaluator->evaluateDanger(target, hero.get()) / (float)hero->getTotalStrength();
-					bool dangerUnknown = ratio == 0;
-					bool dangerTooHigh = ratio > (1 / SAFE_ATTACK_CONSTANT);
+				auto ratio = (float)nullkiller->dangerEvaluator->evaluateDanger(target, hero.get()) / (float)hero->getTotalStrength();
+				bool dangerUnknown = ratio == 0;
+				bool dangerTooHigh = ratio > (1 / SAFE_ATTACK_CONSTANT);
 
+				answer = objects.back()->id == nullkiller->getTargetObject(); // no if we do not aim to visit this object
+
+				if(objType == Obj::BORDERGUARD || objType == Obj::QUEST_GUARD)
+				{
+					answer = true;
+				}
+				else if(objType == Obj::ARTIFACT || objType == Obj::RESOURCE)
+				{
 					logAi->trace("Guarded object query hook: %s by %s danger ratio %f", target.toString(), hero.name, ratio);
 
-					if(text.find("guarded") != std::string::npos && (dangerUnknown || dangerTooHigh))
-						answer = 0; // no
+					answer = dangerUnknown || dangerTooHigh;
 				}
 			}
 
-			answerQuery(askID, answer);
+			answerQuery(askID, answer ? 1 : 0);
 		});
 
 		return;
@@ -1332,7 +1342,10 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
 		if(auto visitedObject = vstd::frontOrNull(cb->getVisitableObjs(h->visitablePos()))) //we stand on something interesting
 		{
 			if(visitedObject != *h)
+			{
 				performObjectInteraction(visitedObject, h);
+				ret = true;
+			}
 		}
 	}
 	if(h) //we could have lost hero after last move

+ 5 - 0
AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp

@@ -14,6 +14,8 @@
 namespace NKAI
 {
 
+HitMapInfo HitMapInfo::NoTreat;
+
 void DangerHitMapAnalyzer::updateHitMap()
 {
 	if(upToDate)
@@ -47,6 +49,9 @@ void DangerHitMapAnalyzer::updateHitMap()
 
 	for(auto pair : heroes)
 	{
+		if(!pair.first.isValidPlayer())
+			continue;
+
 		if(ai->cb->getPlayerRelations(ai->playerID, pair.first) != PlayerRelations::ENEMIES)
 			continue;
 

+ 9 - 0
AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h

@@ -16,10 +16,17 @@ namespace NKAI
 
 struct HitMapInfo
 {
+	static HitMapInfo NoTreat;
+
 	uint64_t danger;
 	uint8_t turn;
 	HeroPtr hero;
 
+	HitMapInfo()
+	{
+		reset();
+	}
+
 	void reset()
 	{
 		danger = 0;
@@ -33,6 +40,8 @@ struct HitMapNode
 	HitMapInfo maximumDanger;
 	HitMapInfo fastestDanger;
 
+	HitMapNode() = default;
+
 	void reset()
 	{
 		maximumDanger.reset();

+ 0 - 52
AI/Nullkiller/Analyzers/HeroManager.cpp

@@ -92,37 +92,6 @@ float HeroManager::evaluateFightingStrength(const CGHeroInstance * hero) const
 	return evaluateSpeciality(hero) + wariorSkillsScores.evaluateSecSkills(hero) + hero->level * 1.5f;
 }
 
-std::vector<std::vector<const CGHeroInstance *>> clusterizeHeroes(CCallback * cb, std::vector<const CGHeroInstance *> heroes)
-{
-	std::vector<std::vector<const CGHeroInstance *>> clusters;
-
-	for(auto hero : heroes)
-	{
-		auto paths = cb->getPathsInfo(hero);
-		std::vector<const CGHeroInstance *> newCluster = {hero};
-
-		for(auto cluster = clusters.begin(); cluster != clusters.end();)
-		{
-			auto hero = std::find_if(cluster->begin(), cluster->end(), [&](const CGHeroInstance * h) -> bool
-			{
-				return paths->getNode(h->visitablePos())->turns < SCOUT_TURN_DISTANCE_LIMIT;
-			});
-
-			if(hero != cluster->end())
-			{
-				vstd::concatenate(newCluster, *cluster);
-				clusters.erase(cluster);
-			}
-			else
-				cluster++;
-		}
-
-		clusters.push_back(newCluster);
-	}
-
-	return clusters;
-}
-
 void HeroManager::update()
 {
 	logAi->trace("Start analysing our heroes");
@@ -149,27 +118,6 @@ void HeroManager::update()
 		heroRoles[hero] = (globalMainCount--) > 0 ? HeroRole::MAIN : HeroRole::SCOUT;
 	}
 
-	for(auto cluster : clusterizeHeroes(cb, myHeroes))
-	{
-		std::sort(cluster.begin(), cluster.end(), scoreSort);
-
-		auto localMainCountMax = (cluster.size() + 2) / 3;
-
-		for(auto hero : cluster)
-		{
-			if(heroRoles[hero] != HeroRole::MAIN)
-			{
-				heroRoles[hero] = HeroRole::MAIN;
-				break;
-			}
-			
-			localMainCountMax--;
-
-			if(localMainCountMax == 0)
-				break;
-		}
-	}
-
 	for(auto hero : myHeroes)
 	{
 		logAi->trace("Hero %s has role %s", hero->getNameTranslated(), heroRoles[hero] == HeroRole::MAIN ? "main" : "scout");

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

@@ -271,7 +271,7 @@ void ObjectClusterizer::clusterize()
 				if(!shouldVisit(ai, path.targetHero, obj))
 				{
 #if NKAI_TRACE_LEVEL >= 2
-					logAi->trace("Hero %s does not need to visit %s", path.targetHero->name, obj->getObjectName());
+					logAi->trace("Hero %s does not need to visit %s", path.targetHero->getObjectName(), obj->getObjectName());
 #endif
 					continue;
 				}
@@ -285,7 +285,7 @@ void ObjectClusterizer::clusterize()
 						if(vstd::contains(heroesProcessed, path.targetHero))
 						{
 #if NKAI_TRACE_LEVEL >= 2
-							logAi->trace("Hero %s is already processed.", path.targetHero->name);
+							logAi->trace("Hero %s is already processed.", path.targetHero->getObjectName());
 #endif
 							continue;
 						}

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

@@ -70,7 +70,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
 		if(ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
 		{
 #if NKAI_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.getHeroStrength());
 #endif
 			continue;
 		}
@@ -113,7 +113,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
 			"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,
+			hero->getObjectName(),
 			path.getHeroStrength(),
 			danger,
 			path.getTotalArmyLoss());

+ 42 - 19
AI/Nullkiller/Behaviors/DefenceBehavior.cpp

@@ -18,6 +18,7 @@
 #include "../Goals/RecruitHero.h"
 #include "../Goals/DismissHero.h"
 #include "../Goals/Composition.h"
+#include "../Goals/CaptureObject.h"
 #include "../Markers/DefendTown.h"
 #include "../Goals/ExchangeSwapTownHeroes.h"
 #include "lib/mapping/CMap.h" //for victory conditions
@@ -29,6 +30,8 @@ namespace NKAI
 extern boost::thread_specific_ptr<CCallback> cb;
 extern boost::thread_specific_ptr<AIGateway> ai;
 
+const double TREAT_IGNORE_RATIO = 0.5;
+
 using namespace Goals;
 
 std::string DefenceBehavior::toString() const
@@ -53,7 +56,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 	logAi->trace("Evaluating defence for %s", town->getNameTranslated());
 
 	auto treatNode = ai->nullkiller->dangerHitMap->getObjectTreat(town);
-	auto treats = { treatNode.fastestDanger, treatNode.maximumDanger };
+	auto treats = { treatNode.maximumDanger, treatNode.fastestDanger };
 
 	if(!treatNode.fastestDanger.hero)
 	{
@@ -68,12 +71,17 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 	{
 		if(!ai->nullkiller->isHeroLocked(town->garrisonHero.get()))
 		{
-			if(!town->visitingHero && cb->getHeroesInfo().size() < GameConstants::MAX_HEROES_PER_PLAYER)
+			if(!town->visitingHero && cb->getHeroCount(ai->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER)
 			{
+				logAi->trace(
+					"Extracting hero %s from garrison of town %s",
+					town->garrisonHero->getNameTranslated(),
+					town->getNameTranslated());
+
 				tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5)));
-			}
 
-			return;
+				return;
+			}
 		}
 
 		logAi->trace(
@@ -113,22 +121,37 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 			if(town->visitingHero && path.getHeroStrength() < town->visitingHero->getHeroStrength())
 				continue;
 
-			if(path.getHeroStrength() > treat.danger)
+			if(treat.hero.validAndSet()
+				&& treat.turn <= 1
+				&& (treat.danger == treatNode.maximumDanger.danger || treat.turn < treatNode.maximumDanger.turn)
+				&& isSafeToVisit(path.targetHero, path.heroArmy, treat.danger))
 			{
-				if((path.turn() <= treat.turn && dayOfWeek + treat.turn < 6 && isSafeToVisit(path.targetHero, path.heroArmy, treat.danger))
-					|| (path.exchangeCount == 1 && path.turn() < treat.turn)
+				Composition composition;
+
+				composition.addNext(DefendTown(town, treat, path)).addNext(CaptureObject(treat.hero.get()));
+
+				tasks.push_back(Goals::sptr(composition));
+			}
+
+			bool treatIsWeak = path.getHeroStrength() / treat.danger > TREAT_IGNORE_RATIO;
+			bool needToSaveGrowth = treat.turn == 0 && dayOfWeek == 7;
+
+			if(treatIsWeak && !needToSaveGrowth)
+			{
+				if((path.exchangeCount == 1 && path.turn() < treat.turn)
 					|| path.turn() < treat.turn - 1
 					|| (path.turn() < treat.turn && treat.turn >= 2))
 				{
 #if NKAI_TRACE_LEVEL >= 1
 					logAi->trace(
 						"Hero %s can eliminate danger for town %s using path %s.",
-						path.targetHero->name,
-						town->name,
+						path.targetHero->getObjectName(),
+						town->getObjectName(),
 						path.toString());
 #endif
 
 					treatIsUnderControl = true;
+
 					break;
 				}
 			}
@@ -152,7 +175,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 					if(cb->getHeroesInfo().size() < ALLOWED_ROAMING_HEROES)
 					{
 #if NKAI_TRACE_LEVEL >= 1
-						logAi->trace("Hero %s can be recruited to defend %s", hero->name, town->name);
+						logAi->trace("Hero %s can be recruited to defend %s", hero->getObjectName(), town->getObjectName());
 #endif
 						tasks.push_back(Goals::sptr(Goals::RecruitHero(town, hero).setpriority(1)));
 						continue;
@@ -202,7 +225,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 #if NKAI_TRACE_LEVEL >= 1
 			logAi->trace(
 				"Hero %s can defend town with force %lld in %s turns, cost: %f, path: %s",
-				path.targetHero->name,
+				path.targetHero->getObjectName(),
 				path.getHeroStrength(),
 				std::to_string(path.turn()),
 				path.movementCost(),
@@ -212,8 +235,8 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 			{
 #if NKAI_TRACE_LEVEL >= 1
 				logAi->trace("Defer defence of %s by %s because he has enough time to reach the town next trun",
-					town->name,
-					path.targetHero->name);
+					town->getObjectName(),
+					path.targetHero->getObjectName());
 #endif
 
 				defferedPaths[path.targetHero].push_back(i);
@@ -225,8 +248,8 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 			{
 #if NKAI_TRACE_LEVEL >= 1
 				logAi->trace("Put %s to garrison of town %s",
-					path.targetHero->name,
-					town->name);
+					path.targetHero->getObjectName(),
+					town->getObjectName());
 #endif
 
 				// dismiss creatures we are not able to pick to be able to hide in garrison
@@ -249,8 +272,8 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 				{
 #if NKAI_TRACE_LEVEL >= 1
 					logAi->trace("Can not move %s to defend town %s. Path is locked.",
-						path.targetHero->name,
-						town->name);
+						path.targetHero->getObjectName(),
+						town->getObjectName());
 
 #endif
 					continue;
@@ -277,8 +300,8 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 
 #if NKAI_TRACE_LEVEL >= 1
 			logAi->trace("Move %s to defend town %s",
-				path.targetHero->name,
-				town->name);
+				path.targetHero->getObjectName(),
+				town->getObjectName());
 #endif
 			Composition composition;
 

+ 9 - 3
AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp

@@ -138,8 +138,8 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
 		logAi->trace(
 			"It is %s to visit %s by %s with army %lld, danger %lld and army loss %lld",
 			isSafe ? "safe" : "not safe",
-			hero->name,
-			path.targetHero->name,
+			hero->getObjectName(),
+			path.targetHero->getObjectName(),
 			path.getHeroStrength(),
 			danger,
 			path.getTotalArmyLoss());
@@ -230,6 +230,12 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
 		auto upgrade = ai->nullkiller->armyManager->calculateCreaturesUpgrade(path.heroArmy, upgrader, availableResources);
 		auto armyValue = (float)upgrade.upgradeValue / path.getHeroStrength();
 
+		if(ai->nullkiller->heroManager->getHeroRole(path.targetHero) == HeroRole::MAIN)
+		{
+			upgrade.upgradeValue +=
+				ai->nullkiller->armyManager->howManyReinforcementsCanGet(path.targetHero, path.heroArmy, upgrader);
+		}
+
 		if(armyValue < 0.1f || upgrade.upgradeValue < 300) // avoid small upgrades
 			continue;
 
@@ -242,7 +248,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
 			"It is %s to visit %s by %s with army %lld, danger %lld and army loss %lld",
 			isSafe ? "safe" : "not safe",
 			upgrader->getObjectName(),
-			path.targetHero->name,
+			path.targetHero->getObjectName(),
 			path.getHeroStrength(),
 			danger,
 			path.getTotalArmyLoss());

+ 19 - 20
AI/Nullkiller/Engine/Nullkiller.cpp

@@ -131,6 +131,7 @@ void Nullkiller::updateAiState(int pass, bool fast)
 	auto start = std::chrono::high_resolution_clock::now();
 
 	activeHero = nullptr;
+	setTargetObject(-1);
 
 	if(!fast)
 	{
@@ -188,7 +189,7 @@ bool Nullkiller::arePathHeroesLocked(const AIPath & path) const
 	if(getHeroLockedReason(path.targetHero) == HeroLockedReason::STARTUP)
 	{
 #if NKAI_TRACE_LEVEL >= 1
-		logAi->trace("Hero %s is locked by STARTUP. Discarding %s", path.targetHero->name, path.toString());
+		logAi->trace("Hero %s is locked by STARTUP. Discarding %s", path.targetHero->getObjectName(), path.toString());
 #endif
 		return true;
 	}
@@ -200,7 +201,7 @@ bool Nullkiller::arePathHeroesLocked(const AIPath & path) const
 		if(lockReason != HeroLockedReason::NOT_LOCKED)
 		{
 #if NKAI_TRACE_LEVEL >= 1
-			logAi->trace("Hero %s is locked by STARTUP. Discarding %s", path.targetHero->name, path.toString());
+			logAi->trace("Hero %s is locked by STARTUP. Discarding %s", path.targetHero->getObjectName(), path.toString());
 #endif
 			return true;
 		}
@@ -221,6 +222,7 @@ void Nullkiller::makeTurn()
 	boost::lock_guard<boost::mutex> sharedStorageLock(AISharedStorage::locker);
 
 	const int MAX_DEPTH = 10;
+	const int FAST_TASK_MINIMAL_PRIORITY = 0.7;
 
 	resetAiState();
 
@@ -240,12 +242,12 @@ void Nullkiller::makeTurn()
 
 			bestTask = choseBestTask(fastTasks);
 
-			if(bestTask->priority >= 1)
+			if(bestTask->priority >= FAST_TASK_MINIMAL_PRIORITY)
 			{
 				executeTask(bestTask);
 				updateAiState(i, true);
 			}
-		} while(bestTask->priority >= 1);
+		} while(bestTask->priority >= FAST_TASK_MINIMAL_PRIORITY);
 
 		Goals::TTaskVec bestTasks = {
 			bestTask,
@@ -272,21 +274,16 @@ void Nullkiller::makeTurn()
 		if(heroRole != HeroRole::MAIN || bestTask->getHeroExchangeCount() <= 1)
 			useHeroChain = false;
 
-		if(bestTask->priority < NEXT_SCAN_MIN_PRIORITY
-			&& scanDepth != ScanDepth::FULL)
+		if((heroRole != HeroRole::MAIN || bestTask->priority < SMALL_SCAN_MIN_PRIORITY)
+			&& scanDepth == ScanDepth::FULL)
 		{
-			if(heroRole == HeroRole::MAIN || bestTask->priority < MIN_PRIORITY)
-			{
-				useHeroChain = false;
-
-				logAi->trace(
-					"Goal %s has too low priority %f so increasing scan depth",
-					bestTask->toString(),
-					bestTask->priority);
-				scanDepth = (ScanDepth)((int)scanDepth + 1);
+			useHeroChain = false;
+			scanDepth = ScanDepth::SMALL;
 
-				continue;
-			}
+			logAi->trace(
+				"Goal %s has too low priority %f so increasing scan depth",
+				bestTask->toString(),
+				bestTask->priority);
 		}
 
 		if(bestTask->priority < MIN_PRIORITY)
@@ -317,10 +314,12 @@ void Nullkiller::executeTask(Goals::TTask task)
 	}
 	catch(cannotFulfillGoalException & e)
 	{
-		logAi->debug("Failed to realize subgoal of type %s, I will stop.", taskDescr);
-		logAi->debug("The error message was: %s", e.what());
+		logAi->error("Failed to realize subgoal of type %s, I will stop.", taskDescr);
+		logAi->error("The error message was: %s", e.what());
 
-		throw;
+#if NKAI_TRACE_LEVEL == 0
+		throw; // will be recatched and AI turn ended
+#endif
 	}
 }
 

+ 6 - 5
AI/Nullkiller/Engine/Nullkiller.h

@@ -24,7 +24,7 @@ namespace NKAI
 
 const float MAX_GOLD_PEASURE = 0.3f;
 const float MIN_PRIORITY = 0.01f;
-const float NEXT_SCAN_MIN_PRIORITY = 0.4f;
+const float SMALL_SCAN_MIN_PRIORITY = 0.4f;
 
 enum class HeroLockedReason
 {
@@ -39,11 +39,9 @@ enum class HeroLockedReason
 
 enum class ScanDepth
 {
-	SMALL = 0,
+	FULL = 0,
 
-	MEDIUM = 1,
-
-	FULL = 2
+	SMALL = 1
 };
 
 class Nullkiller
@@ -51,6 +49,7 @@ class Nullkiller
 private:
 	const CGHeroInstance * activeHero;
 	int3 targetTile;
+	ObjectInstanceID targetObject;
 	std::map<const CGHeroInstance *, HeroLockedReason> lockedHeroes;
 	ScanDepth scanDepth;
 	TResources lockedResources;
@@ -79,6 +78,8 @@ public:
 	HeroPtr getActiveHero() { return activeHero; }
 	HeroLockedReason getHeroLockedReason(const CGHeroInstance * hero) const;
 	int3 getTargetTile() const { return targetTile; }
+	ObjectInstanceID getTargetObject() const { return targetObject; }
+	void setTargetObject(int objid) { targetObject = ObjectInstanceID(objid); }
 	void setActive(const CGHeroInstance * hero, int3 tile) { activeHero = hero; targetTile = tile; }
 	void lockHero(const CGHeroInstance * hero, HeroLockedReason lockReason) { lockedHeroes[hero] = lockReason; }
 	void unlockHero(const CGHeroInstance * hero) { lockedHeroes.erase(hero); }

+ 79 - 21
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -17,10 +17,12 @@
 #include "../../../lib/CPathfinder.h"
 #include "../../../lib/CGameStateFwd.h"
 #include "../../../lib/VCMI_Lib.h"
+#include "../../../lib/StartInfo.h"
 #include "../../../CCallback.h"
 #include "../../../lib/filesystem/Filesystem.h"
 #include "../Goals/ExecuteHeroChain.h"
 #include "../Goals/BuildThis.h"
+#include "../Goals/ExchangeSwapTownHeroes.h"
 #include "../Markers/UnlockCluster.h"
 #include "../Markers/HeroExchange.h"
 #include "../Markers/ArmyUpgrade.h"
@@ -87,10 +89,14 @@ int32_t estimateTownIncome(CCallback * cb, const CGObjectInstance * target, cons
 		return 0; // if we already own it, no additional reward will be received by just visiting it
 
 	auto town = cb->getTown(target->id);
-	auto isNeutral = target->tempOwner == PlayerColor::NEUTRAL;
-	auto isProbablyDeveloped = !isNeutral && town->hasFort();
+	auto fortLevel = town->fortLevel();
 
-	return isProbablyDeveloped ? 1500 : 500;
+	if(town->hasCapitol()) return 4000;
+
+	// probably well developed town will have city hall
+	if(fortLevel == CGTownInstance::CASTLE) return 1500;
+	
+	return town->hasFort() && town->tempOwner != PlayerColor::NEUTRAL  ? 1000 : 500;
 }
 
 TResources getCreatureBankResources(const CGObjectInstance * target, const CGHeroInstance * hero)
@@ -238,7 +244,16 @@ uint64_t RewardEvaluator::getArmyReward(
 	switch(target->ID)
 	{
 	case Obj::TOWN:
-		return target->tempOwner == PlayerColor::NEUTRAL ? 1000 : 10000;
+	{
+		auto town = dynamic_cast<const CGTownInstance *>(target);
+		auto fortLevel = town->fortLevel();
+
+		if(fortLevel < CGTownInstance::CITADEL)
+			return town->hasFort() ? 1000 : 0;
+		else
+			return fortLevel == CGTownInstance::CASTLE ? 10000 : 4000;
+	}
+
 	case Obj::HILL_FORT:
 		return ai->armyManager->calculateCreaturesUpgrade(army, target, ai->cb->getResourceAmount()).upgradeValue;
 	case Obj::CREATURE_BANK:
@@ -374,12 +389,20 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons
 	}
 
 	case Obj::TOWN:
+	{
 		if(ai->buildAnalyzer->getDevelopmentInfo().empty())
 			return 1;
 
-		return dynamic_cast<const CGTownInstance *>(target)->hasFort()
-			? (target->tempOwner == PlayerColor::NEUTRAL ? 0.8f : 1.0f)
-			: 0.7f;
+		auto town = dynamic_cast<const CGTownInstance *>(target);
+		auto fortLevel = town->fortLevel();
+
+		if(town->hasCapitol()) return 1;
+
+		if(fortLevel < CGTownInstance::CITADEL)
+			return town->hasFort() ? 0.6 : 0.4;
+		else
+			return fortLevel == CGTownInstance::CASTLE ? 0.9 : 0.8;
+	}
 
 	case Obj::HERO:
 		return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES
@@ -448,22 +471,17 @@ float RewardEvaluator::getSkillReward(const CGObjectInstance * target, const CGH
 	}
 }
 
-uint64_t RewardEvaluator::getEnemyHeroDanger(const int3 & tile, uint8_t turn) const
+const HitMapInfo & RewardEvaluator::getEnemyHeroDanger(const int3 & tile, uint8_t turn) const
 {
 	auto & treatNode = ai->dangerHitMap->getTileTreat(tile);
 
 	if(treatNode.maximumDanger.danger == 0)
-		return 0;
+		return HitMapInfo::NoTreat;
 
 	if(treatNode.maximumDanger.turn <= turn)
-		return treatNode.maximumDanger.danger;
-
-	return treatNode.fastestDanger.turn <= turn ? treatNode.fastestDanger.danger : 0;
-}
+		return treatNode.maximumDanger;
 
-uint64_t RewardEvaluator::getEnemyHeroDanger(const AIPath & path) const
-{
-	return getEnemyHeroDanger(path.targetTile(), path.turn());
+	return treatNode.fastestDanger.turn <= turn ? treatNode.fastestDanger : HitMapInfo::NoTreat;
 }
 
 int32_t getArmyCost(const CArmedInstance * army)
@@ -564,6 +582,26 @@ public:
 	}
 };
 
+void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uint8_t turn, uint64_t ourStrength)
+{
+	HitMapInfo enemyDanger = evaluationContext.evaluator.getEnemyHeroDanger(tile, turn);
+
+	if(enemyDanger.danger)
+	{
+		auto dangerRatio = enemyDanger.danger / (double)ourStrength;
+		auto enemyHero = evaluationContext.evaluator.ai->cb->getObj(enemyDanger.hero.hid, false);
+		bool isAI =enemyHero
+			&& evaluationContext.evaluator.ai->cb->getStartInfo()->getIthPlayersSettings(enemyHero->getOwner()).isControlledByAI();
+
+		if(isAI)
+		{
+			dangerRatio *= 1.5; // lets make AI bit more afraid of other AI.
+		}
+
+		vstd::amax(evaluationContext.enemyHeroDangerRatio, dangerRatio);
+	}
+}
+
 class DefendTownEvaluator : public IEvaluationContextBuilder
 {
 private:
@@ -596,7 +634,7 @@ public:
 		auto armyIncome = townArmyIncome(town);
 		auto dailyIncome = town->dailyIncome()[Res::GOLD];
 
-		auto strategicalValue = std::sqrt(armyIncome / 20000.0f) + dailyIncome / 10000.0f;
+		auto strategicalValue = std::sqrt(armyIncome / 20000.0f) + dailyIncome / 3000.0f;
 
 		float multiplier = 1;
 
@@ -607,9 +645,7 @@ public:
 		evaluationContext.goldReward += dailyIncome * 5 * multiplier;
 		evaluationContext.strategicalValue += strategicalValue * multiplier;
 		vstd::amax(evaluationContext.danger, defendTown.getTreat().danger);
-
-		auto enemyDanger = evaluationContext.evaluator.getEnemyHeroDanger(town->visitablePos(), defendTown.getTurn());
-		vstd::amax(evaluationContext.enemyHeroDangerRatio, enemyDanger / (double)defendTown.getDefenceStrength());
+		addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength());
 	}
 };
 
@@ -665,7 +701,7 @@ public:
 
 		vstd::amax(evaluationContext.armyLossPersentage, path.getTotalArmyLoss() / (double)path.getHeroStrength());
 		evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroPtr);
-		vstd::amax(evaluationContext.enemyHeroDangerRatio, evaluationContext.evaluator.getEnemyHeroDanger(path) / (double)path.getHeroStrength());
+		addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength());
 		vstd::amax(evaluationContext.turn, path.turn());
 	}
 };
@@ -719,6 +755,27 @@ public:
 	}
 };
 
+class ExchangeSwapTownHeroesContextBuilder : public IEvaluationContextBuilder
+{
+public:
+	virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
+	{
+		if(task->goalType != Goals::EXCHANGE_SWAP_TOWN_HEROES)
+			return;
+
+		Goals::ExchangeSwapTownHeroes & swapCommand = dynamic_cast<Goals::ExchangeSwapTownHeroes &>(*task);
+		const CGHeroInstance * garrisonHero = swapCommand.getGarrisonHero();
+
+		if(garrisonHero && swapCommand.getLockingReason() == HeroLockedReason::DEFENCE)
+		{
+			auto defenderRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(garrisonHero);
+
+			evaluationContext.movementCost += garrisonHero->movement;
+			evaluationContext.movementCostByRole[defenderRole] += garrisonHero->movement;
+		}
+	}
+};
+
 class BuildThisEvaluationContextBuilder : public IEvaluationContextBuilder
 {
 public:
@@ -783,6 +840,7 @@ PriorityEvaluator::PriorityEvaluator(const Nullkiller * ai)
 	evaluationContextBuilders.push_back(std::make_shared<HeroExchangeEvaluator>());
 	evaluationContextBuilders.push_back(std::make_shared<ArmyUpgradeEvaluator>());
 	evaluationContextBuilders.push_back(std::make_shared<DefendTownEvaluator>());
+	evaluationContextBuilders.push_back(std::make_shared<ExchangeSwapTownHeroesContextBuilder>());
 }
 
 EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal) const

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

@@ -23,6 +23,7 @@ namespace NKAI
 
 class BuildingInfo;
 class Nullkiller;
+struct HitMapInfo;
 
 class RewardEvaluator
 {
@@ -41,8 +42,7 @@ public:
 	float getSkillReward(const CGObjectInstance * target, const CGHeroInstance * hero, HeroRole role) const;
 	int32_t getGoldReward(const CGObjectInstance * target, const CGHeroInstance * hero) const;
 	uint64_t getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const;
-	uint64_t getEnemyHeroDanger(const AIPath & path) const;
-	uint64_t getEnemyHeroDanger(const int3 & tile, uint8_t turn) const;
+	const HitMapInfo & getEnemyHeroDanger(const int3 & tile, uint8_t turn) const;
 };
 
 struct DLL_EXPORT EvaluationContext

+ 4 - 1
AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp

@@ -83,7 +83,10 @@ void ExchangeSwapTownHeroes::accept(AIGateway * ai)
 	
 	cb->swapGarrisonHero(town);
 
-	ai->nullkiller->lockHero(garrisonHero, lockingReason);
+	if(lockingReason != HeroLockedReason::NOT_LOCKED)
+	{
+		ai->nullkiller->lockHero(garrisonHero, lockingReason);
+	}
 
 	if(town->visitingHero && town->visitingHero != garrisonHero)
 	{

+ 3 - 0
AI/Nullkiller/Goals/ExchangeSwapTownHeroes.h

@@ -32,6 +32,9 @@ namespace Goals
 		void accept(AIGateway * ai) override;
 		std::string toString() const override;
 		virtual bool operator==(const ExchangeSwapTownHeroes & other) const override;
+
+		const CGHeroInstance * getGarrisonHero() const { return garrisonHero; }
+		HeroLockedReason getLockingReason() const { return lockingReason; }
 	};
 }
 

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

@@ -52,6 +52,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 	logAi->debug("Executing hero chain towards %s. Path %s", targetName, chainPath.toString());
 
 	ai->nullkiller->setActive(chainPath.targetHero, tile);
+	ai->nullkiller->setTargetObject(objid);
 
 	std::set<int> blockedIndexes;