2
0
Эх сурвалжийг харах

Merge pull request #5380 from IvanSavenko/xilmi_develop

[1.6.6] AI improvements from AIL / Xilmi
Ivan Savenko 8 сар өмнө
parent
commit
a272ae8208

+ 2 - 2
AI/Nullkiller/AIUtility.cpp

@@ -774,9 +774,9 @@ bool townHasFreeTavern(const CGTownInstance * town)
 	return canMoveVisitingHeroToGarrison;
 }
 
-uint64_t getHeroArmyStrengthWithCommander(const CGHeroInstance * hero, const CCreatureSet * heroArmy)
+uint64_t getHeroArmyStrengthWithCommander(const CGHeroInstance * hero, const CCreatureSet * heroArmy, int fortLevel)
 {
-	auto armyStrength = heroArmy->getArmyStrength();
+	auto armyStrength = heroArmy->getArmyStrength(fortLevel);
 
 	if(hero && hero->commander && hero->commander->alive)
 	{

+ 1 - 1
AI/Nullkiller/AIUtility.h

@@ -217,7 +217,7 @@ int64_t getArtifactScoreForHero(const CGHeroInstance * hero, const CArtifactInst
 int64_t getPotentialArtifactScore(const CArtifact * art);
 bool townHasFreeTavern(const CGTownInstance * town);
 
-uint64_t getHeroArmyStrengthWithCommander(const CGHeroInstance * hero, const CCreatureSet * heroArmy);
+uint64_t getHeroArmyStrengthWithCommander(const CGHeroInstance * hero, const CCreatureSet * heroArmy, int fortLevel = 0);
 
 uint64_t timeElapsed(std::chrono::time_point<std::chrono::high_resolution_clock> start);
 

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

@@ -291,6 +291,7 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
 				prerequisite.baseCreatureID = info.baseCreatureID;
 				prerequisite.prerequisitesCount++;
 				prerequisite.armyCost = info.armyCost;
+				prerequisite.armyStrength = info.armyStrength;
 				bool haveSameOrBetterFort = false;
 				if (prerequisite.id == BuildingID::FORT && highestFort >= CGTownInstance::EFortLevel::FORT)
 					haveSameOrBetterFort = true;

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

@@ -459,6 +459,8 @@ void ObjectClusterizer::clusterizeObject(
 			continue;
 		}
 
+		float priority = 0;
+
 		if(path.nodes.size() > 1)
 		{
 			auto blocker = getBlocker(path);
@@ -475,7 +477,10 @@ void ObjectClusterizer::clusterizeObject(
 
 				heroesProcessed.insert(path.targetHero);
 
-				float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), PriorityEvaluator::PriorityTier::HUNTER_GATHER);
+				for (int prio = PriorityEvaluator::PriorityTier::BUILDINGS; prio <= PriorityEvaluator::PriorityTier::MAX_PRIORITY_TIER; ++prio)
+				{
+					priority = std::max(priority, priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), prio));
+				}
 
 				if(ai->settings->isUseFuzzy() && priority < MIN_PRIORITY)
 					continue;
@@ -498,7 +503,10 @@ void ObjectClusterizer::clusterizeObject(
 
 		heroesProcessed.insert(path.targetHero);
 
-		float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), PriorityEvaluator::PriorityTier::HUNTER_GATHER);
+		for (int prio = PriorityEvaluator::PriorityTier::BUILDINGS; prio <= PriorityEvaluator::PriorityTier::MAX_PRIORITY_TIER; ++prio)
+		{
+			priority = std::max(priority, priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), prio));
+		}
 
 		if (ai->settings->isUseFuzzy() && priority < MIN_PRIORITY)
 			continue;

+ 12 - 1
AI/Nullkiller/Behaviors/DefenceBehavior.cpp

@@ -214,11 +214,15 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 
 		std::vector<int> pathsToDefend;
 		std::map<const CGHeroInstance *, std::vector<int>> defferedPaths;
