Browse Source

Nullkiller AI: further stabilisation, implement staged hero chain (first with limit 0 turns then 1 turn)

Andrii Danylchenko 4 years ago
parent
commit
eea5cb7f0b

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

@@ -99,7 +99,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getTasks()
 				auto hero = path.targetHero;
 				auto danger = path.getTotalDanger();
 
-				if(danger == 0 && path.exchangeCount > 1)
+				if(ai->ah->getHeroRole(hero) == HeroRole::SCOUT && danger == 0 && path.exchangeCount > 1)
 					continue;
 
 				auto isSafe = isSafeToVisit(hero, path.heroArmy, danger);
@@ -131,13 +131,13 @@ Goals::TGoalVec CaptureObjectsBehavior::getTasks()
 
 			for(auto way : waysToVisitObj)
 			{
+				if(ai->nullkiller->arePathHeroesLocked(way->getPath()))
+					continue;
+
 				way->evaluationContext.closestWayRatio
 					= way->evaluationContext.movementCost / closestWay->evaluationContext.movementCost;
 
-				if(way->hero && ai->nullkiller->canMove(way->hero.h))
-				{
-					tasks.push_back(sptr(*way));
-				}
+				tasks.push_back(sptr(*way));
 			}
 		}
 	};

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

@@ -227,13 +227,25 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 					priority);
 #endif
 
-				tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, town->visitingHero.get()).setpriority(priority)));
+				tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, town->visitingHero.get(), HeroLockedReason::DEFENCE).setpriority(priority)));
 
 				continue;
 			}
 				
 			if(path.turn() <= treat.turn && path.getHeroStrength() * SAFE_ATTACK_CONSTANT >= treat.danger)
 			{
+				if(ai->nullkiller->arePathHeroesLocked(path))
+				{
+#if AI_TRACE_LEVEL >= 1
+					logAi->trace("Can not move %s to defend town %s with priority %f. Path is locked.",
+						path.targetHero->name,
+						town->name,
+						priority);
+
+#endif
+					continue;
+				}
+
 #if AI_TRACE_LEVEL >= 1
 				logAi->trace("Move %s to defend town %s with priority %f",
 					path.targetHero->name,
@@ -242,8 +254,6 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 #endif
 
 				tasks.push_back(Goals::sptr(Goals::ExecuteHeroChain(path, town).setpriority(priority)));
-
-				continue;
 			}
 		}
 	}

+ 5 - 4
AI/Nullkiller/Behaviors/StartupBehavior.cpp

@@ -18,6 +18,7 @@
 #include "lib/mapping/CMap.h" //for victory conditions
 #include "lib/mapObjects/MapObjects.h" //for victory conditions
 #include "lib/CPathfinder.h"
+#include "../Engine/Nullkiller.h"
 
 extern boost::thread_specific_ptr<CCallback> cb;
 extern boost::thread_specific_ptr<VCAI> ai;
@@ -147,17 +148,17 @@ Goals::TGoalVec StartupBehavior::getTasks()
 				{
 					if(canRecruitHero || ai->ah->howManyReinforcementsCanGet(visitingHero, garrisonHero) > 200)
 					{
-						tasks.push_back(Goals::sptr(ExchangeSwapTownHeroes(startupTown, visitingHero).setpriority(100)));
+						tasks.push_back(Goals::sptr(ExchangeSwapTownHeroes(startupTown, visitingHero, HeroLockedReason::STARTUP).setpriority(100)));
 					}
 				}
 				else if(ai->ah->howManyReinforcementsCanGet(garrisonHero, visitingHero) > 200)
 				{
-					tasks.push_back(Goals::sptr(ExchangeSwapTownHeroes(startupTown, garrisonHero).setpriority(100)));
+					tasks.push_back(Goals::sptr(ExchangeSwapTownHeroes(startupTown, garrisonHero, HeroLockedReason::STARTUP).setpriority(100)));
 				}
 			}
 			else if(canRecruitHero)
 			{
-				tasks.push_back(Goals::sptr(ExchangeSwapTownHeroes(startupTown, visitingHero).setpriority(100)));
+				tasks.push_back(Goals::sptr(ExchangeSwapTownHeroes(startupTown, visitingHero, HeroLockedReason::STARTUP).setpriority(100)));
 			}
 		}
 	}
