diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 559eaf6bb..9e4a3f58d 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -77,7 +77,6 @@ AIGateway::AIGateway() destinationTeleport = ObjectInstanceID(); destinationTeleportPos = int3(-1); nullkiller.reset(new Nullkiller()); - announcedCheatingProblem = false; } AIGateway::~AIGateway() @@ -378,7 +377,7 @@ void AIGateway::objectRemoved(const CGObjectInstance * obj, const PlayerColor & nullkiller->memory->removeFromMemory(obj); nullkiller->objectClusterizer->onObjectRemoved(obj->id); - if(nullkiller->baseGraph && nullkiller->settings->isObjectGraphAllowed()) + if(nullkiller->baseGraph && nullkiller->isObjectGraphAllowed()) { nullkiller->baseGraph->removeObject(obj); } @@ -829,13 +828,9 @@ void AIGateway::makeTurn() boost::shared_lock gsLock(CGameState::mutex); setThreadName("AIGateway::makeTurn"); - if(cb->getStartInfo()->extraOptionsInfo.cheatsAllowed) - cb->sendMessage("vcmieagles"); - else + if(nullkiller->isOpenMap()) { - if(!announcedCheatingProblem) - cb->sendMessage("Nullkiller AI currently requires the ability to cheat in order to function correctly! Please enable!"); - announcedCheatingProblem = true; + cb->sendMessage("vcmieagles"); } retrieveVisitableObjs(); diff --git a/AI/Nullkiller/AIGateway.h b/AI/Nullkiller/AIGateway.h index b04e76004..f9315b0ec 100644 --- a/AI/Nullkiller/AIGateway.h +++ b/AI/Nullkiller/AIGateway.h @@ -95,7 +95,7 @@ public: std::unique_ptr makingTurn; private: boost::mutex turnInterruptionMutex; - bool announcedCheatingProblem; + public: ObjectInstanceID selectedObject; diff --git a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp index 77b465610..7b9607390 100644 --- a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp +++ b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp @@ -381,7 +381,7 @@ void ObjectClusterizer::clusterizeObject( logAi->trace("Check object %s%s.", obj->getObjectName(), obj->visitablePos().toString()); #endif - if(ai->settings->isObjectGraphAllowed()) + if(ai->isObjectGraphAllowed()) { ai->pathfinder->calculateQuickPathsWithBlocker(pathCache, heroes, obj->visitablePos()); } diff --git a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp index c061faf13..e20753317 100644 --- a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp +++ b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp @@ -47,7 +47,11 @@ bool CaptureObjectsBehavior::operator==(const CaptureObjectsBehavior & other) co && vectorEquals(objectSubTypes, other.objectSubTypes); } -Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector & paths, const Nullkiller * nullkiller, const CGObjectInstance * objToVisit) +Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals( + const std::vector & paths, + const Nullkiller * nullkiller, + const CGObjectInstance * objToVisit, + bool force) { Goals::TGoalVec tasks; @@ -72,7 +76,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector continue; } - if(objToVisit && !shouldVisit(nullkiller, path.targetHero, objToVisit)) + if(objToVisit && !force && !shouldVisit(nullkiller, path.targetHero, objToVisit)) { #if NKAI_TRACE_LEVEL >= 2 logAi->trace("Ignore path. Hero %s should not visit obj %s", path.targetHero->getNameTranslated(), objToVisit->getObjectName()); @@ -200,12 +204,12 @@ void CaptureObjectsBehavior::decomposeObjects( logAi->trace("Checking object %s, %s", objToVisit->getObjectName(), objToVisit->visitablePos().toString()); #endif - nullkiller->pathfinder->calculatePathInfo(paths, objToVisit->visitablePos(), nullkiller->settings->isObjectGraphAllowed()); + nullkiller->pathfinder->calculatePathInfo(paths, objToVisit->visitablePos(), nullkiller->isObjectGraphAllowed()); #if NKAI_TRACE_LEVEL >= 1 logAi->trace("Found %d paths", paths.size()); #endif - vstd::concatenate(tasksLocal, getVisitGoals(paths, nullkiller, objToVisit)); + vstd::concatenate(tasksLocal, getVisitGoals(paths, nullkiller, objToVisit, specificObjects)); } std::lock_guard lock(sync); diff --git a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.h b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.h index b68815ea8..111732c60 100644 --- a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.h +++ b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.h @@ -68,7 +68,11 @@ namespace Goals bool operator==(const CaptureObjectsBehavior & other) const override; - static Goals::TGoalVec getVisitGoals(const std::vector & paths, const Nullkiller * nullkiller, const CGObjectInstance * objToVisit = nullptr); + static Goals::TGoalVec getVisitGoals( + const std::vector & paths, + const Nullkiller * nullkiller, + const CGObjectInstance * objToVisit = nullptr, + bool force = false); private: bool objectMatchesFilter(const CGObjectInstance * obj) const; diff --git a/AI/Nullkiller/Behaviors/ClusterBehavior.cpp b/AI/Nullkiller/Behaviors/ClusterBehavior.cpp index d6c173c08..75913798a 100644 --- a/AI/Nullkiller/Behaviors/ClusterBehavior.cpp +++ b/AI/Nullkiller/Behaviors/ClusterBehavior.cpp @@ -42,7 +42,7 @@ Goals::TGoalVec ClusterBehavior::decompose(const Nullkiller * ai) const Goals::TGoalVec ClusterBehavior::decomposeCluster(const Nullkiller * ai, std::shared_ptr cluster) const { auto center = cluster->calculateCenter(ai->cb.get()); - auto paths = ai->pathfinder->getPathInfo(center->visitablePos(), ai->settings->isObjectGraphAllowed()); + auto paths = ai->pathfinder->getPathInfo(center->visitablePos(), ai->isObjectGraphAllowed()); auto blockerPos = cluster->blocker->visitablePos(); std::vector blockerPaths; diff --git a/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp b/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp new file mode 100644 index 000000000..74b6de54e --- /dev/null +++ b/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp @@ -0,0 +1,334 @@ +/* +* ExplorationBehavior.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 "ExplorationBehavior.h" +#include "../AIGateway.h" +#include "../AIUtility.h" +#include "../Goals/Invalid.h" +#include "../Goals/Composition.h" +#include "../Goals/ExecuteHeroChain.h" +#include "../Markers/ExplorationPoint.h" +#include "CaptureObjectsBehavior.h" +#include "../Goals/CaptureObject.h" +#include "../../../lib/CPlayerState.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"; +} + +Goals::TGoalVec ExplorationBehavior::decompose(const Nullkiller * ai) const +{ + Goals::TGoalVec tasks; + + for(auto obj : ai->memory->visitableObjs) + { + if(!vstd::contains(ai->memory->alreadyVisited, obj)) + { + switch(obj->ID.num) + { + case Obj::REDWOOD_OBSERVATORY: + case Obj::PILLAR_OF_FIRE: + tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 200)).addNext(CaptureObject(obj)))); + break; + case Obj::MONOLITH_ONE_WAY_ENTRANCE: + case Obj::MONOLITH_TWO_WAY: + case Obj::SUBTERRANEAN_GATE: + auto tObj = dynamic_cast(obj); + if(TeleportChannel::IMPASSABLE != ai->memory->knownTeleportChannels[tObj->channel]->passability) + { + tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(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->memory->knownTeleportChannels[tObj->channel]->passability) + break; + for(auto exit : ai->memory->knownTeleportChannels[tObj->channel]->exits) + { + if(!cb->getObj(exit)) + { + // Always attempt to visit two-way teleports if one of channel exits is not visible + tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj)))); + break; + } + } + break; + } + } + } + + auto heroes = ai->cb->getHeroesInfo(); + + for(const CGHeroInstance * hero : heroes) + { + ExplorationHelper scanResult(hero, ai); + + scanResult.scanSector(1); + + if(!scanResult.bestGoal->invalid()) + { + tasks.push_back(scanResult.makeComposition()); + continue; + } + + scanResult.scanSector(15); + + if(!scanResult.bestGoal->invalid()) + { + tasks.push_back(scanResult.makeComposition()); + continue; + } + + if(ai->getScanDepth() == ScanDepth::ALL_FULL) + { + scanResult.scanMap(); + + if(!scanResult.bestGoal->invalid()) + { + tasks.push_back(scanResult.makeComposition()); + } + } + } + + return tasks; +} + +} diff --git a/AI/Nullkiller/Behaviors/ExplorationBehavior.h b/AI/Nullkiller/Behaviors/ExplorationBehavior.h new file mode 100644 index 000000000..87030f855 --- /dev/null +++ b/AI/Nullkiller/Behaviors/ExplorationBehavior.h @@ -0,0 +1,38 @@ +/* +* ExplorationBehavior.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 "lib/VCMI_Lib.h" +#include "../Goals/CGoal.h" +#include "../AIUtility.h" + +namespace NKAI +{ +namespace Goals +{ + class ExplorationBehavior : public CGoal + { + public: + ExplorationBehavior() + :CGoal(EXPLORATION_BEHAVIOR) + { + } + + TGoalVec decompose(const Nullkiller * ai) const override; + std::string toString() const override; + + bool operator==(const ExplorationBehavior & other) const override + { + return true; + } + }; +} + +} diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp index 89ad3a4bb..b9a675837 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp @@ -69,7 +69,7 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con logAi->trace("Checking ways to gaher army for hero %s, %s", hero->getObjectName(), pos.toString()); #endif - auto paths = ai->pathfinder->getPathInfo(pos, ai->settings->isObjectGraphAllowed()); + auto paths = ai->pathfinder->getPathInfo(pos, ai->isObjectGraphAllowed()); #if NKAI_TRACE_LEVEL >= 1 logAi->trace("Gather army found %d paths", paths.size()); @@ -231,7 +231,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT logAi->trace("Checking ways to upgrade army in town %s, %s", upgrader->getObjectName(), pos.toString()); #endif - auto paths = ai->pathfinder->getPathInfo(pos, ai->settings->isObjectGraphAllowed()); + auto paths = ai->pathfinder->getPathInfo(pos, ai->isObjectGraphAllowed()); auto goals = CaptureObjectsBehavior::getVisitGoals(paths, ai); std::vector> waysToVisitObj; diff --git a/AI/Nullkiller/CMakeLists.txt b/AI/Nullkiller/CMakeLists.txt index 93f94a351..80dc073d0 100644 --- a/AI/Nullkiller/CMakeLists.txt +++ b/AI/Nullkiller/CMakeLists.txt @@ -15,6 +15,8 @@ set(Nullkiller_SRCS Pathfinding/Rules/AIMovementToDestinationRule.cpp Pathfinding/Rules/AIPreviousNodeRule.cpp Pathfinding/ObjectGraph.cpp + Pathfinding/GraphPaths.cpp + Pathfinding/ObjectGraphCalculator.cpp AIUtility.cpp Analyzers/ArmyManager.cpp Analyzers/HeroManager.cpp @@ -42,6 +44,7 @@ set(Nullkiller_SRCS Markers/HeroExchange.cpp Markers/UnlockCluster.cpp Markers/DefendTown.cpp + Markers/ExplorationPoint.cpp Engine/Nullkiller.cpp Engine/DeepDecomposer.cpp Engine/PriorityEvaluator.cpp @@ -57,6 +60,7 @@ set(Nullkiller_SRCS Behaviors/GatherArmyBehavior.cpp Behaviors/ClusterBehavior.cpp Behaviors/StayAtTownBehavior.cpp + Behaviors/ExplorationBehavior.cpp Helpers/ArmyFormation.cpp AIGateway.cpp ) @@ -80,6 +84,8 @@ set(Nullkiller_HEADERS Pathfinding/Rules/AIMovementToDestinationRule.h Pathfinding/Rules/AIPreviousNodeRule.h Pathfinding/ObjectGraph.h + Pathfinding/GraphPaths.h + Pathfinding/ObjectGraphCalculator.h AIUtility.h pforeach.h Analyzers/ArmyManager.h @@ -111,6 +117,7 @@ set(Nullkiller_HEADERS Markers/HeroExchange.h Markers/UnlockCluster.h Markers/DefendTown.h + Markers/ExplorationPoint.h Engine/Nullkiller.h Engine/DeepDecomposer.h Engine/PriorityEvaluator.h @@ -126,6 +133,7 @@ set(Nullkiller_HEADERS Behaviors/GatherArmyBehavior.h Behaviors/ClusterBehavior.h Behaviors/StayAtTownBehavior.h + Behaviors/ExplorationBehavior.h Helpers/ArmyFormation.h AIGateway.h ) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index c7183924c..e8e58437e 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -19,8 +19,11 @@ #include "../Behaviors/GatherArmyBehavior.h" #include "../Behaviors/ClusterBehavior.h" #include "../Behaviors/StayAtTownBehavior.h" +#include "../Behaviors/ExplorationBehavior.h" #include "../Goals/Invalid.h" #include "../Goals/Composition.h" +#include "../../../lib/CPlayerState.h" +#include "../../lib/StartInfo.h" namespace NKAI { @@ -35,13 +38,45 @@ Nullkiller::Nullkiller() { memory = std::make_unique(); settings = std::make_unique(); + + useObjectGraph = settings->isObjectGraphAllowed(); + openMap = settings->isOpenMap() || useObjectGraph; +} + +bool canUseOpenMap(std::shared_ptr cb, PlayerColor playerID) +{ + if(!cb->getStartInfo()->extraOptionsInfo.cheatsAllowed) + { + return false; + } + + const TeamState * team = cb->getPlayerTeam(playerID); + + auto hasHumanInTeam = vstd::contains_if(team->players, [cb](PlayerColor teamMateID) -> bool + { + return cb->getPlayerState(teamMateID)->isHuman(); + }); + + if(hasHumanInTeam) + { + return false; + } + + return cb->getStartInfo()->difficulty >= 3; } void Nullkiller::init(std::shared_ptr cb, AIGateway * gateway) { this->cb = cb; this->gateway = gateway; - this->playerID = gateway->playerID; + + playerID = gateway->playerID; + + if(openMap && !canUseOpenMap(cb, playerID)) + { + useObjectGraph = false; + openMap = false; + } baseGraph.reset(); @@ -190,7 +225,7 @@ void Nullkiller::resetAiState() useHeroChain = true; objectClusterizer->reset(); - if(!baseGraph && settings->isObjectGraphAllowed()) + if(!baseGraph && isObjectGraphAllowed()) { baseGraph = std::make_unique(); baseGraph->updateGraph(this); @@ -237,12 +272,12 @@ void Nullkiller::updateAiState(int pass, bool fast) cfg.useHeroChain = useHeroChain; cfg.allowBypassObjects = true; - if(scanDepth == ScanDepth::SMALL || settings->isObjectGraphAllowed()) + if(scanDepth == ScanDepth::SMALL || isObjectGraphAllowed()) { cfg.mainTurnDistanceLimit = settings->getMainHeroTurnDistanceLimit(); } - if(scanDepth != ScanDepth::ALL_FULL || settings->isObjectGraphAllowed()) + if(scanDepth != ScanDepth::ALL_FULL || isObjectGraphAllowed()) { cfg.scoutTurnDistanceLimit =settings->getScoutHeroTurnDistanceLimit(); } @@ -251,7 +286,7 @@ void Nullkiller::updateAiState(int pass, bool fast) pathfinder->updatePaths(activeHeroes, cfg); - if(settings->isObjectGraphAllowed()) + if(isObjectGraphAllowed()) { pathfinder->updateGraphs( activeHeroes, @@ -354,6 +389,9 @@ void Nullkiller::makeTurn() decompose(bestTasks, sptr(GatherArmyBehavior()), MAX_DEPTH); decompose(bestTasks, sptr(StayAtTownBehavior()), MAX_DEPTH); + if(!isOpenMap()) + decompose(bestTasks, sptr(ExplorationBehavior()), MAX_DEPTH); + if(cb->getDate(Date::DAY) == 1 || heroManager->getHeroRoles().empty()) { decompose(bestTasks, sptr(StartupBehavior()), 1); diff --git a/AI/Nullkiller/Engine/Nullkiller.h b/AI/Nullkiller/Engine/Nullkiller.h index a21e81dd5..af05e354b 100644 --- a/AI/Nullkiller/Engine/Nullkiller.h +++ b/AI/Nullkiller/Engine/Nullkiller.h @@ -76,6 +76,8 @@ private: TResources lockedResources; bool useHeroChain; AIGateway * gateway; + bool openMap; + bool useObjectGraph; public: static std::unique_ptr baseGraph; @@ -116,6 +118,8 @@ public: void lockResources(const TResources & res); const TResources & getLockedResources() const { return lockedResources; } ScanDepth getScanDepth() const { return scanDepth; } + bool isOpenMap() const { return openMap; } + bool isObjectGraphAllowed() const { return useObjectGraph; } private: void resetAiState(); diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index e4fe53d9e..9f563ee32 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -735,6 +735,20 @@ public: } }; +class ExplorePointEvaluator : public IEvaluationContextBuilder +{ +public: + void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override + { + if(task->goalType != Goals::EXPLORATION_POINT) + return; + + int tilesDiscovered = task->value; + + evaluationContext.addNonCriticalStrategicalValue(0.03f * tilesDiscovered); + } +}; + class StayAtTownManaRecoveryEvaluator : public IEvaluationContextBuilder { public: @@ -1056,6 +1070,7 @@ PriorityEvaluator::PriorityEvaluator(const Nullkiller * ai) evaluationContextBuilders.push_back(std::make_shared()); evaluationContextBuilders.push_back(std::make_shared(ai)); evaluationContextBuilders.push_back(std::make_shared()); + evaluationContextBuilders.push_back(std::make_shared()); } EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal) const diff --git a/AI/Nullkiller/Engine/Settings.cpp b/AI/Nullkiller/Engine/Settings.cpp index d10bb0048..db4e3f455 100644 --- a/AI/Nullkiller/Engine/Settings.cpp +++ b/AI/Nullkiller/Engine/Settings.cpp @@ -28,8 +28,9 @@ namespace NKAI scoutHeroTurnDistanceLimit(5), maxGoldPressure(0.3f), maxpass(10), - allowObjectGraph(false), - useTroopsFromGarrisons(false) + allowObjectGraph(true), + useTroopsFromGarrisons(false), + openMap(true) { JsonNode node = JsonUtils::assembleFromFiles("config/ai/nkai/nkai-settings"); @@ -63,6 +64,11 @@ namespace NKAI allowObjectGraph = node.Struct()["allowObjectGraph"].Bool(); } + if(!node.Struct()["openMap"].isNull()) + { + openMap = node.Struct()["openMap"].Bool(); + } + if(!node.Struct()["useTroopsFromGarrisons"].isNull()) { useTroopsFromGarrisons = node.Struct()["useTroopsFromGarrisons"].Bool(); diff --git a/AI/Nullkiller/Engine/Settings.h b/AI/Nullkiller/Engine/Settings.h index 381493889..775f7f399 100644 --- a/AI/Nullkiller/Engine/Settings.h +++ b/AI/Nullkiller/Engine/Settings.h @@ -28,6 +28,7 @@ namespace NKAI float maxGoldPressure; bool allowObjectGraph; bool useTroopsFromGarrisons; + bool openMap; public: Settings(); @@ -39,5 +40,6 @@ namespace NKAI int getScoutHeroTurnDistanceLimit() const { return scoutHeroTurnDistanceLimit; } bool isObjectGraphAllowed() const { return allowObjectGraph; } bool isGarrisonTroopsUsageAllowed() const { return useTroopsFromGarrisons; } + bool isOpenMap() const { return openMap; } }; } diff --git a/AI/Nullkiller/Goals/AbstractGoal.h b/AI/Nullkiller/Goals/AbstractGoal.h index 18eee0265..e715bc2d3 100644 --- a/AI/Nullkiller/Goals/AbstractGoal.h +++ b/AI/Nullkiller/Goals/AbstractGoal.h @@ -73,7 +73,9 @@ namespace Goals CAPTURE_OBJECT, SAVE_RESOURCES, STAY_AT_TOWN_BEHAVIOR, - STAY_AT_TOWN + STAY_AT_TOWN, + EXPLORATION_BEHAVIOR, + EXPLORATION_POINT }; class DLL_EXPORT TSubgoal : public std::shared_ptr diff --git a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp index 54a2005ef..d7b1025da 100644 --- a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp +++ b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp @@ -148,7 +148,7 @@ void ExecuteHeroChain::accept(AIGateway * ai) return; } } - else if(i > 0 && ai->nullkiller->settings->isObjectGraphAllowed()) + else if(i > 0 && ai->nullkiller->isObjectGraphAllowed()) { auto chainMask = i < chainPath.nodes.size() - 1 ? chainPath.nodes[i + 1].chainMask : node->chainMask; diff --git a/AI/Nullkiller/Markers/ExplorationPoint.cpp b/AI/Nullkiller/Markers/ExplorationPoint.cpp new file mode 100644 index 000000000..916ac5b7f --- /dev/null +++ b/AI/Nullkiller/Markers/ExplorationPoint.cpp @@ -0,0 +1,32 @@ +/* +* HeroExchange.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 "ExplorationPoint.h" +#include "../AIGateway.h" +#include "../Engine/Nullkiller.h" +#include "../AIUtility.h" +#include "../Analyzers/ArmyManager.h" + +namespace NKAI +{ + +using namespace Goals; + +bool ExplorationPoint::operator==(const ExplorationPoint & other) const +{ + return false; +} + +std::string ExplorationPoint::toString() const +{ + return "Explore " +tile.toString() + " for " + std::to_string(value) + " tiles"; +} + +} diff --git a/AI/Nullkiller/Markers/ExplorationPoint.h b/AI/Nullkiller/Markers/ExplorationPoint.h new file mode 100644 index 000000000..46469b8ec --- /dev/null +++ b/AI/Nullkiller/Markers/ExplorationPoint.h @@ -0,0 +1,35 @@ +/* +* ExplorationPoint.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 "../Goals/CGoal.h" +#include "../Pathfinding/AINodeStorage.h" + +namespace NKAI +{ +namespace Goals +{ + class DLL_EXPORT ExplorationPoint : public CGoal + { + public: + ExplorationPoint(int3 tile, int tilesToReviel) + : CGoal(Goals::EXPLORATION_POINT) + { + settile(tile); + setvalue(tilesToReviel); + } + + bool operator==(const ExplorationPoint & other) const override; + + std::string toString() const override; + }; +} + +} diff --git a/AI/Nullkiller/Pathfinding/AIPathfinder.h b/AI/Nullkiller/Pathfinding/AIPathfinder.h index 15a277cad..cd490cda4 100644 --- a/AI/Nullkiller/Pathfinding/AIPathfinder.h +++ b/AI/Nullkiller/Pathfinding/AIPathfinder.h @@ -12,6 +12,7 @@ #include "AINodeStorage.h" #include "ObjectGraph.h" +#include "GraphPaths.h" #include "../AIUtility.h" namespace NKAI diff --git a/AI/Nullkiller/Pathfinding/GraphPaths.cpp b/AI/Nullkiller/Pathfinding/GraphPaths.cpp new file mode 100644 index 000000000..9c27c738c --- /dev/null +++ b/AI/Nullkiller/Pathfinding/GraphPaths.cpp @@ -0,0 +1,393 @@ +/* +* GraphPaths.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 "GraphPaths.h" +#include "AIPathfinderConfig.h" +#include "../../../lib/CRandomGenerator.h" +#include "../../../CCallback.h" +#include "../../../lib/mapping/CMap.h" +#include "../Engine/Nullkiller.h" +#include "../../../lib/logging/VisualLogger.h" +#include "Actions/QuestAction.h" +#include "../pforeach.h" +#include "Actions/BoatActions.h" + +namespace NKAI +{ + +bool GraphNodeComparer::operator()(const GraphPathNodePointer & lhs, const GraphPathNodePointer & rhs) const +{ + return pathNodes.at(lhs.coord)[lhs.nodeType].cost > pathNodes.at(rhs.coord)[rhs.nodeType].cost; +} + +GraphPaths::GraphPaths() + : visualKey(""), graph(), pathNodes() +{ +} + +std::shared_ptr getCompositeAction( + const Nullkiller * ai, + std::shared_ptr linkActionFactory, + std::shared_ptr transitionAction) +{ + if(!linkActionFactory) + return transitionAction; + + auto linkAction = linkActionFactory->create(ai); + + if(!transitionAction) + return linkAction; + + std::vector> actionsArray = { + transitionAction, + linkAction + }; + + return std::make_shared(actionsArray); +} + +void GraphPaths::calculatePaths(const CGHeroInstance * targetHero, const Nullkiller * ai, uint8_t scanDepth) +{ + graph.copyFrom(*ai->baseGraph); + graph.connectHeroes(ai); + + visualKey = std::to_string(ai->playerID) + ":" + targetHero->getNameTranslated(); + pathNodes.clear(); + + GraphNodeComparer cmp(pathNodes); + GraphPathNode::TFibHeap pq(cmp); + + pathNodes[targetHero->visitablePos()][GrapthPathNodeType::NORMAL].cost = 0; + pq.emplace(GraphPathNodePointer(targetHero->visitablePos(), GrapthPathNodeType::NORMAL)); + + while(!pq.empty()) + { + GraphPathNodePointer pos = pq.top(); + pq.pop(); + + auto & node = getOrCreateNode(pos); + std::shared_ptr transitionAction; + + if(node.obj) + { + if(node.obj->ID == Obj::QUEST_GUARD + || node.obj->ID == Obj::BORDERGUARD + || node.obj->ID == Obj::BORDER_GATE) + { + auto questObj = dynamic_cast(node.obj); + auto questInfo = QuestInfo(questObj->quest, node.obj, pos.coord); + + if(node.obj->ID == Obj::QUEST_GUARD + && questObj->quest->mission == Rewardable::Limiter{} + && questObj->quest->killTarget == ObjectInstanceID::NONE) + { + continue; + } + + auto questAction = std::make_shared(questInfo); + + if(!questAction->canAct(ai, targetHero)) + { + transitionAction = questAction; + } + } + } + + node.isInQueue = false; + + graph.iterateConnections(pos.coord, [this, ai, &pos, &node, &transitionAction, &pq, scanDepth](int3 target, const ObjectLink & o) + { + auto compositeAction = getCompositeAction(ai, o.specialAction, transitionAction); + auto targetNodeType = o.danger || compositeAction ? GrapthPathNodeType::BATTLE : pos.nodeType; + auto targetPointer = GraphPathNodePointer(target, targetNodeType); + auto & targetNode = getOrCreateNode(targetPointer); + + if(targetNode.tryUpdate(pos, node, o)) + { + if(targetNode.cost > scanDepth) + { + return; + } + + targetNode.specialAction = compositeAction; + + auto targetGraphNode = graph.getNode(target); + + if(targetGraphNode.objID.hasValue()) + { + targetNode.obj = ai->cb->getObj(targetGraphNode.objID, false); + + if(targetNode.obj && targetNode.obj->ID == Obj::HERO) + return; + } + + if(targetNode.isInQueue) + { + pq.increase(targetNode.handle); + } + else + { + targetNode.handle = pq.emplace(targetPointer); + targetNode.isInQueue = true; + } + } + }); + } +} + +void GraphPaths::dumpToLog() const +{ + logVisual->updateWithLock(visualKey, [&](IVisualLogBuilder & logBuilder) + { + for(auto & tile : pathNodes) + { + for(auto & node : tile.second) + { + if(!node.previous.valid()) + continue; + + if(NKAI_GRAPH_TRACE_LEVEL >= 2) + { + logAi->trace( + "%s -> %s: %f !%d", + node.previous.coord.toString(), + tile.first.toString(), + node.cost, + node.danger); + } + + logBuilder.addLine(node.previous.coord, tile.first); + } + } + }); +} + +bool GraphPathNode::tryUpdate(const GraphPathNodePointer & pos, const GraphPathNode & prev, const ObjectLink & link) +{ + auto newCost = prev.cost + link.cost; + + if(newCost < cost) + { + previous = pos; + danger = prev.danger + link.danger; + cost = newCost; + + return true; + } + + return false; +} + +void GraphPaths::addChainInfo(std::vector & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const +{ + auto nodes = pathNodes.find(tile); + + if(nodes == pathNodes.end()) + return; + + for(auto & node : nodes->second) + { + if(!node.reachable()) + continue; + + std::vector tilesToPass; + + uint64_t danger = node.danger; + float cost = node.cost; + bool allowBattle = false; + + auto current = GraphPathNodePointer(nodes->first, node.nodeType); + + while(true) + { + auto currentTile = pathNodes.find(current.coord); + + if(currentTile == pathNodes.end()) + break; + + auto currentNode = currentTile->second[current.nodeType]; + + if(!currentNode.previous.valid()) + break; + + allowBattle = allowBattle || currentNode.nodeType == GrapthPathNodeType::BATTLE; + vstd::amax(danger, currentNode.danger); + vstd::amax(cost, currentNode.cost); + + tilesToPass.push_back(current); + + if(currentNode.cost < 2.0f) + break; + + current = currentNode.previous; + } + + if(tilesToPass.empty()) + continue; + + auto entryPaths = ai->pathfinder->getPathInfo(tilesToPass.back().coord); + + for(auto & path : entryPaths) + { + if(path.targetHero != hero) + continue; + + for(auto graphTile = tilesToPass.rbegin(); graphTile != tilesToPass.rend(); graphTile++) + { + AIPathNodeInfo n; + + n.coord = graphTile->coord; + n.cost = cost; + n.turns = static_cast(cost) + 1; // just in case lets select worst scenario + n.danger = danger; + n.targetHero = hero; + n.parentIndex = -1; + n.specialAction = getNode(*graphTile).specialAction; + + if(n.specialAction) + { + n.actionIsBlocked = !n.specialAction->canAct(ai, n); + } + + for(auto & node : path.nodes) + { + node.parentIndex++; + } + + path.nodes.insert(path.nodes.begin(), n); + } + + path.armyLoss += ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), danger); + path.targetObjectDanger = ai->pathfinder->getStorage()->evaluateDanger(tile, path.targetHero, !allowBattle); + path.targetObjectArmyLoss = ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), path.targetObjectDanger); + + paths.push_back(path); + } + } +} + +void GraphPaths::quickAddChainInfoWithBlocker(std::vector & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const +{ + auto nodes = pathNodes.find(tile); + + if(nodes == pathNodes.end()) + return; + + for(auto & targetNode : nodes->second) + { + if(!targetNode.reachable()) + continue; + + std::vector tilesToPass; + + uint64_t danger = targetNode.danger; + float cost = targetNode.cost; + bool allowBattle = false; + + auto current = GraphPathNodePointer(nodes->first, targetNode.nodeType); + + while(true) + { + auto currentTile = pathNodes.find(current.coord); + + if(currentTile == pathNodes.end()) + break; + + auto currentNode = currentTile->second[current.nodeType]; + + allowBattle = allowBattle || currentNode.nodeType == GrapthPathNodeType::BATTLE; + vstd::amax(danger, currentNode.danger); + vstd::amax(cost, currentNode.cost); + + tilesToPass.push_back(current); + + if(currentNode.cost < 2.0f) + break; + + current = currentNode.previous; + } + + if(tilesToPass.empty()) + continue; + + auto entryPaths = ai->pathfinder->getPathInfo(tilesToPass.back().coord); + + for(auto & entryPath : entryPaths) + { + if(entryPath.targetHero != hero) + continue; + + auto & path = paths.emplace_back(); + + path.targetHero = entryPath.targetHero; + path.heroArmy = entryPath.heroArmy; + path.exchangeCount = entryPath.exchangeCount; + path.armyLoss = entryPath.armyLoss + ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), danger); + path.targetObjectDanger = ai->pathfinder->getStorage()->evaluateDanger(tile, path.targetHero, !allowBattle); + path.targetObjectArmyLoss = ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), path.targetObjectDanger); + + AIPathNodeInfo n; + + n.targetHero = hero; + n.parentIndex = -1; + + // final node + n.coord = tile; + n.cost = targetNode.cost; + n.danger = targetNode.danger; + n.parentIndex = path.nodes.size(); + path.nodes.push_back(n); + + for(auto entryNode = entryPath.nodes.rbegin(); entryNode != entryPath.nodes.rend(); entryNode++) + { + auto blocker = ai->objectClusterizer->getBlocker(*entryNode); + + if(blocker) + { + // blocker node + path.nodes.push_back(*entryNode); + path.nodes.back().parentIndex = path.nodes.size() - 1; + break; + } + } + + if(path.nodes.size() > 1) + continue; + + for(auto graphTile = tilesToPass.rbegin(); graphTile != tilesToPass.rend(); graphTile++) + { + auto & node = getNode(*graphTile); + + n.coord = graphTile->coord; + n.cost = node.cost; + n.turns = static_cast(node.cost); + n.danger = node.danger; + n.specialAction = node.specialAction; + n.parentIndex = path.nodes.size(); + + if(n.specialAction) + { + n.actionIsBlocked = !n.specialAction->canAct(ai, n); + } + + auto blocker = ai->objectClusterizer->getBlocker(n); + + if(!blocker) + continue; + + // blocker node + path.nodes.push_back(n); + break; + } + } + } +} + +} diff --git a/AI/Nullkiller/Pathfinding/GraphPaths.h b/AI/Nullkiller/Pathfinding/GraphPaths.h new file mode 100644 index 000000000..83167eabe --- /dev/null +++ b/AI/Nullkiller/Pathfinding/GraphPaths.h @@ -0,0 +1,118 @@ +/* +* GraphPaths.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 "ObjectGraph.h" + +namespace NKAI +{ + +class Nullkiller; + +struct GraphPathNode; + +enum GrapthPathNodeType +{ + NORMAL, + + BATTLE, + + LAST +}; + +struct GraphPathNodePointer +{ + int3 coord = int3(-1); + GrapthPathNodeType nodeType = GrapthPathNodeType::NORMAL; + + GraphPathNodePointer() = default; + + GraphPathNodePointer(int3 coord, GrapthPathNodeType type) + :coord(coord), nodeType(type) + { } + + bool valid() const + { + return coord.valid(); + } +}; + +using GraphNodeStorage = std::unordered_map; + +class GraphNodeComparer +{ + const GraphNodeStorage & pathNodes; + +public: + GraphNodeComparer(const GraphNodeStorage & pathNodes) + :pathNodes(pathNodes) + { + } + + bool operator()(const GraphPathNodePointer & lhs, const GraphPathNodePointer & rhs) const; +}; + +struct GraphPathNode +{ + const float BAD_COST = 100000; + + GrapthPathNodeType nodeType = GrapthPathNodeType::NORMAL; + GraphPathNodePointer previous; + float cost = BAD_COST; + uint64_t danger = 0; + const CGObjectInstance * obj = nullptr; + std::shared_ptr specialAction; + + using TFibHeap = boost::heap::fibonacci_heap>; + + TFibHeap::handle_type handle; + bool isInQueue = false; + + bool reachable() const + { + return cost < BAD_COST; + } + + bool tryUpdate(const GraphPathNodePointer & pos, const GraphPathNode & prev, const ObjectLink & link); +}; + +class GraphPaths +{ + ObjectGraph graph; + GraphNodeStorage pathNodes; + std::string visualKey; + +public: + GraphPaths(); + void calculatePaths(const CGHeroInstance * targetHero, const Nullkiller * ai, uint8_t scanDepth); + void addChainInfo(std::vector & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const; + void quickAddChainInfoWithBlocker(std::vector & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const; + void dumpToLog() const; + +private: + GraphPathNode & getOrCreateNode(const GraphPathNodePointer & pos) + { + auto & node = pathNodes[pos.coord][pos.nodeType]; + + node.nodeType = pos.nodeType; + + return node; + } + + const GraphPathNode & getNode(const GraphPathNodePointer & pos) const + { + auto & node = pathNodes.at(pos.coord)[pos.nodeType]; + + return node; + } +}; + +} diff --git a/AI/Nullkiller/Pathfinding/ObjectGraph.cpp b/AI/Nullkiller/Pathfinding/ObjectGraph.cpp index a30c41876..eb84128b5 100644 --- a/AI/Nullkiller/Pathfinding/ObjectGraph.cpp +++ b/AI/Nullkiller/Pathfinding/ObjectGraph.cpp @@ -9,6 +9,7 @@ */ #include "StdInc.h" #include "ObjectGraph.h" +#include "ObjectGraphCalculator.h" #include "AIPathfinderConfig.h" #include "../../../lib/CRandomGenerator.h" #include "../../../CCallback.h" @@ -22,392 +23,6 @@ namespace NKAI { -struct ConnectionCostInfo -{ - float totalCost = 0; - float avg = 0; - int connectionsCount = 0; -}; - -class ObjectGraphCalculator -{ -private: - ObjectGraph * target; - const Nullkiller * ai; - std::mutex syncLock; - - std::map actors; - std::map actorObjectMap; - - std::vector> temporaryBoats; - std::vector> temporaryActorHeroes; - -public: - ObjectGraphCalculator(ObjectGraph * target, const Nullkiller * ai) - :ai(ai), target(target), syncLock() - { - } - - void setGraphObjects() - { - for(auto obj : ai->memory->visitableObjs) - { - if(obj && obj->isVisitable() && obj->ID != Obj::HERO && obj->ID != Obj::EVENT) - { - addObjectActor(obj); - } - } - - for(auto town : ai->cb->getTownsInfo()) - { - addObjectActor(town); - } - } - - void calculateConnections() - { - updatePaths(); - - std::vector pathCache; - - foreach_tile_pos(ai->cb.get(), [this, &pathCache](const CPlayerSpecificInfoCallback * cb, const int3 & pos) - { - calculateConnections(pos, pathCache); - }); - - removeExtraConnections(); - } - - float getNeighborConnectionsCost(const int3 & pos, std::vector & pathCache) - { - float neighborCost = std::numeric_limits::max(); - - if(NKAI_GRAPH_TRACE_LEVEL >= 2) - { - logAi->trace("Checking junction %s", pos.toString()); - } - - foreach_neighbour( - ai->cb.get(), - pos, - [this, &neighborCost, &pathCache](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor) - { - ai->pathfinder->calculatePathInfo(pathCache, neighbor); - - auto costTotal = this->getConnectionsCost(pathCache); - - if(costTotal.connectionsCount > 2 && costTotal.avg < neighborCost) - { - neighborCost = costTotal.avg; - - if(NKAI_GRAPH_TRACE_LEVEL >= 2) - { - logAi->trace("Better node found at %s", neighbor.toString()); - } - } - }); - - return neighborCost; - } - - void addMinimalDistanceJunctions() - { - tbb::concurrent_unordered_set> junctions; - - pforeachTilePaths(ai->cb->getMapSize(), ai, [this, &junctions](const int3 & pos, std::vector & paths) - { - if(target->hasNodeAt(pos)) - return; - - if(ai->cb->getGuardingCreaturePosition(pos).valid()) - return; - - ConnectionCostInfo currentCost = getConnectionsCost(paths); - - if(currentCost.connectionsCount <= 2) - return; - - float neighborCost = getNeighborConnectionsCost(pos, paths); - - if(currentCost.avg < neighborCost) - { - junctions.insert(pos); - } - }); - - for(auto pos : junctions) - { - addJunctionActor(pos); - } - } - -private: - void updatePaths() - { - PathfinderSettings ps; - - ps.mainTurnDistanceLimit = 5; - ps.scoutTurnDistanceLimit = 1; - ps.allowBypassObjects = false; - - ai->pathfinder->updatePaths(actors, ps); - } - - void calculateConnections(const int3 & pos, std::vector & pathCache) - { - if(target->hasNodeAt(pos)) - { - foreach_neighbour( - ai->cb.get(), - pos, - [this, &pos, &pathCache](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor) - { - if(target->hasNodeAt(neighbor)) - { - ai->pathfinder->calculatePathInfo(pathCache, neighbor); - - for(auto & path : pathCache) - { - if(pos == path.targetHero->visitablePos()) - { - target->tryAddConnection(pos, neighbor, path.movementCost(), path.getTotalDanger()); - } - } - } - }); - - auto obj = ai->cb->getTopObj(pos); - - if((obj && obj->ID == Obj::BOAT) || target->isVirtualBoat(pos)) - { - ai->pathfinder->calculatePathInfo(pathCache, pos); - - for(AIPath & path : pathCache) - { - auto from = path.targetHero->visitablePos(); - auto fromObj = actorObjectMap[path.targetHero]; - - auto danger = ai->pathfinder->getStorage()->evaluateDanger(pos, path.targetHero, true); - auto updated = target->tryAddConnection( - from, - pos, - path.movementCost(), - danger); - - if(NKAI_GRAPH_TRACE_LEVEL >= 2 && updated) - { - logAi->trace( - "Connected %s[%s] -> %s[%s] through [%s], cost %2f", - fromObj ? fromObj->getObjectName() : "J", from.toString(), - "Boat", pos.toString(), - pos.toString(), - path.movementCost()); - } - } - } - - return; - } - - auto guardPos = ai->cb->getGuardingCreaturePosition(pos); - - ai->pathfinder->calculatePathInfo(pathCache, pos); - - for(AIPath & path1 : pathCache) - { - for(AIPath & path2 : pathCache) - { - if(path1.targetHero == path2.targetHero) - continue; - - auto pos1 = path1.targetHero->visitablePos(); - auto pos2 = path2.targetHero->visitablePos(); - - if(guardPos.valid() && guardPos != pos1 && guardPos != pos2) - continue; - - auto obj1 = actorObjectMap[path1.targetHero]; - auto obj2 = actorObjectMap[path2.targetHero]; - - auto tile1 = cb->getTile(pos1); - auto tile2 = cb->getTile(pos2); - - if(tile2->isWater() && !tile1->isWater()) - { - if(!cb->getTile(pos)->isWater()) - continue; - - auto startingObjIsBoat = (obj1 && obj1->ID == Obj::BOAT) || target->isVirtualBoat(pos1); - - if(!startingObjIsBoat) - continue; - } - - auto danger = ai->pathfinder->getStorage()->evaluateDanger(pos2, path1.targetHero, true); - - auto updated = target->tryAddConnection( - pos1, - pos2, - path1.movementCost() + path2.movementCost(), - danger); - - if(NKAI_GRAPH_TRACE_LEVEL >= 2 && updated) - { - logAi->trace( - "Connected %s[%s] -> %s[%s] through [%s], cost %2f", - obj1 ? obj1->getObjectName() : "J", pos1.toString(), - obj2 ? obj2->getObjectName() : "J", pos2.toString(), - pos.toString(), - path1.movementCost() + path2.movementCost()); - } - } - } - } - - bool isExtraConnection(float direct, float side1, float side2) const - { - float sideRatio = (side1 + side2) / direct; - - return sideRatio < 1.25f && direct > side1 && direct > side2; - } - - void removeExtraConnections() - { - std::vector> connectionsToRemove; - - for(auto & actor : temporaryActorHeroes) - { - auto pos = actor->visitablePos(); - auto & currentNode = target->getNode(pos); - - target->iterateConnections(pos, [this, &pos, &connectionsToRemove, ¤tNode](int3 n1, ObjectLink o1) - { - target->iterateConnections(n1, [&pos, &o1, ¤tNode, &connectionsToRemove, this](int3 n2, ObjectLink o2) - { - auto direct = currentNode.connections.find(n2); - - if(direct != currentNode.connections.end() && isExtraConnection(direct->second.cost, o1.cost, o2.cost)) - { - connectionsToRemove.push_back({pos, n2}); - } - }); - }); - } - - vstd::removeDuplicates(connectionsToRemove); - - for(auto & c : connectionsToRemove) - { - target->removeConnection(c.first, c.second); - - if(NKAI_GRAPH_TRACE_LEVEL >= 2) - { - logAi->trace("Remove ineffective connection %s->%s", c.first.toString(), c.second.toString()); - } - } - } - - void addObjectActor(const CGObjectInstance * obj) - { - auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique(obj->cb)).get(); - - CRandomGenerator rng; - auto visitablePos = obj->visitablePos(); - - objectActor->setOwner(ai->playerID); // lets avoid having multiple colors - objectActor->initHero(rng, static_cast(0)); - objectActor->pos = objectActor->convertFromVisitablePos(visitablePos); - objectActor->initObj(rng); - - if(cb->getTile(visitablePos)->isWater()) - { - objectActor->boat = temporaryBoats.emplace_back(std::make_unique(objectActor->cb)).get(); - } - - assert(objectActor->visitablePos() == visitablePos); - - actorObjectMap[objectActor] = obj; - actors[objectActor] = obj->ID == Obj::TOWN || obj->ID == Obj::BOAT ? HeroRole::MAIN : HeroRole::SCOUT; - - target->addObject(obj); - - auto shipyard = dynamic_cast(obj); - - if(shipyard && shipyard->bestLocation().valid()) - { - int3 virtualBoat = shipyard->bestLocation(); - - addJunctionActor(virtualBoat, true); - target->addVirtualBoat(virtualBoat, obj); - } - } - - void addJunctionActor(const int3 & visitablePos, bool isVirtualBoat = false) - { - std::lock_guard lock(syncLock); - - auto internalCb = temporaryActorHeroes.front()->cb; - auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique(internalCb)).get(); - - CRandomGenerator rng; - - objectActor->setOwner(ai->playerID); // lets avoid having multiple colors - objectActor->initHero(rng, static_cast(0)); - objectActor->pos = objectActor->convertFromVisitablePos(visitablePos); - objectActor->initObj(rng); - - if(isVirtualBoat || ai->cb->getTile(visitablePos)->isWater()) - { - objectActor->boat = temporaryBoats.emplace_back(std::make_unique(objectActor->cb)).get(); - } - - assert(objectActor->visitablePos() == visitablePos); - - actorObjectMap[objectActor] = nullptr; - actors[objectActor] = isVirtualBoat ? HeroRole::MAIN : HeroRole::SCOUT; - - target->registerJunction(visitablePos); - } - - ConnectionCostInfo getConnectionsCost(std::vector & paths) const - { - std::map costs; - - for(auto & path : paths) - { - auto fromPos = path.targetHero->visitablePos(); - auto cost = costs.find(fromPos); - - if(cost == costs.end()) - { - costs.emplace(fromPos, path.movementCost()); - } - else - { - if(path.movementCost() < cost->second) - { - costs[fromPos] = path.movementCost(); - } - } - } - - ConnectionCostInfo result; - - for(auto & cost : costs) - { - result.totalCost += cost.second; - result.connectionsCount++; - } - - if(result.connectionsCount) - { - result.avg = result.totalCost / result.connectionsCount; - } - - return result; - } -}; - bool ObjectGraph::tryAddConnection( const int3 & from, const int3 & to, @@ -538,372 +153,4 @@ void ObjectGraph::dumpToLog(std::string visualKey) const }); } -bool GraphNodeComparer::operator()(const GraphPathNodePointer & lhs, const GraphPathNodePointer & rhs) const -{ - return pathNodes.at(lhs.coord)[lhs.nodeType].cost > pathNodes.at(rhs.coord)[rhs.nodeType].cost; -} - -GraphPaths::GraphPaths() - : visualKey(""), graph(), pathNodes() -{ -} - -std::shared_ptr getCompositeAction( - const Nullkiller * ai, - std::shared_ptr linkActionFactory, - std::shared_ptr transitionAction) -{ - if(!linkActionFactory) - return transitionAction; - - auto linkAction = linkActionFactory->create(ai); - - if(!transitionAction) - return linkAction; - - std::vector> actionsArray = { - transitionAction, - linkAction - }; - - return std::make_shared(actionsArray); -} - -void GraphPaths::calculatePaths(const CGHeroInstance * targetHero, const Nullkiller * ai, uint8_t scanDepth) -{ - graph.copyFrom(*ai->baseGraph); - graph.connectHeroes(ai); - - visualKey = std::to_string(ai->playerID) + ":" + targetHero->getNameTranslated(); - pathNodes.clear(); - - GraphNodeComparer cmp(pathNodes); - GraphPathNode::TFibHeap pq(cmp); - - pathNodes[targetHero->visitablePos()][GrapthPathNodeType::NORMAL].cost = 0; - pq.emplace(GraphPathNodePointer(targetHero->visitablePos(), GrapthPathNodeType::NORMAL)); - - while(!pq.empty()) - { - GraphPathNodePointer pos = pq.top(); - pq.pop(); - - auto & node = getOrCreateNode(pos); - std::shared_ptr transitionAction; - - if(node.obj) - { - if(node.obj->ID == Obj::QUEST_GUARD - || node.obj->ID == Obj::BORDERGUARD - || node.obj->ID == Obj::BORDER_GATE) - { - auto questObj = dynamic_cast(node.obj); - auto questInfo = QuestInfo(questObj->quest, node.obj, pos.coord); - - if(node.obj->ID == Obj::QUEST_GUARD - && questObj->quest->mission == Rewardable::Limiter{} - && questObj->quest->killTarget == ObjectInstanceID::NONE) - { - continue; - } - - auto questAction = std::make_shared(questInfo); - - if(!questAction->canAct(ai, targetHero)) - { - transitionAction = questAction; - } - } - } - - node.isInQueue = false; - - graph.iterateConnections(pos.coord, [this, ai, &pos, &node, &transitionAction, &pq, scanDepth](int3 target, const ObjectLink & o) - { - auto compositeAction = getCompositeAction(ai, o.specialAction, transitionAction); - auto targetNodeType = o.danger || compositeAction ? GrapthPathNodeType::BATTLE : pos.nodeType; - auto targetPointer = GraphPathNodePointer(target, targetNodeType); - auto & targetNode = getOrCreateNode(targetPointer); - - if(targetNode.tryUpdate(pos, node, o)) - { - if(targetNode.cost > scanDepth) - { - return; - } - - targetNode.specialAction = compositeAction; - - auto targetGraphNode = graph.getNode(target); - - if(targetGraphNode.objID.hasValue()) - { - targetNode.obj = ai->cb->getObj(targetGraphNode.objID, false); - - if(targetNode.obj && targetNode.obj->ID == Obj::HERO) - return; - } - - if(targetNode.isInQueue) - { - pq.increase(targetNode.handle); - } - else - { - targetNode.handle = pq.emplace(targetPointer); - targetNode.isInQueue = true; - } - } - }); - } -} - -void GraphPaths::dumpToLog() const -{ - logVisual->updateWithLock(visualKey, [&](IVisualLogBuilder & logBuilder) - { - for(auto & tile : pathNodes) - { - for(auto & node : tile.second) - { - if(!node.previous.valid()) - continue; - - if(NKAI_GRAPH_TRACE_LEVEL >= 2) - { - logAi->trace( - "%s -> %s: %f !%d", - node.previous.coord.toString(), - tile.first.toString(), - node.cost, - node.danger); - } - - logBuilder.addLine(node.previous.coord, tile.first); - } - } - }); -} - -bool GraphPathNode::tryUpdate(const GraphPathNodePointer & pos, const GraphPathNode & prev, const ObjectLink & link) -{ - auto newCost = prev.cost + link.cost; - - if(newCost < cost) - { - previous = pos; - danger = prev.danger + link.danger; - cost = newCost; - - return true; - } - - return false; -} - -void GraphPaths::addChainInfo(std::vector & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const -{ - auto nodes = pathNodes.find(tile); - - if(nodes == pathNodes.end()) - return; - - for(auto & node : nodes->second) - { - if(!node.reachable()) - continue; - - std::vector tilesToPass; - - uint64_t danger = node.danger; - float cost = node.cost; - bool allowBattle = false; - - auto current = GraphPathNodePointer(nodes->first, node.nodeType); - - while(true) - { - auto currentTile = pathNodes.find(current.coord); - - if(currentTile == pathNodes.end()) - break; - - auto currentNode = currentTile->second[current.nodeType]; - - if(!currentNode.previous.valid()) - break; - - allowBattle = allowBattle || currentNode.nodeType == GrapthPathNodeType::BATTLE; - vstd::amax(danger, currentNode.danger); - vstd::amax(cost, currentNode.cost); - - tilesToPass.push_back(current); - - if(currentNode.cost < 2.0f) - break; - - current = currentNode.previous; - } - - if(tilesToPass.empty()) - continue; - - auto entryPaths = ai->pathfinder->getPathInfo(tilesToPass.back().coord); - - for(auto & path : entryPaths) - { - if(path.targetHero != hero) - continue; - - for(auto graphTile = tilesToPass.rbegin(); graphTile != tilesToPass.rend(); graphTile++) - { - AIPathNodeInfo n; - - n.coord = graphTile->coord; - n.cost = cost; - n.turns = static_cast(cost) + 1; // just in case lets select worst scenario - n.danger = danger; - n.targetHero = hero; - n.parentIndex = -1; - n.specialAction = getNode(*graphTile).specialAction; - - if(n.specialAction) - { - n.actionIsBlocked = !n.specialAction->canAct(ai, n); - } - - for(auto & node : path.nodes) - { - node.parentIndex++; - } - - path.nodes.insert(path.nodes.begin(), n); - } - - path.armyLoss += ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), danger); - path.targetObjectDanger = ai->pathfinder->getStorage()->evaluateDanger(tile, path.targetHero, !allowBattle); - path.targetObjectArmyLoss = ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), path.targetObjectDanger); - - paths.push_back(path); - } - } -} - -void GraphPaths::quickAddChainInfoWithBlocker(std::vector & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const -{ - auto nodes = pathNodes.find(tile); - - if(nodes == pathNodes.end()) - return; - - for(auto & targetNode : nodes->second) - { - if(!targetNode.reachable()) - continue; - - std::vector tilesToPass; - - uint64_t danger = targetNode.danger; - float cost = targetNode.cost; - bool allowBattle = false; - - auto current = GraphPathNodePointer(nodes->first, targetNode.nodeType); - - while(true) - { - auto currentTile = pathNodes.find(current.coord); - - if(currentTile == pathNodes.end()) - break; - - auto currentNode = currentTile->second[current.nodeType]; - - allowBattle = allowBattle || currentNode.nodeType == GrapthPathNodeType::BATTLE; - vstd::amax(danger, currentNode.danger); - vstd::amax(cost, currentNode.cost); - - tilesToPass.push_back(current); - - if(currentNode.cost < 2.0f) - break; - - current = currentNode.previous; - } - - if(tilesToPass.empty()) - continue; - - auto entryPaths = ai->pathfinder->getPathInfo(tilesToPass.back().coord); - - for(auto & entryPath : entryPaths) - { - if(entryPath.targetHero != hero) - continue; - - auto & path = paths.emplace_back(); - - path.targetHero = entryPath.targetHero; - path.heroArmy = entryPath.heroArmy; - path.exchangeCount = entryPath.exchangeCount; - path.armyLoss = entryPath.armyLoss + ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), danger); - path.targetObjectDanger = ai->pathfinder->getStorage()->evaluateDanger(tile, path.targetHero, !allowBattle); - path.targetObjectArmyLoss = ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), path.targetObjectDanger); - - AIPathNodeInfo n; - - n.targetHero = hero; - n.parentIndex = -1; - - // final node - n.coord = tile; - n.cost = targetNode.cost; - n.danger = targetNode.danger; - n.parentIndex = path.nodes.size(); - path.nodes.push_back(n); - - for(auto entryNode = entryPath.nodes.rbegin(); entryNode != entryPath.nodes.rend(); entryNode++) - { - auto blocker = ai->objectClusterizer->getBlocker(*entryNode); - - if(blocker) - { - // blocker node - path.nodes.push_back(*entryNode); - path.nodes.back().parentIndex = path.nodes.size() - 1; - break; - } - } - - if(path.nodes.size() > 1) - continue; - - for(auto graphTile = tilesToPass.rbegin(); graphTile != tilesToPass.rend(); graphTile++) - { - auto & node = getNode(*graphTile); - - n.coord = graphTile->coord; - n.cost = node.cost; - n.turns = static_cast(node.cost); - n.danger = node.danger; - n.specialAction = node.specialAction; - n.parentIndex = path.nodes.size(); - - if(n.specialAction) - { - n.actionIsBlocked = !n.specialAction->canAct(ai, n); - } - - auto blocker = ai->objectClusterizer->getBlocker(n); - - if(!blocker) - continue; - - // blocker node - path.nodes.push_back(n); - break; - } - } - } -} - } diff --git a/AI/Nullkiller/Pathfinding/ObjectGraph.h b/AI/Nullkiller/Pathfinding/ObjectGraph.h index 368006331..d60844025 100644 --- a/AI/Nullkiller/Pathfinding/ObjectGraph.h +++ b/AI/Nullkiller/Pathfinding/ObjectGraph.h @@ -112,102 +112,4 @@ public: } }; -struct GraphPathNode; - -enum GrapthPathNodeType -{ - NORMAL, - - BATTLE, - - LAST -}; - -struct GraphPathNodePointer -{ - int3 coord = int3(-1); - GrapthPathNodeType nodeType = GrapthPathNodeType::NORMAL; - - GraphPathNodePointer() = default; - - GraphPathNodePointer(int3 coord, GrapthPathNodeType type) - :coord(coord), nodeType(type) - { } - - bool valid() const - { - return coord.valid(); - } -}; - -typedef std::unordered_map GraphNodeStorage; - -class GraphNodeComparer -{ - const GraphNodeStorage & pathNodes; - -public: - GraphNodeComparer(const GraphNodeStorage & pathNodes) - :pathNodes(pathNodes) - { - } - - bool operator()(const GraphPathNodePointer & lhs, const GraphPathNodePointer & rhs) const; -}; - -struct GraphPathNode -{ - const float BAD_COST = 100000; - - GrapthPathNodeType nodeType = GrapthPathNodeType::NORMAL; - GraphPathNodePointer previous; - float cost = BAD_COST; - uint64_t danger = 0; - const CGObjectInstance * obj = nullptr; - std::shared_ptr specialAction; - - using TFibHeap = boost::heap::fibonacci_heap>; - - TFibHeap::handle_type handle; - bool isInQueue = false; - - bool reachable() const - { - return cost < BAD_COST; - } - - bool tryUpdate(const GraphPathNodePointer & pos, const GraphPathNode & prev, const ObjectLink & link); -}; - -class GraphPaths -{ - ObjectGraph graph; - GraphNodeStorage pathNodes; - std::string visualKey; - -public: - GraphPaths(); - void calculatePaths(const CGHeroInstance * targetHero, const Nullkiller * ai, uint8_t scanDepth); - void addChainInfo(std::vector & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const; - void quickAddChainInfoWithBlocker(std::vector & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const; - void dumpToLog() const; - -private: - GraphPathNode & getOrCreateNode(const GraphPathNodePointer & pos) - { - auto & node = pathNodes[pos.coord][pos.nodeType]; - - node.nodeType = pos.nodeType; - - return node; - } - - const GraphPathNode & getNode(const GraphPathNodePointer & pos) const - { - auto & node = pathNodes.at(pos.coord)[pos.nodeType]; - - return node; - } -}; - } diff --git a/AI/Nullkiller/Pathfinding/ObjectGraphCalculator.cpp b/AI/Nullkiller/Pathfinding/ObjectGraphCalculator.cpp new file mode 100644 index 000000000..539a93fda --- /dev/null +++ b/AI/Nullkiller/Pathfinding/ObjectGraphCalculator.cpp @@ -0,0 +1,387 @@ +/* +* ObjectGraphCalculator.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 "ObjectGraphCalculator.h" +#include "AIPathfinderConfig.h" +#include "../../../lib/CRandomGenerator.h" +#include "../../../CCallback.h" +#include "../../../lib/mapping/CMap.h" +#include "../Engine/Nullkiller.h" +#include "../../../lib/logging/VisualLogger.h" +#include "Actions/QuestAction.h" +#include "../pforeach.h" + +namespace NKAI +{ + +ObjectGraphCalculator::ObjectGraphCalculator(ObjectGraph * target, const Nullkiller * ai) + :ai(ai), target(target), syncLock() +{ +} + +void ObjectGraphCalculator::setGraphObjects() +{ + for(auto obj : ai->memory->visitableObjs) + { + if(obj && obj->isVisitable() && obj->ID != Obj::HERO && obj->ID != Obj::EVENT) + { + addObjectActor(obj); + } + } + + for(auto town : ai->cb->getTownsInfo()) + { + addObjectActor(town); + } +} + +void ObjectGraphCalculator::calculateConnections() +{ + updatePaths(); + + std::vector pathCache; + + foreach_tile_pos(ai->cb.get(), [this, &pathCache](const CPlayerSpecificInfoCallback * cb, const int3 & pos) + { + calculateConnections(pos, pathCache); + }); + + removeExtraConnections(); +} + +float ObjectGraphCalculator::getNeighborConnectionsCost(const int3 & pos, std::vector & pathCache) +{ + float neighborCost = std::numeric_limits::max(); + + if(NKAI_GRAPH_TRACE_LEVEL >= 2) + { + logAi->trace("Checking junction %s", pos.toString()); + } + + foreach_neighbour( + ai->cb.get(), + pos, + [this, &neighborCost, &pathCache](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor) + { + ai->pathfinder->calculatePathInfo(pathCache, neighbor); + + auto costTotal = this->getConnectionsCost(pathCache); + + if(costTotal.connectionsCount > 2 && costTotal.avg < neighborCost) + { + neighborCost = costTotal.avg; + + if(NKAI_GRAPH_TRACE_LEVEL >= 2) + { + logAi->trace("Better node found at %s", neighbor.toString()); + } + } + }); + + return neighborCost; +} + +void ObjectGraphCalculator::addMinimalDistanceJunctions() +{ + tbb::concurrent_unordered_set> junctions; + + pforeachTilePaths(ai->cb->getMapSize(), ai, [this, &junctions](const int3 & pos, std::vector & paths) + { + if(target->hasNodeAt(pos)) + return; + + if(ai->cb->getGuardingCreaturePosition(pos).valid()) + return; + + ConnectionCostInfo currentCost = getConnectionsCost(paths); + + if(currentCost.connectionsCount <= 2) + return; + + float neighborCost = getNeighborConnectionsCost(pos, paths); + + if(currentCost.avg < neighborCost) + { + junctions.insert(pos); + } + }); + + for(auto pos : junctions) + { + addJunctionActor(pos); + } +} + +void ObjectGraphCalculator::updatePaths() +{ + PathfinderSettings ps; + + ps.mainTurnDistanceLimit = 5; + ps.scoutTurnDistanceLimit = 1; + ps.allowBypassObjects = false; + + ai->pathfinder->updatePaths(actors, ps); +} + +void ObjectGraphCalculator::calculateConnections(const int3 & pos, std::vector & pathCache) +{ + if(target->hasNodeAt(pos)) + { + foreach_neighbour( + ai->cb.get(), + pos, + [this, &pos, &pathCache](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor) + { + if(target->hasNodeAt(neighbor)) + { + ai->pathfinder->calculatePathInfo(pathCache, neighbor); + + for(auto & path : pathCache) + { + if(pos == path.targetHero->visitablePos()) + { + target->tryAddConnection(pos, neighbor, path.movementCost(), path.getTotalDanger()); + } + } + } + }); + + auto obj = ai->cb->getTopObj(pos); + + if((obj && obj->ID == Obj::BOAT) || target->isVirtualBoat(pos)) + { + ai->pathfinder->calculatePathInfo(pathCache, pos); + + for(AIPath & path : pathCache) + { + auto from = path.targetHero->visitablePos(); + auto fromObj = actorObjectMap[path.targetHero]; + + auto danger = ai->pathfinder->getStorage()->evaluateDanger(pos, path.targetHero, true); + auto updated = target->tryAddConnection( + from, + pos, + path.movementCost(), + danger); + + if(NKAI_GRAPH_TRACE_LEVEL >= 2 && updated) + { + logAi->trace( + "Connected %s[%s] -> %s[%s] through [%s], cost %2f", + fromObj ? fromObj->getObjectName() : "J", from.toString(), + "Boat", pos.toString(), + pos.toString(), + path.movementCost()); + } + } + } + + return; + } + + auto guardPos = ai->cb->getGuardingCreaturePosition(pos); + + ai->pathfinder->calculatePathInfo(pathCache, pos); + + for(AIPath & path1 : pathCache) + { + for(AIPath & path2 : pathCache) + { + if(path1.targetHero == path2.targetHero) + continue; + + auto pos1 = path1.targetHero->visitablePos(); + auto pos2 = path2.targetHero->visitablePos(); + + if(guardPos.valid() && guardPos != pos1 && guardPos != pos2) + continue; + + auto obj1 = actorObjectMap[path1.targetHero]; + auto obj2 = actorObjectMap[path2.targetHero]; + + auto tile1 = cb->getTile(pos1); + auto tile2 = cb->getTile(pos2); + + if(tile2->isWater() && !tile1->isWater()) + { + if(!cb->getTile(pos)->isWater()) + continue; + + auto startingObjIsBoat = (obj1 && obj1->ID == Obj::BOAT) || target->isVirtualBoat(pos1); + + if(!startingObjIsBoat) + continue; + } + + auto danger = ai->pathfinder->getStorage()->evaluateDanger(pos2, path1.targetHero, true); + + auto updated = target->tryAddConnection( + pos1, + pos2, + path1.movementCost() + path2.movementCost(), + danger); + + if(NKAI_GRAPH_TRACE_LEVEL >= 2 && updated) + { + logAi->trace( + "Connected %s[%s] -> %s[%s] through [%s], cost %2f", + obj1 ? obj1->getObjectName() : "J", pos1.toString(), + obj2 ? obj2->getObjectName() : "J", pos2.toString(), + pos.toString(), + path1.movementCost() + path2.movementCost()); + } + } + } +} + +bool ObjectGraphCalculator::isExtraConnection(float direct, float side1, float side2) const +{ + float sideRatio = (side1 + side2) / direct; + + return sideRatio < 1.25f && direct > side1 && direct > side2; +} + +void ObjectGraphCalculator::removeExtraConnections() +{ + std::vector> connectionsToRemove; + + for(auto & actor : temporaryActorHeroes) + { + auto pos = actor->visitablePos(); + auto & currentNode = target->getNode(pos); + + target->iterateConnections(pos, [this, &pos, &connectionsToRemove, ¤tNode](int3 n1, ObjectLink o1) + { + target->iterateConnections(n1, [&pos, &o1, ¤tNode, &connectionsToRemove, this](int3 n2, ObjectLink o2) + { + auto direct = currentNode.connections.find(n2); + + if(direct != currentNode.connections.end() && isExtraConnection(direct->second.cost, o1.cost, o2.cost)) + { + connectionsToRemove.push_back({pos, n2}); + } + }); + }); + } + + vstd::removeDuplicates(connectionsToRemove); + + for(auto & c : connectionsToRemove) + { + target->removeConnection(c.first, c.second); + + if(NKAI_GRAPH_TRACE_LEVEL >= 2) + { + logAi->trace("Remove ineffective connection %s->%s", c.first.toString(), c.second.toString()); + } + } +} + +void ObjectGraphCalculator::addObjectActor(const CGObjectInstance * obj) +{ + auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique(obj->cb)).get(); + + CRandomGenerator rng; + auto visitablePos = obj->visitablePos(); + + objectActor->setOwner(ai->playerID); // lets avoid having multiple colors + objectActor->initHero(rng, static_cast(0)); + objectActor->pos = objectActor->convertFromVisitablePos(visitablePos); + objectActor->initObj(rng); + + if(cb->getTile(visitablePos)->isWater()) + { + objectActor->boat = temporaryBoats.emplace_back(std::make_unique(objectActor->cb)).get(); + } + + assert(objectActor->visitablePos() == visitablePos); + + actorObjectMap[objectActor] = obj; + actors[objectActor] = obj->ID == Obj::TOWN || obj->ID == Obj::BOAT ? HeroRole::MAIN : HeroRole::SCOUT; + + target->addObject(obj); + + auto shipyard = dynamic_cast(obj); + + if(shipyard && shipyard->bestLocation().valid()) + { + int3 virtualBoat = shipyard->bestLocation(); + + addJunctionActor(virtualBoat, true); + target->addVirtualBoat(virtualBoat, obj); + } +} + +void ObjectGraphCalculator::addJunctionActor(const int3 & visitablePos, bool isVirtualBoat) +{ + std::lock_guard lock(syncLock); + + auto internalCb = temporaryActorHeroes.front()->cb; + auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique(internalCb)).get(); + + CRandomGenerator rng; + + objectActor->setOwner(ai->playerID); // lets avoid having multiple colors + objectActor->initHero(rng, static_cast(0)); + objectActor->pos = objectActor->convertFromVisitablePos(visitablePos); + objectActor->initObj(rng); + + if(isVirtualBoat || ai->cb->getTile(visitablePos)->isWater()) + { + objectActor->boat = temporaryBoats.emplace_back(std::make_unique(objectActor->cb)).get(); + } + + assert(objectActor->visitablePos() == visitablePos); + + actorObjectMap[objectActor] = nullptr; + actors[objectActor] = isVirtualBoat ? HeroRole::MAIN : HeroRole::SCOUT; + + target->registerJunction(visitablePos); +} + +ConnectionCostInfo ObjectGraphCalculator::getConnectionsCost(std::vector & paths) const +{ + std::map costs; + + for(auto & path : paths) + { + auto fromPos = path.targetHero->visitablePos(); + auto cost = costs.find(fromPos); + + if(cost == costs.end()) + { + costs.emplace(fromPos, path.movementCost()); + } + else + { + if(path.movementCost() < cost->second) + { + costs[fromPos] = path.movementCost(); + } + } + } + + ConnectionCostInfo result; + + for(auto & cost : costs) + { + result.totalCost += cost.second; + result.connectionsCount++; + } + + if(result.connectionsCount) + { + result.avg = result.totalCost / result.connectionsCount; + } + + return result; +} + +} diff --git a/AI/Nullkiller/Pathfinding/ObjectGraphCalculator.h b/AI/Nullkiller/Pathfinding/ObjectGraphCalculator.h new file mode 100644 index 000000000..812bd6985 --- /dev/null +++ b/AI/Nullkiller/Pathfinding/ObjectGraphCalculator.h @@ -0,0 +1,56 @@ +/* +* ObjectGraphCalculator.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 "ObjectGraph.h" +#include "../AIUtility.h" + +namespace NKAI +{ + +struct ConnectionCostInfo +{ + float totalCost = 0; + float avg = 0; + int connectionsCount = 0; +}; + +class ObjectGraphCalculator +{ +private: + ObjectGraph * target; + const Nullkiller * ai; + std::mutex syncLock; + + std::map actors; + std::map actorObjectMap; + + std::vector> temporaryBoats; + std::vector> temporaryActorHeroes; + +public: + ObjectGraphCalculator(ObjectGraph * target, const Nullkiller * ai); + void setGraphObjects(); + void calculateConnections(); + float getNeighborConnectionsCost(const int3 & pos, std::vector & pathCache); + void addMinimalDistanceJunctions(); + +private: + void updatePaths(); + void calculateConnections(const int3 & pos, std::vector & pathCache); + bool isExtraConnection(float direct, float side1, float side2) const; + void removeExtraConnections(); + void addObjectActor(const CGObjectInstance * obj); + void addJunctionActor(const int3 & visitablePos, bool isVirtualBoat = false); + ConnectionCostInfo getConnectionsCost(std::vector & paths) const; +}; + +} diff --git a/config/ai/nkai/nkai-settings.json b/config/ai/nkai/nkai-settings.json index a7dd1ec08..f597be497 100644 --- a/config/ai/nkai/nkai-settings.json +++ b/config/ai/nkai/nkai-settings.json @@ -4,5 +4,7 @@ "mainHeroTurnDistanceLimit" : 10, "scoutHeroTurnDistanceLimit" : 5, "maxGoldPressure" : 0.3, - "useTroopsFromGarrisons" : true + "useTroopsFromGarrisons" : true, + "openMap": true, + "allowObjectGraph": true } \ No newline at end of file