+		AIPath* closestWay = nullptr;
 
 		for(int i = 0; i < paths.size(); i++)
 		{
 			auto & path = paths[i];
 
+			if (!closestWay || path.movementCost() < closestWay->movementCost())
+				closestWay = &path;
+
 #if NKAI_TRACE_LEVEL >= 1
 			logAi->trace(
 				"Hero %s can defend town with force %lld in %s turns, cost: %f, path: %s",
@@ -382,7 +386,14 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 					town->getObjectName());
 #endif
 
-			sequence.push_back(sptr(ExecuteHeroChain(path, town)));
+			ExecuteHeroChain heroChain = ExecuteHeroChain(path, town);
+				
+			if (closestWay)
+			{
+				heroChain.closestWayRatio = closestWay->movementCost() / heroChain.getPath().movementCost();
+			}
+
+			sequence.push_back(sptr(heroChain));
 			composition.addNextSequence(sequence);
 
 			auto firstBlockedAction = path.getFirstBlockedAction();

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

@@ -58,6 +58,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
 
 	ai->dangerHitMap->updateHitMap();
 	int treasureSourcesCount = 0;
+	int bestClosestThreat = UINT8_MAX;
 	
 	for(auto town : towns)
 	{
@@ -118,6 +119,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
 					bestScore = score;
 					bestHeroToHire = hero;
 					bestTownToHireFrom = town;
+					bestClosestThreat = closestThreat;
 				}
 			}
 		}
@@ -128,7 +130,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
 	{
 		if (ai->cb->getHeroesInfo().size() == 0
 			|| treasureSourcesCount > ai->cb->getHeroesInfo().size() * 5
-			|| bestHeroToHire->getArmyCost() > GameConstants::HERO_GOLD_COST / 2.0
+			|| (bestHeroToHire->getArmyCost() > GameConstants::HERO_GOLD_COST / 2.0 && (bestClosestThreat < 1 || !ai->buildAnalyzer->isGoldPressureHigh()))
 			|| (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh() && haveCapitol)
 			|| (ai->getFreeResources()[EGameResID::GOLD] > 30000 && !ai->buildAnalyzer->isGoldPressureHigh()))
 		{

+ 2 - 2
AI/Nullkiller/Engine/FuzzyHelper.cpp

@@ -127,9 +127,9 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj)
 			auto fortLevel = town->fortLevel();
 
 			if (fortLevel == CGTownInstance::EFortLevel::CASTLE)
-				danger = std::max(danger * 2, danger + 10000);
+				danger += 10000;
 			else if(fortLevel == CGTownInstance::EFortLevel::CITADEL)
-				danger = std::max(ui64(danger * 1.4), danger + 4000);
+				danger += 4000;
 		}
 
 		return danger;

+ 6 - 3
AI/Nullkiller/Engine/Nullkiller.cpp

@@ -446,7 +446,7 @@ void Nullkiller::makeTurn()
 #if NKAI_TRACE_LEVEL >= 1
 		int prioOfTask = 0;
 #endif
-		for (int prio = PriorityEvaluator::PriorityTier::INSTAKILL; prio <= PriorityEvaluator::PriorityTier::DEFEND; ++prio)
+		for (int prio = PriorityEvaluator::PriorityTier::INSTAKILL; prio <= PriorityEvaluator::PriorityTier::MAX_PRIORITY_TIER; ++prio)
 		{
 #if NKAI_TRACE_LEVEL >= 1
 			prioOfTask = prio;
@@ -535,7 +535,10 @@ void Nullkiller::makeTurn()
 				else
 					return;
 			}
-			hasAnySuccess = true;
+			else
+			{
+				hasAnySuccess = true;
+			}
 		}
 
 		hasAnySuccess |= handleTrading();
@@ -721,7 +724,7 @@ bool Nullkiller::handleTrading()
 				if (toGive && toGive <= available[mostExpendable]) //don't try to sell 0 resources
 				{
 					cb->trade(m->getObjInstanceID(), EMarketMode::RESOURCE_RESOURCE, GameResID(mostExpendable), GameResID(mostWanted), toGive);
-#if NKAI_TRACE_LEVEL >= 1
+#if NKAI_TRACE_LEVEL >= 2
 					logAi->info("Traded %d of %s for %d of %s at %s", toGive, mostExpendable, toGet, mostWanted, obj->getObjectName());
 #endif
 					haveTraded = true;

+ 148 - 43
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -66,7 +66,8 @@ EvaluationContext::EvaluationContext(const Nullkiller* ai)
 	isArmyUpgrade(false),
 	isHero(false),
 	isEnemy(false),
-	explorePriority(0)
+	explorePriority(0),
+	powerRatio(0)
 {
 }
 
@@ -609,9 +610,6 @@ float RewardEvaluator::getConquestValue(const CGObjectInstance* target) const
 			? getEnemyHeroStrategicalValue(dynamic_cast<const CGHeroInstance*>(target))
 			: 0;
 
-	case Obj::KEYMASTER:
-		return 0.6f;
-
 	default:
 		return 0;
 	}
