Browse Source

Merge pull request #4274 from Xilmi/develop

Rule based AI
Ivan Savenko 10 months ago
parent
commit
4180ea9523
46 changed files with 1058 additions and 290 deletions
  1. 75 7
      AI/BattleAI/BattleEvaluator.cpp
  2. 1 1
      AI/BattleAI/BattleExchangeVariant.cpp
  3. 1 1
      AI/Nullkiller/AIUtility.cpp
  4. 0 10
      AI/Nullkiller/Analyzers/ArmyManager.cpp
  5. 31 12
      AI/Nullkiller/Analyzers/BuildAnalyzer.cpp
  6. 1 1
      AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp
  7. 2 0
      AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h
  8. 12 9
      AI/Nullkiller/Analyzers/HeroManager.cpp
  9. 2 2
      AI/Nullkiller/Analyzers/HeroManager.h
  10. 11 6
      AI/Nullkiller/Analyzers/ObjectClusterizer.cpp
  11. 35 12
      AI/Nullkiller/Behaviors/BuildingBehavior.cpp
  12. 9 8
      AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp
  13. 3 8
      AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp
  14. 26 19
      AI/Nullkiller/Behaviors/DefenceBehavior.cpp
  15. 13 29
      AI/Nullkiller/Behaviors/ExplorationBehavior.cpp
  16. 4 29
      AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp
  17. 64 25
      AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp
  18. 1 10
      AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp
  19. 12 3
      AI/Nullkiller/Engine/FuzzyHelper.cpp
  20. 160 17
      AI/Nullkiller/Engine/Nullkiller.cpp
  21. 2 1
      AI/Nullkiller/Engine/Nullkiller.h
  22. 401 45
      AI/Nullkiller/Engine/PriorityEvaluator.cpp
  23. 30 2
      AI/Nullkiller/Engine/PriorityEvaluator.h
  24. 7 1
      AI/Nullkiller/Engine/Settings.cpp
  25. 2 0
      AI/Nullkiller/Engine/Settings.h
  26. 1 0
      AI/Nullkiller/Goals/AbstractGoal.h
  27. 3 0
      AI/Nullkiller/Goals/AdventureSpellCast.cpp
  28. 5 2
      AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp
  29. 2 0
      AI/Nullkiller/Goals/ExecuteHeroChain.cpp
  30. 1 0
      AI/Nullkiller/Goals/RecruitHero.cpp
  31. 1 0
      AI/Nullkiller/Goals/RecruitHero.h
  32. 2 6
      AI/Nullkiller/Goals/StayAtTown.cpp
  33. 12 2
      AI/Nullkiller/Pathfinding/AINodeStorage.cpp
  34. 2 2
      AI/Nullkiller/Pathfinding/AINodeStorage.h
  35. 1 1
      AI/Nullkiller/Pathfinding/Actors.cpp
  36. 1 1
      client/lobby/SelectionTab.cpp
  37. 1 1
      config/ai/nkai/nkai-settings.json
  38. 14 0
      lib/CCreatureSet.cpp
  39. 3 0
      lib/CCreatureSet.h
  40. 20 11
      lib/ResourceSet.h
  41. 1 1
      lib/campaign/CampaignState.cpp
  42. 31 2
      lib/mapObjects/CGHeroInstance.cpp
  43. 4 1
      lib/mapObjects/CGHeroInstance.h
  44. 1 1
      server/CGameHandler.cpp
  45. 46 0
      server/processors/NewTurnProcessor.cpp
  46. 1 1
      test/googletest

+ 75 - 7
AI/BattleAI/BattleEvaluator.cpp

@@ -675,7 +675,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 				spells::BattleCast cast(state.get(), hero, spells::Mode::HERO, ps.spell);
 				cast.castEval(state->getServerCallback(), ps.dest);
 
-				auto allUnits = state->battleGetUnitsIf([](const battle::Unit * u) -> bool { return true; });
+				auto allUnits = state->battleGetUnitsIf([](const battle::Unit * u) -> bool { return u->isValidTarget(); });
 
 				auto needFullEval = vstd::contains_if(allUnits, [&](const battle::Unit * u) -> bool
 					{
@@ -731,7 +731,55 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 
 					ps.value = scoreEvaluator.evaluateExchange(updatedAttack, cachedAttack.turn, *targets, innerCache, state);
 				}
-
+				//! Some units may be dead alltogether. So if they existed before but not now, we know they were killed by the spell
+				for (const auto& unit : all)
+				{
+					if (!unit->isValidTarget())
+						continue;
+					bool isDead = true;
+					for (const auto& remainingUnit : allUnits)
+					{
+						if (remainingUnit->unitId() == unit->unitId())
+							isDead = false;
+					}
+					if (isDead)
+					{
+						auto newHealth = 0;
+						auto oldHealth = vstd::find_or(healthOfStack, unit->unitId(), 0);
+						if (oldHealth != newHealth)
+						{
+							auto damage = std::abs(oldHealth - newHealth);
+							auto originalDefender = cb->getBattle(battleID)->battleGetUnitByID(unit->unitId());
+							auto dpsReduce = AttackPossibility::calculateDamageReduce(
+								nullptr,
+								originalDefender && originalDefender->alive() ? originalDefender : unit,
+								damage,
+								innerCache,
+								state);
+							auto ourUnit = unit->unitSide() == side ? 1 : -1;
+							auto goodEffect = newHealth > oldHealth ? 1 : -1;
+							if (ourUnit * goodEffect == 1)
+							{
+								if (ourUnit && goodEffect && (unit->isClone() || unit->isGhost()))
+									continue;
+								ps.value += dpsReduce * scoreEvaluator.getPositiveEffectMultiplier();
+							}
+							else
+								ps.value -= dpsReduce * scoreEvaluator.getNegativeEffectMultiplier();
+#if BATTLE_TRACE_LEVEL >= 1
+							logAi->trace(
+								"Spell %s to %d affects %s (%d), dps: %2f oldHealth: %d newHealth: %d",
+								ps.spell->getNameTranslated(),
+								ps.dest.at(0).hexValue.hex,
+								unit->creatureId().toCreature()->getNameSingularTranslated(),
+								unit->getCount(),
+								dpsReduce,
+								oldHealth,
+								newHealth);
+#endif
+						}
+					}
+				}
 				for(const auto & unit : allUnits)
 				{
 					if(!unit->isValidTarget(true))
@@ -771,11 +819,31 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 							ps.value -= 4 * dpsReduce * scoreEvaluator.getNegativeEffectMultiplier();
 
 #if BATTLE_TRACE_LEVEL >= 1
-						logAi->trace(
-							"Spell affects %s (%d), dps: %2f",
-							unit->creatureId().toCreature()->getNameSingularTranslated(),
-							unit->getCount(),
-							dpsReduce);
+						// Ensure ps.dest is not empty before accessing the first element
+						if (!ps.dest.empty()) 
+						{
+							logAi->trace(
+								"Spell %s to %d affects %s (%d), dps: %2f oldHealth: %d newHealth: %d",
+								ps.spell->getNameTranslated(),
+								ps.dest.at(0).hexValue.hex,  // Safe to access .at(0) now
+								unit->creatureId().toCreature()->getNameSingularTranslated(),
+								unit->getCount(),
+								dpsReduce,
+								oldHealth,
+								newHealth);
+						}
+						else 
+						{
+							// Handle the case where ps.dest is empty
+							logAi->trace(
+								"Spell %s has no destination, affects %s (%d), dps: %2f oldHealth: %d newHealth: %d",
+								ps.spell->getNameTranslated(),
+								unit->creatureId().toCreature()->getNameSingularTranslated(),
+								unit->getCount(),
+								dpsReduce,
+								oldHealth,
+								newHealth);
+						}
 #endif
 					}
 				}

+ 1 - 1
AI/BattleAI/BattleExchangeVariant.cpp

