浏览代码

Merge pull request #5021 from Xilmi/develop

Fix for AI not defending in some cases
Ivan Savenko 10 月之前
父节点
当前提交
b39e8dc1ef

+ 1 - 5
AI/BattleAI/BattleAI.cpp

@@ -167,14 +167,12 @@ void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack )
 
 		result = evaluator.selectStackAction(stack);
 
-		if(autobattlePreferences.enableSpellsUsage && !skipCastUntilNextBattle && evaluator.canCastSpell())
+		if(autobattlePreferences.enableSpellsUsage && evaluator.canCastSpell())
 		{
 			auto spelCasted = evaluator.attemptCastingSpell(stack);
 
 			if(spelCasted)
 				return;
-			
-			skipCastUntilNextBattle = true;
 		}
 
 		logAi->trace("Spellcast attempt completed in %lld", timeElapsed(start));
@@ -256,8 +254,6 @@ void CBattleAI::battleStart(const BattleID & battleID, const CCreatureSet *army1
 {
 	LOG_TRACE(logAi);
 	side = Side;
-
-	skipCastUntilNextBattle = false;
 }
 
 void CBattleAI::print(const std::string &text) const

+ 0 - 1
AI/BattleAI/BattleAI.h

@@ -62,7 +62,6 @@ class CBattleAI : public CBattleGameInterface
 	bool wasWaitingForRealize;
 	bool wasUnlockingGs;
 	int movesSkippedByDefense;
-	bool skipCastUntilNextBattle;
 
 public:
 	CBattleAI();

+ 92 - 7
AI/BattleAI/BattleEvaluator.cpp

@@ -119,6 +119,58 @@ std::vector<BattleHex> BattleEvaluator::getBrokenWallMoatHexes() const
 	return result;
 }
 