@@ -889,7 +887,14 @@ public:
 
 		Goals::StayAtTown& stayAtTown = dynamic_cast<Goals::StayAtTown&>(*task);
 
-		evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero());
+		if (stayAtTown.getHero() != nullptr && stayAtTown.getHero()->movementPointsRemaining() < 100)
+		{
+			return;
+		}
+
+		if(stayAtTown.town->mageGuildLevel() > 0)
+			evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero());
+
 		if (evaluationContext.armyReward == 0)
 			evaluationContext.isDefend = true;
 		else
@@ -1018,6 +1023,45 @@ public:
 		if(heroRole == HeroRole::MAIN)
 			evaluationContext.heroRole = heroRole;
 
+		if (hero)
+		{
+			// Assuming Slots() returns a collection of slots with slot.second->getCreatureID() and slot.second->getPower()
+			float heroPower = 0;
+			float totalPower = 0;
+
+			// Map to store the aggregated power of creatures by CreatureID
+			std::map<int, float> totalPowerByCreatureID;
+
+			// Calculate hero power and total power by CreatureID
+			for (auto slot : hero->Slots())
+			{
+				int creatureID = slot.second->getCreatureID();
+				float slotPower = slot.second->getPower();
+
+				// Add the power of this slot to the heroPower
+				heroPower += slotPower;
+
+				// Accumulate the total power for the specific CreatureID
+				if (totalPowerByCreatureID.find(creatureID) == totalPowerByCreatureID.end())
+				{
+					// First time encountering this CreatureID, retrieve total creatures' power
+					totalPowerByCreatureID[creatureID] = ai->armyManager->getTotalCreaturesAvailable(creatureID).power;
+				}
+			}
+
+			// Calculate total power based on unique CreatureIDs
+			for (const auto& entry : totalPowerByCreatureID)
+			{
+				totalPower += entry.second;
+			}
+
+			// Compute the power ratio if total power is greater than zero
+			if (totalPower > 0)
+			{
+				evaluationContext.powerRatio = heroPower / totalPower;
+			}
+		}
+
 		if (target)
 		{
 			evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero);
@@ -1030,6 +1074,8 @@ public:
 				evaluationContext.isHero = true;
 			if (target->getOwner().isValidPlayer() && ai->cb->getPlayerRelations(ai->playerID, target->getOwner()) == PlayerRelations::ENEMIES)
 				evaluationContext.isEnemy = true;
+			if (target->ID == Obj::TOWN)
+				evaluationContext.defenseValue = dynamic_cast<const CGTownInstance*>(target)->fortLevel();
 			evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army);
 			if(evaluationContext.danger > 0)
 				evaluationContext.skillReward += (float)evaluationContext.danger / (float)hero->getArmyStrength();
@@ -1169,6 +1215,19 @@ public:
 		evaluationContext.goldCost += cost;
 		evaluationContext.closestWayRatio = 1;
 		evaluationContext.buildingCost += bi.buildCostWithPrerequisites;
+
+		bool alreadyOwn = false;
+		int highestMageGuildPossible = BuildingID::MAGES_GUILD_3;
+		for (auto town : evaluationContext.evaluator.ai->cb->getTownsInfo())
+		{
+			if (town->hasBuilt(bi.id))
+				alreadyOwn = true;
+			if (evaluationContext.evaluator.ai->cb->canBuildStructure(town, BuildingID::MAGES_GUILD_5) != EBuildingState::FORBIDDEN)
+				highestMageGuildPossible = BuildingID::MAGES_GUILD_5;
+			else if (evaluationContext.evaluator.ai->cb->canBuildStructure(town, BuildingID::MAGES_GUILD_4) != EBuildingState::FORBIDDEN)
+				highestMageGuildPossible = BuildingID::MAGES_GUILD_4;
+		}
+
 		if (bi.id == BuildingID::MARKETPLACE || bi.dailyIncome[EGameResID::WOOD] > 0)
 			evaluationContext.isTradeBuilding = true;
 
