Преглед изворни кода

Nullkiller: gold preasure and turn variables for priority evaluation. Tweaking building behavior

Andrii Danylchenko пре 4 година
родитељ
комит
17a960e850

+ 9 - 4
AI/Nullkiller/Analyzers/BuildAnalyzer.cpp

@@ -156,6 +156,12 @@ void BuildAnalyzer::update()
 
 		return val1 > val2;
 	});
+
+	updateDailyIncome();
+
+	goldPreasure = (float)armyCost[Res::GOLD] / (1 + cb->getResourceAmount(Res::GOLD) + (float)dailyIncome[Res::GOLD] * 7.0f);
+
+	logAi->trace("Gold preasure: %f", goldPreasure);
 }
 
 void BuildAnalyzer::reset()
@@ -254,11 +260,12 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
 	return info;
 }
 
-TResources BuildAnalyzer::getDailyIncome() const
+void BuildAnalyzer::updateDailyIncome()
 {
 	auto objects = cb->getMyObjects();
 	auto towns = cb->getTownsInfo();
-	TResources dailyIncome = TResources();
+	
+	dailyIncome = TResources();
 
 	for(const CGObjectInstance* obj : objects)
 	{
@@ -274,8 +281,6 @@ TResources BuildAnalyzer::getDailyIncome() const
 	{
 		dailyIncome += town->dailyIncome();
 	}
-
-	return dailyIncome;
 }
 
 void TownDevelopmentInfo::addExistingDwelling(const BuildingInfo & existingDwelling)

+ 5 - 2
AI/Nullkiller/Analyzers/BuildAnalyzer.h

@@ -70,6 +70,8 @@ private:
 	TResources totalDevelopmentCost;
 	std::vector<TownDevelopmentInfo> developmentInfos;
 	TResources armyCost;
+	TResources dailyIncome;
+	float goldPreasure;
 
 public:
 	void update();
@@ -77,8 +79,8 @@ public:
 	TResources getResourcesRequiredNow() const;
 	TResources getTotalResourcesRequired() const;
 	const std::vector<TownDevelopmentInfo> & getDevelopmentInfo() const { return developmentInfos; }
-
-	TResources getDailyIncome() const;
+	TResources getDailyIncome() const { return dailyIncome; }
+	float getGoldPreasure() const { return goldPreasure; }
 
 private:
 	BuildingInfo getBuildingOrPrerequisite(
@@ -89,5 +91,6 @@ private:
 
 	void updateTownDwellings(TownDevelopmentInfo & developmentInfo);
 	void updateOtherBuildings(TownDevelopmentInfo & developmentInfo);
+	void updateDailyIncome();
 	void reset();
 };

+ 32 - 0
AI/Nullkiller/ArmyManager.cpp

@@ -275,6 +275,35 @@ std::vector<UpgradeInfo> ArmyManager::getDwellingUpgrades(const CCreatureSet * a
 {
 	std::vector<UpgradeInfo> upgrades;
 
+	for(auto creature : army->Slots())
+	{
+		CreatureID initial = creature.second->getCreatureID();
+		auto possibleUpgrades = initial.toCreature()->upgrades;
+
+		vstd::erase_if(possibleUpgrades, [&](CreatureID creID) -> bool
+		{
+			for(auto pair : dwelling->creatures)
+			{
+				if(vstd::contains(pair.second, creID))
+					return false;
+			}
+
+			return true;
+		});
+
+		if(possibleUpgrades.empty())
+			continue;
+
+		CreatureID strongestUpgrade = *vstd::minElementByFun(possibleUpgrades, [](CreatureID cre) -> uint64_t
+		{
+			return cre.toCreature()->AIValue;
+		});
+
+		UpgradeInfo upgrade = UpgradeInfo(initial, strongestUpgrade, creature.second->count);
+
+		upgrades.push_back(upgrade);
+	}
+
 	return upgrades;
 }
 
@@ -304,6 +333,9 @@ ArmyUpgradeInfo ArmyManager::calculateCreateresUpgrade(
 	const CGObjectInstance * upgrader,
 	const TResources & availableResources) const
 {
+	if(!upgrader)
+		return ArmyUpgradeInfo();
+
 	std::vector<UpgradeInfo> upgrades = getPossibleUpgrades(army, upgrader);
 
 	vstd::erase_if(upgrades, [&](const UpgradeInfo & u) -> bool

+ 3 - 1
AI/Nullkiller/Behaviors/BuildingBehavior.cpp

@@ -49,6 +49,7 @@ Goals::TGoalVec BuildingBehavior::getTasks()
 		totalDevelopmentCost.toString());
 
 	auto & developmentInfos = ai->nullkiller->buildAnalyzer->getDevelopmentInfo();
+	auto goldPreasure = ai->nullkiller->buildAnalyzer->getGoldPreasure();
 
 	for(auto & developmentInfo : developmentInfos)
 	{
@@ -56,7 +57,8 @@ Goals::TGoalVec BuildingBehavior::getTasks()
 
 		for(auto & buildingInfo : developmentInfo.toBuild)
 		{
-			tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo)));
+			if(goldPreasure < MAX_GOLD_PEASURE || buildingInfo.dailyIncome[Res::GOLD] > 0)
+				tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo)));
 		}
 	}
 

+ 4 - 0
AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp

@@ -14,6 +14,7 @@
 #include "../AIUtility.h"
 #include "../Goals/BuyArmy.h"
 #include "../Goals/VisitTile.h"
+#include "../Engine/Nullkiller.h"
 #include "lib/mapping/CMap.h" //for victory conditions
 #include "lib/CPathfinder.h"
 
@@ -34,6 +35,9 @@ Goals::TGoalVec BuyArmyBehavior::getTasks()
 
 	if(cb->getDate(Date::DAY) == 1)
 		return tasks;
+
+	if(ai->nullkiller->buildAnalyzer->getGoldPreasure() > MAX_GOLD_PEASURE)
+		return tasks;
 		
 	auto heroes = cb->getHeroesInfo();
 

+ 13 - 10
AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp

@@ -38,11 +38,11 @@ Goals::TGoalVec CaptureObjectsBehavior::getTasks()
 			return;
 		}
 
