Browse Source

Fuzzy rework, added more defence and gather army routines

Andrii Danylchenko 2 years ago
parent
commit
b19ac01bf9

+ 5 - 6
AI/Nullkiller/AIGateway.cpp

@@ -29,7 +29,7 @@ namespace NKAI
 {
 {
 
 
 // our to enemy strength ratio constants
 // our to enemy strength ratio constants
-const float SAFE_ATTACK_CONSTANT = 1.2f;
+const float SAFE_ATTACK_CONSTANT = 1.1f;
 const float RETREAT_THRESHOLD = 0.3f;
 const float RETREAT_THRESHOLD = 0.3f;
 const double RETREAT_ABSOLUTE_THRESHOLD = 10000.;
 const double RETREAT_ABSOLUTE_THRESHOLD = 10000.;
 
 
@@ -90,9 +90,11 @@ void AIGateway::heroMoved(const TryMoveHero & details, bool verbose)
 	LOG_TRACE(logAi);
 	LOG_TRACE(logAi);
 	NET_EVENT_HANDLER;
 	NET_EVENT_HANDLER;
 
 
-	validateObject(details.id); //enemy hero may have left visible area
 	auto hero = cb->getHero(details.id);
 	auto hero = cb->getHero(details.id);
 
 
+	if(!hero)
+		validateObject(details.id); //enemy hero may have left visible area
+
 	const int3 from = hero ? hero->convertToVisitablePos(details.start) : (details.start - int3(0,1,0));;
 	const int3 from = hero ? hero->convertToVisitablePos(details.start) : (details.start - int3(0,1,0));;
 	const int3 to   = hero ? hero->convertToVisitablePos(details.end)   : (details.end   - int3(0,1,0));
 	const int3 to   = hero ? hero->convertToVisitablePos(details.end)   : (details.end   - int3(0,1,0));
 
 
@@ -794,10 +796,7 @@ void AIGateway::makeTurn()
 
 
 	cb->sendMessage("vcmieagles");
 	cb->sendMessage("vcmieagles");
 
 
-	if(cb->getDate(Date::DAY) == 1)
-	{
-		retrieveVisitableObjs();
-	}
+	retrieveVisitableObjs();
 
 
 #if NKAI_TRACE_LEVEL == 0
 #if NKAI_TRACE_LEVEL == 0
 	try
 	try

+ 18 - 6
AI/Nullkiller/Analyzers/ArmyManager.cpp

@@ -238,7 +238,8 @@ std::shared_ptr<CCreatureSet> ArmyManager::getArmyAvailableToBuyAsCCreatureSet(
 ui64 ArmyManager::howManyReinforcementsCanBuy(
 ui64 ArmyManager::howManyReinforcementsCanBuy(
 	const CCreatureSet * targetArmy,
 	const CCreatureSet * targetArmy,
 	const CGDwelling * dwelling,
 	const CGDwelling * dwelling,
-	const TResources & availableResources) const
+	const TResources & availableResources,
+	uint8_t turn) const
 {
 {
 	ui64 aivalue = 0;
 	ui64 aivalue = 0;
 	auto army = getArmyAvailableToBuy(targetArmy, dwelling, availableResources);
 	auto army = getArmyAvailableToBuy(targetArmy, dwelling, availableResources);
@@ -259,17 +260,29 @@ std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(const CCreatureSet * her
 std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
 std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
 	const CCreatureSet * hero,
 	const CCreatureSet * hero,
 	const CGDwelling * dwelling,
 	const CGDwelling * dwelling,
-	TResources availableRes) const
+	TResources availableRes,
+	uint8_t turn) const
 {
 {
 	std::vector<creInfo> creaturesInDwellings;
 	std::vector<creInfo> creaturesInDwellings;
 	int freeHeroSlots = GameConstants::ARMY_SIZE - hero->stacksCount();
 	int freeHeroSlots = GameConstants::ARMY_SIZE - hero->stacksCount();
+	bool countGrowth = (cb->getDate(Date::DAY_OF_WEEK) + turn) > 7;
+
+	const CGTownInstance * town = dwelling->ID == CGTownInstance::TOWN
+		? dynamic_cast<const CGTownInstance *>(dwelling)
+		: nullptr;
 
 
 	for(int i = dwelling->creatures.size() - 1; i >= 0; i--)
 	for(int i = dwelling->creatures.size() - 1; i >= 0; i--)
 	{
 	{
 		auto ci = infoFromDC(dwelling->creatures[i]);
 		auto ci = infoFromDC(dwelling->creatures[i]);
 
 
-		if(!ci.count || ci.creID == -1)
-			continue;
+		if(ci.creID == -1) continue;
+
+		if(i < GameConstants::CREATURES_PER_TOWN && countGrowth)
+		{
+			ci.count += town ? town->creatureGrowth(i) : ci.cre->getGrowth();
+		}
+
+		if(!ci.count) continue;
 
 
 		SlotID dst = hero->getSlotFor(ci.creID);
 		SlotID dst = hero->getSlotFor(ci.creID);
 		if(!hero->hasStackAtSlot(dst)) //need another new slot for this stack
 		if(!hero->hasStackAtSlot(dst)) //need another new slot for this stack
@@ -282,8 +295,7 @@ std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
 
 
 		vstd::amin(ci.count, availableRes / ci.cre->getFullRecruitCost()); //max count we can afford
 		vstd::amin(ci.count, availableRes / ci.cre->getFullRecruitCost()); //max count we can afford
 
 
-		if(!ci.count)
-			continue;
+		if(!ci.count) continue;
 
 
 		ci.level = i; //this is important for Dungeon Summoning Portal
 		ci.level = i; //this is important for Dungeon Summoning Portal
 		creaturesInDwellings.push_back(ci);
 		creaturesInDwellings.push_back(ci);

+ 27 - 6
AI/Nullkiller/Analyzers/ArmyManager.h

@@ -45,20 +45,32 @@ public:
 	virtual	ui64 howManyReinforcementsCanBuy(
 	virtual	ui64 howManyReinforcementsCanBuy(
 		const CCreatureSet * targetArmy,
 		const CCreatureSet * targetArmy,
 		const CGDwelling * dwelling,
 		const CGDwelling * dwelling,
-		const TResources & availableResources) const = 0;
+		const TResources & availableResources,
+		uint8_t turn = 0) const = 0;
+
 	virtual ui64 howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const = 0;
 	virtual ui64 howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const = 0;
-	virtual ui64 howManyReinforcementsCanGet(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const = 0;
+	virtual ui64 howManyReinforcementsCanGet(
+		const IBonusBearer * armyCarrier,
+		const CCreatureSet * target,
+		const CCreatureSet * source) const = 0;
+
 	virtual std::vector<SlotInfo> getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const = 0;
 	virtual std::vector<SlotInfo> getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const = 0;
 	virtual std::vector<SlotInfo>::iterator getWeakestCreature(std::vector<SlotInfo> & army) const = 0;
 	virtual std::vector<SlotInfo>::iterator getWeakestCreature(std::vector<SlotInfo> & army) const = 0;
 	virtual std::vector<SlotInfo> getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const = 0;
 	virtual std::vector<SlotInfo> getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const = 0;
-	virtual std::vector<creInfo> getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling, TResources availableRes) const = 0;
+
+	virtual std::vector<creInfo> getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const = 0;
+	virtual std::vector<creInfo> getArmyAvailableToBuy(
+		const CCreatureSet * hero,
+		const CGDwelling * dwelling,
+		TResources availableRes,
+		uint8_t turn = 0) const = 0;
+
 	virtual uint64_t evaluateStackPower(const CCreature * creature, int count) const = 0;
 	virtual uint64_t evaluateStackPower(const CCreature * creature, int count) const = 0;
 	virtual SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const = 0;
 	virtual SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const = 0;
 	virtual ArmyUpgradeInfo calculateCreaturesUpgrade(
 	virtual ArmyUpgradeInfo calculateCreaturesUpgrade(
 		const CCreatureSet * army,
 		const CCreatureSet * army,
 		const CGObjectInstance * upgrader,
 		const CGObjectInstance * upgrader,
 		const TResources & availableResources) const = 0;
 		const TResources & availableResources) const = 0;
-	virtual std::vector<creInfo> getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const = 0;
 	virtual std::shared_ptr<CCreatureSet> getArmyAvailableToBuyAsCCreatureSet(const CGDwelling * dwelling, TResources availableRes) const = 0;
 	virtual std::shared_ptr<CCreatureSet> getArmyAvailableToBuyAsCCreatureSet(const CGDwelling * dwelling, TResources availableRes) const = 0;
 };
 };
 
 
@@ -74,18 +86,27 @@ private:
 public:
 public:
 	ArmyManager(CPlayerSpecificInfoCallback * CB, const Nullkiller * ai): cb(CB), ai(ai) {}
 	ArmyManager(CPlayerSpecificInfoCallback * CB, const Nullkiller * ai): cb(CB), ai(ai) {}
 	void update() override;
 	void update() override;
+
 	ui64 howManyReinforcementsCanBuy(const CCreatureSet * target, const CGDwelling * source) const override;
 	ui64 howManyReinforcementsCanBuy(const CCreatureSet * target, const CGDwelling * source) const override;
 	ui64 howManyReinforcementsCanBuy(
 	ui64 howManyReinforcementsCanBuy(
 		const CCreatureSet * targetArmy,
 		const CCreatureSet * targetArmy,
 		const CGDwelling * dwelling,
 		const CGDwelling * dwelling,
-		const TResources & availableResources) const override;
+		const TResources & availableResources,
+		uint8_t turn = 0) const override;
+
 	ui64 howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const override;
 	ui64 howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const override;
 	ui64 howManyReinforcementsCanGet(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const override;
 	ui64 howManyReinforcementsCanGet(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const override;
 	std::vector<SlotInfo> getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const override;
 	std::vector<SlotInfo> getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const override;
 	std::vector<SlotInfo>::iterator getWeakestCreature(std::vector<SlotInfo> & army) const override;
 	std::vector<SlotInfo>::iterator getWeakestCreature(std::vector<SlotInfo> & army) const override;
 	std::vector<SlotInfo> getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const override;
 	std::vector<SlotInfo> getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const override;
-	std::vector<creInfo> getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling, TResources availableRes) const override;
+
 	std::vector<creInfo> getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const override;
 	std::vector<creInfo> getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const override;
+	std::vector<creInfo> getArmyAvailableToBuy(
+		const CCreatureSet * hero,
+		const CGDwelling * dwelling,
+		TResources availableRes,
+		uint8_t turn = 0) const override;
+
 	std::shared_ptr<CCreatureSet> getArmyAvailableToBuyAsCCreatureSet(const CGDwelling * dwelling, TResources availableRes) const override;
 	std::shared_ptr<CCreatureSet> getArmyAvailableToBuyAsCCreatureSet(const CGDwelling * dwelling, TResources availableRes) const override;
 	uint64_t evaluateStackPower(const CCreature * creature, int count) const override;
 	uint64_t evaluateStackPower(const CCreature * creature, int count) const override;
 	SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const override;
 	SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const override;

+ 95 - 5
AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp

@@ -19,12 +19,12 @@ HitMapInfo HitMapInfo::NoTreat;
 
 
 void DangerHitMapAnalyzer::updateHitMap()
 void DangerHitMapAnalyzer::updateHitMap()
 {
 {
-	if(upToDate)
+	if(hitMapUpToDate)
 		return;
 		return;
 
 
 	logAi->trace("Update danger hitmap");
 	logAi->trace("Update danger hitmap");
 
 
-	upToDate = true;
+	hitMapUpToDate = true;
 	auto start = std::chrono::high_resolution_clock::now();
 	auto start = std::chrono::high_resolution_clock::now();
 
 
 	auto cb = ai->cb.get();
 	auto cb = ai->cb.get();
@@ -71,8 +71,10 @@ void DangerHitMapAnalyzer::updateHitMap()
 				auto turn = path.turn();
 				auto turn = path.turn();
 				auto & node = hitMap[pos.x][pos.y][pos.z];
 				auto & node = hitMap[pos.x][pos.y][pos.z];
 
 
-				if(tileDanger / (turn / 3 + 1) > node.maximumDanger.danger / (node.maximumDanger.turn / 3 + 1)
-					|| (tileDanger == node.maximumDanger.danger && node.maximumDanger.turn > turn))
+				auto newMaxDanger = tileDanger / std::sqrt(turn / 3.0f + 1);
+				auto currentMaxDanger = node.maximumDanger.danger / std::sqrt(node.maximumDanger.turn / 3.0f + 1);
+
+				if(newMaxDanger > currentMaxDanger)
 				{
 				{
 					node.maximumDanger.danger = tileDanger;
 					node.maximumDanger.danger = tileDanger;
 					node.maximumDanger.turn = turn;
 					node.maximumDanger.turn = turn;
@@ -104,6 +106,94 @@ void DangerHitMapAnalyzer::updateHitMap()
 	logAi->trace("Danger hit map updated in %ld", timeElapsed(start));
 	logAi->trace("Danger hit map updated in %ld", timeElapsed(start));
 }
 }
 
 
+void DangerHitMapAnalyzer::calculateTileOwners()
+{
+	if(tileOwnersUpToDate) return;
+
+	tileOwnersUpToDate = true;
+
+	auto cb = ai->cb.get();
+	auto mapSize = ai->cb->getMapSize();
+
+	tileOwners.resize(boost::extents[mapSize.x][mapSize.y][mapSize.z]);
+
+	std::map<const CGHeroInstance *, HeroRole> townHeroes;
+	std::map<const CGHeroInstance *, const CGTownInstance *> heroTownMap;
+	PathfinderSettings pathfinderSettings;
+
+	pathfinderSettings.mainTurnDistanceLimit = 3;
+
+	auto addTownHero = [&](const CGTownInstance * town)
+	{
+			auto townHero = new CGHeroInstance();
+			CRandomGenerator rng;
+			
+			townHero->pos = town->pos;
+			townHero->setOwner(ai->playerID); // lets avoid having multiple colors
+			townHero->initHero(rng, static_cast<HeroTypeID>(0));
+			townHero->initObj(rng);
+			
+			heroTownMap[townHero] = town;
+			townHeroes[townHero] = HeroRole::MAIN;
+	};
+
+	for(auto obj : ai->memory->visitableObjs)
+	{
+		if(obj && obj->ID == Obj::TOWN)
+		{
+			addTownHero(dynamic_cast<const CGTownInstance *>(obj));
+		}
+	}
+
+	for(auto town : cb->getTownsInfo())
+	{
+		addTownHero(town);
+	}
+
+	ai->pathfinder->updatePaths(townHeroes, PathfinderSettings());
+
+	pforeachTilePos(mapSize, [&](const int3 & pos)
+		{
+			float ourDistance = std::numeric_limits<float>::max();
+			float enemyDistance = std::numeric_limits<float>::max();
+			const CGTownInstance * enemyTown = nullptr;
+
+			for(AIPath & path : ai->pathfinder->getPathInfo(pos))
+			{
+				if(!path.targetHero || path.getFirstBlockedAction())
+					continue;
+
+				auto town = heroTownMap[path.targetHero];
+
+				if(town->getOwner() == ai->playerID)
+				{
+					vstd::amin(ourDistance, path.movementCost());
+				}
+				else
+				{
+					if(enemyDistance > path.movementCost())
+					{
+						enemyDistance = path.movementCost();
+						enemyTown = town;
+					}
+				}
+			}
+
+			if(ourDistance == enemyDistance)
+			{
+				tileOwners[pos.x][pos.y][pos.z] = PlayerColor::NEUTRAL;
+			}
+			else if(!enemyTown || ourDistance < enemyDistance)
+			{
+				tileOwners[pos.x][pos.y][pos.z] = ai->playerID;
+			}
+			else
+			{
+				tileOwners[pos.x][pos.y][pos.z] = enemyTown->getOwner();
+			}
+		});
+}
+
 uint64_t DangerHitMapAnalyzer::enemyCanKillOurHeroesAlongThePath(const AIPath & path) const
 uint64_t DangerHitMapAnalyzer::enemyCanKillOurHeroesAlongThePath(const AIPath & path) const
 {
 {
 	int3 tile = path.targetTile();
 	int3 tile = path.targetTile();
@@ -144,7 +234,7 @@ const std::set<const CGObjectInstance *> & DangerHitMapAnalyzer::getOneTurnAcces
 
 
 void DangerHitMapAnalyzer::reset()
 void DangerHitMapAnalyzer::reset()
 {
 {
-	upToDate = false;
+	hitMapUpToDate = false;
 }
 }
 
 
 }
 }

+ 5 - 1
AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h

@@ -55,19 +55,23 @@ class DangerHitMapAnalyzer
 {
 {
 private:
 private:
 	boost::multi_array<HitMapNode, 3> hitMap;
 	boost::multi_array<HitMapNode, 3> hitMap;
+	boost::multi_array<PlayerColor, 3> tileOwners;
 	std::map<const CGHeroInstance *, std::set<const CGObjectInstance *>> enemyHeroAccessibleObjects;
 	std::map<const CGHeroInstance *, std::set<const CGObjectInstance *>> enemyHeroAccessibleObjects;
-	bool upToDate;
+	bool hitMapUpToDate = false;
+	bool tileOwnersUpToDate = false;
 	const Nullkiller * ai;
 	const Nullkiller * ai;
 
 
 public:
 public:
 	DangerHitMapAnalyzer(const Nullkiller * ai) :ai(ai) {}
 	DangerHitMapAnalyzer(const Nullkiller * ai) :ai(ai) {}
 
 
 	void updateHitMap();
 	void updateHitMap();
+	void calculateTileOwners();
 	uint64_t enemyCanKillOurHeroesAlongThePath(const AIPath & path) const;
 	uint64_t enemyCanKillOurHeroesAlongThePath(const AIPath & path) const;
 	const HitMapNode & getObjectTreat(const CGObjectInstance * obj) const;
 	const HitMapNode & getObjectTreat(const CGObjectInstance * obj) const;
 	const HitMapNode & getTileTreat(const int3 & tile) const;
 	const HitMapNode & getTileTreat(const int3 & tile) const;
 	const std::set<const CGObjectInstance *> & getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const;
 	const std::set<const CGObjectInstance *> & getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const;
 	void reset();
 	void reset();
+	void resetTileOwners() { tileOwnersUpToDate = false; }
 };
 };
 
 
 }
 }

+ 10 - 7
AI/Nullkiller/Analyzers/HeroManager.cpp

@@ -180,6 +180,15 @@ float HeroManager::evaluateHero(const CGHeroInstance * hero) const
 	return evaluateFightingStrength(hero);
 	return evaluateFightingStrength(hero);
 }
 }
 
 
+bool HeroManager::heroCapReached() const
+{
+	const bool includeGarnisoned = true;
+	int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned);
+
+	return heroCount >= ALLOWED_ROAMING_HEROES
+		|| heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP);
+}
+
 bool HeroManager::canRecruitHero(const CGTownInstance * town) const
 bool HeroManager::canRecruitHero(const CGTownInstance * town) const
 {
 {
 	if(!town)
 	if(!town)
@@ -191,13 +200,7 @@ bool HeroManager::canRecruitHero(const CGTownInstance * town) const
 	if(cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST)
 	if(cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST)
 		return false;
 		return false;
 
 
-	const bool includeGarnisoned = true;
-	int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned);
-
-	if(heroCount >= ALLOWED_ROAMING_HEROES)
-		return false;
-
-	if(heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP))
+	if(heroCapReached())
 		return false;
 		return false;
 
 
 	if(!cb->getAvailableHeroes(town).size())
 	if(!cb->getAvailableHeroes(town).size())

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

@@ -31,6 +31,7 @@ public:
 	virtual float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const = 0;
 	virtual float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const = 0;
 	virtual float evaluateHero(const CGHeroInstance * hero) const = 0;
 	virtual float evaluateHero(const CGHeroInstance * hero) const = 0;
 	virtual bool canRecruitHero(const CGTownInstance * t = nullptr) const = 0;
 	virtual bool canRecruitHero(const CGTownInstance * t = nullptr) const = 0;
+	virtual bool heroCapReached() const = 0;
 	virtual const CGHeroInstance * findHeroWithGrail() const = 0;
 	virtual const CGHeroInstance * findHeroWithGrail() const = 0;
 };
 };
 
 
@@ -71,6 +72,7 @@ public:
 	float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const override;
 	float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const override;
 	float evaluateHero(const CGHeroInstance * hero) const override;
 	float evaluateHero(const CGHeroInstance * hero) const override;
 	bool canRecruitHero(const CGTownInstance * t = nullptr) const override;
 	bool canRecruitHero(const CGTownInstance * t = nullptr) const override;
+	bool heroCapReached() const override;
 	const CGHeroInstance * findHeroWithGrail() const override;
 	const CGHeroInstance * findHeroWithGrail() const override;
 
 
 private:
 private:

+ 9 - 5
AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp

@@ -56,7 +56,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
 
 
 	tasks.reserve(paths.size());
 	tasks.reserve(paths.size());
 
 
-	const AIPath * closestWay = nullptr;
+	std::unordered_map<HeroRole, const AIPath *> closestWaysByRole;
 	std::vector<ExecuteHeroChain *> waysToVisitObj;
 	std::vector<ExecuteHeroChain *> waysToVisitObj;
 
 
 	for(auto & path : paths)
 	for(auto & path : paths)
@@ -128,8 +128,9 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
 
 
 			auto heroRole = ai->nullkiller->heroManager->getHeroRole(path.targetHero);
 			auto heroRole = ai->nullkiller->heroManager->getHeroRole(path.targetHero);
 
 
-			if(heroRole == HeroRole::SCOUT
-				&& (!closestWay || closestWay->movementCost() > path.movementCost()))
+			auto & closestWay = closestWaysByRole[heroRole];
+
+			if(!closestWay || closestWay->movementCost() > path.movementCost())
 			{
 			{
 				closestWay = &path;
 				closestWay = &path;
 			}
 			}
@@ -142,9 +143,12 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
 		}
 		}
 	}
 	}
 
 
