Browse Source

Nullkiller AI: stabilization of build and prioritization fixes

Andrii Danylchenko 4 năm trước cách đây
mục cha
commit
400967904b

+ 16 - 5
AI/Nullkiller/AIhelper.cpp

@@ -47,6 +47,12 @@ void AIhelper::setAI(VCAI * AI)
 	heroManager->setAI(AI);
 }
 
+void AIhelper::update()
+{
+	armyManager->update();
+	heroManager->update();
+}
+
 bool AIhelper::getBuildingOptions(const CGTownInstance * t)
 {
 	return buildingManager->getBuildingOptions(t);
@@ -162,6 +168,16 @@ void AIhelper::updatePaths(std::vector<HeroPtr> heroes, bool useHeroChain)
 	pathfindingManager->updatePaths(heroes, useHeroChain);
 }
 
+uint64_t AIhelper::evaluateStackPower(const CCreature * creature, int count) const
+{
+	return armyManager->evaluateStackPower(creature, count);
+}
+
+SlotInfo AIhelper::getTotalCreaturesAvailable(CreatureID creatureID) const
+{
+	return armyManager->getTotalCreaturesAvailable(creatureID);
+}
+
 bool AIhelper::canGetArmy(const CArmedInstance * army, const CArmedInstance * source) const
 {
 	return armyManager->canGetArmy(army, source);
@@ -212,11 +228,6 @@ HeroRole AIhelper::getHeroRole(const HeroPtr & hero) const
 	return heroManager->getHeroRole(hero);
 }
 
-void AIhelper::updateHeroRoles()
-{
-	heroManager->updateHeroRoles();
-}
-
 float AIhelper::evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const
 {
 	return heroManager->evaluateSecSkill(skill, hero);

+ 4 - 1
AI/Nullkiller/AIhelper.h

@@ -80,14 +80,17 @@ public:
 	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;
+	uint64_t evaluateStackPower(const CCreature * creature, int count) const override;
+	SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const override;
 
 	const std::map<HeroPtr, HeroRole> & getHeroRoles() const override;
 	HeroRole getHeroRole(const HeroPtr & hero) const override;
 	int selectBestSkill(const HeroPtr & hero, const std::vector<SecondarySkill> & skills) const override;
-	void updateHeroRoles() override;
 	float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const override;
 	float evaluateHero(const CGHeroInstance * hero) const override;
 
+	void update() override;
+
 private:
 	bool notifyGoalCompleted(Goals::TSubgoal goal) override;
 

+ 330 - 0
AI/Nullkiller/Analyzers/BuildAnalyzer.cpp

@@ -14,6 +14,336 @@
 extern boost::thread_specific_ptr<CCallback> cb;
 extern boost::thread_specific_ptr<VCAI> ai;
 
+void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo)
+{
+	auto townInfo = developmentInfo.town->town;
+	auto creatures = townInfo->creatures;
+	auto buildings = townInfo->getAllBuildings();
+
+	std::map<BuildingID, BuildingID> parentMap;
+
+	for(auto &pair : townInfo->buildings)
+	{
+		if(pair.second->upgrade != -1)
+		{
+			parentMap[pair.second->upgrade] = pair.first;
+		}
+	}
+
+	BuildingID prefixes[] = {BuildingID::DWELL_UP_FIRST, BuildingID::DWELL_FIRST};
+
+	for(int level = 0; level < GameConstants::CREATURES_PER_TOWN; level++)
+	{
+		logAi->trace("Checking dwelling level %d", level);
+		BuildingInfo nextToBuild = BuildingInfo();
+
+		for(BuildingID prefix : prefixes)
+		{
+			BuildingID building = BuildingID(prefix + level);
+
+			if(!vstd::contains(buildings, building))
+				continue; // no such building in town
+
+			auto info = getBuildingOrPrerequisite(developmentInfo.town, building);
+
+			if(info.exists)
+			{
+				developmentInfo.addExistingDwelling(info);
+
+				break;
+			}
+
+			nextToBuild = info;
+		}
+
+		if(nextToBuild.id != BuildingID::NONE)
+		{
+			developmentInfo.addBuildingToBuild(nextToBuild);
+		}
+	}
+}
+
+void BuildAnalyzer::updateOtherBuildings(TownDevelopmentInfo & developmentInfo)
+{
+	logAi->trace("Checking other buildings");
+
+	std::vector<std::vector<BuildingID>> otherBuildings = {
+		{BuildingID::TOWN_HALL, BuildingID::CITY_HALL, BuildingID::CAPITOL}
+	};
+
+	if(developmentInfo.existingDwellings.size() >= 2 && cb->getDate(Date::DAY_OF_WEEK) > boost::date_time::Friday)
+	{
+		otherBuildings.push_back({BuildingID::CITADEL, BuildingID::CASTLE});
+	}
+
+	for(auto & buildingSet : otherBuildings)
+	{
+		for(auto & buildingID : buildingSet)
+		{
+			if(!developmentInfo.town->hasBuilt(buildingID))
+			{
+				developmentInfo.addBuildingToBuild(getBuildingOrPrerequisite(developmentInfo.town, buildingID));
+
+				break;
+			}
+		}
+	}
+}
+
+int32_t convertToGold(const TResources & res)
+{
+	return res[Res::GOLD] 
+		+ 75 * (res[Res::WOOD] + res[Res::ORE]) 
+		+ 125 * (res[Res::GEMS] + res[Res::CRYSTAL] + res[Res::MERCURY] + res[Res::SULFUR]);
+}
+
+TResources BuildAnalyzer::getResourcesRequiredNow() const
+{
+	auto resourcesAvailable = cb->getResourceAmount();
+	auto result = requiredResources - resourcesAvailable;
+
+	result.positive();
+
+	return result;
+}
+
+TResources BuildAnalyzer::getTotalResourcesRequired() const
+{
+	auto resourcesAvailable = cb->getResourceAmount();
+	auto result = totalDevelopmentCost - resourcesAvailable;
+
+	result.positive();
+
+	return result;
+}
+
+void BuildAnalyzer::update()
+{
+	logAi->trace("Start analysing build");
+
+	BuildingInfo bi;
+
+	reset();
+
+	auto towns = cb->getTownsInfo();
+
+	for(const CGTownInstance* town : towns)
+	{
+		logAi->trace("Checking town %s", town->name);
+
+		auto townInfo = town->town;
+
+		developmentInfos.push_back(TownDevelopmentInfo(town));
+		TownDevelopmentInfo & developmentInfo = developmentInfos.back();
+
+		updateTownDwellings(developmentInfo);
+		updateOtherBuildings(developmentInfo);
+
+		requiredResources += developmentInfo.requiredResources;
+		totalDevelopmentCost += developmentInfo.townDevelopmentCost;
+		armyCost += developmentInfo.armyCost;
+
+		for(auto bi : developmentInfo.toBuild)
+		{
+			logAi->trace("Building preferences %s", bi.toString());
+		}
+	}
+
+	std::sort(developmentInfos.begin(), developmentInfos.end(), [](const TownDevelopmentInfo & t1, const TownDevelopmentInfo & t2) -> bool
+	{
+		auto val1 = convertToGold(t1.armyCost) - convertToGold(t1.townDevelopmentCost);
+		auto val2 = convertToGold(t2.armyCost) - convertToGold(t2.townDevelopmentCost);
+
+		return val1 > val2;
+	});
+}
+
 void BuildAnalyzer::reset()
 {
+	requiredResources = TResources();
+	totalDevelopmentCost = TResources();
+	armyCost = TResources();
+	developmentInfos.clear();
 }