@@ -906,7 +906,7 @@ std::vector<const battle::Unit *> BattleExchangeEvaluator::getOneTurnReachableUn
 {
 	std::vector<const battle::Unit *> result;
 
-	for(int i = 0; i < turnOrder.size(); i++)
+	for(int i = 0; i < turnOrder.size(); i++, turn++)
 	{
 		auto & turnQueue = turnOrder[i];
 		HypotheticBattle turnBattle(env.get(), cb);

+ 1 - 1
AI/Nullkiller/AIUtility.cpp

@@ -148,7 +148,7 @@ bool HeroPtr::operator==(const HeroPtr & rhs) const
 
 bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet * heroArmy, uint64_t dangerStrength)
 {
-	const ui64 heroStrength = h->getFightingStrength() * heroArmy->getArmyStrength();
+	const ui64 heroStrength = h->getHeroStrength() * heroArmy->getArmyStrength();
 
 	if(dangerStrength)
 	{

+ 0 - 10
AI/Nullkiller/Analyzers/ArmyManager.cpp

@@ -152,16 +152,6 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
 	uint64_t armyValue = 0;
 
 	TemporaryArmy newArmyInstance;
-	auto bonusModifiers = armyCarrier->getBonuses(Selector::type()(BonusType::MORALE));
-
-	for(auto bonus : *bonusModifiers)
-	{
-		// army bonuses will change and object bonuses are temporary
-		if(bonus->source != BonusSource::ARMY && bonus->source != BonusSource::OBJECT_INSTANCE && bonus->source != BonusSource::OBJECT_TYPE)
-		{
-			newArmyInstance.addNewBonus(std::make_shared<Bonus>(*bonus));
-		}
-	}
 
 	while(allowedFactions.size() < alignmentMap.size())
 	{

+ 31 - 12
AI/Nullkiller/Analyzers/BuildAnalyzer.cpp

@@ -39,7 +39,6 @@ void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo)
 		for(int upgradeIndex : {1, 0})
 		{
 			BuildingID building = BuildingID(BuildingID::getDwellingFromLevel(level, upgradeIndex));
-
 			if(!vstd::contains(buildings, building))
 				continue; // no such building in town
 
@@ -73,11 +72,18 @@ void BuildAnalyzer::updateOtherBuildings(TownDevelopmentInfo & developmentInfo)
 
 	if(developmentInfo.existingDwellings.size() >= 2 && ai->cb->getDate(Date::DAY_OF_WEEK) > boost::date_time::Friday)
 	{
-		otherBuildings.push_back({BuildingID::CITADEL, BuildingID::CASTLE});
 		otherBuildings.push_back({BuildingID::HORDE_1});
 		otherBuildings.push_back({BuildingID::HORDE_2});
 	}
 
+	otherBuildings.push_back({ BuildingID::CITADEL, BuildingID::CASTLE });
+	otherBuildings.push_back({ BuildingID::RESOURCE_SILO });
+	otherBuildings.push_back({ BuildingID::SPECIAL_1 });
+	otherBuildings.push_back({ BuildingID::SPECIAL_2 });
+	otherBuildings.push_back({ BuildingID::SPECIAL_3 });
+	otherBuildings.push_back({ BuildingID::SPECIAL_4 });
+	otherBuildings.push_back({ BuildingID::MARKETPLACE });
+
 	for(auto & buildingSet : otherBuildings)
 	{
 		for(auto & buildingID : buildingSet)
@@ -141,6 +147,8 @@ void BuildAnalyzer::update()
 
 	auto towns = ai->cb->getTownsInfo();
 
+	float economyDevelopmentCost = 0;
+
 	for(const CGTownInstance* town : towns)
 	{
 		logAi->trace("Checking town %s", town->getNameTranslated());
@@ -153,6 +161,11 @@ void BuildAnalyzer::update()
 
 		requiredResources += developmentInfo.requiredResources;
 		totalDevelopmentCost += developmentInfo.townDevelopmentCost;
+		for(auto building : developmentInfo.toBuild)
+		{
+			if (building.dailyIncome[EGameResID::GOLD] > 0)
+				economyDevelopmentCost += building.buildCostWithPrerequisites[EGameResID::GOLD];
+		}
 		armyCost += developmentInfo.armyCost;
 
 		for(auto bi : developmentInfo.toBuild)
@@ -171,15 +184,7 @@ void BuildAnalyzer::update()
 
 	updateDailyIncome();
 
-	if(ai->cb->getDate(Date::DAY) == 1)
-	{
-		goldPressure = 1;
-	}
-	else
-	{
-		goldPressure = ai->getLockedResources()[EGameResID::GOLD] / 5000.0f
-			+ (float)armyCost[EGameResID::GOLD] / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f);
-	}
+	goldPressure = (ai->getLockedResources()[EGameResID::GOLD] + (float)armyCost[EGameResID::GOLD] + economyDevelopmentCost) / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f);
 
 	logAi->trace("Gold pressure: %f", goldPressure);
 }
@@ -237,6 +242,12 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
 	logAi->trace("checking %s", info.name);
 	logAi->trace("buildInfo %s", info.toString());
 
+	int highestFort = 0;
+	for (auto twn : ai->cb->getTownsInfo())
+	{
+		highestFort = std::max(highestFort, (int)twn->fortLevel());
+	}
+
 	if(!town->hasBuilt(building))
 	{
 		auto canBuild = ai->cb->canBuildStructure(town, building);
@@ -281,7 +292,15 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
 				prerequisite.baseCreatureID = info.baseCreatureID;
 				prerequisite.prerequisitesCount++;
 				prerequisite.armyCost = info.armyCost;
-				prerequisite.dailyIncome = info.dailyIncome;
+				bool haveSameOrBetterFort = false;
+				if (prerequisite.id == BuildingID::FORT && highestFort >= CGTownInstance::EFortLevel::FORT)
+					haveSameOrBetterFort = true;
+				if (prerequisite.id == BuildingID::CITADEL && highestFort >= CGTownInstance::EFortLevel::CITADEL)
+					haveSameOrBetterFort = true;
+				if (prerequisite.id == BuildingID::CASTLE && highestFort >= CGTownInstance::EFortLevel::CASTLE)
+					haveSameOrBetterFort = true;
+				if(!haveSameOrBetterFort)
+					prerequisite.dailyIncome = info.dailyIncome;
 
 				return prerequisite;
 			}

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

@@ -89,7 +89,6 @@ void DangerHitMapAnalyzer::updateHitMap()
 
 			heroes[hero->tempOwner][hero] = HeroRole::MAIN;
 		}
-
 		if(obj->ID == Obj::TOWN)
 		{
 			auto town = dynamic_cast<const CGTownInstance *>(obj);
@@ -140,6 +139,7 @@ void DangerHitMapAnalyzer::updateHitMap()
 
 				newThreat.hero = path.targetHero;
 				newThreat.turn = path.turn();
+				newThreat.threat = path.getHeroStrength() * (1 - path.movementCost() / 2.0);
 				newThreat.danger = path.getHeroStrength();
 
 				if(newThreat.value() > node.maximumDanger.value())

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

@@ -22,6 +22,7 @@ struct HitMapInfo
 
 	uint64_t danger;
 	uint8_t turn;
+	float threat;
 	HeroPtr hero;
 
 	HitMapInfo()
@@ -33,6 +34,7 @@ struct HitMapInfo
 	{
 		danger = 0;
 		turn = 255;
+		threat = 0;
 		hero = HeroPtr();
 	}
 

+ 12 - 9
AI/Nullkiller/Analyzers/HeroManager.cpp

@@ -95,7 +95,7 @@ float HeroManager::evaluateSpeciality(const CGHeroInstance * hero) const
 
 float HeroManager::evaluateFightingStrength(const CGHeroInstance * hero) const
 {
-	return evaluateSpeciality(hero) + wariorSkillsScores.evaluateSecSkills(hero) + hero->level * 1.5f;
+	return evaluateSpeciality(hero) + wariorSkillsScores.evaluateSecSkills(hero) + hero->getBasePrimarySkillValue(PrimarySkill::ATTACK) + hero->getBasePrimarySkillValue(PrimarySkill::DEFENSE) + hero->getBasePrimarySkillValue(PrimarySkill::SPELL_POWER) + hero->getBasePrimarySkillValue(PrimarySkill::KNOWLEDGE);
 }
 
 void HeroManager::update()
@@ -108,7 +108,7 @@ void HeroManager::update()
 	for(auto & hero : myHeroes)
 	{
 		scores[hero] = evaluateFightingStrength(hero);
-		knownFightingStrength[hero->id] = hero->getFightingStrength();
+		knownFightingStrength[hero->id] = hero->getHeroStrength();
 	}
 
 	auto scoreSort = [&](const CGHeroInstance * h1, const CGHeroInstance * h2) -> bool
@@ -147,7 +147,10 @@ void HeroManager::update()
 
 HeroRole HeroManager::getHeroRole(const HeroPtr & hero) const
 {
-	return heroRoles.at(hero);
+	if (heroRoles.find(hero) != heroRoles.end())
+		return heroRoles.at(hero);
+	else
+		return HeroRole::SCOUT;
 }
 
 const std::map<HeroPtr, HeroRole> & HeroManager::getHeroRoles() const
@@ -188,10 +191,9 @@ float HeroManager::evaluateHero(const CGHeroInstance * hero) const
 	return evaluateFightingStrength(hero);
 }
 
-bool HeroManager::heroCapReached() const
+bool HeroManager::heroCapReached(bool includeGarrisoned) const
 {
-	const bool includeGarnisoned = true;
-	int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned);
+	int heroCount = cb->getHeroCount(ai->playerID, includeGarrisoned);
 
 	return heroCount >= ALLOWED_ROAMING_HEROES
 		|| heroCount >= ai->settings->getMaxRoamingHeroes()
@@ -204,7 +206,7 @@ float HeroManager::getFightingStrengthCached(const CGHeroInstance * hero) const
 	auto cached = knownFightingStrength.find(hero->id);
 
 	//FIXME: fallback to hero->getFightingStrength() is VERY slow on higher difficulties (no object graph? map reveal?)
-	return cached != knownFightingStrength.end() ? cached->second : hero->getFightingStrength();
+	return cached != knownFightingStrength.end() ? cached->second : hero->getHeroStrength();
 }
 
 float HeroManager::getMagicStrength(const CGHeroInstance * hero) const
@@ -281,7 +283,7 @@ const CGHeroInstance * HeroManager::findHeroWithGrail() const
 	return nullptr;
 }
 
-const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) const
+const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit, const CGTownInstance* townToSpare) const
 {
 	const CGHeroInstance * weakestHero = nullptr;
 	auto myHeroes = ai->cb->getHeroesInfo();
@@ -292,12 +294,13 @@ const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) co
 			|| existingHero->getArmyStrength() >armyLimit
 			|| getHeroRole(existingHero) == HeroRole::MAIN
 			|| existingHero->movementPointsRemaining()
+			|| (townToSpare != nullptr && existingHero->visitedTown == townToSpare)
 			|| existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1))
 		{
 			continue;
 		}
 
-		if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength())
+		if(!weakestHero || weakestHero->getHeroStrength() > existingHero->getHeroStrength())
 		{
 			weakestHero = existingHero;
 		}

+ 2 - 2
AI/Nullkiller/Analyzers/HeroManager.h

@@ -56,9 +56,9 @@ public:
 	float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const;
 	float evaluateHero(const CGHeroInstance * hero) const;
 	bool canRecruitHero(const CGTownInstance * t = nullptr) const;
-	bool heroCapReached() const;
+	bool heroCapReached(bool includeGarrisoned = true) const;
 	const CGHeroInstance * findHeroWithGrail() const;
-	const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const;
+	const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit, const CGTownInstance * townToSpare = nullptr) const;
 	float getMagicStrength(const CGHeroInstance * hero) const;
 	float getFightingStrengthCached(const CGHeroInstance * hero) const;
 

+ 11 - 6
AI/Nullkiller/Analyzers/ObjectClusterizer.cpp

@@ -97,9 +97,10 @@ std::optional<const CGObjectInstance *> ObjectClusterizer::getBlocker(const AIPa
 	{
 		auto guardPos = ai->cb->getGuardingCreaturePosition(node.coord);
 
-		blockers = ai->cb->getVisitableObjs(node.coord);
+		if (ai->cb->isVisible(node.coord))
+			blockers = ai->cb->getVisitableObjs(node.coord);
 
-		if(guardPos.valid())
+		if(guardPos.valid() && ai->cb->isVisible(guardPos))
 		{
 			auto guard = ai->cb->getTopObj(ai->cb->getGuardingCreaturePosition(node.coord));
 
@@ -474,9 +475,11 @@ void ObjectClusterizer::clusterizeObject(
 
 				heroesProcessed.insert(path.targetHero);
 
-				float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)));
+				float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), PriorityEvaluator::PriorityTier::HUNTER_GATHER);
 
-				if(priority < MIN_PRIORITY)
+				if(ai->settings->isUseFuzzy() && priority < MIN_PRIORITY)
+					continue;
+				else if (priority <= 0)
 					continue;
 
 				ClusterMap::accessor cluster;
@@ -495,9 +498,11 @@ void ObjectClusterizer::clusterizeObject(
 
 		heroesProcessed.insert(path.targetHero);
 
-		float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)));
+		float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), PriorityEvaluator::PriorityTier::HUNTER_GATHER);
 
-		if(priority < MIN_PRIORITY)
+		if (ai->settings->isUseFuzzy() && priority < MIN_PRIORITY)
+			continue;
+		else if (priority <= 0)
 			continue;
 
 		bool interestingObject = path.turn() <= 2 || priority > 0.5f;

+ 35 - 12
AI/Nullkiller/Behaviors/BuildingBehavior.cpp

@@ -49,26 +49,49 @@ Goals::TGoalVec BuildingBehavior::decompose(const Nullkiller * ai) const
 	auto & developmentInfos = ai->buildAnalyzer->getDevelopmentInfo();
 	auto isGoldPressureLow = !ai->buildAnalyzer->isGoldPressureHigh();
 
+	ai->dangerHitMap->updateHitMap();
+
 	for(auto & developmentInfo : developmentInfos)
 	{
-		for(auto & buildingInfo : developmentInfo.toBuild)
+		bool emergencyDefense = false;
+		uint8_t closestThreat = std::numeric_limits<uint8_t>::max();
+		for (auto threat : ai->dangerHitMap->getTownThreats(developmentInfo.town))
+		{
+			closestThreat = std::min(closestThreat, threat.turn);
+		}
+		for (auto& buildingInfo : developmentInfo.toBuild)
 		{
-			if(isGoldPressureLow || buildingInfo.dailyIncome[EGameResID::GOLD] > 0)
+			if (closestThreat <= 1 && developmentInfo.town->fortLevel() < CGTownInstance::EFortLevel::CASTLE && !buildingInfo.notEnoughRes)
 			{
-				if(buildingInfo.notEnoughRes)
+				if (buildingInfo.id == BuildingID::CITADEL || buildingInfo.id == BuildingID::CASTLE)
 				{
-					if(ai->getLockedResources().canAfford(buildingInfo.buildCost))
-						continue;
-
-					Composition composition;
+					tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo)));
+					emergencyDefense = true;
+				}
+			}
+		}
+		if (!emergencyDefense)
+		{
+			for (auto& buildingInfo : developmentInfo.toBuild)
+			{
+				if (isGoldPressureLow || buildingInfo.dailyIncome[EGameResID::GOLD] > 0)
+				{
+					if (buildingInfo.notEnoughRes)
+					{
+						if (ai->getLockedResources().canAfford(buildingInfo.buildCost))
+							continue;
 
-					composition.addNext(BuildThis(buildingInfo, developmentInfo));
-					composition.addNext(SaveResources(buildingInfo.buildCost));
+						Composition composition;
 
-					tasks.push_back(sptr(composition));
+						composition.addNext(BuildThis(buildingInfo, developmentInfo));
+						composition.addNext(SaveResources(buildingInfo.buildCost));
+						tasks.push_back(sptr(composition));
+					}
+					else
+					{
+						tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo)));
+					}
 				}
-				else
-					tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo)));
 			}
 		}
 	}

+ 9 - 8
AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp

@@ -28,9 +28,6 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const
 {
 	Goals::TGoalVec tasks;
 
-	if(ai->cb->getDate(Date::DAY) == 1)
-		return tasks;
-		
 	auto heroes = cb->getHeroesInfo();
 
 	if(heroes.empty())
@@ -38,19 +35,23 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const
 		return tasks;
 	}
 
+	ai->dangerHitMap->updateHitMap();
+
 	for(auto town : cb->getTownsInfo())
 	{
+		uint8_t closestThreat = ai->dangerHitMap->getTileThreat(town->visitablePos()).fastestDanger.turn;
+
+		if (closestThreat >=2 && ai->buildAnalyzer->isGoldPressureHigh() && !town->hasBuilt(BuildingID::CITY_HALL) && cb->canBuildStructure(town, BuildingID::CITY_HALL) != EBuildingState::FORBIDDEN)
+		{
+			return tasks;
+		}
+		
 		auto townArmyAvailableToBuy = ai->armyManager->getArmyAvailableToBuyAsCCreatureSet(
 			town,
 			ai->getFreeResources());
 
 		for(const CGHeroInstance * targetHero : heroes)
 		{
-			if(ai->buildAnalyzer->isGoldPressureHigh()	&& !town->hasBuilt(BuildingID::CITY_HALL))
-			{
-				continue;
-			}
-
 			if(ai->heroManager->getHeroRole(targetHero) == HeroRole::MAIN)
 			{
 				auto reinforcement = ai->armyManager->howManyReinforcementsCanGet(

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

@@ -68,14 +68,6 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(
 		logAi->trace("Path found %s", path.toString());
 #endif
 
-		if(nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
-		{
-#if NKAI_TRACE_LEVEL >= 2
-			logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.getHeroStrength());
-#endif
-			continue;
-		}
-
 		if(objToVisit && !force && !shouldVisit(nullkiller, path.targetHero, objToVisit))
 		{
 #if NKAI_TRACE_LEVEL >= 2
@@ -87,6 +79,9 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(
 		auto hero = path.targetHero;
 		auto danger = path.getTotalDanger();
 
+		if (hero->getOwner() != nullkiller->playerID)
+			continue;
+
 		if(nullkiller->heroManager->getHeroRole(hero) == HeroRole::SCOUT
 			&& (path.getTotalDanger() == 0 || path.turn() > 0)
 			&& path.exchangeCount > 1)

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

@@ -41,6 +41,9 @@ 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;
@@ -130,7 +133,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa
 
 			tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5)));
 
-			return true;
+			return false;
 		}
 		else if(ai->heroManager->getHeroRole(town->garrisonHero.get()) == HeroRole::MAIN)
 		{
@@ -141,7 +144,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa
 			{
 				tasks.push_back(Goals::sptr(Goals::DismissHero(heroToDismiss).setpriority(5)));
 
-				return true;
+				return false;
 			}
 		}
 	}
@@ -158,11 +161,10 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 	
 	threats.push_back(threatNode.fastestDanger); // no guarantee that fastest danger will be there
 
-	if(town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks, ai))
+	if (town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks, ai))
 	{
 		return;
 	}