+std::vector<BattleHex> BattleEvaluator::getCastleHexes()
+{
+	std::vector<BattleHex> result;
+
+	// Loop through all wall parts
+
+	std::vector<BattleHex> wallHexes;
+	wallHexes.push_back(50);
+	wallHexes.push_back(183);
+	wallHexes.push_back(182);
+	wallHexes.push_back(130);
+	wallHexes.push_back(78);
+	wallHexes.push_back(29);
+	wallHexes.push_back(12);
+	wallHexes.push_back(97);
+	wallHexes.push_back(45);
+	wallHexes.push_back(62);
+	wallHexes.push_back(112);
+	wallHexes.push_back(147);
+	wallHexes.push_back(165);
+
+	for (BattleHex wallHex : wallHexes) {
+		// Get the starting x-coordinate of the wall hex
+		int startX = wallHex.getX();
+
+		// Initialize current hex with the wall hex
+		BattleHex currentHex = wallHex;
+		while (currentHex.isValid()) {
+			// Check if the x-coordinate has wrapped (smaller than the starting x)
+			if (currentHex.getX() < startX) {
+				break;
+			}
+
+			// Add the hex to the result
+			result.push_back(currentHex);
+
+			// Move to the next hex to the right
+			currentHex = currentHex.cloneInDirection(BattleHex::RIGHT, false);
+		}
+	}
+
+	return result;
+}
+
+bool BattleEvaluator::hasWorkingTowers() const
+{
+	bool keepIntact = cb->getBattle(battleID)->battleGetWallState(EWallPart::KEEP) != EWallState::NONE && cb->getBattle(battleID)->battleGetWallState(EWallPart::KEEP) != EWallState::DESTROYED;
+	bool upperIntact = cb->getBattle(battleID)->battleGetWallState(EWallPart::UPPER_TOWER) != EWallState::NONE && cb->getBattle(battleID)->battleGetWallState(EWallPart::UPPER_TOWER) != EWallState::DESTROYED;
+	bool bottomIntact = cb->getBattle(battleID)->battleGetWallState(EWallPart::BOTTOM_TOWER) != EWallState::NONE && cb->getBattle(battleID)->battleGetWallState(EWallPart::BOTTOM_TOWER) != EWallState::DESTROYED;
+	return keepIntact || upperIntact || bottomIntact;
+}
+
 std::optional<PossibleSpellcast> BattleEvaluator::findBestCreatureSpell(const CStack *stack)
 {
 	//TODO: faerie dragon type spell should be selected by server
@@ -161,6 +213,19 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 
 	auto moveTarget = scoreEvaluator.findMoveTowardsUnreachable(stack, *targets, damageCache, hb);
 	float score = EvaluationResult::INEFFECTIVE_SCORE;
+	auto enemyMellee = hb->getUnitsIf([this](const battle::Unit* u) -> bool
+		{
+			return u->unitSide() == BattleSide::ATTACKER && !hb->battleCanShoot(u);
+		});
+	bool siegeDefense = stack->unitSide() == BattleSide::DEFENDER
+		&& !stack->canShoot()
+		&& hasWorkingTowers()
+		&& !enemyMellee.empty();
+	std::vector<BattleHex> castleHexes = getCastleHexes();
+	for (auto hex : castleHexes)
+	{
+		logAi->trace("Castlehex ID: %d Y: %d X: %d", hex, hex.getY(), hex.getX());
+	}
 
 	if(targets->possibleAttacks.empty() && bestSpellcast.has_value())
 	{
@@ -174,7 +239,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 		logAi->trace("Evaluating attack for %s", stack->getDescription());
 #endif
 
-		auto evaluationResult = scoreEvaluator.findBestTarget(stack, *targets, damageCache, hb);
+		auto evaluationResult = scoreEvaluator.findBestTarget(stack, *targets, damageCache, hb, siegeDefense);
 		auto & bestAttack = evaluationResult.bestAttack;
 
 		cachedAttack.ap = bestAttack;
@@ -227,15 +292,13 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 						return BattleAction::makeDefend(stack);
 					}
 
-					auto enemyMellee = hb->getUnitsIf([this](const battle::Unit * u) -> bool
-						{
-							return u->unitSide() == BattleSide::ATTACKER && !hb->battleCanShoot(u);
+					bool isTargetOutsideFort = std::none_of(castleHexes.begin(), castleHexes.end(),
+						[&](const BattleHex& hex) {
+							return hex == bestAttack.from;
 						});
-
-					bool isTargetOutsideFort = bestAttack.dest.getY() < GameConstants::BFIELD_WIDTH - 4;
 					bool siegeDefense = stack->unitSide() == BattleSide::DEFENDER
 						&& !bestAttack.attack.shooting
-						&& hb->battleGetFortifications().hasMoat
+						&& hasWorkingTowers()
 						&& !enemyMellee.empty()
 						&& isTargetOutsideFort;
 
@@ -349,6 +412,28 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector
 	auto reachability = cb->getBattle(battleID)->getReachability(stack);
 	auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false);
 
+	auto enemyMellee = hb->getUnitsIf([this](const battle::Unit* u) -> bool
+		{
+			return u->unitSide() == BattleSide::ATTACKER && !hb->battleCanShoot(u);
+		});
+
+	bool siegeDefense = stack->unitSide() == BattleSide::DEFENDER
+		&& hasWorkingTowers()
+		&& !enemyMellee.empty();
+
+	if (siegeDefense)
+	{
+		vstd::erase_if(avHexes, [&](const BattleHex& hex) {
+			std::vector<BattleHex> castleHexes = getCastleHexes();
+
+			bool isOutsideWall = std::none_of(castleHexes.begin(), castleHexes.end(),
+				[&](const BattleHex& checkhex) {
+					return checkhex == hex;
+				});
+			return isOutsideWall;
+			});
+	}
+
 	if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked
 	{
 		return BattleAction::makeDefend(stack);

+ 2 - 0
AI/BattleAI/BattleEvaluator.h

@@ -53,6 +53,8 @@ public:
 	std::optional<PossibleSpellcast> findBestCreatureSpell(const CStack * stack);
 	BattleAction goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes, const PotentialTargets & targets);
 	std::vector<BattleHex> getBrokenWallMoatHexes() const;
+	static std::vector<BattleHex> getCastleHexes();
+	bool hasWorkingTowers() const;
 	void evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps); //for offensive damaging spells only
 	void print(const std::string & text) const;
 	BattleAction moveOrAttack(const CStack * stack, BattleHex hex, const PotentialTargets & targets);

+ 32 - 2
AI/BattleAI/BattleExchangeVariant.cpp

@@ -9,6 +9,7 @@
  */
 #include "StdInc.h"
 #include "BattleExchangeVariant.h"
+#include "BattleEvaluator.h"
 #include "../../lib/CStack.h"
 
 AttackerValue::AttackerValue()
@@ -213,9 +214,11 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
 	const battle::Unit * activeStack,
 	PotentialTargets & targets,
 	DamageCache & damageCache,
-	std::shared_ptr<HypotheticBattle> hb)
+	std::shared_ptr<HypotheticBattle> hb,
+	bool siegeDefense)
 {
 	EvaluationResult result(targets.bestAction());
+	std::vector<BattleHex> castleHexes = BattleEvaluator::getCastleHexes();
 
 	if(!activeStack->waited() && !activeStack->acquireState()->hadMorale)
 	{
@@ -231,6 +234,9 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
 
 		for(auto & ap : targets.possibleAttacks)
 		{
+			if (siegeDefense && std::find(castleHexes.begin(), castleHexes.end(), ap.from) == castleHexes.end())
+				continue;
+
 			float score = evaluateExchange(ap, 0, targets, damageCache, hbWaited);
 
 			if(score > result.score)
@@ -263,6 +269,9 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
 
 	for(auto & ap : targets.possibleAttacks)
 	{
+		if (siegeDefense && std::find(castleHexes.begin(), castleHexes.end(), ap.from) == castleHexes.end())
+			continue;
+
 		float score = evaluateExchange(ap, 0, targets, damageCache, hb);
 		bool sameScoreButWaited = vstd::isAlmostEqual(score, result.score) && result.wait;
 
@@ -350,11 +359,32 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
 		if(distance <= speed)
 			continue;
 
+		float penaltyMultiplier = 1.0f; // Default multiplier, no penalty
+		float closestAllyDistance = std::numeric_limits<float>::max();
+
+		for (const battle::Unit* ally : hb->battleAliveUnits()) {
+			if (ally == activeStack) 
+				continue;
+			if (ally->unitSide() != activeStack->unitSide()) 
+				continue;
+
+			float allyDistance = dists.distToNearestNeighbour(ally, enemy);
+			if (allyDistance < closestAllyDistance)
+			{
+				closestAllyDistance = allyDistance;
+			}
+		}
+
+		// If an ally is closer to the enemy, compute the penaltyMultiplier
+		if (closestAllyDistance < distance) {
+			penaltyMultiplier = closestAllyDistance / distance; // Ratio of distances
+		}
+
 		auto turnsToRich = (distance - 1) / speed + 1;
 		auto hexes = enemy->getSurroundingHexes();
 		auto enemySpeed = enemy->getMovementRange();
 		auto speedRatio = speed / static_cast<float>(enemySpeed);
-		auto multiplier = speedRatio > 1 ? 1 : speedRatio;
+		auto multiplier = (speedRatio > 1 ? 1 : speedRatio) * penaltyMultiplier;
 
 		for(auto & hex : hexes)
 		{

+ 2 - 1
AI/BattleAI/BattleExchangeVariant.h

@@ -159,7 +159,8 @@ public:
 		const battle::Unit * activeStack,
 		PotentialTargets & targets,
 		DamageCache & damageCache,
-		std::shared_ptr<HypotheticBattle> hb);
+		std::shared_ptr<HypotheticBattle> hb,
+		bool siegeDefense = false);
 
 	float evaluateExchange(
 		const AttackPossibility & ap,

+ 58 - 3
AI/Nullkiller/Analyzers/ArmyManager.cpp

@@ -309,6 +309,8 @@ std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
 		? dynamic_cast<const CGTownInstance *>(dwelling)
 		: nullptr;
 
+	std::set<SlotID> alreadyDisbanded;
+
 	for(int i = dwelling->creatures.size() - 1; i >= 0; i--)
 	{
 		auto ci = infoFromDC(dwelling->creatures[i]);
@@ -322,18 +324,71 @@ std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
 
 		if(!ci.count) continue;
 
+		// Calculate the market value of the new stack
+		TResources newStackValue = ci.creID.toCreature()->getFullRecruitCost() * ci.count;
+
 		SlotID dst = hero->getSlotFor(ci.creID);
+
+		// Keep track of the least valuable slot in the hero's army
+		SlotID leastValuableSlot;
+		TResources leastValuableStackValue;
+		leastValuableStackValue[6] = std::numeric_limits<int>::max();
+		bool shouldDisband = false;
 		if(!hero->hasStackAtSlot(dst)) //need another new slot for this stack
 		{
-			if(!freeHeroSlots) //no more place for stacks
-				continue;
+			if(!freeHeroSlots) // No free slots; consider replacing
+			{
+				// Check for the least valuable existing stack
+				for (auto& slot : hero->Slots())
+				{
+					if (alreadyDisbanded.find(slot.first) != alreadyDisbanded.end())
+						continue;
+
+					if(slot.second->getCreatureID() != CreatureID::NONE)
+					{
+						TResources currentStackValue = slot.second->getCreatureID().toCreature()->getFullRecruitCost() * slot.second->getCount();
+
+						if (town && slot.second->getCreatureID().toCreature()->getFactionID() == town->getFactionID())
+							continue;
+
+						if(currentStackValue.marketValue() < leastValuableStackValue.marketValue())
+						{
+							leastValuableStackValue = currentStackValue;
+							leastValuableSlot = slot.first;
+						}
+					}
+				}
+
+				// Decide whether to replace the least valuable stack
+				if(newStackValue.marketValue() <= leastValuableStackValue.marketValue())
+				{
+					continue; // Skip if the new stack isn't worth replacing
+				}
+				else
+				{
+					shouldDisband = true;
+				}
+			}
 			else
+			{
 				freeHeroSlots--; //new slot will be occupied
+			}
 		}
 
 		vstd::amin(ci.count, availableRes / ci.creID.toCreature()->getFullRecruitCost()); //max count we can afford
 
-		if(!ci.count) continue;
+		int disbandMalus = 0;
+		
+		if (shouldDisband)
+		{
+			disbandMalus = leastValuableStackValue / ci.creID.toCreature()->getFullRecruitCost();
+			alreadyDisbanded.insert(leastValuableSlot);
+		}
+
+		ci.count -= disbandMalus;
+
+		if(ci.count <= 0)
+			continue;
 
 		ci.level = i; //this is important for Dungeon Summoning Portal
 		creaturesInDwellings.push_back(ci);

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

@@ -505,7 +505,7 @@ void ObjectClusterizer::clusterizeObject(
 		else if (priority <= 0)
 			continue;
 
-		bool interestingObject = path.turn() <= 2 || priority > 0.5f;
+		bool interestingObject = path.turn() <= 2 || priority > (ai->settings->isUseFuzzy() ? 0.5f : 0);
 
 		if(interestingObject)
 		{

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

@@ -64,7 +64,7 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const
 
 				if(reinforcement)
 				{
-					tasks.push_back(Goals::sptr(Goals::BuyArmy(town, reinforcement).setpriority(5)));
+					tasks.push_back(Goals::sptr(Goals::BuyArmy(town, reinforcement).setpriority(reinforcement)));
 				}
 			}
 		}

+ 15 - 3
AI/Nullkiller/Behaviors/DefenceBehavior.cpp

@@ -41,9 +41,6 @@ Goals::TGoalVec DefenceBehavior::decompose(const Nullkiller * ai) const
 	for(auto town : ai->cb->getTownsInfo())
 	{
 		evaluateDefence(tasks, town, ai);
-		//Let's do only one defence-task per pass since otherwise it can try to hire the same hero twice
-		if (!tasks.empty())
-			break;
 	}
 
 	return tasks;
@@ -422,6 +419,21 @@ void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitM
 			if(hero->getTotalStrength() < threat.danger)
 				continue;
 
+			bool heroAlreadyHiredInOtherTown = false;
+			for (const auto& task : tasks) 
+			{
+				if (auto recruitGoal = dynamic_cast<Goals::RecruitHero*>(task.get())) 
+				{
+					if (recruitGoal->getHero() == hero)
+					{
+						heroAlreadyHiredInOtherTown = true;
+						break;
+					}
+				}
+			}
+			if (heroAlreadyHiredInOtherTown)
+				continue;
+
 			auto myHeroes = ai->cb->getHeroesInfo();
 
 #if NKAI_TRACE_LEVEL >= 1

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

@@ -124,6 +124,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
 			|| (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh() && haveCapitol)
 			|| (ai->getFreeResources()[EGameResID::GOLD] > 30000 && !ai->buildAnalyzer->isGoldPressureHigh()))
 		{

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

@@ -397,7 +397,12 @@ void Nullkiller::makeTurn()
 				if(!executeTask(bestTask))
 					return;
 
-				updateAiState(i, true);
+				bool fastUpdate = true;
+
+				if (bestTask->getHero() != nullptr)
+					fastUpdate = false;
+
+				updateAiState(i, fastUpdate);
 			}
 			else
 			{

+ 39 - 17
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -1006,6 +1006,9 @@ public:
 		Goals::ExecuteHeroChain & chain = dynamic_cast<Goals::ExecuteHeroChain &>(*task);
 		const AIPath & path = chain.getPath();
 
+		if (vstd::isAlmostZero(path.movementCost()))
+			return;
+
 		vstd::amax(evaluationContext.danger, path.getTotalDanger());
 		evaluationContext.movementCost += path.movementCost();
 		evaluationContext.closestWayRatio = chain.closestWayRatio;
@@ -1019,12 +1022,20 @@ public:
 				evaluationContext.involvesSailing = true;
 		}
 
+		float highestCostForSingleHero = 0;
 		for(auto pair : costsPerHero)
 		{
 			auto role = evaluationContext.evaluator.ai->heroManager->getHeroRole(pair.first);
-
 			evaluationContext.movementCostByRole[role] += pair.second;
+			if (pair.second > highestCostForSingleHero)
+				highestCostForSingleHero = pair.second;
+		}
+		if (highestCostForSingleHero > 1 && costsPerHero.size() > 1)
+		{
+			//Chains that involve more than 1 hero doing something for more than a turn are too expensive in my book. They often involved heroes doing nothing just standing there waiting to fulfill their part of the chain.
+			return;
 		}
+		evaluationContext.movementCost *= costsPerHero.size(); //further deincentivise chaining as it often involves bringing back the army afterwards
 
 		auto hero = task->hero;
 		bool checkGold = evaluationContext.danger == 0;
@@ -1046,13 +1057,13 @@ public:
 			evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target);
 			if (target->ID == Obj::HERO)
 				evaluationContext.isHero = true;
-			if (target->getOwner() != PlayerColor::NEUTRAL && ai->cb->getPlayerRelations(ai->playerID, target->getOwner()) == PlayerRelations::ENEMIES)
+			if (target->getOwner().isValidPlayer() && ai->cb->getPlayerRelations(ai->playerID, target->getOwner()) == PlayerRelations::ENEMIES)
 				evaluationContext.isEnemy = true;
 			evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army);
-			evaluationContext.armyInvolvement += army->getArmyCost();
 			if(evaluationContext.danger > 0)
 				evaluationContext.skillReward += (float)evaluationContext.danger / (float)hero->getArmyStrength();
 		}
+		evaluationContext.armyInvolvement += army->getArmyCost();
 
 		vstd::amax(evaluationContext.armyLossPersentage, (float)path.getTotalArmyLoss() / (float)army->getArmyStrength());
 		addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength());