-	if(closestWay)
+	for(auto way : waysToVisitObj)
 	{
 	{
-		for(auto way : waysToVisitObj)
+		auto heroRole = ai->nullkiller->heroManager->getHeroRole(way->getPath().targetHero);
+		auto closestWay = closestWaysByRole[heroRole];
+
+		if(closestWay)
 		{
 		{
 			way->closestWayRatio
 			way->closestWayRatio
 				= closestWay->movementCost() / way->getPath().movementCost();
 				= closestWay->movementCost() / way->getPath().movementCost();

+ 155 - 76
AI/Nullkiller/Behaviors/DefenceBehavior.cpp

@@ -60,27 +60,27 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 
 
 	if(town->garrisonHero)
 	if(town->garrisonHero)
 	{
 	{
-		if(!ai->nullkiller->isHeroLocked(town->garrisonHero.get()))
+		if(ai->nullkiller->isHeroLocked(town->garrisonHero.get()))
 		{
 		{
-			if(!town->visitingHero && cb->getHeroCount(ai->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER)
-			{
-				logAi->trace(
-					"Extracting hero %s from garrison of town %s",
-					town->garrisonHero->getNameTranslated(),
-					town->getNameTranslated());
-
-				tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5)));
+			logAi->trace(
+				"Hero %s in garrison of town %s is suposed to defend the town",
+				town->garrisonHero->getNameTranslated(),
+				town->getNameTranslated());
 
 
-				return;
-			}
+			return;
 		}
 		}
 
 
-		logAi->trace(
-			"Hero %s in garrison of town %s is suposed to defend the town",
-			town->garrisonHero->getNameTranslated(),
-			town->getNameTranslated());
+		if(!town->visitingHero && cb->getHeroCount(ai->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER)
+		{
+			logAi->trace(
+				"Extracting hero %s from garrison of town %s",
+				town->garrisonHero->getNameTranslated(),
+				town->getNameTranslated());
 
 
-		return;
+			tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5)));
+
+			return;
+		}
 	}
 	}
 
 
 	if(!treatNode.fastestDanger.hero)
 	if(!treatNode.fastestDanger.hero)