-
 	if(!threatNode.fastestDanger.hero)
 	{
 		logAi->trace("No threat found for town %s", town->getNameTranslated());
@@ -250,6 +252,16 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 				continue;
 			}
 
+			if (!path.targetHero->canBeMergedWith(*town))
+			{
+#if NKAI_TRACE_LEVEL >= 1
+				logAi->trace("Can't merge armies of hero %s and town %s",
+					path.targetHero->getObjectName(),
+					town->getObjectName());
+#endif
+				continue;
+			}
+
 			if(path.targetHero == town->visitingHero.get() && path.exchangeCount == 1)
 			{
 #if NKAI_TRACE_LEVEL >= 1
@@ -261,6 +273,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 				// dismiss creatures we are not able to pick to be able to hide in garrison
 				if(town->garrisonHero
 					|| town->getUpperArmy()->stacksCount() == 0
+					|| path.targetHero->canBeMergedWith(*town)
 					|| (town->getUpperArmy()->getArmyStrength() < 500 && town->fortLevel() >= CGTownInstance::CITADEL))
 				{
 					tasks.push_back(
@@ -343,23 +356,14 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 			}
 			else if(town->visitingHero && path.targetHero != town->visitingHero && !path.containsHero(town->visitingHero))
 			{
-				if(town->garrisonHero)
+				if(town->garrisonHero && town->garrisonHero != path.targetHero)
 				{
-					if(ai->heroManager->getHeroRole(town->visitingHero.get()) == HeroRole::SCOUT
-						&& town->visitingHero->getArmyStrength() < path.heroArmy->getArmyStrength() / 20)
-					{
-						if(path.turn() == 0)
-							sequence.push_back(sptr(DismissHero(town->visitingHero.get())));
-					}
-					else
-					{
 #if NKAI_TRACE_LEVEL >= 1
-						logAi->trace("Cancel moving %s to defend town %s as the town has garrison hero",
-							path.targetHero->getObjectName(),
-							town->getObjectName());
+					logAi->trace("Cancel moving %s to defend town %s as the town has garrison hero",
+						path.targetHero->getObjectName(),
+						town->getObjectName());
 #endif
-						continue;
-					}
+					continue;
 				}
 				else if(path.turn() == 0)
 				{
@@ -405,6 +409,9 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 
 void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & threat, const CGTownInstance * town, const Nullkiller * ai) const
 {
+	if (threat.turn > 0 || town->garrisonHero || town->visitingHero)
+		return;
+	
 	if(town->hasBuilt(BuildingID::TAVERN)
 		&& ai->cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST)
 	{
@@ -451,7 +458,7 @@ void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitM
 			}
 			else if(ai->heroManager->heroCapReached())
 			{
-				heroToDismiss = ai->heroManager->findWeakHeroToDismiss(hero->getArmyStrength());
+				heroToDismiss = ai->heroManager->findWeakHeroToDismiss(hero->getArmyStrength(), town);
 
 				if(!heroToDismiss)
 					continue;

+ 13 - 29
AI/Nullkiller/Behaviors/ExplorationBehavior.cpp

@@ -33,48 +33,32 @@ Goals::TGoalVec ExplorationBehavior::decompose(const Nullkiller * ai) const
 {
 	Goals::TGoalVec tasks;
 
-	for(auto obj : ai->memory->visitableObjs)
+	for (auto obj : ai->memory->visitableObjs)
 	{
-		if(!vstd::contains(ai->memory->alreadyVisited, obj))
+		switch (obj->ID.num)
 		{
-			switch(obj->ID.num)
-			{
 			case Obj::REDWOOD_OBSERVATORY:
 			case Obj::PILLAR_OF_FIRE:
-				tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 200)).addNext(CaptureObject(obj))));
+			{
+				auto rObj = dynamic_cast<const CRewardableObject*>(obj);
+				if (!rObj->wasScouted(ai->playerID))
+					tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 200)).addNext(CaptureObject(obj))));
 				break;
+			}
 			case Obj::MONOLITH_ONE_WAY_ENTRANCE:
 			case Obj::MONOLITH_TWO_WAY:
 			case Obj::SUBTERRANEAN_GATE:
 			case Obj::WHIRLPOOL:
-				auto tObj = dynamic_cast<const CGTeleport *>(obj);
-				if(TeleportChannel::IMPASSABLE != ai->memory->knownTeleportChannels[tObj->channel]->passability)
-				{
-					tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj))));
-				}
-				break;
-			}
-		}
-		else
-		{
-			switch(obj->ID.num)
 			{
-			case Obj::MONOLITH_TWO_WAY:
-			case Obj::SUBTERRANEAN_GATE:
-			case Obj::WHIRLPOOL:
-				auto tObj = dynamic_cast<const CGTeleport *>(obj);
-				if(TeleportChannel::IMPASSABLE == ai->memory->knownTeleportChannels[tObj->channel]->passability)
-					break;
-				for(auto exit : ai->memory->knownTeleportChannels[tObj->channel]->exits)
+				auto tObj = dynamic_cast<const CGTeleport*>(obj);
+				for (auto exit : cb->getTeleportChannelExits(tObj->channel))
 				{
-					if(!cb->getObj(exit))
-					{ 
-						// Always attempt to visit two-way teleports if one of channel exits is not visible
-						tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj))));
-						break;
+					if (exit != tObj->id)
+					{
+						if (!cb->isVisible(cb->getObjInstance(exit)))
+							tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj))));
 					}
 				}
-				break;
 			}
 		}
 	}

+ 4 - 29
AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp

@@ -81,6 +81,9 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con
 		logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength());
 #endif
 		
+		if (path.targetHero->getOwner() != ai->playerID)
+			continue;
+		
 		if(path.containsHero(hero))
 		{
 #if NKAI_TRACE_LEVEL >= 2
@@ -89,14 +92,6 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con
 			continue;
 		}
 
-		if(path.turn() > 0 && ai->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
-		{
-#if NKAI_TRACE_LEVEL >= 2
-			logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength());
-#endif
-			continue;
-		}
-
 		if(ai->arePathHeroesLocked(path))
 		{
 #if NKAI_TRACE_LEVEL >= 2
@@ -292,17 +287,6 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
 			continue;
 		}
 