@@ -1353,17 +1364,18 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 		const float maxWillingToLose = amIInDanger ? 1 : ai->settings->getMaxArmyLossTarget();
 
 		bool arriveNextWeek = false;
-		if (ai->cb->getDate(Date::DAY_OF_WEEK) + evaluationContext.turn > 7)
+		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, 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",
+		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",
 			priorityTier,
 			task->toString(),
 			evaluationContext.armyLossPersentage,
 			(int)evaluationContext.turn,
 			evaluationContext.movementCostByRole[HeroRole::MAIN],
 			evaluationContext.movementCostByRole[HeroRole::SCOUT],
+			evaluationContext.armyInvolvement,
 			goldRewardPerTurn,
 			evaluationContext.goldCost,
 			evaluationContext.armyReward,
@@ -1378,7 +1390,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 			evaluationContext.closestWayRatio,
 			evaluationContext.enemyHeroDangerRatio,
 			evaluationContext.explorePriority,
-			evaluationContext.isDefend);
+			evaluationContext.isDefend,
+			evaluationContext.isEnemy,
+			arriveNextWeek);
 #endif
 
 		switch (priorityTier)
@@ -1387,13 +1401,14 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 			{
 				if (evaluationContext.turn > 0)
 					return 0;
+				if (evaluationContext.movementCost >= 1)
+					return 0;
 				if(evaluationContext.conquestValue > 0)
-					score = 1000;
+					score = evaluationContext.armyInvolvement;
 				if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty()))
 					return 0;
 				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
 					return 0;