@@ -1183,14 +1242,19 @@ public:
 			if(bi.baseCreatureID == bi.creatureID)
 			{
 				evaluationContext.addNonCriticalStrategicalValue((0.5f + 0.1f * bi.creatureLevel) / (float)bi.prerequisitesCount);
-				evaluationContext.armyReward += bi.armyStrength;
+				evaluationContext.armyReward += bi.armyStrength * 1.5;
 			}
 			else
 			{
 				auto potentialUpgradeValue = evaluationContext.evaluator.getUpgradeArmyReward(buildThis.town, bi);
 				
 				evaluationContext.addNonCriticalStrategicalValue(potentialUpgradeValue / 10000.0f / (float)bi.prerequisitesCount);
-				evaluationContext.armyReward += potentialUpgradeValue / (float)bi.prerequisitesCount;
+				if(bi.id.IsDwelling())
+					evaluationContext.armyReward += bi.armyStrength - evaluationContext.evaluator.ai->armyManager->evaluateStackPower(bi.baseCreatureID.toCreature(), bi.creatureGrows);
+				else //This is for prerequisite-buildings
+					evaluationContext.armyReward += evaluationContext.evaluator.ai->armyManager->evaluateStackPower(bi.baseCreatureID.toCreature(), bi.creatureGrows);
+				if(alreadyOwn)
+					evaluationContext.armyReward /= bi.buildCostWithPrerequisites.marketValue();
 			}
 		}
 		else if(bi.id == BuildingID::CITADEL || bi.id == BuildingID::CASTLE)
@@ -1201,9 +1265,14 @@ public:
 		else if(bi.id >= BuildingID::MAGES_GUILD_1 && bi.id <= BuildingID::MAGES_GUILD_5)
 		{
 			evaluationContext.skillReward += 2 * (bi.id - BuildingID::MAGES_GUILD_1);
-			for (auto hero : evaluationContext.evaluator.ai->cb->getHeroesInfo())
+			if (!alreadyOwn && evaluationContext.evaluator.ai->cb->canBuildStructure(buildThis.town, highestMageGuildPossible) != EBuildingState::FORBIDDEN)
 			{
-				evaluationContext.armyInvolvement += hero->getArmyCost();
+				for (auto hero : evaluationContext.evaluator.ai->cb->getHeroesInfo())
+				{
+					if(hero->getPrimSkillLevel(PrimarySkill::SPELL_POWER) + hero->getPrimSkillLevel(PrimarySkill::KNOWLEDGE) > hero->getPrimSkillLevel(PrimarySkill::ATTACK) + hero->getPrimSkillLevel(PrimarySkill::DEFENSE)
+						&& hero->manaLimit() > 30)
+						evaluationContext.armyReward += hero->getArmyCost();
+				}
 			}
 		}
 		int sameTownBonus = 0;