-		logAi->trace("Scanning objects, count %d", objs.size());
+		logAi->debug("Scanning objects, count %d", objs.size());
 
 		for(auto objToVisit : objs)
 		{			
-#ifdef VCMI_TRACE_PATHFINDER
+#ifdef AI_TRACE_LEVEL >= 1
 			logAi->trace("Checking object %s, %s", objToVisit->getObjectName(), objToVisit->visitablePos().toString());
 #endif
 
@@ -55,19 +55,19 @@ Goals::TGoalVec CaptureObjectsBehavior::getTasks()
 			std::vector<std::shared_ptr<ExecuteHeroChain>> waysToVisitObj;
 			std::shared_ptr<ExecuteHeroChain> closestWay;
 					
-#ifdef VCMI_TRACE_PATHFINDER
+#ifdef AI_TRACE_LEVEL >= 1
 			logAi->trace("Found %d paths", paths.size());
 #endif
 
 			for(auto & path : paths)
 			{
-#ifdef VCMI_TRACE_PATHFINDER
+#ifdef AI_TRACE_LEVEL >= 2
 				logAi->trace("Path found %s", path.toString());
 #endif
 
 				if(path.getFirstBlockedAction())
 				{
-#ifdef VCMI_TRACE_PATHFINDER
+#ifdef AI_TRACE_LEVEL >= 2
 					// TODO: decomposition?
 					logAi->trace("Ignore path. Action is blocked.");
 #endif
@@ -76,8 +76,8 @@ 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. Our power %d", path.heroArmy->getArmyStrength());
+#ifdef AI_TRACE_LEVEL >= 2
+					logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength());
 #endif
 					continue;
 				}
@@ -93,11 +93,11 @@ Goals::TGoalVec CaptureObjectsBehavior::getTasks()
 
 				auto isSafe = isSafeToVisit(hero, path.heroArmy, danger);
 				
-#ifdef VCMI_TRACE_PATHFINDER
+#ifdef AI_TRACE_LEVEL >= 2
 				logAi->trace(
 					"It is %s to visit %s by %s with army %lld, danger %lld and army loss %lld", 
 					isSafe ? "safe" : "not safe",
-					objToVisit->instanceName, 
+					objToVisit->getObjectName(), 
 					hero->name,
 					path.getHeroStrength(),
 					danger,
@@ -123,8 +123,11 @@ Goals::TGoalVec CaptureObjectsBehavior::getTasks()
 				if(ai->nullkiller->arePathHeroesLocked(way->getPath()))
 					continue;
 
+				if(ai->nullkiller->getHeroLockedReason(way->hero.get()) == HeroLockedReason::STARTUP)
+					continue;
+
 				way->evaluationContext.closestWayRatio
-					= way->evaluationContext.movementCost / closestWay->evaluationContext.movementCost;
+					= closestWay->evaluationContext.movementCost / way->evaluationContext.movementCost;
 
 				tasks.push_back(sptr(*way));
 			}

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

@@ -177,7 +177,7 @@ Goals::TGoalVec StartupBehavior::getTasks()
 				&& !town->visitingHero
 				&& ai->nullkiller->getHeroLockedReason(town->garrisonHero) != HeroLockedReason::DEFENCE)
 			{
-				tasks.push_back(Goals::sptr(ExchangeSwapTownHeroes(town, nullptr).setpriority(0.0001f)));
+				tasks.push_back(Goals::sptr(ExchangeSwapTownHeroes(town, nullptr).setpriority(MIN_PRIORITY)));
 			}
 		}
 	}

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

@@ -84,7 +84,7 @@ void Nullkiller::updateAiState()
 	{
 		auto lockedReason = getHeroLockedReason(hero.h);
 
-		return lockedReason == HeroLockedReason::DEFENCE || lockedReason == HeroLockedReason::STARTUP;
+		return lockedReason == HeroLockedReason::DEFENCE;
 	});
 
 	ai->ah->updatePaths(activeHeroes, true);
@@ -134,6 +134,13 @@ void Nullkiller::makeTurn()
 			return;
 		}
 
+		if(bestTask->priority < MIN_PRIORITY)
+		{
+			logAi->trace("Goal %s has too low priority. It is not worth doing it. Ending turn.", bestTask->name());
+
+			return;
+		}
+
 		logAi->debug("Trying to realize %s (value %2.3f)", bestTask->name(), bestTask->priority);
 
 		try

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

@@ -15,6 +15,9 @@
 #include "../Goals/AbstractGoal.h"
 #include "../Behaviors/Behavior.h"
 
+const float MAX_GOLD_PEASURE = 0.3f;
+const float MIN_PRIORITY = 0.01f;
+
 enum class HeroLockedReason
 {
 	NOT_LOCKED = 0,

+ 78 - 13
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -49,6 +49,7 @@ void PriorityEvaluator::initVisitTile()
 	armyLossPersentageVariable = engine->getInputVariable("armyLoss");
 	heroRoleVariable = engine->getInputVariable("heroRole");
 	dangerVariable = engine->getInputVariable("danger");
+	turnVariable = engine->getInputVariable("turn");
 	mainTurnDistanceVariable = engine->getInputVariable("mainTurnDistance");
 	scoutTurnDistanceVariable = engine->getInputVariable("scoutTurnDistance");
 	goldRewardVariable = engine->getInputVariable("goldReward");
@@ -57,6 +58,8 @@ void PriorityEvaluator::initVisitTile()
 	rewardTypeVariable = engine->getInputVariable("rewardType");
 	closestHeroRatioVariable = engine->getInputVariable("closestHeroRatio");
 	strategicalValueVariable = engine->getInputVariable("strategicalValue");
+	goldPreasureVariable = engine->getInputVariable("goldPreasure");
+	goldCostVariable = engine->getInputVariable("goldCost");
 	value = engine->getOutputVariable("Value");
 }
 
@@ -119,6 +122,25 @@ uint64_t getDwellingScore(const CGObjectInstance * target, bool checkGold)
 	return score;
 }
 
+int getDwellingArmyCost(const CGObjectInstance * target)
+{
+	auto dwelling = dynamic_cast<const CGDwelling *>(target);
+	int cost = 0;
+
+	for(auto & creLevel : dwelling->creatures)
+	{
+		if(creLevel.first && creLevel.second.size())
+		{
+			auto creature = creLevel.second.back().toCreature();
+			auto creaturesAreFree = creature->level == 1;
+			if(!creaturesAreFree)
+				cost += creature->cost[Res::GOLD] * creLevel.first;
+		}
+	}
+
+	return cost;
+}
+
 uint64_t evaluateArtifactArmyValue(CArtifactInstance * art)
 {
 	if(art->artType->id == ArtifactID::SPELL_SCROLL)
@@ -192,6 +214,30 @@ uint64_t getArmyReward(const CGObjectInstance * target, const CGHeroInstance * h
 	}
 }
 