-		auto heroRole = ai->heroManager->getHeroRole(path.targetHero);
-
-		if(heroRole == HeroRole::SCOUT
-			&& ai->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
-		{
-#if NKAI_TRACE_LEVEL >= 2
-			logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength());
-#endif
-			continue;
-		}
-
 		auto upgrade = ai->armyManager->calculateCreaturesUpgrade(path.heroArmy, upgrader, availableResources);
 
 		if(!upgrader->garrisonHero
@@ -320,14 +304,6 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
 
 			armyToGetOrBuy.upgradeValue -= path.heroArmy->getArmyStrength();
 
-			armyToGetOrBuy.addArmyToBuy(
-				ai->armyManager->toSlotInfo(
-					ai->armyManager->getArmyAvailableToBuy(
-						path.heroArmy,
-						upgrader,
-						ai->getFreeResources(),
-						path.turn())));
-
 			upgrade.upgradeValue += armyToGetOrBuy.upgradeValue;
 			upgrade.upgradeCost += armyToGetOrBuy.upgradeCost;
 			vstd::concatenate(upgrade.resultingArmy, armyToGetOrBuy.resultingArmy);
@@ -339,8 +315,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
 			{
 				for(auto hero : cb->getAvailableHeroes(upgrader))
 				{
-					auto scoutReinforcement =  ai->armyManager->howManyReinforcementsCanBuy(hero, upgrader)
-						+ ai->armyManager->howManyReinforcementsCanGet(hero, upgrader);
+					auto scoutReinforcement = ai->armyManager->howManyReinforcementsCanGet(hero, upgrader);
 
 					if(scoutReinforcement >= armyToGetOrBuy.upgradeValue
 						&& ai->getFreeGold() >20000

+ 64 - 25
AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp

@@ -31,9 +31,11 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
 
 	auto ourHeroes = ai->heroManager->getHeroRoles();
 	auto minScoreToHireMain = std::numeric_limits<float>::max();
+	int currentArmyValue = 0;
 
 	for(auto hero : ourHeroes)
 	{
+		currentArmyValue += hero.first->getArmyCost();
 		if(hero.second != HeroRole::MAIN)
 			continue;
 
@@ -45,51 +47,88 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
 			minScoreToHireMain = newScore;
 		}
 	}
-
+	// If we don't have any heros we might want to lower our expectations.
+	if (ourHeroes.empty())
+		minScoreToHireMain = 0;
+
+	const CGHeroInstance* bestHeroToHire = nullptr;
+	const CGTownInstance* bestTownToHireFrom = nullptr;
+	float bestScore = 0;
+	bool haveCapitol = false;
+
+	ai->dangerHitMap->updateHitMap();
+	int treasureSourcesCount = 0;
+	
 	for(auto town : towns)
 	{
+		uint8_t closestThreat = UINT8_MAX;
+		for (auto threat : ai->dangerHitMap->getTownThreats(town))
+		{
+			closestThreat = std::min(closestThreat, threat.turn);
+		}
+		//Don't hire a hero where there already is one present
+		if (town->visitingHero && town->garrisonHero)
+			continue;
+		float visitability = 0;
+		for (auto checkHero : ourHeroes)
+		{
+			if (ai->dangerHitMap->getClosestTown(checkHero.first.get()->visitablePos()) == town)
+				visitability++;
+		}
 		if(ai->heroManager->canRecruitHero(town))
 		{
 			auto availableHeroes = ai->cb->getAvailableHeroes(town);
-
-			for(auto hero : availableHeroes)
+			
+			for (auto obj : ai->objectClusterizer->getNearbyObjects())
 			{
-				auto score = ai->heroManager->evaluateHero(hero);
-
-				if(score > minScoreToHireMain)
-				{
-					tasks.push_back(Goals::sptr(Goals::RecruitHero(town, hero).setpriority(200)));
-					break;
-				}
-			}
-
-			int treasureSourcesCount = 0;
-
-			for(auto obj : ai->objectClusterizer->getNearbyObjects())
-			{
-				if((obj->ID == Obj::RESOURCE)
+				if ((obj->ID == Obj::RESOURCE)
 					|| obj->ID == Obj::TREASURE_CHEST
 					|| obj->ID == Obj::CAMPFIRE
 					|| isWeeklyRevisitable(ai, obj)
-					|| obj->ID ==Obj::ARTIFACT)
+					|| obj->ID == Obj::ARTIFACT)
 				{
 					auto tile = obj->visitablePos();
 					auto closestTown = ai->dangerHitMap->getClosestTown(tile);
 
-					if(town == closestTown)
+					if (town == closestTown)
 						treasureSourcesCount++;
 				}
 			}
 
-			if(treasureSourcesCount < 5 && (town->garrisonHero || town->getUpperArmy()->getArmyStrength() < 10000))
-				continue;
-
-			if(ai->cb->getHeroesInfo().size() < ai->cb->getTownsInfo().size() + 1
-				|| (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh()))
+			for(auto hero : availableHeroes)
 			{
-				tasks.push_back(Goals::sptr(Goals::RecruitHero(town).setpriority(3)));
+				auto score = ai->heroManager->evaluateHero(hero);
+				if(score > minScoreToHireMain)
+				{
+					score *= score / minScoreToHireMain;
+				}
+				score *= (hero->getArmyCost() + currentArmyValue);
+				if (hero->getFactionID() == town->getFactionID())
+					score *= 1.5;
+				if (vstd::isAlmostZero(visitability))
+					score *= 30 * town->getTownLevel();
+				else
+					score *= town->getTownLevel() / visitability;
+				if (score > bestScore)
+				{
+					bestScore = score;
+					bestHeroToHire = hero;
+					bestTownToHireFrom = town;
+				}
 			}
 		}
+		if (town->hasCapitol())
+			haveCapitol = true;
+	}
+	if (bestHeroToHire && bestTownToHireFrom)
+	{
+		if (ai->cb->getHeroesInfo().size() == 0
+			|| treasureSourcesCount > ai->cb->getHeroesInfo().size() * 5
+			|| (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh() && haveCapitol)
+			|| (ai->getFreeResources()[EGameResID::GOLD] > 30000 && !ai->buildAnalyzer->isGoldPressureHigh()))
+		{
+			tasks.push_back(Goals::sptr(Goals::RecruitHero(bestTownToHireFrom, bestHeroToHire).setpriority((float)3 / (ourHeroes.size() + 1))));
+		}
 	}
 
 	return tasks;

+ 1 - 10
AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp

@@ -39,9 +39,6 @@ Goals::TGoalVec StayAtTownBehavior::decompose(const Nullkiller * ai) const
 
 	for(auto town : towns)
 	{
-		if(!town->hasBuilt(BuildingID::MAGES_GUILD_1))
-			continue;
-
 		ai->pathfinder->calculatePathInfo(paths, town->visitablePos());
 
 		for(auto & path : paths)
@@ -49,14 +46,8 @@ Goals::TGoalVec StayAtTownBehavior::decompose(const Nullkiller * ai) const
 			if(town->visitingHero && town->visitingHero.get() != path.targetHero)
 				continue;
 
-			if(!path.targetHero->hasSpellbook() || path.targetHero->mana >= 0.75f * path.targetHero->manaLimit())
-				continue;
-
-			if(path.turn() == 0 && !path.getFirstBlockedAction() && path.exchangeCount <= 1)
+			if(!path.getFirstBlockedAction() && path.exchangeCount <= 1)
 			{
-				if(path.targetHero->mana == path.targetHero->manaLimit())
-					continue;
-
 				Composition stayAtTown;
 
 				stayAtTown.addNextSequence({

+ 12 - 3
AI/Nullkiller/Engine/FuzzyHelper.cpp

@@ -52,6 +52,15 @@ ui64 FuzzyHelper::evaluateDanger(const int3 & tile, const CGHeroInstance * visit
 			{
 				objectDanger += evaluateDanger(hero->visitedTown.get());
 			}
+			objectDanger *= ai->heroManager->getFightingStrengthCached(hero);
+		}
+		if (objWithID<Obj::TOWN>(dangerousObject))
+		{
+			auto town = dynamic_cast<const CGTownInstance*>(dangerousObject);
+			auto hero = town->garrisonHero;
+
+			if (hero)
+				objectDanger *= ai->heroManager->getFightingStrengthCached(hero);
 		}
 
 		if(objectDanger)
@@ -117,10 +126,10 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj)
 		{
 			auto fortLevel = town->fortLevel();
 
-			if(fortLevel == CGTownInstance::EFortLevel::CASTLE)
-				danger += 10000;
+			if (fortLevel == CGTownInstance::EFortLevel::CASTLE)
+				danger = std::max(danger * 2, danger + 10000);
 			else if(fortLevel == CGTownInstance::EFortLevel::CITADEL)
-				danger += 4000;
+				danger = std::max(ui64(danger * 1.4), danger + 4000);
 		}
 
 		return danger;

+ 160 - 17
AI/Nullkiller/Engine/Nullkiller.cpp

@@ -122,11 +122,14 @@ void TaskPlan::merge(TSubgoal task)
 {
 	TGoalVec blockers;
 
+	if (task->asTask()->priority <= 0)
+		return;
+
 	for(auto & item : tasks)
 	{
 		for(auto objid : item.affectedObjects)
 		{
-			if(task == item.task || task->asTask()->isObjectAffected(objid))
+			if(task == item.task || task->asTask()->isObjectAffected(objid) || (task->asTask()->getHero() != nullptr && task->asTask()->getHero() == item.task->asTask()->getHero()))
 			{
 				if(item.task->asTask()->priority >= task->asTask()->priority)
 					return;
@@ -166,20 +169,19 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TGoalVec & tasks) const
 	return taskptr(*bestTask);
 }
 
-Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks) const
+Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks, int priorityTier) const
 {
 	TaskPlan taskPlan;
 
-	tbb::parallel_for(tbb::blocked_range<size_t>(0, tasks.size()), [this, &tasks](const tbb::blocked_range<size_t> & r)
+	tbb::parallel_for(tbb::blocked_range<size_t>(0, tasks.size()), [this, &tasks, priorityTier](const tbb::blocked_range<size_t> & r)
 		{
 			auto evaluator = this->priorityEvaluators->acquire();
 
 			for(size_t i = r.begin(); i != r.end(); i++)
 			{
 				auto task = tasks[i];
-
-				if(task->asTask()->priority <= 0)
-					task->asTask()->priority = evaluator->evaluate(task);
+				if (task->asTask()->priority <= 0 || priorityTier != PriorityEvaluator::PriorityTier::BUILDINGS)
+					task->asTask()->priority = evaluator->evaluate(task, priorityTier);
 			}
 		});
 
@@ -326,7 +328,7 @@ bool Nullkiller::arePathHeroesLocked(const AIPath & path) const
 		if(lockReason != HeroLockedReason::NOT_LOCKED)
 		{
 #if NKAI_TRACE_LEVEL >= 1
-			logAi->trace("Hero %s is locked by STARTUP. Discarding %s", path.targetHero->getObjectName(), path.toString());
+			logAi->trace("Hero %s is locked by %d. Discarding %s", path.targetHero->getObjectName(), (int)lockReason,  path.toString());
 #endif
 			return true;
 		}
@@ -347,12 +349,24 @@ void Nullkiller::makeTurn()
 	boost::lock_guard<boost::mutex> sharedStorageLock(AISharedStorage::locker);
 
 	const int MAX_DEPTH = 10;
-	const float FAST_TASK_MINIMAL_PRIORITY = 0.7f;
 
 	resetAiState();
 
 	Goals::TGoalVec bestTasks;
 
+#if NKAI_TRACE_LEVEL >= 1
+	float totalHeroStrength = 0;
+	int totalTownLevel = 0;
+	for (auto heroInfo : cb->getHeroesInfo())
+	{
+		totalHeroStrength += heroInfo->getTotalStrength();
+	}
+	for (auto townInfo : cb->getTownsInfo())
+	{
+		totalTownLevel += townInfo->getTownLevel();
+	}
+	logAi->info("Beginning: Strength: %f Townlevel: %d Resources: %s", totalHeroStrength, totalTownLevel, cb->getResourceAmount().toString());
+#endif
 	for(int i = 1; i <= settings->getMaxPass() && cb->getPlayerStatus(playerID) == EPlayerStatus::INGAME; i++)
 	{
 		auto start = std::chrono::high_resolution_clock::now();
@@ -360,17 +374,21 @@ void Nullkiller::makeTurn()
 
 		Goals::TTask bestTask = taskptr(Goals::Invalid());
 
-		for(;i <= settings->getMaxPass(); i++)
+		while(true)
 		{
 			bestTasks.clear();
 
+			decompose(bestTasks, sptr(RecruitHeroBehavior()), 1);
 			decompose(bestTasks, sptr(BuyArmyBehavior()), 1);
 			decompose(bestTasks, sptr(BuildingBehavior()), 1);
 
 			bestTask = choseBestTask(bestTasks);
 
-			if(bestTask->priority >= FAST_TASK_MINIMAL_PRIORITY)
+			if(bestTask->priority > 0)
 			{
+#if NKAI_TRACE_LEVEL >= 1
+				logAi->info("Pass %d: Performing prio 0 task %s with prio: %d", i, bestTask->toString(), bestTask->priority);
+#endif
 				if(!executeTask(bestTask))
 					return;
 
@@ -382,7 +400,6 @@ void Nullkiller::makeTurn()
 			}
 		}
 
-		decompose(bestTasks, sptr(RecruitHeroBehavior()), 1);
 		decompose(bestTasks, sptr(CaptureObjectsBehavior()), 1);
 		decompose(bestTasks, sptr(ClusterBehavior()), MAX_DEPTH);
 		decompose(bestTasks, sptr(DefenceBehavior()), MAX_DEPTH);
@@ -392,12 +409,24 @@ void Nullkiller::makeTurn()
 		if(!isOpenMap())
 			decompose(bestTasks, sptr(ExplorationBehavior()), MAX_DEPTH);
 
-		if(cb->getDate(Date::DAY) == 1 || heroManager->getHeroRoles().empty())
+		TTaskVec selectedTasks;
+#if NKAI_TRACE_LEVEL >= 1
+		int prioOfTask = 0;
+#endif
+		for (int prio = PriorityEvaluator::PriorityTier::INSTAKILL; prio <= PriorityEvaluator::PriorityTier::DEFEND; ++prio)
 		{
-			decompose(bestTasks, sptr(StartupBehavior()), 1);
+#if NKAI_TRACE_LEVEL >= 1
+			prioOfTask = prio;
+#endif
+			selectedTasks = buildPlan(bestTasks, prio);
+			if (!selectedTasks.empty() || settings->isUseFuzzy())
+				break;
 		}
 
-		auto selectedTasks = buildPlan(bestTasks);
+		std::sort(selectedTasks.begin(), selectedTasks.end(), [](const TTask& a, const TTask& b) 
+		{
+			return a->priority > b->priority;
+		});
 
 		logAi->debug("Decision madel in %ld", timeElapsed(start));
 
@@ -438,7 +467,7 @@ void Nullkiller::makeTurn()
 					bestTask->priority);
 			}
 
-			if(bestTask->priority < MIN_PRIORITY)
+			if((settings->isUseFuzzy() && bestTask->priority < MIN_PRIORITY) || (!settings->isUseFuzzy() && bestTask->priority <= 0))
 			{
 				auto heroes = cb->getHeroesInfo();
 				auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool
@@ -463,7 +492,9 @@ void Nullkiller::makeTurn()
 
 				continue;
 			}
-
+#if NKAI_TRACE_LEVEL >= 1
+			logAi->info("Pass %d: Performing prio %d task %s with prio: %d", i, prioOfTask, bestTask->toString(), bestTask->priority);
+#endif
 			if(!executeTask(bestTask))
 			{
 				if(hasAnySuccess)
@@ -471,13 +502,27 @@ void Nullkiller::makeTurn()
 				else
 					return;
 			}
-
 			hasAnySuccess = true;
 		}
 
+		hasAnySuccess |= handleTrading();
+
 		if(!hasAnySuccess)
 		{
 			logAi->trace("Nothing was done this turn. Ending turn.");
+#if NKAI_TRACE_LEVEL >= 1
+			totalHeroStrength = 0;
+			totalTownLevel = 0;
+			for (auto heroInfo : cb->getHeroesInfo())
+			{
+				totalHeroStrength += heroInfo->getTotalStrength();
+			}
+			for (auto townInfo : cb->getTownsInfo())
+			{
+				totalTownLevel += townInfo->getTownLevel();
+			}
+			logAi->info("End: Strength: %f Townlevel: %d Resources: %s", totalHeroStrength, totalTownLevel, cb->getResourceAmount().toString());
+#endif
 			return;
 		}
 
@@ -554,4 +599,102 @@ void Nullkiller::lockResources(const TResources & res)
 	lockedResources += res;
 }
 
+bool Nullkiller::handleTrading()
+{
+	bool haveTraded = false;
+	bool shouldTryToTrade = true;
+	int marketId = -1;
+	for (auto town : cb->getTownsInfo())
+	{
+		if (town->hasBuiltSomeTradeBuilding())
+		{
+			marketId = town->id;
+		}
+	}
+	if (marketId == -1)
+		return false;
+	if (const CGObjectInstance* obj = cb->getObj(ObjectInstanceID(marketId), false))
+	{
+		if (const auto* m = dynamic_cast<const IMarket*>(obj))
+		{
+			while (shouldTryToTrade)
+			{
+				shouldTryToTrade = false;
+				buildAnalyzer->update();
+				TResources required = buildAnalyzer->getTotalResourcesRequired();
+				TResources income = buildAnalyzer->getDailyIncome();
+				TResources available = cb->getResourceAmount();
+#if NKAI_TRACE_LEVEL >= 2
+				logAi->debug("Available %s", available.toString());
+				logAi->debug("Required  %s", required.toString());
+#endif
+				int mostWanted = -1;
+				int mostExpendable = -1;
+				float minRatio = std::numeric_limits<float>::max();
+				float maxRatio = std::numeric_limits<float>::min();
+
+				for (int i = 0; i < required.size(); ++i)
+				{
+					if (required[i] <= 0)
+						continue;
+					float ratio = static_cast<float>(available[i]) / required[i];
+
+					if (ratio < minRatio) {
+						minRatio = ratio;
+						mostWanted = i;
+					}
+				}
+
+				for (int i = 0; i < required.size(); ++i)
+				{
+					float ratio = available[i];
+					if (required[i] > 0)
+						ratio = static_cast<float>(available[i]) / required[i];
+					else
+						ratio = available[i];
+
+					bool okToSell = false;
+
+					if (i == GameResID::GOLD)
+					{
+						if (income[i] > 0 && !buildAnalyzer->isGoldPressureHigh())
+							okToSell = true;
+					}
+					else
+					{
+						if (required[i] <= 0 && income[i] > 0)
+							okToSell = true;
+					}
+
+					if (ratio > maxRatio && okToSell) {
+						maxRatio = ratio;
+						mostExpendable = i;
+					}
+				}
+#if NKAI_TRACE_LEVEL >= 2
+				logAi->debug("mostExpendable: %d mostWanted: %d", mostExpendable, mostWanted);
+#endif
+				if (mostExpendable == mostWanted || mostWanted == -1 || mostExpendable == -1)
+					return false;
+
+				int toGive;
+				int toGet;
+				m->getOffer(mostExpendable, mostWanted, toGive, toGet, EMarketMode::RESOURCE_RESOURCE);
+				//logAi->info("Offer is: I get %d of %s for %d of %s at %s", toGet, mostWanted, toGive, mostExpendable, obj->getObjectName());
+				//TODO trade only as much as needed
+				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
+					logAi->info("Traded %d of %s for %d of %s at %s", toGive, mostExpendable, toGet, mostWanted, obj->getObjectName());
+#endif
+					haveTraded = true;
+					shouldTryToTrade = true;
+				}
+			}
+		}
+	}
+	return haveTraded;
+}
+
 }

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

@@ -120,13 +120,14 @@ public:
 	ScanDepth getScanDepth() const { return scanDepth; }
 	bool isOpenMap() const { return openMap; }
 	bool isObjectGraphAllowed() const { return useObjectGraph; }
+	bool handleTrading();
 
 private:
 	void resetAiState();
 	void updateAiState(int pass, bool fast = false);
 	void decompose(Goals::TGoalVec & result, Goals::TSubgoal behavior, int decompositionMaxDepth) const;
 	Goals::TTask choseBestTask(Goals::TGoalVec & tasks) const;
-	Goals::TTaskVec buildPlan(Goals::TGoalVec & tasks) const;
+	Goals::TTaskVec buildPlan(Goals::TGoalVec & tasks, int priorityTier) const;
 	bool executeTask(Goals::TTask task);
 	bool areAffectedObjectsPresent(Goals::TTask task) const;
 	HeroRole getTaskRole(Goals::TTask task) const;

+ 401 - 45
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -15,6 +15,8 @@
 #include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h"
 #include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
 #include "../../../lib/mapObjects/MapObjects.h"
+#include "../../../lib/mapping/CMapDefines.h"
+#include "../../../lib/RoadHandler.h"
 #include "../../../lib/CCreatureHandler.h"
 #include "../../../lib/VCMI_Lib.h"
 #include "../../../lib/StartInfo.h"
@@ -37,7 +39,7 @@ namespace NKAI
 #define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us
 const float MIN_CRITICAL_VALUE = 2.0f;
 
-EvaluationContext::EvaluationContext(const Nullkiller * ai)
+EvaluationContext::EvaluationContext(const Nullkiller* ai)
 	: movementCost(0.0),
 	manaCost(0),
 	danger(0),
@@ -51,9 +53,22 @@ EvaluationContext::EvaluationContext(const Nullkiller * ai)
 	heroRole(HeroRole::SCOUT),
 	turn(0),
 	strategicalValue(0),
+	conquestValue(0),
 	evaluator(ai),
 	enemyHeroDangerRatio(0),
-	armyGrowth(0)
+	threat(0),
+	armyGrowth(0),
+	armyInvolvement(0),
+	defenseValue(0),
+	isDefend(false),
+	threatTurns(INT_MAX),
+	involvesSailing(false),
+	isTradeBuilding(false),
+	isExchange(false),
+	isArmyUpgrade(false),
+	isHero(false),
+	isEnemy(false),
+	explorePriority(0)
 {
 }
 
@@ -225,7 +240,7 @@ int getDwellingArmyCost(const CGObjectInstance * target)
 			auto creature = creLevel.second.back().toCreature();
 			auto creaturesAreFree = creature->getLevel() == 1;
 			if(!creaturesAreFree)
-				cost += creature->getRecruitCost(EGameResID::GOLD) * creLevel.first;
+				cost += creature->getFullRecruitCost().marketValue() * creLevel.first;
 		}
 	}
 
@@ -251,6 +266,8 @@ static uint64_t evaluateArtifactArmyValue(const CArtifact * art)
 
 	switch(art->aClass)
 	{
+	case CArtifact::EartClass::ART_TREASURE:
+		//FALL_THROUGH
 	case CArtifact::EartClass::ART_MINOR:
 		classValue = 1000;
 		break;
@@ -289,6 +306,8 @@ uint64_t RewardEvaluator::getArmyReward(
 	case Obj::CREATURE_GENERATOR3:
 	case Obj::CREATURE_GENERATOR4:
 		return getDwellingArmyValue(ai->cb.get(), target, checkGold);
+	case Obj::SPELL_SCROLL:
+		//FALL_THROUGH
 	case Obj::ARTIFACT:
 		return evaluateArtifactArmyValue(dynamic_cast<const CGArtifact *>(target)->storedArtifact->getType());
 	case Obj::HERO:
@@ -479,7 +498,7 @@ uint64_t RewardEvaluator::townArmyGrowth(const CGTownInstance * town) const
 	return result;
 }
 
-uint64_t RewardEvaluator::getManaRecoveryArmyReward(const CGHeroInstance * hero) const
+float RewardEvaluator::getManaRecoveryArmyReward(const CGHeroInstance * hero) const
 {
 	return ai->heroManager->getMagicStrength(hero) * 10000 * (1.0f - std::sqrt(static_cast<float>(hero->mana) / hero->manaLimit()));
 }
@@ -581,6 +600,54 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target, cons
 	return 0;
 }
 
+float RewardEvaluator::getConquestValue(const CGObjectInstance* target) const
+{
+	if (!target)
+		return 0;
+	if (target->getOwner() == ai->playerID)
+		return 0;
+	switch (target->ID)
+	{
+	case Obj::TOWN:
+	{
+		if (ai->buildAnalyzer->getDevelopmentInfo().empty())
+			return 10.0f;
+
+		auto town = dynamic_cast<const CGTownInstance*>(target);
+
+		if (town->getOwner() == ai->playerID)
+		{
+			auto armyIncome = townArmyGrowth(town);
+			auto dailyIncome = town->dailyIncome()[EGameResID::GOLD];
+
+			return std::min(1.0f, std::sqrt(armyIncome / 40000.0f)) + std::min(0.3f, dailyIncome / 10000.0f);
+		}
+
+		auto fortLevel = town->fortLevel();
+		auto booster = 1.0f;
+
+		if (town->hasCapitol())
+			return booster * 1.5;
+
+		if (fortLevel < CGTownInstance::CITADEL)
+			return booster * (town->hasFort() ? 1.0 : 0.8);
+		else
+			return booster * (fortLevel == CGTownInstance::CASTLE ? 1.4 : 1.2);
+	}
+
+	case Obj::HERO:
+		return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES
+			? getEnemyHeroStrategicalValue(dynamic_cast<const CGHeroInstance*>(target))
+			: 0;
+
+	case Obj::KEYMASTER:
+		return 0.6f;
+
+	default:
+		return 0;
+	}
+}
+
 float RewardEvaluator::evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const
 {
 	auto rewardable = dynamic_cast<const CRewardableObject *>(hut);
@@ -705,7 +772,7 @@ int32_t getArmyCost(const CArmedInstance * army)
 
 	for(auto stack : army->Slots())
 	{
-		value += stack.second->getCreatureID().toCreature()->getRecruitCost(EGameResID::GOLD) * stack.second->count;
+		value += stack.second->getCreatureID().toCreature()->getFullRecruitCost().marketValue() * stack.second->count;
 	}
 
 	return value;
@@ -786,7 +853,9 @@ public:
 		uint64_t armyStrength = heroExchange.getReinforcementArmyStrength(evaluationContext.evaluator.ai);
 
 		evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength());
+		evaluationContext.conquestValue += 2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength();
 		evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero);
