mirror of
				https://github.com/vcmi/vcmi.git
				synced 2025-10-31 00:07:39 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			449 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			449 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /*
 | |
| * Explore.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 "Goals.h"
 | |
| #include "../VCAI.h"
 | |
| #include "../AIUtility.h"
 | |
| #include "../AIhelper.h"
 | |
| #include "../FuzzyHelper.h"
 | |
| #include "../ResourceManager.h"
 | |
| #include "../BuildingManager.h"
 | |
| #include "../../../lib/constants/StringConstants.h"
 | |
| #include "../../../lib/CPlayerState.h"
 | |
| 
 | |
| using namespace Goals;
 | |
| 
 | |
| namespace Goals
 | |
| {
 | |
| 	struct ExplorationHelper
 | |
| 	{
 | |
| 		HeroPtr hero;
 | |
| 		int sightRadius;
 | |
| 		float bestValue;
 | |
| 		TSubgoal bestGoal;
 | |
| 		VCAI * aip;
 | |
| 		CCallback * cbp;
 | |
| 		const TeamState * ts;
 | |
| 		int3 ourPos;
 | |
| 		bool allowDeadEndCancellation;
 | |
| 		bool allowGatherArmy;
 | |
| 
 | |
| 		ExplorationHelper(HeroPtr h, bool gatherArmy)
 | |
| 		{
 | |
| 			cbp = cb;
 | |
| 			aip = ai;
 | |
| 			hero = h;
 | |
| 			ts = cbp->getPlayerTeam(ai->playerID);
 | |
| 			sightRadius = hero->getSightRadius();
 | |
| 			bestGoal = sptr(Goals::Invalid());
 | |
| 			bestValue = 0;
 | |
| 			ourPos = h->visitablePos();
 | |
| 			allowDeadEndCancellation = true;
 | |
| 			allowGatherArmy = gatherArmy;
 | |
| 		}
 | |
| 
 | |
| 		void scanSector(int scanRadius)
 | |
| 		{
 | |
| 			int3 tile = int3(0, 0, ourPos.z);
 | |
| 
 | |
| 			const auto & slice = ts->fogOfWarMap[ourPos.z];
 | |
| 
 | |
| 			for(tile.x = ourPos.x - scanRadius; tile.x <= ourPos.x + scanRadius; tile.x++)
 | |
| 			{
 | |
| 				for(tile.y = ourPos.y - scanRadius; tile.y <= ourPos.y + scanRadius; tile.y++)
 | |
| 				{
 | |
| 
 | |
| 					if(cbp->isInTheMap(tile) && slice[tile.x][tile.y])
 | |
| 					{
 | |
| 						scanTile(tile);
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		void scanMap()
 | |
| 		{
 | |
| 			int3 mapSize = cbp->getMapSize();
 | |
| 			int perimeter = 2 * sightRadius * (mapSize.x + mapSize.y);
 | |
| 
 | |
| 			std::vector<int3> from;
 | |
| 			std::vector<int3> to;
 | |
| 
 | |
| 			from.reserve(perimeter);
 | |
| 			to.reserve(perimeter);
 | |
| 
 | |
| 			foreach_tile_pos([&](const int3 & pos)
 | |
| 			{
 | |
| 				if(ts->fogOfWarMap[pos.z][pos.x][pos.y])
 | |
| 				{
 | |
| 					bool hasInvisibleNeighbor = false;
 | |
| 
 | |
| 					foreach_neighbour(cbp, pos, [&](CCallback * cbp, int3 neighbour)
 | |
| 					{
 | |
| 						if(!ts->fogOfWarMap[neighbour.z][neighbour.x][neighbour.y])
 | |
| 						{
 | |
| 							hasInvisibleNeighbor = true;
 | |
| 						}
 | |
| 					});
 | |
| 
 | |
| 					if(hasInvisibleNeighbor)
 | |
| 						from.push_back(pos);
 | |
| 				}
 | |
| 			});
 | |
| 
 | |
| 			logAi->debug("Exploration scan visible area perimeter for hero %s", hero.name);
 | |
| 
 | |
| 			for(const int3 & tile : from)
 | |
| 			{
 | |
| 				scanTile(tile);
 | |
| 			}
 | |
| 
 | |
| 			if(!bestGoal->invalid())
 | |
| 			{
 | |
| 				return;
 | |
| 			}
 | |
| 
 | |
| 			allowDeadEndCancellation = false;
 | |
| 
 | |
| 			for(int i = 0; i < sightRadius; i++)
 | |
| 			{
 | |
| 				getVisibleNeighbours(from, to);
 | |
| 				vstd::concatenate(from, to);
 | |
| 				vstd::removeDuplicates(from);
 | |
| 			}
 | |
| 
 | |
| 			logAi->debug("Exploration scan all possible tiles for hero %s", hero.name);
 | |
| 
 | |
| 			for(const int3 & tile : from)
 | |
| 			{
 | |
| 				scanTile(tile);
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		void scanTile(const int3 & tile)
 | |
| 		{
 | |
| 			if(tile == ourPos
 | |
| 				|| !aip->ah->isTileAccessible(hero, tile)) //shouldn't happen, but it does
 | |
| 				return;
 | |
| 
 | |
| 			int tilesDiscovered = howManyTilesWillBeDiscovered(tile);
 | |
| 			if(!tilesDiscovered)
 | |
| 				return;
 | |
| 
 | |
| 			auto waysToVisit = aip->ah->howToVisitTile(hero, tile, allowGatherArmy);
 | |
| 			for(auto goal : waysToVisit)
 | |
| 			{
 | |
| 				if(goal->evaluationContext.movementCost <= 0.0) // should not happen
 | |
| 					continue;
 | |
| 
 | |
| 				float ourValue = (float)tilesDiscovered * tilesDiscovered / goal->evaluationContext.movementCost;
 | |
| 
 | |
| 				if(ourValue > bestValue) //avoid costly checks of tiles that don't reveal much
 | |
| 				{
 | |
| 					auto obj = cb->getTopObj(tile);
 | |
| 
 | |
| 					// picking up resources does not yield any exploration at all.
 | |
| 					// if it blocks the way to some explorable tile AIPathfinder will take care of it
 | |
| 					if(obj && obj->isBlockedVisitable())
 | |
| 					{
 | |
| 						continue;
 | |
| 					}
 | |
| 
 | |
| 					if(isSafeToVisit(hero, tile))
 | |
| 					{
 | |
| 						bestGoal = goal;
 | |
| 						bestValue = ourValue;
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		void getVisibleNeighbours(const std::vector<int3> & tiles, std::vector<int3> & out) const
 | |
| 		{
 | |
| 			for(const int3 & tile : tiles)
 | |
| 			{
 | |
| 				foreach_neighbour(cbp, tile, [&](CCallback * cbp, int3 neighbour)
 | |
| 				{
 | |
| 					if(ts->fogOfWarMap[neighbour.z][neighbour.x][neighbour.y])
 | |
| 					{
 | |
| 						out.push_back(neighbour);
 | |
| 					}
 | |
| 				});
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		int howManyTilesWillBeDiscovered(const int3 & pos) const
 | |
| 		{
 | |
| 			int ret = 0;
 | |
| 			int3 npos = int3(0, 0, pos.z);
 | |
| 
 | |
| 			const auto & slice = ts->fogOfWarMap[pos.z];
 | |
| 
 | |
| 			for(npos.x = pos.x - sightRadius; npos.x <= pos.x + sightRadius; npos.x++)
 | |
| 			{
 | |
| 				for(npos.y = pos.y - sightRadius; npos.y <= pos.y + sightRadius; npos.y++)
 | |
| 				{
 | |
| 					if(cbp->isInTheMap(npos)
 | |
| 						&& pos.dist2d(npos) - 0.5 < sightRadius
 | |
| 						&& !slice[npos.x][npos.y])
 | |
| 					{
 | |
| 						if(allowDeadEndCancellation
 | |
| 							&& !hasReachableNeighbor(npos))
 | |
| 						{
 | |
| 							continue;
 | |
| 						}
 | |
| 
 | |
| 						ret++;
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			return ret;
 | |
| 		}
 | |
| 
 | |
| 		bool hasReachableNeighbor(const int3 &pos) const
 | |
| 		{
 | |
| 			for(crint3 dir : int3::getDirs())
 | |
| 			{
 | |
| 				int3 tile = pos + dir;
 | |
| 				if(cbp->isInTheMap(tile))
 | |
| 				{
 | |
| 					auto isAccessible = aip->ah->isTileAccessible(hero, tile);
 | |
| 
 | |
| 					if(isAccessible)
 | |
| 						return true;
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			return false;
 | |
| 		}
 | |
| 	};
 | |
| }
 | |
| 
 | |
| bool Explore::operator==(const Explore & other) const
 | |
| {
 | |
| 	return other.hero.h == hero.h && other.allowGatherArmy == allowGatherArmy;
 | |
| }
 | |
| 
 | |
| std::string Explore::completeMessage() const
 | |
| {
 | |
| 	return "Hero " + hero.get()->getNameTranslated() + " completed exploration";
 | |
| }
 | |
| 
 | |
| TSubgoal Explore::whatToDoToAchieve()
 | |
| {
 | |
| 	return fh->chooseSolution(getAllPossibleSubgoals());
 | |
| }
 | |
| 
 | |
| TGoalVec Explore::getAllPossibleSubgoals()
 | |
| {
 | |
| 	TGoalVec ret;
 | |
| 	std::vector<const CGHeroInstance *> heroes;
 | |
| 
 | |
| 	if(hero)
 | |
| 	{
 | |
| 		heroes.push_back(hero.h);
 | |
| 	}
 | |
| 	else
 | |
| 	{
 | |
| 		//heroes = ai->getUnblockedHeroes();
 | |
| 		heroes = cb->getHeroesInfo();
 | |
| 		vstd::erase_if(heroes, [](const HeroPtr h)
 | |
| 		{
 | |
| 			if(ai->getGoal(h)->goalType == EXPLORE) //do not reassign hero who is already explorer
 | |
| 				return true;
 | |
| 
 | |
| 			if(!ai->isAbleToExplore(h))
 | |
| 				return true;
 | |
| 
 | |
| 			return !h->movementPointsRemaining(); //saves time, immobile heroes are useless anyway
 | |
| 		});
 | |
| 	}
 | |
| 
 | |
| 	//try to use buildings that uncover map
 | |
| 	std::vector<const CGObjectInstance *> objs;
 | |
| 	for(auto obj : ai->visitableObjs)
 | |
| 	{
 | |
| 		if(!vstd::contains(ai->alreadyVisited, obj))
 | |
| 		{
 | |
| 			switch(obj->ID.num)
 | |
| 			{
 | |
| 			case Obj::REDWOOD_OBSERVATORY:
 | |
| 			case Obj::PILLAR_OF_FIRE:
 | |
| 			case Obj::CARTOGRAPHER:
 | |
| 				objs.push_back(obj);
 | |
| 				break;
 | |
| 			case Obj::MONOLITH_ONE_WAY_ENTRANCE:
 | |
| 			case Obj::MONOLITH_TWO_WAY:
 | |
| 			case Obj::SUBTERRANEAN_GATE:
 | |
| 				auto tObj = dynamic_cast<const CGTeleport *>(obj);
 | |
| 				assert(ai->knownTeleportChannels.find(tObj->channel) != ai->knownTeleportChannels.end());
 | |
| 				if(TeleportChannel::IMPASSABLE != ai->knownTeleportChannels[tObj->channel]->passability)
 | |
| 					objs.push_back(obj);
 | |
| 				break;
 | |
| 			}
 | |
| 		}
 | |
| 		else
 | |
| 		{
 | |
| 			switch(obj->ID.num)
 | |
| 			{
 | |
| 			case Obj::MONOLITH_TWO_WAY:
 | |
| 			case Obj::SUBTERRANEAN_GATE:
 | |
| 				auto tObj = dynamic_cast<const CGTeleport *>(obj);
 | |
| 				if(TeleportChannel::IMPASSABLE == ai->knownTeleportChannels[tObj->channel]->passability)
 | |
| 					break;
 | |
| 				for(auto exit : ai->knownTeleportChannels[tObj->channel]->exits)
 | |
| 				{
 | |
| 					if(!cb->getObj(exit))
 | |
| 					{ // Always attempt to visit two-way teleports if one of channel exits is not visible
 | |
| 						objs.push_back(obj);
 | |
| 						break;
 | |
| 					}
 | |
| 				}
 | |
| 				break;
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	for(auto h : heroes)
 | |
| 	{
 | |
| 		for(auto obj : objs) //double loop, performance risk?
 | |
| 		{
 | |
| 			auto waysToVisitObj = ai->ah->howToVisitObj(h, obj, allowGatherArmy);
 | |
| 
 | |
| 			vstd::concatenate(ret, waysToVisitObj);
 | |
| 		}
 | |
| 
 | |
| 		TSubgoal goal = exploreNearestNeighbour(h);
 | |
| 
 | |
| 		if(!goal->invalid())
 | |
| 		{
 | |
| 			ret.push_back(goal);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if(ret.empty())
 | |
| 	{
 | |
| 		for(auto h : heroes)
 | |
| 		{
 | |
| 			logAi->trace("Exploration searching for a new point for hero %s", h->getNameTranslated());
 | |
| 
 | |
| 			TSubgoal goal = explorationNewPoint(h);
 | |
| 
 | |
| 			if(goal->invalid())
 | |
| 			{
 | |
| 				ai->markHeroUnableToExplore(h); //there is no freely accessible tile, do not poll this hero anymore
 | |
| 			}
 | |
| 			else
 | |
| 			{
 | |
| 				ret.push_back(goal);
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	//we either don't have hero yet or none of heroes can explore
 | |
| 	if((!hero || ret.empty()) && ai->canRecruitAnyHero())
 | |
| 		ret.push_back(sptr(RecruitHero()));
 | |
| 
 | |
| 	if(ret.empty())
 | |
| 	{
 | |
| 		throw goalFulfilledException(sptr(Explore().sethero(hero)));
 | |
| 	}
 | |
| 
 | |
| 	return ret;
 | |
| }
 | |
| 
 | |
| bool Explore::fulfillsMe(TSubgoal goal)
 | |
| {
 | |
| 	if(goal->goalType == EXPLORE)
 | |
| 	{
 | |
| 		if(goal->hero)
 | |
| 			return hero == goal->hero;
 | |
| 		else
 | |
| 			return true; //cancel ALL exploration
 | |
| 	}
 | |
| 	return false;
 | |
| }
 | |
| 
 | |
| TSubgoal Explore::explorationBestNeighbour(int3 hpos, HeroPtr h) const
 | |
| {
 | |
| 	ExplorationHelper scanResult(h, allowGatherArmy);
 | |
| 
 | |
| 	for(crint3 dir : int3::getDirs())
 | |
| 	{
 | |
| 		int3 tile = hpos + dir;
 | |
| 		if(cb->isInTheMap(tile))
 | |
| 		{
 | |
| 			scanResult.scanTile(tile);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return scanResult.bestGoal;
 | |
| }
 | |
| 
 | |
| 
 | |
| TSubgoal Explore::explorationNewPoint(HeroPtr h) const
 | |
| {
 | |
| 	ExplorationHelper scanResult(h, allowGatherArmy);
 | |
| 
 | |
| 	scanResult.scanSector(10);
 | |
| 
 | |
| 	if(!scanResult.bestGoal->invalid())
 | |
| 	{
 | |
| 		return scanResult.bestGoal;
 | |
| 	}
 | |
| 
 | |
| 	scanResult.scanMap();
 | |
| 
 | |
| 	return scanResult.bestGoal;
 | |
| }
 | |
| 
 | |
| 
 | |
| TSubgoal Explore::exploreNearestNeighbour(HeroPtr h) const
 | |
| {
 | |
| 	TimeCheck tc("where to explore");
 | |
| 	int3 hpos = h->visitablePos();
 | |
| 
 | |
| 	//look for nearby objs -> visit them if they're close enough
 | |
| 	const int DIST_LIMIT = 3;
 | |
| 	const float COST_LIMIT = .2f; //todo: fine tune
 | |
| 
 | |
| 	std::vector<const CGObjectInstance *> nearbyVisitableObjs;
 | |
| 	for(int x = hpos.x - DIST_LIMIT; x <= hpos.x + DIST_LIMIT; ++x) //get only local objects instead of all possible objects on the map
 | |
| 	{
 | |
| 		for(int y = hpos.y - DIST_LIMIT; y <= hpos.y + DIST_LIMIT; ++y)
 | |
| 		{
 | |
| 			for(auto obj : cb->getVisitableObjs(int3(x, y, hpos.z), false))
 | |
| 			{
 | |
| 				if(ai->isGoodForVisit(obj, h, COST_LIMIT))
 | |
| 				{
 | |
| 					nearbyVisitableObjs.push_back(obj);
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if(nearbyVisitableObjs.size())
 | |
| 	{
 | |
| 		vstd::removeDuplicates(nearbyVisitableObjs); //one object may occupy multiple tiles
 | |
| 		boost::sort(nearbyVisitableObjs, CDistanceSorter(h.get()));
 | |
| 
 | |
| 		TSubgoal pickupNearestObj = fh->chooseSolution(ai->ah->howToVisitObj(h, nearbyVisitableObjs.back(), false));
 | |
| 
 | |
| 		if(!pickupNearestObj->invalid())
 | |
| 		{
 | |
| 			return pickupNearestObj;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	//check if nearby tiles allow us to reveal anything - this is quick
 | |
| 	return explorationBestNeighbour(hpos, h);
 | |
| }
 |