+
+BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
+	const CGTownInstance* town,
+	BuildingID toBuild,
+	bool excludeDwellingDependencies) const
+{
+	BuildingID building = toBuild;
+	auto townInfo = town->town;
+
+	const CBuilding * buildPtr = townInfo->buildings.at(building);
+	const CCreature * creature = nullptr;
+	CreatureID baseCreatureID;
+
+	if(BuildingID::DWELL_FIRST <= toBuild && toBuild <= BuildingID::DWELL_UP_LAST)
+	{
+		int level = toBuild - BuildingID::DWELL_FIRST;
+		auto creatures = townInfo->creatures.at(level % GameConstants::CREATURES_PER_TOWN);
+		auto creatureID = creatures.at(level / GameConstants::CREATURES_PER_TOWN);
+
+		baseCreatureID = creatures.front();
+		creature = creatureID.toCreature();
+	}
+
+	auto info = BuildingInfo(buildPtr, creature, baseCreatureID);
+
+	logAi->trace("checking %s", buildPtr->Name());
+	logAi->trace("buildInfo %s", info.toString());
+
+	buildPtr = nullptr;
+
+	if(!town->hasBuilt(building))
+	{
+		auto canBuild = cb->canBuildStructure(town, building);
+
+		if(canBuild == EBuildingState::ALLOWED)
+		{
+			info.canBuild = true;
+		}
+		else if(canBuild == EBuildingState::NO_RESOURCES)
+		{
+			logAi->trace("cant build. Not enough resources. Need %s", info.buildCost.toString());
+			info.notEnoughRes = true;
+		}
+		else if(canBuild == EBuildingState::PREREQUIRES)
+		{
+			auto buildExpression = town->genBuildingRequirements(building, false);
+			auto missingBuildings = buildExpression.getFulfillmentCandidates([&](const BuildingID & id) -> bool
+			{
+				return town->hasBuilt(id);
+			});
+
+			auto otherDwelling = [](const BuildingID & id) -> bool
+			{
+				return BuildingID::DWELL_FIRST <= id && id <= BuildingID::DWELL_UP_LAST;
+			};
+
+			if(vstd::contains_if(missingBuildings, otherDwelling))
+			{
+				logAi->trace("cant build. Need other dwelling");
+			}
+			else
+			{
+				buildPtr = townInfo->buildings.at(building);
+
+				logAi->trace("cant build. Need %d", missingBuildings[0].num);
+
+				BuildingInfo prerequisite = getBuildingOrPrerequisite(town, missingBuildings[0], excludeDwellingDependencies);
+
+				prerequisite.buildCostWithPrerequisits += info.buildCost;
+				prerequisite.creatureCost = info.creatureCost;
+				prerequisite.creatureGrows = info.creatureGrows;
+				prerequisite.creatureLevel = info.creatureLevel;
+				prerequisite.creatureID = info.creatureID;
+				prerequisite.baseCreatureID = info.baseCreatureID;
+				prerequisite.prerequisitesCount++;
+
+				return prerequisite;
+			}
+		}
+	}
+	else
+	{
+		logAi->trace("exists");
+		info.exists = true;
+	}
+
+	return info;
+}
+
+TResources BuildAnalyzer::getDailyIncome() const
+{
+	auto objects = cb->getMyObjects();
+	auto towns = cb->getTownsInfo();
+	TResources dailyIncome = TResources();
+
+	for(const CGObjectInstance* obj : objects)
+	{
+		const CGMine* mine = dynamic_cast<const CGMine*>(obj);
+
+		if(mine)
+		{
+			dailyIncome[mine->producedResource] += mine->producedQuantity;
+		}
+	}
+
+	for(const CGTownInstance* town : towns)
+	{
+		dailyIncome += town->dailyIncome();
+	}
+
+	return dailyIncome;
+}
+
+void TownDevelopmentInfo::addExistingDwelling(const BuildingInfo & existingDwelling)
+{
+	existingDwellings.push_back(existingDwelling);
+
+	armyCost += existingDwelling.creatureCost * existingDwelling.creatureGrows;
+}
+
+void TownDevelopmentInfo::addBuildingToBuild(const BuildingInfo & nextToBuild)
+{
+	townDevelopmentCost += nextToBuild.buildCostWithPrerequisits;
+
+	if(nextToBuild.canBuild)
+	{
+		toBuild.push_back(nextToBuild);
+		hasSomethingToBuild = true;
+	}
+	else if(nextToBuild.notEnoughRes)
+	{
+		requiredResources += nextToBuild.buildCost;
+		hasSomethingToBuild = true;
+	}
+}
+
+BuildingInfo::BuildingInfo()
+{
+	id = BuildingID::NONE;
+	creatureGrows = 0;
+	creatureID = CreatureID::NONE;
+	buildCost = 0;
+	buildCostWithPrerequisits = 0;
+	prerequisitesCount = 0;
+	name = "";
+}
+
+BuildingInfo::BuildingInfo(const CBuilding * building, const CCreature * creature, CreatureID baseCreature)
+{
+	id = building->bid;
+	buildCost = building->resources;
+	buildCostWithPrerequisits = building->resources;
+	dailyIncome = building->produce;
+	exists = false;;
+	prerequisitesCount = 1;
+	name = building->Name();
+
+	if(creature)
+	{
+		creatureGrows = creature->growth;
+		creatureID = creature->idNumber;
+		creatureCost = creature->cost;
+		creatureLevel = creature->level;
+		baseCreatureID = baseCreature;
+	}
+	else
+	{
+		creatureGrows = 0;
+		creatureID = CreatureID::NONE;
+		baseCreatureID = CreatureID::NONE;
+		creatureCost = TResources();
+		creatureLevel = 0;
+	}
+}
+
+std::string BuildingInfo::toString() const
+{
+	return name + ", cost: " + buildCost.toString()
+		+ ", creature: " + std::to_string(creatureGrows) + " x " + std::to_string(creatureLevel)
+		+ " x " + creatureCost.toString()
+		+ ", daily: " + dailyIncome.toString();
+}

+ 71 - 0
AI/Nullkiller/Analyzers/BuildAnalyzer.h

@@ -12,11 +12,82 @@
 #include "../VCAI.h"
 #include "../../../lib/ResourceSet.h"
 