+		evaluationContext.isExchange = true;
 	}
 };
 
@@ -804,6 +873,7 @@ public:
 
 		evaluationContext.armyReward += upgradeValue;
 		evaluationContext.addNonCriticalStrategicalValue(upgradeValue / (float)armyUpgrade.hero->getArmyStrength());
+		evaluationContext.isArmyUpgrade = true;
 	}
 };
 
@@ -818,22 +888,46 @@ public:
 		int tilesDiscovered = task->value;
 
 		evaluationContext.addNonCriticalStrategicalValue(0.03f * tilesDiscovered);
+		for (auto obj : evaluationContext.evaluator.ai->cb->getVisitableObjs(task->tile))
+		{
+			switch (obj->ID.num)
+			{
+			case Obj::MONOLITH_ONE_WAY_ENTRANCE:
+			case Obj::MONOLITH_TWO_WAY:
+			case Obj::SUBTERRANEAN_GATE:
+				evaluationContext.explorePriority = 1;
+				break;
+			case Obj::REDWOOD_OBSERVATORY:
+			case Obj::PILLAR_OF_FIRE:
+				evaluationContext.explorePriority = 2;
+				break;
+			}
+		}
+		if(evaluationContext.evaluator.ai->cb->getTile(task->tile)->roadType != RoadId::NO_ROAD)
+			evaluationContext.explorePriority = 1;
+		if (evaluationContext.explorePriority == 0)
+			evaluationContext.explorePriority = 3;
 	}
 };
 
 class StayAtTownManaRecoveryEvaluator : public IEvaluationContextBuilder
 {
 public:
-	void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
+	void buildEvaluationContext(EvaluationContext& evaluationContext, Goals::TSubgoal task) const override
 	{
-		if(task->goalType != Goals::STAY_AT_TOWN)
+		if (task->goalType != Goals::STAY_AT_TOWN)
 			return;
 
-		Goals::StayAtTown & stayAtTown = dynamic_cast<Goals::StayAtTown &>(*task);
+		Goals::StayAtTown& stayAtTown = dynamic_cast<Goals::StayAtTown&>(*task);
 
 		evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero());
-		evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted();
-		evaluationContext.movementCost += stayAtTown.getMovementWasted();
+		if (evaluationContext.armyReward == 0)
+			evaluationContext.isDefend = true;
+		else
+		{
+			evaluationContext.movementCost += stayAtTown.getMovementWasted();
+			evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted();
+		}
 	}
 };
 
@@ -844,15 +938,8 @@ void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uin
 	if(enemyDanger.danger)
 	{
 		auto dangerRatio = enemyDanger.danger / (double)ourStrength;
-		auto enemyHero = evaluationContext.evaluator.ai->cb->getObj(enemyDanger.hero.hid, false);
-		bool isAI = enemyHero && isAnotherAi(enemyHero, *evaluationContext.evaluator.ai->cb);
-
-		if(isAI)
-		{
-			dangerRatio *= 1.5; // lets make AI bit more afraid of other AI.
-		}
-
 		vstd::amax(evaluationContext.enemyHeroDangerRatio, dangerRatio);
+		vstd::amax(evaluationContext.threat, enemyDanger.threat);
 	}
 }
 
@@ -896,6 +983,10 @@ public:
 		else
 			evaluationContext.addNonCriticalStrategicalValue(1.7f * multiplier * strategicalValue);
 
+		evaluationContext.defenseValue = town->fortLevel();
+		evaluationContext.isDefend = true;
+		evaluationContext.threatTurns = treat.turn;
+
 		vstd::amax(evaluationContext.danger, defendTown.getTreat().danger);
 		addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength());
 	}
@@ -926,6 +1017,8 @@ public:
 		for(auto & node : path.nodes)
 		{
 			vstd::amax(costsPerHero[node.targetHero], node.cost);
+			if (node.layer == EPathfindingLayer::SAIL)
+				evaluationContext.involvesSailing = true;
 		}
 
 		for(auto pair : costsPerHero)
@@ -952,10 +1045,18 @@ public:
 			evaluationContext.armyGrowth += evaluationContext.evaluator.getArmyGrowth(target, hero, army);
 			evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, heroRole);
 			evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target));
+			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)
+				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();
 		}
 
-		vstd::amax(evaluationContext.armyLossPersentage, path.getTotalArmyLoss() / (double)path.getHeroStrength());
+		vstd::amax(evaluationContext.armyLossPersentage, (float)path.getTotalArmyLoss() / (float)army->getArmyStrength());
 		addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength());
 		vstd::amax(evaluationContext.turn, path.turn());
 	}
@@ -996,6 +1097,7 @@ public:
 			evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold) / boost;
 			evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, role) / boost;
 			evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target) / boost);
