/* * 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 from; std::vector 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 & tiles, std::vector & 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 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 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(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(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 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); }