@@ -113,11 +113,21 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 
 
 		for(AIPath & path : paths)
 		for(AIPath & path : paths)
 		{
 		{
-			if(town->visitingHero && path.targetHero != town->visitingHero.get())
-				continue;
-
-			if(town->visitingHero && path.getHeroStrength() < town->visitingHero->getHeroStrength())
-				continue;
+			if(town->visitingHero && path.targetHero == town->visitingHero.get())
+			{
+				if(path.getHeroStrength() < town->visitingHero->getHeroStrength())
+					continue;
+			}
+			else if(town->garrisonHero && path.targetHero == town->garrisonHero.get())
+			{
+				if(path.getHeroStrength() < town->visitingHero->getHeroStrength())
+					continue;
+			}
+			else
+			{
+				if(town->visitingHero)
+					continue;
+			}
 
 
 			if(treat.hero.validAndSet()
 			if(treat.hero.validAndSet()
 				&& treat.turn <= 1
 				&& treat.turn <= 1
@@ -158,53 +168,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 		if(treatIsUnderControl)
 		if(treatIsUnderControl)
 			continue;
 			continue;
 
 
-		if(!town->visitingHero
-			&& town->hasBuilt(BuildingID::TAVERN)
-			&& cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST)
-		{
-			auto heroesInTavern = cb->getAvailableHeroes(town);
-
-			for(auto hero : heroesInTavern)
-			{
-				if(hero->getTotalStrength() > treat.danger)
-				{
-					auto myHeroes = cb->getHeroesInfo();
-
-					if(cb->getHeroesInfo().size() < ALLOWED_ROAMING_HEROES)
-					{
-#if NKAI_TRACE_LEVEL >= 1
-						logAi->trace("Hero %s can be recruited to defend %s", hero->getObjectName(), town->getObjectName());
-#endif
-						tasks.push_back(Goals::sptr(Goals::RecruitHero(town, hero).setpriority(1)));
-						continue;
-					}
-					else
-					{
-						const CGHeroInstance * weakestHero = nullptr;
-
-						for(auto existingHero : myHeroes)
-						{
-							if(ai->nullkiller->isHeroLocked(existingHero)
-								|| existingHero->getArmyStrength() > hero->getArmyStrength()
-								|| ai->nullkiller->heroManager->getHeroRole(existingHero) == HeroRole::MAIN
-								|| existingHero->movementPointsRemaining()
-								|| existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1))
-								continue;
-
-							if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength())
-							{
-								weakestHero = existingHero;
-							}
-
-							if(weakestHero)
-							{
-								tasks.push_back(Goals::sptr(Goals::DismissHero(weakestHero)));
-							}
-						}
-					}
-				}
-			}
-		}
+		evaluateRecruitingHero(tasks, treat, town);
 
 
 		if(paths.empty())
 		if(paths.empty())
 		{
 		{
@@ -275,9 +239,11 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 				tasks.push_back(
 				tasks.push_back(
 					Goals::sptr(Composition()
 					Goals::sptr(Composition()
 						.addNext(DefendTown(town, treat, path))
 						.addNext(DefendTown(town, treat, path))
-						.addNext(ExchangeSwapTownHeroes(town, town->visitingHero.get()))
-						.addNext(ExecuteHeroChain(path, town))
-						.addNext(ExchangeSwapTownHeroes(town, path.targetHero, HeroLockedReason::DEFENCE))));
+						.addNextSequence({
+								sptr(ExchangeSwapTownHeroes(town, town->visitingHero.get())),
+								sptr(ExecuteHeroChain(path, town)),
+								sptr(ExchangeSwapTownHeroes(town, path.targetHero, HeroLockedReason::DEFENCE))
+							})));
 
 
 				continue;
 				continue;
 			}
 			}
@@ -313,15 +279,45 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 					continue;
 					continue;
 				}
 				}
 			}
 			}
+			Composition composition;
 
 
+			composition.addNext(DefendTown(town, treat, path));
+			TGoalVec sequence;
+
+			if(town->visitingHero && path.targetHero != town->visitingHero && !path.containsHero(town->visitingHero))
+			{
+				if(town->garrisonHero)
+				{
+					if(ai->nullkiller->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
 #if NKAI_TRACE_LEVEL >= 1
-			logAi->trace("Move %s to defend town %s",
-				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
 #endif
-			Composition composition;
+						continue;
+					}
+				}
+				else if(path.turn() == 0)
+				{
+					sequence.push_back(sptr(ExchangeSwapTownHeroes(town, town->visitingHero.get())));
+				}
+			}
 
 
-			composition.addNext(DefendTown(town, treat, path)).addNext(ExecuteHeroChain(path, town));
+#if NKAI_TRACE_LEVEL >= 1
+				logAi->trace("Move %s to defend town %s",
+					path.targetHero->getObjectName(),
+					town->getObjectName());
+#endif
+
+			sequence.push_back(sptr(ExecuteHeroChain(path, town)));
+			composition.addNextSequence(sequence);
 
 
 			auto firstBlockedAction = path.getFirstBlockedAction();
 			auto firstBlockedAction = path.getFirstBlockedAction();
 			if(firstBlockedAction)
 			if(firstBlockedAction)
@@ -350,4 +346,87 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 	logAi->debug("Found %d tasks", tasks.size());
 	logAi->debug("Found %d tasks", tasks.size());
 }
 }
 
 