-				score *= evaluationContext.closestWayRatio;
 				if (evaluationContext.movementCost > 0)
 					score /= evaluationContext.movementCost;
 				break;
@@ -1404,17 +1419,18 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 					score = evaluationContext.armyInvolvement;
 				if (evaluationContext.isEnemy && maxWillingToLose - evaluationContext.armyLossPersentage < 0)
 					return 0;
-				score *= evaluationContext.closestWayRatio;
 				break;
 			}
 			case PriorityTier::KILL: //Take towns / kill heroes that are further away
+				//FALL_THROUGH
+			case PriorityTier::FAR_KILL:
 			{
 				if (evaluationContext.turn > 0 && evaluationContext.isHero)
 					return 0;
 				if (arriveNextWeek && evaluationContext.isEnemy)
 					return 0;
 				if (evaluationContext.conquestValue > 0)
-					score = 1000;
+					score = evaluationContext.armyInvolvement;
 				if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty()))
 					return 0;
 				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
@@ -1432,8 +1448,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 					return 0;
 				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
 					return 0;
+				if (vstd::isAlmostZero(evaluationContext.armyLossPersentage) && evaluationContext.closestWayRatio < 1.0)
+					return 0;
 				score = 1000;
-				score *= evaluationContext.closestWayRatio;
 				if (evaluationContext.movementCost > 0)
 					score /= evaluationContext.movementCost;
 				break;