+
+class BuildingInfo
+{
+public:
+	BuildingID id;
+	TResources buildCost;
+	TResources buildCostWithPrerequisits;
+	int creatureGrows;
+	uint8_t creatureLevel;
+	TResources creatureCost;
+	CreatureID creatureID;
+	CreatureID baseCreatureID;
+	TResources dailyIncome;
+	uint8_t prerequisitesCount;
+	std::string name;
+	bool exists = false;
+	bool canBuild = false;
+	bool notEnoughRes = false;
+
+	BuildingInfo();
+
+	BuildingInfo(const CBuilding* building, const CCreature* creature, CreatureID baseCreatureID);
+
+	std::string toString() const;
+};
+
+class TownDevelopmentInfo
+{
+public:
+	const CGTownInstance* town;
+	std::vector<BuildingInfo> toBuild;
+	std::vector<BuildingInfo> existingDwellings;
+	TResources townDevelopmentCost;
+	TResources requiredResources;
+	TResources armyCost;
+	int armyScore;
+	int economicsScore;
+	HeroRole townRole;
+	bool hasSomethingToBuild;
+
+	TownDevelopmentInfo(const CGTownInstance* town)
+		:town(town), armyScore(0), economicsScore(0), toBuild(), townDevelopmentCost(), requiredResources(), townRole(HeroRole::SCOUT), hasSomethingToBuild(false)
+	{
+	}
+
+	TownDevelopmentInfo() : TownDevelopmentInfo(nullptr) {}
+
+	void addBuildingToBuild(const BuildingInfo & building);
+	void addExistingDwelling(const BuildingInfo & existingDwelling);
+};
+
 class BuildAnalyzer
 {
 private:
 	TResources requiredResources;
+	TResources totalDevelopmentCost;
+	std::vector<TownDevelopmentInfo> developmentInfos;
+	TResources armyCost;
 
 public:
+	void update();
+
+	TResources getResourcesRequiredNow() const;
+	TResources getTotalResourcesRequired() const;
+	const std::vector<TownDevelopmentInfo> & getDevelopmentInfo() const { return developmentInfos; }
+
+	TResources getDailyIncome() const;
+
+private:
+	BuildingInfo getBuildingOrPrerequisite(
+		const CGTownInstance* town,
+		BuildingID toBuild,
+		bool excludeDwellingDependencies = true) const;
+
+
+	void updateTownDwellings(TownDevelopmentInfo & developmentInfo);
+	void updateOtherBuildings(TownDevelopmentInfo & developmentInfo);
 	void reset();
 };

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

@@ -49,6 +49,9 @@ void DangerHitMapAnalyzer::updateHitMap()
 		{
 			for(AIPath & path : ai->ah->getPathsToTile(pos))
 			{
+				if(path.getFirstBlockedAction())
+					continue;
+
 				auto tileDanger = path.getHeroStrength();
 				auto turn = path.turn();
 				auto & node = hitMap[pos.x][pos.y][pos.z];

+ 40 - 0
AI/Nullkiller/ArmyManager.cpp

@@ -168,3 +168,43 @@ ui64 ArmyManager::howManyReinforcementsCanGet(const CCreatureSet * target, const
 
 	return newArmy > oldArmy ? newArmy - oldArmy : 0;
 }
+
+uint64_t ArmyManager::evaluateStackPower(const CCreature * creature, int count) const
+{
+	return creature->AIValue * count;
+}
+
+SlotInfo ArmyManager::getTotalCreaturesAvailable(CreatureID creatureID) const
+{
+	auto creatureInfo = totalArmy.find(creatureID);
+
+	return creatureInfo == totalArmy.end() ? SlotInfo() : creatureInfo->second;
+}
+
+void ArmyManager::update()
+{
+	logAi->trace("Start analysing army");
+
+	std::vector<const CCreatureSet *> total;
+	auto heroes = cb->getHeroesInfo();
+	auto towns = cb->getTownsInfo();
+
+	std::copy(heroes.begin(), heroes.end(), std::back_inserter(total));
+	std::copy(towns.begin(), towns.end(), std::back_inserter(total));
+
+	totalArmy.clear();
+
+	for(auto army : total)
+	{
+		for(auto slot : army->Slots())
+		{
+			totalArmy[slot.second->getCreatureID()].count += slot.second->count;
+		}
+	}
+
+	for(auto army : totalArmy)
+	{
+		army.second.creature = army.first.toCreature();
+		army.second.power = evaluateStackPower(army.second.creature, army.second.count);
+	}
+}

+ 7 - 0
AI/Nullkiller/ArmyManager.h

@@ -30,6 +30,7 @@ class DLL_EXPORT IArmyManager //: public: IAbstractManager
 public:
 	virtual void init(CPlayerSpecificInfoCallback * CB) = 0;
 	virtual void setAI(VCAI * AI) = 0;
+	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;
@@ -37,6 +38,8 @@ public:
 	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;
+	virtual uint64_t evaluateStackPower(const CCreature * creature, int count) const = 0;
+	virtual SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const = 0;
 };
 
 class DLL_EXPORT ArmyManager : public IArmyManager
@@ -44,10 +47,12 @@ class DLL_EXPORT ArmyManager : public IArmyManager
 private:
 	CPlayerSpecificInfoCallback * cb; //this is enough, but we downcast from CCallback
 	VCAI * ai;
+	std::map<CreatureID, SlotInfo> totalArmy;
 
 public:
 	void init(CPlayerSpecificInfoCallback * CB) override;
 	void setAI(VCAI * AI) override;
+	void update() override;
 
 	bool canGetArmy(const CArmedInstance * target, const CArmedInstance * source) const override;
 	ui64 howManyReinforcementsCanBuy(const CCreatureSet * target, const CGDwelling * source) const override;
@@ -56,4 +61,6 @@ public:
 	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;
+	uint64_t evaluateStackPower(const CCreature * creature, int count) const override;
+	SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const override;
 };

+ 26 - 0
AI/Nullkiller/Behaviors/BuildingBehavior.cpp

@@ -33,6 +33,32 @@ Goals::TGoalVec BuildingBehavior::getTasks()
 {
 	Goals::TGoalVec tasks;
 
+	TResources resourcesRequired = ai->nullkiller->buildAnalyzer->getResourcesRequiredNow();
+	TResources totalDevelopmentCost = ai->nullkiller->buildAnalyzer->getTotalResourcesRequired();
+	TResources availableResources = cb->getResourceAmount();
+	TResources dailyIncome = ai->nullkiller->buildAnalyzer->getDailyIncome();
+
+	logAi->trace("Resources amount: %s", availableResources.toString());
+
+	resourcesRequired -= availableResources;
+	resourcesRequired.positive();
+
+	logAi->trace("daily income: %s", dailyIncome.toString());
+	logAi->trace("resources required to develop towns now: %s, total: %s",
+		resourcesRequired.toString(),
+		totalDevelopmentCost.toString());
+
+	auto & developmentInfos = ai->nullkiller->buildAnalyzer->getDevelopmentInfo();
+
+	for(auto & developmentInfo : developmentInfos)
+	{
+		auto town = developmentInfo.town;
+
+		for(auto & buildingInfo : developmentInfo.toBuild)
+		{
+			tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo)));
+		}
+	}
 
 	return tasks;
 }