+void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & treat, const CGTownInstance * town) const
+{
+	if(town->hasBuilt(BuildingID::TAVERN)
+		&& cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST)
+	{
+		auto heroesInTavern = cb->getAvailableHeroes(town);
+
+		for(auto hero : heroesInTavern)
+		{
+			if(hero->getTotalStrength() < treat.danger)
+				continue;
+
+			auto myHeroes = cb->getHeroesInfo();
+
+#if NKAI_TRACE_LEVEL >= 1
+			logAi->trace("Hero %s can be recruited to defend %s", hero->getObjectName(), town->getObjectName());
+#endif
+			bool needSwap = false;
+			const CGHeroInstance * heroToDismiss = nullptr;
+
+			if(town->visitingHero)
+			{
+				if(!town->garrisonHero)
+					needSwap = true;
+				else
+				{
+					if(town->visitingHero->getArmyStrength() < town->garrisonHero->getArmyStrength())
+					{
+						if(town->visitingHero->getArmyStrength() >= hero->getArmyStrength())
+							continue;
+
+						heroToDismiss = town->visitingHero.get();
+					}
+					else if(town->garrisonHero->getArmyStrength() >= hero->getArmyStrength())
+						continue;
+					else
+					{
+						needSwap = true;
+						heroToDismiss = town->garrisonHero.get();
+					}
+				}
+			}
+			else if(ai->nullkiller->heroManager->heroCapReached())
+			{
+				const CGHeroInstance * weakestHero = nullptr;
+
+				for(auto existingHero : myHeroes)
+				{
+					if(ai->nullkiller->isHeroLocked(existingHero)
+						|| existingHero->getArmyStrength() > hero->getArmyStrength()
+						|| ai->nullkiller->heroManager->getHeroRole(existingHero) == HeroRole::MAIN
+						|| existingHero->movementPointsRemaining()
+						|| existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1))
+						continue;
+
+					if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength())
+					{
+						weakestHero = existingHero;
+					}
+				}
+
+				if(!weakestHero)
+					continue;
+				
+				heroToDismiss = weakestHero;
+			}
+
+			TGoalVec sequence;
+			Goals::Composition recruitHeroComposition;
+
+			if(needSwap)
+				sequence.push_back(sptr(ExchangeSwapTownHeroes(town, town->visitingHero.get())));
+
+			if(heroToDismiss)
+				sequence.push_back(sptr(DismissHero(heroToDismiss)));
+
+			sequence.push_back(sptr(Goals::RecruitHero(town, hero)));
+
+			tasks.push_back(sptr(Goals::Composition().addNext(DefendTown(town, treat, hero)).addNextSequence(sequence)));
+		}
+	}
+}
+
 }
 }

+ 5 - 0
AI/Nullkiller/Behaviors/DefenceBehavior.h

@@ -15,8 +15,12 @@
 
 
 namespace NKAI
 namespace NKAI
 {
 {
+
+struct HitMapInfo;
+
 namespace Goals
 namespace Goals
 {
 {
+
 	class DefenceBehavior : public CGoal<DefenceBehavior>
 	class DefenceBehavior : public CGoal<DefenceBehavior>
 	{
 	{
 	public:
 	public:
@@ -35,6 +39,7 @@ namespace Goals
 
 
 	private:
 	private:
 		void evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town) const;
 		void evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town) const;
+		void evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & treat, const CGTownInstance * town) const;
 	};
 	};
 }
 }
 
 

+ 37 - 8
AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp

@@ -16,6 +16,7 @@
 #include "../Markers/ArmyUpgrade.h"
 #include "../Markers/ArmyUpgrade.h"
 #include "GatherArmyBehavior.h"
 #include "GatherArmyBehavior.h"
 #include "../AIUtility.h"
 #include "../AIUtility.h"
+#include "../Goals/ExchangeSwapTownHeroes.h"
 
 
 namespace NKAI
 namespace NKAI
 {
 {
@@ -78,20 +79,27 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
 	for(const AIPath & path : paths)
 	for(const AIPath & path : paths)
 	{
 	{
 #if NKAI_TRACE_LEVEL >= 2
 #if NKAI_TRACE_LEVEL >= 2
-		logAi->trace("Path found %s", path.toString());
+		logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength());
 #endif
 #endif
 		
 		
-		if(path.containsHero(hero)) continue;
+		if(path.containsHero(hero))
+		{
+#if NKAI_TRACE_LEVEL >= 2
+			logAi->trace("Selfcontaining path. Ignore");
+#endif
+			continue;
+		}
+
+		bool garrisoned = false;
 
 
 		if(path.turn() == 0 && hero->inTownGarrison)
 		if(path.turn() == 0 && hero->inTownGarrison)
 		{
 		{
 #if NKAI_TRACE_LEVEL >= 1
 #if NKAI_TRACE_LEVEL >= 1
-			logAi->trace("Skipping garnisoned hero %s, %s", hero->getObjectName(), pos.toString());
+			garrisoned = true;
 #endif
 #endif
-			continue;
 		}
 		}
 
 
-		if(ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
+		if(path.turn() > 0 && ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
 		{
 		{
 #if NKAI_TRACE_LEVEL >= 2
 #if NKAI_TRACE_LEVEL >= 2
 			logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength());
 			logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength());
@@ -172,7 +180,21 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
 			exchangePath.closestWayRatio = 1;
 			exchangePath.closestWayRatio = 1;
 
 
 			composition.addNext(heroExchange);
 			composition.addNext(heroExchange);
-			composition.addNext(exchangePath);
+
+			if(garrisoned && path.turn() == 0)
+			{
+				auto lockReason = ai->nullkiller->getHeroLockedReason(hero);
+
+				composition.addNextSequence({
+					sptr(ExchangeSwapTownHeroes(hero->visitedTown)),
+					sptr(exchangePath),
+					sptr(ExchangeSwapTownHeroes(hero->visitedTown, hero, lockReason))
+				});
+			}
+			else
+			{
+				composition.addNext(exchangePath);
+			}
 
 
 			auto blockedAction = path.getFirstBlockedAction();
 			auto blockedAction = path.getFirstBlockedAction();
 
 
@@ -221,7 +243,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
 	for(const AIPath & path : paths)
 	for(const AIPath & path : paths)
 	{
 	{
 #if NKAI_TRACE_LEVEL >= 2
 #if NKAI_TRACE_LEVEL >= 2
-		logAi->trace("Path found %s", path.toString());
+		logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength());
 #endif
 #endif
 		if(upgrader->visitingHero && upgrader->visitingHero.get() != path.targetHero)
 		if(upgrader->visitingHero && upgrader->visitingHero.get() != path.targetHero)
 		{
 		{
@@ -267,7 +289,14 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
 				ai->nullkiller->armyManager->howManyReinforcementsCanGet(
 				ai->nullkiller->armyManager->howManyReinforcementsCanGet(
 					path.targetHero,
 					path.targetHero,
 					path.heroArmy,
 					path.heroArmy,
-					upgrader->getUpperArmy());	
+					upgrader->getUpperArmy());
+
+			upgrade.upgradeValue +=
+				ai->nullkiller->armyManager->howManyReinforcementsCanBuy(
+					path.heroArmy,
+					upgrader,
+					ai->nullkiller->getFreeResources(),
+					path.turn());
 		}
 		}
 
 
 		auto armyValue = (float)upgrade.upgradeValue / path.getHeroStrength();
 		auto armyValue = (float)upgrade.upgradeValue / path.getHeroStrength();

+ 2 - 0
AI/Nullkiller/CMakeLists.txt

@@ -52,6 +52,7 @@ set(Nullkiller_SRCS
 		Behaviors/BuildingBehavior.cpp
 		Behaviors/BuildingBehavior.cpp
 		Behaviors/GatherArmyBehavior.cpp
 		Behaviors/GatherArmyBehavior.cpp
 		Behaviors/ClusterBehavior.cpp
 		Behaviors/ClusterBehavior.cpp
+		Helpers/ArmyFormation.cpp
 		AIGateway.cpp
 		AIGateway.cpp
 )
 )
 
 
@@ -114,6 +115,7 @@ set(Nullkiller_HEADERS
 		Behaviors/BuildingBehavior.h
 		Behaviors/BuildingBehavior.h
 		Behaviors/GatherArmyBehavior.h
 		Behaviors/GatherArmyBehavior.h
 		Behaviors/ClusterBehavior.h
 		Behaviors/ClusterBehavior.h
+		Helpers/ArmyFormation.h
 		AIGateway.h
 		AIGateway.h
 )
 )
 
 

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

@@ -61,6 +61,7 @@ void Nullkiller::init(std::shared_ptr<CCallback> cb, PlayerColor playerID)
 	armyManager.reset(new ArmyManager(cb.get(), this));
 	armyManager.reset(new ArmyManager(cb.get(), this));
 	heroManager.reset(new HeroManager(cb.get(), this));
 	heroManager.reset(new HeroManager(cb.get(), this));
 	decomposer.reset(new DeepDecomposer());
 	decomposer.reset(new DeepDecomposer());
+	armyFormation.reset(new ArmyFormation(cb, this));
 }
 }
 
 
 Goals::TTask Nullkiller::choseBestTask(Goals::TTaskVec & tasks) const
 Goals::TTask Nullkiller::choseBestTask(Goals::TTaskVec & tasks) const
@@ -137,6 +138,7 @@ void Nullkiller::updateAiState(int pass, bool fast)
 	{
 	{
 		memory->removeInvisibleObjects(cb.get());
 		memory->removeInvisibleObjects(cb.get());
 
 
+		dangerHitMap->calculateTileOwners();
 		dangerHitMap->updateHitMap();
 		dangerHitMap->updateHitMap();
 
 
 		boost::this_thread::interruption_point();
 		boost::this_thread::interruption_point();
@@ -222,7 +224,7 @@ void Nullkiller::makeTurn()
 	boost::lock_guard<boost::mutex> sharedStorageLock(AISharedStorage::locker);
 	boost::lock_guard<boost::mutex> sharedStorageLock(AISharedStorage::locker);
 
 
 	const int MAX_DEPTH = 10;
 	const int MAX_DEPTH = 10;
-	const float FAST_TASK_MINIMAL_PRIORITY = 0.7;
+	const float FAST_TASK_MINIMAL_PRIORITY = 0.7f;
 
 
 	resetAiState();
 	resetAiState();
 
 

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

@@ -18,6 +18,7 @@
 #include "../Analyzers/ArmyManager.h"
 #include "../Analyzers/ArmyManager.h"
 #include "../Analyzers/HeroManager.h"
 #include "../Analyzers/HeroManager.h"
 #include "../Analyzers/ObjectClusterizer.h"
 #include "../Analyzers/ObjectClusterizer.h"
+#include "../Helpers/ArmyFormation.h"
 
 
 namespace NKAI
 namespace NKAI
 {
 {
@@ -67,6 +68,7 @@ public:
 	std::unique_ptr<AIMemory> memory;
 	std::unique_ptr<AIMemory> memory;
 	std::unique_ptr<FuzzyHelper> dangerEvaluator;
 	std::unique_ptr<FuzzyHelper> dangerEvaluator;
 	std::unique_ptr<DeepDecomposer> decomposer;
 	std::unique_ptr<DeepDecomposer> decomposer;
+	std::unique_ptr<ArmyFormation> armyFormation;
 	PlayerColor playerID;
 	PlayerColor playerID;
 	std::shared_ptr<CCallback> cb;
 	std::shared_ptr<CCallback> cb;
 
 

+ 55 - 16
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -23,6 +23,7 @@
 #include "../Goals/ExecuteHeroChain.h"
 #include "../Goals/ExecuteHeroChain.h"
 #include "../Goals/BuildThis.h"
 #include "../Goals/BuildThis.h"
 #include "../Goals/ExchangeSwapTownHeroes.h"
 #include "../Goals/ExchangeSwapTownHeroes.h"
+#include "../Goals/DismissHero.h"
 #include "../Markers/UnlockCluster.h"
 #include "../Markers/UnlockCluster.h"
 #include "../Markers/HeroExchange.h"
 #include "../Markers/HeroExchange.h"
 #include "../Markers/ArmyUpgrade.h"
 #include "../Markers/ArmyUpgrade.h"
@@ -33,6 +34,7 @@ namespace NKAI
 
 
 #define MIN_AI_STRENGHT (0.5f) //lower when combat AI gets smarter
 #define MIN_AI_STRENGHT (0.5f) //lower when combat AI gets smarter
 #define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us
 #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),
 	: movementCost(0.0),
@@ -54,6 +56,11 @@ EvaluationContext::EvaluationContext(const Nullkiller * ai)
 {
 {
 }
 }
 
 
+void EvaluationContext::addNonCriticalStrategicalValue(float value)
+{
+	vstd::amax(strategicalValue, std::min(value, MIN_CRITICAL_VALUE));
+}
+
 PriorityEvaluator::~PriorityEvaluator()
 PriorityEvaluator::~PriorityEvaluator()
 {
 {
 	delete engine;
 	delete engine;
@@ -399,7 +406,7 @@ float RewardEvaluator::getEnemyHeroStrategicalValue(const CGHeroInstance * enemy
 	  2. The formula quickly approaches 1.0 as hero level increases,
 	  2. The formula quickly approaches 1.0 as hero level increases,
 	  but higher level always means higher value and the minimal value for level 1 hero is 0.5
 	  but higher level always means higher value and the minimal value for level 1 hero is 0.5
 	*/
 	*/
-	return std::min(1.0f, objectValue * 0.9f + (1.0f - (1.0f / (1 + enemy->level))));
+	return std::min(1.5f, objectValue * 0.9f + (1.5f - (1.5f / (1 + enemy->level))));
 }
 }
 
 
 float RewardEvaluator::getResourceRequirementStrength(int resType) const
 float RewardEvaluator::getResourceRequirementStrength(int resType) const
@@ -640,7 +647,8 @@ public:
 
 
 		uint64_t armyStrength = heroExchange.getReinforcementArmyStrength();
 		uint64_t armyStrength = heroExchange.getReinforcementArmyStrength();
 
 
-		evaluationContext.strategicalValue += 0.5f * armyStrength / heroExchange.hero.get()->getArmyStrength();
+		evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero.get()->getArmyStrength());
+		evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero.get());
 	}
 	}
 };
 };
 
 
@@ -657,7 +665,7 @@ public:
 		uint64_t upgradeValue = armyUpgrade.getUpgradeValue();
 		uint64_t upgradeValue = armyUpgrade.getUpgradeValue();
 
 
 		evaluationContext.armyReward += upgradeValue;
 		evaluationContext.armyReward += upgradeValue;
-		evaluationContext.strategicalValue += upgradeValue / (float)armyUpgrade.hero->getArmyStrength();
+		evaluationContext.addNonCriticalStrategicalValue(upgradeValue / (float)armyUpgrade.hero->getArmyStrength());
 	}
 	}
 };
 };
 
 