@@ -1333,18 +1402,35 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 	else
 	{
 		float score = 0;
-		const bool amIInDanger = ai->cb->getTownsInfo().empty() || (evaluationContext.isDefend && evaluationContext.threatTurns == 0);
-		const float maxWillingToLose = amIInDanger ? 1 : ai->settings->getMaxArmyLossTarget();
+		bool currentPositionThreatened = false;
+		if (task->hero)
+		{
+			auto currentTileThreat = ai->dangerHitMap->getTileThreat(task->hero->visitablePos());
+			if (currentTileThreat.fastestDanger.turn < 1 && currentTileThreat.fastestDanger.danger > task->hero->getTotalStrength())
+				currentPositionThreatened = true;
+		}
+		if (priorityTier == PriorityTier::FAR_HUNTER_GATHER && currentPositionThreatened == false)
+		{
+#if NKAI_TRACE_LEVEL >= 2
+			logAi->trace("Skip FAR_HUNTER_GATHER because hero is not threatened.");
+#endif
+			return 0;
+		}
+		const bool amIInDanger = ai->cb->getTownsInfo().empty();
+		const float maxWillingToLose = amIInDanger ? 1 : ai->settings->getMaxArmyLossTarget() * evaluationContext.powerRatio > 0 ? ai->settings->getMaxArmyLossTarget() * evaluationContext.powerRatio : 1.0;
+		float dangerThreshold = 1;
+		dangerThreshold *= evaluationContext.powerRatio > 0 ? evaluationContext.powerRatio : 1.0;
 
 		bool arriveNextWeek = false;
 		if (ai->cb->getDate(Date::DAY_OF_WEEK) + evaluationContext.turn > 7 && priorityTier < PriorityTier::FAR_KILL)
 			arriveNextWeek = true;
 
 #if NKAI_TRACE_LEVEL >= 2
-		logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, army-involvement: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, explorePriority: %d isDefend: %d isEnemy: %d arriveNextWeek: %d",
+		logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, maxWillingToLose: %f, turn: %d, turns main: %f, scout: %f, army-involvement: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, dangerThreshold: %f explorePriority: %d isDefend: %d isEnemy: %d arriveNextWeek: %d powerRatio: %f",
 			priorityTier,
 			task->toString(),
 			evaluationContext.armyLossPersentage,
+			maxWillingToLose,
 			(int)evaluationContext.turn,
 			evaluationContext.movementCostByRole[HeroRole::MAIN],
 			evaluationContext.movementCostByRole[HeroRole::SCOUT],
@@ -1362,23 +1448,27 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 			evaluationContext.conquestValue,
 			evaluationContext.closestWayRatio,
 			evaluationContext.enemyHeroDangerRatio,
+			dangerThreshold,
 			evaluationContext.explorePriority,
 			evaluationContext.isDefend,
 			evaluationContext.isEnemy,
-			arriveNextWeek);
+			arriveNextWeek,
+			evaluationContext.powerRatio);
 #endif
 
 		switch (priorityTier)
 		{
 			case PriorityTier::INSTAKILL: //Take towns / kill heroes in immediate reach
 			{
-				if (evaluationContext.turn > 0)
+				if (evaluationContext.turn > 0 || evaluationContext.isExchange)
 					return 0;
 				if (evaluationContext.movementCost >= 1)
 					return 0;
+				if (evaluationContext.defenseValue < 2 && evaluationContext.enemyHeroDangerRatio > dangerThreshold)
+					return 0;
 				if(evaluationContext.conquestValue > 0)
 					score = evaluationContext.armyInvolvement;
-				if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty()))
+				if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > dangerThreshold && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty()))
 					return 0;
 				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
 					return 0;
@@ -1388,23 +1478,47 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 			}
 			case PriorityTier::INSTADEFEND: //Defend immediately threatened towns
 			{
-				if (evaluationContext.isDefend && evaluationContext.threatTurns == 0 && evaluationContext.turn == 0)
-					score = evaluationContext.armyInvolvement;
-				if (evaluationContext.isEnemy && maxWillingToLose - evaluationContext.armyLossPersentage < 0)
+				//No point defending if we don't have defensive-structures
+				if (evaluationContext.defenseValue < 2)
+					return 0;
+				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
 					return 0;
+				if (evaluationContext.closestWayRatio < 1.0)
+					return 0;
+				if (evaluationContext.isEnemy && evaluationContext.turn > 0)
+					return 0;
+				if (evaluationContext.isDefend && evaluationContext.threatTurns <= evaluationContext.turn)
+				{
+					const float OPTIMAL_PERCENTAGE = 0.75f; // We want army to be 75% of the threat
+					float optimalStrength = evaluationContext.threat * OPTIMAL_PERCENTAGE;
+
+					// Calculate how far the army is from optimal strength
+					float deviation = std::abs(evaluationContext.armyInvolvement - optimalStrength);
+
+					// Convert deviation to a percentage of the threat to normalize it
+					float deviationPercentage = deviation / evaluationContext.threat;
+
+					// Calculate score: 1.0 is perfect, decreasing as deviation increases
+					score = 1.0f / (1.0f + deviationPercentage);
+
+					// Apply turn penalty to still prefer earlier moves when scores are close
+					score = score / (evaluationContext.turn + 1);
+				}
 				break;
 			}
 			case PriorityTier::KILL: //Take towns / kill heroes that are further away
 				//FALL_THROUGH
 			case PriorityTier::FAR_KILL:
 			{
+				if (evaluationContext.defenseValue < 2 && evaluationContext.enemyHeroDangerRatio > dangerThreshold)
+					return 0;
 				if (evaluationContext.turn > 0 && evaluationContext.isHero)
 					return 0;
 				if (arriveNextWeek && evaluationContext.isEnemy)
 					return 0;
 				if (evaluationContext.conquestValue > 0)
 					score = evaluationContext.armyInvolvement;
-				if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty()))
+				if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > dangerThreshold && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty()))
 					return 0;
 				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
 					return 0;
