mirror of
				https://github.com/vcmi/vcmi.git
				synced 2025-10-31 00:07:39 +02:00 
			
		
		
		
	Nullkiller AI: further stabilisation, implement staged hero chain (first with limit 0 turns then 1 turn)
This commit is contained in:
		
				
					committed by
					
						 Andrii Danylchenko
						Andrii Danylchenko
					
				
			
			
				
	
			
			
			
						parent
						
							6bebb766a6
						
					
				
				
					commit
					eea5cb7f0b
				
			| @@ -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)); | ||||
| 			} | ||||
| 		} | ||||
| 	}; | ||||
|   | ||||
| @@ -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; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -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))); | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -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); | ||||
|  | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
| 	{ | ||||
|   | ||||
| @@ -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 | ||||
| 		{ | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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; } | ||||
| 	}; | ||||
| } | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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( | ||||
|   | ||||
| @@ -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); | ||||
| } | ||||
|   | ||||
| @@ -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(); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user