diff --git a/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp b/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp index 74b6de54e..cab2707f3 100644 --- a/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp +++ b/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp @@ -15,233 +15,15 @@ #include "../Goals/Composition.h" #include "../Goals/ExecuteHeroChain.h" #include "../Markers/ExplorationPoint.h" -#include "CaptureObjectsBehavior.h" #include "../Goals/CaptureObject.h" -#include "../../../lib/CPlayerState.h" +#include "../Goals/ExploreNeighbourTile.h" +#include "../Helpers/ExplorationHelper.h" namespace NKAI { using namespace Goals; -struct ExplorationHelper -{ - const CGHeroInstance * hero; - int sightRadius; - float bestValue; - TSubgoal bestGoal; - int3 bestTile; - int bestTilesDiscovered; - const Nullkiller * ai; - CCallback * cbp; - const TeamState * ts; - int3 ourPos; - bool allowDeadEndCancellation; - - ExplorationHelper(const CGHeroInstance * hero, const Nullkiller * ai) - :ai(ai), cbp(ai->cb.get()), hero(hero) - { - ts = cbp->getPlayerTeam(ai->playerID); - sightRadius = hero->getSightRadius(); - bestGoal = sptr(Goals::Invalid()); - bestValue = 0; - bestTilesDiscovered = 0; - ourPos = hero->visitablePos(); - allowDeadEndCancellation = true; - } - - TSubgoal makeComposition() const - { - Composition c; - c.addNext(ExplorationPoint(bestTile, bestTilesDiscovered)); - c.addNext(bestGoal); - return sptr(c); - } - - 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->getNameTranslated()); - - 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->getNameTranslated()); - - for(const int3 & tile : from) - { - scanTile(tile); - } - } - - void scanTile(const int3 & tile) - { - if(tile == ourPos - || !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())) - { - bestGoal = goal; - bestValue = ourValue; - bestTile = tile; - bestTilesDiscovered = tilesDiscovered; - } - } - } - } - - 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(const int3 & dir : int3::getDirs()) - { - int3 tile = pos + dir; - if(cbp->isInTheMap(tile)) - { - auto isAccessible = ai->pathfinder->isTileAccessible(hero, tile); - - if(isAccessible) - return true; - } - } - - return false; - } -}; - std::string ExplorationBehavior::toString() const { return "Explore"; @@ -301,17 +83,13 @@ Goals::TGoalVec ExplorationBehavior::decompose(const Nullkiller * ai) const { ExplorationHelper scanResult(hero, ai); - scanResult.scanSector(1); - - if(!scanResult.bestGoal->invalid()) + if(scanResult.scanSector(1)) { tasks.push_back(scanResult.makeComposition()); continue; } - scanResult.scanSector(15); - - if(!scanResult.bestGoal->invalid()) + if(scanResult.scanSector(15)) { tasks.push_back(scanResult.makeComposition()); continue; @@ -319,9 +97,7 @@ Goals::TGoalVec ExplorationBehavior::decompose(const Nullkiller * ai) const if(ai->getScanDepth() == ScanDepth::ALL_FULL) { - scanResult.scanMap(); - - if(!scanResult.bestGoal->invalid()) + if(scanResult.scanMap()) { tasks.push_back(scanResult.makeComposition()); } diff --git a/AI/Nullkiller/CMakeLists.txt b/AI/Nullkiller/CMakeLists.txt index 80dc073d0..8ea2634b4 100644 --- a/AI/Nullkiller/CMakeLists.txt +++ b/AI/Nullkiller/CMakeLists.txt @@ -40,6 +40,7 @@ set(Nullkiller_SRCS Goals/ExchangeSwapTownHeroes.cpp Goals/CompleteQuest.cpp Goals/StayAtTown.cpp + Goals/ExploreNeighbourTile.cpp Markers/ArmyUpgrade.cpp Markers/HeroExchange.cpp Markers/UnlockCluster.cpp @@ -62,6 +63,7 @@ set(Nullkiller_SRCS Behaviors/StayAtTownBehavior.cpp Behaviors/ExplorationBehavior.cpp Helpers/ArmyFormation.cpp + Helpers/ExplorationHelper.cpp AIGateway.cpp ) @@ -113,6 +115,7 @@ set(Nullkiller_HEADERS Goals/CompleteQuest.h Goals/Goals.h Goals/StayAtTown.h + Goals/ExploreNeighbourTile.h Markers/ArmyUpgrade.h Markers/HeroExchange.h Markers/UnlockCluster.h @@ -135,6 +138,7 @@ set(Nullkiller_HEADERS Behaviors/StayAtTownBehavior.h Behaviors/ExplorationBehavior.h Helpers/ArmyFormation.h + Helpers/ExplorationHelper.h AIGateway.h ) diff --git a/AI/Nullkiller/Goals/AbstractGoal.h b/AI/Nullkiller/Goals/AbstractGoal.h index e715bc2d3..b191f96a5 100644 --- a/AI/Nullkiller/Goals/AbstractGoal.h +++ b/AI/Nullkiller/Goals/AbstractGoal.h @@ -75,7 +75,8 @@ namespace Goals STAY_AT_TOWN_BEHAVIOR, STAY_AT_TOWN, EXPLORATION_BEHAVIOR, - EXPLORATION_POINT + EXPLORATION_POINT, + EXPLORE_NEIGHBOUR_TILE }; class DLL_EXPORT TSubgoal : public std::shared_ptr diff --git a/AI/Nullkiller/Goals/ExploreNeighbourTile.cpp b/AI/Nullkiller/Goals/ExploreNeighbourTile.cpp new file mode 100644 index 000000000..6459a71fe --- /dev/null +++ b/AI/Nullkiller/Goals/ExploreNeighbourTile.cpp @@ -0,0 +1,69 @@ +/* +* ExploreNeighbourTile.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 "ExploreNeighbourTile.h" +#include "../AIGateway.h" +#include "../AIUtility.h" +#include "../Helpers/ExplorationHelper.h" + + +namespace NKAI +{ + +using namespace Goals; + +bool ExploreNeighbourTile::operator==(const ExploreNeighbourTile & other) const +{ + return false; +} + +void ExploreNeighbourTile::accept(AIGateway * ai) +{ + ExplorationHelper h(hero, ai->nullkiller.get()); + + for(int i = 0; i < tilesToExplore && hero->movementPointsRemaining() > 0; i++) + { + int3 pos = hero->visitablePos(); + float value = 0; + int3 target = int3(-1); + foreach_neighbour(pos, [&](int3 tile) + { + auto pathInfo = ai->myCb->getPathsInfo(hero)->getPathInfo(tile); + + if(pathInfo->turns > 0) + return; + + if(pathInfo->accessible == EPathAccessibility::ACCESSIBLE) + { + float newValue = h.howManyTilesWillBeDiscovered(tile); + + newValue /= std::min(0.1f, pathInfo->getCost()); + + if(newValue > value) + { + value = newValue; + target = tile; + } + } + }); + + if(!target.valid() || !ai->moveHeroToTile(target, hero)) + { + return; + } + } +} + +std::string ExploreNeighbourTile::toString() const +{ + return "Explore neighbour tiles by " + hero->getNameTranslated(); +} + +} diff --git a/AI/Nullkiller/Goals/ExploreNeighbourTile.h b/AI/Nullkiller/Goals/ExploreNeighbourTile.h new file mode 100644 index 000000000..9a4dc0ea7 --- /dev/null +++ b/AI/Nullkiller/Goals/ExploreNeighbourTile.h @@ -0,0 +1,45 @@ +/* +* ExploreNeighbourTile.h, 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 +* +*/ +#pragma once + +#include "CGoal.h" + +namespace NKAI +{ + +class AIGateway; +class FuzzyHelper; + +namespace Goals +{ + class DLL_EXPORT ExploreNeighbourTile : public ElementarGoal + { + private: + int tilesToExplore; + + public: + ExploreNeighbourTile(const CGHeroInstance * hero, int amount) + : ElementarGoal(Goals::EXPLORE_NEIGHBOUR_TILE) + { + tilesToExplore = amount; + sethero(hero); + } + + bool operator==(const ExploreNeighbourTile & other) const override; + + void accept(AIGateway * ai) override; + std::string toString() const override; + + private: + //TSubgoal decomposeSingle() const override; + }; +} + +} diff --git a/AI/Nullkiller/Helpers/ExplorationHelper.cpp b/AI/Nullkiller/Helpers/ExplorationHelper.cpp new file mode 100644 index 000000000..ce2bf62ca --- /dev/null +++ b/AI/Nullkiller/Helpers/ExplorationHelper.cpp @@ -0,0 +1,235 @@ +/* +* 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) + :ai(ai), cbp(ai->cb.get()), hero(hero) +{ + 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 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->getNameTranslated()); + + for(const int3 & tile : from) + { + scanTile(tile); + } + + if(!bestGoal->invalid()) + { + return false; + } + + 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->getNameTranslated()); + + for(const int3 & tile : from) + { + scanTile(tile); + } + + 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())) + { + bestGoal = goal; + bestValue = ourValue; + bestTile = tile; + bestTilesDiscovered = tilesDiscovered; + } + } + } +} + +void ExplorationHelper::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 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 = ai->pathfinder->isTileAccessible(hero, tile); + + if(isAccessible) + return true; + } + } + + return false; +} + +} diff --git a/AI/Nullkiller/Helpers/ExplorationHelper.h b/AI/Nullkiller/Helpers/ExplorationHelper.h new file mode 100644 index 000000000..bee6e2807 --- /dev/null +++ b/AI/Nullkiller/Helpers/ExplorationHelper.h @@ -0,0 +1,51 @@ +/* +* ExplorationHelper.h, 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 +* +*/ +#pragma once + +#include "../AIUtility.h" + +#include "../../../lib/GameConstants.h" +#include "../../../lib/VCMI_Lib.h" +#include "../../../lib/CTownHandler.h" +#include "../../../lib/CBuildingHandler.h" +#include "../Goals/AbstractGoal.h" + +namespace NKAI +{ + +class ExplorationHelper +{ +private: + const CGHeroInstance * hero; + int sightRadius; + float bestValue; + Goals::TSubgoal bestGoal; + int3 bestTile; + int bestTilesDiscovered; + const Nullkiller * ai; + CCallback * cbp; + const TeamState * ts; + int3 ourPos; + bool allowDeadEndCancellation; + +public: + ExplorationHelper(const CGHeroInstance * hero, const Nullkiller * ai); + Goals::TSubgoal makeComposition() const; + bool scanSector(int scanRadius); + bool scanMap(); + int howManyTilesWillBeDiscovered(const int3 & pos) const; + +private: + void scanTile(const int3 & tile); + bool hasReachableNeighbor(const int3 & pos) const; + void getVisibleNeighbours(const std::vector & tiles, std::vector & out) const; +}; + +}