+ 3 - 14
AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp

@@ -28,17 +28,6 @@ std::string CaptureObjectsBehavior::toString() const
 	return "Capture objects";
 }
 
-std::shared_ptr<const ISpecialAction> getFirstBlockedAction(const AIPath & path)
-{
-	for(auto node : path.nodes)
-	{
-		if(node.specialAction && !node.specialAction->canAct(node.targetHero))
-			return node.specialAction;
-	}
-
-	return std::shared_ptr<const ISpecialAction>();
-}
-
 Goals::TGoalVec CaptureObjectsBehavior::getTasks()
 {
 	Goals::TGoalVec tasks;
@@ -76,7 +65,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getTasks()
 				logAi->trace("Path found %s", path.toString());
 #endif
 
-				if(getFirstBlockedAction(path))
+				if(path.getFirstBlockedAction())
 				{
 #ifdef VCMI_TRACE_PATHFINDER
 					// TODO: decomposition?
@@ -88,7 +77,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getTasks()
 				if(ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
 				{
 #ifdef VCMI_TRACE_PATHFINDER
-					logAi->trace("Ignore path. Target hero can be killed by enemy");
+					logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %d", path.heroArmy->getArmyStrength());
 #endif
 					continue;
 				}
@@ -112,7 +101,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getTasks()
 					hero->name,
 					path.getHeroStrength(),
 					danger,
-					path.armyLoss);
+					path.getTotalArmyLoss());
 #endif
 
 				if(isSafe)

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

@@ -40,7 +40,7 @@ Goals::TGoalVec RecruitHeroBehavior::getTasks()
 			if(cb->getHeroesInfo().size() < cb->getTownsInfo().size() + 1
 				|| cb->getResourceAmount(Res::GOLD) > 10000)
 			{
-				tasks.push_back(Goals::sptr(Goals::RecruitHero().settown(town)));
+				tasks.push_back(Goals::sptr(Goals::RecruitHero().settown(town).setpriority(3)));
 			}
 		}
 	}

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

@@ -16,6 +16,7 @@
 #include "../Behaviors/BuyArmyBehavior.h"
 #include "../Behaviors/StartupBehavior.h"
 #include "../Behaviors/DefenceBehavior.h"
+#include "../Behaviors/BuildingBehavior.h"
 #include "../Goals/Invalid.h"
 
 extern boost::thread_specific_ptr<CCallback> cb;
@@ -25,6 +26,7 @@ Nullkiller::Nullkiller()
 {
 	priorityEvaluator.reset(new PriorityEvaluator());
 	dangerHitMap.reset(new DangerHitMapAnalyzer());
+	buildAnalyzer.reset(new BuildAnalyzer());
 }
 
 Goals::TSubgoal Nullkiller::choseBestTask(Goals::TGoalVec & tasks) const
@@ -78,14 +80,17 @@ void Nullkiller::updateAiState()
 	// TODO: move to hero manager
 	auto activeHeroes = ai->getMyHeroes();
 
-	vstd::erase_if(activeHeroes, [this](const HeroPtr & hero) -> bool{
+	vstd::erase_if(activeHeroes, [this](const HeroPtr & hero) -> bool
+	{
 		auto lockedReason = getHeroLockedReason(hero.h);
 
 		return lockedReason == HeroLockedReason::DEFENCE || lockedReason == HeroLockedReason::STARTUP;
 	});
 
 	ai->ah->updatePaths(activeHeroes, true);
-	ai->ah->updateHeroRoles();
+	ai->ah->update();
+
+	buildAnalyzer->update();
 }
 
 bool Nullkiller::arePathHeroesLocked(const AIPath & path) const
@@ -111,7 +116,8 @@ void Nullkiller::makeTurn()
 			choseBestTask(std::make_shared<BuyArmyBehavior>()),
 			choseBestTask(std::make_shared<CaptureObjectsBehavior>()),
 			choseBestTask(std::make_shared<RecruitHeroBehavior>()),
-			choseBestTask(std::make_shared<DefenceBehavior>())
+			choseBestTask(std::make_shared<DefenceBehavior>()),
+			choseBestTask(std::make_shared<BuildingBehavior>())
 		};
 
 		if(cb->getDate(Date::DAY) == 1)
@@ -140,12 +146,12 @@ void Nullkiller::makeTurn()
 		{
 			logAi->trace(bestTask->completeMessage());
 		}
-		catch(std::exception & e)
+		/*catch(std::exception & e)
 		{
 			logAi->debug("Failed to realize subgoal of type %s, I will stop.", bestTask->name());
 			logAi->debug("The error message was: %s", e.what());
 
 			return;
-		}
+		}*/
 	}
 }

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

@@ -35,6 +35,7 @@ private:
 
 public:
 	std::unique_ptr<DangerHitMapAnalyzer> dangerHitMap;
+	std::unique_ptr<BuildAnalyzer> buildAnalyzer;
 
 	Nullkiller();
 	void makeTurn();

+ 145 - 49
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -35,11 +35,6 @@ class CGTownInstance;
 extern boost::thread_specific_ptr<CCallback> cb;
 extern boost::thread_specific_ptr<VCAI> ai;
 
