From 88fb532d904564fd0763aff51fb62539f3da50ed Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Mon, 13 Mar 2023 19:58:44 +0200 Subject: [PATCH 1/3] NKAI: more fixes to defense and pandora --- AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp | 2 +- AI/Nullkiller/Behaviors/DefenceBehavior.cpp | 4 ++-- AI/Nullkiller/Engine/FuzzyHelper.cpp | 3 +-- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 6 ++++-- config/ai/object-priorities.txt | 3 +++ 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp index ce065b4ca..b17f2e026 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp @@ -70,7 +70,7 @@ void DangerHitMapAnalyzer::updateHitMap() auto turn = path.turn(); auto & node = hitMap[pos.x][pos.y][pos.z]; - if(tileDanger > node.maximumDanger.danger + if(tileDanger / (turn + 1) > node.maximumDanger.danger / (node.maximumDanger.turn + 1) || (tileDanger == node.maximumDanger.danger && node.maximumDanger.turn > turn)) { node.maximumDanger.danger = tileDanger; diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp index 8964e173e..54c3d159c 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp @@ -30,7 +30,7 @@ namespace NKAI extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; -const double TREAT_IGNORE_RATIO = 0.5; +const float TREAT_IGNORE_RATIO = 2; using namespace Goals; @@ -133,7 +133,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta tasks.push_back(Goals::sptr(composition)); } - bool treatIsWeak = path.getHeroStrength() / treat.danger > TREAT_IGNORE_RATIO; + bool treatIsWeak = path.getHeroStrength() / (float)treat.danger > TREAT_IGNORE_RATIO; bool needToSaveGrowth = treat.turn == 0 && dayOfWeek == 7; if(treatIsWeak && !needToSaveGrowth) diff --git a/AI/Nullkiller/Engine/FuzzyHelper.cpp b/AI/Nullkiller/Engine/FuzzyHelper.cpp index d0c5d7108..540e58240 100644 --- a/AI/Nullkiller/Engine/FuzzyHelper.cpp +++ b/AI/Nullkiller/Engine/FuzzyHelper.cpp @@ -130,8 +130,6 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj) return danger; } - case Obj::PANDORAS_BOX: - return 10000; //Who knows what awaits us there case Obj::ARTIFACT: case Obj::RESOURCE: @@ -148,6 +146,7 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj) case Obj::CREATURE_GENERATOR4: case Obj::MINE: case Obj::ABANDONED_MINE: + case Obj::PANDORAS_BOX: { const CArmedInstance * a = dynamic_cast(obj); return a->getArmyStrength(); diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 25f6e0091..e90f14a4b 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -781,9 +781,11 @@ public: if(garrisonHero && swapCommand.getLockingReason() == HeroLockedReason::DEFENCE) { auto defenderRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(garrisonHero); + auto mpLeft = garrisonHero->movement / (float)garrisonHero->maxMovePoints(true); - evaluationContext.movementCost += garrisonHero->movement; - evaluationContext.movementCostByRole[defenderRole] += garrisonHero->movement; + evaluationContext.movementCost += mpLeft; + evaluationContext.movementCostByRole[defenderRole] += mpLeft; + evaluationContext.heroRole = defenderRole; } } }; diff --git a/config/ai/object-priorities.txt b/config/ai/object-priorities.txt index 18f2a605d..b90a350f7 100644 --- a/config/ai/object-priorities.txt +++ b/config/ai/object-priorities.txt @@ -191,7 +191,10 @@ RuleBlock: gold reward rule: if armyReward is LOW and heroRole is MAIN and danger is NONE and mainTurnDistance is LOWEST then Value is HIGH rule: if skillReward is LOW and heroRole is MAIN and armyLoss is LOW then Value is BITHIGH rule: if skillReward is MEDIUM and heroRole is MAIN and armyLoss is LOW and fear is not HIGH then Value is BITHIGH + rule: if skillReward is MEDIUM and heroRole is MAIN and rewardType is MIXED and armyLoss is LOW and fear is not HIGH then Value is HIGH with 0.5 rule: if skillReward is HIGH and heroRole is MAIN and armyLoss is LOW and fear is not HIGH then Value is HIGH + rule: if skillReward is MEDIUM and heroRole is SCOUT then Value is LOWEST + rule: if skillReward is HIGH and heroRole is SCOUT then Value is LOWEST rule: if strategicalValue is LOW and heroRole is MAIN and armyLoss is LOW then Value is BITHIGH rule: if strategicalValue is LOWEST and heroRole is MAIN and armyLoss is LOW then Value is LOW rule: if strategicalValue is LOW and heroRole is SCOUT and armyLoss is LOW and fear is not HIGH then Value is HIGH with 0.5 From 06fcbf891b8d7b22eacaf546e02b2335a4453c14 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Thu, 16 Mar 2023 20:39:15 +0200 Subject: [PATCH 2/3] Fix battle AI using Fire elementals (had score 0) --- AI/BattleAI/BattleAI.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index 0b7e41f05..99a5c922e 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -596,9 +596,18 @@ void CBattleAI::attemptCastingSpell() size_t ourUnits = 0; - for(auto unit : all) + std::set unitIds; + + state.battleGetUnitsIf([&](const battle::Unit * u)->bool + { + if(!u->isGhost() && !u->isTurret()) + unitIds.insert(u->unitId()); + + return false; + }); + + for(auto unitId : unitIds) { - auto unitId = unit->unitId(); auto localUnit = state.battleGetUnitByID(unitId); newHealthOfStack[unitId] = localUnit->getAvailableHealth(); @@ -620,9 +629,8 @@ void CBattleAI::attemptCastingSpell() { int64_t totalGain = 0; - for(auto unit : all) + for(auto unitId : unitIds) { - auto unitId = unit->unitId(); auto localUnit = state.battleGetUnitByID(unitId); auto newValue = getValOr(newValueOfStack, unitId, 0); From ab84cb99315414625c41e45c8388f3b345101718 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 19 Mar 2023 19:04:12 +0200 Subject: [PATCH 3/3] NKAI: fix defense tasks priority evaluation --- .../Analyzers/DangerHitMapAnalyzer.cpp | 2 +- AI/Nullkiller/Behaviors/DefenceBehavior.cpp | 20 ++++++++- .../Behaviors/GatherArmyBehavior.cpp | 43 ++++++++++++------- AI/Nullkiller/Engine/Nullkiller.cpp | 4 +- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 7 ++- 5 files changed, 56 insertions(+), 20 deletions(-) diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp index b17f2e026..9b8e891fb 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp @@ -70,7 +70,7 @@ void DangerHitMapAnalyzer::updateHitMap() auto turn = path.turn(); auto & node = hitMap[pos.x][pos.y][pos.z]; - if(tileDanger / (turn + 1) > node.maximumDanger.danger / (node.maximumDanger.turn + 1) + if(tileDanger / (turn / 3 + 1) > node.maximumDanger.danger / (node.maximumDanger.turn / 3 + 1) || (tileDanger == node.maximumDanger.danger && node.maximumDanger.turn > turn)) { node.maximumDanger.danger = tileDanger; diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp index 54c3d159c..78c8fe261 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp @@ -244,7 +244,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta continue; } - if(path.targetHero == town->visitingHero && path.exchangeCount == 1) + if(path.targetHero == town->visitingHero.get() && path.exchangeCount == 1) { #if NKAI_TRACE_LEVEL >= 1 logAi->trace("Put %s to garrison of town %s", @@ -265,6 +265,24 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta continue; } + + // main without army and visiting scout with army, very specific case + if(town->visitingHero && town->getUpperArmy()->stacksCount() == 0 + && path.targetHero != town->visitingHero.get() && path.exchangeCount == 1 && path.turn() == 0 + && ai->nullkiller->heroManager->evaluateHero(path.targetHero) > ai->nullkiller->heroManager->evaluateHero(town->visitingHero.get()) + && 10 * path.targetHero->getTotalStrength() < town->visitingHero->getTotalStrength()) + { + path.heroArmy = town->visitingHero.get(); + + tasks.push_back( + Goals::sptr(Composition() + .addNext(DefendTown(town, treat, path)) + .addNext(ExchangeSwapTownHeroes(town, town->visitingHero.get())) + .addNext(ExecuteHeroChain(path, town)) + .addNext(ExchangeSwapTownHeroes(town, path.targetHero, HeroLockedReason::DEFENCE)))); + + continue; + } if(treat.turn == 0 || (path.turn() <= treat.turn && path.getHeroStrength() * SAFE_ATTACK_CONSTANT >= treat.danger)) { diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp index b1a46d555..b2005d5f1 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp @@ -65,6 +65,7 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her { Goals::TGoalVec tasks; const int3 pos = hero->visitablePos(); + auto targetHeroScore = ai->nullkiller->heroManager->evaluateHero(hero); #if NKAI_TRACE_LEVEL >= 1 logAi->trace("Checking ways to gaher army for hero %s, %s", hero->getObjectName(), pos.toString()); @@ -113,7 +114,7 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her float armyValue = (float)heroExchange.getReinforcementArmyStrength() / hero->getArmyStrength(); // avoid transferring very small amount of army - if(armyValue < 0.1f) + if(armyValue < 0.1f && armyValue < 20000) { #if NKAI_TRACE_LEVEL >= 2 logAi->trace("Army value is too small."); @@ -122,31 +123,33 @@ 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) { - bool hasOtherMainInPath = false; + if(!node.targetHero) continue; - for(auto node : path.nodes) + auto heroRole = ai->nullkiller->heroManager->getHeroRole(node.targetHero); + + if(heroRole == HeroRole::MAIN) { - if(!node.targetHero) continue; + auto score = ai->nullkiller->heroManager->evaluateHero(node.targetHero); - auto heroRole = ai->nullkiller->heroManager->getHeroRole(node.targetHero); - - if(heroRole == HeroRole::MAIN) + if(score >= targetHeroScore) { hasOtherMainInPath = true; break; } } + } - if(hasOtherMainInPath) - { + 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(); @@ -180,7 +183,17 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her #if NKAI_TRACE_LEVEL >= 2 logAi->trace("Action is blocked. Considering decomposition."); #endif - composition.addNext(blockedAction->decompose(path.targetHero)); + auto subGoal = blockedAction->decompose(path.targetHero); + + if(subGoal->invalid()) + { +#if NKAI_TRACE_LEVEL >= 1 + logAi->trace("Path is invalid. Skipping"); +#endif + continue; + } + + composition.addNext(subGoal); } tasks.push_back(sptr(composition)); @@ -261,7 +274,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) auto armyValue = (float)upgrade.upgradeValue / path.getHeroStrength(); - if(armyValue < 0.25f || upgrade.upgradeValue < 300) // avoid small upgrades + if((armyValue < 0.1f && armyValue < 20000) || upgrade.upgradeValue < 300) // avoid small upgrades { #if NKAI_TRACE_LEVEL >= 2 logAi->trace("Ignore path. Army value is too small (%f)", armyValue); diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 85f7a6442..b4e85fc97 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -299,6 +299,7 @@ void Nullkiller::makeTurn() void Nullkiller::executeTask(Goals::TTask task) { + auto start = std::chrono::high_resolution_clock::now(); std::string taskDescr = task->toString(); boost::this_thread::interruption_point(); @@ -307,10 +308,11 @@ void Nullkiller::executeTask(Goals::TTask task) try { task->accept(ai.get()); + logAi->trace("Task %s completed in %lld", taskDescr, timeElapsed(start)); } catch(goalFulfilledException &) { - logAi->trace("Task %s completed", task->toString()); + logAi->trace("Task %s completed in %lld", taskDescr, timeElapsed(start)); } catch(cannotFulfillGoalException & e) { diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index e90f14a4b..ac079ae77 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -591,7 +591,7 @@ public: uint64_t upgradeValue = armyUpgrade.getUpgradeValue(); evaluationContext.armyReward += upgradeValue; - evaluationContext.strategicalValue += upgradeValue / armyUpgrade.hero->getTotalStrength(); + evaluationContext.strategicalValue += upgradeValue / (float)armyUpgrade.hero->getArmyStrength(); } }; @@ -627,7 +627,7 @@ private: continue; auto creature = creatureInfo.second.back().toCreature(); - result += creature->AIValue * town->getGrowthInfo(creature->level).totalGrowth(); + result += creature->AIValue * town->getGrowthInfo(creature->getLevel() - 1).totalGrowth(); } return result; @@ -648,6 +648,9 @@ public: auto strategicalValue = std::sqrt(armyIncome / 20000.0f) + dailyIncome / 3000.0f; + if(evaluationContext.evaluator.ai->buildAnalyzer->getDevelopmentInfo().size() == 1) + strategicalValue = 1; + float multiplier = 1; if(treat.turn < defendTown.getTurn())