@@ -712,19 +720,21 @@ public:
 		auto armyIncome = townArmyIncome(town);
 		auto armyIncome = townArmyIncome(town);
 		auto dailyIncome = town->dailyIncome()[EGameResID::GOLD];
 		auto dailyIncome = town->dailyIncome()[EGameResID::GOLD];
 
 
-		auto strategicalValue = std::sqrt(armyIncome / 20000.0f) + dailyIncome / 3000.0f;
+		auto strategicalValue = std::sqrt(armyIncome / 60000.0f) + dailyIncome / 10000.0f;
 
 
 		if(evaluationContext.evaluator.ai->buildAnalyzer->getDevelopmentInfo().size() == 1)
 		if(evaluationContext.evaluator.ai->buildAnalyzer->getDevelopmentInfo().size() == 1)
-			strategicalValue = 1;
+			vstd::amax(evaluationContext.strategicalValue, 10.0);
 
 
 		float multiplier = 1;
 		float multiplier = 1;
 
 
 		if(treat.turn < defendTown.getTurn())
 		if(treat.turn < defendTown.getTurn())
 			multiplier /= 1 + (defendTown.getTurn() - treat.turn);
 			multiplier /= 1 + (defendTown.getTurn() - treat.turn);
 
 
-		evaluationContext.armyReward += armyIncome * multiplier;
+		multiplier /= 1.0f + treat.turn / 5.0f;
+
+		evaluationContext.armyGrowth += armyIncome * multiplier;
 		evaluationContext.goldReward += dailyIncome * 5 * multiplier;
 		evaluationContext.goldReward += dailyIncome * 5 * multiplier;
-		evaluationContext.strategicalValue += strategicalValue * multiplier;
+		evaluationContext.addNonCriticalStrategicalValue(strategicalValue * multiplier);
 		vstd::amax(evaluationContext.danger, defendTown.getTreat().danger);
 		vstd::amax(evaluationContext.danger, defendTown.getTreat().danger);
 		addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength());
 		addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength());
 	}
 	}
@@ -770,19 +780,22 @@ public:
 		auto army = path.heroArmy;
 		auto army = path.heroArmy;
 
 
 		const CGObjectInstance * target = ai->cb->getObj((ObjectInstanceID)task->objid, false);
 		const CGObjectInstance * target = ai->cb->getObj((ObjectInstanceID)task->objid, false);
+		auto heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroPtr);
+
+		if(heroRole == HeroRole::MAIN)
+			evaluationContext.heroRole = heroRole;
 
 
 		if (target && ai->cb->getPlayerRelations(target->tempOwner, hero->tempOwner) == PlayerRelations::ENEMIES)
 		if (target && ai->cb->getPlayerRelations(target->tempOwner, hero->tempOwner) == PlayerRelations::ENEMIES)
 		{
 		{
 			evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero);
 			evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero);
 			evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold);
 			evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold);
 			evaluationContext.armyGrowth += evaluationContext.evaluator.getArmyGrowth(target, hero, army);
 			evaluationContext.armyGrowth += evaluationContext.evaluator.getArmyGrowth(target, hero, army);
-			evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, evaluationContext.heroRole);
-			evaluationContext.strategicalValue += evaluationContext.evaluator.getStrategicalValue(target);
+			evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, heroRole);
+			evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target));
 			evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army);
 			evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army);
 		}
 		}
 
 
 		vstd::amax(evaluationContext.armyLossPersentage, path.getTotalArmyLoss() / (double)path.getHeroStrength());
 		vstd::amax(evaluationContext.armyLossPersentage, path.getTotalArmyLoss() / (double)path.getHeroStrength());
-		evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroPtr);
 		addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength());
 		addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength());
 		vstd::amax(evaluationContext.turn, path.turn());
 		vstd::amax(evaluationContext.turn, path.turn());
 	}
 	}
@@ -822,7 +835,7 @@ public:
 			evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero) / boost;
 			evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero) / boost;
 			evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold) / boost;
 			evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold) / boost;
 			evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, role) / boost;
 			evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, role) / boost;
-			evaluationContext.strategicalValue += evaluationContext.evaluator.getStrategicalValue(target) / boost;
+			evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target) / boost);
 			evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army) / boost;
 			evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army) / boost;
 			evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost;
 			evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost;
 			evaluationContext.movementCost += objInfo.second.movementCost / boost;
 			evaluationContext.movementCost += objInfo.second.movementCost / boost;
@@ -860,6 +873,31 @@ public:
 	}
 	}
 };
 };
 
 