-PriorityEvaluator::PriorityEvaluator()
-{
-	initVisitTile();
-}
-
 PriorityEvaluator::~PriorityEvaluator()
 {
 	delete engine;
@@ -112,7 +107,8 @@ uint64_t getDwellingScore(const CGObjectInstance * target, bool checkGold)
 		if(creLevel.first && creLevel.second.size())
 		{
 			auto creature = creLevel.second.back().toCreature();
-			if(checkGold &&	!cb->getResourceAmount().canAfford(creature->cost * creLevel.first))
+			auto creaturesAreFree = creature->level == 1;
+			if(!creaturesAreFree && checkGold && !cb->getResourceAmount().canAfford(creature->cost * creLevel.first))
 				continue;
 
 			score += creature->AIValue * creLevel.first;
@@ -179,7 +175,7 @@ uint64_t getArmyReward(const CGObjectInstance * target, const CGHeroInstance * h
 	case Obj::SHIPWRECK:
 	case Obj::SHIPWRECK_SURVIVOR:
 	case Obj::WARRIORS_TOMB:
-		return 1500;
+		return 1000;
 	case Obj::ARTIFACT:
 		return evaluateArtifactArmyValue(dynamic_cast<const CGArtifact *>(target)->storedArtifact);
 	case Obj::DRAGON_UTOPIA:
@@ -208,6 +204,34 @@ float getEnemyHeroStrategicalValue(const CGHeroInstance * enemy)
 	return objectValue / 2.0f + enemy->level / 15.0f;
 }
 
+float getResourceRequirementStrength(int resType)
+{
+	TResources requiredResources = ai->nullkiller->buildAnalyzer->getResourcesRequiredNow();
+	TResources dailyIncome = ai->nullkiller->buildAnalyzer->getDailyIncome();
+
+	if(requiredResources[resType] == 0)
+		return 0;
+
+	if(dailyIncome[resType] == 0)
+		return 1;
+
+	return (float)requiredResources[resType] / dailyIncome[resType] / 3;
+}
+
+float getTotalResourceRequirementStrength(int resType)
+{
+	TResources requiredResources = ai->nullkiller->buildAnalyzer->getTotalResourcesRequired();
+	TResources dailyIncome = ai->nullkiller->buildAnalyzer->getDailyIncome();
+
+	if(requiredResources[resType] == 0)
+		return 0;
+
+	if(dailyIncome[resType] == 0)
+		return requiredResources[resType] / 30;
+
+	return (float)requiredResources[resType] / dailyIncome[resType] / 30;
+}
+
 float getStrategicalValue(const CGObjectInstance * target)
 {
 	if(!target)
@@ -215,6 +239,12 @@ float getStrategicalValue(const CGObjectInstance * target)
 
 	switch(target->ID)
 	{
+	case Obj::MINE:
+		return target->subID == Res::GOLD ? 0.8f : 0.05f + 0.3f * getTotalResourceRequirementStrength(target->subID) + 0.5f * getResourceRequirementStrength(target->subID);
+
+	case Obj::RESOURCE:
+		return target->subID == Res::GOLD ? 0 : 0.5f * getResourceRequirementStrength(target->subID);
+
 	case Obj::TOWN:
 		return target->tempOwner == PlayerColor::NEUTRAL ? 0.5 : 1;
 
@@ -261,7 +291,10 @@ float getSkillReward(const CGObjectInstance * target, const CGHeroInstance * her
 	case Obj::MERCENARY_CAMP:
 	case Obj::SHRINE_OF_MAGIC_GESTURE:
 	case Obj::SHRINE_OF_MAGIC_INCANTATION:
+	case Obj::TREE_OF_KNOWLEDGE:
 		return 1;
+	case Obj::LEARNING_STONE:
+		return 1.0f / std::sqrtf(hero->level);
 	case Obj::ARENA:
 	case Obj::SHRINE_OF_MAGIC_THOUGHT:
 		return 2;
@@ -339,6 +372,86 @@ int32_t getGoldReward(const CGObjectInstance * target, const CGHeroInstance * he
 	}
 }
 
+class ExecuteHeroChainEvaluationContextBuilder : public IEvaluationContextBuilder
+{
+public:
+	virtual Goals::EvaluationContext buildEvaluationContext(Goals::TSubgoal task) const override
+	{
+		auto evaluationContext = task->evaluationContext;
+
+		int objId = task->objid;
+
+		if(task->parent)
+			objId = task->parent->objid;
+
+		auto heroPtr = task->hero;
+		const CGObjectInstance * target = cb->getObj((ObjectInstanceID)objId, false);
+		auto day = cb->getDate(Date::DAY);
+		auto hero = heroPtr.get();
+		bool checkGold = evaluationContext.danger == 0;
+
+		evaluationContext.armyLossPersentage = task->evaluationContext.armyLoss / (double)task->evaluationContext.heroStrength;
+		evaluationContext.heroRole = ai->ah->getHeroRole(heroPtr);
+		evaluationContext.goldReward = getGoldReward(target, hero);
+		evaluationContext.armyReward = getArmyReward(target, hero, checkGold);
+		evaluationContext.skillReward = getSkillReward(target, hero, evaluationContext.heroRole);
+		evaluationContext.strategicalValue = getStrategicalValue(target);
+
+		return evaluationContext;
+	}
+};
+
+class BuildThisEvaluationContextBuilder : public IEvaluationContextBuilder
+{
+public:
+	virtual Goals::EvaluationContext buildEvaluationContext(Goals::TSubgoal task) const override
+	{
+		Goals::EvaluationContext evaluationContext;
+		Goals::BuildThis & buildThis = dynamic_cast<Goals::BuildThis &>(*task);
+		auto & bi = buildThis.buildingInfo;
+		
+		evaluationContext.goldReward = bi.dailyIncome[Res::GOLD] / 2;
+		evaluationContext.heroRole = HeroRole::MAIN;
+		evaluationContext.movementCostByRole[evaluationContext.heroRole] = bi.prerequisitesCount;
+		evaluationContext.armyReward = 0;
+		evaluationContext.strategicalValue = buildThis.townInfo.armyScore / 50000.0;
+
+		if(bi.creatureID != CreatureID::NONE)
+		{
+			evaluationContext.strategicalValue += 0.5f + 0.1f * bi.creatureLevel;
+
+			if(bi.baseCreatureID == bi.creatureID)
+			{
+				evaluationContext.armyReward = ai->ah->evaluateStackPower(bi.creatureID.toCreature(), bi.creatureGrows);
+			}
+			
+			auto creaturesToUpgrade = ai->ah->getTotalCreaturesAvailable(bi.baseCreatureID);
+			auto upgradedPower = ai->ah->evaluateStackPower(bi.creatureID.toCreature(), creaturesToUpgrade.count);
+
+			evaluationContext.armyReward = upgradedPower - creaturesToUpgrade.power;
+		}
+		
+		return evaluationContext;
+	}
+};
+
+PriorityEvaluator::PriorityEvaluator()
+{
+	initVisitTile();
+	evaluationContextBuilders[Goals::EXECUTE_HERO_CHAIN] = std::make_shared<ExecuteHeroChainEvaluationContextBuilder>();
+	evaluationContextBuilders[Goals::BUILD_STRUCTURE] = std::make_shared<BuildThisEvaluationContextBuilder>();
+}
+
+Goals::EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal) const
+{
+	auto builder = evaluationContextBuilders.find(goal->goalType);
+
+	if(builder == evaluationContextBuilders.end())
+		return goal->evaluationContext;
+
+	return builder->second->buildEvaluationContext(goal);
+}
+
 /// distance
 /// nearest hero?
 /// gold income
@@ -351,45 +464,28 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
 	if(task->priority > 0)
 		return task->priority;
 
-	auto heroPtr = task->hero;
-
-	if(!heroPtr.validAndSet())
-		return 2; 
-
-	int objId = task->objid;
+	auto evaluationContext = buildEvaluationContext(task);
 
-	if(task->parent)
-		objId = task->parent->objid;
-
-	const CGObjectInstance * target = cb->getObj((ObjectInstanceID)objId, false);
+	int rewardType = (evaluationContext.goldReward > 0 ? 1 : 0) 
+		+ (evaluationContext.armyReward > 0 ? 1 : 0)
+		+ (evaluationContext.skillReward > 0 ? 1 : 0)
+		+ (evaluationContext.strategicalValue > 0 ? 1 : 0);
 	
-	auto day = cb->getDate(Date::DAY);
-	auto hero = heroPtr.get();
-	auto armyTotal = task->evaluationContext.heroStrength;
-	double armyLossPersentage = task->evaluationContext.armyLoss / (double)armyTotal;
-	uint64_t danger = task->evaluationContext.danger;
-	HeroRole heroRole = ai->ah->getHeroRole(heroPtr);
-	int32_t goldReward = getGoldReward(target, hero);
-	bool checkGold = danger == 0;
-	uint64_t armyReward = getArmyReward(target, hero, checkGold);
-	float skillReward = getSkillReward(target, hero, heroRole);
-	float strategicalValue = getStrategicalValue(target);
 	double result = 0;
-	int rewardType = (goldReward > 0 ? 1 : 0) + (armyReward > 0 ? 1 : 0) + (skillReward > 0 ? 1 : 0) + (strategicalValue > 0 ? 1 : 0);
-	
+
 	try
 	{
-		armyLossPersentageVariable->setValue(armyLossPersentage);
-		heroRoleVariable->setValue(heroRole);
-		mainTurnDistanceVariable->setValue(task->evaluationContext.movementCostByRole[HeroRole::MAIN]);
-		scoutTurnDistanceVariable->setValue(task->evaluationContext.movementCostByRole[HeroRole::SCOUT]);
-		goldRewardVariable->setValue(goldReward);
-		armyRewardVariable->setValue(armyReward);
-		skillRewardVariable->setValue(skillReward);
-		dangerVariable->setValue(danger);
+		armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage);
+		heroRoleVariable->setValue(evaluationContext.heroRole);
+		mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]);
+		scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]);
+		goldRewardVariable->setValue(evaluationContext.goldReward);
+		armyRewardVariable->setValue(evaluationContext.armyReward);
+		skillRewardVariable->setValue(evaluationContext.skillReward);
+		dangerVariable->setValue(evaluationContext.danger);
 		rewardTypeVariable->setValue(rewardType);