+			evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target);
 			evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army) / boost;
 			evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost;
 			evaluationContext.movementCost += objInfo.second.movementCost / boost;
@@ -1021,6 +1123,14 @@ public:
 		Goals::ExchangeSwapTownHeroes & swapCommand = dynamic_cast<Goals::ExchangeSwapTownHeroes &>(*task);
 		const CGHeroInstance * garrisonHero = swapCommand.getGarrisonHero();
 
+		logAi->trace("buildEvaluationContext ExchangeSwapTownHeroesContextBuilder %s affected objects: %d", swapCommand.toString(), swapCommand.getAffectedObjects().size());
+		for (auto obj : swapCommand.getAffectedObjects())
+		{
+			logAi->trace("affected object: %s", evaluationContext.evaluator.ai->cb->getObj(obj)->getObjectName());
+		}
+		if (garrisonHero)
+			logAi->debug("with %s and %d", garrisonHero->getNameTranslated(), int(swapCommand.getLockingReason()));
+
 		if(garrisonHero && swapCommand.getLockingReason() == HeroLockedReason::DEFENCE)
 		{
 			auto defenderRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(garrisonHero);
@@ -1029,6 +1139,9 @@ public:
 			evaluationContext.movementCost += mpLeft;
 			evaluationContext.movementCostByRole[defenderRole] += mpLeft;
 			evaluationContext.heroRole = defenderRole;
+			evaluationContext.isDefend = true;
+			evaluationContext.armyInvolvement = garrisonHero->getArmyStrength();
+			logAi->debug("evaluationContext.isDefend: %d", evaluationContext.isDefend);
 		}
 	}
 };
@@ -1072,8 +1185,14 @@ public:
 		evaluationContext.goldReward += 7 * bi.dailyIncome[EGameResID::GOLD] / 2; // 7 day income but half we already have
 		evaluationContext.heroRole = HeroRole::MAIN;
 		evaluationContext.movementCostByRole[evaluationContext.heroRole] += bi.prerequisitesCount;
-		evaluationContext.goldCost += bi.buildCostWithPrerequisites[EGameResID::GOLD];
+		int32_t cost = bi.buildCost[EGameResID::GOLD];
+		evaluationContext.goldCost += cost;
 		evaluationContext.closestWayRatio = 1;
+		evaluationContext.buildingCost += bi.buildCostWithPrerequisites;
+		if (bi.id == BuildingID::MARKETPLACE || bi.dailyIncome[EGameResID::WOOD] > 0)
+			evaluationContext.isTradeBuilding = true;
+
+		logAi->trace("Building costs for %s : %s MarketValue: %d",bi.toString(), evaluationContext.buildingCost.toString(), evaluationContext.buildingCost.marketValue());
 
 		if(bi.creatureID != CreatureID::NONE)
 		{
@@ -1100,7 +1219,18 @@ 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())
+			{
+				evaluationContext.armyInvolvement += hero->getArmyCost();
+			}
 		}
+		int sameTownBonus = 0;
+		for (auto town : evaluationContext.evaluator.ai->cb->getTownsInfo())
+		{
+			if (buildThis.town->getFaction() == town->getFaction())
+				sameTownBonus += town->getTownLevel();
+		}
+		evaluationContext.armyReward *= sameTownBonus;
 		
 		if(evaluationContext.goldReward)
 		{
@@ -1162,6 +1292,7 @@ EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal
 	for(auto subgoal : parts)
 	{
 		context.goldCost += subgoal->goldCost;
+		context.buildingCost += subgoal->buildingCost;
 
 		for(auto builder : evaluationContextBuilders)
 		{
@@ -1172,7 +1303,7 @@ EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal
 	return context;
 }
 
-float PriorityEvaluator::evaluate(Goals::TSubgoal task)
+float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 {
 	auto evaluationContext = buildEvaluationContext(task);
 
@@ -1185,36 +1316,256 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
 	
 	double result = 0;
 
-	try
+	if (ai->settings->isUseFuzzy())
 	{
-		armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage);
-		heroRoleVariable->setValue(evaluationContext.heroRole);
-		mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]);
-		scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]);
-		goldRewardVariable->setValue(goldRewardPerTurn);
-		armyRewardVariable->setValue(evaluationContext.armyReward);
-		armyGrowthVariable->setValue(evaluationContext.armyGrowth);
-		skillRewardVariable->setValue(evaluationContext.skillReward);
-		dangerVariable->setValue(evaluationContext.danger);
-		rewardTypeVariable->setValue(rewardType);
-		closestHeroRatioVariable->setValue(evaluationContext.closestWayRatio);
-		strategicalValueVariable->setValue(evaluationContext.strategicalValue);
-		goldPressureVariable->setValue(ai->buildAnalyzer->getGoldPressure());
-		goldCostVariable->setValue(evaluationContext.goldCost / ((float)ai->getFreeResources()[EGameResID::GOLD] + (float)ai->buildAnalyzer->getDailyIncome()[EGameResID::GOLD] + 1.0f));
-		turnVariable->setValue(evaluationContext.turn);
-		fearVariable->setValue(evaluationContext.enemyHeroDangerRatio);
-
-		engine->process();
-
-		result = value->getValue();
+		float fuzzyResult = 0;
+		try
+		{
+			armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage);
+			heroRoleVariable->setValue(evaluationContext.heroRole);
+			mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]);
+			scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]);
+			goldRewardVariable->setValue(goldRewardPerTurn);
+			armyRewardVariable->setValue(evaluationContext.armyReward);
+			armyGrowthVariable->setValue(evaluationContext.armyGrowth);
+			skillRewardVariable->setValue(evaluationContext.skillReward);
+			dangerVariable->setValue(evaluationContext.danger);
+			rewardTypeVariable->setValue(rewardType);
+			closestHeroRatioVariable->setValue(evaluationContext.closestWayRatio);
+			strategicalValueVariable->setValue(evaluationContext.strategicalValue);
+			goldPressureVariable->setValue(ai->buildAnalyzer->getGoldPressure());
+			goldCostVariable->setValue(evaluationContext.goldCost / ((float)ai->getFreeResources()[EGameResID::GOLD] + (float)ai->buildAnalyzer->getDailyIncome()[EGameResID::GOLD] + 1.0f));
+			turnVariable->setValue(evaluationContext.turn);
+			fearVariable->setValue(evaluationContext.enemyHeroDangerRatio);
+
+			engine->process();
+
+			fuzzyResult = value->getValue();
+		}
+		catch (fl::Exception& fe)
+		{
+			logAi->error("evaluate VisitTile: %s", fe.getWhat());
+		}
+		result = fuzzyResult;
 	}
-	catch(fl::Exception & fe)
+	else
 	{
-		logAi->error("evaluate VisitTile: %s", fe.getWhat());
+		float score = 0;
+		float maxWillingToLose = ai->cb->getTownsInfo().empty() || (evaluationContext.isDefend && evaluationContext.threatTurns == 0) ? 1 : 0.25;
+
+		bool arriveNextWeek = false;
+		if (ai->cb->getDate(Date::DAY_OF_WEEK) + evaluationContext.turn > 7)
+			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",
+			priorityTier,
+			task->toString(),
+			evaluationContext.armyLossPersentage,
+			(int)evaluationContext.turn,
+			evaluationContext.movementCostByRole[HeroRole::MAIN],
+			evaluationContext.movementCostByRole[HeroRole::SCOUT],
+			goldRewardPerTurn,
+			evaluationContext.goldCost,
+			evaluationContext.armyReward,
+			evaluationContext.armyGrowth,
+			evaluationContext.skillReward,
+			evaluationContext.danger,
+			evaluationContext.threatTurns,
+			evaluationContext.threat,
+			evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout",
+			evaluationContext.strategicalValue,
+			evaluationContext.conquestValue,
+			evaluationContext.closestWayRatio,
+			evaluationContext.enemyHeroDangerRatio,
+			evaluationContext.explorePriority,
+			evaluationContext.isDefend);
+#endif
+
+		switch (priorityTier)
+		{
+			case PriorityTier::INSTAKILL: //Take towns / kill heroes in immediate reach
+			{
+				if (evaluationContext.turn > 0)
+					return 0;
+				if(evaluationContext.conquestValue > 0)
+					score = 1000;
+				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;
+			}
+			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)
+					return 0;
+				score *= evaluationContext.closestWayRatio;
+				break;
+			}
+			case PriorityTier::KILL: //Take towns / kill heroes that are further away
+			{
+				if (evaluationContext.turn > 0 && evaluationContext.isHero)
+					return 0;
+				if (arriveNextWeek && evaluationContext.isEnemy)
+					return 0;
+				if (evaluationContext.conquestValue > 0)
+					score = 1000;
+				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;
+			}
+			case PriorityTier::UPGRADE:
+			{
+				if (!evaluationContext.isArmyUpgrade)
+					return 0;
+				if (evaluationContext.enemyHeroDangerRatio > 1)
+					return 0;
+				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
+					return 0;
+				score = 1000;
+				score *= evaluationContext.closestWayRatio;
+				if (evaluationContext.movementCost > 0)
+					score /= evaluationContext.movementCost;
+				break;
+			}
+			case PriorityTier::HIGH_PRIO_EXPLORE:
+			{
+				if (evaluationContext.enemyHeroDangerRatio > 1)
+					return 0;
+				if (evaluationContext.explorePriority != 1)
+					return 0;
+				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
+					return 0;
+				score = 1000;
+				score *= evaluationContext.closestWayRatio;
+				if (evaluationContext.movementCost > 0)
+					score /= evaluationContext.movementCost;
+				break;
+			}
+			case PriorityTier::HUNTER_GATHER: //Collect guarded stuff
+			{
+				if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend)
+					return 0;
+				if (evaluationContext.buildingCost.marketValue() > 0)
+					return 0;
+				if (evaluationContext.isDefend && (evaluationContext.enemyHeroDangerRatio < 1 || 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)
+					return 0;
+				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
+					return 0;
+				score += evaluationContext.strategicalValue * 1000;
+				score += evaluationContext.goldReward;
+				score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05;
+				score += evaluationContext.armyReward;
+				score += evaluationContext.armyGrowth;
+				score -= evaluationContext.goldCost;
+				score -= evaluationContext.armyInvolvement * evaluationContext.armyLossPersentage;
+				if (score > 0)
+				{
+					score = 1000;
+					score *= evaluationContext.closestWayRatio;
+					if (evaluationContext.movementCost > 0)
+						score /= evaluationContext.movementCost;
+				}
+				break;
+			}
+			case PriorityTier::LOW_PRIO_EXPLORE:
+			{
+				if (evaluationContext.enemyHeroDangerRatio > 1)
+					return 0;
+				if (evaluationContext.explorePriority != 3)
+					return 0;
+				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
+					return 0;
+				score = 1000;
+				score *= evaluationContext.closestWayRatio;
+				if (evaluationContext.movementCost > 0)
+					score /= evaluationContext.movementCost;
+				break;
+			}
+			case PriorityTier::DEFEND: //Defend whatever if nothing else is to do
+			{
+				if (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.isExchange)
+					return 0;
+				if (evaluationContext.isDefend || evaluationContext.isArmyUpgrade)
+					score = 1000;
+				score *= evaluationContext.closestWayRatio;
+				score /= (evaluationContext.turn + 1);
+				break;
+			}
+			case PriorityTier::BUILDINGS: //For buildings and buying army
+			{
+				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
+					return 0;
+				//If we already have locked resources, we don't look at other buildings
+				if (ai->getLockedResources().marketValue() > 0)
+					return 0;
+				score += evaluationContext.conquestValue * 1000;
+				score += evaluationContext.strategicalValue * 1000;
+				score += evaluationContext.goldReward;
+				score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05;
+				score += evaluationContext.armyReward;
+				score += evaluationContext.armyGrowth;
+				if (evaluationContext.buildingCost.marketValue() > 0)
+				{
+					if (!evaluationContext.isTradeBuilding && ai->getFreeResources()[EGameResID::WOOD] - evaluationContext.buildingCost[EGameResID::WOOD] < 5 && ai->buildAnalyzer->getDailyIncome()[EGameResID::WOOD] < 1)
+					{
+						logAi->trace("Should make sure to build market-place instead of %s", task->toString());
+						for (auto town : ai->cb->getTownsInfo())
+						{
+							if (!town->hasBuiltSomeTradeBuilding())
+								return 0;
+						}
+					}
+					score += 1000;
+					auto resourcesAvailable = evaluationContext.evaluator.ai->getFreeResources();
+					auto income = ai->buildAnalyzer->getDailyIncome();
+					if(ai->buildAnalyzer->isGoldPressureHigh())
+						score /= evaluationContext.buildingCost.marketValue();
+					if (!resourcesAvailable.canAfford(evaluationContext.buildingCost))
+					{
+						TResources needed = evaluationContext.buildingCost - resourcesAvailable;
+						needed.positive();
+						int turnsTo = needed.maxPurchasableCount(income);
+						if (turnsTo == INT_MAX)
+							return 0;
+						else
+							score /= turnsTo;
+					}
+				}
+				else
+				{
+					if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend && vstd::isAlmostZero(evaluationContext.conquestValue))
+						return 0;
+				}
+				break;
+			}
+		}
+		result = score;
+		//TODO: Figure out the root cause for why evaluationContext.closestWayRatio has become -nan(ind).
+		if (std::isnan(result))
+			return 0;
 	}
 
 #if NKAI_TRACE_LEVEL >= 2
