1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-12-22 22:13:35 +02:00
vcmi/AI/Nullkiller/Helpers/ExplorationHelper.cpp
2024-11-28 11:53:51 +00:00

238 lines
5.8 KiB
C++

/*
* ExplorationHelper.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 "ExplorationHelper.h"
#include "../../../lib/mapObjects/CGTownInstance.h"
#include "../Engine/Nullkiller.h"
#include "../Goals/Invalid.h"
#include "../Goals/Composition.h"
#include "../Goals/ExecuteHeroChain.h"
#include "../Markers/ExplorationPoint.h"
#include "../../../lib/CPlayerState.h"
#include "../Behaviors/CaptureObjectsBehavior.h"
#include "../Goals/ExploreNeighbourTile.h"
namespace NKAI
{
using namespace Goals;
ExplorationHelper::ExplorationHelper(const CGHeroInstance * hero, const Nullkiller * ai, bool useCPathfinderAccessibility)
:ai(ai), cbp(ai->cb.get()), hero(hero), useCPathfinderAccessibility(useCPathfinderAccessibility)
{
ts = cbp->getPlayerTeam(ai->playerID);
sightRadius = hero->getSightRadius();
bestGoal = sptr(Goals::Invalid());
bestValue = 0;
bestTilesDiscovered = 0;
ourPos = hero->visitablePos();
allowDeadEndCancellation = true;
}
TSubgoal ExplorationHelper::makeComposition() const
{
Composition c;
c.addNext(ExplorationPoint(bestTile, bestTilesDiscovered));
c.addNextSequence({bestGoal, sptr(ExploreNeighbourTile(hero, 5))});
return sptr(c);
}
bool ExplorationHelper::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);
}
}
}
return !bestGoal->invalid();
}
bool ExplorationHelper::scanMap()
{
int3 mapSize = cbp->getMapSize();
int perimeter = 2 * sightRadius * (mapSize.x + mapSize.y);
std::vector<int3> edgeTiles;
edgeTiles.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)
edgeTiles.push_back(pos);
}
});
logAi->debug("Exploration scan visible area perimeter for hero %s", hero->getNameTranslated());
for(const int3 & tile : edgeTiles)
{
scanTile(tile);
}
if(!bestGoal->invalid())
{
return true;
}
allowDeadEndCancellation = false;
logAi->debug("Exploration scan all possible tiles for hero %s", hero->getNameTranslated());
boost::multi_array<ui8, 3> potentialTiles = ts->fogOfWarMap;
std::vector<int3> tilesToExploreFrom = edgeTiles;
// WARNING: POTENTIAL BUG
// AI attempts to move to any tile within sight radius to reveal some new tiles
// however sight radius is circular, while this method assumes square radius
// standing on the edge of a square will NOT reveal tile in opposite corner
for(int i = 0; i < sightRadius; i++)
{
std::vector<int3> newTilesToExploreFrom;
for(const int3 & tile : tilesToExploreFrom)
{
foreach_neighbour(cbp, tile, [&](CCallback * cbp, int3 neighbour)
{
if(potentialTiles[neighbour.z][neighbour.x][neighbour.y])
{
newTilesToExploreFrom.push_back(neighbour);
potentialTiles[neighbour.z][neighbour.x][neighbour.y] = false;
}
});
}
for(const int3 & tile : newTilesToExploreFrom)
{
scanTile(tile);
}
std::swap(tilesToExploreFrom, newTilesToExploreFrom);
}
return !bestGoal->invalid();
}
void ExplorationHelper::scanTile(const int3 & tile)
{
if(tile == ourPos
|| !ai->cb->getTile(tile, false)
|| !ai->pathfinder->isTileAccessible(hero, tile)) //shouldn't happen, but it does
return;
int tilesDiscovered = howManyTilesWillBeDiscovered(tile);
if(!tilesDiscovered)
return;
auto paths = ai->pathfinder->getPathInfo(tile);
auto waysToVisit = CaptureObjectsBehavior::getVisitGoals(paths, ai, ai->cb->getTopObj(tile));
for(int i = 0; i != paths.size(); i++)
{
auto & path = paths[i];
auto goal = waysToVisit[i];
if(path.exchangeCount > 1 || path.targetHero != hero || path.movementCost() <= 0.0 || goal->invalid())
continue;
float ourValue = (float)tilesDiscovered * tilesDiscovered / path.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, path.heroArmy, path.getTotalDanger(), ai->settings->getSafeAttackRatio()))
{
bestGoal = goal;
bestValue = ourValue;
bestTile = tile;
bestTilesDiscovered = tilesDiscovered;
}
}
}
}
int ExplorationHelper::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 ExplorationHelper::hasReachableNeighbor(const int3 & pos) const
{
for(const int3 & dir : int3::getDirs())
{
int3 tile = pos + dir;
if(cbp->isInTheMap(tile))
{
auto isAccessible = useCPathfinderAccessibility
? ai->cb->getPathsInfo(hero)->getPathInfo(tile)->reachable()
: ai->pathfinder->isTileAccessible(hero, tile);
if(isAccessible)
return true;
}
}
return false;
}
}