-		closestHeroRatioVariable->setValue(task->evaluationContext.closestWayRatio);
-		strategicalValueVariable->setValue(strategicalValue);
+		closestHeroRatioVariable->setValue(evaluationContext.closestWayRatio);
+		strategicalValueVariable->setValue(evaluationContext.strategicalValue);
 
 		engine->process();
 		//engine.process(VISIT_TILE); //TODO: Process only Visit_Tile
@@ -402,16 +498,16 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
 	assert(result >= 0);
 
 #ifdef VCMI_TRACE_PATHFINDER
-	logAi->trace("Evaluated %s, hero %s, loss: %f, turns: %f, gold: %d, army gain: %d, danger: %d, role: %s, strategical value: %f, result %f",
+	logAi->trace("Evaluated %s, loss: %f, turns main: %f, scout: %f, gold: %d, army gain: %d, danger: %d, role: %s, strategical value: %f, result %f",
 		task->name(),
-		hero->name,
-		armyLossPersentage,
-		task->evaluationContext.movementCost,
-		goldReward,
-		armyReward,
-		danger,
-		heroRole ? "scout" : "main",
-		strategicalValue,
+		evaluationContext.armyLossPersentage,
+		evaluationContext.movementCostByRole[HeroRole::MAIN],
+		evaluationContext.movementCostByRole[HeroRole::SCOUT],
+		evaluationContext.goldReward,
+		evaluationContext.armyReward,
+		evaluationContext.danger,
+		evaluationContext.heroRole ? "scout" : "main",
+		evaluationContext.strategicalValue,
 		result);
 #endif
 

+ 8 - 5
AI/Nullkiller/Engine/PriorityEvaluator.h

@@ -10,12 +10,12 @@
 #pragma once
 #include "fl/Headers.h"
 #include "../Goals/Goals.h"
-#include "../FuzzyEngines.h"
 
-class VCAI;
-class CArmedInstance;
-class CBank;
-struct SectorMap;
+class IEvaluationContextBuilder
+{
+public:
+	virtual Goals::EvaluationContext buildEvaluationContext(Goals::TSubgoal goal) const = 0;
+};
 
 class PriorityEvaluator
 {
@@ -40,4 +40,7 @@ private:
 	fl::InputVariable * rewardTypeVariable;
 	fl::InputVariable * closestHeroRatioVariable;
 	fl::OutputVariable * value;
+	std::map<Goals::EGoals, std::shared_ptr<IEvaluationContextBuilder>> evaluationContextBuilders;
+
+	Goals::EvaluationContext buildEvaluationContext(Goals::TSubgoal goal) const;
 };

+ 12 - 2
AI/Nullkiller/Goals/AbstractGoal.h

@@ -96,12 +96,17 @@ namespace Goals
 	{
 		float movementCost;
 		std::map<HeroRole, float> movementCostByRole;
-		float scoutMovementCost;
 		int manaCost;
 		uint64_t danger;
 		float closestWayRatio;
 		uint64_t armyLoss;
 		uint64_t heroStrength;
+		float armyLossPersentage;
+		float armyReward;
+		int32_t goldReward;
+		float skillReward;
+		float strategicalValue;
+		HeroRole heroRole;
 
 		EvaluationContext()
 			: movementCost(0.0),
@@ -110,7 +115,12 @@ namespace Goals
 			closestWayRatio(1),
 			armyLoss(0),
 			heroStrength(0),
-			movementCostByRole()
+			movementCostByRole(),
+			skillReward(0),
+			goldReward(0),
+			armyReward(0),
+			armyLossPersentage(0),
+			heroRole(HeroRole::SCOUT)
 		{
 		}
 	};

+ 5 - 0
AI/Nullkiller/Goals/BuildThis.cpp

@@ -31,6 +31,11 @@ bool BuildThis::operator==(const BuildThis & other) const
 	return town == other.town && bid == other.bid;
 }
 
+std::string BuildThis::name() const
+{
+	return "Build " + buildingInfo.name + "(" + std::to_string(bid) + ") in " + town->name;
+}
+
 TSubgoal BuildThis::whatToDoToAchieve()
 {
 	auto b = BuildingID(bid);

+ 11 - 0
AI/Nullkiller/Goals/BuildThis.h

@@ -10,6 +10,7 @@
 #pragma once
 
 #include "CGoal.h"
+#include "../Analyzers/BuildAnalyzer.h"
 
 struct HeroPtr;
 class VCAI;
@@ -20,10 +21,19 @@ namespace Goals
 	class DLL_EXPORT BuildThis : public CGoal<BuildThis>
 	{
 	public:
+		BuildingInfo buildingInfo;
+		TownDevelopmentInfo townInfo;
+
 		BuildThis() //should be private, but unit test uses it
 			: CGoal(Goals::BUILD_STRUCTURE)
 		{
 		}
+		BuildThis(const BuildingInfo & buildingInfo, const TownDevelopmentInfo & townInfo) //should be private, but unit test uses it
+			: CGoal(Goals::BUILD_STRUCTURE), buildingInfo(buildingInfo), townInfo(townInfo)
+		{
+			bid = buildingInfo.id;
+			town = townInfo.town;
+		}
 		BuildThis(BuildingID Bid, const CGTownInstance * tid)
 			: CGoal(Goals::BUILD_STRUCTURE)
 		{
@@ -44,5 +54,6 @@ namespace Goals
 		TSubgoal whatToDoToAchieve() override;
 		//bool fulfillsMe(TSubgoal goal) override;
 		virtual bool operator==(const BuildThis & other) const override;
+		virtual std::string name() const override;
 	};
 }

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

@@ -175,7 +175,7 @@ void ExecuteHeroChain::accept(VCAI * ai)
 
 std::string ExecuteHeroChain::name() const
 {
-	return "ExecuteHeroChain " + targetName;
+	return "ExecuteHeroChain " + targetName + " by " + chainPath.targetHero->name;
 }
 
 std::string ExecuteHeroChain::completeMessage() const

+ 3 - 1
AI/Nullkiller/HeroManager.cpp

@@ -101,8 +101,10 @@ float HeroManager::evaluateFightingStrength(const CGHeroInstance * hero) const
 	return evaluateSpeciality(hero) + wariorSkillsScores.evaluateSecSkills(hero) + hero->level * 1.5f;
 }
 