+int getGoldCost(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army)
+{
+	if(!target)
+		return 0;
+
+	switch(target->ID)
+	{
+	case Obj::HILL_FORT:
+		return ai->ah->calculateCreateresUpgrade(army, target, cb->getResourceAmount()).upgradeCost[Res::GOLD];
+	case Obj::SCHOOL_OF_MAGIC:
+	case Obj::SCHOOL_OF_WAR:
+		return 1000;
+	case Obj::UNIVERSITY:
+		return 2000;
+	case Obj::CREATURE_GENERATOR1:
+	case Obj::CREATURE_GENERATOR2:
+	case Obj::CREATURE_GENERATOR3:
+	case Obj::CREATURE_GENERATOR4:
+		return getDwellingArmyCost(target);
+	default:
+		return 0;
+	}
+}
+
 float getStrategicalValue(const CGObjectInstance * target);
 
 float getEnemyHeroStrategicalValue(const CGHeroInstance * enemy)
@@ -216,9 +262,11 @@ float getResourceRequirementStrength(int resType)
 		return 0;
 
 	if(dailyIncome[resType] == 0)
-		return 1;
+		return 1.0f;
 
-	return (float)requiredResources[resType] / dailyIncome[resType] / 3;
+	float ratio = (float)requiredResources[resType] / dailyIncome[resType] / 2;
+
+	return std::min(ratio, 1.0f);
 }
 
 float getTotalResourceRequirementStrength(int resType)
@@ -229,10 +277,11 @@ float getTotalResourceRequirementStrength(int resType)
 	if(requiredResources[resType] == 0)
 		return 0;
 
-	if(dailyIncome[resType] == 0)
-		return requiredResources[resType] / 30;
+	float ratio = dailyIncome[resType] == 0
+		? requiredResources[resType] / 50
+		: (float)requiredResources[resType] / dailyIncome[resType] / 50;
 
-	return (float)requiredResources[resType] / dailyIncome[resType] / 30;
+	return std::min(ratio, 1.0f);
 }
 
 float getStrategicalValue(const CGObjectInstance * target)
@@ -243,18 +292,21 @@ 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);
+		return target->subID == Res::GOLD ? 0.5f : 0.05f * getTotalResourceRequirementStrength(target->subID) + 0.05f * getResourceRequirementStrength(target->subID);
 
 	case Obj::RESOURCE:
-		return target->subID == Res::GOLD ? 0 : 0.5f * getResourceRequirementStrength(target->subID);
+		return target->subID == Res::GOLD ? 0 : 0.3f * getResourceRequirementStrength(target->subID);
 
 	case Obj::TOWN:
-		return target->tempOwner == PlayerColor::NEUTRAL ? 0.5 : 1;
+		return dynamic_cast<const CGTownInstance *>(target)->hasFort()
+			? (target->tempOwner == PlayerColor::NEUTRAL ? 0.8f : 1.0f)
+			: 0.4f;
 
 	case Obj::HERO:
 		return cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES
 			? getEnemyHeroStrategicalValue(dynamic_cast<const CGHeroInstance *>(target))
 			: 0;
+
 	default:
 		return 0;
 	}
@@ -388,13 +440,16 @@ public:
 		auto day = cb->getDate(Date::DAY);
 		auto hero = heroPtr.get();
 		bool checkGold = evaluationContext.danger == 0;
+		auto army = chain.getPath().heroArmy;
 
 		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, chain.getPath().heroArmy, checkGold);
+		evaluationContext.armyReward = getArmyReward(target, hero, army, checkGold);
 		evaluationContext.skillReward = getSkillReward(target, hero, evaluationContext.heroRole);
 		evaluationContext.strategicalValue = getStrategicalValue(target);
+		evaluationContext.goldCost = getGoldCost(target, hero, army);
+		evaluationContext.turn = chain.getPath().turn();
 
 		return evaluationContext;
 	}
@@ -409,15 +464,16 @@ public:
 		Goals::BuildThis & buildThis = dynamic_cast<Goals::BuildThis &>(*task);
 		auto & bi = buildThis.buildingInfo;
 		
-		evaluationContext.goldReward = bi.dailyIncome[Res::GOLD] / 2;
+		evaluationContext.goldReward = 7 * bi.dailyIncome[Res::GOLD] / 2; // 7 day income but half we already have
 		evaluationContext.heroRole = HeroRole::MAIN;
 		evaluationContext.movementCostByRole[evaluationContext.heroRole] = bi.prerequisitesCount;
 		evaluationContext.armyReward = 0;
 		evaluationContext.strategicalValue = buildThis.townInfo.armyScore / 50000.0;
+		evaluationContext.goldCost = bi.buildCostWithPrerequisits[Res::GOLD];
 
 		if(bi.creatureID != CreatureID::NONE)
 		{
-			evaluationContext.strategicalValue += 0.5f + 0.1f * bi.creatureLevel;
+			evaluationContext.strategicalValue += (0.5f + 0.1f * bi.creatureLevel) / (float)bi.prerequisitesCount;
 
 			if(bi.baseCreatureID == bi.creatureID)
 			{
@@ -429,7 +485,11 @@ public:
 
 			evaluationContext.armyReward = upgradedPower - creaturesToUpgrade.power;
 		}
-		
+		else
+		{
+			evaluationContext.strategicalValue = ai->nullkiller->buildAnalyzer->getGoldPreasure() * evaluationContext.goldReward / 300.0f;
+		}
+
 		return evaluationContext;
 	}
 };
@@ -485,6 +545,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
 		rewardTypeVariable->setValue(rewardType);
 		closestHeroRatioVariable->setValue(evaluationContext.closestWayRatio);
 		strategicalValueVariable->setValue(evaluationContext.strategicalValue);
+		goldPreasureVariable->setValue(ai->nullkiller->buildAnalyzer->getGoldPreasure());
+		goldCostVariable->setValue(evaluationContext.goldCost / ((float)cb->getResourceAmount(Res::GOLD) + (float)ai->nullkiller->buildAnalyzer->getDailyIncome()[Res::GOLD] + 1.0f));
+		turnVariable->setValue(evaluationContext.turn);
 
 		engine->process();
 		//engine.process(VISIT_TILE); //TODO: Process only Visit_Tile
@@ -497,16 +560,18 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
 	assert(result >= 0);
 
 #ifdef VCMI_TRACE_PATHFINDER