+class DismissHeroContextBuilder : public IEvaluationContextBuilder
+{
+private:
+	const Nullkiller * ai;
+
+public:
+	DismissHeroContextBuilder(const Nullkiller * ai) : ai(ai) {}
+
+	virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
+	{
+		if(task->goalType != Goals::DISMISS_HERO)
+			return;
+
+		Goals::DismissHero & dismissCommand = dynamic_cast<Goals::DismissHero &>(*task);
+		const CGHeroInstance * dismissedHero = dismissCommand.getHero().get();
+
+		auto role = ai->heroManager->getHeroRole(dismissedHero);
+		auto mpLeft = dismissedHero->movement;
+			
+		evaluationContext.movementCost += mpLeft;
+		evaluationContext.movementCostByRole[role] += mpLeft;
+		evaluationContext.goldCost += GameConstants::HERO_GOLD_COST + getArmyCost(dismissedHero);
+	}
+};
+
 class BuildThisEvaluationContextBuilder : public IEvaluationContextBuilder
 class BuildThisEvaluationContextBuilder : public IEvaluationContextBuilder
 {
 {
 public:
 public:
@@ -878,31 +916,31 @@ public:
 
 
 		if(bi.creatureID != CreatureID::NONE)
 		if(bi.creatureID != CreatureID::NONE)
 		{
 		{
-			evaluationContext.strategicalValue += buildThis.townInfo.armyStrength / 50000.0;
+			evaluationContext.addNonCriticalStrategicalValue(buildThis.townInfo.armyStrength / 50000.0);
 
 
 			if(bi.baseCreatureID == bi.creatureID)
 			if(bi.baseCreatureID == bi.creatureID)
 			{
 			{
-				evaluationContext.strategicalValue += (0.5f + 0.1f * bi.creatureLevel) / (float)bi.prerequisitesCount;
+				evaluationContext.addNonCriticalStrategicalValue((0.5f + 0.1f * bi.creatureLevel) / (float)bi.prerequisitesCount);
 				evaluationContext.armyReward += bi.armyStrength;
 				evaluationContext.armyReward += bi.armyStrength;
 			}
 			}
 			else
 			else
 			{
 			{
 				auto potentialUpgradeValue = evaluationContext.evaluator.getUpgradeArmyReward(buildThis.town, bi);
 				auto potentialUpgradeValue = evaluationContext.evaluator.getUpgradeArmyReward(buildThis.town, bi);
 				
 				
-				evaluationContext.strategicalValue += potentialUpgradeValue / 10000.0f / (float)bi.prerequisitesCount;
+				evaluationContext.addNonCriticalStrategicalValue(potentialUpgradeValue / 10000.0f / (float)bi.prerequisitesCount);
 				evaluationContext.armyReward += potentialUpgradeValue / (float)bi.prerequisitesCount;
 				evaluationContext.armyReward += potentialUpgradeValue / (float)bi.prerequisitesCount;
 			}
 			}
 		}
 		}
 		else if(bi.id == BuildingID::CITADEL || bi.id == BuildingID::CASTLE)
 		else if(bi.id == BuildingID::CITADEL || bi.id == BuildingID::CASTLE)
 		{
 		{
-			evaluationContext.strategicalValue += buildThis.town->creatures.size() * 0.2f;
+			evaluationContext.addNonCriticalStrategicalValue(buildThis.town->creatures.size() * 0.2f);
 			evaluationContext.armyReward += buildThis.townInfo.armyStrength / 2;
 			evaluationContext.armyReward += buildThis.townInfo.armyStrength / 2;
 		}
 		}
 		else
 		else
 		{
 		{
 			auto goldPreasure = evaluationContext.evaluator.ai->buildAnalyzer->getGoldPreasure();
 			auto goldPreasure = evaluationContext.evaluator.ai->buildAnalyzer->getGoldPreasure();
 
 
-			evaluationContext.strategicalValue += evaluationContext.goldReward * goldPreasure / 3500.0f / bi.prerequisitesCount;
+			evaluationContext.addNonCriticalStrategicalValue(evaluationContext.goldReward * goldPreasure / 3500.0f / bi.prerequisitesCount);
 		}
 		}
 
 
 		if(bi.notEnoughRes && bi.prerequisitesCount == 1)
 		if(bi.notEnoughRes && bi.prerequisitesCount == 1)
@@ -934,6 +972,7 @@ PriorityEvaluator::PriorityEvaluator(const Nullkiller * ai)
 	evaluationContextBuilders.push_back(std::make_shared<ArmyUpgradeEvaluator>());
 	evaluationContextBuilders.push_back(std::make_shared<ArmyUpgradeEvaluator>());
 	evaluationContextBuilders.push_back(std::make_shared<DefendTownEvaluator>());
 	evaluationContextBuilders.push_back(std::make_shared<DefendTownEvaluator>());
 	evaluationContextBuilders.push_back(std::make_shared<ExchangeSwapTownHeroesContextBuilder>());
 	evaluationContextBuilders.push_back(std::make_shared<ExchangeSwapTownHeroesContextBuilder>());
+	evaluationContextBuilders.push_back(std::make_shared<DismissHeroContextBuilder>(ai));
 }
 }
 
 
 EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal) const
 EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal) const

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

@@ -66,6 +66,8 @@ struct DLL_EXPORT EvaluationContext
 	float enemyHeroDangerRatio;
 	float enemyHeroDangerRatio;
 
 
 	EvaluationContext(const Nullkiller * ai);
 	EvaluationContext(const Nullkiller * ai);
+
+	void addNonCriticalStrategicalValue(float value);
 };
 };
 
 
 class IEvaluationContextBuilder
 class IEvaluationContextBuilder

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

@@ -71,7 +71,7 @@ void BuyArmy::accept(AIGateway * ai)
 		throw cannotFulfillGoalException("No creatures to buy.");
 		throw cannotFulfillGoalException("No creatures to buy.");
 	}
 	}
 
 
-	if(town->visitingHero)
+	if(town->visitingHero && !town->garrisonHero)
 	{
 	{
 		ai->moveHeroToTile(town->visitablePos(), town->visitingHero.get());
 		ai->moveHeroToTile(town->visitablePos(), town->visitingHero.get());
 	}
 	}

+ 49 - 9
AI/Nullkiller/Goals/Composition.cpp

@@ -31,9 +31,17 @@ std::string Composition::toString() const
 {
 {
 	std::string result = "Composition";
 	std::string result = "Composition";
 
 
-	for(auto goal : subtasks)
+	for(auto step : subtasks)
 	{
 	{
-		result += " " + goal->toString();
+		result += "[";
+		for(auto goal : step)
+		{
+			if(goal->isElementar())
+				result +=  goal->toString() + " => ";
+			else
+				result += goal->toString() + ", ";
+		}
+		result += "] ";
 	}
 	}
 
 
 	return result;
 	return result;
@@ -41,17 +49,34 @@ std::string Composition::toString() const
 
 
 void Composition::accept(AIGateway * ai)
 void Composition::accept(AIGateway * ai)
 {
 {
-	taskptr(*subtasks.back())->accept(ai);
+	for(auto task : subtasks.back())
+	{
+		if(task->isElementar())
+		{
+			taskptr(*task)->accept(ai);
+		}
+		else
+		{
+			break;
+		}
+	}
 }
 }
 
 
 TGoalVec Composition::decompose() const
 TGoalVec Composition::decompose() const
 {
 {
-	return subtasks;
+	TGoalVec result;
+
+	for(const TGoalVec & step : subtasks)
+		vstd::concatenate(result, step);
+
+	return result;
 }
 }
 
 
-Composition & Composition::addNext(const AbstractGoal & goal)
+Composition & Composition::addNextSequence(const TGoalVec & taskSequence)
 {
 {
-	return addNext(sptr(goal));
+	subtasks.push_back(taskSequence);
+
+	return *this;
 }
 }
 
 
 Composition & Composition::addNext(TSubgoal goal)
 Composition & Composition::addNext(TSubgoal goal)
@@ -64,20 +89,35 @@ Composition & Composition::addNext(TSubgoal goal)
 	}
 	}
 	else
 	else
 	{
 	{
-		subtasks.push_back(goal);
+		subtasks.push_back({goal});
 	}
 	}
 
 
 	return *this;
 	return *this;
 }
 }
 
 
+Composition & Composition::addNext(const AbstractGoal & goal)
+{
+	return addNext(sptr(goal));
+}
+
 bool Composition::isElementar() const
 bool Composition::isElementar() const
 {
 {
-	return subtasks.back()->isElementar();
+	return subtasks.back().front()->isElementar();
 }
 }
 
 
 int Composition::getHeroExchangeCount() const
 int Composition::getHeroExchangeCount() const
 {
 {
-	return isElementar() ? taskptr(*subtasks.back())->getHeroExchangeCount() : 0;
+	auto result = 0;
+
+	for(auto task : subtasks.back())
+	{
+		if(task->isElementar())
+		{
+			result += taskptr(*task)->getHeroExchangeCount();
+		}
+	}
+	
+	return result;
 }
 }
 
 
 }
 }

+ 2 - 6
AI/Nullkiller/Goals/Composition.h

@@ -18,7 +18,7 @@ namespace Goals
 	class DLL_EXPORT Composition : public ElementarGoal<Composition>
 	class DLL_EXPORT Composition : public ElementarGoal<Composition>
 	{
 	{
 	private:
 	private:
-		TGoalVec subtasks;
+		std::vector<TGoalVec> subtasks; // things we want to do now
 
 
 	public:
 	public:
 		Composition()
 		Composition()
@@ -26,16 +26,12 @@ namespace Goals
 		{
 		{
 		}
 		}
 
 
-		Composition(TGoalVec subtasks)
-			: ElementarGoal(Goals::COMPOSITION), subtasks(subtasks)
-		{
-		}
-
 		virtual bool operator==(const Composition & other) const override;
 		virtual bool operator==(const Composition & other) const override;
 		virtual std::string toString() const override;
 		virtual std::string toString() const override;
 		void accept(AIGateway * ai) override;
 		void accept(AIGateway * ai) override;
 		Composition & addNext(const AbstractGoal & goal);
 		Composition & addNext(const AbstractGoal & goal);
 		Composition & addNext(TSubgoal goal);
 		Composition & addNext(TSubgoal goal);
+		Composition & addNextSequence(const TGoalVec & taskSequence);
 		virtual TGoalVec decompose() const override;
 		virtual TGoalVec decompose() const override;
 		virtual bool isElementar() const override;
 		virtual bool isElementar() const override;
 		virtual int getHeroExchangeCount() const override;
 		virtual int getHeroExchangeCount() const override;

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

@@ -52,6 +52,20 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 	ai->nullkiller->setActive(chainPath.targetHero, tile);
 	ai->nullkiller->setActive(chainPath.targetHero, tile);
 	ai->nullkiller->setTargetObject(objid);
 	ai->nullkiller->setTargetObject(objid);
 
 
+	auto targetObject = ai->myCb->getObj(static_cast<ObjectInstanceID>(objid), false);
+
+	if(chainPath.turn() == 0 && targetObject && targetObject->ID == Obj::TOWN)
+	{
+		auto relations = ai->myCb->getPlayerRelations(ai->playerID, targetObject->getOwner());
+
+		if(relations == PlayerRelations::ENEMIES)
+		{
+			ai->nullkiller->armyFormation->rearrangeArmyForSiege(
+				dynamic_cast<const CGTownInstance *>(targetObject),
+				chainPath.targetHero);
+		}
+	}
+
 	std::set<int> blockedIndexes;
 	std::set<int> blockedIndexes;
 
 
 	for(int i = chainPath.nodes.size() - 1; i >= 0; i--)
 	for(int i = chainPath.nodes.size() - 1; i >= 0; i--)

+ 12 - 9
AI/Nullkiller/Goals/RecruitHero.cpp

@@ -24,7 +24,10 @@ using namespace Goals;
 
 
 std::string RecruitHero::toString() const
 std::string RecruitHero::toString() const
 {
 {
-	return "Recruit hero at " + town->getNameTranslated();
+	if(heroToBuy)
+		return "Recruit " + heroToBuy->getNameTranslated() + " at " + town->getNameTranslated();
+	else
+		return "Recruit hero at " + town->getNameTranslated();
 }
 }
 
 
 void RecruitHero::accept(AIGateway * ai)
 void RecruitHero::accept(AIGateway * ai)
@@ -45,20 +48,20 @@ void RecruitHero::accept(AIGateway * ai)
 		throw cannotFulfillGoalException("No available heroes in tavern in " + t->nodeName());
 		throw cannotFulfillGoalException("No available heroes in tavern in " + t->nodeName());
 	}
 	}
 
 