-	logAi->trace("Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, danger: %d, role: %s, strategical value: %f, cwr: %f, fear: %f, result %f",
+	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",
+		priorityTier,
 		task->toString(),
 		evaluationContext.armyLossPersentage,
 		(int)evaluationContext.turn,
@@ -1223,9 +1574,14 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
 		goldRewardPerTurn,
 		evaluationContext.goldCost,
 		evaluationContext.armyReward,
+		evaluationContext.armyGrowth,
+		evaluationContext.skillReward,
 		evaluationContext.danger,
+		evaluationContext.threatTurns,
+		evaluationContext.threat,
 		evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout",
 		evaluationContext.strategicalValue,
+		evaluationContext.conquestValue,
 		evaluationContext.closestWayRatio,
 		evaluationContext.enemyHeroDangerRatio,
 		result);

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

@@ -41,6 +41,7 @@ public:
 	float getResourceRequirementStrength(int resType) const;
 	float getResourceRequirementStrength(const TResources & res) const;
 	float getStrategicalValue(const CGObjectInstance * target, const CGHeroInstance * hero = nullptr) const;
+	float getConquestValue(const CGObjectInstance* target) const;
 	float getTotalResourceRequirementStrength(int resType) const;
 	float evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const;
 	float getSkillReward(const CGObjectInstance * target, const CGHeroInstance * hero, HeroRole role) const;
@@ -48,7 +49,7 @@ public:
 	uint64_t getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const;
 	const HitMapInfo & getEnemyHeroDanger(const int3 & tile, uint8_t turn) const;
 	uint64_t townArmyGrowth(const CGTownInstance * town) const;
-	uint64_t getManaRecoveryArmyReward(const CGHeroInstance * hero) const;
+	float getManaRecoveryArmyReward(const CGHeroInstance * hero) const;
 };
 
 struct DLL_EXPORT EvaluationContext
@@ -65,10 +66,24 @@ struct DLL_EXPORT EvaluationContext
 	int32_t goldCost;
 	float skillReward;
 	float strategicalValue;
+	float conquestValue;
 	HeroRole heroRole;
 	uint8_t turn;
 	RewardEvaluator evaluator;
 	float enemyHeroDangerRatio;
+	float threat;
+	float armyInvolvement;
+	int defenseValue;
+	bool isDefend;
+	int threatTurns;
+	TResources buildingCost;
+	bool involvesSailing;
+	bool isTradeBuilding;
+	bool isExchange;
+	bool isArmyUpgrade;
+	bool isHero;
+	bool isEnemy;
+	int explorePriority;
 
 	EvaluationContext(const Nullkiller * ai);
 
@@ -91,7 +106,20 @@ public:
 	~PriorityEvaluator();
 	void initVisitTile();
 
-	float evaluate(Goals::TSubgoal task);
+	float evaluate(Goals::TSubgoal task, int priorityTier = BUILDINGS);
+
+	enum PriorityTier : int32_t
+	{
+		BUILDINGS = 0,
+		INSTAKILL,
+		INSTADEFEND,
+		KILL,
+		UPGRADE,
+		HIGH_PRIO_EXPLORE,
+		HUNTER_GATHER,
+		LOW_PRIO_EXPLORE,
+		DEFEND
+	};
 
 private:
 	const Nullkiller * ai;

+ 7 - 1
AI/Nullkiller/Engine/Settings.cpp

@@ -30,7 +30,8 @@ namespace NKAI
 		maxpass(10),
 		allowObjectGraph(true),
 		useTroopsFromGarrisons(false),
-		openMap(true)
+		openMap(true),
+		useFuzzy(false)
 	{
 		JsonNode node = JsonUtils::assembleFromFiles("config/ai/nkai/nkai-settings");
 
@@ -69,6 +70,11 @@ namespace NKAI
 			openMap = node.Struct()["openMap"].Bool();
 		}
 
+		if (!node.Struct()["useFuzzy"].isNull())
+		{
+			useFuzzy = node.Struct()["useFuzzy"].Bool();
+		}
+
 		if(!node.Struct()["useTroopsFromGarrisons"].isNull())
 		{
 			useTroopsFromGarrisons = node.Struct()["useTroopsFromGarrisons"].Bool();

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

@@ -29,6 +29,7 @@ namespace NKAI
 		bool allowObjectGraph;
 		bool useTroopsFromGarrisons;
 		bool openMap;
+		bool useFuzzy;
 
 	public:
 		Settings();
@@ -41,5 +42,6 @@ namespace NKAI
 		bool isObjectGraphAllowed() const { return allowObjectGraph; }
 		bool isGarrisonTroopsUsageAllowed() const { return useTroopsFromGarrisons; }
 		bool isOpenMap() const { return openMap; }
+		bool isUseFuzzy() const { return useFuzzy; }
 	};
 }

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

@@ -104,6 +104,7 @@ namespace Goals
 		bool isAbstract; SETTER(bool, isAbstract)
 		int value; SETTER(int, value)
 		ui64 goldCost; SETTER(ui64, goldCost)
+		TResources buildingCost; SETTER(TResources, buildingCost)
 		int resID; SETTER(int, resID)
 		int objid; SETTER(int, objid)
 		int aid; SETTER(int, aid)

+ 3 - 0
AI/Nullkiller/Goals/AdventureSpellCast.cpp

@@ -53,6 +53,9 @@ void AdventureSpellCast::accept(AIGateway * ai)
 			throw cannotFulfillGoalException("The town is already occupied by " + town->visitingHero->getNameTranslated());
 	}
 
+	if (hero->inTownGarrison)
+		ai->myCb->swapGarrisonHero(hero->visitedTown);
+
 	auto wait = cb->waitTillRealize;
 
 	cb->waitTillRealize = true;

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

@@ -90,9 +90,12 @@ void ExchangeSwapTownHeroes::accept(AIGateway * ai)
 	
 	if(!town->garrisonHero)
 	{
-		while(upperArmy->stacksCount() != 0)
+		if (!garrisonHero->canBeMergedWith(*town))
 		{
-			cb->dismissCreature(upperArmy, upperArmy->Slots().begin()->first);
+			while (upperArmy->stacksCount() != 0)
+			{
+				cb->dismissCreature(upperArmy, upperArmy->Slots().begin()->first);
+			}
 		}
 	}
 	

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

@@ -22,6 +22,7 @@ ExecuteHeroChain::ExecuteHeroChain(const AIPath & path, const CGObjectInstance *
 {
 	hero = path.targetHero;
 	tile = path.targetTile();
+	closestWayRatio = 1;
 
 	if(obj)
 	{
@@ -85,6 +86,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 
 	ai->nullkiller->setActive(chainPath.targetHero, tile);
 	ai->nullkiller->setTargetObject(objid);
+	ai->nullkiller->objectClusterizer->reset();
 
 	auto targetObject = ai->myCb->getObj(static_cast<ObjectInstanceID>(objid), false);
 

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

@@ -73,6 +73,7 @@ void RecruitHero::accept(AIGateway * ai)
 		std::unique_lock lockGuard(ai->nullkiller->aiStateMutex);
 
 		ai->nullkiller->heroManager->update();
+		ai->nullkiller->objectClusterizer->reset();
 	}
 }
 

+ 1 - 0
AI/Nullkiller/Goals/RecruitHero.h

@@ -44,6 +44,7 @@ namespace Goals
 		}
 
 		std::string toString() const override;
+		const CGHeroInstance* getHero() const override { return heroToBuy; }
 		void accept(AIGateway * ai) override;
 	};
 }

+ 2 - 6
AI/Nullkiller/Goals/StayAtTown.cpp

@@ -36,16 +36,12 @@ std::string StayAtTown::toString() const
 {
 	return "Stay at town " + town->getNameTranslated()
 		+ " hero " + hero->getNameTranslated()
-		+ ", mana: " + std::to_string(hero->mana);
+		+ ", mana: " + std::to_string(hero->mana)
+		+ " / " + std::to_string(hero->manaLimit());
 }
 
 void StayAtTown::accept(AIGateway * ai)
 {
-	if(hero->visitedTown != town)
-	{
-		logAi->error("Hero %s expected visiting town %s", hero->getNameTranslated(), town->getNameTranslated());
-	}
-
 	ai->nullkiller->lockHero(hero, HeroLockedReason::DEFENCE);
 }
 

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