-	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",
+	logAi->trace("Evaluated %s, loss: %f, turns main: %f, scout: %f, gold: %d, cost: %d, army gain: %d, danger: %d, role: %s, strategical value: %f, cwr: %f, result %f",
 		task->name(),
 		evaluationContext.armyLossPersentage,
 		evaluationContext.movementCostByRole[HeroRole::MAIN],
 		evaluationContext.movementCostByRole[HeroRole::SCOUT],
 		evaluationContext.goldReward,
+		evaluationContext.goldCost,
 		evaluationContext.armyReward,
 		evaluationContext.danger,
 		evaluationContext.heroRole ? "scout" : "main",
 		evaluationContext.strategicalValue,
+		evaluationContext.closestWayRatio,
 		result);
 #endif
 

+ 3 - 0
AI/Nullkiller/Engine/PriorityEvaluator.h

@@ -32,6 +32,7 @@ private:
 	fl::InputVariable * heroRoleVariable;
 	fl::InputVariable * mainTurnDistanceVariable;
 	fl::InputVariable * scoutTurnDistanceVariable;
+	fl::InputVariable * turnVariable;
 	fl::InputVariable * goldRewardVariable;
 	fl::InputVariable * armyRewardVariable;
 	fl::InputVariable * dangerVariable;
@@ -39,6 +40,8 @@ private:
 	fl::InputVariable * strategicalValueVariable;
 	fl::InputVariable * rewardTypeVariable;
 	fl::InputVariable * closestHeroRatioVariable;
+	fl::InputVariable * goldPreasureVariable;
+	fl::InputVariable * goldCostVariable;
 	fl::OutputVariable * value;
 	std::map<Goals::EGoals, std::shared_ptr<IEvaluationContextBuilder>> evaluationContextBuilders;
 

+ 8 - 5
AI/Nullkiller/FuzzyHelper.cpp

@@ -210,7 +210,7 @@ ui64 FuzzyHelper::evaluateDanger(crint3 tile, const CGHeroInstance * visitor)
 	return evaluateDanger(tile, visitor, ai.get());
 }
 
-ui64 FuzzyHelper::evaluateDanger(crint3 tile, const CGHeroInstance * visitor, const VCAI * ai)
+ui64 FuzzyHelper::evaluateDanger(crint3 tile, const CGHeroInstance * visitor, const VCAI * ai, bool checkGuards)
 {
 	auto cb = ai->myCb;
 	const TerrainTile * t = cb->getTile(tile, false);
@@ -260,12 +260,15 @@ ui64 FuzzyHelper::evaluateDanger(crint3 tile, const CGHeroInstance * visitor, co
 		}
 	}
 
-	auto guards = cb->getGuardingCreatures(tile);
-	for(auto cre : guards)
+	if(checkGuards)
 	{
-		float tacticalAdvantage = tacticalAdvantageEngine.getTacticalAdvantage(visitor, dynamic_cast<const CArmedInstance *>(cre));
+		auto guards = cb->getGuardingCreatures(tile);
+		for(auto cre : guards)
+		{
+			float tacticalAdvantage = tacticalAdvantageEngine.getTacticalAdvantage(visitor, dynamic_cast<const CArmedInstance *>(cre));
 
-		vstd::amax(guardDanger, evaluateDanger(cre, ai) * tacticalAdvantage); //we are interested in strongest monster around
+			vstd::amax(guardDanger, evaluateDanger(cre, ai) * tacticalAdvantage); //we are interested in strongest monster around
+		}
 	}
 
 	//TODO mozna odwiedzic blockvis nie ruszajac straznika

+ 1 - 1
AI/Nullkiller/FuzzyHelper.h

@@ -44,6 +44,6 @@ public:
 	//std::shared_ptr<AbstractGoal> chooseSolution (std::vector<std::shared_ptr<AbstractGoal>> & vec);
 
 	ui64 evaluateDanger(const CGObjectInstance * obj, const VCAI * ai);
-	ui64 evaluateDanger(crint3 tile, const CGHeroInstance * visitor, const VCAI * ai);
+	ui64 evaluateDanger(crint3 tile, const CGHeroInstance * visitor, const VCAI * ai, bool checkGuards = true);
 	ui64 evaluateDanger(crint3 tile, const CGHeroInstance * visitor);
 };

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

@@ -104,9 +104,11 @@ namespace Goals
 		float armyLossPersentage;
 		float armyReward;
 		int32_t goldReward;
+		int32_t goldCost;
 		float skillReward;
 		float strategicalValue;
 		HeroRole heroRole;
+		uint8_t turn;
 
 		EvaluationContext()
 			: movementCost(0.0),
@@ -118,9 +120,11 @@ namespace Goals
 			movementCostByRole(),
 			skillReward(0),
 			goldReward(0),
+			goldCost(0),
 			armyReward(0),
 			armyLossPersentage(0),
-			heroRole(HeroRole::SCOUT)
+			heroRole(HeroRole::SCOUT),
+			turn(0)
 		{
 		}
 	};

+ 2 - 2
AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp

@@ -69,7 +69,7 @@ void ExchangeSwapTownHeroes::accept(VCAI * ai)
 	if(town->visitingHero && town->visitingHero.get() != garrisonHero)
 		cb->swapGarrisonHero(town);
 
-	makePossibleUpgrades(town);
+	ai->makePossibleUpgrades(town);
 	ai->moveHeroToTile(town->visitablePos(), garrisonHero);
 
 	auto upperArmy = town->getUpperArmy();
@@ -98,7 +98,7 @@ void ExchangeSwapTownHeroes::accept(VCAI * ai)
 	if(town->visitingHero && town->visitingHero != garrisonHero)
 	{
 		ai->nullkiller->unlockHero(town->visitingHero.get());
-		makePossibleUpgrades(town->visitingHero);
+		ai->makePossibleUpgrades(town->visitingHero);
 	}
 
 	logAi->debug("Put hero %s to garrison of %s", garrisonHero->name, town->name);

+ 16 - 7
AI/Nullkiller/Pathfinding/AINodeStorage.cpp

@@ -11,6 +11,8 @@
 #include "AINodeStorage.h"
 #include "Actions/TownPortalAction.h"
 #include "../Goals/Goals.h"
+#include "../VCAI.h"
+#include "../Engine/Nullkiller.h"
 #include "../../../CCallback.h"
 #include "../../../lib/mapping/CMap.h"
 #include "../../../lib/mapObjects/MapObjects.h"