@@ -1446,13 +1463,16 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 					return 0;
 				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
 					return 0;
+				if (vstd::isAlmostZero(evaluationContext.armyLossPersentage) && evaluationContext.closestWayRatio < 1.0)
+					return 0;
 				score = 1000;
-				score *= evaluationContext.closestWayRatio;
 				if (evaluationContext.movementCost > 0)
 					score /= evaluationContext.movementCost;
 				break;
 			}
 			case PriorityTier::HUNTER_GATHER: //Collect guarded stuff
+				//FALL_THROUGH
+			case PriorityTier::FAR_HUNTER_GATHER:
 			{
 				if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend)
 					return 0;
@@ -1468,6 +1488,8 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 					return 0;
 				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
 					return 0;
+				if (vstd::isAlmostZero(evaluationContext.armyLossPersentage) && evaluationContext.closestWayRatio < 1.0)
+					return 0;
 				score += evaluationContext.strategicalValue * 1000;
 				score += evaluationContext.goldReward;
 				score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05;
@@ -1478,7 +1500,6 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 				if (score > 0)
 				{
 					score = 1000;
-					score *= evaluationContext.closestWayRatio;
 					if (evaluationContext.movementCost > 0)
 						score /= evaluationContext.movementCost;
 				}
@@ -1492,8 +1513,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 					return 0;
 				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
 					return 0;