-void HeroManager::updateHeroRoles()
+void HeroManager::update()
 {
+	logAi->trace("Start analysing our heroes");
+
 	std::map<HeroPtr, float> scores;
 	auto myHeroes = ai->getMyHeroes();
 

+ 2 - 2
AI/Nullkiller/HeroManager.h

@@ -26,7 +26,7 @@ public:
 	virtual const std::map<HeroPtr, HeroRole> & getHeroRoles() const = 0;
 	virtual int selectBestSkill(const HeroPtr & hero, const std::vector<SecondarySkill> & skills) const = 0;
 	virtual HeroRole getHeroRole(const HeroPtr & hero) const = 0;
-	virtual void updateHeroRoles() = 0;
+	virtual void update() = 0;
 	virtual float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const = 0;
 	virtual float evaluateHero(const CGHeroInstance * hero) const = 0;
 };
@@ -64,7 +64,7 @@ public:
 	const std::map<HeroPtr, HeroRole> & getHeroRoles() const override;
 	HeroRole getHeroRole(const HeroPtr & hero) const override;
 	int selectBestSkill(const HeroPtr & hero, const std::vector<SecondarySkill> & skills) const override;
-	void updateHeroRoles() override;
+	void update() override;
 	float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const override;
 	float evaluateHero(const CGHeroInstance * hero) const override;
 

+ 83 - 26
AI/Nullkiller/Pathfinding/AINodeStorage.cpp

@@ -76,7 +76,7 @@ void AINodeStorage::initialize(const PathfinderOptions & options, const CGameSta
 void AINodeStorage::clear()
 {
 	actors.clear();
-	heroChainPass = false;
+	heroChainPass = EHeroChainPass::INITIAL;
 	heroChainTurn = 0;
 	heroChainMaxTurns = 1;
 }
@@ -262,9 +262,37 @@ bool AINodeStorage::increaseHeroChainTurnLimit()
 	return true;
 }
 