@@ -278,6 +280,7 @@ bool AINodeStorage::calculateHeroChainFinal()
 			for(AIPathNode & node : chains)
 			{
 				if(node.turns > heroChainTurn
+					&& !node.locked
 					&& node.action != CGPathNode::ENodeAction::UNKNOWN
 					&& node.actor->actorExchangeCount > 1
 					&& !hasBetterChain(&node, &node, chains))
@@ -450,7 +453,7 @@ void AINodeStorage::calculateHeroChain(
 	if(carrier->armyLoss < carrier->actor->armyValue
 		&& (carrier->action != CGPathNode::BATTLE || (carrier->actor->allowBattle && carrier->specialAction))
 		&& carrier->action != CGPathNode::BLOCKING_VISIT
-		&& other->armyLoss < other->actor->armyValue
+		&& (other->armyLoss == 0 || other->armyLoss < other->actor->armyValue)
 		&& carrier->actor->canExchange(other->actor))
 	{
 #if AI_TRACE_LEVEL >= 2
@@ -632,18 +635,24 @@ void AINodeStorage::setTownsAndDwellings(
 	{
 		uint64_t mask = 1 << actors.size();
 
-		if(!town->garrisonHero && town->getUpperArmy()->getArmyStrength())
+		if(!town->garrisonHero || ai->nullkiller->getHeroLockedReason(town->garrisonHero) != HeroLockedReason::DEFENCE)
 		{
 			actors.push_back(std::make_shared<TownGarrisonActor>(town, mask));
 		}
 	}
 
 	/*auto dayOfWeek = cb->getDate(Date::DAY_OF_WEEK);
-	auto waitForGrowth = dayOfWeek > 4;
+	auto waitForGrowth = dayOfWeek > 4;*/
 
 	for(auto obj: visitableObjs)
 	{
-		const CGDwelling * dwelling = dynamic_cast<const CGDwelling *>(obj);
+		if(obj->ID == Obj::HILL_FORT)
+		{
+			uint64_t mask = 1 << actors.size();
+
+			actors.push_back(std::make_shared<HillFortActor>(obj, mask));
+		}
+		/*const CGDwelling * dwelling = dynamic_cast<const CGDwelling *>(obj);
 
 		if(dwelling)
 		{
@@ -665,8 +674,8 @@ void AINodeStorage::setTownsAndDwellings(
 					actors.push_back(dwellingActor);
 				}
 			}
-		}
-	}*/
+		}*/
+	}
 }
 
 std::vector<CGPathNode *> AINodeStorage::calculateTeleportations(
@@ -979,7 +988,7 @@ std::vector<AIPath> AINodeStorage::getChainInfo(const int3 & pos, bool isOnLand)
 		path.targetHero = node.actor->hero;
 		path.heroArmy = node.actor->creatureSet;
 		path.armyLoss = node.armyLoss;
-		path.targetObjectDanger = evaluateDanger(pos, path.targetHero);
+		path.targetObjectDanger = evaluateDanger(pos, path.targetHero, false);
 		path.targetObjectArmyLoss = evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), path.targetObjectDanger);
 		path.chainMask = node.actor->chainMask;
 		path.exchangeCount = node.actor->actorExchangeCount;

+ 7 - 6
AI/Nullkiller/Pathfinding/AINodeStorage.h

@@ -159,13 +159,14 @@ public:
 
 	bool isMovementIneficient(const PathNodeInfo & source, CDestinationNodeInfo & destination) const
 	{
-		// further chain distribution is calculated as the last stage
-		if(heroChainPass == EHeroChainPass::CHAIN && destination.node->turns > heroChainTurn)
-			return true;
-
 		return hasBetterChain(source, destination);
 	}
 
+	bool isDistanceLimitReached(const PathNodeInfo & source, CDestinationNodeInfo & destination) const
+	{
+		return heroChainPass == EHeroChainPass::CHAIN && destination.node->turns > heroChainTurn;
+	}
+
 	template<class NodeRange>
 	bool hasBetterChain(
 		const CGPathNode * source, 
@@ -185,9 +186,9 @@ public:
 	bool calculateHeroChain();
 	bool calculateHeroChainFinal();
 
-	uint64_t evaluateDanger(const int3 &  tile, const CGHeroInstance * hero) const
+	uint64_t evaluateDanger(const int3 &  tile, const CGHeroInstance * hero, bool checkGuards) const
 	{
-		return dangerEvaluator->evaluateDanger(tile, hero, ai);
+		return dangerEvaluator->evaluateDanger(tile, hero, ai, checkGuards);
 	}
 
 	uint64_t evaluateArmyLoss(const CGHeroInstance * hero, uint64_t armyValue, uint64_t danger) const

+ 93 - 12
AI/Nullkiller/Pathfinding/Actors.cpp

@@ -16,6 +16,8 @@
 #include "../../../lib/mapping/CMap.h"
 #include "../../../lib/mapObjects/MapObjects.h"
 
+CCreatureSet emptyArmy;
+
 bool HeroExchangeArmy::needsLastStack() const
 {
 	return true;
@@ -56,6 +58,21 @@ std::string ChainActor::toString() const
 	return hero->name;
 }
 
+ObjectActor::ObjectActor(const CGObjectInstance * obj, const CCreatureSet * army, uint64_t chainMask, int initialTurn)
+	:ChainActor(obj, army, chainMask, initialTurn), object(obj)
+{
+}
+
+const CGObjectInstance * ObjectActor::getActorObject() const
+{
+	return object;
+}
+
+std::string ObjectActor::toString() const
+{
+	return object->getObjectName() + " at " + object->visitablePos().toString();
+}
+
 HeroActor::HeroActor(const CGHeroInstance * hero, uint64_t chainMask, const VCAI * ai)
 	:ChainActor(hero, chainMask)
 {
@@ -151,18 +168,38 @@ bool HeroExchangeMap::canExchange(const ChainActor * other)
 
 		if(result)
 		{
-			if(other->armyCost.nonZero())
-			{
-				TResources resources = ai->myCb->getResourceAmount();
+			TResources resources = ai->myCb->getResourceAmount();
 
-				if(!resources.canAfford(actor->armyCost + other->armyCost))
-				{
-					result = false;
-					return;
-				}
+			if(!resources.canAfford(actor->armyCost + other->armyCost))
+			{
+				result = false;
+#if AI_TRACE_LEVEL >= 2
+				logAi->trace(
+					"Can not afford exchange because of total cost %s but we have %s",
+					(actor->armyCost + other->armyCost).toString(),
+					resources.toString());
+#endif
+				return;
 			}
 
-			uint64_t reinforcment = ai->ah->howManyReinforcementsCanGet(actor->creatureSet, other->creatureSet);
+			auto upgradeInfo = ai->ah->calculateCreateresUpgrade(
+				actor->creatureSet, 
+				other->getActorObject(),
+				resources - actor->armyCost - other->armyCost);
+
+			uint64_t reinforcment = upgradeInfo.upgradeValue;
+			
+			if(other->creatureSet->Slots().size())
+				reinforcment += ai->ah->howManyReinforcementsCanGet(actor->creatureSet, other->creatureSet);
+
+#if AI_TRACE_LEVEL >= 2
+			logAi->trace(
+				"Exchange %s->%s reinforcement: %d, %f%%",
+				actor->toString(),
+				other->toString(),
+				reinforcment,
+				100.0f * reinforcment / actor->armyValue);
+#endif
 
 			result = reinforcment > actor->armyValue / 10 || reinforcment > 1000;
 		}
@@ -204,7 +241,27 @@ HeroActor * HeroExchangeMap::exchange(const ChainActor * other)
 		result = exchangeMap.at(other);
 	else 
 	{
-		CCreatureSet * newArmy = pickBestCreatures(actor->creatureSet, other->creatureSet);
+		TResources availableResources = ai->myCb->getResourceAmount() - actor->armyCost - other->armyCost;
+		CCreatureSet * upgradedInitialArmy = tryUpgrade(actor->creatureSet, other->getActorObject(), availableResources);
+		CCreatureSet * newArmy;
+		
+		if(other->creatureSet->Slots().size())
+		{
+			if(upgradedInitialArmy)
+			{
+				newArmy = pickBestCreatures(upgradedInitialArmy, other->creatureSet);
+				delete upgradedInitialArmy;
+			}
+			else
+			{
+				newArmy = pickBestCreatures(actor->creatureSet, other->creatureSet);
+			}
+		}
+		else
+		{
+			newArmy = upgradedInitialArmy;
+		}
+
 		result = new HeroActor(actor, other, newArmy, ai);
 		exchangeMap[other] = result;
 	}
@@ -212,6 +269,25 @@ HeroActor * HeroExchangeMap::exchange(const ChainActor * other)
 	return result;
 }
 
+CCreatureSet * HeroExchangeMap::tryUpgrade(const CCreatureSet * army, const CGObjectInstance * upgrader, TResources resources) const
+{
+	auto upgradeInfo = ai->ah->calculateCreateresUpgrade(army, upgrader, resources);
+
+	if(!upgradeInfo.upgradeValue)
+		return nullptr;
+
+	CCreatureSet * target = new HeroExchangeArmy();
+
+	for(auto & slotInfo : upgradeInfo.resultingArmy)
+	{
+		auto targetSlot = target->getFreeSlot();
+
+		target->addToSlot(targetSlot, slotInfo.creature->idNumber, TQuantity(slotInfo.count));
+	}
+
+	return target;
+}
+
 CCreatureSet * HeroExchangeMap::pickBestCreatures(const CCreatureSet * army1, const CCreatureSet * army2) const
 {
 	CCreatureSet * target = new HeroExchangeArmy();
@@ -227,8 +303,13 @@ CCreatureSet * HeroExchangeMap::pickBestCreatures(const CCreatureSet * army1, co
 	return target;
 }
 
+HillFortActor::HillFortActor(const CGObjectInstance * hillFort, uint64_t chainMask)
+	:ObjectActor(hillFort, &emptyArmy, chainMask, 0)
+{
+}
+
 DwellingActor::DwellingActor(const CGDwelling * dwelling, uint64_t chainMask, bool waitForGrowth, int dayOfWeek)
-	:ChainActor(
+	:ObjectActor(
 		dwelling, 
 		getDwellingCreatures(dwelling, waitForGrowth), 
 		chainMask, 
@@ -288,7 +369,7 @@ CCreatureSet * DwellingActor::getDwellingCreatures(const CGDwelling * dwelling,
 }
 
 TownGarrisonActor::TownGarrisonActor(const CGTownInstance * town, uint64_t chainMask)
-	:ChainActor(town, town->getUpperArmy(), chainMask, 0), town(town)
+	:ObjectActor(town, town->getUpperArmy(), chainMask, 0), town(town)
 {
 }
 

+ 21 - 2
AI/Nullkiller/Pathfinding/Actors.h

@@ -60,6 +60,7 @@ public:
 	virtual std::string toString() const;
 	ChainActor * exchange(const ChainActor * other) const { return exchange(this, other); }
 	void setBaseActor(HeroActor * base);
+	virtual const CGObjectInstance * getActorObject() const	{ return hero; }
 
 protected:
 	virtual ChainActor * exchange(const ChainActor * specialActor, const ChainActor * other) const;
@@ -86,6 +87,7 @@ public:
 
 private:
 	CCreatureSet * pickBestCreatures(const CCreatureSet * army1, const CCreatureSet * army2) const;
+	CCreatureSet * tryUpgrade(const CCreatureSet * army, const CGObjectInstance * upgrader, TResources resources) const;
 };
 
 class HeroActor : public ChainActor
@@ -112,7 +114,24 @@ protected:
 	virtual ChainActor * exchange(const ChainActor * specialActor, const ChainActor * other) const override;
 };
 
-class DwellingActor : public ChainActor
+class ObjectActor : public ChainActor
+{
+private:
+	const CGObjectInstance * object;
+
+public:
+	ObjectActor(const CGObjectInstance * obj, const CCreatureSet * army, uint64_t chainMask, int initialTurn);
+	virtual std::string toString() const override;
+	const CGObjectInstance * getActorObject() const override;
+};
+
+class HillFortActor : public ObjectActor
+{
+public:
+	HillFortActor(const CGObjectInstance * hillFort, uint64_t chainMask);
+};
+
+class DwellingActor : public ObjectActor
 {
 private:
 	const CGDwelling * dwelling;
@@ -127,7 +146,7 @@ protected:
 	CCreatureSet * getDwellingCreatures(const CGDwelling * dwelling, bool waitForGrowth);
 };
 
-class TownGarrisonActor : public ChainActor
+class TownGarrisonActor : public ObjectActor
 {
 private:
 	const CGTownInstance * town;

+ 118 - 98
AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp

@@ -47,151 +47,171 @@ namespace AIPathfinding
 	{
 		if(nodeStorage->isMovementIneficient(source, destination))
 		{
+			destination.node->locked = true;
 			destination.blocked = true;
 
 			return;
 		}
 
 		auto blocker = getBlockingReason(source, destination, pathfinderConfig, pathfinderHelper);
-
 		if(blocker == BlockingReason::NONE)
 			return;
 
-		if(blocker == BlockingReason::DESTINATION_BLOCKVIS && destination.nodeObject)
+		auto destGuardians = cb->getGuardingCreatures(destination.coord);
+		bool allowBypass = true;
+
+		switch(blocker)
 		{
-			auto enemyHero = destination.nodeHero && destination.heroRelations == PlayerRelations::ENEMIES;
+		case BlockingReason::DESTINATION_GUARDED:
+			allowBypass = bypassDestinationGuards(destGuardians, source, destination, pathfinderConfig, pathfinderHelper);
 
-			if(!enemyHero && !isObjectRemovable(destination.nodeObject))
-			{
-				if(nodeStorage->getHero(destination.node) == destination.nodeHero)
-					return;
+			break;
 
-				destination.blocked = true;
-			}
+		case BlockingReason::DESTINATION_BLOCKVIS:
+			allowBypass = destination.nodeObject && bypassRemovableObject(source, destination, pathfinderConfig, pathfinderHelper);
+			
+			if(allowBypass && destGuardians.size())
+				allowBypass = bypassDestinationGuards(destGuardians, source, destination, pathfinderConfig, pathfinderHelper);
 
-			if(destination.nodeObject->ID == Obj::QUEST_GUARD || destination.nodeObject->ID == Obj::BORDERGUARD)
-			{
-				auto questObj = dynamic_cast<const IQuestObject *>(destination.nodeObject);
-				auto nodeHero = pathfinderHelper->hero;
-				
-				if(!destination.nodeObject->wasVisited(nodeHero->tempOwner)
-					|| !questObj->checkQuest(nodeHero))
-				{
-					nodeStorage->updateAINode(destination.node, [&](AIPathNode * node)
-					{
-						auto questInfo = QuestInfo(questObj->quest, destination.nodeObject, destination.coord);
+			break;
+		}
 
-						node->specialAction.reset(new QuestAction(questInfo));
-					});
-				}
-			}
+		destination.blocked = !allowBypass || nodeStorage->isDistanceLimitReached(source, destination);
+		destination.node->locked = !allowBypass;
+	}
 
-			return;
-		}
+	bool AIMovementAfterDestinationRule::bypassRemovableObject(
+		const PathNodeInfo & source,
+		CDestinationNodeInfo & destination,
+		const PathfinderConfig * pathfinderConfig,
+		CPathfinderHelper * pathfinderHelper) const
+	{
+		auto enemyHero = destination.nodeHero && destination.heroRelations == PlayerRelations::ENEMIES;
 
-		if(blocker == BlockingReason::DESTINATION_VISIT)
+		if(!enemyHero && !isObjectRemovable(destination.nodeObject))
 		{
-			return;
+			if(nodeStorage->getHero(destination.node) == destination.nodeHero)
+				return true;
+
+			return false;
 		}
 
-		if(blocker == BlockingReason::DESTINATION_GUARDED)
+		if(destination.nodeObject->ID == Obj::QUEST_GUARD || destination.nodeObject->ID == Obj::BORDERGUARD)
 		{
-			auto srcGuardians = cb->getGuardingCreatures(source.coord);
-			auto destGuardians = cb->getGuardingCreatures(destination.coord);
+			auto questObj = dynamic_cast<const IQuestObject *>(destination.nodeObject);
+			auto nodeHero = pathfinderHelper->hero;
 
-			if(destGuardians.empty())
+			if(!destination.nodeObject->wasVisited(nodeHero->tempOwner)
+				|| !questObj->checkQuest(nodeHero))
 			{
-				destination.blocked = true;
+				nodeStorage->updateAINode(destination.node, [&](AIPathNode * node)
+				{
+					auto questInfo = QuestInfo(questObj->quest, destination.nodeObject, destination.coord);
 
-				return;
+					node->specialAction.reset(new QuestAction(questInfo));
+				});
 			}
+		}
 
-			vstd::erase_if(destGuardians, [&](const CGObjectInstance * destGuard) -> bool
-			{
-				return vstd::contains(srcGuardians, destGuard);
-			});
+		return true;
+	}
 
-			auto guardsAlreadyBypassed = destGuardians.empty() && srcGuardians.size();
-			auto srcNode = nodeStorage->getAINode(source.node);
-			if(guardsAlreadyBypassed && srcNode->actor->allowBattle)
-			{
-#ifdef VCMI_TRACE_PATHFINDER
-				logAi->trace(
-					"Bypass guard at destination while moving %s -> %s",
-					source.coord.toString(),
-					destination.coord.toString());
-#endif
+	bool AIMovementAfterDestinationRule::bypassDestinationGuards(
+		std::vector<const CGObjectInstance *> destGuardians,
+		const PathNodeInfo & source,
+		CDestinationNodeInfo & destination,
+		const PathfinderConfig * pathfinderConfig,
+		CPathfinderHelper * pathfinderHelper) const
+	{
+		auto srcGuardians = cb->getGuardingCreatures(source.coord);
 
-				return;
-			}
+		if(destGuardians.empty())
+		{
+			return false;
+		}
 
-			const AIPathNode * destNode = nodeStorage->getAINode(destination.node);
-			auto battleNodeOptional = nodeStorage->getOrCreateNode(
-				destination.coord,
-				destination.node->layer,
-				destNode->actor->battleActor);
+		auto srcNode = nodeStorage->getAINode(source.node);
 
-			if(!battleNodeOptional)
-			{
+		vstd::erase_if(destGuardians, [&](const CGObjectInstance * destGuard) -> bool
+		{
+			return vstd::contains(srcGuardians, destGuard);
+		});
+
+		auto guardsAlreadyBypassed = destGuardians.empty() && srcGuardians.size();
+
+		if(guardsAlreadyBypassed && srcNode->actor->allowBattle)
+		{
 #ifdef VCMI_TRACE_PATHFINDER
-				logAi->trace(
-					"Can not allocate battle node while moving %s -> %s",
-					source.coord.toString(),
-					destination.coord.toString());
+			logAi->trace(
+				"Bypass guard at destination while moving %s -> %s",
+				source.coord.toString(),
+				destination.coord.toString());
 #endif
 
-				destination.blocked = true;
-
-				return;
-			}
+			return true;
+		}
 
-			AIPathNode * battleNode = battleNodeOptional.get();
+		const AIPathNode * destNode = nodeStorage->getAINode(destination.node);
+		auto battleNodeOptional = nodeStorage->getOrCreateNode(
+			destination.coord,
+			destination.node->layer,
+			destNode->actor->battleActor);
 
-			if(battleNode->locked)
-			{
+		if(!battleNodeOptional)
+		{
 #ifdef VCMI_TRACE_PATHFINDER
-				logAi->trace(
-					"Block bypass guard at destination while moving %s -> %s",
-					source.coord.toString(),
-					destination.coord.toString());
+			logAi->trace(
+				"Can not allocate battle node while moving %s -> %s",
+				source.coord.toString(),
+				destination.coord.toString());
 #endif
-				destination.blocked = true;
+			return false;
+		}
 
-				return;
-			}
+		AIPathNode * battleNode = battleNodeOptional.get();
 
-			auto hero = nodeStorage->getHero(source.node);
-			auto danger = nodeStorage->evaluateDanger(destination.coord, hero);
-			double actualArmyValue = srcNode->actor->armyValue - srcNode->armyLoss;
-			double loss = nodeStorage->evaluateArmyLoss(hero, actualArmyValue, danger);
+		if(battleNode->locked)
+		{
+#ifdef VCMI_TRACE_PATHFINDER
+			logAi->trace(
+				"Block bypass guard at destination while moving %s -> %s",
+				source.coord.toString(),
+				destination.coord.toString());
+#endif
+			return false;
+		}
 
-			if(loss < actualArmyValue)
-			{
-				destination.node = battleNode;
-				nodeStorage->commit(destination, source);
+		auto hero = nodeStorage->getHero(source.node);
+		auto danger = nodeStorage->evaluateDanger(destination.coord, hero, true);
+		double actualArmyValue = srcNode->actor->armyValue - srcNode->armyLoss;
+		double loss = nodeStorage->evaluateArmyLoss(hero, actualArmyValue, danger);
 
-				battleNode->armyLoss += loss;
+		if(loss < actualArmyValue)
+		{
+			destination.node = battleNode;
+			nodeStorage->commit(destination, source);
 
-				vstd::amax(battleNode->danger, danger);
+			battleNode->armyLoss += loss;
 
-				battleNode->specialAction = std::make_shared<BattleAction>(destination.coord);
+			vstd::amax(battleNode->danger, danger);
 
-				if(source.nodeObject && isObjectRemovable(source.nodeObject))
-				{
-					battleNode->theNodeBefore = source.node;
-				}
+			battleNode->specialAction = std::make_shared<BattleAction>(destination.coord);
+
+			if(source.nodeObject && isObjectRemovable(source.nodeObject))
+			{
+				battleNode->theNodeBefore = source.node;
+			}
 
 #ifdef VCMI_TRACE_PATHFINDER
-				logAi->trace(
-					"Begin bypass guard at destination with danger %s while moving %s -> %s",
-					std::to_string(danger),
-					source.coord.toString(),
-					destination.coord.toString());
+			logAi->trace(
+				"Begin bypass guard at destination with danger %s while moving %s -> %s",
+				std::to_string(danger),
+				source.coord.toString(),
+				destination.coord.toString());
 #endif
-				return;
-			}
+			return true;
 		}
 
-		destination.blocked = true;
+		return false;
 	}
 }

+ 14 - 0
AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.h

@@ -32,5 +32,19 @@ namespace AIPathfinding
 			CDestinationNodeInfo & destination,
 			const PathfinderConfig * pathfinderConfig,
 			CPathfinderHelper * pathfinderHelper) const override;
+
+	private:
+		bool bypassDestinationGuards(
+			std::vector<const CGObjectInstance *> destGuardians,
+			const PathNodeInfo & source,
+			CDestinationNodeInfo & destination,
+			const PathfinderConfig * pathfinderConfig,
+			CPathfinderHelper * pathfinderHelper) const;
+
+		bool bypassRemovableObject(
+			const PathNodeInfo & source,
+			CDestinationNodeInfo & destination,
+			const PathfinderConfig * pathfinderConfig,
+			CPathfinderHelper * pathfinderHelper) const;
 	};
 }