@@ -1413,24 +1527,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 					score /= evaluationContext.movementCost;
 				break;
 			}
-			case PriorityTier::UPGRADE:
-			{
-				if (!evaluationContext.isArmyUpgrade)
-					return 0;
-				if (evaluationContext.enemyHeroDangerRatio > 1)
-					return 0;
-				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
-					return 0;
-				if (vstd::isAlmostZero(evaluationContext.armyLossPersentage) && evaluationContext.closestWayRatio < 1.0)
-					return 0;
-				score = 1000;
-				if (evaluationContext.movementCost > 0)
-					score /= evaluationContext.movementCost;
-				break;
-			}
 			case PriorityTier::HIGH_PRIO_EXPLORE:
 			{
-				if (evaluationContext.enemyHeroDangerRatio > 1)
+				if (evaluationContext.enemyHeroDangerRatio > dangerThreshold)
 					return 0;
 				if (evaluationContext.explorePriority != 1)
 					return 0;
@@ -1447,17 +1546,15 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 				//FALL_THROUGH
 			case PriorityTier::FAR_HUNTER_GATHER:
 			{
-				if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend)
+				if (evaluationContext.enemyHeroDangerRatio > dangerThreshold && !evaluationContext.isDefend && priorityTier != PriorityTier::FAR_HUNTER_GATHER)
 					return 0;
 				if (evaluationContext.buildingCost.marketValue() > 0)
 					return 0;
-				if (evaluationContext.isDefend && (evaluationContext.enemyHeroDangerRatio < 1 || evaluationContext.threatTurns > 0 || evaluationContext.turn > 0))
+				if (priorityTier != PriorityTier::FAR_HUNTER_GATHER && evaluationContext.isDefend && (evaluationContext.enemyHeroDangerRatio > dangerThreshold || evaluationContext.threatTurns > 0 || evaluationContext.turn > 0))
 					return 0;
 				if (evaluationContext.explorePriority == 3)
 					return 0;
-				if (evaluationContext.isArmyUpgrade)
-					return 0;
-				if ((evaluationContext.enemyHeroDangerRatio > 0 && arriveNextWeek) || evaluationContext.enemyHeroDangerRatio > 1)
+				if (priorityTier != PriorityTier::FAR_HUNTER_GATHER && ((evaluationContext.enemyHeroDangerRatio > 0 && arriveNextWeek) || evaluationContext.enemyHeroDangerRatio > dangerThreshold))
 					return 0;
 				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
 					return 0;
@@ -1475,12 +1572,14 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 					score = 1000;
 					if (evaluationContext.movementCost > 0)
 						score /= evaluationContext.movementCost;
+					if(priorityTier == PriorityTier::FAR_HUNTER_GATHER && evaluationContext.enemyHeroDangerRatio > 0)
+						score /= evaluationContext.enemyHeroDangerRatio;
 				}
 				break;
 			}
 			case PriorityTier::LOW_PRIO_EXPLORE:
 			{
-				if (evaluationContext.enemyHeroDangerRatio > 1)
+				if (evaluationContext.enemyHeroDangerRatio > dangerThreshold)
 					return 0;
 				if (evaluationContext.explorePriority != 3)
 					return 0;
@@ -1495,7 +1594,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 			}
 			case PriorityTier::DEFEND: //Defend whatever if nothing else is to do
 			{
-				if (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.isExchange)
+				if (evaluationContext.enemyHeroDangerRatio > dangerThreshold)
 					return 0;
 				if (evaluationContext.isDefend || evaluationContext.isArmyUpgrade)
 					score = evaluationContext.armyInvolvement;
@@ -1536,9 +1635,15 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 						TResources needed = evaluationContext.buildingCost - resourcesAvailable;
 						needed.positive();
 						int turnsTo = needed.maxPurchasableCount(income);
+						bool haveEverythingButGold = true;
+						for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; i++)
+						{
+							if (i != GameResID::GOLD && resourcesAvailable[i] < evaluationContext.buildingCost[i])
+								haveEverythingButGold = false;
+						}
 						if (turnsTo == INT_MAX)
 							return 0;