+				if (evaluationContext.closestWayRatio < 1.0)
+					return 0;
 				score = 1000;
-				score *= evaluationContext.closestWayRatio;
 				if (evaluationContext.movementCost > 0)
 					score /= evaluationContext.movementCost;
 				break;
@@ -1503,8 +1525,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 				if (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.isExchange)
 					return 0;
 				if (evaluationContext.isDefend || evaluationContext.isArmyUpgrade)
-					score = 1000;
-				score *= evaluationContext.closestWayRatio;
+					score = evaluationContext.armyInvolvement;
 				score /= (evaluationContext.turn + 1);
 				break;
 			}
@@ -1563,13 +1584,14 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 	}
 
 #if NKAI_TRACE_LEVEL >= 2
-	logAi->trace("priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %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, result %f",
+	logAi->trace("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, result %f",
 		priorityTier,
 		task->toString(),
 		evaluationContext.armyLossPersentage,
 		(int)evaluationContext.turn,
 		evaluationContext.movementCostByRole[HeroRole::MAIN],
 		evaluationContext.movementCostByRole[HeroRole::SCOUT],
+		evaluationContext.armyInvolvement,
 		goldRewardPerTurn,
 		evaluationContext.goldCost,
 		evaluationContext.armyReward,

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

@@ -118,6 +118,8 @@ public:
 		HIGH_PRIO_EXPLORE,
 		HUNTER_GATHER,
 		LOW_PRIO_EXPLORE,
+		FAR_KILL,
+		FAR_HUNTER_GATHER,
 		DEFEND
 	};
 

+ 30 - 1
AI/Nullkiller/Goals/BuyArmy.cpp

@@ -58,7 +58,36 @@ void BuyArmy::accept(AIGateway * ai)
 
 		if(ci.count)
 		{
-			cb->recruitCreatures(town, town->getUpperArmy(), ci.creID, ci.count, ci.level);
+			if (town->getUpperArmy()->stacksCount() == GameConstants::ARMY_SIZE)
+			{
+				SlotID lowestValueSlot;
+				int lowestValue = std::numeric_limits<int>::max();
+				for (auto slot : town->getUpperArmy()->Slots())
+				{
+					if (slot.second->getCreatureID() != CreatureID::NONE)
+					{
+						int currentStackMarketValue =
+							slot.second->getCreatureID().toCreature()->getFullRecruitCost().marketValue() * slot.second->getCount();
+
+						if (slot.second->getCreatureID().toCreature()->getFactionID() == town->getFactionID())
+							continue;
+
+						if (currentStackMarketValue < lowestValue)
+						{
+							lowestValue = currentStackMarketValue;
+							lowestValueSlot = slot.first;
+						}
+					}
+				}
+				if (lowestValueSlot.validSlot())
+				{
+					cb->dismissCreature(town->getUpperArmy(), lowestValueSlot);
+				}
+			}
+			if (town->getUpperArmy()->stacksCount() < GameConstants::ARMY_SIZE || town->getUpperArmy()->getSlotFor(ci.creID).validSlot()) //It is possible we don't scrap despite we wanted to due to not scrapping stacks that fit our faction
+			{
+				cb->recruitCreatures(town, town->getUpperArmy(), ci.creID, ci.count, ci.level);
+			}
 			valueBought += ci.count * ci.creID.toCreature()->getAIValue();
 		}
 	}

+ 6 - 4
AI/Nullkiller/Pathfinding/Actors.cpp

@@ -374,10 +374,12 @@ HeroExchangeArmy * HeroExchangeMap::tryUpgrade(
 		for(auto & creatureToBuy : buyArmy)
 		{
 			auto targetSlot = target->getSlotFor(creatureToBuy.creID.toCreature());
-
-			target->addToSlot(targetSlot, creatureToBuy.creID, creatureToBuy.count);
-			target->armyCost += creatureToBuy.creID.toCreature()->getFullRecruitCost() * creatureToBuy.count;
-			target->requireBuyArmy = true;
+			if (targetSlot.validSlot())
+			{
+				target->addToSlot(targetSlot, creatureToBuy.creID, creatureToBuy.count);
+				target->armyCost += creatureToBuy.creID.toCreature()->getFullRecruitCost() * creatureToBuy.count;
+				target->requireBuyArmy = true;
+			}
 		}
 	}