+ 14 - 6
AI/Nullkiller/VCAI.cpp

@@ -762,24 +762,29 @@ void VCAI::loadGame(BinaryDeserializer & h, const int version)
 	serializeInternal(h, version);
 }
 
-void makePossibleUpgrades(const CArmedInstance * obj)
+bool VCAI::makePossibleUpgrades(const CArmedInstance * obj)
 {
 	if(!obj)
-		return;
+		return false;
+
+	bool upgraded = false;
 
 	for(int i = 0; i < GameConstants::ARMY_SIZE; i++)
 	{
 		if(const CStackInstance * s = obj->getStackPtr(SlotID(i)))
 		{
 			UpgradeInfo ui;
-			cb->getUpgradeInfo(obj, SlotID(i), ui);
-			if(ui.oldID >= 0 && cb->getResourceAmount().canAfford(ui.cost[0] * s->count))
+			myCb->getUpgradeInfo(obj, SlotID(i), ui);
+			if(ui.oldID >= 0 && myCb->getResourceAmount().canAfford(ui.cost[0] * s->count))
 			{
-				cb->upgradeCreature(obj, SlotID(i), ui.newID[0]);
+				myCb->upgradeCreature(obj, SlotID(i), ui.newID[0]);
+				upgraded = true;
 				logAi->debug("Upgraded %d %s to %s", s->count, ui.oldID.toCreature()->namePl, ui.newID[0].toCreature()->namePl);
 			}
 		}
 	}
+
+	return upgraded;
 }
 
 void VCAI::makeTurn()