@@ -719,6 +719,7 @@ void HeroChainCalculationTask::calculateHeroChain(
 		if(node->action == EPathNodeAction::BATTLE
 			|| node->action == EPathNodeAction::TELEPORT_BATTLE
 			|| node->action == EPathNodeAction::TELEPORT_NORMAL
+			|| node->action == EPathNodeAction::DISEMBARK
 			|| node->action == EPathNodeAction::TELEPORT_BLOCKING_VISIT)
 		{
 			continue;
@@ -961,7 +962,7 @@ void AINodeStorage::setHeroes(std::map<const CGHeroInstance *, HeroRole> heroes)
 		// do not allow our own heroes in garrison to act on map
 		if(hero.first->getOwner() == ai->playerID
 			&& hero.first->inTownGarrison
-			&& (ai->isHeroLocked(hero.first) || ai->heroManager->heroCapReached()))
+			&& (ai->isHeroLocked(hero.first) || ai->heroManager->heroCapReached(false)))
 		{
 			continue;
 		}
@@ -1196,6 +1197,11 @@ void AINodeStorage::calculateTownPortal(
 					continue;
 			}
 
+			if (targetTown->visitingHero
+				&& (targetTown->visitingHero.get()->getFactionID() != actor->hero->getFactionID()
+					|| targetTown->getUpperArmy()->stacksCount()))
+				continue;
+
 			auto nodeOptional = townPortalFinder.createTownPortalNode(targetTown);
 
 			if(nodeOptional)
@@ -1418,6 +1424,10 @@ void AINodeStorage::calculateChainInfo(std::vector<AIPath> & paths, const int3 &
 		path.heroArmy = node.actor->creatureSet;
 		path.armyLoss = node.armyLoss;
 		path.targetObjectDanger = ai->dangerEvaluator->evaluateDanger(pos, path.targetHero, !node.actor->allowBattle);
+		for (auto pathNode : path.nodes)
+		{
+			path.targetObjectDanger = std::max(ai->dangerEvaluator->evaluateDanger(pathNode.coord, path.targetHero, !node.actor->allowBattle), path.targetObjectDanger);
+		}
 
 		if(path.targetObjectDanger > 0)
 		{
@@ -1564,7 +1574,7 @@ uint8_t AIPath::turn() const
 
 uint64_t AIPath::getHeroStrength() const
 {
-	return targetHero->getFightingStrength() * getHeroArmyStrengthWithCommander(targetHero, heroArmy);
+	return targetHero->getHeroStrength() * getHeroArmyStrengthWithCommander(targetHero, heroArmy);
 }
 
 uint64_t AIPath::getTotalDanger() const

+ 2 - 2
AI/Nullkiller/Pathfinding/AINodeStorage.h

@@ -29,8 +29,8 @@ namespace NKAI
 {
 namespace AIPathfinding
 {
-	const int BUCKET_COUNT = 3;
-	const int BUCKET_SIZE = 7;
+	const int BUCKET_COUNT = 1;
+	const int BUCKET_SIZE = 32;
 	const int NUM_CHAINS = BUCKET_COUNT * BUCKET_SIZE;
 	const int CHAIN_MAX_DEPTH = 4;
 }

+ 1 - 1
AI/Nullkiller/Pathfinding/Actors.cpp

@@ -46,7 +46,7 @@ ChainActor::ChainActor(const CGHeroInstance * hero, HeroRole heroRole, uint64_t
 	initialMovement = hero->movementPointsRemaining();
 	initialTurn = 0;
 	armyValue = getHeroArmyStrengthWithCommander(hero, hero);
-	heroFightingStrength = hero->getFightingStrength();
+	heroFightingStrength = hero->getHeroStrength();
 	tiCache.reset(new TurnInfo(hero));
 }
 

+ 1 - 1
client/lobby/SelectionTab.cpp

@@ -1112,6 +1112,6 @@ void SelectionTab::ListItem::updateItem(std::shared_ptr<ElementInfo> info, bool
 		labelName->alignment = ETextAlignment::CENTER;
 		labelName->moveTo(Point(pos.x + LABEL_POS_X, labelName->pos.y));
 	}
-	labelName->setText(info->name);
+	labelName->setText(info->getNameForList());
 	labelName->setColor(color);
 }

+ 1 - 1
config/ai/nkai/nkai-settings.json

@@ -6,5 +6,5 @@
 	"maxGoldPressure" : 0.3,
 	"useTroopsFromGarrisons" : true,
 	"openMap": true,
-	"allowObjectGraph": true
+	"allowObjectGraph": false
 }

+ 14 - 0
lib/CCreatureSet.cpp

@@ -361,6 +361,14 @@ ui64 CCreatureSet::getArmyStrength() const
 	return ret;
 }
 
+ui64 CCreatureSet::getArmyCost() const
+{
+	ui64 ret = 0;
+	for (const auto& elem : stacks)
+		ret += elem.second->getMarketValue();
+	return ret;
+}
+
 ui64 CCreatureSet::getPower(const SlotID & slot) const
 {
 	return getStack(slot).getPower();
@@ -853,6 +861,12 @@ ui64 CStackInstance::getPower() const
 	return static_cast<ui64>(getType()->getAIValue()) * count;
 }
 
+ui64 CStackInstance::getMarketValue() const
+{
+	assert(getType());
+	return getType()->getFullRecruitCost().marketValue() * count;
+}
+
 ArtBearer::ArtBearer CStackInstance::bearerType() const
 {
 	return ArtBearer::CREATURE;

+ 3 - 0
lib/CCreatureSet.h

@@ -107,6 +107,8 @@ public:
 	FactionID getFactionID() const override;
 
 	virtual ui64 getPower() const;
+	/// Returns total market value of resources needed to recruit this unit
+	virtual ui64 getMarketValue() const;
 	CCreature::CreatureQuantityId getQuantityID() const;
 	std::string getQuantityTXT(bool capitalized = true) const;
 	virtual int getExpRank() const;
@@ -272,6 +274,7 @@ public:
 	int stacksCount() const;
 	virtual bool needsLastStack() const; //true if last stack cannot be taken
 	ui64 getArmyStrength() 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
 	std::string getArmyDescription() const;

+ 20 - 11
lib/ResourceSet.h

@@ -148,6 +148,26 @@ public:
 		return ret;
 	}
 
+	//Returns how many items of "this" we can afford with provided funds
+	int maxPurchasableCount(const ResourceSet& availableFunds) {
+		int ret = 0; // Initialize to 0 because we want the maximum number of accumulations
+
+		for (size_t i = 0; i < container.size(); ++i) {
+			if (container.at(i) > 0) { // We only care about fulfilling positive needs
+				if (availableFunds[i] == 0) {
+					// If income is 0 and we need a positive amount, it's impossible to fulfill
+					return INT_MAX;
+				}
+				else {
+					// Calculate the number of times we need to accumulate income to fulfill the need
+					int ceiledResult = vstd::divideAndCeil(container.at(i), availableFunds[i]);
+					ret = std::max(ret, ceiledResult);
+				}
+			}
+		}
+		return ret;
+	}
+
 	ResourceSet & operator=(const TResource &rhs)
 	{
 		for(int & i : container)
@@ -169,17 +189,6 @@ public:
 		return this->container == rhs.container;
 	}
 
-// WARNING: comparison operators are used for "can afford" relation: a <= b means that foreach i a[i] <= b[i]
-// that doesn't work the other way: a > b doesn't mean that a cannot be afforded with b, it's still b can afford a
-// 		bool operator<(const ResourceSet &rhs)
-// 		{
-// 			for(int i = 0; i < size(); i++)
-// 				if(at(i) >= rhs[i])
-// 					return false;
-//
-// 			return true;
-// 		}
-
 	template <typename Handler> void serialize(Handler &h)
 	{
 		h & container;

+ 1 - 1
lib/campaign/CampaignState.cpp

@@ -339,7 +339,7 @@ void CampaignState::setCurrentMapAsConquered(std::vector<CGHeroInstance *> heroe
 {
 	range::sort(heroes, [](const CGHeroInstance * a, const CGHeroInstance * b)
 	{
-		return a->getHeroStrength() > b->getHeroStrength();
+		return a->getHeroStrengthForCampaign() > b->getHeroStrengthForCampaign();
 	});
 
 	logGlobal->info("Scenario %d of campaign %s (%s) has been completed", currentMap->getNum(), getFilename(), getNameTranslated());

+ 31 - 2
lib/mapObjects/CGHeroInstance.cpp

@@ -711,7 +711,25 @@ double CGHeroInstance::getFightingStrength() const
 
 double CGHeroInstance::getMagicStrength() const
 {
-	return sqrt((1.0 + 0.05*getPrimSkillLevel(PrimarySkill::KNOWLEDGE)) * (1.0 + 0.05*getPrimSkillLevel(PrimarySkill::SPELL_POWER)));
+	if (!hasSpellbook())
+		return 1;
+	bool atLeastOneCombatSpell = false;
+	for (auto spell : spells)
+	{
+		if (spellbookContainsSpell(spell) && spell.toSpell()->isCombat())
+		{
+			atLeastOneCombatSpell = true;
+			break;
+		}
+	}
+	if (!atLeastOneCombatSpell)
+		return 1;
+	return sqrt((1.0 + 0.05*getPrimSkillLevel(PrimarySkill::KNOWLEDGE) * mana / manaLimit()) * (1.0 + 0.05*getPrimSkillLevel(PrimarySkill::SPELL_POWER) * mana / manaLimit()));
+}
+
+double CGHeroInstance::getMagicStrengthForCampaign() const
+{
+	return sqrt((1.0 + 0.05 * getPrimSkillLevel(PrimarySkill::KNOWLEDGE)) * (1.0 + 0.05 * getPrimSkillLevel(PrimarySkill::SPELL_POWER)));
 }
 
 double CGHeroInstance::getHeroStrength() const
@@ -719,9 +737,14 @@ double CGHeroInstance::getHeroStrength() const
 	return sqrt(pow(getFightingStrength(), 2.0) * pow(getMagicStrength(), 2.0));
 }
 
+double CGHeroInstance::getHeroStrengthForCampaign() const
+{
+	return sqrt(pow(getFightingStrength(), 2.0) * pow(getMagicStrengthForCampaign(), 2.0));
+}
+
 ui64 CGHeroInstance::getTotalStrength() const
 {
-	double ret = getFightingStrength() * getArmyStrength();
+	double ret = getHeroStrength() * getArmyStrength();
 	return static_cast<ui64>(ret);
 }
 
@@ -1896,5 +1919,11 @@ const IOwnableObject * CGHeroInstance::asOwnable() const
 	return this;
 }
 
+int CGHeroInstance::getBasePrimarySkillValue(PrimarySkill which) const
+{
+	std::string cachingStr = "type_PRIMARY_SKILL_base_" + std::to_string(static_cast<int>(which));
+	auto selector = Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(which)).And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL));
+	return valOfBonuses(selector, cachingStr);
+}
 
 VCMI_LIB_NAMESPACE_END

+ 4 - 1
lib/mapObjects/CGHeroInstance.h

@@ -223,10 +223,13 @@ public:
 	int movementPointsAfterEmbark(int MPsBefore, int basicCost, bool disembark = false, const TurnInfo * ti = nullptr) const;
 
 	double getFightingStrength() const; // takes attack / defense skill into account
-	double getMagicStrength() const; // takes knowledge / spell power skill into account
+	double getMagicStrength() const; // takes knowledge / spell power skill but also current mana, whether the hero owns a spell-book and whether that books contains anything into account
+	double getMagicStrengthForCampaign() const; // takes knowledge / spell power skill into account
 	double getHeroStrength() const; // includes fighting and magic strength
+	double getHeroStrengthForCampaign() const; // includes fighting and the for-campaign-version of magic strength
 	ui64 getTotalStrength() const; // includes fighting strength and army strength
 	TExpType calculateXp(TExpType exp) const; //apply learning skill
+	int getBasePrimarySkillValue(PrimarySkill which) const; //the value of a base-skill without items or temporary bonuses
 
 	CStackBasicDescriptor calculateNecromancy (const BattleResult &battleResult) const;
 	void showNecromancyDialog(const CStackBasicDescriptor &raisedStack, vstd::RNG & rand) const;

+ 1 - 1
server/CGameHandler.cpp

@@ -886,7 +886,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, EMovementMode moveme
 		return complainRet("Cannot disembark hero, tile is blocked!");
 
 	if(distance(h->pos, dst) >= 1.5 && movementMode == EMovementMode::STANDARD)
-		return complainRet("Tiles are not neighboring!");
+		return complainRet("Tiles " + h->pos.toString()+ " and "+ dst.toString() +" are not neighboring!");
 
 	if(h->inTownGarrison)
 		return complainRet("Can not move garrisoned hero!");

+ 46 - 0
server/processors/NewTurnProcessor.cpp

@@ -237,6 +237,52 @@ ResourceSet NewTurnProcessor::generatePlayerIncome(PlayerColor playerID, bool ne
 	for (auto obj :	state.getOwnedObjects())
 		incomeHandicapped += obj->asOwnable()->dailyIncome();
 
+	if (!state.isHuman())
+	{
+		// Initialize bonuses for different resources
+		std::array<int, GameResID::COUNT> weeklyBonuses = {};
+
+		// Calculate weekly bonuses based on difficulty
+		if (gameHandler->gameState()->getStartInfo()->difficulty == 0)
+		{
+			weeklyBonuses[EGameResID::GOLD] = static_cast<int>(std::round(incomeHandicapped[EGameResID::GOLD] * (0.75 - 1) * 7));
+		}
+		else if (gameHandler->gameState()->getStartInfo()->difficulty == 3)
+		{
+			weeklyBonuses[EGameResID::GOLD] = static_cast<int>(std::round(incomeHandicapped[EGameResID::GOLD] * 0.25 * 7));
+			weeklyBonuses[EGameResID::WOOD] = static_cast<int>(std::round(incomeHandicapped[EGameResID::WOOD] * 0.39 * 7));
+			weeklyBonuses[EGameResID::ORE] = static_cast<int>(std::round(incomeHandicapped[EGameResID::ORE] * 0.39 * 7));
+			weeklyBonuses[EGameResID::MERCURY] = static_cast<int>(std::round(incomeHandicapped[EGameResID::MERCURY] * 0.14 * 7));
+			weeklyBonuses[EGameResID::CRYSTAL] = static_cast<int>(std::round(incomeHandicapped[EGameResID::CRYSTAL] * 0.14 * 7));
+			weeklyBonuses[EGameResID::SULFUR] = static_cast<int>(std::round(incomeHandicapped[EGameResID::SULFUR] * 0.14 * 7));
+			weeklyBonuses[EGameResID::GEMS] = static_cast<int>(std::round(incomeHandicapped[EGameResID::GEMS] * 0.14 * 7));
+		}
+		else if (gameHandler->gameState()->getStartInfo()->difficulty == 4)
+		{
+			weeklyBonuses[EGameResID::GOLD] = static_cast<int>(std::round(incomeHandicapped[EGameResID::GOLD] * 0.5 * 7));
+			weeklyBonuses[EGameResID::WOOD] = static_cast<int>(std::round(incomeHandicapped[EGameResID::WOOD] * 0.53 * 7));
+			weeklyBonuses[EGameResID::ORE] = static_cast<int>(std::round(incomeHandicapped[EGameResID::ORE] * 0.53 * 7));
+			weeklyBonuses[EGameResID::MERCURY] = static_cast<int>(std::round(incomeHandicapped[EGameResID::MERCURY] * 0.28 * 7));
+			weeklyBonuses[EGameResID::CRYSTAL] = static_cast<int>(std::round(incomeHandicapped[EGameResID::CRYSTAL] * 0.28 * 7));
+			weeklyBonuses[EGameResID::SULFUR] = static_cast<int>(std::round(incomeHandicapped[EGameResID::SULFUR] * 0.28 * 7));
+			weeklyBonuses[EGameResID::GEMS] = static_cast<int>(std::round(incomeHandicapped[EGameResID::GEMS] * 0.28 * 7));
+		}
+
+		// Distribute weekly bonuses over 7 days, depending on the current day of the week
+		for (int i = 0; i < GameResID::COUNT; ++i)
+		{
+			int dailyBonus = weeklyBonuses[i] / 7;
+			int remainderBonus = weeklyBonuses[i] % 7;
+
+			// Apply the daily bonus for each day, and distribute the remainder accordingly
+			incomeHandicapped[static_cast<GameResID>(i)] += dailyBonus;
+			if (gameHandler->gameState()->getDate(Date::DAY_OF_WEEK) - 1 < remainderBonus)
+			{
+				incomeHandicapped[static_cast<GameResID>(i)] += 1;
+			}
+		}
+	}
+
 	return incomeHandicapped;
 }
 

+ 1 - 1
test/googletest

@@ -1 +1 @@
-Subproject commit b514bdc898e2951020cbdca1304b75f5950d1f59
+Subproject commit b796f7d44681514f58a683a3a71ff17c94edb0c1