@@ -171,7 +172,7 @@ Goals::TGoalVec StartupBehavior::getTasks()
 	{
 		for(const CGTownInstance * town : towns)
 		{
-			if(town->garrisonHero && town->garrisonHero->movement)
+			if(town->garrisonHero && town->garrisonHero->movement && ai->nullkiller->getHeroLockedReason(town->garrisonHero) != HeroLockedReason::DEFENCE)
 				tasks.push_back(Goals::sptr(ExchangeSwapTownHeroes(town, nullptr).setpriority(0.0001f)));
 		}
 	}

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

@@ -79,13 +79,26 @@ void Nullkiller::updateAiState()
 	auto activeHeroes = ai->getMyHeroes();
 
 	vstd::erase_if(activeHeroes, [this](const HeroPtr & hero) -> bool{
-		return isHeroLocked(hero.h);
+		auto lockedReason = getHeroLockedReason(hero.h);
+
+		return lockedReason == HeroLockedReason::DEFENCE || lockedReason == HeroLockedReason::STARTUP;
 	});
 
 	ai->ah->updatePaths(activeHeroes, true);
 	ai->ah->updateHeroRoles();
 }
 
+bool Nullkiller::arePathHeroesLocked(const AIPath & path) const
+{
+	for(auto & node : path.nodes)
+	{
+		if(isHeroLocked(node.targetHero))
+			return true;
+	}
+
+	return false;
+}
+
 void Nullkiller::makeTurn()
 {
 	resetAiState();
@@ -97,17 +110,14 @@ void Nullkiller::makeTurn()
 		Goals::TGoalVec bestTasks = {
 			choseBestTask(std::make_shared<BuyArmyBehavior>()),
 			choseBestTask(std::make_shared<CaptureObjectsBehavior>()),
-			choseBestTask(std::make_shared<RecruitHeroBehavior>())
+			choseBestTask(std::make_shared<RecruitHeroBehavior>()),
+			choseBestTask(std::make_shared<DefenceBehavior>())
 		};
 
 		if(cb->getDate(Date::DAY) == 1)
 		{
 			bestTasks.push_back(choseBestTask(std::make_shared<StartupBehavior>()));
 		}
-		else
-		{
-			bestTasks.push_back(choseBestTask(std::make_shared<DefenceBehavior>()));
-		}
 
 		Goals::TSubgoal bestTask = choseBestTask(bestTasks);
 

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

@@ -14,12 +14,23 @@
 #include "../Goals/AbstractGoal.h"
 #include "../Behaviors/Behavior.h"
 
+enum class HeroLockedReason
+{
+	NOT_LOCKED = 0,
+
+	STARTUP = 1,
+
+	DEFENCE = 2,
+
+	HERO_CHAIN = 3
+};
+
 class Nullkiller
 {
 private:
 	std::unique_ptr<PriorityEvaluator> priorityEvaluator;
 	const CGHeroInstance * activeHero;
-	std::set<const CGHeroInstance *> lockedHeroes;
+	std::map<const CGHeroInstance *, HeroLockedReason> lockedHeroes;
 
 public:
 	std::unique_ptr<DangerHitMapAnalyzer> dangerHitMap;
@@ -28,10 +39,11 @@ public:
 	void makeTurn();
 	bool isActive(const CGHeroInstance * hero) const { return activeHero == hero; }
 	bool isHeroLocked(const CGHeroInstance * hero) const { return vstd::contains(lockedHeroes, hero); }
+	HeroLockedReason getHeroLockedReason(const CGHeroInstance * hero) const { return isHeroLocked(hero) ? lockedHeroes.at(hero) : HeroLockedReason::NOT_LOCKED; }
 	void setActive(const CGHeroInstance * hero) { activeHero = hero; }
-	void lockHero(const CGHeroInstance * hero) { lockedHeroes.insert(hero); }
+	void lockHero(const CGHeroInstance * hero, HeroLockedReason lockReason) { lockedHeroes[hero] = lockReason; }
 	void unlockHero(const CGHeroInstance * hero) { lockedHeroes.erase(hero); }
-	bool canMove(const CGHeroInstance * hero) { return hero->movement; }
+	bool arePathHeroesLocked(const AIPath & path) const;
 
 private:
 	void resetAiState();

+ 4 - 3
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -201,10 +201,10 @@ float getEnemyHeroStrategicalValue(const CGHeroInstance * enemy)
 
 	for(auto obj : objectsUnderTreat)
 	{
-		objectValue += getStrategicalValue(obj);
+		vstd::amax(objectValue, getStrategicalValue(obj));
 	}
 
-	return objectValue + enemy->level / 15.0f;
+	return objectValue / 2.0f + enemy->level / 15.0f;
 }
 
 float getStrategicalValue(const CGObjectInstance * target)
@@ -400,7 +400,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
 	assert(result >= 0);
 
 #ifdef VCMI_TRACE_PATHFINDER
-	logAi->trace("Evaluated %s, hero %s, loss: %f, turns: %f, gold: %d, army gain: %d, danger: %d, role: %s, result %f",
+	logAi->trace("Evaluated %s, hero %s, loss: %f, turns: %f, gold: %d, army gain: %d, danger: %d, role: %s, strategical value: %f, result %f",
 		task->name(),
 		hero->name,
 		armyLossPersentage,
@@ -409,6 +409,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
 		armyReward,
 		danger,
 		heroRole ? "scout" : "main",
+		strategicalValue,
 		result);
 #endif
 

+ 6 - 3
AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp

@@ -23,8 +23,11 @@ extern FuzzyHelper * fh;
 
 using namespace Goals;
 
-ExchangeSwapTownHeroes::ExchangeSwapTownHeroes(const CGTownInstance * town, const CGHeroInstance * garrisonHero)
-	:CGoal(Goals::EXCHANGE_SWAP_TOWN_HEROES), town(town), garrisonHero(garrisonHero)
+ExchangeSwapTownHeroes::ExchangeSwapTownHeroes(
+	const CGTownInstance * town, 
+	const CGHeroInstance * garrisonHero,
+	HeroLockedReason lockingReason)
+	:CGoal(Goals::EXCHANGE_SWAP_TOWN_HEROES), town(town), garrisonHero(garrisonHero), lockingReason(lockingReason)
 {
 }
 
@@ -90,7 +93,7 @@ void ExchangeSwapTownHeroes::accept(VCAI * ai)
 		cb->swapGarrisonHero(town); // selected hero left in garrison with strongest army
 	}
 
-	ai->nullkiller->lockHero(garrisonHero);
+	ai->nullkiller->lockHero(garrisonHero, lockingReason);
 
 	if(town->visitingHero && town->visitingHero != garrisonHero)
 	{

+ 6 - 1
AI/Nullkiller/Goals/ExchangeSwapTownHeroes.h

@@ -10,6 +10,7 @@
 #pragma once
 
 #include "CGoal.h"
+#include "..\Engine\Nullkiller.h"
 
 namespace Goals
 {
@@ -18,9 +19,13 @@ namespace Goals
 	private:
 		const CGTownInstance * town;
 		const CGHeroInstance * garrisonHero;
+		HeroLockedReason lockingReason;
 
 	public:
-		ExchangeSwapTownHeroes(const CGTownInstance * town, const CGHeroInstance * garrisonHero = nullptr);
+		ExchangeSwapTownHeroes(
+			const CGTownInstance * town,
+			const CGHeroInstance * garrisonHero = nullptr,
+			HeroLockedReason lockingReason = HeroLockedReason::NOT_LOCKED);
 
 		TGoalVec getAllPossibleSubgoals() override
 		{

+ 4 - 8
AI/Nullkiller/Goals/ExecuteHeroChain.cpp

@@ -69,7 +69,7 @@ void ExecuteHeroChain::accept(VCAI * ai)
 		if(vstd::contains(blockedIndexes, i))
 		{
 			blockedIndexes.insert(node.parentIndex);
-			ai->nullkiller->lockHero(hero.get());
+			ai->nullkiller->lockHero(hero.get(), HeroLockedReason::HERO_CHAIN);
 
 			continue;
 		}
@@ -128,7 +128,7 @@ void ExecuteHeroChain::accept(VCAI * ai)
 							{
 								logAi->warn("Hero %s has %d mp which is not enough to continue his way towards %s.", hero.name, hero->movement, node.coord.toString());
 
-								ai->nullkiller->lockHero(hero.get());
+								ai->nullkiller->lockHero(hero.get(), HeroLockedReason::HERO_CHAIN);
 								return;
 							}
 						}
@@ -148,13 +148,9 @@ void ExecuteHeroChain::accept(VCAI * ai)
 
 				return;
 			}
-
-			// do not lock hero if it is simple one hero chain
-			if(chainPath.exchangeCount == 1)
-				return;
-
+			
 			// no exception means we were not able to rich the tile
-			ai->nullkiller->lockHero(hero.get());
+			ai->nullkiller->lockHero(hero.get(), HeroLockedReason::HERO_CHAIN);
 			blockedIndexes.insert(node.parentIndex);
 		}
 		catch(goalFulfilledException)

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

@@ -32,5 +32,6 @@ namespace Goals
 		std::string name() const override;
 		std::string completeMessage() const override;
 		virtual bool operator==(const ExecuteHeroChain & other) const override;
+		const AIPath & getPath() const { return chainPath; }
 	};
 }

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

