From 148c3436df3cd007f5e66d12c241b7e09d8e91c5 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 5 Mar 2023 15:42:15 +0200 Subject: [PATCH] NKAI: improve army gathering --- AI/Nullkiller/AIGateway.cpp | 20 +++--- AI/Nullkiller/Analyzers/HeroManager.cpp | 2 +- .../Behaviors/CaptureObjectsBehavior.cpp | 19 ++++-- .../Behaviors/GatherArmyBehavior.cpp | 63 +++++++++++++------ AI/Nullkiller/Engine/FuzzyHelper.cpp | 16 ++++- AI/Nullkiller/Engine/Nullkiller.cpp | 2 +- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 28 ++++++--- 7 files changed, 106 insertions(+), 44 deletions(-) diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index e9a714c6e..5485d5b88 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -590,13 +590,18 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vectorID; - + auto topObj = objects.front()->id == hero->id ? objects.back() : objects.front(); + auto objType = topObj->ID; // top object should be our hero + auto goalObjectID = nullkiller->getTargetObject(); auto ratio = (float)nullkiller->dangerEvaluator->evaluateDanger(target, hero.get()) / (float)hero->getTotalStrength(); - bool dangerUnknown = ratio == 0; - bool dangerTooHigh = ratio > (1 / SAFE_ATTACK_CONSTANT); - answer = objects.back()->id == nullkiller->getTargetObject(); // no if we do not aim to visit this object + answer = topObj->id == goalObjectID; // no if we do not aim to visit this object + logAi->trace("Query hook: %s(%s) by %s danger ratio %f", target.toString(), topObj->getObjectName(), hero.name, ratio); + + if(cb->getObj(goalObjectID, false)) + { + logAi->trace("AI expected %s", cb->getObj(goalObjectID, false)->getObjectName()); + } if(objType == Obj::BORDERGUARD || objType == Obj::QUEST_GUARD) { @@ -604,9 +609,10 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vectortrace("Guarded object query hook: %s by %s danger ratio %f", target.toString(), hero.name, ratio); + bool dangerUnknown = ratio == 0; + bool dangerTooHigh = ratio > (1 / SAFE_ATTACK_CONSTANT); - answer = dangerUnknown || dangerTooHigh; + answer = !dangerUnknown && !dangerTooHigh; } } diff --git a/AI/Nullkiller/Analyzers/HeroManager.cpp b/AI/Nullkiller/Analyzers/HeroManager.cpp index 6faca6d97..edea22385 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -109,7 +109,7 @@ void HeroManager::update() return scores.at(h1) > scores.at(h2); }; - int globalMainCount = std::min(((int)myHeroes.size() + 2) / 3, cb->getMapSize().x / 100 + 1); + int globalMainCount = std::min(((int)myHeroes.size() + 2) / 3, cb->getMapSize().x / 50 + 1); std::sort(myHeroes.begin(), myHeroes.end(), scoreSort); diff --git a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp index c3a2b443e..8717900b6 100644 --- a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp +++ b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp @@ -81,7 +81,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector auto hero = path.targetHero; auto danger = path.getTotalDanger(); - if(ai->nullkiller->heroManager->getHeroRole(hero) == HeroRole::SCOUT && danger == 0 && path.exchangeCount > 1) + if(ai->nullkiller->heroManager->getHeroRole(hero) == HeroRole::SCOUT && path.exchangeCount > 1) continue; auto firstBlockedAction = path.getFirstBlockedAction(); @@ -126,8 +126,13 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector sharedPtr.reset(newWay); - if(!closestWay || closestWay->movementCost() > path.movementCost()) + auto heroRole = ai->nullkiller->heroManager->getHeroRole(path.targetHero); + + if(heroRole == HeroRole::SCOUT + && (!closestWay || closestWay->movementCost() > path.movementCost())) + { closestWay = &path; + } if(!ai->nullkiller->arePathHeroesLocked(path)) { @@ -137,11 +142,13 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector } } - assert(closestWay || waysToVisitObj.empty()); - for(auto way : waysToVisitObj) + if(closestWay) { - way->closestWayRatio - = closestWay->movementCost() / way->getPath().movementCost(); + for(auto way : waysToVisitObj) + { + way->closestWayRatio + = closestWay->movementCost() / way->getPath().movementCost(); + } } return tasks; diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp index 6e0bb8be0..2ed9e21f1 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp @@ -45,8 +45,7 @@ Goals::TGoalVec GatherArmyBehavior::decompose() const for(const CGHeroInstance * hero : heroes) { - if(ai->nullkiller->heroManager->getHeroRole(hero) == HeroRole::MAIN - && hero->getArmyStrength() >= 300) + if(ai->nullkiller->heroManager->getHeroRole(hero) == HeroRole::MAIN) { vstd::concatenate(tasks, deliverArmyToHero(hero)); } @@ -70,13 +69,6 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her #if NKAI_TRACE_LEVEL >= 1 logAi->trace("Checking ways to gaher army for hero %s, %s", hero->getObjectName(), pos.toString()); #endif - if(ai->nullkiller->isHeroLocked(hero)) - { -#if NKAI_TRACE_LEVEL >= 1 - logAi->trace("Skipping locked hero %s, %s", hero->getObjectName(), pos.toString()); -#endif - return tasks; - } auto paths = ai->nullkiller->pathfinder->getPathInfo(pos); @@ -92,6 +84,14 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her if(path.containsHero(hero)) continue; + if(path.turn() == 0 && hero->inTownGarrison) + { +#if NKAI_TRACE_LEVEL >= 1 + logAi->trace("Skipping garnisoned hero %s, %s", hero->getObjectName(), pos.toString()); +#endif + continue; + } + if(ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path)) { #if NKAI_TRACE_LEVEL >= 2 @@ -124,14 +124,32 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her // avoid trying to move bigger army to the weaker one. if(armyValue > 1) { + bool hasOtherMainInPath = false; + + for(auto node : path.nodes) + { + if(!node.targetHero) continue; + + auto heroRole = ai->nullkiller->heroManager->getHeroRole(node.targetHero); + + if(heroRole == HeroRole::MAIN) + { + hasOtherMainInPath = true; + + break; + } + } + + if(hasOtherMainInPath) + { #if NKAI_TRACE_LEVEL >= 2 - logAi->trace("Army value is too large."); + logAi->trace("Army value is too large."); #endif - continue; + continue; + } } auto danger = path.getTotalDanger(); - auto isSafe = isSafeToVisit(hero, path.heroArmy, danger); #if NKAI_TRACE_LEVEL >= 2 @@ -194,7 +212,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) #if NKAI_TRACE_LEVEL >= 2 logAi->trace("Path found %s", path.toString()); #endif - if(upgrader->visitingHero != path.targetHero) + if(upgrader->visitingHero && upgrader->visitingHero.get() != path.targetHero) { #if NKAI_TRACE_LEVEL >= 2 logAi->trace("Ignore path. Town has visiting hero."); @@ -219,7 +237,10 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) continue; } - if(ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path)) + auto heroRole = ai->nullkiller->heroManager->getHeroRole(path.targetHero); + + if(heroRole == HeroRole::SCOUT + && ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path)) { #if NKAI_TRACE_LEVEL >= 2 logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength()); @@ -228,16 +249,22 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) } auto upgrade = ai->nullkiller->armyManager->calculateCreaturesUpgrade(path.heroArmy, upgrader, availableResources); - auto armyValue = (float)upgrade.upgradeValue / path.getHeroStrength(); if(ai->nullkiller->heroManager->getHeroRole(path.targetHero) == HeroRole::MAIN) { - upgrade.upgradeValue += - ai->nullkiller->armyManager->howManyReinforcementsCanGet(path.targetHero, path.heroArmy, upgrader); + upgrade.upgradeValue += + ai->nullkiller->armyManager->howManyReinforcementsCanGet(path.targetHero, path.heroArmy, upgrader); } - if(armyValue < 0.1f || upgrade.upgradeValue < 300) // avoid small upgrades + auto armyValue = (float)upgrade.upgradeValue / path.getHeroStrength(); + + if(armyValue < 0.25f || upgrade.upgradeValue < 300) // avoid small upgrades + { +#if NKAI_TRACE_LEVEL >= 2 + logAi->trace("Ignore path. Army value is too small (%f)", armyValue); +#endif continue; + } auto danger = path.getTotalDanger(); diff --git a/AI/Nullkiller/Engine/FuzzyHelper.cpp b/AI/Nullkiller/Engine/FuzzyHelper.cpp index 5e6297e27..68fdcd5db 100644 --- a/AI/Nullkiller/Engine/FuzzyHelper.cpp +++ b/AI/Nullkiller/Engine/FuzzyHelper.cpp @@ -115,8 +115,20 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj) { case Obj::TOWN: { - const CGTownInstance * cre = dynamic_cast(obj); - return cre->getUpperArmy()->getArmyStrength(); + const CGTownInstance * town = dynamic_cast(obj); + auto danger = town->getUpperArmy()->getArmyStrength(); + + if(danger || town->visitingHero) + { + auto fortLevel = town->fortLevel(); + + if(fortLevel == CGTownInstance::EFortLevel::CASTLE) + danger += 10000; + else if(fortLevel == CGTownInstance::EFortLevel::CITADEL) + danger += 4000; + } + + return danger; } case Obj::ARTIFACT: case Obj::RESOURCE: diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index dfe15e3a7..7d2059cc7 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -222,7 +222,7 @@ void Nullkiller::makeTurn() boost::lock_guard sharedStorageLock(AISharedStorage::locker); const int MAX_DEPTH = 10; - const int FAST_TASK_MINIMAL_PRIORITY = 0.7; + const float FAST_TASK_MINIMAL_PRIORITY = 0.7; resetAiState(); diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 586505a4e..27674f2f4 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -81,6 +81,12 @@ void PriorityEvaluator::initVisitTile() value = engine->getOutputVariable("Value"); } +bool isAnotherAi(const CGObjectInstance * obj, const CPlayerSpecificInfoCallback & cb) +{ + return obj->getOwner().isValidPlayer() + && cb.getStartInfo()->getIthPlayersSettings(obj->getOwner()).isControlledByAI(); +} + int32_t estimateTownIncome(CCallback * cb, const CGObjectInstance * target, const CGHeroInstance * hero) { auto relations = cb->getPlayerRelations(hero->tempOwner, target->tempOwner); @@ -88,15 +94,17 @@ int32_t estimateTownIncome(CCallback * cb, const CGObjectInstance * target, cons if(relations != PlayerRelations::ENEMIES) return 0; // if we already own it, no additional reward will be received by just visiting it + auto booster = isAnotherAi(target, *cb) ? 1 : 2; + auto town = cb->getTown(target->id); auto fortLevel = town->fortLevel(); - if(town->hasCapitol()) return 4000; + if(town->hasCapitol()) return booster * 2000; // probably well developed town will have city hall - if(fortLevel == CGTownInstance::CASTLE) return 1500; + if(fortLevel == CGTownInstance::CASTLE) return booster * 750; - return town->hasFort() && town->tempOwner != PlayerColor::NEUTRAL ? 1000 : 500; + return booster * (town->hasFort() && town->tempOwner != PlayerColor::NEUTRAL ? booster * 500 : 250); } TResources getCreatureBankResources(const CGObjectInstance * target, const CGHeroInstance * hero) @@ -247,11 +255,12 @@ uint64_t RewardEvaluator::getArmyReward( { auto town = dynamic_cast(target); auto fortLevel = town->fortLevel(); + auto booster = isAnotherAi(town, *ai->cb) ? 1 : 2; if(fortLevel < CGTownInstance::CITADEL) - return town->hasFort() ? 1000 : 0; + return town->hasFort() ? booster * 500 : 0; else - return fortLevel == CGTownInstance::CASTLE ? 10000 : 4000; + return booster * (fortLevel == CGTownInstance::CASTLE ? 5000 : 2000); } case Obj::HILL_FORT: @@ -395,13 +404,14 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons auto town = dynamic_cast(target); auto fortLevel = town->fortLevel(); + auto booster = isAnotherAi(town, *ai->cb) ? 0.3 : 1; if(town->hasCapitol()) return 1; if(fortLevel < CGTownInstance::CITADEL) - return town->hasFort() ? 0.6 : 0.4; + return booster * (town->hasFort() ? 0.6 : 0.4); else - return fortLevel == CGTownInstance::CASTLE ? 0.9 : 0.8; + return booster * (fortLevel == CGTownInstance::CASTLE ? 0.9 : 0.8); } case Obj::HERO: @@ -579,6 +589,7 @@ public: uint64_t upgradeValue = armyUpgrade.getUpgradeValue(); evaluationContext.armyReward += upgradeValue; + evaluationContext.strategicalValue += upgradeValue / armyUpgrade.hero->getTotalStrength(); } }; @@ -590,8 +601,7 @@ void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uin { auto dangerRatio = enemyDanger.danger / (double)ourStrength; auto enemyHero = evaluationContext.evaluator.ai->cb->getObj(enemyDanger.hero.hid, false); - bool isAI =enemyHero - && evaluationContext.evaluator.ai->cb->getStartInfo()->getIthPlayersSettings(enemyHero->getOwner()).isControlledByAI(); + bool isAI = enemyHero && isAnotherAi(enemyHero, *evaluationContext.evaluator.ai->cb); if(isAI) {