From ed76d8a652d1704e967ef2ed4b42732cb47c384e Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 14 Apr 2024 15:23:44 +0300 Subject: [PATCH 1/2] NKAI: allow multiple tasks to be executed from one calculation --- AI/Nullkiller/AIGateway.cpp | 9 +- AI/Nullkiller/AIUtility.cpp | 27 +- AI/Nullkiller/AIUtility.h | 2 +- AI/Nullkiller/Analyzers/ObjectClusterizer.cpp | 142 +++++++-- AI/Nullkiller/Analyzers/ObjectClusterizer.h | 29 +- AI/Nullkiller/Behaviors/ClusterBehavior.cpp | 2 +- AI/Nullkiller/Behaviors/StartupBehavior.cpp | 10 +- AI/Nullkiller/Engine/DeepDecomposer.cpp | 8 +- AI/Nullkiller/Engine/DeepDecomposer.h | 2 +- AI/Nullkiller/Engine/Nullkiller.cpp | 299 ++++++++++++------ AI/Nullkiller/Engine/Nullkiller.h | 25 +- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 29 +- AI/Nullkiller/Goals/AbstractGoal.cpp | 8 +- AI/Nullkiller/Goals/AbstractGoal.h | 17 +- AI/Nullkiller/Goals/AdventureSpellCast.cpp | 6 +- AI/Nullkiller/Goals/AdventureSpellCast.h | 2 +- AI/Nullkiller/Goals/CGoal.h | 31 +- AI/Nullkiller/Goals/Composition.cpp | 32 ++ AI/Nullkiller/Goals/Composition.h | 3 + AI/Nullkiller/Goals/DigAtTile.cpp | 2 +- AI/Nullkiller/Goals/DismissHero.cpp | 8 +- AI/Nullkiller/Goals/DismissHero.h | 6 +- .../Goals/ExchangeSwapTownHeroes.cpp | 20 ++ AI/Nullkiller/Goals/ExchangeSwapTownHeroes.h | 3 + AI/Nullkiller/Goals/ExecuteHeroChain.cpp | 32 ++ AI/Nullkiller/Goals/ExecuteHeroChain.h | 3 + AI/Nullkiller/Goals/StayAtTown.cpp | 2 +- AI/Nullkiller/Markers/ArmyUpgrade.cpp | 2 +- AI/Nullkiller/Markers/DefendTown.cpp | 2 +- AI/Nullkiller/Markers/HeroExchange.cpp | 4 +- AI/Nullkiller/Markers/UnlockCluster.h | 2 +- AI/Nullkiller/Pathfinding/AIPathfinder.cpp | 4 + .../Pathfinding/Actions/QuestAction.cpp | 5 +- 33 files changed, 559 insertions(+), 219 deletions(-) diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 1ce6b6fd4..46833fa68 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -251,6 +251,7 @@ void AIGateway::heroVisit(const CGHeroInstance * visitor, const CGObjectInstance if(start && visitedObj) //we can end visit with null object, anyway { nullkiller->memory->markObjectVisited(visitedObj); + nullkiller->objectClusterizer->invalidate(visitedObj->id); } status.heroVisit(visitedObj, start); @@ -373,6 +374,7 @@ void AIGateway::objectRemoved(const CGObjectInstance * obj, const PlayerColor & return; nullkiller->memory->removeFromMemory(obj); + nullkiller->objectClusterizer->onObjectRemoved(obj->id); if(nullkiller->baseGraph && nullkiller->settings->isObjectGraphAllowed()) { @@ -1152,11 +1154,6 @@ void AIGateway::retrieveVisitableObjs() { for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false)) { - if(!obj->appearance) - { - logAi->error("Bad!"); - } - addVisitableObj(obj); } }); @@ -1401,7 +1398,7 @@ void AIGateway::tryRealize(Goals::DigAtTile & g) assert(g.hero->visitablePos() == g.tile); //surely we want to crash here? if(g.hero->diggingStatus() == EDiggingStatus::CAN_DIG) { - cb->dig(g.hero.get()); + cb->dig(g.hero); } else { diff --git a/AI/Nullkiller/AIUtility.cpp b/AI/Nullkiller/AIUtility.cpp index 0c05aae8d..95d4b502a 100644 --- a/AI/Nullkiller/AIUtility.cpp +++ b/AI/Nullkiller/AIUtility.cpp @@ -91,33 +91,10 @@ bool HeroPtr::operator<(const HeroPtr & rhs) const const CGHeroInstance * HeroPtr::get(bool doWeExpectNull) const { - //TODO? check if these all assertions every time we get info about hero affect efficiency - // - //behave terribly when attempting unauthorized access to hero that is not ours (or was lost) - assert(doWeExpectNull || h); - - if(h) - { - auto obj = cb->getObj(hid); - //const bool owned = obj && obj->tempOwner == ai->playerID; - - if(doWeExpectNull && !obj) - { - return nullptr; - } - else - { - if (!obj) - logAi->error("Accessing no longer accessible hero %s!", h->getNameTranslated()); - //assert(obj); - //assert(owned); - } - } - - return h; + return get(cb); } -const CGHeroInstance * HeroPtr::get(CCallback * cb, bool doWeExpectNull) const +const CGHeroInstance * HeroPtr::get(const CPlayerSpecificInfoCallback * cb, bool doWeExpectNull) const { //TODO? check if these all assertions every time we get info about hero affect efficiency // diff --git a/AI/Nullkiller/AIUtility.h b/AI/Nullkiller/AIUtility.h index b08cffef7..e09ca2a9d 100644 --- a/AI/Nullkiller/AIUtility.h +++ b/AI/Nullkiller/AIUtility.h @@ -109,7 +109,7 @@ public: } const CGHeroInstance * get(bool doWeExpectNull = false) const; - const CGHeroInstance * get(CCallback * cb, bool doWeExpectNull = false) const; + const CGHeroInstance * get(const CPlayerSpecificInfoCallback * cb, bool doWeExpectNull = false) const; bool validAndSet() const; diff --git a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp index 9b95fb18e..77b465610 100644 --- a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp +++ b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp @@ -20,7 +20,7 @@ void ObjectCluster::addObject(const CGObjectInstance * obj, const AIPath & path, { ClusterObjects::accessor info; - objects.insert(info, ClusterObjects::value_type(obj, ClusterObjectInfo())); + objects.insert(info, ClusterObjects::value_type(obj->id, ClusterObjectInfo())); if(info->second.priority < priority) { @@ -31,15 +31,14 @@ void ObjectCluster::addObject(const CGObjectInstance * obj, const AIPath & path, } } -const CGObjectInstance * ObjectCluster::calculateCenter() const +const CGObjectInstance * ObjectCluster::calculateCenter(const CPlayerSpecificInfoCallback * cb) const { - auto v = getObjects(); auto tile = int3(0); float priority = 0; - for(auto pair : objects) + for(auto & pair : objects) { - auto newPoint = pair.first->visitablePos(); + auto newPoint = cb->getObj(pair.first)->visitablePos(); float newPriority = std::pow(pair.second.priority, 4); // lets make high priority targets more weghtful int3 direction = newPoint - tile; float priorityRatio = newPriority / (priority + newPriority); @@ -48,21 +47,21 @@ const CGObjectInstance * ObjectCluster::calculateCenter() const priority += newPriority; } - auto closestPair = *vstd::minElementByFun(objects, [&](const std::pair & pair) -> int + auto closestPair = *vstd::minElementByFun(objects, [&](const std::pair & pair) -> int { - return pair.first->visitablePos().dist2dSQ(tile); + return cb->getObj(pair.first)->visitablePos().dist2dSQ(tile); }); - return closestPair.first; + return cb->getObj(closestPair.first); } -std::vector ObjectCluster::getObjects() const +std::vector ObjectCluster::getObjects(const CPlayerSpecificInfoCallback * cb) const { std::vector result; - for(auto pair : objects) + for(auto & pair : objects) { - result.push_back(pair.first); + result.push_back(cb->getObj(pair.first)); } return result; @@ -70,19 +69,19 @@ std::vector ObjectCluster::getObjects() const std::vector ObjectClusterizer::getNearbyObjects() const { - return nearObjects.getObjects(); + return nearObjects.getObjects(ai->cb.get()); } std::vector ObjectClusterizer::getFarObjects() const { - return farObjects.getObjects(); + return farObjects.getObjects(ai->cb.get()); } std::vector> ObjectClusterizer::getLockedClusters() const { std::vector> result; - for(auto pair : blockedObjects) + for(auto & pair : blockedObjects) { result.push_back(pair.second); } @@ -163,6 +162,69 @@ const CGObjectInstance * ObjectClusterizer::getBlocker(const AIPath & path) cons return nullptr; } +void ObjectClusterizer::invalidate(ObjectInstanceID id) +{ + nearObjects.objects.erase(id); + farObjects.objects.erase(id); + invalidated.push_back(id); + + for(auto & c : blockedObjects) + { + c.second->objects.erase(id); + } +} + +void ObjectClusterizer::validateObjects() +{ + std::vector toRemove; + + auto scanRemovedObjects = [this, &toRemove](const ObjectCluster & cluster) + { + for(auto & pair : cluster.objects) + { + if(!ai->cb->getObj(pair.first, false)) + toRemove.push_back(pair.first); + } + }; + + scanRemovedObjects(nearObjects); + scanRemovedObjects(farObjects); + + for(auto & pair : blockedObjects) + { + if(!ai->cb->getObj(pair.first, false) || pair.second->objects.empty()) + toRemove.push_back(pair.first); + else + scanRemovedObjects(*pair.second); + } + + vstd::removeDuplicates(toRemove); + + for(auto id : toRemove) + { + onObjectRemoved(id); + } +} + +void ObjectClusterizer::onObjectRemoved(ObjectInstanceID id) +{ + invalidate(id); + + vstd::erase_if_present(invalidated, id); + + NKAI::ClusterMap::accessor cluster; + + if(blockedObjects.find(cluster, id)) + { + for(auto & unlocked : cluster->second->objects) + { + invalidated.push_back(unlocked.first); + } + + blockedObjects.erase(cluster); + } +} + bool ObjectClusterizer::shouldVisitObject(const CGObjectInstance * obj) const { if(isObjectRemovable(obj)) @@ -222,17 +284,45 @@ Obj ObjectClusterizer::IgnoredObjectTypes[] = { void ObjectClusterizer::clusterize() { - auto start = std::chrono::high_resolution_clock::now(); + if(isUpToDate) + { + validateObjects(); + } - nearObjects.reset(); - farObjects.reset(); - blockedObjects.clear(); + if(isUpToDate && invalidated.empty()) + return; + + auto start = std::chrono::high_resolution_clock::now(); logAi->debug("Begin object clusterization"); - std::vector objs( - ai->memory->visitableObjs.begin(), - ai->memory->visitableObjs.end()); + std::vector objs; + + if(isUpToDate) + { + for(auto id : invalidated) + { + auto obj = cb->getObj(id, false); + + if(obj) + { + objs.push_back(obj); + } + } + + invalidated.clear(); + } + else + { + nearObjects.reset(); + farObjects.reset(); + blockedObjects.clear(); + invalidated.clear(); + + objs = std::vector( + ai->memory->visitableObjs.begin(), + ai->memory->visitableObjs.end()); + } #if NKAI_TRACE_LEVEL == 0 tbb::parallel_for(tbb::blocked_range(0, objs.size()), [&](const tbb::blocked_range & r) { @@ -256,16 +346,20 @@ void ObjectClusterizer::clusterize() for(auto pair : blockedObjects) { - logAi->trace("Cluster %s %s count: %i", pair.first->getObjectName(), pair.first->visitablePos().toString(), pair.second->objects.size()); + auto blocker = cb->getObj(pair.first); + + logAi->trace("Cluster %s %s count: %i", blocker->getObjectName(), blocker->visitablePos().toString(), pair.second->objects.size()); #if NKAI_TRACE_LEVEL >= 1 - for(auto obj : pair.second->getObjects()) + for(auto obj : pair.second->getObjects(ai->cb.get())) { logAi->trace("Object %s %s", obj->getObjectName(), obj->visitablePos().toString()); } #endif } + isUpToDate = true; + logAi->trace("Clusterization complete in %ld", timeElapsed(start)); } @@ -381,7 +475,7 @@ void ObjectClusterizer::clusterizeObject( ClusterMap::accessor cluster; blockedObjects.insert( cluster, - ClusterMap::value_type(blocker, std::make_shared(blocker))); + ClusterMap::value_type(blocker->id, std::make_shared(blocker))); cluster->second->addObject(obj, path, priority); diff --git a/AI/Nullkiller/Analyzers/ObjectClusterizer.h b/AI/Nullkiller/Analyzers/ObjectClusterizer.h index 2291478bf..ef584a607 100644 --- a/AI/Nullkiller/Analyzers/ObjectClusterizer.h +++ b/AI/Nullkiller/Analyzers/ObjectClusterizer.h @@ -23,7 +23,15 @@ struct ClusterObjectInfo uint8_t turn; }; -using ClusterObjects = tbb::concurrent_hash_map; +struct ObjectInstanceIDHash +{ + ObjectInstanceID::hash hash; + bool equal(ObjectInstanceID o1, ObjectInstanceID o2) const + { + return o1 == o2; + } +}; +using ClusterObjects = tbb::concurrent_hash_map; struct ObjectCluster { @@ -44,11 +52,11 @@ public: { } - std::vector getObjects() const; - const CGObjectInstance * calculateCenter() const; + std::vector getObjects(const CPlayerSpecificInfoCallback * cb) const; + const CGObjectInstance * calculateCenter(const CPlayerSpecificInfoCallback * cb) const; }; -using ClusterMap = tbb::concurrent_hash_map>; +using ClusterMap = tbb::concurrent_hash_map, ObjectInstanceIDHash>; class ObjectClusterizer { @@ -60,6 +68,8 @@ private: ClusterMap blockedObjects; const Nullkiller * ai; RewardEvaluator valueEvaluator; + bool isUpToDate; + std::vector invalidated; public: void clusterize(); @@ -69,7 +79,16 @@ public: const CGObjectInstance * getBlocker(const AIPath & path) const; std::optional getBlocker(const AIPathNodeInfo & node) const; - ObjectClusterizer(const Nullkiller * ai): ai(ai), valueEvaluator(ai) {} + ObjectClusterizer(const Nullkiller * ai): ai(ai), valueEvaluator(ai), isUpToDate(false){} + + void validateObjects(); + void onObjectRemoved(ObjectInstanceID id); + void invalidate(ObjectInstanceID id); + + void reset() { + isUpToDate = false; + invalidated.clear(); + } private: bool shouldVisitObject(const CGObjectInstance * obj) const; diff --git a/AI/Nullkiller/Behaviors/ClusterBehavior.cpp b/AI/Nullkiller/Behaviors/ClusterBehavior.cpp index dc38363a4..d70c8b201 100644 --- a/AI/Nullkiller/Behaviors/ClusterBehavior.cpp +++ b/AI/Nullkiller/Behaviors/ClusterBehavior.cpp @@ -41,7 +41,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(); + auto center = cluster->calculateCenter(ai->cb.get()); auto paths = ai->pathfinder->getPathInfo(center->visitablePos(), ai->settings->isObjectGraphAllowed()); auto blockerPos = cluster->blocker->visitablePos(); diff --git a/AI/Nullkiller/Behaviors/StartupBehavior.cpp b/AI/Nullkiller/Behaviors/StartupBehavior.cpp index 37573232d..859d4975f 100644 --- a/AI/Nullkiller/Behaviors/StartupBehavior.cpp +++ b/AI/Nullkiller/Behaviors/StartupBehavior.cpp @@ -71,7 +71,15 @@ bool needToRecruitHero(const Nullkiller * ai, const CGTownInstance * startupTown for(auto obj : ai->objectClusterizer->getNearbyObjects()) { - if((obj->ID == Obj::RESOURCE && dynamic_cast(obj)->resourceID() == EGameResID::GOLD) + auto armed = dynamic_cast(obj); + + if(armed && armed->getArmyStrength() > 0) + continue; + + bool isGoldPile = dynamic_cast(obj) + && dynamic_cast(obj)->resourceID() == EGameResID::GOLD; + + if(isGoldPile || obj->ID == Obj::TREASURE_CHEST || obj->ID == Obj::CAMPFIRE || obj->ID == Obj::WATER_WHEEL) diff --git a/AI/Nullkiller/Engine/DeepDecomposer.cpp b/AI/Nullkiller/Engine/DeepDecomposer.cpp index 468433653..85625ecc5 100644 --- a/AI/Nullkiller/Engine/DeepDecomposer.cpp +++ b/AI/Nullkiller/Engine/DeepDecomposer.cpp @@ -37,10 +37,8 @@ void DeepDecomposer::reset() goals.clear(); } -Goals::TGoalVec DeepDecomposer::decompose(TSubgoal behavior, int depthLimit) +void DeepDecomposer::decompose(TGoalVec & result, TSubgoal behavior, int depthLimit) { - TGoalVec tasks; - goals.clear(); goals.resize(depthLimit); decompositionCache.resize(depthLimit); @@ -79,7 +77,7 @@ Goals::TGoalVec DeepDecomposer::decompose(TSubgoal behavior, int depthLimit) #endif if(!isCompositionLoop(subgoal)) { - tasks.push_back(task); + result.push_back(task); if(!fromCache) { @@ -121,8 +119,6 @@ Goals::TGoalVec DeepDecomposer::decompose(TSubgoal behavior, int depthLimit) } } } - - return tasks; } Goals::TSubgoal DeepDecomposer::aggregateGoals(int startDepth, TSubgoal last) diff --git a/AI/Nullkiller/Engine/DeepDecomposer.h b/AI/Nullkiller/Engine/DeepDecomposer.h index 147c2c7d5..7a30d99ee 100644 --- a/AI/Nullkiller/Engine/DeepDecomposer.h +++ b/AI/Nullkiller/Engine/DeepDecomposer.h @@ -35,7 +35,7 @@ private: public: DeepDecomposer(const Nullkiller * ai); void reset(); - Goals::TGoalVec decompose(Goals::TSubgoal behavior, int depthLimit); + void decompose(Goals::TGoalVec & result, Goals::TSubgoal behavior, int depthLimit); private: Goals::TSubgoal aggregateGoals(int startDepth, Goals::TSubgoal last); diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 6bffeae35..24f2a2a33 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -64,16 +64,104 @@ void Nullkiller::init(std::shared_ptr cb, AIGateway * gateway) armyFormation.reset(new ArmyFormation(cb, this)); } -Goals::TTask Nullkiller::choseBestTask(Goals::TTaskVec & tasks) const +TaskPlanItem::TaskPlanItem(TSubgoal task) + :task(task), affectedObjects(task->asTask()->getAffectedObjects()) { - Goals::TTask bestTask = *vstd::maxElementByFun(tasks, [](Goals::TTask task) -> float{ - return task->priority; - }); - - return bestTask; } -Goals::TTask Nullkiller::choseBestTask(Goals::TSubgoal behavior, int decompositionMaxDepth) const +Goals::TTaskVec TaskPlan::getTasks() const +{ + Goals::TTaskVec result; + + for(auto & item : tasks) + { + result.push_back(taskptr(*item.task)); + } + + vstd::removeDuplicates(result); + + return result; +} + +void TaskPlan::merge(TSubgoal task) +{ + TGoalVec blockers; + + for(auto & item : tasks) + { + for(auto objid : item.affectedObjects) + { + if(task == item.task || task->asTask()->isObjectAffected(objid)) + { + if(item.task->asTask()->priority >= task->asTask()->priority) + return; + + blockers.push_back(item.task); + break; + } + } + } + + vstd::erase_if(tasks, [&](const TaskPlanItem & task) + { + return vstd::contains(blockers, task.task); + }); + + tasks.emplace_back(task); +} + +Goals::TTask Nullkiller::choseBestTask(Goals::TGoalVec & tasks) const +{ + if(tasks.empty()) + { + return taskptr(Invalid()); + } + + for(TSubgoal & task : tasks) + { + if(task->asTask()->priority <= 0) + task->asTask()->priority = priorityEvaluator->evaluate(task); + } + + auto bestTask = *vstd::maxElementByFun(tasks, [](Goals::TSubgoal task) -> float + { + return task->asTask()->priority; + }); + + return taskptr(*bestTask); +} + +Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks) const +{ + TaskPlan taskPlan; + + tbb::parallel_for(tbb::blocked_range(0, tasks.size()), [this, &tasks](const tbb::blocked_range & r) + { + auto evaluator = this->priorityEvaluators->acquire(); + + for(size_t i = r.begin(); i != r.end(); i++) + { + auto task = tasks[i]; + + if(task->asTask()->priority <= 0) + task->asTask()->priority = evaluator->evaluate(task); + } + }); + + std::sort(tasks.begin(), tasks.end(), [](TSubgoal g1, TSubgoal g2) -> bool + { + return g2->asTask()->priority < g1->asTask()->priority; + }); + + for(TSubgoal & task : tasks) + { + taskPlan.merge(task); + } + + return taskPlan.getTasks(); +} + +void Nullkiller::decompose(Goals::TGoalVec & result, Goals::TSubgoal behavior, int decompositionMaxDepth) const { boost::this_thread::interruption_point(); @@ -81,38 +169,14 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TSubgoal behavior, int decompositi auto start = std::chrono::high_resolution_clock::now(); - Goals::TGoalVec elementarGoals = decomposer->decompose(behavior, decompositionMaxDepth); - Goals::TTaskVec tasks; + decomposer->decompose(result, behavior, decompositionMaxDepth); boost::this_thread::interruption_point(); - - for(auto goal : elementarGoals) - { - Goals::TTask task = Goals::taskptr(*goal); - - if(task->priority <= 0) - task->priority = priorityEvaluator->evaluate(goal); - - tasks.push_back(task); - } - - if(tasks.empty()) - { - logAi->debug("Behavior %s found no tasks. Time taken %ld", behavior->toString(), timeElapsed(start)); - - return Goals::taskptr(Goals::Invalid()); - } - - auto task = choseBestTask(tasks); logAi->debug( - "Behavior %s returns %s, priority %f. Time taken %ld", + "Behavior %s. Time taken %ld", behavior->toString(), - task->toString(), - task->priority, timeElapsed(start)); - - return task; } void Nullkiller::resetAiState() @@ -124,6 +188,7 @@ void Nullkiller::resetAiState() lockedHeroes.clear(); dangerHitMap->reset(); useHeroChain = true; + objectClusterizer->reset(); if(!baseGraph && settings->isObjectGraphAllowed()) { @@ -251,6 +316,8 @@ void Nullkiller::makeTurn() resetAiState(); + Goals::TGoalVec bestTasks; + for(int i = 1; i <= settings->getMaxPass(); i++) { auto start = std::chrono::high_resolution_clock::now(); @@ -260,16 +327,18 @@ void Nullkiller::makeTurn() for(;i <= settings->getMaxPass(); i++) { - Goals::TTaskVec fastTasks = { - choseBestTask(sptr(BuyArmyBehavior()), 1), - choseBestTask(sptr(BuildingBehavior()), 1) - }; + bestTasks.clear(); - bestTask = choseBestTask(fastTasks); + decompose(bestTasks, sptr(BuyArmyBehavior()), 1); + decompose(bestTasks, sptr(BuildingBehavior()), 1); + + bestTask = choseBestTask(bestTasks); if(bestTask->priority >= FAST_TASK_MINIMAL_PRIORITY) { - executeTask(bestTask); + if(!executeTask(bestTask)) + return; + updateAiState(i, true); } else @@ -278,83 +347,105 @@ void Nullkiller::makeTurn() } } - Goals::TTaskVec bestTasks = { - bestTask, - choseBestTask(sptr(RecruitHeroBehavior()), 1), - choseBestTask(sptr(CaptureObjectsBehavior()), 1), - choseBestTask(sptr(ClusterBehavior()), MAX_DEPTH), - choseBestTask(sptr(DefenceBehavior()), MAX_DEPTH), - choseBestTask(sptr(GatherArmyBehavior()), MAX_DEPTH), - choseBestTask(sptr(StayAtTownBehavior()), MAX_DEPTH) - }; + decompose(bestTasks, sptr(RecruitHeroBehavior()), 1); + decompose(bestTasks, sptr(CaptureObjectsBehavior()), 1); + decompose(bestTasks, sptr(ClusterBehavior()), MAX_DEPTH); + decompose(bestTasks, sptr(DefenceBehavior()), MAX_DEPTH); + decompose(bestTasks, sptr(GatherArmyBehavior()), MAX_DEPTH); + decompose(bestTasks, sptr(StayAtTownBehavior()), MAX_DEPTH); if(cb->getDate(Date::DAY) == 1) { - bestTasks.push_back(choseBestTask(sptr(StartupBehavior()), 1)); + decompose(bestTasks, sptr(StartupBehavior()), 1); } - bestTask = choseBestTask(bestTasks); - - std::string taskDescription = bestTask->toString(); - HeroPtr hero = bestTask->getHero(); - HeroRole heroRole = HeroRole::MAIN; - - if(hero.validAndSet()) - heroRole = heroManager->getHeroRole(hero); - - if(heroRole != HeroRole::MAIN || bestTask->getHeroExchangeCount() <= 1) - useHeroChain = false; - - // TODO: better to check turn distance here instead of priority - if((heroRole != HeroRole::MAIN || bestTask->priority < SMALL_SCAN_MIN_PRIORITY) - && scanDepth == ScanDepth::MAIN_FULL) - { - useHeroChain = false; - scanDepth = ScanDepth::SMALL; - - logAi->trace( - "Goal %s has low priority %f so decreasing scan depth to gain performance.", - taskDescription, - bestTask->priority); - } - - if(bestTask->priority < MIN_PRIORITY) - { - auto heroes = cb->getHeroesInfo(); - auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool - { - return h->movementPointsRemaining() > 100; - }); - - if(hasMp && scanDepth != ScanDepth::ALL_FULL) - { - logAi->trace( - "Goal %s has too low priority %f so increasing scan depth to full.", - taskDescription, - bestTask->priority); - - scanDepth = ScanDepth::ALL_FULL; - useHeroChain = false; - continue; - } - - logAi->trace("Goal %s has too low priority. It is not worth doing it. Ending turn.", taskDescription); - - return; - } + auto selectedTasks = buildPlan(bestTasks); logAi->debug("Decission madel in %ld", timeElapsed(start)); - executeTask(bestTask); + if(selectedTasks.empty()) + { + return; + } + + bool hasAnySuccess = false; + + for(auto bestTask : selectedTasks) + { + std::string taskDescription = bestTask->toString(); + HeroPtr hero = bestTask->getHero(); + HeroRole heroRole = HeroRole::MAIN; + + if(hero.validAndSet()) + heroRole = heroManager->getHeroRole(hero); + + if(heroRole != HeroRole::MAIN || bestTask->getHeroExchangeCount() <= 1) + useHeroChain = false; + + // TODO: better to check turn distance here instead of priority + if((heroRole != HeroRole::MAIN || bestTask->priority < SMALL_SCAN_MIN_PRIORITY) + && scanDepth == ScanDepth::MAIN_FULL) + { + useHeroChain = false; + scanDepth = ScanDepth::SMALL; + + logAi->trace( + "Goal %s has low priority %f so decreasing scan depth to gain performance.", + taskDescription, + bestTask->priority); + } + + if(bestTask->priority < MIN_PRIORITY) + { + auto heroes = cb->getHeroesInfo(); + auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool + { + return h->movementPointsRemaining() > 100; + }); + + if(hasMp && scanDepth != ScanDepth::ALL_FULL) + { + logAi->trace( + "Goal %s has too low priority %f so increasing scan depth to full.", + taskDescription, + bestTask->priority); + + scanDepth = ScanDepth::ALL_FULL; + useHeroChain = false; + hasAnySuccess = true; + break;; + } + + logAi->trace("Goal %s has too low priority. It is not worth doing it.", taskDescription); + + continue; + } + + if(!executeTask(bestTask)) + { + if(hasAnySuccess) + break; + else + return; + } + + hasAnySuccess = true; + } + + if(!hasAnySuccess) + { + logAi->trace("Nothing was done this turn. Ending turn."); + return; + } if(i == settings->getMaxPass()) { - logAi->warn("Goal %s exceeded maxpass. Terminating AI turn.", taskDescription); + logAi->warn("Maxpass exceeded. Terminating AI turn."); } } } -void Nullkiller::executeTask(Goals::TTask task) +bool Nullkiller::executeTask(Goals::TTask task) { auto start = std::chrono::high_resolution_clock::now(); std::string taskDescr = task->toString(); @@ -376,10 +467,10 @@ void Nullkiller::executeTask(Goals::TTask task) logAi->error("Failed to realize subgoal of type %s, I will stop.", taskDescr); logAi->error("The error message was: %s", e.what()); -#if NKAI_TRACE_LEVEL == 0 - throw; // will be recatched and AI turn ended -#endif + return false; } + + return true; } TResources Nullkiller::getFreeResources() const diff --git a/AI/Nullkiller/Engine/Nullkiller.h b/AI/Nullkiller/Engine/Nullkiller.h index ae6425b84..a79062755 100644 --- a/AI/Nullkiller/Engine/Nullkiller.h +++ b/AI/Nullkiller/Engine/Nullkiller.h @@ -47,6 +47,24 @@ enum class ScanDepth ALL_FULL = 2 }; +struct TaskPlanItem +{ + std::vector affectedObjects; + Goals::TSubgoal task; + + TaskPlanItem(Goals::TSubgoal goal); +}; + +class TaskPlan +{ +private: + std::vector tasks; + +public: + Goals::TTaskVec getTasks() const; + void merge(Goals::TSubgoal task); +}; + class Nullkiller { private: @@ -102,9 +120,10 @@ public: private: void resetAiState(); void updateAiState(int pass, bool fast = false); - Goals::TTask choseBestTask(Goals::TSubgoal behavior, int decompositionMaxDepth) const; - Goals::TTask choseBestTask(Goals::TTaskVec & tasks) const; - void executeTask(Goals::TTask task); + void decompose(Goals::TGoalVec & result, Goals::TSubgoal behavior, int decompositionMaxDepth) const; + Goals::TTask choseBestTask(Goals::TGoalVec & tasks) const; + Goals::TTaskVec buildPlan(Goals::TGoalVec & tasks) const; + bool executeTask(Goals::TTask task); }; } diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 2c87d4a1c..389c7bd96 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -655,7 +655,7 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG case Obj::RESOURCE: { auto * res = dynamic_cast(target); - return res->resourceID() == GameResID::GOLD ? 600 : 100; + return res && res->resourceID() == GameResID::GOLD ? 600 : 100; } case Obj::TREASURE_CHEST: return 1500; @@ -711,8 +711,8 @@ public: uint64_t armyStrength = heroExchange.getReinforcementArmyStrength(evaluationContext.evaluator.ai); - evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero.get()->getArmyStrength()); - evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero.get()); + evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength()); + evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero); } }; @@ -743,7 +743,7 @@ public: Goals::StayAtTown & stayAtTown = dynamic_cast(*task); - evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero().get()); + evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero()); evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted(); evaluationContext.movementCost += stayAtTown.getMovementWasted(); } @@ -792,7 +792,7 @@ public: if(defendTown.getTurn() > 0 && defendTown.isCounterAttack()) { auto ourSpeed = defendTown.hero->movementPointsLimit(true); - auto enemySpeed = treat.hero->movementPointsLimit(true); + auto enemySpeed = treat.hero.get(evaluationContext.evaluator.ai->cb.get())->movementPointsLimit(true); if(enemySpeed > ourSpeed) multiplier *= 0.7f; } @@ -847,13 +847,12 @@ public: evaluationContext.movementCostByRole[role] += pair.second; } - auto heroPtr = task->hero; - auto hero = heroPtr.get(ai->cb.get()); + auto hero = task->hero; bool checkGold = evaluationContext.danger == 0; auto army = path.heroArmy; const CGObjectInstance * target = ai->cb->getObj((ObjectInstanceID)task->objid, false); - auto heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroPtr); + auto heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(hero); if(heroRole == HeroRole::MAIN) evaluationContext.heroRole = heroRole; @@ -887,21 +886,21 @@ public: Goals::UnlockCluster & clusterGoal = dynamic_cast(*task); std::shared_ptr cluster = clusterGoal.getCluster(); - auto hero = clusterGoal.hero.get(); - auto role = evaluationContext.evaluator.ai->heroManager->getHeroRole(clusterGoal.hero); + auto hero = clusterGoal.hero; + auto role = evaluationContext.evaluator.ai->heroManager->getHeroRole(hero); - std::vector> objects(cluster->objects.begin(), cluster->objects.end()); + std::vector> objects(cluster->objects.begin(), cluster->objects.end()); - std::sort(objects.begin(), objects.end(), [](std::pair o1, std::pair o2) -> bool + std::sort(objects.begin(), objects.end(), [](std::pair o1, std::pair o2) -> bool { return o1.second.priority > o2.second.priority; }); int boost = 1; - for(auto objInfo : objects) + for(auto & objInfo : objects) { - auto target = objInfo.first; + auto target = evaluationContext.evaluator.ai->cb->getObj(objInfo.first); bool checkGold = objInfo.second.danger == 0; auto army = hero; @@ -960,7 +959,7 @@ public: return; Goals::DismissHero & dismissCommand = dynamic_cast(*task); - const CGHeroInstance * dismissedHero = dismissCommand.getHero().get(); + const CGHeroInstance * dismissedHero = dismissCommand.getHero(); auto role = ai->heroManager->getHeroRole(dismissedHero); auto mpLeft = dismissedHero->movementPointsRemaining(); diff --git a/AI/Nullkiller/Goals/AbstractGoal.cpp b/AI/Nullkiller/Goals/AbstractGoal.cpp index 98f6a4c49..d71b905b1 100644 --- a/AI/Nullkiller/Goals/AbstractGoal.cpp +++ b/AI/Nullkiller/Goals/AbstractGoal.cpp @@ -31,12 +31,12 @@ TTask Goals::taskptr(const AbstractGoal & tmp) if(!tmp.isElementar()) throw cannotFulfillGoalException(tmp.toString() + " is not elementar"); - ptr.reset(dynamic_cast(tmp.clone())); + ptr.reset(tmp.clone()->asTask()); return ptr; } -std::string AbstractGoal::toString() const //TODO: virtualize +std::string AbstractGoal::toString() const { std::string desc; switch(goalType) @@ -63,8 +63,10 @@ std::string AbstractGoal::toString() const //TODO: virtualize default: return std::to_string(goalType); } - if(hero.get(true)) //FIXME: used to crash when we lost hero and failed goal + + if(hero) desc += " (" + hero->getNameTranslated() + ")"; + return desc; } diff --git a/AI/Nullkiller/Goals/AbstractGoal.h b/AI/Nullkiller/Goals/AbstractGoal.h index bbd2d5148..18eee0265 100644 --- a/AI/Nullkiller/Goals/AbstractGoal.h +++ b/AI/Nullkiller/Goals/AbstractGoal.h @@ -10,9 +10,8 @@ #pragma once #include "../../../lib/VCMI_Lib.h" -#include "../../../lib/CBuildingHandler.h" -#include "../../../lib/CCreatureHandler.h" -#include "../../../lib/CTownHandler.h" +#include "../../../lib/mapObjects/CGTownInstance.h" +#include "../../../lib/mapObjects/CGHeroInstance.h" #include "../AIUtility.h" namespace NKAI @@ -106,7 +105,7 @@ namespace Goals int objid; SETTER(int, objid) int aid; SETTER(int, aid) int3 tile; SETTER(int3, tile) - HeroPtr hero; SETTER(HeroPtr, hero) + const CGHeroInstance * hero; SETTER(CGHeroInstance *, hero) const CGTownInstance *town; SETTER(CGTownInstance *, town) int bid; SETTER(int, bid) @@ -119,6 +118,7 @@ namespace Goals objid = -1; tile = int3(-1, -1, -1); town = nullptr; + hero = nullptr; bid = -1; goldCost = 0; } @@ -147,6 +147,11 @@ namespace Goals virtual bool hasHash() const { return false; } virtual uint64_t getHash() const { return 0; } + + virtual ITask * asTask() + { + throw std::runtime_error("Abstract goal is not a task"); + } bool operator!=(const AbstractGoal & g) const { @@ -165,9 +170,11 @@ namespace Goals //TODO: make accept work for std::shared_ptr... somehow virtual void accept(AIGateway * ai) = 0; //unhandled goal will report standard error virtual std::string toString() const = 0; - virtual HeroPtr getHero() const = 0; + virtual const CGHeroInstance * getHero() const = 0; virtual ~ITask() {} virtual int getHeroExchangeCount() const = 0; + virtual bool isObjectAffected(ObjectInstanceID h) const = 0; + virtual std::vector getAffectedObjects() const = 0; }; } diff --git a/AI/Nullkiller/Goals/AdventureSpellCast.cpp b/AI/Nullkiller/Goals/AdventureSpellCast.cpp index 17b24c0c5..1868d7c60 100644 --- a/AI/Nullkiller/Goals/AdventureSpellCast.cpp +++ b/AI/Nullkiller/Goals/AdventureSpellCast.cpp @@ -18,12 +18,12 @@ using namespace Goals; bool AdventureSpellCast::operator==(const AdventureSpellCast & other) const { - return hero.h == other.hero.h; + return hero == other.hero; } void AdventureSpellCast::accept(AIGateway * ai) { - if(!hero.validAndSet()) + if(!hero) throw cannotFulfillGoalException("Invalid hero!"); auto spell = getSpell(); @@ -56,7 +56,7 @@ void AdventureSpellCast::accept(AIGateway * ai) auto wait = cb->waitTillRealize; cb->waitTillRealize = true; - cb->castSpell(hero.h, spellID, tile); + cb->castSpell(hero, spellID, tile); if(town && spellID == SpellID::TOWN_PORTAL) { diff --git a/AI/Nullkiller/Goals/AdventureSpellCast.h b/AI/Nullkiller/Goals/AdventureSpellCast.h index de403905a..febb43662 100644 --- a/AI/Nullkiller/Goals/AdventureSpellCast.h +++ b/AI/Nullkiller/Goals/AdventureSpellCast.h @@ -22,7 +22,7 @@ namespace Goals SpellID spellID; public: - AdventureSpellCast(HeroPtr hero, SpellID spellID) + AdventureSpellCast(const CGHeroInstance * hero, SpellID spellID) : ElementarGoal(Goals::ADVENTURE_SPELL_CAST), spellID(spellID) { sethero(hero); diff --git a/AI/Nullkiller/Goals/CGoal.h b/AI/Nullkiller/Goals/CGoal.h index ab8db512b..9a5821eae 100644 --- a/AI/Nullkiller/Goals/CGoal.h +++ b/AI/Nullkiller/Goals/CGoal.h @@ -14,7 +14,6 @@ namespace NKAI { -struct HeroPtr; class AIGateway; namespace Goals @@ -92,9 +91,37 @@ namespace Goals bool isElementar() const override { return true; } - HeroPtr getHero() const override { return AbstractGoal::hero; } + const CGHeroInstance * getHero() const override { return AbstractGoal::hero; } int getHeroExchangeCount() const override { return 0; } + + bool isObjectAffected(ObjectInstanceID id) const override + { + return (AbstractGoal::hero && AbstractGoal::hero->id == id) + || AbstractGoal::objid == id + || (AbstractGoal::town && AbstractGoal::town->id == id); + } + + std::vector getAffectedObjects() const override + { + auto result = std::vector(); + + if(AbstractGoal::hero) + result.push_back(AbstractGoal::hero->id); + + if(AbstractGoal::objid != -1) + result.push_back(ObjectInstanceID(AbstractGoal::objid)); + + if(AbstractGoal::town) + result.push_back(AbstractGoal::town->id); + + return result; + } + + ITask * asTask() override + { + return this; + } }; } diff --git a/AI/Nullkiller/Goals/Composition.cpp b/AI/Nullkiller/Goals/Composition.cpp index beb8275cf..190767a6b 100644 --- a/AI/Nullkiller/Goals/Composition.cpp +++ b/AI/Nullkiller/Goals/Composition.cpp @@ -117,4 +117,36 @@ int Composition::getHeroExchangeCount() const return result; } +std::vector Composition::getAffectedObjects() const +{ + std::vector affectedObjects; + + for(auto sequence : subtasks) + { + for(auto task : sequence) + { + if(task->isElementar()) + vstd::concatenate(affectedObjects, task->asTask()->getAffectedObjects()); + } + } + + vstd::removeDuplicates(affectedObjects); + + return affectedObjects; +} + +bool Composition::isObjectAffected(ObjectInstanceID id) const +{ + for(auto sequence : subtasks) + { + for(auto task : sequence) + { + if(task->isElementar() && task->asTask()->isObjectAffected(id)) + return true; + } + } + + return false; +} + } diff --git a/AI/Nullkiller/Goals/Composition.h b/AI/Nullkiller/Goals/Composition.h index 08db692a0..b8a6bdd07 100644 --- a/AI/Nullkiller/Goals/Composition.h +++ b/AI/Nullkiller/Goals/Composition.h @@ -35,6 +35,9 @@ namespace Goals TGoalVec decompose(const Nullkiller * ai) const override; bool isElementar() const override; int getHeroExchangeCount() const override; + + std::vector getAffectedObjects() const override; + bool isObjectAffected(ObjectInstanceID id) const override; }; } diff --git a/AI/Nullkiller/Goals/DigAtTile.cpp b/AI/Nullkiller/Goals/DigAtTile.cpp index 6dce16b59..ec7ddfd92 100644 --- a/AI/Nullkiller/Goals/DigAtTile.cpp +++ b/AI/Nullkiller/Goals/DigAtTile.cpp @@ -20,7 +20,7 @@ using namespace Goals; bool DigAtTile::operator==(const DigAtTile & other) const { - return other.hero.h == hero.h && other.tile == tile; + return other.hero == hero && other.tile == tile; } // //TSubgoal DigAtTile::decomposeSingle() const diff --git a/AI/Nullkiller/Goals/DismissHero.cpp b/AI/Nullkiller/Goals/DismissHero.cpp index 543b16e29..b2a70fb05 100644 --- a/AI/Nullkiller/Goals/DismissHero.cpp +++ b/AI/Nullkiller/Goals/DismissHero.cpp @@ -18,22 +18,22 @@ using namespace Goals; bool DismissHero::operator==(const DismissHero & other) const { - return hero.h == other.hero.h; + return hero == other.hero; } void DismissHero::accept(AIGateway * ai) { - if(!hero.validAndSet()) + if(!hero) throw cannotFulfillGoalException("Invalid hero!"); - cb->dismissHero(hero.h); + cb->dismissHero(hero); throw goalFulfilledException(sptr(*this)); } std::string DismissHero::toString() const { - return "DismissHero " + hero.name; + return "DismissHero " + heroName; } } diff --git a/AI/Nullkiller/Goals/DismissHero.h b/AI/Nullkiller/Goals/DismissHero.h index 98bfdf1fa..39116daf1 100644 --- a/AI/Nullkiller/Goals/DismissHero.h +++ b/AI/Nullkiller/Goals/DismissHero.h @@ -17,11 +17,15 @@ namespace Goals { class DLL_EXPORT DismissHero : public ElementarGoal { + private: + std::string heroName; + public: - DismissHero(HeroPtr hero) + DismissHero(const CGHeroInstance * hero) : ElementarGoal(Goals::DISMISS_HERO) { sethero(hero); + heroName = hero->getNameTranslated(); } void accept(AIGateway * ai) override; diff --git a/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp b/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp index 12ff31847..e03901910 100644 --- a/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp +++ b/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp @@ -26,6 +26,26 @@ ExchangeSwapTownHeroes::ExchangeSwapTownHeroes( { } +std::vector ExchangeSwapTownHeroes::getAffectedObjects() const +{ + std::vector affectedObjects = { town->id }; + + if(town->garrisonHero) + affectedObjects.push_back(town->garrisonHero->id); + + if(town->visitingHero) + affectedObjects.push_back(town->visitingHero->id); + + return affectedObjects; +} + +bool ExchangeSwapTownHeroes::isObjectAffected(ObjectInstanceID id) const +{ + return town->id == id + || (town->visitingHero && town->visitingHero->id == id) + || (town->garrisonHero && town->garrisonHero->id == id); +} + std::string ExchangeSwapTownHeroes::toString() const { return "Exchange and swap heroes of " + town->getNameTranslated(); diff --git a/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.h b/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.h index 8a86c364e..714350a7b 100644 --- a/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.h +++ b/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.h @@ -35,6 +35,9 @@ namespace Goals const CGHeroInstance * getGarrisonHero() const { return garrisonHero; } HeroLockedReason getLockingReason() const { return lockingReason; } + + std::vector getAffectedObjects() const override; + bool isObjectAffected(ObjectInstanceID id) const override; }; } diff --git a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp index e8a2c3192..48568ba53 100644 --- a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp +++ b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp @@ -42,6 +42,38 @@ bool ExecuteHeroChain::operator==(const ExecuteHeroChain & other) const && chainPath.chainMask == other.chainPath.chainMask; } +std::vector ExecuteHeroChain::getAffectedObjects() const +{ + std::vector affectedObjects = { chainPath.targetHero->id }; + + if(objid != -1) + affectedObjects.push_back(ObjectInstanceID(objid)); + + for(auto & node : chainPath.nodes) + { + if(node.targetHero) + affectedObjects.push_back(node.targetHero->id); + } + + vstd::removeDuplicates(affectedObjects); + + return affectedObjects; +} + +bool ExecuteHeroChain::isObjectAffected(ObjectInstanceID id) const +{ + if(chainPath.targetHero->id == id || objid == id) + return true; + + for(auto & node : chainPath.nodes) + { + if(node.targetHero && node.targetHero->id == id) + return true; + } + + return false; +} + void ExecuteHeroChain::accept(AIGateway * ai) { logAi->debug("Executing hero chain towards %s. Path %s", targetName, chainPath.toString()); diff --git a/AI/Nullkiller/Goals/ExecuteHeroChain.h b/AI/Nullkiller/Goals/ExecuteHeroChain.h index 3018f8eec..5ab09c4df 100644 --- a/AI/Nullkiller/Goals/ExecuteHeroChain.h +++ b/AI/Nullkiller/Goals/ExecuteHeroChain.h @@ -34,6 +34,9 @@ namespace Goals int getHeroExchangeCount() const override { return chainPath.exchangeCount; } + std::vector getAffectedObjects() const override; + bool isObjectAffected(ObjectInstanceID id) const override; + private: bool moveHeroToTile(AIGateway * ai, const CGHeroInstance * hero, const int3 & tile); }; diff --git a/AI/Nullkiller/Goals/StayAtTown.cpp b/AI/Nullkiller/Goals/StayAtTown.cpp index 82342cb51..346b2c44d 100644 --- a/AI/Nullkiller/Goals/StayAtTown.cpp +++ b/AI/Nullkiller/Goals/StayAtTown.cpp @@ -46,7 +46,7 @@ void StayAtTown::accept(AIGateway * ai) logAi->error("Hero %s expected visiting town %s", hero->getNameTranslated(), town->getNameTranslated()); } - ai->nullkiller->lockHero(hero.get(), HeroLockedReason::DEFENCE); + ai->nullkiller->lockHero(hero, HeroLockedReason::DEFENCE); } } diff --git a/AI/Nullkiller/Markers/ArmyUpgrade.cpp b/AI/Nullkiller/Markers/ArmyUpgrade.cpp index ff61a6454..21f060486 100644 --- a/AI/Nullkiller/Markers/ArmyUpgrade.cpp +++ b/AI/Nullkiller/Markers/ArmyUpgrade.cpp @@ -22,7 +22,7 @@ ArmyUpgrade::ArmyUpgrade(const AIPath & upgradePath, const CGObjectInstance * up : CGoal(Goals::ARMY_UPGRADE), upgrader(upgrader), upgradeValue(upgrade.upgradeValue), initialValue(upgradePath.heroArmy->getArmyStrength()), goldCost(upgrade.upgradeCost[EGameResID::GOLD]) { - sethero(upgradePath.targetHero); + hero = upgradePath.targetHero; } ArmyUpgrade::ArmyUpgrade(const CGHeroInstance * targetMain, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade) diff --git a/AI/Nullkiller/Markers/DefendTown.cpp b/AI/Nullkiller/Markers/DefendTown.cpp index dd7d08e25..123b87b5e 100644 --- a/AI/Nullkiller/Markers/DefendTown.cpp +++ b/AI/Nullkiller/Markers/DefendTown.cpp @@ -22,7 +22,7 @@ DefendTown::DefendTown(const CGTownInstance * town, const HitMapInfo & treat, co : CGoal(Goals::DEFEND_TOWN), treat(treat), defenceArmyStrength(defencePath.getHeroStrength()), turn(defencePath.turn()), counterattack(isCounterAttack) { settown(town); - sethero(defencePath.targetHero); + hero = defencePath.targetHero; } DefendTown::DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const CGHeroInstance * defender) diff --git a/AI/Nullkiller/Markers/HeroExchange.cpp b/AI/Nullkiller/Markers/HeroExchange.cpp index 68c5e6f4f..80117aeeb 100644 --- a/AI/Nullkiller/Markers/HeroExchange.cpp +++ b/AI/Nullkiller/Markers/HeroExchange.cpp @@ -26,12 +26,12 @@ bool HeroExchange::operator==(const HeroExchange & other) const std::string HeroExchange::toString() const { - return "Hero exchange for " +hero.get()->getObjectName() + " by " + exchangePath.toString(); + return "Hero exchange for " +hero->getObjectName() + " by " + exchangePath.toString(); } uint64_t HeroExchange::getReinforcementArmyStrength(const Nullkiller * ai) const { - uint64_t armyValue = ai->armyManager->howManyReinforcementsCanGet(hero.get(), exchangePath.heroArmy); + uint64_t armyValue = ai->armyManager->howManyReinforcementsCanGet(hero, exchangePath.heroArmy); return armyValue; } diff --git a/AI/Nullkiller/Markers/UnlockCluster.h b/AI/Nullkiller/Markers/UnlockCluster.h index e504a3318..ee18802d1 100644 --- a/AI/Nullkiller/Markers/UnlockCluster.h +++ b/AI/Nullkiller/Markers/UnlockCluster.h @@ -33,7 +33,7 @@ namespace Goals : CGoal(Goals::UNLOCK_CLUSTER), cluster(cluster), pathToCenter(pathToCenter) { tile = cluster->blocker->visitablePos(); - sethero(pathToCenter.targetHero); + hero = pathToCenter.targetHero; } bool operator==(const UnlockCluster & other) const override; diff --git a/AI/Nullkiller/Pathfinding/AIPathfinder.cpp b/AI/Nullkiller/Pathfinding/AIPathfinder.cpp index 0d4623aca..a46584649 100644 --- a/AI/Nullkiller/Pathfinding/AIPathfinder.cpp +++ b/AI/Nullkiller/Pathfinding/AIPathfinder.cpp @@ -105,7 +105,11 @@ void AIPathfinder::updatePaths(const std::map cb->calculatePaths(config); if(!pathfinderSettings.useHeroChain) + { + logAi->trace("Recalculated paths in %ld", timeElapsed(start)); + return; + } do { diff --git a/AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp b/AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp index c4f71fc32..ab29a7177 100644 --- a/AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp +++ b/AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp @@ -35,7 +35,10 @@ namespace AIPathfinding return dynamic_cast(questInfo.obj)->checkQuest(hero); } - return questInfo.quest->activeForPlayers.count(hero->getOwner()) + auto notActivated = !questInfo.obj->wasVisited(ai->playerID) + && !questInfo.quest->activeForPlayers.count(hero->getOwner()); + + return notActivated || questInfo.quest->checkQuest(hero); } From 3939c4b9d39d5d88553d51cd4417b6add4c8da75 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sat, 20 Apr 2024 10:33:37 +0300 Subject: [PATCH 2/2] NKAI: stability fixes and regression --- AI/Nullkiller/AIGateway.cpp | 19 ++++++++++++++++++- AI/Nullkiller/AIUtility.cpp | 18 ++++++++++++++++-- AI/Nullkiller/AIUtility.h | 1 + AI/Nullkiller/Engine/Nullkiller.cpp | 5 ++++- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 6 ++++-- AI/Nullkiller/Goals/ExecuteHeroChain.cpp | 7 +++++++ client/CMT.cpp | 2 ++ 7 files changed, 52 insertions(+), 6 deletions(-) diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 46833fa68..d1fc2a4a5 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -1111,7 +1111,24 @@ void AIGateway::recruitCreatures(const CGDwelling * d, const CArmedInstance * re if(!recruiter->getSlotFor(creID).validSlot()) { - continue; + for(auto stack : recruiter->Slots()) + { + if(!stack.second->type) + continue; + + auto duplicatingSlot = recruiter->getSlotFor(stack.second->type); + + if(duplicatingSlot != stack.first) + { + cb->mergeStacks(recruiter, recruiter, stack.first, duplicatingSlot); + break; + } + } + + if(!recruiter->getSlotFor(creID).validSlot()) + { + continue; + } } vstd::amin(count, cb->getResourceAmount() / creID.toCreature()->getFullRecruitCost()); diff --git a/AI/Nullkiller/AIUtility.cpp b/AI/Nullkiller/AIUtility.cpp index 95d4b502a..8e5ae878d 100644 --- a/AI/Nullkiller/AIUtility.cpp +++ b/AI/Nullkiller/AIUtility.cpp @@ -91,7 +91,7 @@ bool HeroPtr::operator<(const HeroPtr & rhs) const const CGHeroInstance * HeroPtr::get(bool doWeExpectNull) const { - return get(cb); + return get(cb, doWeExpectNull); } const CGHeroInstance * HeroPtr::get(const CPlayerSpecificInfoCallback * cb, bool doWeExpectNull) const @@ -300,6 +300,19 @@ uint64_t timeElapsed(std::chrono::time_point return std::chrono::duration_cast(end - start).count(); } +int getDuplicatingSlots(const CArmedInstance * army) +{ + int duplicatingSlots = 0; + + for(auto stack : army->Slots()) + { + if(stack.second->type && army->getSlotFor(stack.second->type) != stack.first) + duplicatingSlots++; + } + + return duplicatingSlots; +} + // todo: move to obj manager bool shouldVisit(const Nullkiller * ai, const CGHeroInstance * h, const CGObjectInstance * obj) { @@ -347,13 +360,14 @@ bool shouldVisit(const Nullkiller * ai, const CGHeroInstance * h, const CGObject return false; const CGDwelling * d = dynamic_cast(obj); + auto duplicatingSlotsCount = getDuplicatingSlots(h); for(auto level : d->creatures) { for(auto c : level.second) { if(level.first - && h->getSlotFor(CreatureID(c)) != SlotID() + && (h->getSlotFor(CreatureID(c)) != SlotID() || duplicatingSlotsCount > 0) && ai->cb->getResourceAmount().canAfford(c.toCreature()->getFullRecruitCost())) { return true; diff --git a/AI/Nullkiller/AIUtility.h b/AI/Nullkiller/AIUtility.h index e09ca2a9d..787913a43 100644 --- a/AI/Nullkiller/AIUtility.h +++ b/AI/Nullkiller/AIUtility.h @@ -242,6 +242,7 @@ uint64_t timeElapsed(std::chrono::time_point // todo: move to obj manager bool shouldVisit(const Nullkiller * ai, const CGHeroInstance * h, const CGObjectInstance * obj); +int getDuplicatingSlots(const CArmedInstance * army); template class SharedPool diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 24f2a2a33..cb551da84 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -318,7 +318,7 @@ void Nullkiller::makeTurn() Goals::TGoalVec bestTasks; - for(int i = 1; i <= settings->getMaxPass(); i++) + for(int i = 1; i <= settings->getMaxPass() && cb->getPlayerStatus(playerID) == EPlayerStatus::INGAME; i++) { auto start = std::chrono::high_resolution_clock::now(); updateAiState(i); @@ -372,6 +372,9 @@ void Nullkiller::makeTurn() for(auto bestTask : selectedTasks) { + if(cb->getPlayerStatus(playerID) != EPlayerStatus::INGAME) + return; + std::string taskDescription = bestTask->toString(); HeroPtr hero = bestTask->getHero(); HeroRole heroRole = HeroRole::MAIN; diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 389c7bd96..40e2d2e80 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -158,6 +158,8 @@ uint64_t getCreatureBankArmyReward(const CGObjectInstance * target, const CGHero const auto& slots = hero->Slots(); ui64 weakestStackPower = 0; + int duplicatingSlots = getDuplicatingSlots(hero); + if (slots.size() >= GameConstants::ARMY_SIZE) { //No free slot, we might discard our weakest stack @@ -172,7 +174,7 @@ uint64_t getCreatureBankArmyReward(const CGObjectInstance * target, const CGHero { //Only if hero has slot for this creature in the army auto ccre = dynamic_cast(c.data.type); - if (hero->getSlotFor(ccre).validSlot()) + if (hero->getSlotFor(ccre).validSlot() || duplicatingSlots > 0) { result += (c.data.type->getAIValue() * c.data.count) * c.chance; } @@ -1125,7 +1127,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task) } #if NKAI_TRACE_LEVEL >= 2 - logAi->trace("Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %d, danger: %d, role: %s, strategical value: %f, cwr: %f, fear: %f, result %f", + logAi->trace("Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, danger: %d, role: %s, strategical value: %f, cwr: %f, fear: %f, result %f", task->toString(), evaluationContext.armyLossPersentage, (int)evaluationContext.turn, diff --git a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp index 48568ba53..54a2005ef 100644 --- a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp +++ b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp @@ -104,6 +104,13 @@ void ExecuteHeroChain::accept(AIGateway * ai) const CGHeroInstance * hero = node->targetHero; HeroPtr heroPtr = hero; + if(!heroPtr.validAndSet()) + { + logAi->error("Hero %s was lost. Exit hero chain.", heroPtr.name); + + return; + } + if(node->parentIndex >= i) { logAi->error("Invalid parentIndex while executing node " + node->coord.toString()); diff --git a/client/CMT.cpp b/client/CMT.cpp index 6ae29ab22..d23c15277 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -375,6 +375,8 @@ int main(int argc, char * argv[]) while(!headlessQuit) boost::this_thread::sleep_for(boost::chrono::milliseconds(200)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(500)); + quitApplication(); }