@@ -77,7 +77,8 @@ void AINodeStorage::clear()
 {
 	actors.clear();
 	heroChainPass = false;
-	heroChainTurn = 1;
+	heroChainTurn = 0;
+	heroChainMaxTurns = 1;
 }
 
 const AIPathNode * AINodeStorage::getAINode(const CGPathNode * node) const
@@ -251,6 +252,16 @@ std::vector<CGPathNode *> AINodeStorage::calculateNeighbours(
 	return neighbours;
 }
 
+bool AINodeStorage::increaseHeroChainTurnLimit()
+{
+	if(heroChainTurn >= heroChainMaxTurns)
+		return false;
+
+	heroChainTurn++;
+
+	return true;
+}
+
 bool AINodeStorage::calculateHeroChain()
 {
 	heroChainPass = true;

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

@@ -102,6 +102,7 @@ private:
 	std::vector<CGPathNode *> heroChain;
 	bool heroChainPass; // true if we need to calculate hero chain
 	int heroChainTurn;
+	int heroChainMaxTurns;
 	PlayerColor playerID;
 
 public:
@@ -113,6 +114,8 @@ public:
 
 	void initialize(const PathfinderOptions & options, const CGameState * gs) override;
 
+	bool increaseHeroChainTurnLimit();
+
 	virtual std::vector<CGPathNode *> getInitialNodes() override;
 
 	virtual std::vector<CGPathNode *> calculateNeighbours(

+ 17 - 4
AI/Nullkiller/Pathfinding/AIPathfinder.cpp

@@ -62,12 +62,25 @@ void AIPathfinder::updatePaths(std::vector<HeroPtr> heroes, bool useHeroChain)
 	}
 
 	auto config = std::make_shared<AIPathfinding::AIPathfinderConfig>(cb, ai, storage);
+	bool continueCalculation = false;
 
-	do {
+	do
+	{
 		logAi->trace("Recalculate paths pass %d", pass++);
 		cb->calculatePaths(config);
 
-		logAi->trace("Recalculate chain pass %d", pass);
-		useHeroChain = useHeroChain && storage->calculateHeroChain();
-	} while(useHeroChain);
+		if(useHeroChain)
+		{
+			logAi->trace("Recalculate chain pass %d", pass);
+
+			continueCalculation = storage->calculateHeroChain();
+
+			if(!continueCalculation)
+			{
+				logAi->trace("Increase chain turn limit");
+
+				continueCalculation = storage->increaseHeroChainTurnLimit() && storage->calculateHeroChain();
+			}
+		}
+	} while(continueCalculation);
 }

+ 11 - 4
AI/Nullkiller/VCAI.cpp

@@ -546,15 +546,22 @@ void VCAI::objectPropertyChanged(const SetObjectProperty * sop)
 	NET_EVENT_HANDLER;
 	if(sop->what == ObjProperty::OWNER)
 	{
-		if(myCb->getPlayerRelations(playerID, (PlayerColor)sop->val) == PlayerRelations::ENEMIES)
+		auto relations = myCb->getPlayerRelations(playerID, (PlayerColor)sop->val);
+		auto obj = myCb->getObj(sop->id, false);
+
+		if(obj)
 		{
-			//we want to visit objects owned by oppponents
-			auto obj = myCb->getObj(sop->id, false);
-			if(obj)
+			if(relations == PlayerRelations::ENEMIES)
 			{
+				//we want to visit objects owned by oppponents
 				addVisitableObj(obj); // TODO: Remove once save compatability broken. In past owned objects were removed from this set
 				vstd::erase_if_present(alreadyVisited, obj);
 			}
+			else if(relations == PlayerRelations::SAME_PLAYER && obj->ID == Obj::TOWN && nullkiller)
+			{
+				// reevaluate defence for a new town
+				nullkiller->dangerHitMap->reset();
+			}
 		}
 	}
 }