From e385c83a88a54887f0d73f66f448f6695f1c5f04 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 16 May 2021 14:56:42 +0300 Subject: [PATCH] Nullkiller - rewrite decomposition, decomposition cache, morale management --- AI/Nullkiller/Analyzers/ArmyManager.cpp | 101 ++++++-- AI/Nullkiller/Analyzers/ArmyManager.h | 12 +- .../Behaviors/CaptureObjectsBehavior.cpp | 7 + AI/Nullkiller/Behaviors/StartupBehavior.cpp | 2 +- AI/Nullkiller/CMakeLists.txt | 2 + AI/Nullkiller/Engine/DeepDecomposer.cpp | 222 ++++++++++++++++++ AI/Nullkiller/Engine/DeepDecomposer.h | 41 ++++ AI/Nullkiller/Engine/Nullkiller.cpp | 94 ++------ AI/Nullkiller/Engine/Nullkiller.h | 5 +- AI/Nullkiller/Goals/AbstractGoal.h | 4 + AI/Nullkiller/Goals/CompleteQuest.cpp | 33 ++- AI/Nullkiller/Goals/CompleteQuest.h | 3 +- AI/Nullkiller/Goals/Composition.cpp | 58 +---- AI/Nullkiller/Markers/HeroExchange.cpp | 1 + AI/Nullkiller/Pathfinding/AINodeStorage.h | 2 +- AI/Nullkiller/Pathfinding/Actors.cpp | 4 +- .../Rules/AIMovementAfterDestinationRule.cpp | 7 + AI/Nullkiller/VCAI.cpp | 26 +- 18 files changed, 470 insertions(+), 154 deletions(-) create mode 100644 AI/Nullkiller/Engine/DeepDecomposer.cpp create mode 100644 AI/Nullkiller/Engine/DeepDecomposer.h diff --git a/AI/Nullkiller/Analyzers/ArmyManager.cpp b/AI/Nullkiller/Analyzers/ArmyManager.cpp index 78fc83dab..63a572ebe 100644 --- a/AI/Nullkiller/Analyzers/ArmyManager.cpp +++ b/AI/Nullkiller/Analyzers/ArmyManager.cpp @@ -31,6 +31,11 @@ public: } }; +uint64_t ArmyManager::howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const +{ + return howManyReinforcementsCanGet(hero, hero, source); +} + std::vector ArmyManager::getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const { const CCreatureSet * armies[] = { target, source }; @@ -75,15 +80,84 @@ std::vector::iterator ArmyManager::getWeakestCreature(std::vector ArmyManager::getBestArmy(const CCreatureSet * target, const CCreatureSet * source) const +std::vector ArmyManager::getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const { - auto resultingArmy = getSortedSlots(target, source); + auto sortedSlots = getSortedSlots(target, source); + std::map alignmentMap; - if(resultingArmy.size() > GameConstants::ARMY_SIZE) + for(auto & slot : sortedSlots) { - resultingArmy.resize(GameConstants::ARMY_SIZE); + alignmentMap[slot.creature->faction] += slot.power; } - else if(source->needsLastStack()) + + std::set allowedFactions; + std::vector resultingArmy; + uint64_t armyValue = 0; + + CArmedInstance newArmyInstance; + auto bonusModifiers = armyCarrier->getBonuses(Selector::type(Bonus::MORALE)); + + for(auto bonus : *bonusModifiers) + { + // army bonuses will change and object bonuses are temporary + if(bonus->source != Bonus::ARMY || bonus->source != Bonus::OBJECT) + { + newArmyInstance.addNewBonus(bonus); + } + } + + while(allowedFactions.size() < alignmentMap.size()) + { + auto strongestAlignment = vstd::maxElementByFun(alignmentMap, [&](std::pair pair) -> uint64_t + { + return vstd::contains(allowedFactions, pair.first) ? 0 : pair.second; + }); + + allowedFactions.insert(strongestAlignment->first); + + std::vector newArmy; + uint64_t newValue = 0; + newArmyInstance.clear(); + + for(auto & slot : sortedSlots) + { + if(vstd::contains(allowedFactions, slot.creature->faction)) + { + auto slotID = newArmyInstance.getSlotFor(slot.creature); + + if(slotID.validSlot()) + { + newArmyInstance.setCreature(slotID, slot.creature->idNumber, slot.count); + newArmy.push_back(slot); + } + } + } + + for(auto & slot : newArmyInstance.Slots()) + { + auto morale = slot.second->MoraleVal(); + auto multiplier = 1.0f; + + if(morale < 0) + { + multiplier += morale * 0.083f; + } + + newValue += multiplier * slot.second->getPower(); + } + + if(armyValue >= newValue) + { + break; + } + + resultingArmy = newArmy; + armyValue = newValue; + } + + if(resultingArmy.size() <= GameConstants::ARMY_SIZE + && allowedFactions.size() == alignmentMap.size() + && source->needsLastStack()) { auto weakest = getWeakestCreature(resultingArmy); @@ -101,19 +175,6 @@ std::vector ArmyManager::getBestArmy(const CCreatureSet * target, cons return resultingArmy; } -bool ArmyManager::canGetArmy(const CArmedInstance * target, const CArmedInstance * source) const -{ - //TODO: merge with pickBestCreatures - //if (ai->primaryHero().h == source) - if(target->tempOwner != source->tempOwner) - { - logAi->error("Why are we even considering exchange between heroes from different players?"); - return false; - } - - return 0 < howManyReinforcementsCanGet(target, source); -} - ui64 ArmyManager::howManyReinforcementsCanBuy(const CCreatureSet * h, const CGDwelling * t) const { ui64 aivalue = 0; @@ -162,9 +223,9 @@ std::vector ArmyManager::getArmyAvailableToBuy(const CCreatureSet * her return creaturesInDwellings; } -ui64 ArmyManager::howManyReinforcementsCanGet(const CCreatureSet * target, const CCreatureSet * source) const +ui64 ArmyManager::howManyReinforcementsCanGet(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const { - auto bestArmy = getBestArmy(target, source); + auto bestArmy = getBestArmy(armyCarrier, target, source); uint64_t newArmy = 0; uint64_t oldArmy = target->getArmyStrength(); diff --git a/AI/Nullkiller/Analyzers/ArmyManager.h b/AI/Nullkiller/Analyzers/ArmyManager.h index 78f74bbf5..04a054565 100644 --- a/AI/Nullkiller/Analyzers/ArmyManager.h +++ b/AI/Nullkiller/Analyzers/ArmyManager.h @@ -42,10 +42,10 @@ class DLL_EXPORT IArmyManager //: public: IAbstractManager { public: virtual void update() = 0; - virtual bool canGetArmy(const CArmedInstance * target, const CArmedInstance * source) const = 0; virtual ui64 howManyReinforcementsCanBuy(const CCreatureSet * target, const CGDwelling * source) const = 0; - virtual ui64 howManyReinforcementsCanGet(const CCreatureSet * target, const CCreatureSet * source) const = 0; - virtual std::vector getBestArmy(const CCreatureSet * target, const CCreatureSet * source) const = 0; + virtual ui64 howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const = 0; + virtual ui64 howManyReinforcementsCanGet(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const = 0; + virtual std::vector getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const = 0; virtual std::vector::iterator getWeakestCreature(std::vector & army) const = 0; virtual std::vector getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const = 0; virtual std::vector getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const = 0; @@ -69,10 +69,10 @@ private: public: ArmyManager(CPlayerSpecificInfoCallback * CB, const Nullkiller * ai): cb(CB), ai(ai) {} void update() override; - bool canGetArmy(const CArmedInstance * target, const CArmedInstance * source) const override; ui64 howManyReinforcementsCanBuy(const CCreatureSet * target, const CGDwelling * source) const override; - ui64 howManyReinforcementsCanGet(const CCreatureSet * target, const CCreatureSet * source) const override; - std::vector getBestArmy(const CCreatureSet * target, const CCreatureSet * source) const override; + ui64 howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const override; + ui64 howManyReinforcementsCanGet(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const override; + std::vector getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const override; std::vector::iterator getWeakestCreature(std::vector & army) const override; std::vector getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const override; std::vector getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const override; diff --git a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp index fbba09c1f..de6411b98 100644 --- a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp +++ b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp @@ -185,6 +185,13 @@ Goals::TGoalVec CaptureObjectsBehavior::decompose() const { captureObjects(objectsToCapture); } + else if(objectTypes.size()) + { + captureObjects( + std::vector( + ai->nullkiller->memory->visitableObjs.begin(), + ai->nullkiller->memory->visitableObjs.end())); + } else { captureObjects(ai->nullkiller->objectClusterizer->getNearbyObjects()); diff --git a/AI/Nullkiller/Behaviors/StartupBehavior.cpp b/AI/Nullkiller/Behaviors/StartupBehavior.cpp index de2237d34..3c16f6599 100644 --- a/AI/Nullkiller/Behaviors/StartupBehavior.cpp +++ b/AI/Nullkiller/Behaviors/StartupBehavior.cpp @@ -139,7 +139,7 @@ Goals::TGoalVec StartupBehavior::decompose() const { if(!startupTown->visitingHero) { - if(ai->nullkiller->armyManager->howManyReinforcementsCanGet(startupTown->getUpperArmy(), closestHero) > 200) + if(ai->nullkiller->armyManager->howManyReinforcementsCanGet(startupTown->getUpperArmy(), startupTown->getUpperArmy(), closestHero) > 200) { auto paths = ai->nullkiller->pathfinder->getPathInfo(startupTown->visitablePos()); diff --git a/AI/Nullkiller/CMakeLists.txt b/AI/Nullkiller/CMakeLists.txt index b93066b60..e73108075 100644 --- a/AI/Nullkiller/CMakeLists.txt +++ b/AI/Nullkiller/CMakeLists.txt @@ -36,6 +36,7 @@ set(VCAI_SRCS Markers/HeroExchange.cpp Markers/UnlockCluster.cpp Engine/Nullkiller.cpp + Engine/DeepDecomposer.cpp Engine/PriorityEvaluator.cpp Analyzers/DangerHitMapAnalyzer.cpp Analyzers/BuildAnalyzer.cpp @@ -93,6 +94,7 @@ set(VCAI_HEADERS Markers/HeroExchange.h Markers/UnlockCluster.h Engine/Nullkiller.h + Engine/DeepDecomposer.h Engine/PriorityEvaluator.h Analyzers/DangerHitMapAnalyzer.h Analyzers/BuildAnalyzer.h diff --git a/AI/Nullkiller/Engine/DeepDecomposer.cpp b/AI/Nullkiller/Engine/DeepDecomposer.cpp new file mode 100644 index 000000000..9c9649cee --- /dev/null +++ b/AI/Nullkiller/Engine/DeepDecomposer.cpp @@ -0,0 +1,222 @@ +/* +* Nullkiller.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 "DeepDecomposer.h" +#include "../VCAI.h" +#include "../Behaviors/CaptureObjectsBehavior.h" +#include "../Behaviors/RecruitHeroBehavior.h" +#include "../Behaviors/BuyArmyBehavior.h" +#include "../Behaviors/StartupBehavior.h" +#include "../Behaviors/DefenceBehavior.h" +#include "../Behaviors/BuildingBehavior.h" +#include "../Behaviors/GatherArmyBehavior.h" +#include "../Behaviors/ClusterBehavior.h" +#include "../Goals/Invalid.h" +#include "../Goals/Composition.h" + +extern boost::thread_specific_ptr cb; +extern boost::thread_specific_ptr ai; + +using namespace Goals; + +void DeepDecomposer::reset() +{ + decompositionCache.clear(); + goals.clear(); +} + +Goals::TGoalVec DeepDecomposer::decompose(TSubgoal behavior, int depthLimit) +{ + TGoalVec tasks; + + goals.clear(); + goals.resize(depthLimit); + decompositionCache.resize(depthLimit); + depth = 0; + + goals[0] = {behavior}; + + while(goals[0].size()) + { + bool fromCache; + TSubgoal current = goals[depth].back(); + TGoalVec subgoals = decomposeCached(unwrapComposition(current), fromCache); + +#if AI_TRACE_LEVEL >= 1 + logAi->trace("Decomposition level %d returned %d goals", depth, subgoals.size()); +#endif + + if(depth < depthLimit - 1) + { + goals[depth + 1].clear(); + } + + for(TSubgoal subgoal : subgoals) + { + if(subgoal->invalid()) + continue; + + if(subgoal->isElementar()) + { + // need to get rid of priority control in behaviors like Startup to avoid this check. + // 0 - goals directly from behavior + Goals::TSubgoal task = depth >= 1 ? aggregateGoals(0, subgoal) : subgoal; + +#if AI_TRACE_LEVEL >= 1 + logAi->trace("Found task %s", task->toString()); +#endif + if(!isCompositionLoop(subgoal)) + { + tasks.push_back(task); + + if(!fromCache) + { + addToCache(subgoal); + } + } + } + else if(depth < depthLimit - 1) + { +#if AI_TRACE_LEVEL >= 1 + logAi->trace("Found abstract goal %s", subgoal->toString()); +#endif + if(!isCompositionLoop(subgoal)) + { + // depth 0 gives reward, deepest goal - task to execute, all the rest is not important + // so avoid details to reduce decomposition complexity but they can give some negative effect so + auto goalToAdd = depth >= 1 ? unwrapComposition(subgoal) : subgoal; + + if(!vstd::contains(goals[depth + 1], goalToAdd)) + { + goals[depth + 1].push_back(subgoal); + } + } + } + } + + if(depth < depthLimit - 1 && goals[depth + 1].size()) + { + depth++; + } + else + { + goals[depth].pop_back(); + + while(depth > 0 && goals[depth].empty()) + { + depth--; + goals[depth].pop_back(); + } + } + } + + return tasks; +} + +Goals::TSubgoal DeepDecomposer::aggregateGoals(int startDepth, TSubgoal last) +{ + Goals::Composition composition; + + for(int i = 0; i <= depth; i++) + { + composition.addNext(goals[i].back()); + } + + composition.addNext(last); + + return sptr(composition); +} + +Goals::TSubgoal DeepDecomposer::unwrapComposition(Goals::TSubgoal goal) +{ + return goal->goalType == Goals::COMPOSITION ? goal->decompose().back() : goal; +} + +bool DeepDecomposer::isCompositionLoop(TSubgoal goal) +{ + auto goalsToTest = goal->goalType == Goals::COMPOSITION ? goal->decompose() : TGoalVec{goal}; + + for(auto goalToTest : goalsToTest) + { + for(int i = depth; i >= 0; i--) + { + auto parent = unwrapComposition(goals[i].back()); + + if(parent == goalToTest) + { + return true; + } + } + } + + return false; +} + +TGoalVec DeepDecomposer::decomposeCached(TSubgoal goal, bool & fromCache) +{ +#if AI_TRACE_LEVEL >= 1 + logAi->trace("Decomposing %s, level %s", goal->toString(), depth); +#endif + + if(goal->hasHash()) + { + for(int i = 0; i <= depth; i++) + { + auto cached = decompositionCache[i].find(goal); + + if(cached != decompositionCache[i].end()) + { +#if AI_TRACE_LEVEL >= 1 + logAi->trace("Use decomposition cache for %s, level: %d", goal->toString(), depth); +#endif + fromCache = true; + + return cached->second; + } + } + + decompositionCache[depth][goal] = {}; // if goal decomposition yields no goals we still need it in cache to not decompose again + } + +#if AI_TRACE_LEVEL >= 2 + logAi->trace("Calling decompose on %s, level %s", goal->toString(), depth); +#endif + + fromCache = false; + + return goal->decompose(); +} + +void DeepDecomposer::addToCache(TSubgoal goal) +{ + bool trusted = true; + + for(int parentDepth = 1; parentDepth <= depth; parentDepth++) + { + TSubgoal parent = unwrapComposition(goals[parentDepth].back()); + + if(parent->hasHash()) + { + auto solution = parentDepth < depth ? aggregateGoals(parentDepth + 1, goal) : goal; + +#if AI_TRACE_LEVEL >= 2 + logAi->trace("Adding %s to decomosition cache of %s at level %d", solution->toString(), parent->toString(), parentDepth); +#endif + + decompositionCache[parentDepth][parent].push_back(solution); + + if(trusted && parentDepth != 0) + { + decompositionCache[0][parent].push_back(solution); + trusted = false; + } + } + } +} \ No newline at end of file diff --git a/AI/Nullkiller/Engine/DeepDecomposer.h b/AI/Nullkiller/Engine/DeepDecomposer.h new file mode 100644 index 000000000..bf10dbe3e --- /dev/null +++ b/AI/Nullkiller/Engine/DeepDecomposer.h @@ -0,0 +1,41 @@ +/* +* DeepDecomposer.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/AbstractGoal.h" + +struct GoalHash +{ + uint64_t operator()(const Goals::TSubgoal & goal) const + { + return goal->getHash(); + } +}; + +typedef std::unordered_map TGoalHashSet; + +class DeepDecomposer +{ +private: + std::vector goals; + std::vector decompositionCache; + int depth; + +public: + void reset(); + Goals::TGoalVec decompose(Goals::TSubgoal behavior, int depthLimit); + +private: + Goals::TSubgoal aggregateGoals(int startDepth, Goals::TSubgoal last); + Goals::TSubgoal unwrapComposition(Goals::TSubgoal goal); + bool isCompositionLoop(Goals::TSubgoal goal); + Goals::TGoalVec decomposeCached(Goals::TSubgoal goal, bool & fromCache); + void addToCache(Goals::TSubgoal goal); +}; \ No newline at end of file diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 5269a09ff..0abfe64dd 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -19,6 +19,7 @@ #include "../Behaviors/GatherArmyBehavior.h" #include "../Behaviors/ClusterBehavior.h" #include "../Goals/Invalid.h" +#include "../Goals/Composition.h" extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; @@ -49,6 +50,7 @@ void Nullkiller::init(std::shared_ptr cb, PlayerColor playerID) pathfinder.reset(new AIPathfinder(cb.get(), this)); armyManager.reset(new ArmyManager(cb.get(), this)); heroManager.reset(new HeroManager(cb.get(), this)); + decomposer.reset(new DeepDecomposer()); } Goals::TTask Nullkiller::choseBestTask(Goals::TTaskVec & tasks) const @@ -60,76 +62,23 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TTaskVec & tasks) const return bestTask; } -Goals::TTask Nullkiller::choseBestTask(Goals::TSubgoal behavior) const +Goals::TTask Nullkiller::choseBestTask(Goals::TSubgoal behavior, int decompositionMaxDepth) const { logAi->debug("Checking behavior %s", behavior->toString()); - const int MAX_DEPTH = 10; - Goals::TGoalVec goals[MAX_DEPTH + 1]; - Goals::TTaskVec tasks; - std::map decompositionMap; auto start = boost::chrono::high_resolution_clock::now(); - - goals[0] = {behavior}; - - int depth = 0; - while(goals[0].size()) + + Goals::TGoalVec elementarGoals = decomposer->decompose(behavior, decompositionMaxDepth); + Goals::TTaskVec tasks; + + for(auto goal : elementarGoals) { - TSubgoal current = goals[depth].back(); + Goals::TTask task = Goals::taskptr(*goal); -#if AI_TRACE_LEVEL >= 1 - logAi->trace("Decomposing %s, level: %d", current->toString(), depth); -#endif + if(task->priority <= 0) + task->priority = priorityEvaluator->evaluate(goal); - TGoalVec subgoals = current->decompose(); - -#if AI_TRACE_LEVEL >= 1 - logAi->trace("Found %d goals", subgoals.size()); -#endif - - if(depth < MAX_DEPTH) - { - goals[depth + 1].clear(); - } - - for(auto subgoal : subgoals) - { - if(subgoal->isElementar()) - { - auto task = taskptr(*subgoal); - -#if AI_TRACE_LEVEL >= 1 - logAi->trace("Found task %s", task->toString()); -#endif - - if(task->priority <= 0) - task->priority = priorityEvaluator->evaluate(subgoal); - - tasks.push_back(task); - } - else if(depth < MAX_DEPTH) - { -#if AI_TRACE_LEVEL >= 1 - logAi->trace("Found abstract goal %s", subgoal->toString()); -#endif - goals[depth + 1].push_back(subgoal); - } - } - - if(depth < MAX_DEPTH && goals[depth + 1].size()) - { - depth++; - } - else - { - goals[depth].pop_back(); - - while(depth > 0 && goals[depth].empty()) - { - depth--; - goals[depth].pop_back(); - } - } + tasks.push_back(task); } if(tasks.empty()) @@ -190,6 +139,7 @@ void Nullkiller::updateAiState(int pass) objectClusterizer->clusterize(); buildAnalyzer->update(); + decomposer->reset(); logAi->debug("AI state updated in %ld", timeElapsed(start)); } @@ -234,6 +184,8 @@ HeroLockedReason Nullkiller::getHeroLockedReason(const CGHeroInstance * hero) co void Nullkiller::makeTurn() { + const int MAX_DEPTH = 10; + resetAiState(); for(int i = 1; i <= MAXPASS; i++) @@ -241,18 +193,18 @@ void Nullkiller::makeTurn() updateAiState(i); Goals::TTaskVec bestTasks = { - choseBestTask(sptr(BuyArmyBehavior())), - choseBestTask(sptr(CaptureObjectsBehavior())), - choseBestTask(sptr(ClusterBehavior())), - choseBestTask(sptr(RecruitHeroBehavior())), - choseBestTask(sptr(DefenceBehavior())), - choseBestTask(sptr(BuildingBehavior())), - choseBestTask(sptr(GatherArmyBehavior())) + choseBestTask(sptr(BuyArmyBehavior()), 1), + choseBestTask(sptr(CaptureObjectsBehavior()), 1), + choseBestTask(sptr(ClusterBehavior()), MAX_DEPTH), + choseBestTask(sptr(RecruitHeroBehavior()), 1), + choseBestTask(sptr(DefenceBehavior()), MAX_DEPTH), + choseBestTask(sptr(BuildingBehavior()), 1), + choseBestTask(sptr(GatherArmyBehavior()), MAX_DEPTH) }; if(cb->getDate(Date::DAY) == 1) { - bestTasks.push_back(choseBestTask(sptr(StartupBehavior()))); + bestTasks.push_back(choseBestTask(sptr(StartupBehavior()), 1)); } Goals::TTask bestTask = choseBestTask(bestTasks); diff --git a/AI/Nullkiller/Engine/Nullkiller.h b/AI/Nullkiller/Engine/Nullkiller.h index b690a01f3..9aba47612 100644 --- a/AI/Nullkiller/Engine/Nullkiller.h +++ b/AI/Nullkiller/Engine/Nullkiller.h @@ -12,12 +12,12 @@ #include "PriorityEvaluator.h" #include "FuzzyHelper.h" #include "AIMemory.h" +#include "DeepDecomposer.h" #include "../Analyzers/DangerHitMapAnalyzer.h" #include "../Analyzers/BuildAnalyzer.h" #include "../Analyzers/ArmyManager.h" #include "../Analyzers/HeroManager.h" #include "../Analyzers/ObjectClusterizer.h" -#include "../Goals/AbstractGoal.h" const float MAX_GOLD_PEASURE = 0.3f; const float MIN_PRIORITY = 0.01f; @@ -50,6 +50,7 @@ public: std::unique_ptr armyManager; std::unique_ptr memory; std::unique_ptr dangerEvaluator; + std::unique_ptr decomposer; PlayerColor playerID; std::shared_ptr cb; @@ -69,6 +70,6 @@ public: private: void resetAiState(); void updateAiState(int pass); - Goals::TTask choseBestTask(Goals::TSubgoal behavior) const; + Goals::TTask choseBestTask(Goals::TSubgoal behavior, int decompositionMaxDepth) const; Goals::TTask choseBestTask(Goals::TTaskVec & tasks) const; }; diff --git a/AI/Nullkiller/Goals/AbstractGoal.h b/AI/Nullkiller/Goals/AbstractGoal.h index 70b749e61..e520d304e 100644 --- a/AI/Nullkiller/Goals/AbstractGoal.h +++ b/AI/Nullkiller/Goals/AbstractGoal.h @@ -143,6 +143,10 @@ namespace Goals virtual bool operator==(const AbstractGoal & g) const; virtual bool isElementar() const { return false; } + + virtual bool hasHash() const { return false; } + + virtual uint64_t getHash() const { return 0; } bool operator!=(const AbstractGoal & g) const { diff --git a/AI/Nullkiller/Goals/CompleteQuest.cpp b/AI/Nullkiller/Goals/CompleteQuest.cpp index b652314df..e74f48240 100644 --- a/AI/Nullkiller/Goals/CompleteQuest.cpp +++ b/AI/Nullkiller/Goals/CompleteQuest.cpp @@ -13,12 +13,19 @@ #include "../VCAI.h" #include "../../../lib/mapping/CMap.h" //for victory conditions #include "../../../lib/CPathfinder.h" +#include "../../../lib/VCMI_Lib.h" +#include "../../../lib/CGeneralTextHandler.h" extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; using namespace Goals; +bool isKeyMaster(const QuestInfo & q) +{ + return q.obj && (q.obj->ID == Obj::BORDER_GATE || q.obj->ID == Obj::BORDERGUARD); +} + std::string CompleteQuest::toString() const { return "Complete quest " + questToString(); @@ -26,7 +33,7 @@ std::string CompleteQuest::toString() const TGoalVec CompleteQuest::decompose() const { - if(q.obj && (q.obj->ID == Obj::BORDER_GATE || q.obj->ID == Obj::BORDERGUARD)) + if(isKeyMaster(q)) { return missionKeymaster(); } @@ -73,11 +80,35 @@ TGoalVec CompleteQuest::decompose() const bool CompleteQuest::operator==(const CompleteQuest & other) const { + if(isKeyMaster(q)) + { + return isKeyMaster(other.q) && q.obj->subID == other.q.obj->subID; + } + else if(isKeyMaster(other.q)) + { + return false; + } + return q.quest->qid == other.q.quest->qid; } +uint64_t CompleteQuest::getHash() const +{ + if(isKeyMaster(q)) + { + return q.obj->subID; + } + + return q.quest->qid; +} + std::string CompleteQuest::questToString() const { + if(isKeyMaster(q)) + { + return "find " + VLC->generaltexth->tentColors[q.obj->subID] + " keymaster tent"; + } + if(q.quest->missionType == CQuest::MISSION_NONE) return "inactive quest"; diff --git a/AI/Nullkiller/Goals/CompleteQuest.h b/AI/Nullkiller/Goals/CompleteQuest.h index ab3306471..b2895917c 100644 --- a/AI/Nullkiller/Goals/CompleteQuest.h +++ b/AI/Nullkiller/Goals/CompleteQuest.h @@ -29,11 +29,12 @@ namespace Goals virtual Goals::TGoalVec decompose() const override; virtual std::string toString() const override; + virtual bool hasHash() const override { return true; } + virtual uint64_t getHash() const override; virtual bool operator==(const CompleteQuest & other) const override; private: - TGoalVec getQuestTasks() const; TGoalVec tryCompleteQuest() const; TGoalVec missionArt() const; TGoalVec missionHero() const; diff --git a/AI/Nullkiller/Goals/Composition.cpp b/AI/Nullkiller/Goals/Composition.cpp index 9e5c62c26..3b7e649e1 100644 --- a/AI/Nullkiller/Goals/Composition.cpp +++ b/AI/Nullkiller/Goals/Composition.cpp @@ -45,52 +45,7 @@ void Composition::accept(VCAI * ai) TGoalVec Composition::decompose() const { - if(isElementar()) - return subtasks; - - auto tasks = subtasks; - tasks.pop_back(); - - TSubgoal last = subtasks.back(); - auto decomposed = last->decompose(); - TGoalVec result; - - for(TSubgoal goal : decomposed) - { - if(goal->invalid() || goal == last || vstd::contains(tasks, goal)) - continue; - - auto newComposition = Composition(tasks); - - if(goal->goalType == COMPOSITION) - { - Composition & other = dynamic_cast(*goal); - bool cancel = false; - - for(auto subgoal : other.subtasks) - { - if(subgoal == last || vstd::contains(tasks, subgoal)) - { - cancel = true; - - break; - } - - newComposition.addNext(subgoal); - } - - if(cancel) - continue; - } - else - { - newComposition.addNext(goal); - } - - result.push_back(sptr(newComposition)); - } - - return result; + return subtasks; } Composition & Composition::addNext(const AbstractGoal & goal) @@ -100,7 +55,16 @@ Composition & Composition::addNext(const AbstractGoal & goal) Composition & Composition::addNext(TSubgoal goal) { - subtasks.push_back(goal); + if(goal->goalType == COMPOSITION) + { + Composition & other = dynamic_cast(*goal); + + vstd::concatenate(subtasks, other.subtasks); + } + else + { + subtasks.push_back(goal); + } return *this; } diff --git a/AI/Nullkiller/Markers/HeroExchange.cpp b/AI/Nullkiller/Markers/HeroExchange.cpp index aab4516cc..d58b6838e 100644 --- a/AI/Nullkiller/Markers/HeroExchange.cpp +++ b/AI/Nullkiller/Markers/HeroExchange.cpp @@ -12,6 +12,7 @@ #include "../VCAI.h" #include "../Engine/Nullkiller.h" #include "../AIUtility.h" +#include "../Analyzers/ArmyManager.h" extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.h b/AI/Nullkiller/Pathfinding/AINodeStorage.h index 2a982008a..44b693c7c 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.h +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.h @@ -11,7 +11,7 @@ #pragma once #define PATHFINDER_TRACE_LEVEL 0 -#define AI_TRACE_LEVEL 2 +#define AI_TRACE_LEVEL 1 #define SCOUT_TURN_DISTANCE_LIMIT 3 #include "../../../lib/CPathfinder.h" diff --git a/AI/Nullkiller/Pathfinding/Actors.cpp b/AI/Nullkiller/Pathfinding/Actors.cpp index 875b2de9e..3206a9aea 100644 --- a/AI/Nullkiller/Pathfinding/Actors.cpp +++ b/AI/Nullkiller/Pathfinding/Actors.cpp @@ -190,7 +190,7 @@ bool HeroExchangeMap::canExchange(const ChainActor * other) uint64_t reinforcment = upgradeInfo.upgradeValue; if(other->creatureSet->Slots().size()) - reinforcment += ai->armyManager->howManyReinforcementsCanGet(actor->creatureSet, other->creatureSet); + reinforcment += ai->armyManager->howManyReinforcementsCanGet(actor->hero, actor->creatureSet, other->creatureSet); #if PATHFINDER_TRACE_LEVEL >= 2 logAi->trace( @@ -291,7 +291,7 @@ CCreatureSet * HeroExchangeMap::tryUpgrade(const CCreatureSet * army, const CGOb CCreatureSet * HeroExchangeMap::pickBestCreatures(const CCreatureSet * army1, const CCreatureSet * army2) const { CCreatureSet * target = new HeroExchangeArmy(); - auto bestArmy = ai->armyManager->getBestArmy(army1, army2); + auto bestArmy = ai->armyManager->getBestArmy(actor->hero, army1, army2); for(auto & slotInfo : bestArmy) { diff --git a/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp b/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp index 4661c7b2b..bd86b5f73 100644 --- a/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp +++ b/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp @@ -90,6 +90,13 @@ namespace AIPathfinding return bypassBattle(source, destination, pathfinderConfig, pathfinderHelper); } + if(destination.nodeObject + && (destination.nodeObject->ID == Obj::GARRISON || destination.nodeObject->ID == Obj::GARRISON2) + && destination.objectRelations == PlayerRelations::ENEMIES) + { + return bypassBattle(source, destination, pathfinderConfig, pathfinderHelper); + } + return false; } diff --git a/AI/Nullkiller/VCAI.cpp b/AI/Nullkiller/VCAI.cpp index be4ceb006..38400b059 100644 --- a/AI/Nullkiller/VCAI.cpp +++ b/AI/Nullkiller/VCAI.cpp @@ -803,11 +803,33 @@ void VCAI::pickBestCreatures(const CArmedInstance * destinationArmy, const CArme { const CArmedInstance * armies[] = {destinationArmy, source}; - auto bestArmy = nullkiller->armyManager->getSortedSlots(destinationArmy, source); + auto bestArmy = nullkiller->armyManager->getBestArmy(destinationArmy, destinationArmy, source); //foreach best type -> iterate over slots in both armies and if it's the appropriate type, send it to the slot where it belongs - for(SlotID i = SlotID(0); i.getNum() < bestArmy.size() && i.validSlot(); i.advance(1)) //i-th strongest creature type will go to i-th slot + for(SlotID i = SlotID(0); i.validSlot(); i.advance(1)) //i-th strongest creature type will go to i-th slot { + if(i.getNum() >= bestArmy.size()) + { + if(destinationArmy->hasStackAtSlot(i)) + { + auto creature = destinationArmy->getCreature(i); + auto targetSlot = source->getSlotFor(creature); + + if(targetSlot.validSlot()) + { + // remove unwanted creatures + cb->mergeOrSwapStacks(destinationArmy, source, i, targetSlot); + } + else if(destinationArmy->getStack(i).getPower() < destinationArmy->getArmyStrength() / 100) + { + // dismiss creatures if the amount is small + cb->dismissCreature(destinationArmy, i); + } + } + + continue; + } + const CCreature * targetCreature = bestArmy[i.getNum()].creature; for(auto armyPtr : armies)