+EPathfindingLayer phisycalLayers[2] = {EPathfindingLayer::LAND, EPathfindingLayer::SAIL};
+
+bool AINodeStorage::calculateHeroChainFinal()
+{
+	heroChainPass = EHeroChainPass::FINAL;
+	heroChain.resize(0);
+
+	for(auto layer : phisycalLayers)
+	{
+		foreach_tile_pos([&](const int3 & pos)
+		{
+			auto chains = nodes[pos.x][pos.y][pos.z][layer];
+
+			for(AIPathNode & node : chains)
+			{
+				if(node.turns > heroChainTurn
+					&& node.action != CGPathNode::ENodeAction::UNKNOWN
+					&& node.actor->actorExchangeCount > 1)
+				{
+					heroChain.push_back(&node);
+				}
+			}
+		});
+	}
+	
+	return heroChain.size();
+}
+
 bool AINodeStorage::calculateHeroChain()
 {
-	heroChainPass = true;
+	heroChainPass = EHeroChainPass::CHAIN;
 	heroChain.resize(0);
 
 	std::vector<AIPathNode *> existingChains;
@@ -273,37 +301,40 @@ bool AINodeStorage::calculateHeroChain()
 	existingChains.reserve(NUM_CHAINS);
 	newChains.reserve(NUM_CHAINS);
 
-	foreach_tile_pos([&](const int3 & pos) {
-		auto layer = EPathfindingLayer::LAND;
-		auto chains = nodes[pos.x][pos.y][pos.z][layer];
+	for(auto layer : phisycalLayers)
+	{
+		foreach_tile_pos([&](const int3 & pos)
+		{
+			auto chains = nodes[pos.x][pos.y][pos.z][layer];
 
-		existingChains.resize(0);
-		newChains.resize(0);
+			existingChains.resize(0);
+			newChains.resize(0);
 
-		for(AIPathNode & node : chains)
-		{
-			if(node.turns <= heroChainTurn && node.action != CGPathNode::ENodeAction::UNKNOWN)
-				existingChains.push_back(&node);
-		}
+			for(AIPathNode & node : chains)
+			{
+				if(node.turns <= heroChainTurn && node.action != CGPathNode::ENodeAction::UNKNOWN)
+					existingChains.push_back(&node);
+			}
 
-		for(AIPathNode * node : existingChains)
-		{
-			if(node->actor->isMovable)
+			for(AIPathNode * node : existingChains)
 			{
-				calculateHeroChain(node, existingChains, newChains);
+				if(node->actor->isMovable)
+				{
+					calculateHeroChain(node, existingChains, newChains);
+				}
 			}
-		}
 
-		cleanupInefectiveChains(newChains);
-		addHeroChain(newChains);
-	});
+			cleanupInefectiveChains(newChains);
+			addHeroChain(newChains);
+		});
+	}
 
 	return heroChain.size();
 }
 
 void AINodeStorage::cleanupInefectiveChains(std::vector<ExchangeCandidate> & result) const
 {
-	vstd::erase_if(result, [&](ExchangeCandidate & chainInfo) -> bool
+	vstd::erase_if(result, [&](const ExchangeCandidate & chainInfo) -> bool
 	{
 		auto pos = chainInfo.coord;
 		auto chains = nodes[pos.x][pos.y][pos.z][EPathfindingLayer::LAND];
@@ -320,12 +351,27 @@ void AINodeStorage::calculateHeroChain(
 {
 	for(AIPathNode * node : variants)
 	{
-		if(node == srcNode 
-			|| !node->actor 
-			|| node->turns > heroChainTurn 
+		if(node == srcNode || !node->actor)
+			continue;
+
+		if(node->turns > heroChainTurn 
 			|| (node->action == CGPathNode::ENodeAction::UNKNOWN && node->actor->hero)
 			|| (node->actor->chainMask & srcNode->actor->chainMask) != 0)
 		{
+#if AI_TRACE_LEVEL >= 2
+			logAi->trace(
+				"Skip exchange %s[%x] -> %s[%x] at %s because of %s",
+				node->actor->toString(),
+				node->actor->chainMask,
+				srcNode->actor->toString(),
+				srcNode->actor->chainMask,
+				srcNode->coord.toString(),
+				(node->turns > heroChainTurn 
+					? "turn limit" 
+					: (node->action == CGPathNode::ENodeAction::UNKNOWN && node->actor->hero)
+						? "action unknown"
+						: "chain mask"));
+#endif
 			continue;
 		}
 
@@ -793,7 +839,7 @@ bool AINodeStorage::hasBetterChain(
 			}
 		}
 
-		if((candidateActor->chainMask & node.actor->chainMask) == 0)
+		if(candidateActor->chainMask != node.actor->chainMask)
 			continue;
 
 		auto nodeActor = node.actor;
@@ -931,6 +977,17 @@ AIPath::AIPath()
 {
 }
 
+std::shared_ptr<const ISpecialAction> AIPath::getFirstBlockedAction() const
+{
+	for(auto node : nodes)
+	{
+		if(node.specialAction && !node.specialAction->canAct(node.targetHero))
+			return node.specialAction;
+	}
+
+	return std::shared_ptr<const ISpecialAction>();
+}
+
 int3 AIPath::firstTileToGet() const
 {
 	if(nodes.size())
@@ -1005,7 +1062,7 @@ uint64_t AIPath::getTotalArmyLoss() const
 	return armyLoss + targetObjectArmyLoss;
 }
 
-std::string AIPath::toString()
+std::string AIPath::toString() const
 {
 	std::stringstream str;
 

+ 15 - 3
AI/Nullkiller/Pathfinding/AINodeStorage.h

@@ -79,7 +79,9 @@ struct AIPath
 
 	uint64_t getHeroStrength() const;
 
-	std::string toString();
+	std::string toString() const;
+
+	std::shared_ptr<const ISpecialAction> AIPath::getFirstBlockedAction() const;
 };
 
 struct ExchangeCandidate : public AIPathNode
@@ -88,6 +90,15 @@ struct ExchangeCandidate : public AIPathNode
 	AIPathNode * otherParent;
 };
 
+enum EHeroChainPass
+{
+	INITIAL, // single heroes unlimited distance
+
+	CHAIN, // chains with limited distance
+
+	FINAL // same as SINGLE but for heroes from CHAIN pass
+};
+
 class AINodeStorage : public INodeStorage
 {
 private:
@@ -100,7 +111,7 @@ private:
 	std::unique_ptr<FuzzyHelper> dangerEvaluator;
 	std::vector<std::shared_ptr<ChainActor>> actors;
 	std::vector<CGPathNode *> heroChain;
-	bool heroChainPass; // true if we need to calculate hero chain
+	EHeroChainPass heroChainPass; // true if we need to calculate hero chain
 	int heroChainTurn;
 	int heroChainMaxTurns;
 	PlayerColor playerID;
@@ -146,7 +157,7 @@ public:
 	bool isMovementIneficient(const PathNodeInfo & source, CDestinationNodeInfo & destination) const
 	{
 		// further chain distribution is calculated as the last stage
-		if(heroChainPass && destination.node->turns > heroChainTurn)
+		if(heroChainPass == EHeroChainPass::CHAIN && destination.node->turns > heroChainTurn)
 			return true;
 
 		return hasBetterChain(source, destination);
@@ -169,6 +180,7 @@ public:
 	const std::set<const CGHeroInstance *> getAllHeroes() const;
 	void clear();
 	bool calculateHeroChain();
+	bool calculateHeroChainFinal();
 
 	uint64_t evaluateDanger(const int3 &  tile, const CGHeroInstance * hero) const
 	{

+ 12 - 12
AI/Nullkiller/Pathfinding/AIPathfinder.cpp

@@ -66,21 +66,21 @@ void AIPathfinder::updatePaths(std::vector<HeroPtr> heroes, bool useHeroChain)
 
 	do
 	{
-		logAi->trace("Recalculate paths pass %d", pass++);
-		cb->calculatePaths(config);
-
-		if(useHeroChain)
+		do
 		{
-			logAi->trace("Recalculate chain pass %d", pass);
-
-			continueCalculation = storage->calculateHeroChain();
+			logAi->trace("Recalculate paths pass %d", pass++);
+			cb->calculatePaths(config);
+		} while(useHeroChain && storage->calculateHeroChain());
 
-			if(!continueCalculation)
-			{
-				logAi->trace("Increase chain turn limit");
+		if(!useHeroChain)
+			break;
 
-				continueCalculation = storage->increaseHeroChainTurnLimit() && storage->calculateHeroChain();
-			}
+		if(storage->calculateHeroChainFinal())
+		{
+			logAi->trace("Recalculate paths pass final");
+			cb->calculatePaths(config);
 		}
+
+		continueCalculation = storage->increaseHeroChainTurnLimit() && storage->calculateHeroChain();
 	} while(continueCalculation);
 }

+ 14 - 5
AI/Nullkiller/VCAI.cpp

@@ -853,10 +853,10 @@ void VCAI::makeTurn()
 		logAi->debug("Making turn thread has been interrupted. We'll end without calling endTurn.");
 		return;
 	}
-	catch (std::exception & e)
+	/*catch (std::exception & e)
 	{
 		logAi->debug("Making turn thread has caught an exception: %s", e.what());
-	}
+	}*/
 
 	endTurn();
 }
@@ -1079,7 +1079,12 @@ void VCAI::performObjectInteraction(const CGObjectInstance * obj, HeroPtr h)
 				moveCreaturesToHero(h->visitedTown);
 
 			townVisitsThisWeek[h].insert(h->visitedTown);
-			ah->updateHeroRoles();
+
+			if(!ai->nullkiller)
+			{
+				ah->update();
+			}
+
 			if(ah->getHeroRole(h) == HeroRole::MAIN && !h->hasSpellbook() && ah->freeGold() >= GameConstants::SPELLBOOK_GOLD_COST)
 			{
 				if(h->visitedTown->hasBuilt(BuildingID::MAGES_GUILD_1))
@@ -1994,9 +1999,11 @@ bool VCAI::moveHeroToTile(int3 dst, HeroPtr h)
 				doChannelProbing();
 		}
 
-		if(path.nodes[0].action == CGPathNode::BLOCKING_VISIT)
+		if(path.nodes[0].action == CGPathNode::BLOCKING_VISIT || path.nodes[0].action == CGPathNode::BATTLE)
 		{
-			ret = h && i == 0; // when we take resource we do not reach its position. We even might not move
+			// when we take resource we do not reach its position. We even might not move
+			// also guarded town is not get visited automatically after capturing
+			ret = h && i == 0;
 		}
 	}
 	if(h)
@@ -2485,6 +2492,8 @@ void VCAI::recruitHero(const CGTownInstance * t, bool throwing)
 		}
 		cb->recruitHero(t, hero);
 
+		ai->ah->update();
+
 		if(t->visitingHero)
 			moveHeroToTile(t->visitablePos(), t->visitingHero.get());