@@ -2203,12 +2208,15 @@ void VCAI::tryRealize(Goals::BuyArmy & g)
 	ui64 valueBought = 0;
 	//buy the stacks with largest AI value
 
-	makePossibleUpgrades(t);
+	auto upgradeSuccessfull = makePossibleUpgrades(t);
 
 	auto armyToBuy = ah->getArmyAvailableToBuy(t->getUpperArmy(), t);
 
 	if(armyToBuy.empty())
 	{
+		if(upgradeSuccessfull)
+			throw goalFulfilledException(sptr(g));
+
 		throw cannotFulfillGoalException("No creatures to buy.");
 	}
 

+ 1 - 2
AI/Nullkiller/VCAI.h

@@ -220,6 +220,7 @@ public:
 	void pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * other = nullptr);
 	void moveCreaturesToHero(const CGTownInstance * t);
 	void performObjectInteraction(const CGObjectInstance * obj, HeroPtr h);
+	bool makePossibleUpgrades(const CArmedInstance * obj);
 
 	bool moveHeroToTile(int3 dst, HeroPtr h);
 	void buildStructure(const CGTownInstance * t, BuildingID building); //TODO: move to BuildingManager
@@ -401,5 +402,3 @@ public:
 		return msg.c_str();
 	}
 };
-
-void makePossibleUpgrades(const CArmedInstance * obj);