-						else
+						if (!haveEverythingButGold)
 							score /= turnsTo;
 					}
 				}

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

@@ -84,6 +84,7 @@ struct DLL_EXPORT EvaluationContext
 	bool isHero;
 	bool isEnemy;
 	int explorePriority;
+	float powerRatio;
 
 	EvaluationContext(const Nullkiller * ai);
 
@@ -114,13 +115,13 @@ public:
 		INSTAKILL,
 		INSTADEFEND,
 		KILL,
-		UPGRADE,
 		HIGH_PRIO_EXPLORE,
 		HUNTER_GATHER,
 		LOW_PRIO_EXPLORE,
 		FAR_KILL,
+		DEFEND,
 		FAR_HUNTER_GATHER,
-		DEFEND
+		MAX_PRIORITY_TIER = FAR_HUNTER_GATHER
 	};
 
 private:

+ 12 - 1
AI/Nullkiller/Pathfinding/AINodeStorage.cpp

@@ -1447,9 +1447,20 @@ void AINodeStorage::calculateChainInfo(std::vector<AIPath> & paths, const int3 &
 			}
 		}
 
+		int fortLevel = 0;
+		auto visitableObjects = cb->getVisitableObjs(pos);
+		for (auto obj : visitableObjects)
+		{
+			if (objWithID<Obj::TOWN>(obj))
+			{
+				auto town = dynamic_cast<const CGTownInstance*>(obj);
+				fortLevel = town->fortLevel();
+			}
+		}
+
 		path.targetObjectArmyLoss = evaluateArmyLoss(
 			path.targetHero,
-			getHeroArmyStrengthWithCommander(path.targetHero, path.heroArmy),
+			getHeroArmyStrengthWithCommander(path.targetHero, path.heroArmy, fortLevel),
 			path.targetObjectDanger);
 
 		path.chainMask = node.actor->chainMask;

+ 15 - 3
lib/CCreatureSet.cpp

@@ -353,11 +353,23 @@ bool CCreatureSet::needsLastStack() const
 	return false;
 }
 
-ui64 CCreatureSet::getArmyStrength() const
+ui64 CCreatureSet::getArmyStrength(int fortLevel) const
 {
 	ui64 ret = 0;
-	for(const auto & elem : stacks)
-		ret += elem.second->getPower();
+	for (const auto& elem : stacks)
+	{
+		ui64 powerToAdd = elem.second->getPower();
+		if (fortLevel > 0)
+		{
+			if (!elem.second->hasBonusOfType(BonusType::FLYING))
+			{
+				powerToAdd /= fortLevel;
+				if (!elem.second->hasBonusOfType(BonusType::SHOOTER))
+					powerToAdd /= fortLevel;
+			}
+		} 
+		ret += powerToAdd;
+	}
 	return ret;
 }
 

+ 1 - 1
lib/CCreatureSet.h

@@ -283,7 +283,7 @@ public:
 	bool slotEmpty(const SlotID & slot) const;
 	int stacksCount() const;
 	virtual bool needsLastStack() const; //true if last stack cannot be taken
-	ui64 getArmyStrength() const; //sum of AI values of creatures
+	ui64 getArmyStrength(int fortLevel = 0) const; //sum of AI values of creatures
 	ui64 getArmyCost() const; //sum of cost of creatures
 	ui64 getPower(const SlotID & slot) const; //value of specific stack
 	std::string getRoughAmount(const SlotID & slot, int mode = 0) const; //rough size of specific stack