-	auto heroToHire = heroes[0];
+	auto heroToHire = heroToBuy;
 
 
-	for(auto hero : heroes)
+	if(!heroToHire)
 	{
 	{
-		if(objid == hero->id.getNum())
+		for(auto hero : heroes)
 		{
 		{
-			heroToHire = hero;
-			break;
+			if(!heroToHire || hero->getTotalStrength() > heroToHire->getTotalStrength())
+				heroToHire = hero;
 		}
 		}
-
-		if(hero->getTotalStrength() > heroToHire->getTotalStrength())
-			heroToHire = hero;
 	}
 	}
 
 
+	if(!heroToHire)
+		throw cannotFulfillGoalException("No hero to hire!");
+
 	if(t->visitingHero)
 	if(t->visitingHero)
 	{
 	{
 		cb->swapGarrisonHero(t);
 		cb->swapGarrisonHero(t);

+ 7 - 5
AI/Nullkiller/Goals/RecruitHero.h

@@ -22,18 +22,20 @@ namespace Goals
 {
 {
 	class DLL_EXPORT RecruitHero : public ElementarGoal<RecruitHero>
 	class DLL_EXPORT RecruitHero : public ElementarGoal<RecruitHero>
 	{
 	{
+	private:
+		const CGHeroInstance * heroToBuy;
+
 	public:
 	public:
 		RecruitHero(const CGTownInstance * townWithTavern, const CGHeroInstance * heroToBuy)
 		RecruitHero(const CGTownInstance * townWithTavern, const CGHeroInstance * heroToBuy)
-			: RecruitHero(townWithTavern)
+			: ElementarGoal(Goals::RECRUIT_HERO), heroToBuy(heroToBuy)
 		{
 		{
-			objid = heroToBuy->id.getNum();
+			town = townWithTavern;
+			priority = 1;
 		}
 		}
 
 
 		RecruitHero(const CGTownInstance * townWithTavern)
 		RecruitHero(const CGTownInstance * townWithTavern)
-			: ElementarGoal(Goals::RECRUIT_HERO)
+			: RecruitHero(townWithTavern, nullptr)
 		{
 		{
-			priority = 1;
-			town = townWithTavern;
 		}
 		}
 
 
 		virtual bool operator==(const RecruitHero & other) const override
 		virtual bool operator==(const RecruitHero & other) const override

+ 68 - 0
AI/Nullkiller/Helpers/ArmyFormation.cpp

@@ -0,0 +1,68 @@
+/*
+* ArmyFormation.cpp, part of VCMI engine
+*
+* Authors: listed in file AUTHORS in main folder
+*
+* License: GNU General Public License v2.0 or later
+* Full text of license available in license.txt file, in main folder
+*
+*/
+#include "StdInc.h"
+#include "ArmyFormation.h"
+#include "../../../lib/mapObjects/CGTownInstance.h"
+
+namespace NKAI
+{
+
+void ArmyFormation::rearrangeArmyForSiege(const CGTownInstance * town, const CGHeroInstance * attacker)
+{
+	auto freeSlots = attacker->getFreeSlotsQueue();
+
+	while(!freeSlots.empty())
+	{
+		auto weakestCreature = vstd::minElementByFun(attacker->Slots(), [](const std::pair<SlotID, CStackInstance *> & slot) -> int
+			{
+				return slot.second->getCount() == 1
+					? std::numeric_limits<int>::max()
+					: slot.second->getCreatureID().toCreature()->getAIValue();
+			});
+
+		if(weakestCreature == attacker->Slots().end() || weakestCreature->second->getCount() == 1)
+		{
+			break;
+		}
+
+		cb->splitStack(attacker, attacker, weakestCreature->first, freeSlots.front(), 1);
+		freeSlots.pop();
+	}
+
+	if(town->fortLevel() > CGTownInstance::FORT)
+	{
+		std::vector<CStackInstance *> stacks;
+
+		for(auto slot : attacker->Slots())
+			stacks.push_back(slot.second);
+
+		boost::sort(
+			stacks,
+			[](CStackInstance * slot1, CStackInstance * slot2) -> bool
+			{
+				auto cre1 = slot1->getCreatureID().toCreature();
+				auto cre2 = slot2->getCreatureID().toCreature();
+				auto flying = cre1->hasBonusOfType(BonusType::FLYING) - cre2->hasBonusOfType(BonusType::FLYING);
+			
+				if(flying != 0) return flying < 0;
+				else return cre1->getAIValue() < cre2->getAIValue();
+			});
+
+		for(int i = 0; i < stacks.size(); i++)
+		{
+			auto pos = vstd::findKey(attacker->Slots(), stacks[i]);
+
+			if(pos.getNum() != i)
+				cb->swapCreatures(attacker, attacker, static_cast<SlotID>(i), pos);
+		}
+	}
+}
+
+}

+ 39 - 0
AI/Nullkiller/Helpers/ArmyFormation.h

@@ -0,0 +1,39 @@
+/*
+* ArmyFormation.h, part of VCMI engine
+*
+* Authors: listed in file AUTHORS in main folder
+*
+* License: GNU General Public License v2.0 or later
+* Full text of license available in license.txt file, in main folder
+*
+*/
+#pragma once
+
+#include "../AIUtility.h"
+
+#include "../../../lib/GameConstants.h"
+#include "../../../lib/VCMI_Lib.h"
+#include "../../../lib/CTownHandler.h"
+#include "../../../lib/CBuildingHandler.h"
+
+namespace NKAI
+{
+
+struct HeroPtr;
+class AIGateway;
+class FuzzyHelper;
+class Nullkiller;
+
+class DLL_EXPORT ArmyFormation
+{
+private:
+	std::shared_ptr<CCallback> cb; //this is enough, but we downcast from CCallback
+	const Nullkiller * ai;
+
+public:
+	ArmyFormation(std::shared_ptr<CCallback> CB, const Nullkiller * ai): cb(CB), ai(ai) {}
+
+	void rearrangeArmyForSiege(const CGTownInstance * town, const CGHeroInstance * attacker);
+};
+
+}

+ 1 - 1
AI/Nullkiller/Markers/HeroExchange.cpp

@@ -29,7 +29,7 @@ bool HeroExchange::operator==(const HeroExchange & other) const
 
 
 std::string HeroExchange::toString() const
 std::string HeroExchange::toString() const
 {
 {
-	return "Hero exchange " + exchangePath.toString();
+	return "Hero exchange for " +hero.get()->getObjectName() + " by " + exchangePath.toString();
 }
 }
 
 
 uint64_t HeroExchange::getReinforcementArmyStrength() const
 uint64_t HeroExchange::getReinforcementArmyStrength() const

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

@@ -879,7 +879,7 @@ void AINodeStorage::setHeroes(std::map<const CGHeroInstance *, HeroRole> heroes)
 	for(auto & hero : heroes)
 	for(auto & hero : heroes)
 	{
 	{
 		// do not allow our own heroes in garrison to act on map
 		// do not allow our own heroes in garrison to act on map
-		if(hero.first->getOwner() == ai->playerID && hero.first->inTownGarrison)
+		if(hero.first->getOwner() == ai->playerID && hero.first->inTownGarrison && ai->isHeroLocked(hero.first))
 			continue;
 			continue;
 
 
 		uint64_t mask = FirstActorMask << actors.size();
 		uint64_t mask = FirstActorMask << actors.size();

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

@@ -11,7 +11,7 @@
 #pragma once
 #pragma once
 
 
 #define NKAI_PATHFINDER_TRACE_LEVEL 0
 #define NKAI_PATHFINDER_TRACE_LEVEL 0
-#define NKAI_TRACE_LEVEL 0
+#define NKAI_TRACE_LEVEL 2
 
 
 #include "../../../lib/pathfinder/CGPathNode.h"
 #include "../../../lib/pathfinder/CGPathNode.h"
 #include "../../../lib/pathfinder/INodeStorage.h"
 #include "../../../lib/pathfinder/INodeStorage.h"
@@ -24,8 +24,8 @@
 
 
 namespace NKAI
 namespace NKAI
 {
 {
-	const int SCOUT_TURN_DISTANCE_LIMIT = 3;
-	const int MAIN_TURN_DISTANCE_LIMIT = 5;
+	const int SCOUT_TURN_DISTANCE_LIMIT = 5;
+	const int MAIN_TURN_DISTANCE_LIMIT = 10;
 
 
 namespace AIPathfinding
 namespace AIPathfinding
 {
 {

+ 77 - 19
config/ai/object-priorities.txt

@@ -6,9 +6,9 @@ InputVariable: mainTurnDistance
   range: 0.000 10.000
   range: 0.000 10.000
   lock-range: true
   lock-range: true
   term: LOWEST Ramp 0.250 0.000
   term: LOWEST Ramp 0.250 0.000
-  term: LOW Discrete 0.000 1.000 0.500 0.800 1.000 0.000
-  term: MEDIUM Discrete 0.000 0.000 0.500 0.200 1.000 1.000 3.000 0.000
-  term: LONG Discrete 1.000 0.000 1.500 0.200 3.000 0.800 10.000 1.000
+  term: LOW Discrete 0.000 1.000 0.500 0.800 0.800 0.300 2.000 0.000
+  term: MEDIUM Discrete 0.000 0.000 0.500 0.200 0.800 0.700 2.000 1.000 6.000 0.000
+  term: LONG Discrete 2.000 0.000 6.000 1.000 10.000 0.800
 InputVariable: scoutTurnDistance
 InputVariable: scoutTurnDistance
   description: distance to tile in turns
   description: distance to tile in turns
   enabled: true
   enabled: true
@@ -43,6 +43,7 @@ InputVariable: armyLoss
   term: LOW Ramp 0.200 0.000
   term: LOW Ramp 0.200 0.000
   term: MEDIUM Triangle 0.000 0.200 0.500
   term: MEDIUM Triangle 0.000 0.200 0.500
   term: HIGH Ramp 0.200 0.500
   term: HIGH Ramp 0.200 0.500
+  term: ALL Ramp 0.700 1.000
 InputVariable: heroRole
 InputVariable: heroRole
   enabled: true
   enabled: true
   range: -0.100 1.100
   range: -0.100 1.100
@@ -82,13 +83,14 @@ InputVariable: closestHeroRatio
 InputVariable: strategicalValue
 InputVariable: strategicalValue
   description: Some abstract long term benefit non gold or army or skill
   description: Some abstract long term benefit non gold or army or skill
   enabled: true
   enabled: true
-  range: 0.000 1.000
+  range: 0.000 3.000
   lock-range: false
   lock-range: false
   term: NONE Ramp 0.200 0.000
   term: NONE Ramp 0.200 0.000
   term: LOWEST Triangle 0.000 0.010 0.250
   term: LOWEST Triangle 0.000 0.010 0.250
-  term: LOW Triangle 0.000 0.250 0.700
-  term: MEDIUM Triangle 0.250 0.700 1.000
-  term: HIGH Ramp 0.700 1.000
+  term: LOW Triangle 0.000 0.250 1.000
+  term: MEDIUM Triangle 0.250 1.000 2.000
+  term: HIGH Triangle 1.000 2.000 3.000
+  term: CRITICAL Ramp 2.000 3.000
 InputVariable: goldPreasure
 InputVariable: goldPreasure
   description: Ratio between weekly army cost and gold income
   description: Ratio between weekly army cost and gold income
   enabled: true
   enabled: true
@@ -132,21 +134,22 @@ InputVariable: armyGrowth
   term: HUGE Ramp 8000.000 20000.000
   term: HUGE Ramp 8000.000 20000.000
 OutputVariable: Value
 OutputVariable: Value
   enabled: true
   enabled: true
-  range: -1.500 2.000
+  range: -1.500 2.500
   lock-range: false
   lock-range: false
   aggregation: AlgebraicSum
   aggregation: AlgebraicSum
   defuzzifier: Centroid 100
   defuzzifier: Centroid 100
   default: 0.500
   default: 0.500
   lock-previous: false
   lock-previous: false
-  term: WORST Binary -1.000 -inf 0.500
+  term: WORST Binary -1.000 -inf 0.700
   term: BAD Rectangle -1.000 -0.700 0.500
   term: BAD Rectangle -1.000 -0.700 0.500
-  term: BASE Rectangle -0.200 0.200 0.400
-  term: LOW Rectangle 1.110 1.190 0.320
-  term: HIGHEST Discrete 0.300 0.000 0.300 1.000 0.600 1.000 0.600 0.000 1.700 0.000 1.700 1.000 2.000 1.000 2.000 0.000 0.500
-  term: HIGH Discrete 0.600 0.000 0.600 1.000 0.850 1.000 0.850 0.000 1.450 0.000 1.450 1.000 1.700 1.000 1.700 0.000 0.400
-  term: BITHIGH Discrete 0.850 0.000 0.850 1.000 1.000 1.000 1.000 0.000 1.300 0.000 1.300 1.000 1.450 1.000 1.450 0.000 0.350
-  term: MEDIUM Discrete 1.000 0.000 1.000 1.000 1.100 1.000 1.100 0.000 1.200 0.000 1.200 1.000 1.300 1.000 1.300 0.000 0.330
-RuleBlock: gold reward
+  term: BASE Rectangle -0.200 0.200 0.350
+  term: MEDIUM Rectangle 0.910 1.090 0.500
+  term: SMALL Rectangle 0.960 1.040 0.600
+  term: BITHIGH Rectangle 0.850 1.150 0.400
+  term: HIGH Rectangle 0.750 1.250 0.400
+  term: HIGHEST Rectangle 0.500 1.500 0.350
+  term: CRITICAL Ramp 0.500 2.000 0.500
+RuleBlock: basic
   enabled: true
   enabled: true
   conjunction: AlgebraicProduct
   conjunction: AlgebraicProduct
   disjunction: AlgebraicSum
   disjunction: AlgebraicSum
@@ -154,6 +157,61 @@ RuleBlock: gold reward
   activation: General
   activation: General
   rule: if heroRole is MAIN then Value is BASE
   rule: if heroRole is MAIN then Value is BASE
   rule: if heroRole is SCOUT then Value is BASE
   rule: if heroRole is SCOUT then Value is BASE
-  rule: if heroRole is MAIN and armyGrowth is HUGE then Value is HIGH
-  rule: if heroRole is MAIN and armyGrowth is BIG then Value is BITHIGH
-  rule: if heroRole is MAIN and strategicalValue is HIGH then Value is HIGHEST
+  rule: if heroRole is MAIN and armyGrowth is HUGE and fear is not HIGH then Value is HIGH
+  rule: if heroRole is MAIN and armyGrowth is BIG and mainTurnDistance is LOW then Value is HIGH
+  rule: if heroRole is MAIN and armyGrowth is BIG and mainTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH
+  rule: if heroRole is MAIN and armyGrowth is BIG and mainTurnDistance is LONG and fear is not HIGH then Value is MEDIUM
+  rule: if armyLoss is ALL then Value is WORST
+  rule: if turn is not NOW then Value is BAD with 0.1
+  rule: if closestHeroRatio is LOWEST and heroRole is SCOUT then Value is WORST
+  rule: if closestHeroRatio is LOW and heroRole is SCOUT then Value is BAD
+  rule: if closestHeroRatio is LOWEST and heroRole is MAIN then Value is BAD
+RuleBlock: strategicalValue
+  enabled: true
+  conjunction: AlgebraicProduct
+  disjunction: NormalizedSum
+  implication: AlgebraicProduct
+  activation: General
+  rule: if heroRole is MAIN and strategicalValue is HIGH and turn is NOW then Value is HIGHEST
+  rule: if heroRole is MAIN and strategicalValue is HIGH and turn is not NOW and mainTurnDistance is LOW and fear is not HIGH then Value is HIGHEST
+  rule: if heroRole is MAIN and strategicalValue is HIGH and mainTurnDistance is MEDIUM and fear is not HIGH then Value is HIGHEST with 0.5
+  rule: if heroRole is MAIN and strategicalValue is HIGH and mainTurnDistance is LONG and fear is not HIGH then Value is HIGH
+  rule: if heroRole is MAIN and strategicalValue is MEDIUM and mainTurnDistance is LOW then Value is HIGH
+  rule: if heroRole is MAIN and strategicalValue is MEDIUM and mainTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH
+  rule: if heroRole is MAIN and strategicalValue is MEDIUM and mainTurnDistance is LONG and fear is not HIGH then Value is MEDIUM
+  rule: if heroRole is MAIN and strategicalValue is LOW and mainTurnDistance is LOW and fear is not HIGH then Value is MEDIUM
+  rule: if heroRole is MAIN and strategicalValue is LOW and mainTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL
+  rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is not NONE and turn is NOW then Value is HIGH
+  rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is not NONE and turn is not NOW and scoutTurnDistance is LOW and fear is not HIGH then Value is HIGH
+  rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is not NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is HIGH with 0.5
+  rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is not NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is BITHIGH
+  rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is not NONE and scoutTurnDistance is LOW then Value is BITHIGH
+  rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is not NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is MEDIUM
+  rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is not NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is SMALL
+  rule: if heroRole is SCOUT and strategicalValue is LOW and danger is not NONE and scoutTurnDistance is LOW and fear is not HIGH then Value is SMALL
+  rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is NONE and turn is NOW then Value is HIGHEST
+  rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is NONE and turn is not NOW and scoutTurnDistance is LOW and fear is not HIGH then Value is HIGHEST
+  rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is HIGHEST with 0.5
+  rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is HIGH
+  rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is NONE and scoutTurnDistance is LOW then Value is HIGH
+  rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH
+  rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is MEDIUM
+  rule: if heroRole is SCOUT and strategicalValue is LOW and danger is NONE and scoutTurnDistance is LOW and fear is not HIGH then Value is MEDIUM
+  rule: if heroRole is SCOUT and strategicalValue is LOW and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL
+  rule: if armyLoss is HIGH and strategicalValue is LOW then Value is BAD
+  rule: if armyLoss is HIGH and strategicalValue is MEDIUM then Value is BAD with 0.7
+  rule: if strategicalValue is CRITICAL then Value is CRITICAL
+RuleBlock: armyReward
+  enabled: true
+  conjunction: AlgebraicProduct
+  disjunction: AlgebraicSum
+  implication: AlgebraicProduct
+  activation: General
+  rule: if heroRole is MAIN and armyReward is HIGH and mainTurnDistance is LOW and fear is not HIGH then Value is HIGHEST
+  rule: if heroRole is MAIN and armyReward is HIGH and mainTurnDistance is MEDIUM and fear is not HIGH then Value is HIGH
+  rule: if heroRole is MAIN and armyReward is HIGH and mainTurnDistance is LONG and fear is not HIGH then Value is BITHIGH
+  rule: if heroRole is MAIN and armyReward is MEDIUM and mainTurnDistance is LOW then Value is HIGH
+  rule: if heroRole is MAIN and armyReward is MEDIUM and mainTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH
+  rule: if heroRole is MAIN and armyReward is MEDIUM and mainTurnDistance is LONG and fear is not HIGH then Value is MEDIUM
+  rule: if heroRole is MAIN and armyReward is LOW and mainTurnDistance is LOW and fear is not HIGH then Value is MEDIUM
+  rule: if heroRole is MAIN and armyReward is LOW and mainTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL