2018-12-01 10:30:37 +02:00
|
|
|
/*
|
|
|
|
* 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/mapping/CMap.h" //for victory conditions
|
|
|
|
#include "../../../lib/CPathfinder.h"
|
|
|
|
#include "../../../lib/StringConstants.h"
|
2018-12-29 15:55:20 +02:00
|
|
|
#include "../../../lib/CPlayerState.h"
|
2018-12-01 10:30:37 +02:00
|
|
|
|
|
|
|
extern boost::thread_specific_ptr<CCallback> cb;
|
|
|
|
extern boost::thread_specific_ptr<VCAI> ai;
|
|
|
|
extern FuzzyHelper * fh;
|
|
|
|
|
|
|
|
using namespace Goals;
|
|
|
|
|
|
|
|
bool Explore::operator==(const Explore & other) const
|
|
|
|
{
|
2019-01-02 22:51:26 +02:00
|
|
|
return other.hero.h == hero.h && other.allowGatherArmy == allowGatherArmy;
|
2018-12-01 10:30:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
std::string Explore::completeMessage() const
|
|
|
|
{
|
|
|
|
return "Hero " + hero.get()->name + " completed exploration";
|
|
|
|
}
|
|
|
|
|
|
|
|
TSubgoal Explore::whatToDoToAchieve()
|
|
|
|
{
|
|
|
|
auto ret = fh->chooseSolution(getAllPossibleSubgoals());
|
|
|
|
if(hero) //use best step for this hero
|
|
|
|
{
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
if(ret->hero.get(true))
|
2018-12-20 23:42:31 +02:00
|
|
|
return sptr(Explore().sethero(ret->hero.h)); //choose this hero and then continue with him
|
2018-12-01 10:30:37 +02:00
|
|
|
else
|
|
|
|
return ret; //other solutions, like buying hero from tavern
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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->movement; //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?
|
|
|
|
{
|
2019-01-02 22:51:26 +02:00
|
|
|
auto waysToVisitObj = ai->ah->howToVisitObj(h, obj, allowGatherArmy);
|
2018-12-01 10:30:37 +02:00
|
|
|
|
|
|
|
vstd::concatenate(ret, waysToVisitObj);
|
|
|
|
}
|
|
|
|
|
2019-01-02 22:51:26 +02:00
|
|
|
TSubgoal goal = exploreNearestNeighbour(h);
|
2018-12-24 15:19:04 +02:00
|
|
|
|
2019-01-02 22:51:26 +02:00
|
|
|
if(!goal->invalid())
|
2018-12-01 10:30:37 +02:00
|
|
|
{
|
2019-01-02 22:51:26 +02:00
|
|
|
ret.push_back(goal);
|
2018-12-01 10:30:37 +02:00
|
|
|
}
|
2019-01-02 22:51:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if(ret.empty())
|
|
|
|
{
|
|
|
|
for(auto h : heroes)
|
2018-12-01 10:30:37 +02:00
|
|
|
{
|
2019-01-02 22:51:26 +02:00
|
|
|
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);
|
|
|
|
}
|
2018-12-01 10:30:37 +02:00
|
|
|
}
|
|
|
|
}
|
2018-12-24 15:19:04 +02:00
|
|
|
|
2018-12-01 10:30:37 +02:00
|
|
|
//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;
|
|
|
|
}
|
2018-12-24 15:19:04 +02:00
|
|
|
|
|
|
|
bool Explore::hasReachableNeighbor(const int3 &pos, HeroPtr hero, CCallback * cbp, VCAI * vcai) const
|
|
|
|
{
|
|
|
|
for(crint3 dir : int3::getDirs())
|
|
|
|
{
|
|
|
|
int3 tile = pos + dir;
|
|
|
|
if(cbp->isInTheMap(tile))
|
|
|
|
{
|
2018-12-29 15:55:20 +02:00
|
|
|
auto isAccessible = vcai->ah->isTileAccessible(hero, tile);
|
2018-12-24 15:19:04 +02:00
|
|
|
|
2018-12-29 15:55:20 +02:00
|
|
|
if(isAccessible)
|
|
|
|
return true;
|
2018-12-24 15:19:04 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
int Explore::howManyTilesWillBeDiscovered(
|
2019-01-07 23:40:09 +02:00
|
|
|
const int3 & pos,
|
|
|
|
int radious,
|
|
|
|
CCallback * cbp,
|
2018-12-29 15:55:20 +02:00
|
|
|
const TeamState * ts,
|
|
|
|
VCAI * aip,
|
|
|
|
HeroPtr h) const
|
2018-12-24 15:19:04 +02:00
|
|
|
{
|
|
|
|
int ret = 0;
|
|
|
|
for(int x = pos.x - radious; x <= pos.x + radious; x++)
|
|
|
|
{
|
|
|
|
for(int y = pos.y - radious; y <= pos.y + radious; y++)
|
|
|
|
{
|
|
|
|
int3 npos = int3(x, y, pos.z);
|
2019-01-07 23:40:09 +02:00
|
|
|
if(cbp->isInTheMap(npos)
|
|
|
|
&& pos.dist2d(npos) - 0.5 < radious
|
2018-12-29 15:55:20 +02:00
|
|
|
&& !ts->fogOfWarMap[npos.x][npos.y][npos.z]
|
|
|
|
&& hasReachableNeighbor(npos, h, cbp, aip))
|
2018-12-24 15:19:04 +02:00
|
|
|
{
|
|
|
|
ret++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
TSubgoal Explore::explorationBestNeighbour(int3 hpos, int radius, HeroPtr h) const
|
|
|
|
{
|
|
|
|
std::map<int3, int> dstToRevealedTiles;
|
|
|
|
VCAI * aip = ai.get();
|
|
|
|
CCallback * cbp = cb.get();
|
2018-12-29 15:55:20 +02:00
|
|
|
const TeamState * ts = cbp->getPlayerTeam(ai->playerID);
|
2018-12-24 15:19:04 +02:00
|
|
|
|
|
|
|
for(crint3 dir : int3::getDirs())
|
|
|
|
{
|
|
|
|
int3 tile = hpos + dir;
|
|
|
|
if(cb->isInTheMap(tile))
|
|
|
|
{
|
|
|
|
if(isBlockVisitObj(tile))
|
|
|
|
continue;
|
|
|
|
|
|
|
|
if(isSafeToVisit(h, tile) && ai->isAccessibleForHero(tile, h))
|
|
|
|
{
|
|
|
|
auto distance = hpos.dist2d(tile); // diagonal movement opens more tiles but spends more mp
|
2018-12-29 15:55:20 +02:00
|
|
|
int tilesDiscovered = howManyTilesWillBeDiscovered(tile, radius, cbp, ts, aip, h);
|
2018-12-24 15:19:04 +02:00
|
|
|
|
2019-01-15 05:00:00 +02:00
|
|
|
if(tilesDiscovered > 0)
|
|
|
|
dstToRevealedTiles[tile] = tilesDiscovered / distance;
|
2018-12-24 15:19:04 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(dstToRevealedTiles.empty()) //yes, it DID happen!
|
|
|
|
return sptr(Invalid());
|
|
|
|
|
2019-01-15 07:52:55 +02:00
|
|
|
auto paths = cb->getPathsInfo(h.get());
|
|
|
|
|
2018-12-24 15:19:04 +02:00
|
|
|
auto best = dstToRevealedTiles.begin();
|
|
|
|
for(auto i = dstToRevealedTiles.begin(); i != dstToRevealedTiles.end(); i++)
|
|
|
|
{
|
2019-01-15 07:52:55 +02:00
|
|
|
const CGPathNode * pn = paths->getPathInfo(i->first);
|
2018-12-24 15:19:04 +02:00
|
|
|
if(best->second < i->second && pn->reachable() && pn->accessible == CGPathNode::ACCESSIBLE)
|
|
|
|
best = i;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(best->second)
|
|
|
|
return sptr(VisitTile(best->first).sethero(h));
|
|
|
|
|
|
|
|
return sptr(Invalid());
|
|
|
|
}
|
|
|
|
|
|
|
|
TSubgoal Explore::explorationNewPoint(HeroPtr h) const
|
|
|
|
{
|
|
|
|
int radius = h->getSightRadius();
|
|
|
|
CCallback * cbp = cb.get();
|
|
|
|
VCAI *aip = ai.get();
|
2018-12-29 15:55:20 +02:00
|
|
|
const TeamState * ts = cbp->getPlayerTeam(aip->playerID);
|
2018-12-24 15:19:04 +02:00
|
|
|
|
2019-01-02 22:51:26 +02:00
|
|
|
int3 mapSize = cbp->getMapSize();
|
|
|
|
int perimiter = 2 * radius * (mapSize.x + mapSize.y);
|
2019-01-07 23:40:09 +02:00
|
|
|
|
2019-01-02 22:51:26 +02:00
|
|
|
std::vector<int3> from;
|
|
|
|
std::vector<int3> to;
|
|
|
|
|
|
|
|
from.reserve(perimiter);
|
|
|
|
to.reserve(perimiter);
|
2018-12-24 15:19:04 +02:00
|
|
|
|
|
|
|
foreach_tile_pos([&](const int3 & pos)
|
|
|
|
{
|
2019-01-02 22:51:26 +02:00
|
|
|
if(ts->fogOfWarMap[pos.x][pos.y][pos.z])
|
|
|
|
{
|
|
|
|
bool hasInvisibleNeighbor = false;
|
|
|
|
|
|
|
|
foreach_neighbour(cbp, pos, [&](CCallback * cbp, int3 neighbour)
|
|
|
|
{
|
|
|
|
if(!ts->fogOfWarMap[neighbour.x][neighbour.y][neighbour.z])
|
|
|
|
{
|
|
|
|
hasInvisibleNeighbor = true;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if(hasInvisibleNeighbor)
|
|
|
|
from.push_back(pos);
|
|
|
|
}
|
2018-12-24 15:19:04 +02:00
|
|
|
});
|
|
|
|
|
2019-01-02 22:51:26 +02:00
|
|
|
for(int i = 0; i < radius; i++)
|
|
|
|
{
|
|
|
|
getVisibleNeighbours(from, to, cbp, ts);
|
|
|
|
vstd::concatenate(from, to);
|
|
|
|
vstd::removeDuplicates(from);
|
|
|
|
}
|
|
|
|
|
|
|
|
return explorationScanRange(h, from);
|
|
|
|
}
|
|
|
|
|
|
|
|
TSubgoal Explore::explorationScanRange(HeroPtr h, std::vector<int3> & range) const
|
|
|
|
{
|
|
|
|
int radius = h->getSightRadius();
|
|
|
|
CCallback * cbp = cb.get();
|
|
|
|
VCAI *aip = ai.get();
|
|
|
|
const TeamState * ts = cbp->getPlayerTeam(aip->playerID);
|
|
|
|
|
2018-12-24 15:19:04 +02:00
|
|
|
float bestValue = 0; //discovered tile to node distance ratio
|
|
|
|
TSubgoal bestWay = sptr(Invalid());
|
|
|
|
int3 ourPos = h->convertPosition(h->pos, false);
|
|
|
|
|
2019-01-02 22:51:26 +02:00
|
|
|
for(const int3 & tile : range)
|
2018-12-24 15:19:04 +02:00
|
|
|
{
|
2019-01-02 22:51:26 +02:00
|
|
|
if(tile == ourPos) //shouldn't happen, but it does
|
|
|
|
continue;
|
|
|
|
|
|
|
|
int tilesDiscovered = howManyTilesWillBeDiscovered(tile, radius, cbp, ts, aip, h);
|
|
|
|
if(!tilesDiscovered)
|
|
|
|
continue;
|
2018-12-24 15:19:04 +02:00
|
|
|
|
2019-01-02 22:51:26 +02:00
|
|
|
auto waysToVisit = aip->ah->howToVisitTile(h, tile, allowGatherArmy);
|
|
|
|
for(auto goal : waysToVisit)
|
2018-12-24 15:19:04 +02:00
|
|
|
{
|
2019-01-15 07:52:55 +02:00
|
|
|
if(goal->evaluationContext.movementCost <= 0.0) // should not happen
|
2018-12-24 15:19:04 +02:00
|
|
|
continue;
|
2019-01-02 22:51:26 +02:00
|
|
|
|
|
|
|
float ourValue = (float)tilesDiscovered * tilesDiscovered / goal->evaluationContext.movementCost;
|
|
|
|
|
|
|
|
if(ourValue > bestValue) //avoid costly checks of tiles that don't reveal much
|
2018-12-24 15:19:04 +02:00
|
|
|
{
|
2019-01-02 22:51:26 +02:00
|
|
|
auto obj = cb->getTopObj(tile);
|
|
|
|
if(obj && obj->blockVisit && !isObjectRemovable(obj)) //we can't stand on that object
|
|
|
|
{
|
2018-12-24 15:19:04 +02:00
|
|
|
continue;
|
2019-01-02 22:51:26 +02:00
|
|
|
}
|
2018-12-24 15:19:04 +02:00
|
|
|
|
2019-01-02 22:51:26 +02:00
|
|
|
if(isSafeToVisit(h, tile))
|
2018-12-24 15:19:04 +02:00
|
|
|
{
|
2019-01-02 22:51:26 +02:00
|
|
|
bestWay = goal;
|
|
|
|
bestValue = ourValue;
|
2018-12-24 15:19:04 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-01-02 22:51:26 +02:00
|
|
|
}
|
2018-12-24 15:19:04 +02:00
|
|
|
|
2019-01-02 22:51:26 +02:00
|
|
|
if(!bestWay->invalid())
|
|
|
|
{
|
|
|
|
return bestWay;
|
2018-12-24 15:19:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return bestWay;
|
|
|
|
}
|
|
|
|
|
2019-01-02 22:51:26 +02:00
|
|
|
TSubgoal Explore::exploreNearestNeighbour(HeroPtr h) const
|
2018-12-24 15:19:04 +02:00
|
|
|
{
|
|
|
|
TimeCheck tc("where to explore");
|
|
|
|
int radius = h->getSightRadius();
|
|
|
|
int3 hpos = h->visitablePos();
|
|
|
|
|
2019-01-15 05:00:00 +02:00
|
|
|
//look for nearby objs -> visit them if they're close enough
|
2018-12-24 15:19:04 +02:00
|
|
|
const int DIST_LIMIT = 3;
|
2019-01-15 07:52:55 +02:00
|
|
|
const float COST_LIMIT = .2; //todo: fine tune
|
2018-12-24 15:19:04 +02:00
|
|
|
|
|
|
|
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))
|
|
|
|
{
|
2019-01-15 07:52:55 +02:00
|
|
|
if(ai->isGoodForVisit(obj, h, COST_LIMIT))
|
2018-12-24 15:19:04 +02:00
|
|
|
{
|
|
|
|
nearbyVisitableObjs.push_back(obj);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(nearbyVisitableObjs.size())
|
|
|
|
{
|
2019-01-02 22:51:26 +02:00
|
|
|
vstd::removeDuplicates(nearbyVisitableObjs); //one object may occupy multiple tiles
|
|
|
|
boost::sort(nearbyVisitableObjs, CDistanceSorter(h.get()));
|
|
|
|
|
2018-12-24 15:19:04 +02:00
|
|
|
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
|
2019-01-02 22:51:26 +02:00
|
|
|
return explorationBestNeighbour(hpos, radius, h);
|
2018-12-24 15:19:04 +02:00
|
|
|
}
|
|
|
|
|
2018-12-29 15:55:20 +02:00
|
|
|
void Explore::getVisibleNeighbours(const std::vector<int3> & tiles, std::vector<int3> & out, CCallback * cbp, const TeamState * ts) const
|
2018-12-24 15:19:04 +02:00
|
|
|
{
|
|
|
|
for(const int3 & tile : tiles)
|
|
|
|
{
|
2018-12-29 15:55:20 +02:00
|
|
|
foreach_neighbour(cbp, tile, [&](CCallback * cbp, int3 neighbour)
|
2018-12-24 15:19:04 +02:00
|
|
|
{
|
2018-12-29 15:55:20 +02:00
|
|
|
if(ts->fogOfWarMap[neighbour.x][neighbour.y][neighbour.z])
|
2018-12-24 15:19:04 +02:00
|
|
|
{
|
2019-01-02 22:51:26 +02:00
|
|
|
out.push_back(neighbour);
|
2018-12-24 15:19:04 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2019-01-07 23:40:09 +02:00
|
|
|
}
|