From 38a98387e490be61d3d11a4870758241d87a6b68 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sat, 13 May 2023 10:25:40 +0300 Subject: [PATCH 01/21] Temp fix for blocked in garrison ai. --- AI/Nullkiller/Pathfinding/AINodeStorage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index c0d650464..2fbd50a72 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -879,7 +879,7 @@ void AINodeStorage::setHeroes(std::map heroes) for(auto & hero : heroes) { // do not allow our own heroes in garrison to act on map - if(hero.first->getOwner() == ai->playerID && hero.first->inTownGarrison) + if(hero.first->getOwner() == ai->playerID && hero.first->inTownGarrison && ai->isHeroLocked(hero.first)) continue; uint64_t mask = FirstActorMask << actors.size(); From b1ca663eb61e110a7cc048fa3e017d04922da7eb Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 14 May 2023 09:17:15 +0300 Subject: [PATCH 02/21] Fuzzy rework --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 69 +++++++++++- AI/Nullkiller/Engine/PriorityEvaluator.h | 3 + AI/Nullkiller/Pathfinding/AINodeStorage.cpp | 2 +- config/ai/object-priorities.txt | 113 ++++---------------- 4 files changed, 93 insertions(+), 94 deletions(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 05be5a68c..eabd52403 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -49,7 +49,8 @@ EvaluationContext::EvaluationContext(const Nullkiller * ai) turn(0), strategicalValue(0), evaluator(ai), - enemyHeroDangerRatio(0) + enemyHeroDangerRatio(0), + armyGrowth(0) { } @@ -64,6 +65,7 @@ void PriorityEvaluator::initVisitTile() std::string str = std::string((char *)file.first.get(), file.second); engine = fl::FllImporter().fromString(str); armyLossPersentageVariable = engine->getInputVariable("armyLoss"); + armyGrowthVariable = engine->getInputVariable("armyGrowth"); heroRoleVariable = engine->getInputVariable("heroRole"); dangerVariable = engine->getInputVariable("danger"); turnVariable = engine->getInputVariable("turn"); @@ -164,7 +166,7 @@ uint64_t getCreatureBankArmyReward(const CGObjectInstance * target, const CGHero return result; } -uint64_t getDwellingScore(CCallback * cb, const CGObjectInstance * target, bool checkGold) +uint64_t getDwellingArmyValue(CCallback * cb, const CGObjectInstance * target, bool checkGold) { auto dwelling = dynamic_cast(target); uint64_t score = 0; @@ -185,6 +187,27 @@ uint64_t getDwellingScore(CCallback * cb, const CGObjectInstance * target, bool return score; } +uint64_t getDwellingArmyGrowth(CCallback * cb, const CGObjectInstance * target, PlayerColor myColor) +{ + auto dwelling = dynamic_cast(target); + uint64_t score = 0; + + if(dwelling->getOwner() == myColor) + return 0; + + for(auto & creLevel : dwelling->creatures) + { + if(creLevel.second.size()) + { + auto creature = creLevel.second.back().toCreature(); + + score += creature->getAIValue() * creature->getGrowth(); + } + } + + return score; +} + int getDwellingArmyCost(const CGObjectInstance * target) { auto dwelling = dynamic_cast(target); @@ -272,7 +295,7 @@ uint64_t RewardEvaluator::getArmyReward( case Obj::CREATURE_GENERATOR2: case Obj::CREATURE_GENERATOR3: case Obj::CREATURE_GENERATOR4: - return getDwellingScore(ai->cb.get(), target, checkGold); + return getDwellingArmyValue(ai->cb.get(), target, checkGold); case Obj::CRYPT: case Obj::SHIPWRECK: case Obj::SHIPWRECK_SURVIVOR: @@ -293,6 +316,44 @@ uint64_t RewardEvaluator::getArmyReward( } } +uint64_t RewardEvaluator::getArmyGrowth( + const CGObjectInstance * target, + const CGHeroInstance * hero, + const CCreatureSet * army) const +{ + const float enemyArmyEliminationRewardRatio = 0.5f; + + if(!target) + return 0; + + switch(target->ID) + { + case Obj::TOWN: + { + auto town = dynamic_cast(target); + auto fortLevel = town->fortLevel(); + auto neutral = !town->getOwner().isValidPlayer(); + auto booster = isAnotherAi(town, *ai->cb) || neutral ? 1 : 2; + + if(fortLevel < CGTownInstance::CITADEL) + return town->hasFort() ? booster * 500 : 0; + else + return booster * (fortLevel == CGTownInstance::CASTLE ? 5000 : 2000); + } + + case Obj::CREATURE_GENERATOR1: + case Obj::CREATURE_GENERATOR2: + case Obj::CREATURE_GENERATOR3: + case Obj::CREATURE_GENERATOR4: + return getDwellingArmyGrowth(ai->cb.get(), target, hero->getOwner()); + case Obj::ARTIFACT: + // it is not supported now because hero will not sit in town on 7th day but later parts of legion may be counted as army growth as well. + return 0; + default: + return 0; + } +} + int RewardEvaluator::getGoldCost(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army) const { if(!target) @@ -714,6 +775,7 @@ public: { evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero); evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold); + evaluationContext.armyGrowth += evaluationContext.evaluator.getArmyGrowth(target, hero, army); evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, evaluationContext.heroRole); evaluationContext.strategicalValue += evaluationContext.evaluator.getStrategicalValue(target); evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army); @@ -920,6 +982,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task) scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]); goldRewardVariable->setValue(evaluationContext.goldReward); armyRewardVariable->setValue(evaluationContext.armyReward); + armyGrowthVariable->setValue(evaluationContext.armyGrowth); skillRewardVariable->setValue(evaluationContext.skillReward); dangerVariable->setValue(evaluationContext.danger); rewardTypeVariable->setValue(rewardType); diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index 840169970..a07ebf3f6 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -33,6 +33,7 @@ public: RewardEvaluator(const Nullkiller * ai) : ai(ai) {} uint64_t getArmyReward(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army, bool checkGold) const; + uint64_t getArmyGrowth(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army) const; int getGoldCost(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army) const; float getEnemyHeroStrategicalValue(const CGHeroInstance * enemy) const; float getResourceRequirementStrength(int resType) const; @@ -54,6 +55,7 @@ struct DLL_EXPORT EvaluationContext float closestWayRatio; float armyLossPersentage; float armyReward; + uint64_t armyGrowth; int32_t goldReward; int32_t goldCost; float skillReward; @@ -95,6 +97,7 @@ private: fl::InputVariable * turnVariable; fl::InputVariable * goldRewardVariable; fl::InputVariable * armyRewardVariable; + fl::InputVariable * armyGrowthVariable; fl::InputVariable * dangerVariable; fl::InputVariable * skillRewardVariable; fl::InputVariable * strategicalValueVariable; diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index 2fbd50a72..c0d650464 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -879,7 +879,7 @@ void AINodeStorage::setHeroes(std::map heroes) for(auto & hero : heroes) { // do not allow our own heroes in garrison to act on map - if(hero.first->getOwner() == ai->playerID && hero.first->inTownGarrison && ai->isHeroLocked(hero.first)) + if(hero.first->getOwner() == ai->playerID && hero.first->inTownGarrison) continue; uint64_t mask = FirstActorMask << actors.size(); diff --git a/config/ai/object-priorities.txt b/config/ai/object-priorities.txt index b90a350f7..1714c8477 100644 --- a/config/ai/object-priorities.txt +++ b/config/ai/object-priorities.txt @@ -121,106 +121,39 @@ InputVariable: fear term: LOW Triangle 0.000 0.500 1.000 term: MEDIUM Triangle 0.500 1.000 1.500 term: HIGH Ramp 1.000 1.800 +InputVariable: armyGrowth + enabled: true + range: 0.000 20000.000 + lock-range: false + term: NONE Ramp 100.000 0.000 + term: SMALL Triangle 0.000 1000.000 3000.000 + term: MEDIUM Triangle 1000.000 3000.000 8000.000 + term: BIG Triangle 3000.000 8000.000 20000.000 + term: HUGE Ramp 8000.000 20000.000 OutputVariable: Value enabled: true - range: -0.500 1.500 + range: -1.500 2.000 lock-range: false aggregation: AlgebraicSum defuzzifier: Centroid 100 default: 0.500 lock-previous: false - term: LOWEST Discrete -0.500 0.000 -0.500 1.000 -0.200 1.000 -0.200 0.000 0.200 0.000 0.200 1.000 0.500 1.000 0.500 0.000 0.500 - term: BITLOW Rectangle -0.010 0.010 0.500 - term: LOW Discrete -0.150 0.000 -0.150 1.000 -0.050 1.000 -0.050 0.000 0.050 0.000 0.050 1.000 0.150 1.000 0.150 0.000 0.500 - term: MEDIUM Triangle 0.450 0.500 0.550 0.050 - term: HIGH Discrete 0.850 0.000 0.850 1.000 0.950 1.000 0.950 0.000 1.050 0.000 1.050 1.000 1.150 1.000 1.150 0.000 0.500 - term: HIGHEST Discrete 0.500 0.000 0.500 1.000 0.800 1.000 0.800 0.000 1.200 0.000 1.200 1.000 1.500 1.000 1.500 0.000 0.500 - term: BITHIGH Rectangle 0.990 1.010 0.500 + term: WORST Binary -1.000 -inf 0.500 + term: BAD Rectangle -1.000 -0.700 0.500 + term: BASE Rectangle -0.200 0.200 0.400 + term: LOW Rectangle 1.110 1.190 0.320 + term: HIGHEST Discrete 0.300 0.000 0.300 1.000 0.600 1.000 0.600 0.000 1.700 0.000 1.700 1.000 2.000 1.000 2.000 0.000 0.500 + term: HIGH Discrete 0.600 0.000 0.600 1.000 0.850 1.000 0.850 0.000 1.450 0.000 1.450 1.000 1.700 1.000 1.700 0.000 0.400 + term: BITHIGH Discrete 0.850 0.000 0.850 1.000 1.000 1.000 1.000 0.000 1.300 0.000 1.300 1.000 1.450 1.000 1.450 0.000 0.350 + term: MEDIUM Discrete 1.000 0.000 1.000 1.000 1.100 1.000 1.100 0.000 1.200 0.000 1.200 1.000 1.300 1.000 1.300 0.000 0.330 RuleBlock: gold reward enabled: true conjunction: AlgebraicProduct disjunction: AlgebraicSum implication: AlgebraicProduct activation: General - rule: if turn is NOW and mainTurnDistance is very LONG and heroRole is SCOUT then Value is LOW with 0.5 - rule: if turn is NOW and mainTurnDistance is LONG and heroRole is SCOUT then Value is LOW with 0.3 - rule: if turn is NOW and scoutTurnDistance is LONG and heroRole is SCOUT then Value is LOW with 0.3 - rule: if turn is NOW and mainTurnDistance is LONG and heroRole is MAIN then Value is LOW with 0.3 - rule: if turn is NEXT and mainTurnDistance is very LONG and heroRole is SCOUT then Value is LOW with 0.8 - rule: if turn is NEXT and scoutTurnDistance is LONG and heroRole is SCOUT then Value is BITLOW - rule: if turn is NEXT and mainTurnDistance is LONG and heroRole is MAIN then Value is LOW with 0.3 - rule: if turn is NEXT and mainTurnDistance is LONG and heroRole is SCOUT then Value is BITLOW with 0.3 - rule: if turn is FUTURE and scoutTurnDistance is very LONG and heroRole is SCOUT then Value is LOWEST with 0.3 - rule: if turn is FUTURE and mainTurnDistance is very LONG and heroRole is SCOUT then Value is LOWEST with 0.5 - rule: if turn is FUTURE and mainTurnDistance is very LONG and heroRole is MAIN and strategicalValue is NONE then Value is LOWEST with 0.5 - rule: if turn is FUTURE and mainTurnDistance is very LONG and heroRole is MAIN and strategicalValue is LOW then Value is LOWEST with 0.3 - rule: if turn is FUTURE and mainTurnDistance is very LONG and heroRole is MAIN and strategicalValue is MEDIUM then Value is LOW with 0.5 - rule: if turn is FUTURE and mainTurnDistance is very LONG and heroRole is MAIN and strategicalValue is HIGH then Value is BITLOW - rule: if turn is FUTURE and scoutTurnDistance is LONG and heroRole is SCOUT then Value is LOW - rule: if turn is FUTURE and mainTurnDistance is LONG and heroRole is MAIN then Value is LOW - rule: if turn is FUTURE and mainTurnDistance is LONG and heroRole is SCOUT then Value is LOW - rule: if scoutTurnDistance is MEDIUM and heroRole is SCOUT then Value is BITLOW - rule: if mainTurnDistance is MEDIUM then Value is BITLOW - rule: if scoutTurnDistance is LOW and heroRole is SCOUT then Value is MEDIUM - rule: if mainTurnDistance is LOW then Value is MEDIUM - rule: if goldReward is HIGH and goldPreasure is HIGH and heroRole is SCOUT and danger is not NONE and armyLoss is LOW then Value is BITHIGH - rule: if goldReward is HIGH and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is HIGH with 0.7 - rule: if goldReward is HIGH and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW and fear is not HIGH then Value is HIGHEST - rule: if goldReward is HIGH and goldPreasure is HIGH and heroRole is MAIN and danger is NONE then Value is BITHIGH - rule: if goldReward is MEDIUM and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is HIGH - rule: if goldReward is MEDIUM and goldPreasure is HIGH and armyLoss is LOW and heroRole is SCOUT and danger is not NONE then Value is MEDIUM - rule: if goldReward is MEDIUM and heroRole is MAIN and danger is NONE and rewardType is SINGLE then Value is BITLOW - rule: if goldReward is MEDIUM and goldPreasure is HIGH and armyLoss is LOW and heroRole is MAIN and danger is not NONE then Value is BITHIGH - rule: if goldReward is LOW and goldPreasure is HIGH and heroRole is SCOUT and armyLoss is LOW then Value is BITHIGH - rule: if goldReward is LOW and heroRole is MAIN and danger is not NONE and rewardType is SINGLE and armyLoss is LOW then Value is BITLOW - rule: if goldReward is LOW and heroRole is MAIN and danger is NONE and rewardType is SINGLE then Value is LOW - rule: if goldReward is LOWEST and heroRole is MAIN and danger is NONE and rewardType is SINGLE then Value is LOWEST - rule: if armyReward is HIGH and heroRole is SCOUT and danger is not NONE and armyLoss is LOW then Value is HIGH with 0.5 - rule: if armyReward is HIGH and heroRole is SCOUT and danger is NONE then Value is HIGHEST - rule: if armyReward is HIGH and heroRole is MAIN and rewardType is MIXED and armyLoss is LOW and fear is not HIGH then Value is HIGHEST - rule: if armyReward is HIGH and heroRole is MAIN and rewardType is SINGLE and mainTurnDistance is LOWEST then Value is HIGHEST - rule: if armyReward is HIGH and heroRole is MAIN and rewardType is SINGLE and danger is NONE and fear is not HIGH then Value is HIGH - rule: if armyReward is HIGH and heroRole is MAIN and rewardType is SINGLE and danger is not NONE and armyLoss is LOW and fear is not HIGH then Value is HIGHEST - rule: if armyReward is MEDIUM and heroRole is MAIN and danger is not NONE and armyLoss is LOW and fear is not HIGH then Value is HIGHEST with 0.5 - rule: if armyReward is MEDIUM and heroRole is MAIN and danger is NONE then Value is BITHIGH - rule: if armyReward is MEDIUM and heroRole is MAIN and danger is NONE and mainTurnDistance is LOWEST then Value is HIGH with 0.2 - rule: if armyReward is MEDIUM and heroRole is SCOUT and danger is NONE then Value is HIGHEST with 0.5 - rule: if armyReward is LOW and heroRole is SCOUT and danger is NONE then Value is HIGH - rule: if armyReward is LOW and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is HIGH - rule: if armyReward is LOW and heroRole is MAIN and danger is NONE then Value is BITLOW with 0.5 - 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 - rule: if strategicalValue is MEDIUM and heroRole is SCOUT and danger is NONE and fear is not HIGH then Value is HIGH - rule: if strategicalValue is HIGH and heroRole is SCOUT and danger is NONE and fear is not HIGH then Value is HIGHEST with 0.5 - rule: if strategicalValue is HIGH and heroRole is MAIN and armyLoss is LOW and fear is not HIGH then Value is HIGHEST - rule: if strategicalValue is HIGH and heroRole is MAIN and armyLoss is MEDIUM and fear is not HIGH then Value is HIGH - rule: if strategicalValue is MEDIUM and heroRole is MAIN and armyLoss is LOW and fear is not HIGH then Value is HIGH - rule: if rewardType is NONE then Value is LOWEST - rule: if armyLoss is HIGH and strategicalValue is not HIGH and heroRole is MAIN then Value is LOWEST - rule: if armyLoss is HIGH and strategicalValue is HIGH and heroRole is MAIN then Value is LOW - rule: if armyLoss is HIGH and heroRole is SCOUT then Value is LOWEST - rule: if heroRole is SCOUT and closestHeroRatio is LOW then Value is LOW - rule: if heroRole is SCOUT and closestHeroRatio is LOWEST then Value is LOWEST - rule: if heroRole is MAIN and danger is NONE and skillReward is NONE and rewardType is SINGLE and closestHeroRatio is LOW then Value is LOW - rule: if heroRole is MAIN and danger is NONE and skillReward is NONE and rewardType is SINGLE and closestHeroRatio is LOWEST then Value is LOWEST - rule: if heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is BITHIGH with 0.2 - rule: if heroRole is SCOUT then Value is BITLOW - rule: if goldCost is not NONE and goldReward is NONE and goldPreasure is HIGH then Value is LOWEST - rule: if turn is NOW then Value is LOW with 0.3 - rule: if turn is not NOW then Value is LOW with 0.4 - rule: if goldPreasure is HIGH and goldReward is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW and fear is not HIGH then Value is HIGHEST - rule: if goldPreasure is HIGH and goldReward is MEDIUM and heroRole is MAIN and danger is not NONE and armyLoss is LOW and fear is not HIGH then Value is HIGH - rule: if goldPreasure is HIGH and goldReward is HIGH and heroRole is SCOUT and danger is NONE and armyLoss is LOW and fear is not HIGH then Value is HIGHEST - rule: if goldPreasure is HIGH and goldReward is MEDIUM and heroRole is SCOUT and danger is NONE and armyLoss is LOW and fear is not HIGH then Value is HIGH - rule: if goldPreasure is HIGH and goldReward is LOW and heroRole is SCOUT and armyLoss is LOW then Value is BITHIGH - rule: if goldPreasure is HIGH and goldReward is LOW and heroRole is SCOUT and scoutTurnDistance is LOW and armyLoss is LOW then Value is HIGH with 0.5 - rule: if fear is MEDIUM then Value is LOW - rule: if fear is HIGH then Value is LOWEST \ No newline at end of file + rule: if heroRole is MAIN then Value is BASE + rule: if heroRole is SCOUT then Value is BASE + rule: if heroRole is MAIN and armyGrowth is HUGE then Value is HIGH + rule: if heroRole is MAIN and armyGrowth is BIG then Value is BITHIGH + rule: if heroRole is MAIN and strategicalValue is HIGH then Value is HIGHEST \ No newline at end of file From b19ac01bf9c734a83e3fba92980723a1d1ac247c Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 4 Jun 2023 16:02:02 +0300 Subject: [PATCH 03/21] Fuzzy rework, added more defence and gather army routines --- AI/Nullkiller/AIGateway.cpp | 11 +- AI/Nullkiller/Analyzers/ArmyManager.cpp | 24 +- AI/Nullkiller/Analyzers/ArmyManager.h | 33 ++- .../Analyzers/DangerHitMapAnalyzer.cpp | 100 +++++++- .../Analyzers/DangerHitMapAnalyzer.h | 6 +- AI/Nullkiller/Analyzers/HeroManager.cpp | 17 +- AI/Nullkiller/Analyzers/HeroManager.h | 2 + .../Behaviors/CaptureObjectsBehavior.cpp | 14 +- AI/Nullkiller/Behaviors/DefenceBehavior.cpp | 235 ++++++++++++------ AI/Nullkiller/Behaviors/DefenceBehavior.h | 5 + .../Behaviors/GatherArmyBehavior.cpp | 49 +++- AI/Nullkiller/CMakeLists.txt | 2 + AI/Nullkiller/Engine/Nullkiller.cpp | 4 +- AI/Nullkiller/Engine/Nullkiller.h | 2 + AI/Nullkiller/Engine/PriorityEvaluator.cpp | 71 ++++-- AI/Nullkiller/Engine/PriorityEvaluator.h | 2 + AI/Nullkiller/Goals/BuyArmy.cpp | 2 +- AI/Nullkiller/Goals/Composition.cpp | 58 ++++- AI/Nullkiller/Goals/Composition.h | 8 +- AI/Nullkiller/Goals/ExecuteHeroChain.cpp | 14 ++ AI/Nullkiller/Goals/RecruitHero.cpp | 21 +- AI/Nullkiller/Goals/RecruitHero.h | 12 +- AI/Nullkiller/Helpers/ArmyFormation.cpp | 68 +++++ AI/Nullkiller/Helpers/ArmyFormation.h | 39 +++ AI/Nullkiller/Markers/HeroExchange.cpp | 2 +- AI/Nullkiller/Pathfinding/AINodeStorage.cpp | 2 +- AI/Nullkiller/Pathfinding/AINodeStorage.h | 6 +- config/ai/object-priorities.txt | 96 +++++-- 28 files changed, 710 insertions(+), 195 deletions(-) create mode 100644 AI/Nullkiller/Helpers/ArmyFormation.cpp create mode 100644 AI/Nullkiller/Helpers/ArmyFormation.h diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 16cd34849..c3bebecbb 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -29,7 +29,7 @@ namespace NKAI { // our to enemy strength ratio constants -const float SAFE_ATTACK_CONSTANT = 1.2f; +const float SAFE_ATTACK_CONSTANT = 1.1f; const float RETREAT_THRESHOLD = 0.3f; const double RETREAT_ABSOLUTE_THRESHOLD = 10000.; @@ -90,9 +90,11 @@ void AIGateway::heroMoved(const TryMoveHero & details, bool verbose) LOG_TRACE(logAi); NET_EVENT_HANDLER; - validateObject(details.id); //enemy hero may have left visible area auto hero = cb->getHero(details.id); + if(!hero) + validateObject(details.id); //enemy hero may have left visible area + const int3 from = hero ? hero->convertToVisitablePos(details.start) : (details.start - int3(0,1,0));; const int3 to = hero ? hero->convertToVisitablePos(details.end) : (details.end - int3(0,1,0)); @@ -794,10 +796,7 @@ void AIGateway::makeTurn() cb->sendMessage("vcmieagles"); - if(cb->getDate(Date::DAY) == 1) - { - retrieveVisitableObjs(); - } + retrieveVisitableObjs(); #if NKAI_TRACE_LEVEL == 0 try diff --git a/AI/Nullkiller/Analyzers/ArmyManager.cpp b/AI/Nullkiller/Analyzers/ArmyManager.cpp index 71ce9630f..b8ce631b6 100644 --- a/AI/Nullkiller/Analyzers/ArmyManager.cpp +++ b/AI/Nullkiller/Analyzers/ArmyManager.cpp @@ -238,7 +238,8 @@ std::shared_ptr ArmyManager::getArmyAvailableToBuyAsCCreatureSet( ui64 ArmyManager::howManyReinforcementsCanBuy( const CCreatureSet * targetArmy, const CGDwelling * dwelling, - const TResources & availableResources) const + const TResources & availableResources, + uint8_t turn) const { ui64 aivalue = 0; auto army = getArmyAvailableToBuy(targetArmy, dwelling, availableResources); @@ -259,17 +260,29 @@ std::vector ArmyManager::getArmyAvailableToBuy(const CCreatureSet * her std::vector ArmyManager::getArmyAvailableToBuy( const CCreatureSet * hero, const CGDwelling * dwelling, - TResources availableRes) const + TResources availableRes, + uint8_t turn) const { std::vector creaturesInDwellings; int freeHeroSlots = GameConstants::ARMY_SIZE - hero->stacksCount(); + bool countGrowth = (cb->getDate(Date::DAY_OF_WEEK) + turn) > 7; + + const CGTownInstance * town = dwelling->ID == CGTownInstance::TOWN + ? dynamic_cast(dwelling) + : nullptr; for(int i = dwelling->creatures.size() - 1; i >= 0; i--) { auto ci = infoFromDC(dwelling->creatures[i]); - if(!ci.count || ci.creID == -1) - continue; + if(ci.creID == -1) continue; + + if(i < GameConstants::CREATURES_PER_TOWN && countGrowth) + { + ci.count += town ? town->creatureGrowth(i) : ci.cre->getGrowth(); + } + + if(!ci.count) continue; SlotID dst = hero->getSlotFor(ci.creID); if(!hero->hasStackAtSlot(dst)) //need another new slot for this stack @@ -282,8 +295,7 @@ std::vector ArmyManager::getArmyAvailableToBuy( vstd::amin(ci.count, availableRes / ci.cre->getFullRecruitCost()); //max count we can afford - if(!ci.count) - continue; + if(!ci.count) continue; ci.level = i; //this is important for Dungeon Summoning Portal creaturesInDwellings.push_back(ci); diff --git a/AI/Nullkiller/Analyzers/ArmyManager.h b/AI/Nullkiller/Analyzers/ArmyManager.h index b6f27adf9..10848e0d6 100644 --- a/AI/Nullkiller/Analyzers/ArmyManager.h +++ b/AI/Nullkiller/Analyzers/ArmyManager.h @@ -45,20 +45,32 @@ public: virtual ui64 howManyReinforcementsCanBuy( const CCreatureSet * targetArmy, const CGDwelling * dwelling, - const TResources & availableResources) const = 0; + const TResources & availableResources, + uint8_t turn = 0) 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 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, TResources availableRes) const = 0; + + virtual std::vector getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const = 0; + virtual std::vector getArmyAvailableToBuy( + const CCreatureSet * hero, + const CGDwelling * dwelling, + TResources availableRes, + uint8_t turn = 0) const = 0; + virtual uint64_t evaluateStackPower(const CCreature * creature, int count) const = 0; virtual SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const = 0; virtual ArmyUpgradeInfo calculateCreaturesUpgrade( const CCreatureSet * army, const CGObjectInstance * upgrader, const TResources & availableResources) const = 0; - virtual std::vector getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const = 0; virtual std::shared_ptr getArmyAvailableToBuyAsCCreatureSet(const CGDwelling * dwelling, TResources availableRes) const = 0; }; @@ -74,18 +86,27 @@ private: public: ArmyManager(CPlayerSpecificInfoCallback * CB, const Nullkiller * ai): cb(CB), ai(ai) {} void update() override; + ui64 howManyReinforcementsCanBuy(const CCreatureSet * target, const CGDwelling * source) const override; ui64 howManyReinforcementsCanBuy( const CCreatureSet * targetArmy, const CGDwelling * dwelling, - const TResources & availableResources) const override; + const TResources & availableResources, + uint8_t turn = 0) 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, TResources availableRes) const override; + std::vector getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const override; + std::vector getArmyAvailableToBuy( + const CCreatureSet * hero, + const CGDwelling * dwelling, + TResources availableRes, + uint8_t turn = 0) const override; + std::shared_ptr getArmyAvailableToBuyAsCCreatureSet(const CGDwelling * dwelling, TResources availableRes) const override; uint64_t evaluateStackPower(const CCreature * creature, int count) const override; SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const override; diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp index 83bb0a02b..0124b0c27 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp @@ -19,12 +19,12 @@ HitMapInfo HitMapInfo::NoTreat; void DangerHitMapAnalyzer::updateHitMap() { - if(upToDate) + if(hitMapUpToDate) return; logAi->trace("Update danger hitmap"); - upToDate = true; + hitMapUpToDate = true; auto start = std::chrono::high_resolution_clock::now(); auto cb = ai->cb.get(); @@ -71,8 +71,10 @@ void DangerHitMapAnalyzer::updateHitMap() auto turn = path.turn(); auto & node = hitMap[pos.x][pos.y][pos.z]; - if(tileDanger / (turn / 3 + 1) > node.maximumDanger.danger / (node.maximumDanger.turn / 3 + 1) - || (tileDanger == node.maximumDanger.danger && node.maximumDanger.turn > turn)) + auto newMaxDanger = tileDanger / std::sqrt(turn / 3.0f + 1); + auto currentMaxDanger = node.maximumDanger.danger / std::sqrt(node.maximumDanger.turn / 3.0f + 1); + + if(newMaxDanger > currentMaxDanger) { node.maximumDanger.danger = tileDanger; node.maximumDanger.turn = turn; @@ -104,6 +106,94 @@ void DangerHitMapAnalyzer::updateHitMap() logAi->trace("Danger hit map updated in %ld", timeElapsed(start)); } +void DangerHitMapAnalyzer::calculateTileOwners() +{ + if(tileOwnersUpToDate) return; + + tileOwnersUpToDate = true; + + auto cb = ai->cb.get(); + auto mapSize = ai->cb->getMapSize(); + + tileOwners.resize(boost::extents[mapSize.x][mapSize.y][mapSize.z]); + + std::map townHeroes; + std::map heroTownMap; + PathfinderSettings pathfinderSettings; + + pathfinderSettings.mainTurnDistanceLimit = 3; + + auto addTownHero = [&](const CGTownInstance * town) + { + auto townHero = new CGHeroInstance(); + CRandomGenerator rng; + + townHero->pos = town->pos; + townHero->setOwner(ai->playerID); // lets avoid having multiple colors + townHero->initHero(rng, static_cast(0)); + townHero->initObj(rng); + + heroTownMap[townHero] = town; + townHeroes[townHero] = HeroRole::MAIN; + }; + + for(auto obj : ai->memory->visitableObjs) + { + if(obj && obj->ID == Obj::TOWN) + { + addTownHero(dynamic_cast(obj)); + } + } + + for(auto town : cb->getTownsInfo()) + { + addTownHero(town); + } + + ai->pathfinder->updatePaths(townHeroes, PathfinderSettings()); + + pforeachTilePos(mapSize, [&](const int3 & pos) + { + float ourDistance = std::numeric_limits::max(); + float enemyDistance = std::numeric_limits::max(); + const CGTownInstance * enemyTown = nullptr; + + for(AIPath & path : ai->pathfinder->getPathInfo(pos)) + { + if(!path.targetHero || path.getFirstBlockedAction()) + continue; + + auto town = heroTownMap[path.targetHero]; + + if(town->getOwner() == ai->playerID) + { + vstd::amin(ourDistance, path.movementCost()); + } + else + { + if(enemyDistance > path.movementCost()) + { + enemyDistance = path.movementCost(); + enemyTown = town; + } + } + } + + if(ourDistance == enemyDistance) + { + tileOwners[pos.x][pos.y][pos.z] = PlayerColor::NEUTRAL; + } + else if(!enemyTown || ourDistance < enemyDistance) + { + tileOwners[pos.x][pos.y][pos.z] = ai->playerID; + } + else + { + tileOwners[pos.x][pos.y][pos.z] = enemyTown->getOwner(); + } + }); +} + uint64_t DangerHitMapAnalyzer::enemyCanKillOurHeroesAlongThePath(const AIPath & path) const { int3 tile = path.targetTile(); @@ -144,7 +234,7 @@ const std::set & DangerHitMapAnalyzer::getOneTurnAcces void DangerHitMapAnalyzer::reset() { - upToDate = false; + hitMapUpToDate = false; } } diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h index 660dfd593..e96987b33 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h @@ -55,19 +55,23 @@ class DangerHitMapAnalyzer { private: boost::multi_array hitMap; + boost::multi_array tileOwners; std::map> enemyHeroAccessibleObjects; - bool upToDate; + bool hitMapUpToDate = false; + bool tileOwnersUpToDate = false; const Nullkiller * ai; public: DangerHitMapAnalyzer(const Nullkiller * ai) :ai(ai) {} void updateHitMap(); + void calculateTileOwners(); uint64_t enemyCanKillOurHeroesAlongThePath(const AIPath & path) const; const HitMapNode & getObjectTreat(const CGObjectInstance * obj) const; const HitMapNode & getTileTreat(const int3 & tile) const; const std::set & getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const; void reset(); + void resetTileOwners() { tileOwnersUpToDate = false; } }; } diff --git a/AI/Nullkiller/Analyzers/HeroManager.cpp b/AI/Nullkiller/Analyzers/HeroManager.cpp index 089c4436a..6afa78ec7 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -180,6 +180,15 @@ float HeroManager::evaluateHero(const CGHeroInstance * hero) const return evaluateFightingStrength(hero); } +bool HeroManager::heroCapReached() const +{ + const bool includeGarnisoned = true; + int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned); + + return heroCount >= ALLOWED_ROAMING_HEROES + || heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP); +} + bool HeroManager::canRecruitHero(const CGTownInstance * town) const { if(!town) @@ -191,13 +200,7 @@ bool HeroManager::canRecruitHero(const CGTownInstance * town) const if(cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST) return false; - const bool includeGarnisoned = true; - int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned); - - if(heroCount >= ALLOWED_ROAMING_HEROES) - return false; - - if(heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)) + if(heroCapReached()) return false; if(!cb->getAvailableHeroes(town).size()) diff --git a/AI/Nullkiller/Analyzers/HeroManager.h b/AI/Nullkiller/Analyzers/HeroManager.h index 9c98443f3..84da85b98 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.h +++ b/AI/Nullkiller/Analyzers/HeroManager.h @@ -31,6 +31,7 @@ public: virtual float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const = 0; virtual float evaluateHero(const CGHeroInstance * hero) const = 0; virtual bool canRecruitHero(const CGTownInstance * t = nullptr) const = 0; + virtual bool heroCapReached() const = 0; virtual const CGHeroInstance * findHeroWithGrail() const = 0; }; @@ -71,6 +72,7 @@ public: float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const override; float evaluateHero(const CGHeroInstance * hero) const override; bool canRecruitHero(const CGTownInstance * t = nullptr) const override; + bool heroCapReached() const override; const CGHeroInstance * findHeroWithGrail() const override; private: diff --git a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp index e63e26c3a..132861467 100644 --- a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp +++ b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp @@ -56,7 +56,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector tasks.reserve(paths.size()); - const AIPath * closestWay = nullptr; + std::unordered_map closestWaysByRole; std::vector waysToVisitObj; for(auto & path : paths) @@ -128,8 +128,9 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector auto heroRole = ai->nullkiller->heroManager->getHeroRole(path.targetHero); - if(heroRole == HeroRole::SCOUT - && (!closestWay || closestWay->movementCost() > path.movementCost())) + auto & closestWay = closestWaysByRole[heroRole]; + + if(!closestWay || closestWay->movementCost() > path.movementCost()) { closestWay = &path; } @@ -142,9 +143,12 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector } } - if(closestWay) + for(auto way : waysToVisitObj) { - for(auto way : waysToVisitObj) + auto heroRole = ai->nullkiller->heroManager->getHeroRole(way->getPath().targetHero); + auto closestWay = closestWaysByRole[heroRole]; + + if(closestWay) { way->closestWayRatio = closestWay->movementCost() / way->getPath().movementCost(); diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp index 2a1bbf4c5..0f971d290 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp @@ -60,27 +60,27 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta if(town->garrisonHero) { - if(!ai->nullkiller->isHeroLocked(town->garrisonHero.get())) + if(ai->nullkiller->isHeroLocked(town->garrisonHero.get())) { - if(!town->visitingHero && cb->getHeroCount(ai->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER) - { - logAi->trace( - "Extracting hero %s from garrison of town %s", - town->garrisonHero->getNameTranslated(), - town->getNameTranslated()); + logAi->trace( + "Hero %s in garrison of town %s is suposed to defend the town", + town->garrisonHero->getNameTranslated(), + town->getNameTranslated()); - tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5))); - - return; - } + return; } - logAi->trace( - "Hero %s in garrison of town %s is suposed to defend the town", - town->garrisonHero->getNameTranslated(), - town->getNameTranslated()); + if(!town->visitingHero && cb->getHeroCount(ai->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER) + { + logAi->trace( + "Extracting hero %s from garrison of town %s", + town->garrisonHero->getNameTranslated(), + town->getNameTranslated()); - return; + tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5))); + + return; + } } if(!treatNode.fastestDanger.hero) @@ -113,11 +113,21 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta for(AIPath & path : paths) { - if(town->visitingHero && path.targetHero != town->visitingHero.get()) - continue; - - if(town->visitingHero && path.getHeroStrength() < town->visitingHero->getHeroStrength()) - continue; + if(town->visitingHero && path.targetHero == town->visitingHero.get()) + { + if(path.getHeroStrength() < town->visitingHero->getHeroStrength()) + continue; + } + else if(town->garrisonHero && path.targetHero == town->garrisonHero.get()) + { + if(path.getHeroStrength() < town->visitingHero->getHeroStrength()) + continue; + } + else + { + if(town->visitingHero) + continue; + } if(treat.hero.validAndSet() && treat.turn <= 1 @@ -158,53 +168,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta if(treatIsUnderControl) continue; - if(!town->visitingHero - && town->hasBuilt(BuildingID::TAVERN) - && cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST) - { - auto heroesInTavern = cb->getAvailableHeroes(town); - - for(auto hero : heroesInTavern) - { - if(hero->getTotalStrength() > treat.danger) - { - auto myHeroes = cb->getHeroesInfo(); - - if(cb->getHeroesInfo().size() < ALLOWED_ROAMING_HEROES) - { -#if NKAI_TRACE_LEVEL >= 1 - logAi->trace("Hero %s can be recruited to defend %s", hero->getObjectName(), town->getObjectName()); -#endif - tasks.push_back(Goals::sptr(Goals::RecruitHero(town, hero).setpriority(1))); - continue; - } - else - { - const CGHeroInstance * weakestHero = nullptr; - - for(auto existingHero : myHeroes) - { - if(ai->nullkiller->isHeroLocked(existingHero) - || existingHero->getArmyStrength() > hero->getArmyStrength() - || ai->nullkiller->heroManager->getHeroRole(existingHero) == HeroRole::MAIN - || existingHero->movementPointsRemaining() - || existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1)) - continue; - - if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength()) - { - weakestHero = existingHero; - } - - if(weakestHero) - { - tasks.push_back(Goals::sptr(Goals::DismissHero(weakestHero))); - } - } - } - } - } - } + evaluateRecruitingHero(tasks, treat, town); if(paths.empty()) { @@ -275,9 +239,11 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta 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)))); + .addNextSequence({ + sptr(ExchangeSwapTownHeroes(town, town->visitingHero.get())), + sptr(ExecuteHeroChain(path, town)), + sptr(ExchangeSwapTownHeroes(town, path.targetHero, HeroLockedReason::DEFENCE)) + }))); continue; } @@ -313,15 +279,45 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta continue; } } - -#if NKAI_TRACE_LEVEL >= 1 - logAi->trace("Move %s to defend town %s", - path.targetHero->getObjectName(), - town->getObjectName()); -#endif Composition composition; - composition.addNext(DefendTown(town, treat, path)).addNext(ExecuteHeroChain(path, town)); + composition.addNext(DefendTown(town, treat, path)); + TGoalVec sequence; + + if(town->visitingHero && path.targetHero != town->visitingHero && !path.containsHero(town->visitingHero)) + { + if(town->garrisonHero) + { + if(ai->nullkiller->heroManager->getHeroRole(town->visitingHero.get()) == HeroRole::SCOUT + && town->visitingHero->getArmyStrength() < path.heroArmy->getArmyStrength() / 20) + { + if(path.turn() == 0) + sequence.push_back(sptr(DismissHero(town->visitingHero.get()))); + } + else + { +#if NKAI_TRACE_LEVEL >= 1 + logAi->trace("Cancel moving %s to defend town %s as the town has garrison hero", + path.targetHero->getObjectName(), + town->getObjectName()); +#endif + continue; + } + } + else if(path.turn() == 0) + { + sequence.push_back(sptr(ExchangeSwapTownHeroes(town, town->visitingHero.get()))); + } + } + +#if NKAI_TRACE_LEVEL >= 1 + logAi->trace("Move %s to defend town %s", + path.targetHero->getObjectName(), + town->getObjectName()); +#endif + + sequence.push_back(sptr(ExecuteHeroChain(path, town))); + composition.addNextSequence(sequence); auto firstBlockedAction = path.getFirstBlockedAction(); if(firstBlockedAction) @@ -350,4 +346,87 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta logAi->debug("Found %d tasks", tasks.size()); } +void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & treat, const CGTownInstance * town) const +{ + if(town->hasBuilt(BuildingID::TAVERN) + && cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST) + { + auto heroesInTavern = cb->getAvailableHeroes(town); + + for(auto hero : heroesInTavern) + { + if(hero->getTotalStrength() < treat.danger) + continue; + + auto myHeroes = cb->getHeroesInfo(); + +#if NKAI_TRACE_LEVEL >= 1 + logAi->trace("Hero %s can be recruited to defend %s", hero->getObjectName(), town->getObjectName()); +#endif + bool needSwap = false; + const CGHeroInstance * heroToDismiss = nullptr; + + if(town->visitingHero) + { + if(!town->garrisonHero) + needSwap = true; + else + { + if(town->visitingHero->getArmyStrength() < town->garrisonHero->getArmyStrength()) + { + if(town->visitingHero->getArmyStrength() >= hero->getArmyStrength()) + continue; + + heroToDismiss = town->visitingHero.get(); + } + else if(town->garrisonHero->getArmyStrength() >= hero->getArmyStrength()) + continue; + else + { + needSwap = true; + heroToDismiss = town->garrisonHero.get(); + } + } + } + else if(ai->nullkiller->heroManager->heroCapReached()) + { + const CGHeroInstance * weakestHero = nullptr; + + for(auto existingHero : myHeroes) + { + if(ai->nullkiller->isHeroLocked(existingHero) + || existingHero->getArmyStrength() > hero->getArmyStrength() + || ai->nullkiller->heroManager->getHeroRole(existingHero) == HeroRole::MAIN + || existingHero->movementPointsRemaining() + || existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1)) + continue; + + if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength()) + { + weakestHero = existingHero; + } + } + + if(!weakestHero) + continue; + + heroToDismiss = weakestHero; + } + + TGoalVec sequence; + Goals::Composition recruitHeroComposition; + + if(needSwap) + sequence.push_back(sptr(ExchangeSwapTownHeroes(town, town->visitingHero.get()))); + + if(heroToDismiss) + sequence.push_back(sptr(DismissHero(heroToDismiss))); + + sequence.push_back(sptr(Goals::RecruitHero(town, hero))); + + tasks.push_back(sptr(Goals::Composition().addNext(DefendTown(town, treat, hero)).addNextSequence(sequence))); + } + } +} + } diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.h b/AI/Nullkiller/Behaviors/DefenceBehavior.h index bd606cd36..b9b7c486e 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.h +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.h @@ -15,8 +15,12 @@ namespace NKAI { + +struct HitMapInfo; + namespace Goals { + class DefenceBehavior : public CGoal { public: @@ -35,6 +39,7 @@ namespace Goals private: void evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town) const; + void evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & treat, const CGTownInstance * town) const; }; } diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp index c2622cdc0..66416df54 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp @@ -16,6 +16,7 @@ #include "../Markers/ArmyUpgrade.h" #include "GatherArmyBehavior.h" #include "../AIUtility.h" +#include "../Goals/ExchangeSwapTownHeroes.h" namespace NKAI { @@ -78,20 +79,27 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her for(const AIPath & path : paths) { #if NKAI_TRACE_LEVEL >= 2 - logAi->trace("Path found %s", path.toString()); + logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength()); #endif - if(path.containsHero(hero)) continue; - - if(path.turn() == 0 && hero->inTownGarrison) + if(path.containsHero(hero)) { -#if NKAI_TRACE_LEVEL >= 1 - logAi->trace("Skipping garnisoned hero %s, %s", hero->getObjectName(), pos.toString()); +#if NKAI_TRACE_LEVEL >= 2 + logAi->trace("Selfcontaining path. Ignore"); #endif continue; } - if(ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path)) + bool garrisoned = false; + + if(path.turn() == 0 && hero->inTownGarrison) + { +#if NKAI_TRACE_LEVEL >= 1 + garrisoned = true; +#endif + } + + if(path.turn() > 0 && 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()); @@ -172,7 +180,21 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her exchangePath.closestWayRatio = 1; composition.addNext(heroExchange); - composition.addNext(exchangePath); + + if(garrisoned && path.turn() == 0) + { + auto lockReason = ai->nullkiller->getHeroLockedReason(hero); + + composition.addNextSequence({ + sptr(ExchangeSwapTownHeroes(hero->visitedTown)), + sptr(exchangePath), + sptr(ExchangeSwapTownHeroes(hero->visitedTown, hero, lockReason)) + }); + } + else + { + composition.addNext(exchangePath); + } auto blockedAction = path.getFirstBlockedAction(); @@ -221,7 +243,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) for(const AIPath & path : paths) { #if NKAI_TRACE_LEVEL >= 2 - logAi->trace("Path found %s", path.toString()); + logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength()); #endif if(upgrader->visitingHero && upgrader->visitingHero.get() != path.targetHero) { @@ -267,7 +289,14 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) ai->nullkiller->armyManager->howManyReinforcementsCanGet( path.targetHero, path.heroArmy, - upgrader->getUpperArmy()); + upgrader->getUpperArmy()); + + upgrade.upgradeValue += + ai->nullkiller->armyManager->howManyReinforcementsCanBuy( + path.heroArmy, + upgrader, + ai->nullkiller->getFreeResources(), + path.turn()); } auto armyValue = (float)upgrade.upgradeValue / path.getHeroStrength(); diff --git a/AI/Nullkiller/CMakeLists.txt b/AI/Nullkiller/CMakeLists.txt index b1abec00c..a6560989f 100644 --- a/AI/Nullkiller/CMakeLists.txt +++ b/AI/Nullkiller/CMakeLists.txt @@ -52,6 +52,7 @@ set(Nullkiller_SRCS Behaviors/BuildingBehavior.cpp Behaviors/GatherArmyBehavior.cpp Behaviors/ClusterBehavior.cpp + Helpers/ArmyFormation.cpp AIGateway.cpp ) @@ -114,6 +115,7 @@ set(Nullkiller_HEADERS Behaviors/BuildingBehavior.h Behaviors/GatherArmyBehavior.h Behaviors/ClusterBehavior.h + Helpers/ArmyFormation.h AIGateway.h ) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index c5aa3324f..55976d638 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -61,6 +61,7 @@ void Nullkiller::init(std::shared_ptr cb, PlayerColor playerID) armyManager.reset(new ArmyManager(cb.get(), this)); heroManager.reset(new HeroManager(cb.get(), this)); decomposer.reset(new DeepDecomposer()); + armyFormation.reset(new ArmyFormation(cb, this)); } Goals::TTask Nullkiller::choseBestTask(Goals::TTaskVec & tasks) const @@ -137,6 +138,7 @@ void Nullkiller::updateAiState(int pass, bool fast) { memory->removeInvisibleObjects(cb.get()); + dangerHitMap->calculateTileOwners(); dangerHitMap->updateHitMap(); boost::this_thread::interruption_point(); @@ -222,7 +224,7 @@ void Nullkiller::makeTurn() boost::lock_guard sharedStorageLock(AISharedStorage::locker); const int MAX_DEPTH = 10; - const float FAST_TASK_MINIMAL_PRIORITY = 0.7; + const float FAST_TASK_MINIMAL_PRIORITY = 0.7f; resetAiState(); diff --git a/AI/Nullkiller/Engine/Nullkiller.h b/AI/Nullkiller/Engine/Nullkiller.h index 1b5e513e6..d47e634ea 100644 --- a/AI/Nullkiller/Engine/Nullkiller.h +++ b/AI/Nullkiller/Engine/Nullkiller.h @@ -18,6 +18,7 @@ #include "../Analyzers/ArmyManager.h" #include "../Analyzers/HeroManager.h" #include "../Analyzers/ObjectClusterizer.h" +#include "../Helpers/ArmyFormation.h" namespace NKAI { @@ -67,6 +68,7 @@ public: std::unique_ptr memory; std::unique_ptr dangerEvaluator; std::unique_ptr decomposer; + std::unique_ptr armyFormation; PlayerColor playerID; std::shared_ptr cb; diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index eabd52403..66ef95c7c 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -23,6 +23,7 @@ #include "../Goals/ExecuteHeroChain.h" #include "../Goals/BuildThis.h" #include "../Goals/ExchangeSwapTownHeroes.h" +#include "../Goals/DismissHero.h" #include "../Markers/UnlockCluster.h" #include "../Markers/HeroExchange.h" #include "../Markers/ArmyUpgrade.h" @@ -33,6 +34,7 @@ namespace NKAI #define MIN_AI_STRENGHT (0.5f) //lower when combat AI gets smarter #define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us +const float MIN_CRITICAL_VALUE = 2.0f; EvaluationContext::EvaluationContext(const Nullkiller * ai) : movementCost(0.0), @@ -54,6 +56,11 @@ EvaluationContext::EvaluationContext(const Nullkiller * ai) { } +void EvaluationContext::addNonCriticalStrategicalValue(float value) +{ + vstd::amax(strategicalValue, std::min(value, MIN_CRITICAL_VALUE)); +} + PriorityEvaluator::~PriorityEvaluator() { delete engine; @@ -399,7 +406,7 @@ float RewardEvaluator::getEnemyHeroStrategicalValue(const CGHeroInstance * enemy 2. The formula quickly approaches 1.0 as hero level increases, but higher level always means higher value and the minimal value for level 1 hero is 0.5 */ - return std::min(1.0f, objectValue * 0.9f + (1.0f - (1.0f / (1 + enemy->level)))); + return std::min(1.5f, objectValue * 0.9f + (1.5f - (1.5f / (1 + enemy->level)))); } float RewardEvaluator::getResourceRequirementStrength(int resType) const @@ -640,7 +647,8 @@ public: uint64_t armyStrength = heroExchange.getReinforcementArmyStrength(); - evaluationContext.strategicalValue += 0.5f * armyStrength / heroExchange.hero.get()->getArmyStrength(); + evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero.get()->getArmyStrength()); + evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero.get()); } }; @@ -657,7 +665,7 @@ public: uint64_t upgradeValue = armyUpgrade.getUpgradeValue(); evaluationContext.armyReward += upgradeValue; - evaluationContext.strategicalValue += upgradeValue / (float)armyUpgrade.hero->getArmyStrength(); + evaluationContext.addNonCriticalStrategicalValue(upgradeValue / (float)armyUpgrade.hero->getArmyStrength()); } }; @@ -712,19 +720,21 @@ public: auto armyIncome = townArmyIncome(town); auto dailyIncome = town->dailyIncome()[EGameResID::GOLD]; - auto strategicalValue = std::sqrt(armyIncome / 20000.0f) + dailyIncome / 3000.0f; + auto strategicalValue = std::sqrt(armyIncome / 60000.0f) + dailyIncome / 10000.0f; if(evaluationContext.evaluator.ai->buildAnalyzer->getDevelopmentInfo().size() == 1) - strategicalValue = 1; + vstd::amax(evaluationContext.strategicalValue, 10.0); float multiplier = 1; if(treat.turn < defendTown.getTurn()) multiplier /= 1 + (defendTown.getTurn() - treat.turn); - evaluationContext.armyReward += armyIncome * multiplier; + multiplier /= 1.0f + treat.turn / 5.0f; + + evaluationContext.armyGrowth += armyIncome * multiplier; evaluationContext.goldReward += dailyIncome * 5 * multiplier; - evaluationContext.strategicalValue += strategicalValue * multiplier; + evaluationContext.addNonCriticalStrategicalValue(strategicalValue * multiplier); vstd::amax(evaluationContext.danger, defendTown.getTreat().danger); addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength()); } @@ -770,19 +780,22 @@ public: auto army = path.heroArmy; const CGObjectInstance * target = ai->cb->getObj((ObjectInstanceID)task->objid, false); + auto heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroPtr); + + if(heroRole == HeroRole::MAIN) + evaluationContext.heroRole = heroRole; if (target && ai->cb->getPlayerRelations(target->tempOwner, hero->tempOwner) == PlayerRelations::ENEMIES) { evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero); evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold); evaluationContext.armyGrowth += evaluationContext.evaluator.getArmyGrowth(target, hero, army); - evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, evaluationContext.heroRole); - evaluationContext.strategicalValue += evaluationContext.evaluator.getStrategicalValue(target); + evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, heroRole); + evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target)); evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army); } vstd::amax(evaluationContext.armyLossPersentage, path.getTotalArmyLoss() / (double)path.getHeroStrength()); - evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroPtr); addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength()); vstd::amax(evaluationContext.turn, path.turn()); } @@ -822,7 +835,7 @@ public: evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero) / boost; evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold) / boost; evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, role) / boost; - evaluationContext.strategicalValue += evaluationContext.evaluator.getStrategicalValue(target) / boost; + evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target) / boost); evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army) / boost; evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost; evaluationContext.movementCost += objInfo.second.movementCost / boost; @@ -860,6 +873,31 @@ public: } }; +class DismissHeroContextBuilder : public IEvaluationContextBuilder +{ +private: + const Nullkiller * ai; + +public: + DismissHeroContextBuilder(const Nullkiller * ai) : ai(ai) {} + + virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override + { + if(task->goalType != Goals::DISMISS_HERO) + return; + + Goals::DismissHero & dismissCommand = dynamic_cast(*task); + const CGHeroInstance * dismissedHero = dismissCommand.getHero().get(); + + auto role = ai->heroManager->getHeroRole(dismissedHero); + auto mpLeft = dismissedHero->movement; + + evaluationContext.movementCost += mpLeft; + evaluationContext.movementCostByRole[role] += mpLeft; + evaluationContext.goldCost += GameConstants::HERO_GOLD_COST + getArmyCost(dismissedHero); + } +}; + class BuildThisEvaluationContextBuilder : public IEvaluationContextBuilder { public: @@ -878,31 +916,31 @@ public: if(bi.creatureID != CreatureID::NONE) { - evaluationContext.strategicalValue += buildThis.townInfo.armyStrength / 50000.0; + evaluationContext.addNonCriticalStrategicalValue(buildThis.townInfo.armyStrength / 50000.0); if(bi.baseCreatureID == bi.creatureID) { - evaluationContext.strategicalValue += (0.5f + 0.1f * bi.creatureLevel) / (float)bi.prerequisitesCount; + evaluationContext.addNonCriticalStrategicalValue((0.5f + 0.1f * bi.creatureLevel) / (float)bi.prerequisitesCount); evaluationContext.armyReward += bi.armyStrength; } else { auto potentialUpgradeValue = evaluationContext.evaluator.getUpgradeArmyReward(buildThis.town, bi); - evaluationContext.strategicalValue += potentialUpgradeValue / 10000.0f / (float)bi.prerequisitesCount; + evaluationContext.addNonCriticalStrategicalValue(potentialUpgradeValue / 10000.0f / (float)bi.prerequisitesCount); evaluationContext.armyReward += potentialUpgradeValue / (float)bi.prerequisitesCount; } } else if(bi.id == BuildingID::CITADEL || bi.id == BuildingID::CASTLE) { - evaluationContext.strategicalValue += buildThis.town->creatures.size() * 0.2f; + evaluationContext.addNonCriticalStrategicalValue(buildThis.town->creatures.size() * 0.2f); evaluationContext.armyReward += buildThis.townInfo.armyStrength / 2; } else { auto goldPreasure = evaluationContext.evaluator.ai->buildAnalyzer->getGoldPreasure(); - evaluationContext.strategicalValue += evaluationContext.goldReward * goldPreasure / 3500.0f / bi.prerequisitesCount; + evaluationContext.addNonCriticalStrategicalValue(evaluationContext.goldReward * goldPreasure / 3500.0f / bi.prerequisitesCount); } if(bi.notEnoughRes && bi.prerequisitesCount == 1) @@ -934,6 +972,7 @@ PriorityEvaluator::PriorityEvaluator(const Nullkiller * ai) evaluationContextBuilders.push_back(std::make_shared()); evaluationContextBuilders.push_back(std::make_shared()); evaluationContextBuilders.push_back(std::make_shared()); + evaluationContextBuilders.push_back(std::make_shared(ai)); } EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal) const diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index a07ebf3f6..8f7f478f7 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -66,6 +66,8 @@ struct DLL_EXPORT EvaluationContext float enemyHeroDangerRatio; EvaluationContext(const Nullkiller * ai); + + void addNonCriticalStrategicalValue(float value); }; class IEvaluationContextBuilder diff --git a/AI/Nullkiller/Goals/BuyArmy.cpp b/AI/Nullkiller/Goals/BuyArmy.cpp index ddbabe936..f2e4aca05 100644 --- a/AI/Nullkiller/Goals/BuyArmy.cpp +++ b/AI/Nullkiller/Goals/BuyArmy.cpp @@ -71,7 +71,7 @@ void BuyArmy::accept(AIGateway * ai) throw cannotFulfillGoalException("No creatures to buy."); } - if(town->visitingHero) + if(town->visitingHero && !town->garrisonHero) { ai->moveHeroToTile(town->visitablePos(), town->visitingHero.get()); } diff --git a/AI/Nullkiller/Goals/Composition.cpp b/AI/Nullkiller/Goals/Composition.cpp index a0a487820..30d3791f9 100644 --- a/AI/Nullkiller/Goals/Composition.cpp +++ b/AI/Nullkiller/Goals/Composition.cpp @@ -31,9 +31,17 @@ std::string Composition::toString() const { std::string result = "Composition"; - for(auto goal : subtasks) + for(auto step : subtasks) { - result += " " + goal->toString(); + result += "["; + for(auto goal : step) + { + if(goal->isElementar()) + result += goal->toString() + " => "; + else + result += goal->toString() + ", "; + } + result += "] "; } return result; @@ -41,17 +49,34 @@ std::string Composition::toString() const void Composition::accept(AIGateway * ai) { - taskptr(*subtasks.back())->accept(ai); + for(auto task : subtasks.back()) + { + if(task->isElementar()) + { + taskptr(*task)->accept(ai); + } + else + { + break; + } + } } TGoalVec Composition::decompose() const { - return subtasks; + TGoalVec result; + + for(const TGoalVec & step : subtasks) + vstd::concatenate(result, step); + + return result; } -Composition & Composition::addNext(const AbstractGoal & goal) +Composition & Composition::addNextSequence(const TGoalVec & taskSequence) { - return addNext(sptr(goal)); + subtasks.push_back(taskSequence); + + return *this; } Composition & Composition::addNext(TSubgoal goal) @@ -64,20 +89,35 @@ Composition & Composition::addNext(TSubgoal goal) } else { - subtasks.push_back(goal); + subtasks.push_back({goal}); } return *this; } +Composition & Composition::addNext(const AbstractGoal & goal) +{ + return addNext(sptr(goal)); +} + bool Composition::isElementar() const { - return subtasks.back()->isElementar(); + return subtasks.back().front()->isElementar(); } int Composition::getHeroExchangeCount() const { - return isElementar() ? taskptr(*subtasks.back())->getHeroExchangeCount() : 0; + auto result = 0; + + for(auto task : subtasks.back()) + { + if(task->isElementar()) + { + result += taskptr(*task)->getHeroExchangeCount(); + } + } + + return result; } } diff --git a/AI/Nullkiller/Goals/Composition.h b/AI/Nullkiller/Goals/Composition.h index f7de02bb0..ecb4a1ab9 100644 --- a/AI/Nullkiller/Goals/Composition.h +++ b/AI/Nullkiller/Goals/Composition.h @@ -18,7 +18,7 @@ namespace Goals class DLL_EXPORT Composition : public ElementarGoal { private: - TGoalVec subtasks; + std::vector subtasks; // things we want to do now public: Composition() @@ -26,16 +26,12 @@ namespace Goals { } - Composition(TGoalVec subtasks) - : ElementarGoal(Goals::COMPOSITION), subtasks(subtasks) - { - } - virtual bool operator==(const Composition & other) const override; virtual std::string toString() const override; void accept(AIGateway * ai) override; Composition & addNext(const AbstractGoal & goal); Composition & addNext(TSubgoal goal); + Composition & addNextSequence(const TGoalVec & taskSequence); virtual TGoalVec decompose() const override; virtual bool isElementar() const override; virtual int getHeroExchangeCount() const override; diff --git a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp index 41b0d4e49..d367f96b4 100644 --- a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp +++ b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp @@ -52,6 +52,20 @@ void ExecuteHeroChain::accept(AIGateway * ai) ai->nullkiller->setActive(chainPath.targetHero, tile); ai->nullkiller->setTargetObject(objid); + auto targetObject = ai->myCb->getObj(static_cast(objid), false); + + if(chainPath.turn() == 0 && targetObject && targetObject->ID == Obj::TOWN) + { + auto relations = ai->myCb->getPlayerRelations(ai->playerID, targetObject->getOwner()); + + if(relations == PlayerRelations::ENEMIES) + { + ai->nullkiller->armyFormation->rearrangeArmyForSiege( + dynamic_cast(targetObject), + chainPath.targetHero); + } + } + std::set blockedIndexes; for(int i = chainPath.nodes.size() - 1; i >= 0; i--) diff --git a/AI/Nullkiller/Goals/RecruitHero.cpp b/AI/Nullkiller/Goals/RecruitHero.cpp index 9dc63020b..e787c7529 100644 --- a/AI/Nullkiller/Goals/RecruitHero.cpp +++ b/AI/Nullkiller/Goals/RecruitHero.cpp @@ -24,7 +24,10 @@ using namespace Goals; std::string RecruitHero::toString() const { - return "Recruit hero at " + town->getNameTranslated(); + if(heroToBuy) + return "Recruit " + heroToBuy->getNameTranslated() + " at " + town->getNameTranslated(); + else + return "Recruit hero at " + town->getNameTranslated(); } void RecruitHero::accept(AIGateway * ai) @@ -45,20 +48,20 @@ void RecruitHero::accept(AIGateway * ai) throw cannotFulfillGoalException("No available heroes in tavern in " + t->nodeName()); } - auto heroToHire = heroes[0]; + auto heroToHire = heroToBuy; - for(auto hero : heroes) + if(!heroToHire) { - if(objid == hero->id.getNum()) + for(auto hero : heroes) { - heroToHire = hero; - break; + if(!heroToHire || hero->getTotalStrength() > heroToHire->getTotalStrength()) + heroToHire = hero; } - - if(hero->getTotalStrength() > heroToHire->getTotalStrength()) - heroToHire = hero; } + if(!heroToHire) + throw cannotFulfillGoalException("No hero to hire!"); + if(t->visitingHero) { cb->swapGarrisonHero(t); diff --git a/AI/Nullkiller/Goals/RecruitHero.h b/AI/Nullkiller/Goals/RecruitHero.h index 34a968f33..78c6b0867 100644 --- a/AI/Nullkiller/Goals/RecruitHero.h +++ b/AI/Nullkiller/Goals/RecruitHero.h @@ -22,18 +22,20 @@ namespace Goals { class DLL_EXPORT RecruitHero : public ElementarGoal { + private: + const CGHeroInstance * heroToBuy; + public: RecruitHero(const CGTownInstance * townWithTavern, const CGHeroInstance * heroToBuy) - : RecruitHero(townWithTavern) + : ElementarGoal(Goals::RECRUIT_HERO), heroToBuy(heroToBuy) { - objid = heroToBuy->id.getNum(); + town = townWithTavern; + priority = 1; } RecruitHero(const CGTownInstance * townWithTavern) - : ElementarGoal(Goals::RECRUIT_HERO) + : RecruitHero(townWithTavern, nullptr) { - priority = 1; - town = townWithTavern; } virtual bool operator==(const RecruitHero & other) const override diff --git a/AI/Nullkiller/Helpers/ArmyFormation.cpp b/AI/Nullkiller/Helpers/ArmyFormation.cpp new file mode 100644 index 000000000..464cb10d0 --- /dev/null +++ b/AI/Nullkiller/Helpers/ArmyFormation.cpp @@ -0,0 +1,68 @@ +/* +* ArmyFormation.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 "ArmyFormation.h" +#include "../../../lib/mapObjects/CGTownInstance.h" + +namespace NKAI +{ + +void ArmyFormation::rearrangeArmyForSiege(const CGTownInstance * town, const CGHeroInstance * attacker) +{ + auto freeSlots = attacker->getFreeSlotsQueue(); + + while(!freeSlots.empty()) + { + auto weakestCreature = vstd::minElementByFun(attacker->Slots(), [](const std::pair & slot) -> int + { + return slot.second->getCount() == 1 + ? std::numeric_limits::max() + : slot.second->getCreatureID().toCreature()->getAIValue(); + }); + + if(weakestCreature == attacker->Slots().end() || weakestCreature->second->getCount() == 1) + { + break; + } + + cb->splitStack(attacker, attacker, weakestCreature->first, freeSlots.front(), 1); + freeSlots.pop(); + } + + if(town->fortLevel() > CGTownInstance::FORT) + { + std::vector stacks; + + for(auto slot : attacker->Slots()) + stacks.push_back(slot.second); + + boost::sort( + stacks, + [](CStackInstance * slot1, CStackInstance * slot2) -> bool + { + auto cre1 = slot1->getCreatureID().toCreature(); + auto cre2 = slot2->getCreatureID().toCreature(); + auto flying = cre1->hasBonusOfType(BonusType::FLYING) - cre2->hasBonusOfType(BonusType::FLYING); + + if(flying != 0) return flying < 0; + else return cre1->getAIValue() < cre2->getAIValue(); + }); + + for(int i = 0; i < stacks.size(); i++) + { + auto pos = vstd::findKey(attacker->Slots(), stacks[i]); + + if(pos.getNum() != i) + cb->swapCreatures(attacker, attacker, static_cast(i), pos); + } + } +} + +} diff --git a/AI/Nullkiller/Helpers/ArmyFormation.h b/AI/Nullkiller/Helpers/ArmyFormation.h new file mode 100644 index 000000000..e5a900c01 --- /dev/null +++ b/AI/Nullkiller/Helpers/ArmyFormation.h @@ -0,0 +1,39 @@ +/* +* ArmyFormation.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 "../AIUtility.h" + +#include "../../../lib/GameConstants.h" +#include "../../../lib/VCMI_Lib.h" +#include "../../../lib/CTownHandler.h" +#include "../../../lib/CBuildingHandler.h" + +namespace NKAI +{ + +struct HeroPtr; +class AIGateway; +class FuzzyHelper; +class Nullkiller; + +class DLL_EXPORT ArmyFormation +{ +private: + std::shared_ptr cb; //this is enough, but we downcast from CCallback + const Nullkiller * ai; + +public: + ArmyFormation(std::shared_ptr CB, const Nullkiller * ai): cb(CB), ai(ai) {} + + void rearrangeArmyForSiege(const CGTownInstance * town, const CGHeroInstance * attacker); +}; + +} diff --git a/AI/Nullkiller/Markers/HeroExchange.cpp b/AI/Nullkiller/Markers/HeroExchange.cpp index 562215bb3..499122327 100644 --- a/AI/Nullkiller/Markers/HeroExchange.cpp +++ b/AI/Nullkiller/Markers/HeroExchange.cpp @@ -29,7 +29,7 @@ bool HeroExchange::operator==(const HeroExchange & other) const std::string HeroExchange::toString() const { - return "Hero exchange " + exchangePath.toString(); + return "Hero exchange for " +hero.get()->getObjectName() + " by " + exchangePath.toString(); } uint64_t HeroExchange::getReinforcementArmyStrength() const diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index c0d650464..2fbd50a72 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -879,7 +879,7 @@ void AINodeStorage::setHeroes(std::map heroes) for(auto & hero : heroes) { // do not allow our own heroes in garrison to act on map - if(hero.first->getOwner() == ai->playerID && hero.first->inTownGarrison) + if(hero.first->getOwner() == ai->playerID && hero.first->inTownGarrison && ai->isHeroLocked(hero.first)) continue; uint64_t mask = FirstActorMask << actors.size(); diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.h b/AI/Nullkiller/Pathfinding/AINodeStorage.h index 22c2d6b21..02364ad11 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.h +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.h @@ -11,7 +11,7 @@ #pragma once #define NKAI_PATHFINDER_TRACE_LEVEL 0 -#define NKAI_TRACE_LEVEL 0 +#define NKAI_TRACE_LEVEL 2 #include "../../../lib/pathfinder/CGPathNode.h" #include "../../../lib/pathfinder/INodeStorage.h" @@ -24,8 +24,8 @@ namespace NKAI { - const int SCOUT_TURN_DISTANCE_LIMIT = 3; - const int MAIN_TURN_DISTANCE_LIMIT = 5; + const int SCOUT_TURN_DISTANCE_LIMIT = 5; + const int MAIN_TURN_DISTANCE_LIMIT = 10; namespace AIPathfinding { diff --git a/config/ai/object-priorities.txt b/config/ai/object-priorities.txt index 1714c8477..13ec86617 100644 --- a/config/ai/object-priorities.txt +++ b/config/ai/object-priorities.txt @@ -6,9 +6,9 @@ InputVariable: mainTurnDistance range: 0.000 10.000 lock-range: true term: LOWEST Ramp 0.250 0.000 - term: LOW Discrete 0.000 1.000 0.500 0.800 1.000 0.000 - term: MEDIUM Discrete 0.000 0.000 0.500 0.200 1.000 1.000 3.000 0.000 - term: LONG Discrete 1.000 0.000 1.500 0.200 3.000 0.800 10.000 1.000 + term: LOW Discrete 0.000 1.000 0.500 0.800 0.800 0.300 2.000 0.000 + term: MEDIUM Discrete 0.000 0.000 0.500 0.200 0.800 0.700 2.000 1.000 6.000 0.000 + term: LONG Discrete 2.000 0.000 6.000 1.000 10.000 0.800 InputVariable: scoutTurnDistance description: distance to tile in turns enabled: true @@ -43,6 +43,7 @@ InputVariable: armyLoss term: LOW Ramp 0.200 0.000 term: MEDIUM Triangle 0.000 0.200 0.500 term: HIGH Ramp 0.200 0.500 + term: ALL Ramp 0.700 1.000 InputVariable: heroRole enabled: true range: -0.100 1.100 @@ -82,13 +83,14 @@ InputVariable: closestHeroRatio InputVariable: strategicalValue description: Some abstract long term benefit non gold or army or skill enabled: true - range: 0.000 1.000 + range: 0.000 3.000 lock-range: false term: NONE Ramp 0.200 0.000 term: LOWEST Triangle 0.000 0.010 0.250 - term: LOW Triangle 0.000 0.250 0.700 - term: MEDIUM Triangle 0.250 0.700 1.000 - term: HIGH Ramp 0.700 1.000 + term: LOW Triangle 0.000 0.250 1.000 + term: MEDIUM Triangle 0.250 1.000 2.000 + term: HIGH Triangle 1.000 2.000 3.000 + term: CRITICAL Ramp 2.000 3.000 InputVariable: goldPreasure description: Ratio between weekly army cost and gold income enabled: true @@ -132,21 +134,22 @@ InputVariable: armyGrowth term: HUGE Ramp 8000.000 20000.000 OutputVariable: Value enabled: true - range: -1.500 2.000 + range: -1.500 2.500 lock-range: false aggregation: AlgebraicSum defuzzifier: Centroid 100 default: 0.500 lock-previous: false - term: WORST Binary -1.000 -inf 0.500 + term: WORST Binary -1.000 -inf 0.700 term: BAD Rectangle -1.000 -0.700 0.500 - term: BASE Rectangle -0.200 0.200 0.400 - term: LOW Rectangle 1.110 1.190 0.320 - term: HIGHEST Discrete 0.300 0.000 0.300 1.000 0.600 1.000 0.600 0.000 1.700 0.000 1.700 1.000 2.000 1.000 2.000 0.000 0.500 - term: HIGH Discrete 0.600 0.000 0.600 1.000 0.850 1.000 0.850 0.000 1.450 0.000 1.450 1.000 1.700 1.000 1.700 0.000 0.400 - term: BITHIGH Discrete 0.850 0.000 0.850 1.000 1.000 1.000 1.000 0.000 1.300 0.000 1.300 1.000 1.450 1.000 1.450 0.000 0.350 - term: MEDIUM Discrete 1.000 0.000 1.000 1.000 1.100 1.000 1.100 0.000 1.200 0.000 1.200 1.000 1.300 1.000 1.300 0.000 0.330 -RuleBlock: gold reward + term: BASE Rectangle -0.200 0.200 0.350 + term: MEDIUM Rectangle 0.910 1.090 0.500 + term: SMALL Rectangle 0.960 1.040 0.600 + term: BITHIGH Rectangle 0.850 1.150 0.400 + term: HIGH Rectangle 0.750 1.250 0.400 + term: HIGHEST Rectangle 0.500 1.500 0.350 + term: CRITICAL Ramp 0.500 2.000 0.500 +RuleBlock: basic enabled: true conjunction: AlgebraicProduct disjunction: AlgebraicSum @@ -154,6 +157,61 @@ RuleBlock: gold reward activation: General rule: if heroRole is MAIN then Value is BASE rule: if heroRole is SCOUT then Value is BASE - rule: if heroRole is MAIN and armyGrowth is HUGE then Value is HIGH - rule: if heroRole is MAIN and armyGrowth is BIG then Value is BITHIGH - rule: if heroRole is MAIN and strategicalValue is HIGH then Value is HIGHEST \ No newline at end of file + rule: if heroRole is MAIN and armyGrowth is HUGE and fear is not HIGH then Value is HIGH + rule: if heroRole is MAIN and armyGrowth is BIG and mainTurnDistance is LOW then Value is HIGH + rule: if heroRole is MAIN and armyGrowth is BIG and mainTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH + rule: if heroRole is MAIN and armyGrowth is BIG and mainTurnDistance is LONG and fear is not HIGH then Value is MEDIUM + rule: if armyLoss is ALL then Value is WORST + rule: if turn is not NOW then Value is BAD with 0.1 + rule: if closestHeroRatio is LOWEST and heroRole is SCOUT then Value is WORST + rule: if closestHeroRatio is LOW and heroRole is SCOUT then Value is BAD + rule: if closestHeroRatio is LOWEST and heroRole is MAIN then Value is BAD +RuleBlock: strategicalValue + enabled: true + conjunction: AlgebraicProduct + disjunction: NormalizedSum + implication: AlgebraicProduct + activation: General + rule: if heroRole is MAIN and strategicalValue is HIGH and turn is NOW then Value is HIGHEST + rule: if heroRole is MAIN and strategicalValue is HIGH and turn is not NOW and mainTurnDistance is LOW and fear is not HIGH then Value is HIGHEST + rule: if heroRole is MAIN and strategicalValue is HIGH and mainTurnDistance is MEDIUM and fear is not HIGH then Value is HIGHEST with 0.5 + rule: if heroRole is MAIN and strategicalValue is HIGH and mainTurnDistance is LONG and fear is not HIGH then Value is HIGH + rule: if heroRole is MAIN and strategicalValue is MEDIUM and mainTurnDistance is LOW then Value is HIGH + rule: if heroRole is MAIN and strategicalValue is MEDIUM and mainTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH + rule: if heroRole is MAIN and strategicalValue is MEDIUM and mainTurnDistance is LONG and fear is not HIGH then Value is MEDIUM + rule: if heroRole is MAIN and strategicalValue is LOW and mainTurnDistance is LOW and fear is not HIGH then Value is MEDIUM + rule: if heroRole is MAIN and strategicalValue is LOW and mainTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL + rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is not NONE and turn is NOW then Value is HIGH + rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is not NONE and turn is not NOW and scoutTurnDistance is LOW and fear is not HIGH then Value is HIGH + rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is not NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is HIGH with 0.5 + rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is not NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is BITHIGH + rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is not NONE and scoutTurnDistance is LOW then Value is BITHIGH + rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is not NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is MEDIUM + rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is not NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is SMALL + rule: if heroRole is SCOUT and strategicalValue is LOW and danger is not NONE and scoutTurnDistance is LOW and fear is not HIGH then Value is SMALL + rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is NONE and turn is NOW then Value is HIGHEST + rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is NONE and turn is not NOW and scoutTurnDistance is LOW and fear is not HIGH then Value is HIGHEST + rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is HIGHEST with 0.5 + rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is HIGH + rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is NONE and scoutTurnDistance is LOW then Value is HIGH + rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH + rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is MEDIUM + rule: if heroRole is SCOUT and strategicalValue is LOW and danger is NONE and scoutTurnDistance is LOW and fear is not HIGH then Value is MEDIUM + rule: if heroRole is SCOUT and strategicalValue is LOW and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL + rule: if armyLoss is HIGH and strategicalValue is LOW then Value is BAD + rule: if armyLoss is HIGH and strategicalValue is MEDIUM then Value is BAD with 0.7 + rule: if strategicalValue is CRITICAL then Value is CRITICAL +RuleBlock: armyReward + enabled: true + conjunction: AlgebraicProduct + disjunction: AlgebraicSum + implication: AlgebraicProduct + activation: General + rule: if heroRole is MAIN and armyReward is HIGH and mainTurnDistance is LOW and fear is not HIGH then Value is HIGHEST + rule: if heroRole is MAIN and armyReward is HIGH and mainTurnDistance is MEDIUM and fear is not HIGH then Value is HIGH + rule: if heroRole is MAIN and armyReward is HIGH and mainTurnDistance is LONG and fear is not HIGH then Value is BITHIGH + rule: if heroRole is MAIN and armyReward is MEDIUM and mainTurnDistance is LOW then Value is HIGH + rule: if heroRole is MAIN and armyReward is MEDIUM and mainTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH + rule: if heroRole is MAIN and armyReward is MEDIUM and mainTurnDistance is LONG and fear is not HIGH then Value is MEDIUM + rule: if heroRole is MAIN and armyReward is LOW and mainTurnDistance is LOW and fear is not HIGH then Value is MEDIUM + rule: if heroRole is MAIN and armyReward is LOW and mainTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL \ No newline at end of file From 6ba74f02bc0581af07c80ebb16cc5d89e3a4dc56 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 11 Jun 2023 19:21:50 +0300 Subject: [PATCH 04/21] NKAI: playing around with defence --- AI/Nullkiller/Analyzers/ArmyManager.cpp | 43 ++++++++- AI/Nullkiller/Analyzers/ArmyManager.h | 9 +- .../Analyzers/DangerHitMapAnalyzer.cpp | 93 +++++++++++++++---- .../Analyzers/DangerHitMapAnalyzer.h | 13 ++- AI/Nullkiller/Behaviors/DefenceBehavior.cpp | 24 +++-- .../Behaviors/GatherArmyBehavior.cpp | 68 +++++++++++--- .../Behaviors/RecruitHeroBehavior.cpp | 21 +++++ AI/Nullkiller/Engine/PriorityEvaluator.cpp | 82 ++++++++-------- AI/Nullkiller/Engine/PriorityEvaluator.h | 1 + AI/Nullkiller/Markers/ArmyUpgrade.cpp | 7 ++ AI/Nullkiller/Markers/ArmyUpgrade.h | 1 + AI/Nullkiller/Markers/DefendTown.cpp | 4 +- AI/Nullkiller/Markers/DefendTown.h | 5 +- 13 files changed, 286 insertions(+), 85 deletions(-) diff --git a/AI/Nullkiller/Analyzers/ArmyManager.cpp b/AI/Nullkiller/Analyzers/ArmyManager.cpp index b8ce631b6..359286a02 100644 --- a/AI/Nullkiller/Analyzers/ArmyManager.cpp +++ b/AI/Nullkiller/Analyzers/ArmyManager.cpp @@ -33,6 +33,45 @@ public: } }; +void ArmyUpgradeInfo::addArmyToBuy(std::vector army) +{ + for(auto slot : army) + { + resultingArmy.push_back(slot); + + upgradeValue += slot.power; + upgradeCost += slot.creature->getFullRecruitCost() * slot.count; + } +} + +void ArmyUpgradeInfo::addArmyToGet(std::vector army) +{ + for(auto slot : army) + { + resultingArmy.push_back(slot); + + upgradeValue += slot.power; + } +} + +std::vector ArmyManager::toSlotInfo(std::vector army) const +{ + std::vector result; + + for(auto i : army) + { + SlotInfo slot; + + slot.creature = VLC->creh->objects[i.cre->getId()]; + slot.count = i.count; + slot.power = evaluateStackPower(i.cre, i.count); + + result.push_back(slot); + } + + return result; +} + uint64_t ArmyManager::howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const { return howManyReinforcementsCanGet(hero, hero, source); @@ -136,7 +175,7 @@ std::vector ArmyManager::getBestArmy(const IBonusBearer * armyCarrier, { if(vstd::contains(allowedFactions, slot.creature->getFaction())) { - auto slotID = newArmyInstance.getSlotFor(slot.creature); + auto slotID = newArmyInstance.getSlotFor(slot.creature->getId()); if(slotID.validSlot()) { @@ -319,7 +358,7 @@ ui64 ArmyManager::howManyReinforcementsCanGet(const IBonusBearer * armyCarrier, return newArmy > oldArmy ? newArmy - oldArmy : 0; } -uint64_t ArmyManager::evaluateStackPower(const CCreature * creature, int count) const +uint64_t ArmyManager::evaluateStackPower(const Creature * creature, int count) const { return creature->getAIValue() * count; } diff --git a/AI/Nullkiller/Analyzers/ArmyManager.h b/AI/Nullkiller/Analyzers/ArmyManager.h index 10848e0d6..1617bd1bd 100644 --- a/AI/Nullkiller/Analyzers/ArmyManager.h +++ b/AI/Nullkiller/Analyzers/ArmyManager.h @@ -34,6 +34,9 @@ struct ArmyUpgradeInfo std::vector resultingArmy; uint64_t upgradeValue = 0; TResources upgradeCost; + + void addArmyToBuy(std::vector army); + void addArmyToGet(std::vector army); }; class DLL_EXPORT IArmyManager //: public: IAbstractManager @@ -57,6 +60,7 @@ public: 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 toSlotInfo(std::vector creatures) const = 0; virtual std::vector getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const = 0; virtual std::vector getArmyAvailableToBuy( @@ -65,7 +69,7 @@ public: TResources availableRes, uint8_t turn = 0) const = 0; - virtual uint64_t evaluateStackPower(const CCreature * creature, int count) const = 0; + virtual uint64_t evaluateStackPower(const Creature * creature, int count) const = 0; virtual SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const = 0; virtual ArmyUpgradeInfo calculateCreaturesUpgrade( const CCreatureSet * army, @@ -99,6 +103,7 @@ public: 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 toSlotInfo(std::vector creatures) const override; std::vector getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const override; std::vector getArmyAvailableToBuy( @@ -108,7 +113,7 @@ public: uint8_t turn = 0) const override; std::shared_ptr getArmyAvailableToBuyAsCCreatureSet(const CGDwelling * dwelling, TResources availableRes) const override; - uint64_t evaluateStackPower(const CCreature * creature, int count) const override; + uint64_t evaluateStackPower(const Creature * creature, int count) const override; SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const override; ArmyUpgradeInfo calculateCreaturesUpgrade( const CCreatureSet * army, diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp index 0124b0c27..7bd330137 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp @@ -17,6 +17,11 @@ namespace NKAI HitMapInfo HitMapInfo::NoTreat; +double HitMapInfo::value() const +{ + return danger / std::sqrt(turn / 3.0f + 1); +} + void DangerHitMapAnalyzer::updateHitMap() { if(hitMapUpToDate) @@ -29,8 +34,12 @@ void DangerHitMapAnalyzer::updateHitMap() auto cb = ai->cb.get(); auto mapSize = ai->cb->getMapSize(); - hitMap.resize(boost::extents[mapSize.x][mapSize.y][mapSize.z]); + + if(hitMap.shape()[0] != mapSize.x || hitMap.shape()[1] != mapSize.y || hitMap.shape()[2] != mapSize.z) + hitMap.resize(boost::extents[mapSize.x][mapSize.y][mapSize.z]); + enemyHeroAccessibleObjects.clear(); + townTreats.clear(); std::map> heroes; @@ -67,29 +76,26 @@ void DangerHitMapAnalyzer::updateHitMap() if(path.getFirstBlockedAction()) continue; - auto tileDanger = path.getHeroStrength(); - auto turn = path.turn(); auto & node = hitMap[pos.x][pos.y][pos.z]; - auto newMaxDanger = tileDanger / std::sqrt(turn / 3.0f + 1); - auto currentMaxDanger = node.maximumDanger.danger / std::sqrt(node.maximumDanger.turn / 3.0f + 1); + HitMapInfo newTreat; - if(newMaxDanger > currentMaxDanger) + newTreat.hero = path.targetHero; + newTreat.turn = path.turn(); + newTreat.danger = path.getHeroStrength(); + + if(newTreat.value() > node.maximumDanger.value()) { - node.maximumDanger.danger = tileDanger; - node.maximumDanger.turn = turn; - node.maximumDanger.hero = path.targetHero; + node.maximumDanger = newTreat; } - if(turn < node.fastestDanger.turn - || (turn == node.fastestDanger.turn && node.fastestDanger.danger < tileDanger)) + if(newTreat.turn < node.fastestDanger.turn + || (newTreat.turn == node.fastestDanger.turn && node.fastestDanger.danger < newTreat.danger)) { - node.fastestDanger.danger = tileDanger; - node.fastestDanger.turn = turn; - node.fastestDanger.hero = path.targetHero; + node.fastestDanger = newTreat; } - if(turn == 0) + if(newTreat.turn == 0) { auto objects = cb->getVisitableObjs(pos, false); @@ -97,6 +103,26 @@ void DangerHitMapAnalyzer::updateHitMap() { if(cb->getPlayerRelations(obj->tempOwner, ai->playerID) != PlayerRelations::ENEMIES) enemyHeroAccessibleObjects[path.targetHero].insert(obj); + + if(obj->ID == Obj::TOWN && obj->getOwner() == ai->playerID) + { + auto & treats = townTreats[obj->id]; + auto treat = std::find_if(treats.begin(), treats.end(), [&](const HitMapInfo & i) -> bool + { + return i.hero.hid == path.targetHero->id; + }); + + if(treat == treats.end()) + { + treats.emplace_back(); + treat = std::prev(treats.end(), 1); + } + + if(newTreat.value() > treat->value()) + { + *treat = newTreat; + } + } } } } @@ -115,7 +141,8 @@ void DangerHitMapAnalyzer::calculateTileOwners() auto cb = ai->cb.get(); auto mapSize = ai->cb->getMapSize(); - tileOwners.resize(boost::extents[mapSize.x][mapSize.y][mapSize.z]); + if(hitMap.shape()[0] != mapSize.x || hitMap.shape()[1] != mapSize.y || hitMap.shape()[2] != mapSize.z) + hitMap.resize(boost::extents[mapSize.x][mapSize.y][mapSize.z]); std::map townHeroes; std::map heroTownMap; @@ -157,6 +184,7 @@ void DangerHitMapAnalyzer::calculateTileOwners() float ourDistance = std::numeric_limits::max(); float enemyDistance = std::numeric_limits::max(); const CGTownInstance * enemyTown = nullptr; + const CGTownInstance * ourTown = nullptr; for(AIPath & path : ai->pathfinder->getPathInfo(pos)) { @@ -167,7 +195,11 @@ void DangerHitMapAnalyzer::calculateTileOwners() if(town->getOwner() == ai->playerID) { - vstd::amin(ourDistance, path.movementCost()); + if(ourDistance > path.movementCost()) + { + ourDistance = path.movementCost(); + ourTown = town; + } } else { @@ -181,19 +213,40 @@ void DangerHitMapAnalyzer::calculateTileOwners() if(ourDistance == enemyDistance) { - tileOwners[pos.x][pos.y][pos.z] = PlayerColor::NEUTRAL; + hitMap[pos.x][pos.y][pos.z].closestTown = nullptr; } else if(!enemyTown || ourDistance < enemyDistance) { - tileOwners[pos.x][pos.y][pos.z] = ai->playerID; + hitMap[pos.x][pos.y][pos.z].closestTown = ourTown; } else { - tileOwners[pos.x][pos.y][pos.z] = enemyTown->getOwner(); + hitMap[pos.x][pos.y][pos.z].closestTown = enemyTown; } }); } +const std::vector & DangerHitMapAnalyzer::getTownTreats(const CGTownInstance * town) const +{ + static const std::vector empty = {}; + + auto result = townTreats.find(town->id); + + return result == townTreats.end() ? empty : result->second; +} + +PlayerColor DangerHitMapAnalyzer::getTileOwner(const int3 & tile) const +{ + auto town = hitMap[tile.x][tile.y][tile.z].closestTown; + + return town ? town->getOwner() : PlayerColor::NEUTRAL; +} + +const CGTownInstance * DangerHitMapAnalyzer::getClosestTown(const int3 & tile) const +{ + return hitMap[tile.x][tile.y][tile.z].closestTown; +} + uint64_t DangerHitMapAnalyzer::enemyCanKillOurHeroesAlongThePath(const AIPath & path) const { int3 tile = path.targetTile(); diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h index e96987b33..4fed77412 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h @@ -35,6 +35,8 @@ struct HitMapInfo turn = 255; hero = HeroPtr(); } + + double value() const; }; struct HitMapNode @@ -42,6 +44,8 @@ struct HitMapNode HitMapInfo maximumDanger; HitMapInfo fastestDanger; + const CGTownInstance * closestTown = nullptr; + HitMapNode() = default; void reset() @@ -51,15 +55,19 @@ struct HitMapNode } }; +struct TileOwner +{ +}; + class DangerHitMapAnalyzer { private: boost::multi_array hitMap; - boost::multi_array tileOwners; std::map> enemyHeroAccessibleObjects; bool hitMapUpToDate = false; bool tileOwnersUpToDate = false; const Nullkiller * ai; + std::map> townTreats; public: DangerHitMapAnalyzer(const Nullkiller * ai) :ai(ai) {} @@ -72,6 +80,9 @@ public: const std::set & getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const; void reset(); void resetTileOwners() { tileOwnersUpToDate = false; } + PlayerColor getTileOwner(const int3 & tile) const; + const CGTownInstance * getClosestTown(const int3 & tile) const; + const std::vector & getTownTreats(const CGTownInstance * town) const; }; } diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp index 0f971d290..229cd9121 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp @@ -54,7 +54,9 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta logAi->trace("Evaluating defence for %s", town->getNameTranslated()); auto treatNode = ai->nullkiller->dangerHitMap->getObjectTreat(town); - auto treats = { treatNode.maximumDanger, treatNode.fastestDanger }; + std::vector treats = ai->nullkiller->dangerHitMap->getTownTreats(town); + + treats.push_back(treatNode.fastestDanger); // no guarantee that fastest danger will be there int dayOfWeek = cb->getDate(Date::DAY_OF_WEEK); @@ -131,14 +133,24 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta if(treat.hero.validAndSet() && treat.turn <= 1 - && (treat.danger == treatNode.maximumDanger.danger || treat.turn < treatNode.maximumDanger.turn) - && isSafeToVisit(path.targetHero, path.heroArmy, treat.danger)) + && (treat.danger == treatNode.maximumDanger.danger || treat.turn < treatNode.maximumDanger.turn)) { - Composition composition; + auto heroCapturingPaths = ai->nullkiller->pathfinder->getPathInfo(treat.hero->visitablePos()); + auto goals = CaptureObjectsBehavior::getVisitGoals(heroCapturingPaths, treat.hero.get()); - composition.addNext(DefendTown(town, treat, path)).addNext(CaptureObject(treat.hero.get())); + for(int i = 0; i < heroCapturingPaths.size(); i++) + { + AIPath & path = heroCapturingPaths[i]; + TSubgoal goal = goals[i]; - tasks.push_back(Goals::sptr(composition)); + if(!goal || goal->invalid() || !goal->isElementar()) continue; + + Composition composition; + + composition.addNext(DefendTown(town, treat, path, true)).addNext(goal); + + tasks.push_back(Goals::sptr(composition)); + } } bool treatIsWeak = path.getHeroStrength() / (float)treat.danger > TREAT_IGNORE_RATIO; diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp index 66416df54..e88ab5928 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp @@ -12,6 +12,7 @@ #include "../Engine/Nullkiller.h" #include "../Goals/ExecuteHeroChain.h" #include "../Goals/Composition.h" +#include "../Goals/RecruitHero.h" #include "../Markers/HeroExchange.h" #include "../Markers/ArmyUpgrade.h" #include "GatherArmyBehavior.h" @@ -240,12 +241,22 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) logAi->trace("Found %d paths", paths.size()); #endif + bool hasMainAround = false; + + for(const AIPath & path : paths) + { + auto heroRole = ai->nullkiller->heroManager->getHeroRole(path.targetHero); + + if(heroRole == HeroRole::MAIN && path.turn() < SCOUT_TURN_DISTANCE_LIMIT) + hasMainAround = true; + } + for(const AIPath & path : paths) { #if NKAI_TRACE_LEVEL >= 2 logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength()); #endif - if(upgrader->visitingHero && upgrader->visitingHero.get() != path.targetHero) + if(upgrader->visitingHero && (upgrader->visitingHero.get() != path.targetHero || path.exchangeCount == 1)) { #if NKAI_TRACE_LEVEL >= 2 logAi->trace("Ignore path. Town has visiting hero."); @@ -283,25 +294,58 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) auto upgrade = ai->nullkiller->armyManager->calculateCreaturesUpgrade(path.heroArmy, upgrader, availableResources); - if(!upgrader->garrisonHero && ai->nullkiller->heroManager->getHeroRole(path.targetHero) == HeroRole::MAIN) + if(!upgrader->garrisonHero + && ( + hasMainAround + || ai->nullkiller->heroManager->getHeroRole(path.targetHero) == HeroRole::MAIN)) { - upgrade.upgradeValue += - ai->nullkiller->armyManager->howManyReinforcementsCanGet( + ArmyUpgradeInfo armyToGetOrBuy; + + armyToGetOrBuy.addArmyToGet( + ai->nullkiller->armyManager->getBestArmy( path.targetHero, path.heroArmy, - upgrader->getUpperArmy()); + upgrader->getUpperArmy())); - upgrade.upgradeValue += - ai->nullkiller->armyManager->howManyReinforcementsCanBuy( - path.heroArmy, - upgrader, - ai->nullkiller->getFreeResources(), - path.turn()); + armyToGetOrBuy.addArmyToBuy( + ai->nullkiller->armyManager->toSlotInfo( + ai->nullkiller->armyManager->getArmyAvailableToBuy( + path.heroArmy, + upgrader, + ai->nullkiller->getFreeResources(), + path.turn()))); + + upgrade.upgradeValue += armyToGetOrBuy.upgradeValue; + upgrade.upgradeCost += armyToGetOrBuy.upgradeCost; + vstd::concatenate(upgrade.resultingArmy, armyToGetOrBuy.resultingArmy); + + auto getOrBuyArmyValue = (float)armyToGetOrBuy.upgradeValue / path.getHeroStrength(); + + if(!upgrade.upgradeValue + && armyToGetOrBuy.upgradeValue > 20000 + && ai->nullkiller->heroManager->canRecruitHero(town) + && path.turn() < SCOUT_TURN_DISTANCE_LIMIT) + { + for(auto hero : cb->getAvailableHeroes(town)) + { + auto scoutReinforcement = ai->nullkiller->armyManager->howManyReinforcementsCanBuy(hero, town) + + ai->nullkiller->armyManager->howManyReinforcementsCanGet(hero, town); + + if(scoutReinforcement >= armyToGetOrBuy.upgradeValue + && ai->nullkiller->getFreeGold() >20000 + && ai->nullkiller->buildAnalyzer->getGoldPreasure() < MAX_GOLD_PEASURE) + { + Composition recruitHero; + + recruitHero.addNext(ArmyUpgrade(path.targetHero, town, armyToGetOrBuy)).addNext(RecruitHero(town, hero)); + } + } + } } auto armyValue = (float)upgrade.upgradeValue / path.getHeroStrength(); - if((armyValue < 0.1f && armyValue < 20000) || upgrade.upgradeValue < 300) // avoid small upgrades + if((armyValue < 0.1f && upgrade.upgradeValue < 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/Behaviors/RecruitHeroBehavior.cpp b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp index bbc9dd737..fdf75ba3d 100644 --- a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp +++ b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp @@ -66,6 +66,27 @@ Goals::TGoalVec RecruitHeroBehavior::decompose() const } } + int treasureSourcesCount = 0; + + for(auto obj : ai->nullkiller->objectClusterizer->getNearbyObjects()) + { + if((obj->ID == Obj::RESOURCE && obj->subID == GameResID(EGameResID::GOLD)) + || obj->ID == Obj::TREASURE_CHEST + || obj->ID == Obj::CAMPFIRE + || obj->ID == Obj::WATER_WHEEL + || obj->ID ==Obj::ARTIFACT) + { + auto tile = obj->visitablePos(); + auto closestTown = ai->nullkiller->dangerHitMap->getClosestTown(tile); + + if(town == closestTown) + treasureSourcesCount++; + } + } + + if(treasureSourcesCount < 10) + continue; + if(cb->getHeroesInfo().size() < cb->getTownsInfo().size() + 1 || (ai->nullkiller->getFreeResources()[EGameResID::GOLD] > 10000 && ai->nullkiller->buildAnalyzer->getGoldPreasure() < MAX_GOLD_PEASURE)) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 66ef95c7c..135f1451a 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -282,18 +282,6 @@ uint64_t RewardEvaluator::getArmyReward( switch(target->ID) { - case Obj::TOWN: - { - auto town = dynamic_cast(target); - auto fortLevel = town->fortLevel(); - auto booster = isAnotherAi(town, *ai->cb) ? 1 : 2; - - if(fortLevel < CGTownInstance::CITADEL) - return town->hasFort() ? booster * 500 : 0; - else - return booster * (fortLevel == CGTownInstance::CASTLE ? 5000 : 2000); - } - case Obj::HILL_FORT: return ai->armyManager->calculateCreaturesUpgrade(army, target, ai->cb->getResourceAmount()).upgradeValue; case Obj::CREATURE_BANK: @@ -440,6 +428,22 @@ float RewardEvaluator::getTotalResourceRequirementStrength(int resType) const return std::min(ratio, 1.0f); } +uint64_t RewardEvaluator::townArmyGrowth(const CGTownInstance * town) const +{ + uint64_t result = 0; + + for(auto creatureInfo : town->creatures) + { + if(creatureInfo.second.empty()) + continue; + + auto creature = creatureInfo.second.back().toCreature(); + result += creature->getAIValue() * town->getGrowthInfo(creature->getLevel() - 1).totalGrowth(); + } + + return result; +} + float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) const { if(!target) @@ -475,13 +479,22 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons case Obj::TOWN: { if(ai->buildAnalyzer->getDevelopmentInfo().empty()) - return 1; + return 10.0f; auto town = dynamic_cast(target); - auto fortLevel = town->fortLevel(); - auto booster = isAnotherAi(town, *ai->cb) ? 0.3 : 1; - if(town->hasCapitol()) return 1; + if(town->getOwner() == ai->playerID) + { + auto armyIncome = townArmyGrowth(town); + auto dailyIncome = town->dailyIncome()[EGameResID::GOLD]; + + return std::min(1.0f, std::sqrt(armyIncome / 40000.0f)) + std::min(0.3f, dailyIncome / 10000.0f); + } + + auto fortLevel = town->fortLevel(); + auto booster = isAnotherAi(town, *ai->cb) ? 0.3f : 0.7f; + + if(town->hasCapitol()) return booster; if(fortLevel < CGTownInstance::CITADEL) return booster * (town->hasFort() ? 0.6 : 0.4); @@ -690,23 +703,6 @@ void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uin class DefendTownEvaluator : public IEvaluationContextBuilder { -private: - uint64_t townArmyIncome(const CGTownInstance * town) const - { - uint64_t result = 0; - - for(auto creatureInfo : town->creatures) - { - if(creatureInfo.second.empty()) - continue; - - auto creature = creatureInfo.second.back().toCreature(); - result += creature->getAIValue() * town->getGrowthInfo(creature->getLevel() - 1).totalGrowth(); - } - - return result; - } - public: virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override { @@ -717,10 +713,7 @@ public: const CGTownInstance * town = defendTown.town; auto & treat = defendTown.getTreat(); - auto armyIncome = townArmyIncome(town); - auto dailyIncome = town->dailyIncome()[EGameResID::GOLD]; - - auto strategicalValue = std::sqrt(armyIncome / 60000.0f) + dailyIncome / 10000.0f; + auto strategicalValue = evaluationContext.evaluator.getStrategicalValue(town); if(evaluationContext.evaluator.ai->buildAnalyzer->getDevelopmentInfo().size() == 1) vstd::amax(evaluationContext.strategicalValue, 10.0); @@ -732,9 +725,20 @@ public: multiplier /= 1.0f + treat.turn / 5.0f; - evaluationContext.armyGrowth += armyIncome * multiplier; + if(defendTown.getTurn() > 0 && defendTown.isContrAttack()) + { + auto ourSpeed = defendTown.hero->maxMovePoints(true); + auto enemySpeed = treat.hero->maxMovePoints(true); + + if(enemySpeed > ourSpeed) multiplier *= 0.7f; + } + + auto dailyIncome = town->dailyIncome()[EGameResID::GOLD]; + auto armyGrowth = evaluationContext.evaluator.townArmyGrowth(town); + + evaluationContext.armyGrowth += armyGrowth * multiplier; evaluationContext.goldReward += dailyIncome * 5 * multiplier; - evaluationContext.addNonCriticalStrategicalValue(strategicalValue * multiplier); + evaluationContext.addNonCriticalStrategicalValue(1.7f * multiplier * strategicalValue); vstd::amax(evaluationContext.danger, defendTown.getTreat().danger); addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength()); } diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index 8f7f478f7..fb8085494 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -44,6 +44,7 @@ public: int32_t getGoldReward(const CGObjectInstance * target, const CGHeroInstance * hero) const; uint64_t getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const; const HitMapInfo & getEnemyHeroDanger(const int3 & tile, uint8_t turn) const; + uint64_t townArmyGrowth(const CGTownInstance * town) const; }; struct DLL_EXPORT EvaluationContext diff --git a/AI/Nullkiller/Markers/ArmyUpgrade.cpp b/AI/Nullkiller/Markers/ArmyUpgrade.cpp index bc475583d..0f6d41090 100644 --- a/AI/Nullkiller/Markers/ArmyUpgrade.cpp +++ b/AI/Nullkiller/Markers/ArmyUpgrade.cpp @@ -28,6 +28,13 @@ ArmyUpgrade::ArmyUpgrade(const AIPath & upgradePath, const CGObjectInstance * up sethero(upgradePath.targetHero); } +ArmyUpgrade::ArmyUpgrade(const CGHeroInstance * targetMain, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade) + : CGoal(Goals::ARMY_UPGRADE), upgrader(upgrader), upgradeValue(upgrade.upgradeValue), + initialValue(targetMain->getArmyStrength()), goldCost(upgrade.upgradeCost[EGameResID::GOLD]) +{ + sethero(targetMain); +} + bool ArmyUpgrade::operator==(const ArmyUpgrade & other) const { return false; diff --git a/AI/Nullkiller/Markers/ArmyUpgrade.h b/AI/Nullkiller/Markers/ArmyUpgrade.h index df30c3588..1af41a5bf 100644 --- a/AI/Nullkiller/Markers/ArmyUpgrade.h +++ b/AI/Nullkiller/Markers/ArmyUpgrade.h @@ -27,6 +27,7 @@ namespace Goals public: ArmyUpgrade(const AIPath & upgradePath, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade); + ArmyUpgrade(const CGHeroInstance * targetMain, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade); virtual bool operator==(const ArmyUpgrade & other) const override; virtual std::string toString() const override; diff --git a/AI/Nullkiller/Markers/DefendTown.cpp b/AI/Nullkiller/Markers/DefendTown.cpp index b09952f22..096def550 100644 --- a/AI/Nullkiller/Markers/DefendTown.cpp +++ b/AI/Nullkiller/Markers/DefendTown.cpp @@ -18,8 +18,8 @@ namespace NKAI using namespace Goals; -DefendTown::DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath) - : CGoal(Goals::DEFEND_TOWN), treat(treat), defenceArmyStrength(defencePath.getHeroStrength()), turn(defencePath.turn()) +DefendTown::DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath, bool isContrattack) + : CGoal(Goals::DEFEND_TOWN), treat(treat), defenceArmyStrength(defencePath.getHeroStrength()), turn(defencePath.turn()), contrattack(isContrattack) { settown(town); sethero(defencePath.targetHero); diff --git a/AI/Nullkiller/Markers/DefendTown.h b/AI/Nullkiller/Markers/DefendTown.h index 083f9b07f..bfe2314d9 100644 --- a/AI/Nullkiller/Markers/DefendTown.h +++ b/AI/Nullkiller/Markers/DefendTown.h @@ -24,9 +24,10 @@ namespace Goals uint64_t defenceArmyStrength; HitMapInfo treat; uint8_t turn; + bool contrattack; public: - DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath); + DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath, bool isContrattack = false); DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const CGHeroInstance * defender); virtual bool operator==(const DefendTown & other) const override; @@ -37,6 +38,8 @@ namespace Goals uint64_t getDefenceStrength() const { return defenceArmyStrength; } uint8_t getTurn() const { return turn; } + + bool isContrAttack() { return contrattack; } }; } From 0fd118d3ce88baab8e2d7da4dc7d2f712d139f69 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 2 Jul 2023 10:27:30 +0300 Subject: [PATCH 05/21] NKAI: gold reward --- .../Behaviors/GatherArmyBehavior.cpp | 2 ++ AI/Nullkiller/Engine/PriorityEvaluator.cpp | 4 ++- config/ai/object-priorities.txt | 33 +++++++++++++++---- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp index e88ab5928..4a2b3fc83 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp @@ -307,6 +307,8 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) path.heroArmy, upgrader->getUpperArmy())); + armyToGetOrBuy.upgradeValue -= path.heroArmy->getArmyStrength(); + armyToGetOrBuy.addArmyToBuy( ai->nullkiller->armyManager->toSlotInfo( ai->nullkiller->armyManager->getArmyAvailableToBuy( diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 135f1451a..11efc0ec1 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -1014,6 +1014,8 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task) + (evaluationContext.armyReward > 0 ? 1 : 0) + (evaluationContext.skillReward > 0 ? 1 : 0) + (evaluationContext.strategicalValue > 0 ? 1 : 0); + + auto goldRewardPerTurn = evaluationContext.goldReward / std::log2f(evaluationContext.movementCost * 10); double result = 0; @@ -1023,7 +1025,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task) heroRoleVariable->setValue(evaluationContext.heroRole); mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]); scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]); - goldRewardVariable->setValue(evaluationContext.goldReward); + goldRewardVariable->setValue(goldRewardPerTurn); armyRewardVariable->setValue(evaluationContext.armyReward); armyGrowthVariable->setValue(evaluationContext.armyGrowth); skillRewardVariable->setValue(evaluationContext.skillReward); diff --git a/config/ai/object-priorities.txt b/config/ai/object-priorities.txt index 13ec86617..acef84421 100644 --- a/config/ai/object-priorities.txt +++ b/config/ai/object-priorities.txt @@ -23,11 +23,11 @@ InputVariable: goldReward enabled: true range: 0.000 5000.000 lock-range: true - term: LOW Triangle 10.000 500.000 2000.000 - term: MEDIUM Triangle 500.000 2000.000 5000.000 - term: HIGH Ramp 2000.000 5000.000 - term: NONE Ramp 100.000 0.000 - term: LOWEST Triangle 0.000 100.000 500.000 + term: LOWEST Triangle 0.000 100.000 200.000 + term: SMALL Triangle 100.000 200.000 400.000 + term: MEDIUM Triangle 200.000 400.000 1000.000 + term: BIG Triangle 400.000 1000.000 5000.000 + term: HUGE Ramp 1000.000 5000.000 InputVariable: armyReward enabled: true range: 0.000 10000.000 @@ -97,7 +97,7 @@ InputVariable: goldPreasure range: 0.000 1.000 lock-range: false term: LOW Ramp 0.300 0.000 - term: HIGH Discrete 0.100 0.000 0.250 0.100 0.300 0.200 0.400 0.700 1.000 1.000 + term: HIGH Discrete 0.100 0.000 0.250 0.200 0.300 0.300 0.400 0.700 1.000 1.000 InputVariable: goldCost description: Action cost in gold enabled: true @@ -214,4 +214,23 @@ RuleBlock: armyReward rule: if heroRole is MAIN and armyReward is MEDIUM and mainTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH rule: if heroRole is MAIN and armyReward is MEDIUM and mainTurnDistance is LONG and fear is not HIGH then Value is MEDIUM rule: if heroRole is MAIN and armyReward is LOW and mainTurnDistance is LOW and fear is not HIGH then Value is MEDIUM - rule: if heroRole is MAIN and armyReward is LOW and mainTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL \ No newline at end of file + rule: if heroRole is MAIN and armyReward is LOW and mainTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL +RuleBlock: gold + enabled: true + conjunction: AlgebraicProduct + disjunction: AlgebraicSum + implication: AlgebraicProduct + activation: General + rule: if goldReward is HUGE and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is HIGHEST + rule: if goldReward is HUGE and goldPreasure is HIGH and heroRole is SCOUT and danger is not NONE and armyLoss is LOW then Value is BITHIGH + rule: if goldReward is HUGE and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is HIGHEST + rule: if goldReward is HUGE and goldPreasure is HIGH and heroRole is MAIN and danger is NONE then Value is HIGH + rule: if goldReward is BIG and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is HIGH + rule: if goldReward is BIG and goldPreasure is HIGH and heroRole is SCOUT and danger is not NONE then Value is MEDIUM + rule: if goldReward is BIG and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is HIGH + rule: if goldReward is BIG and goldPreasure is HIGH and heroRole is MAIN and danger is NONE then Value is MEDIUM + rule: if goldReward is MEDIUM and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is BITHIGH + rule: if goldReward is MEDIUM and goldPreasure is HIGH and heroRole is SCOUT and danger is not NONE then Value is SMALL + rule: if goldReward is MEDIUM and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is BITHIGH + rule: if goldReward is SMALL and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is MEDIUM + rule: if goldReward is SMALL and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is SMALL \ No newline at end of file From 69ceee5dd6bc542c55a088ff5185859556f6e255 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 9 Jul 2023 09:56:58 +0300 Subject: [PATCH 06/21] NKAI: penalty for extra chains --- AI/Nullkiller/Analyzers/ArmyManager.cpp | 3 ++- config/ai/object-priorities.txt | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/AI/Nullkiller/Analyzers/ArmyManager.cpp b/AI/Nullkiller/Analyzers/ArmyManager.cpp index 359286a02..37dfb7a76 100644 --- a/AI/Nullkiller/Analyzers/ArmyManager.cpp +++ b/AI/Nullkiller/Analyzers/ArmyManager.cpp @@ -13,6 +13,7 @@ #include "../Engine/Nullkiller.h" #include "../../../CCallback.h" #include "../../../lib/mapObjects/MapObjects.h" +#include "../../../lib/GameConstants.h" namespace NKAI { @@ -306,7 +307,7 @@ std::vector ArmyManager::getArmyAvailableToBuy( int freeHeroSlots = GameConstants::ARMY_SIZE - hero->stacksCount(); bool countGrowth = (cb->getDate(Date::DAY_OF_WEEK) + turn) > 7; - const CGTownInstance * town = dwelling->ID == CGTownInstance::TOWN + const CGTownInstance * town = dwelling->ID == Obj::TOWN ? dynamic_cast(dwelling) : nullptr; diff --git a/config/ai/object-priorities.txt b/config/ai/object-priorities.txt index acef84421..d15d8608b 100644 --- a/config/ai/object-priorities.txt +++ b/config/ai/object-priorities.txt @@ -166,6 +166,11 @@ RuleBlock: basic rule: if closestHeroRatio is LOWEST and heroRole is SCOUT then Value is WORST rule: if closestHeroRatio is LOW and heroRole is SCOUT then Value is BAD rule: if closestHeroRatio is LOWEST and heroRole is MAIN then Value is BAD + rule: if heroRole is SCOUT and turn is NOW and mainTurnDistance is LONG then Value is WORST + rule: if heroRole is SCOUT and turn is NOW and mainTurnDistance is MEDIUM then Value is BAD + rule: if heroRole is SCOUT and turn is NEXT and mainTurnDistance is LONG then Value is BAD + rule: if heroRole is SCOUT and turn is NOW and scoutTurnDistance is LONG then Value is BAD + rule: if heroRole is SCOUT and turn is NOW and scoutTurnDistance is MEDIUM then Value is BAD with 0.3 RuleBlock: strategicalValue enabled: true conjunction: AlgebraicProduct From 5bffea0aac3a4a05d0162fca4b844971d8091a1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Sun, 9 Jul 2023 11:35:50 +0200 Subject: [PATCH 07/21] Use new interface --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 11efc0ec1..e3b66256c 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -727,8 +727,8 @@ public: if(defendTown.getTurn() > 0 && defendTown.isContrAttack()) { - auto ourSpeed = defendTown.hero->maxMovePoints(true); - auto enemySpeed = treat.hero->maxMovePoints(true); + auto ourSpeed = defendTown.hero->movementPointsLimit(true); + auto enemySpeed = treat.hero->movementPointsLimit(true); if(enemySpeed > ourSpeed) multiplier *= 0.7f; } @@ -894,7 +894,7 @@ public: const CGHeroInstance * dismissedHero = dismissCommand.getHero().get(); auto role = ai->heroManager->getHeroRole(dismissedHero); - auto mpLeft = dismissedHero->movement; + auto mpLeft = dismissedHero->movementPointsRemaining(); evaluationContext.movementCost += mpLeft; evaluationContext.movementCostByRole[role] += mpLeft; From e483f06e0feccf5aa62fdc4b5eeb5da45292920c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Sun, 16 Jul 2023 08:23:37 +0200 Subject: [PATCH 08/21] Remove unused variable --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index e3b66256c..cb492f7f6 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -316,8 +316,6 @@ uint64_t RewardEvaluator::getArmyGrowth( const CGHeroInstance * hero, const CCreatureSet * army) const { - const float enemyArmyEliminationRewardRatio = 0.5f; - if(!target) return 0; From 88178567b1c741db14bd207e64b31743e5132eed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Sun, 16 Jul 2023 08:42:06 +0200 Subject: [PATCH 09/21] Remove unused variable --- AI/Nullkiller/Helpers/ArmyFormation.h | 1 - 1 file changed, 1 deletion(-) diff --git a/AI/Nullkiller/Helpers/ArmyFormation.h b/AI/Nullkiller/Helpers/ArmyFormation.h index e5a900c01..eb7d07657 100644 --- a/AI/Nullkiller/Helpers/ArmyFormation.h +++ b/AI/Nullkiller/Helpers/ArmyFormation.h @@ -28,7 +28,6 @@ class DLL_EXPORT ArmyFormation { private: std::shared_ptr cb; //this is enough, but we downcast from CCallback - const Nullkiller * ai; public: ArmyFormation(std::shared_ptr CB, const Nullkiller * ai): cb(CB), ai(ai) {} From 850da65f80175c798d739027effb8d6fe0c058b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Sun, 16 Jul 2023 08:53:23 +0200 Subject: [PATCH 10/21] Another build fix --- AI/Nullkiller/Helpers/ArmyFormation.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/Nullkiller/Helpers/ArmyFormation.h b/AI/Nullkiller/Helpers/ArmyFormation.h index eb7d07657..817c6158d 100644 --- a/AI/Nullkiller/Helpers/ArmyFormation.h +++ b/AI/Nullkiller/Helpers/ArmyFormation.h @@ -30,7 +30,7 @@ private: std::shared_ptr cb; //this is enough, but we downcast from CCallback public: - ArmyFormation(std::shared_ptr CB, const Nullkiller * ai): cb(CB), ai(ai) {} + ArmyFormation(std::shared_ptr CB, const Nullkiller * ai): cb(CB) {} void rearrangeArmyForSiege(const CGTownInstance * town, const CGHeroInstance * attacker); }; From db2be3ee053cbf9df6b1fd5e2967a152f85a0935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Sun, 16 Jul 2023 09:02:14 +0200 Subject: [PATCH 11/21] Anoother unused variable --- AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp index 4a2b3fc83..80c74e8b1 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp @@ -321,8 +321,6 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) upgrade.upgradeCost += armyToGetOrBuy.upgradeCost; vstd::concatenate(upgrade.resultingArmy, armyToGetOrBuy.resultingArmy); - auto getOrBuyArmyValue = (float)armyToGetOrBuy.upgradeValue / path.getHeroStrength(); - if(!upgrade.upgradeValue && armyToGetOrBuy.upgradeValue > 20000 && ai->nullkiller->heroManager->canRecruitHero(town) From 5083100d3b9ab366c14b3686d225bdbc961e849c Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Mon, 24 Jul 2023 22:19:09 +0300 Subject: [PATCH 12/21] NKA: fix accessing removed hero and heroExchangeCount --- AI/Nullkiller/Analyzers/HeroManager.cpp | 1 + AI/Nullkiller/Pathfinding/Actors.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/AI/Nullkiller/Analyzers/HeroManager.cpp b/AI/Nullkiller/Analyzers/HeroManager.cpp index 6afa78ec7..76da42619 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -125,6 +125,7 @@ void HeroManager::update() } std::sort(myHeroes.begin(), myHeroes.end(), scoreSort); + heroRoles.clear(); for(auto hero : myHeroes) { diff --git a/AI/Nullkiller/Pathfinding/Actors.cpp b/AI/Nullkiller/Pathfinding/Actors.cpp index 3fd24a222..bf176b3ec 100644 --- a/AI/Nullkiller/Pathfinding/Actors.cpp +++ b/AI/Nullkiller/Pathfinding/Actors.cpp @@ -134,6 +134,7 @@ void ChainActor::setBaseActor(HeroActor * base) armyCost = base->armyCost; actorAction = base->actorAction; tiCache = base->tiCache; + actorExchangeCount = base->actorExchangeCount; } void HeroActor::setupSpecialActors() From 202e13ce2e31824f41692871cc2ba0df8c67b42e Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Tue, 25 Jul 2023 17:26:35 +0300 Subject: [PATCH 13/21] NKAI: log paths scan depth --- AI/Nullkiller/Pathfinding/AIPathfinder.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/AI/Nullkiller/Pathfinding/AIPathfinder.cpp b/AI/Nullkiller/Pathfinding/AIPathfinder.cpp index 17cff54db..b51d19bbe 100644 --- a/AI/Nullkiller/Pathfinding/AIPathfinder.cpp +++ b/AI/Nullkiller/Pathfinding/AIPathfinder.cpp @@ -61,6 +61,11 @@ void AIPathfinder::updatePaths(std::map heroes storage->setScoutTurnDistanceLimit(pathfinderSettings.scoutTurnDistanceLimit); storage->setMainTurnDistanceLimit(pathfinderSettings.mainTurnDistanceLimit); + logAi->trace( + "Scout turn distance: %s, main %s", + std::to_string(pathfinderSettings.scoutTurnDistanceLimit), + std::to_string(pathfinderSettings.mainTurnDistanceLimit)); + if(pathfinderSettings.useHeroChain) { storage->setTownsAndDwellings(cb->getTownsInfo(), ai->memory->visitableObjs); From c93bb0a5028018c02265fd03481241f02294f7f7 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Thu, 27 Jul 2023 15:58:49 +0300 Subject: [PATCH 14/21] nkai: fixes and skill rewards --- AI/Nullkiller/AIGateway.cpp | 5 + AI/Nullkiller/Analyzers/BuildAnalyzer.cpp | 48 +++- .../Behaviors/CaptureObjectsBehavior.cpp | 2 +- AI/Nullkiller/Behaviors/DefenceBehavior.cpp | 230 ++++++++++-------- .../Behaviors/GatherArmyBehavior.cpp | 23 +- AI/Nullkiller/Engine/Nullkiller.cpp | 46 +++- AI/Nullkiller/Engine/Nullkiller.h | 6 +- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 8 +- config/ai/object-priorities.txt | 22 +- 9 files changed, 261 insertions(+), 129 deletions(-) diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index c3bebecbb..6301b9223 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -1058,6 +1058,11 @@ void AIGateway::recruitCreatures(const CGDwelling * d, const CArmedInstance * re int count = d->creatures[i].first; CreatureID creID = d->creatures[i].second.back(); + if(!recruiter->getSlotFor(creID).validSlot()) + { + continue; + } + vstd::amin(count, cb->getResourceAmount() / creID.toCreature()->getFullRecruitCost()); if(count > 0) cb->recruitCreatures(d, recruiter, creID, count, i); diff --git a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp index 8318718c8..e946ecea6 100644 --- a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp @@ -68,19 +68,22 @@ void BuildAnalyzer::updateOtherBuildings(TownDevelopmentInfo & developmentInfo) logAi->trace("Checking other buildings"); std::vector> otherBuildings = { - {BuildingID::TOWN_HALL, BuildingID::CITY_HALL, BuildingID::CAPITOL} + {BuildingID::TOWN_HALL, BuildingID::CITY_HALL, BuildingID::CAPITOL}, + {BuildingID::MAGES_GUILD_3, BuildingID::MAGES_GUILD_5} }; if(developmentInfo.existingDwellings.size() >= 2 && ai->cb->getDate(Date::DAY_OF_WEEK) > boost::date_time::Friday) { otherBuildings.push_back({BuildingID::CITADEL, BuildingID::CASTLE}); + otherBuildings.push_back({BuildingID::HORDE_1}); + otherBuildings.push_back({BuildingID::HORDE_2}); } for(auto & buildingSet : otherBuildings) { for(auto & buildingID : buildingSet) { - if(!developmentInfo.town->hasBuilt(buildingID)) + if(!developmentInfo.town->hasBuilt(buildingID) && developmentInfo.town->town->buildings.count(buildingID)) { developmentInfo.addBuildingToBuild(getBuildingOrPrerequisite(developmentInfo.town, buildingID)); @@ -190,12 +193,28 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite( const CCreature * creature = nullptr; CreatureID baseCreatureID; + int creatureLevel = -1; + int creatureUpgrade = 0; + if(BuildingID::DWELL_FIRST <= toBuild && toBuild <= BuildingID::DWELL_UP_LAST) { - int level = toBuild - BuildingID::DWELL_FIRST; - auto creatures = townInfo->creatures.at(level % GameConstants::CREATURES_PER_TOWN); - auto creatureID = creatures.size() > level / GameConstants::CREATURES_PER_TOWN - ? creatures.at(level / GameConstants::CREATURES_PER_TOWN) + creatureLevel = (toBuild - BuildingID::DWELL_FIRST) % GameConstants::CREATURES_PER_TOWN; + creatureUpgrade = (toBuild - BuildingID::DWELL_FIRST) / GameConstants::CREATURES_PER_TOWN; + } + else if(toBuild == BuildingID::HORDE_1 || toBuild == BuildingID::HORDE_1_UPGR) + { + creatureLevel = townInfo->hordeLvl.at(0); + } + else if(toBuild == BuildingID::HORDE_2 || toBuild == BuildingID::HORDE_2_UPGR) + { + creatureLevel = townInfo->hordeLvl.at(1); + } + + if(creatureLevel >= 0) + { + auto creatures = townInfo->creatures.at(creatureLevel); + auto creatureID = creatures.size() > creatureUpgrade + ? creatures.at(creatureUpgrade) : creatures.front(); baseCreatureID = creatures.front(); @@ -366,12 +385,19 @@ BuildingInfo::BuildingInfo( } else { - creatureGrows = creature->getGrowth(); + if(BuildingID::DWELL_FIRST <= id && id <= BuildingID::DWELL_UP_LAST) + { + creatureGrows = creature->getGrowth(); - if(town->hasBuilt(BuildingID::CASTLE)) - creatureGrows *= 2; - else if(town->hasBuilt(BuildingID::CITADEL)) - creatureGrows += creatureGrows / 2; + if(town->hasBuilt(BuildingID::CASTLE)) + creatureGrows *= 2; + else if(town->hasBuilt(BuildingID::CITADEL)) + creatureGrows += creatureGrows / 2; + } + else + { + creatureGrows = creature->getHorde(); + } } armyStrength = ai->armyManager->evaluateStackPower(creature, creatureGrows); diff --git a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp index 132861467..155f45af6 100644 --- a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp +++ b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp @@ -213,7 +213,7 @@ Goals::TGoalVec CaptureObjectsBehavior::decompose() const { captureObjects(ai->nullkiller->objectClusterizer->getNearbyObjects()); - if(tasks.empty() || ai->nullkiller->getScanDepth() == ScanDepth::FULL) + if(tasks.empty() || ai->nullkiller->getScanDepth() != ScanDepth::SMALL) captureObjects(ai->nullkiller->objectClusterizer->getFarObjects()); } diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp index 229cd9121..56bd4118c 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp @@ -49,41 +49,98 @@ Goals::TGoalVec DefenceBehavior::decompose() const return tasks; } +bool isTreatUnderControl(const CGTownInstance * town, const HitMapInfo & treat, const std::vector & paths) +{ + int dayOfWeek = cb->getDate(Date::DAY_OF_WEEK); + + for(const AIPath & path : paths) + { + bool treatIsWeak = path.getHeroStrength() / (float)treat.danger > TREAT_IGNORE_RATIO; + bool needToSaveGrowth = treat.turn == 0 && dayOfWeek == 7; + + if(treatIsWeak && !needToSaveGrowth) + { + if((path.exchangeCount == 1 && path.turn() < treat.turn) + || path.turn() < treat.turn - 1 + || (path.turn() < treat.turn && treat.turn >= 2)) + { +#if NKAI_TRACE_LEVEL >= 1 + logAi->trace( + "Hero %s can eliminate danger for town %s using path %s.", + path.targetHero->getObjectName(), + town->getObjectName(), + path.toString()); +#endif + + return true; + } + } + } + + return false; +} + +void handleCounterAttack( + const CGTownInstance * town, + const HitMapInfo & treat, + const HitMapInfo & maximumDanger, + Goals::TGoalVec & tasks) +{ + if(treat.hero.validAndSet() + && treat.turn <= 1 + && (treat.danger == maximumDanger.danger || treat.turn < maximumDanger.turn)) + { + auto heroCapturingPaths = ai->nullkiller->pathfinder->getPathInfo(treat.hero->visitablePos()); + auto goals = CaptureObjectsBehavior::getVisitGoals(heroCapturingPaths, treat.hero.get()); + + for(int i = 0; i < heroCapturingPaths.size(); i++) + { + AIPath & path = heroCapturingPaths[i]; + TSubgoal goal = goals[i]; + + if(!goal || goal->invalid() || !goal->isElementar()) continue; + + Composition composition; + + composition.addNext(DefendTown(town, treat, path, true)).addNext(goal); + + tasks.push_back(Goals::sptr(composition)); + } + } +} + +bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoalVec & tasks) +{ + if(ai->nullkiller->isHeroLocked(town->garrisonHero.get())) + { + logAi->trace( + "Hero %s in garrison of town %s is suposed to defend the town", + town->garrisonHero->getNameTranslated(), + town->getNameTranslated()); + + return true; + } + + if(!town->visitingHero && cb->getHeroCount(ai->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER) + { + logAi->trace( + "Extracting hero %s from garrison of town %s", + town->garrisonHero->getNameTranslated(), + town->getNameTranslated()); + + tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5))); + + return true; + } + + return false; +} + void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town) const { logAi->trace("Evaluating defence for %s", town->getNameTranslated()); auto treatNode = ai->nullkiller->dangerHitMap->getObjectTreat(town); - std::vector treats = ai->nullkiller->dangerHitMap->getTownTreats(town); - - treats.push_back(treatNode.fastestDanger); // no guarantee that fastest danger will be there - - int dayOfWeek = cb->getDate(Date::DAY_OF_WEEK); - - if(town->garrisonHero) - { - if(ai->nullkiller->isHeroLocked(town->garrisonHero.get())) - { - logAi->trace( - "Hero %s in garrison of town %s is suposed to defend the town", - town->garrisonHero->getNameTranslated(), - town->getNameTranslated()); - - return; - } - - if(!town->visitingHero && cb->getHeroCount(ai->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER) - { - logAi->trace( - "Extracting hero %s from garrison of town %s", - town->garrisonHero->getNameTranslated(), - town->getNameTranslated()); - - tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5))); - - return; - } - } if(!treatNode.fastestDanger.hero) { @@ -91,6 +148,15 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta return; } + + std::vector treats = ai->nullkiller->dangerHitMap->getTownTreats(town); + + treats.push_back(treatNode.fastestDanger); // no guarantee that fastest danger will be there + + if(town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks)) + { + return; + } uint64_t reinforcement = ai->nullkiller->armyManager->howManyReinforcementsCanBuy(town->getUpperArmy(), town); @@ -111,74 +177,12 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta std::to_string(treat.turn), treat.hero->getNameTranslated()); - bool treatIsUnderControl = false; + handleCounterAttack(town, treat, treatNode.maximumDanger, tasks); - for(AIPath & path : paths) + if(isTreatUnderControl(town, treat, paths)) { - if(town->visitingHero && path.targetHero == town->visitingHero.get()) - { - if(path.getHeroStrength() < town->visitingHero->getHeroStrength()) - continue; - } - else if(town->garrisonHero && path.targetHero == town->garrisonHero.get()) - { - if(path.getHeroStrength() < town->visitingHero->getHeroStrength()) - continue; - } - else - { - if(town->visitingHero) - continue; - } - - if(treat.hero.validAndSet() - && treat.turn <= 1 - && (treat.danger == treatNode.maximumDanger.danger || treat.turn < treatNode.maximumDanger.turn)) - { - auto heroCapturingPaths = ai->nullkiller->pathfinder->getPathInfo(treat.hero->visitablePos()); - auto goals = CaptureObjectsBehavior::getVisitGoals(heroCapturingPaths, treat.hero.get()); - - for(int i = 0; i < heroCapturingPaths.size(); i++) - { - AIPath & path = heroCapturingPaths[i]; - TSubgoal goal = goals[i]; - - if(!goal || goal->invalid() || !goal->isElementar()) continue; - - Composition composition; - - composition.addNext(DefendTown(town, treat, path, true)).addNext(goal); - - tasks.push_back(Goals::sptr(composition)); - } - } - - bool treatIsWeak = path.getHeroStrength() / (float)treat.danger > TREAT_IGNORE_RATIO; - bool needToSaveGrowth = treat.turn == 0 && dayOfWeek == 7; - - if(treatIsWeak && !needToSaveGrowth) - { - if((path.exchangeCount == 1 && path.turn() < treat.turn) - || path.turn() < treat.turn - 1 - || (path.turn() < treat.turn && treat.turn >= 2)) - { -#if NKAI_TRACE_LEVEL >= 1 - logAi->trace( - "Hero %s can eliminate danger for town %s using path %s.", - path.targetHero->getObjectName(), - town->getObjectName(), - path.toString()); -#endif - - treatIsUnderControl = true; - - break; - } - } - } - - if(treatIsUnderControl) continue; + } evaluateRecruitingHero(tasks, treat, town); @@ -205,6 +209,27 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta path.movementCost(), path.toString()); #endif + + auto townDefenseStrength = town->garrisonHero + ? town->garrisonHero->getTotalStrength() + : (town->visitingHero ? town->visitingHero->getTotalStrength() : town->getUpperArmy()->getArmyStrength()); + + if(town->visitingHero && path.targetHero == town->visitingHero.get()) + { + if(path.getHeroStrength() < townDefenseStrength) + continue; + } + else if(town->garrisonHero && path.targetHero == town->garrisonHero.get()) + { + if(path.getHeroStrength() < townDefenseStrength) + continue; + } + else + { + if(town->visitingHero) + continue; + } + if(path.turn() <= treat.turn - 2) { #if NKAI_TRACE_LEVEL >= 1 @@ -296,7 +321,20 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta composition.addNext(DefendTown(town, treat, path)); TGoalVec sequence; - if(town->visitingHero && path.targetHero != town->visitingHero && !path.containsHero(town->visitingHero)) + if(town->garrisonHero && path.targetHero == town->garrisonHero.get() && path.exchangeCount == 1) + { + composition.addNext(ExchangeSwapTownHeroes(town, town->garrisonHero.get(), HeroLockedReason::DEFENCE)); + tasks.push_back(Goals::sptr(composition)); + +#if NKAI_TRACE_LEVEL >= 1 + logAi->trace("Locking hero %s in garrison of %s", + town->garrisonHero.get()->getObjectName(), + town->getObjectName()); +#endif + + continue; + } + else if(town->visitingHero && path.targetHero != town->visitingHero && !path.containsHero(town->visitingHero)) { if(town->garrisonHero) { diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp index 80c74e8b1..192628111 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp @@ -16,6 +16,7 @@ #include "../Markers/HeroExchange.h" #include "../Markers/ArmyUpgrade.h" #include "GatherArmyBehavior.h" +#include "CaptureObjectsBehavior.h" #include "../AIUtility.h" #include "../Goals/ExchangeSwapTownHeroes.h" @@ -235,6 +236,8 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) #endif auto paths = ai->nullkiller->pathfinder->getPathInfo(pos); + auto goals = CaptureObjectsBehavior::getVisitGoals(paths); + std::vector> waysToVisitObj; #if NKAI_TRACE_LEVEL >= 1 @@ -251,11 +254,23 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) hasMainAround = true; } - for(const AIPath & path : paths) + for(int i = 0; i < paths.size(); i++) { + auto & path = paths[i]; + auto visitGoal = goals[i]; + #if NKAI_TRACE_LEVEL >= 2 logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength()); #endif + + if(visitGoal->invalid()) + { +#if NKAI_TRACE_LEVEL >= 2 + logAi->trace("Ignore path. Not valid way."); +#endif + continue; + } + if(upgrader->visitingHero && (upgrader->visitingHero.get() != path.targetHero || path.exchangeCount == 1)) { #if NKAI_TRACE_LEVEL >= 2 @@ -370,11 +385,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) if(isSafe) { - ExecuteHeroChain newWay(path, upgrader); - - newWay.closestWayRatio = 1; - - tasks.push_back(sptr(Composition().addNext(ArmyUpgrade(path, upgrader, upgrade)).addNext(newWay))); + tasks.push_back(sptr(Composition().addNext(ArmyUpgrade(path, upgrader, upgrade)).addNext(visitGoal))); } } diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 55976d638..8aadd804b 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -118,7 +118,7 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TSubgoal behavior, int decompositi void Nullkiller::resetAiState() { lockedResources = TResources(); - scanDepth = ScanDepth::FULL; + scanDepth = ScanDepth::MAIN_FULL; playerID = ai->playerID; lockedHeroes.clear(); dangerHitMap->reset(); @@ -158,11 +158,15 @@ void Nullkiller::updateAiState(int pass, bool fast) PathfinderSettings cfg; cfg.useHeroChain = useHeroChain; - cfg.scoutTurnDistanceLimit = SCOUT_TURN_DISTANCE_LIMIT; - if(scanDepth != ScanDepth::FULL) + if(scanDepth == ScanDepth::SMALL) { - cfg.mainTurnDistanceLimit = MAIN_TURN_DISTANCE_LIMIT * ((int)scanDepth + 1); + cfg.mainTurnDistanceLimit = MAIN_TURN_DISTANCE_LIMIT; + } + + if(scanDepth != ScanDepth::ALL_FULL) + { + cfg.scoutTurnDistanceLimit = SCOUT_TURN_DISTANCE_LIMIT; } boost::this_thread::interruption_point(); @@ -233,8 +237,8 @@ void Nullkiller::makeTurn() updateAiState(i); Goals::TTask bestTask = taskptr(Goals::Invalid()); - - do + + for(;i <= MAXPASS; i++) { Goals::TTaskVec fastTasks = { choseBestTask(sptr(BuyArmyBehavior()), 1), @@ -248,7 +252,11 @@ void Nullkiller::makeTurn() executeTask(bestTask); updateAiState(i, true); } - } while(bestTask->priority >= FAST_TASK_MINIMAL_PRIORITY); + else + { + break; + } + } Goals::TTaskVec bestTasks = { bestTask, @@ -267,7 +275,6 @@ void Nullkiller::makeTurn() bestTask = choseBestTask(bestTasks); HeroPtr hero = bestTask->getHero(); - HeroRole heroRole = HeroRole::MAIN; if(hero.validAndSet()) @@ -276,20 +283,39 @@ void Nullkiller::makeTurn() 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::FULL) + && scanDepth == ScanDepth::MAIN_FULL) { useHeroChain = false; scanDepth = ScanDepth::SMALL; logAi->trace( - "Goal %s has too low priority %f so increasing scan depth", + "Goal %s has low priority %f so decreasing scan depth to gain performance.", bestTask->toString(), 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.", + bestTask->toString(), + 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.", bestTask->toString()); return; diff --git a/AI/Nullkiller/Engine/Nullkiller.h b/AI/Nullkiller/Engine/Nullkiller.h index d47e634ea..36f3504fd 100644 --- a/AI/Nullkiller/Engine/Nullkiller.h +++ b/AI/Nullkiller/Engine/Nullkiller.h @@ -40,9 +40,11 @@ enum class HeroLockedReason enum class ScanDepth { - FULL = 0, + MAIN_FULL = 0, - SMALL = 1 + SMALL = 1, + + ALL_FULL = 2 }; class Nullkiller diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index cb492f7f6..b45f7e4d5 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -915,6 +915,7 @@ public: evaluationContext.heroRole = HeroRole::MAIN; evaluationContext.movementCostByRole[evaluationContext.heroRole] += bi.prerequisitesCount; evaluationContext.goldCost += bi.buildCostWithPrerequisits[EGameResID::GOLD]; + evaluationContext.closestWayRatio = 1; if(bi.creatureID != CreatureID::NONE) { @@ -938,7 +939,12 @@ public: evaluationContext.addNonCriticalStrategicalValue(buildThis.town->creatures.size() * 0.2f); evaluationContext.armyReward += buildThis.townInfo.armyStrength / 2; } - else + else if(bi.id >= BuildingID::MAGES_GUILD_1 && bi.id <= BuildingID::MAGES_GUILD_5) + { + evaluationContext.skillReward += 2 * (bi.id - BuildingID::MAGES_GUILD_1); + } + + if(evaluationContext.goldReward) { auto goldPreasure = evaluationContext.evaluator.ai->buildAnalyzer->getGoldPreasure(); diff --git a/config/ai/object-priorities.txt b/config/ai/object-priorities.txt index d15d8608b..a5b3fa5ed 100644 --- a/config/ai/object-priorities.txt +++ b/config/ai/object-priorities.txt @@ -5,7 +5,7 @@ InputVariable: mainTurnDistance enabled: true range: 0.000 10.000 lock-range: true - term: LOWEST Ramp 0.250 0.000 + term: LOWEST Ramp 0.400 0.000 term: LOW Discrete 0.000 1.000 0.500 0.800 0.800 0.300 2.000 0.000 term: MEDIUM Discrete 0.000 0.000 0.500 0.200 0.800 0.700 2.000 1.000 6.000 0.000 term: LONG Discrete 2.000 0.000 6.000 1.000 10.000 0.800 @@ -238,4 +238,22 @@ RuleBlock: gold rule: if goldReward is MEDIUM and goldPreasure is HIGH and heroRole is SCOUT and danger is not NONE then Value is SMALL rule: if goldReward is MEDIUM and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is BITHIGH rule: if goldReward is SMALL and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is MEDIUM - rule: if goldReward is SMALL and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is SMALL \ No newline at end of file + rule: if goldReward is SMALL and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is SMALL +RuleBlock: skill reward + enabled: true + conjunction: AlgebraicProduct + disjunction: AlgebraicSum + implication: AlgebraicProduct + activation: General + rule: if heroRole is MAIN and skillReward is LOW and mainTurnDistance is LOWEST and fear is not HIGH then Value is HIGH + rule: if heroRole is MAIN and skillReward is MEDIUM and mainTurnDistance is LOWEST and fear is not HIGH then Value is HIGHEST + rule: if heroRole is MAIN and skillReward is HIGH and mainTurnDistance is LOWEST and fear is not HIGH then Value is HIGHEST + rule: if heroRole is MAIN and skillReward is LOW and mainTurnDistance is LOW and fear is not HIGH then Value is BITHIGH + rule: if heroRole is MAIN and skillReward is MEDIUM and mainTurnDistance is LOW and fear is not HIGH then Value is HIGH + rule: if heroRole is MAIN and skillReward is HIGH and mainTurnDistance is LOW and fear is not HIGH then Value is HIGHEST + rule: if heroRole is MAIN and skillReward is LOW and mainTurnDistance is MEDIUM and fear is not HIGH then Value is MEDIUM + rule: if heroRole is MAIN and skillReward is MEDIUM and mainTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH + rule: if heroRole is MAIN and skillReward is HIGH and mainTurnDistance is MEDIUM and fear is not HIGH then Value is HIGH + rule: if heroRole is MAIN and skillReward is LOW and mainTurnDistance is LONG and fear is not HIGH then Value is SMALL + rule: if heroRole is MAIN and skillReward is MEDIUM and mainTurnDistance is LONG and fear is not HIGH then Value is MEDIUM + rule: if heroRole is MAIN and skillReward is HIGH and mainTurnDistance is LONG and fear is not HIGH then Value is BITHIGH \ No newline at end of file From 6490c65490ed8ea3073b65e727827ac301049a00 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Fri, 28 Jul 2023 14:17:01 +0300 Subject: [PATCH 15/21] nkai: fix freezes --- AI/Nullkiller/Analyzers/HeroManager.cpp | 25 ++++++++ AI/Nullkiller/Analyzers/HeroManager.h | 2 + AI/Nullkiller/Behaviors/DefenceBehavior.cpp | 70 +++++++++------------ AI/Nullkiller/Engine/PriorityEvaluator.cpp | 11 ++-- config/ai/object-priorities.txt | 15 ++++- 5 files changed, 78 insertions(+), 45 deletions(-) diff --git a/AI/Nullkiller/Analyzers/HeroManager.cpp b/AI/Nullkiller/Analyzers/HeroManager.cpp index 76da42619..44b1df23a 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -229,6 +229,31 @@ const CGHeroInstance * HeroManager::findHeroWithGrail() const return nullptr; } +const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) const +{ + const CGHeroInstance * weakestHero = nullptr; + auto myHeroes = ai->cb->getHeroesInfo(); + + for(auto existingHero : myHeroes) + { + if(ai->isHeroLocked(existingHero) + || existingHero->getArmyStrength() >armyLimit + || getHeroRole(existingHero) == HeroRole::MAIN + || existingHero->movementPointsRemaining() + || existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1)) + { + continue; + } + + if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength()) + { + weakestHero = existingHero; + } + } + + return weakestHero; +} + SecondarySkillScoreMap::SecondarySkillScoreMap(std::map scoreMap) :scoreMap(scoreMap) { diff --git a/AI/Nullkiller/Analyzers/HeroManager.h b/AI/Nullkiller/Analyzers/HeroManager.h index 84da85b98..1009fd31e 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.h +++ b/AI/Nullkiller/Analyzers/HeroManager.h @@ -33,6 +33,7 @@ public: virtual bool canRecruitHero(const CGTownInstance * t = nullptr) const = 0; virtual bool heroCapReached() const = 0; virtual const CGHeroInstance * findHeroWithGrail() const = 0; + virtual const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const = 0; }; class DLL_EXPORT ISecondarySkillRule @@ -74,6 +75,7 @@ public: bool canRecruitHero(const CGTownInstance * t = nullptr) const override; bool heroCapReached() const override; const CGHeroInstance * findHeroWithGrail() const override; + const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const override; private: float evaluateFightingStrength(const CGHeroInstance * hero) const; diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp index 56bd4118c..3e1ee23f2 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp @@ -121,16 +121,31 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa return true; } - if(!town->visitingHero && cb->getHeroCount(ai->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER) + if(!town->visitingHero) { - logAi->trace( - "Extracting hero %s from garrison of town %s", - town->garrisonHero->getNameTranslated(), - town->getNameTranslated()); + if(cb->getHeroCount(ai->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER) + { + logAi->trace( + "Extracting hero %s from garrison of town %s", + town->garrisonHero->getNameTranslated(), + town->getNameTranslated()); - tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5))); + tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5))); - return true; + return true; + } + else if(ai->nullkiller->heroManager->getHeroRole(town->garrisonHero.get()) == HeroRole::MAIN) + { + auto armyDismissLimit = 1000; + auto heroToDismiss = ai->nullkiller->heroManager->findWeakHeroToDismiss(armyDismissLimit); + + if(heroToDismiss) + { + tasks.push_back(Goals::sptr(Goals::DismissHero(heroToDismiss).setpriority(5))); + + return true; + } + } } return false; @@ -141,14 +156,6 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta logAi->trace("Evaluating defence for %s", town->getNameTranslated()); auto treatNode = ai->nullkiller->dangerHitMap->getObjectTreat(town); - - if(!treatNode.fastestDanger.hero) - { - logAi->trace("No treat found for town %s", town->getNameTranslated()); - - return; - } - std::vector treats = ai->nullkiller->dangerHitMap->getTownTreats(town); treats.push_back(treatNode.fastestDanger); // no guarantee that fastest danger will be there @@ -157,6 +164,13 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta { return; } + + if(!treatNode.fastestDanger.hero) + { + logAi->trace("No treat found for town %s", town->getNameTranslated()); + + return; + } uint64_t reinforcement = ai->nullkiller->armyManager->howManyReinforcementsCanBuy(town->getUpperArmy(), town); @@ -224,11 +238,6 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta if(path.getHeroStrength() < townDefenseStrength) continue; } - else - { - if(town->visitingHero) - continue; - } if(path.turn() <= treat.turn - 2) { @@ -440,27 +449,10 @@ void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitM } else if(ai->nullkiller->heroManager->heroCapReached()) { - const CGHeroInstance * weakestHero = nullptr; + heroToDismiss = ai->nullkiller->heroManager->findWeakHeroToDismiss(hero->getArmyStrength()); - for(auto existingHero : myHeroes) - { - if(ai->nullkiller->isHeroLocked(existingHero) - || existingHero->getArmyStrength() > hero->getArmyStrength() - || ai->nullkiller->heroManager->getHeroRole(existingHero) == HeroRole::MAIN - || existingHero->movementPointsRemaining() - || existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1)) - continue; - - if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength()) - { - weakestHero = existingHero; - } - } - - if(!weakestHero) + if(!heroToDismiss) continue; - - heroToDismiss = weakestHero; } TGoalVec sequence; diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index b45f7e4d5..ac6c9618c 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -592,7 +592,6 @@ int32_t getArmyCost(const CArmedInstance * army) return value; } -/// Gets aproximated reward in gold. Daily income is multiplied by 5 int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CGHeroInstance * hero) const { if(!target) @@ -713,9 +712,6 @@ public: auto strategicalValue = evaluationContext.evaluator.getStrategicalValue(town); - if(evaluationContext.evaluator.ai->buildAnalyzer->getDevelopmentInfo().size() == 1) - vstd::amax(evaluationContext.strategicalValue, 10.0); - float multiplier = 1; if(treat.turn < defendTown.getTurn()) @@ -736,7 +732,12 @@ public: evaluationContext.armyGrowth += armyGrowth * multiplier; evaluationContext.goldReward += dailyIncome * 5 * multiplier; - evaluationContext.addNonCriticalStrategicalValue(1.7f * multiplier * strategicalValue); + + if(evaluationContext.evaluator.ai->buildAnalyzer->getDevelopmentInfo().size() == 1) + vstd::amax(evaluationContext.strategicalValue, 2.5f * multiplier * strategicalValue); + else + evaluationContext.addNonCriticalStrategicalValue(1.7f * multiplier * strategicalValue); + vstd::amax(evaluationContext.danger, defendTown.getTreat().danger); addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength()); } diff --git a/config/ai/object-priorities.txt b/config/ai/object-priorities.txt index a5b3fa5ed..d5439ee18 100644 --- a/config/ai/object-priorities.txt +++ b/config/ai/object-priorities.txt @@ -171,6 +171,10 @@ RuleBlock: basic rule: if heroRole is SCOUT and turn is NEXT and mainTurnDistance is LONG then Value is BAD rule: if heroRole is SCOUT and turn is NOW and scoutTurnDistance is LONG then Value is BAD rule: if heroRole is SCOUT and turn is NOW and scoutTurnDistance is MEDIUM then Value is BAD with 0.3 + rule: if heroRole is SCOUT and fear is HIGH then Value is BAD with 0.8 + rule: if heroRole is SCOUT and fear is MEDIUM then Value is BAD with 0.5 + rule: if heroRole is MAIN and fear is HIGH then Value is BAD with 0.5 + rule: if heroRole is MAIN and fear is MEDIUM then Value is BAD with 0.2 RuleBlock: strategicalValue enabled: true conjunction: AlgebraicProduct @@ -205,7 +209,8 @@ RuleBlock: strategicalValue rule: if heroRole is SCOUT and strategicalValue is LOW and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL rule: if armyLoss is HIGH and strategicalValue is LOW then Value is BAD rule: if armyLoss is HIGH and strategicalValue is MEDIUM then Value is BAD with 0.7 - rule: if strategicalValue is CRITICAL then Value is CRITICAL + rule: if strategicalValue is CRITICAL and heroRole is MAIN then Value is CRITICAL + rule: if strategicalValue is CRITICAL and heroRole is SCOUT then Value is CRITICAL with 0.7 RuleBlock: armyReward enabled: true conjunction: AlgebraicProduct @@ -220,6 +225,14 @@ RuleBlock: armyReward rule: if heroRole is MAIN and armyReward is MEDIUM and mainTurnDistance is LONG and fear is not HIGH then Value is MEDIUM rule: if heroRole is MAIN and armyReward is LOW and mainTurnDistance is LOW and fear is not HIGH then Value is MEDIUM rule: if heroRole is MAIN and armyReward is LOW and mainTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL + rule: if heroRole is SCOUT and armyReward is HIGH and danger is NONE and scoutTurnDistance is LOW and fear is not HIGH then Value is HIGH + rule: if heroRole is SCOUT and armyReward is HIGH and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is HIGH with 0.7 + rule: if heroRole is SCOUT and armyReward is HIGH and danger is NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is BITHIGH + rule: if heroRole is SCOUT and armyReward is MEDIUM and danger is NONE and scoutTurnDistance is LOW then Value is HIGH with 0.7 + rule: if heroRole is SCOUT and armyReward is MEDIUM and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH + rule: if heroRole is SCOUT and armyReward is MEDIUM and danger is NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is MEDIUM + rule: if heroRole is SCOUT and armyReward is LOW and danger is NONE and scoutTurnDistance is LOW and fear is not HIGH then Value is MEDIUM + rule: if heroRole is SCOUT and armyReward is LOW and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL RuleBlock: gold enabled: true conjunction: AlgebraicProduct From fb7477047a3a78a584b9c17c853cc8d5fee62e20 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sat, 29 Jul 2023 18:54:20 +0300 Subject: [PATCH 16/21] NKAI: loosen gold presure on build system. --- AI/Nullkiller/Analyzers/BuildAnalyzer.cpp | 2 +- AI/Nullkiller/Analyzers/HeroManager.cpp | 2 +- AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp | 2 +- AI/Nullkiller/Engine/Nullkiller.cpp | 5 +++-- AI/Nullkiller/Engine/Nullkiller.h | 2 +- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 10 ++++++---- AI/Nullkiller/Pathfinding/AINodeStorage.cpp | 6 +++++- 7 files changed, 18 insertions(+), 11 deletions(-) diff --git a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp index e946ecea6..690437447 100644 --- a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp @@ -166,7 +166,7 @@ void BuildAnalyzer::update() } else { - goldPreasure = ai->getLockedResources()[EGameResID::GOLD] / 10000.0f + goldPreasure = ai->getLockedResources()[EGameResID::GOLD] / 5000.0f + (float)armyCost[EGameResID::GOLD] / (1 + ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f); } diff --git a/AI/Nullkiller/Analyzers/HeroManager.cpp b/AI/Nullkiller/Analyzers/HeroManager.cpp index 44b1df23a..ac5fff683 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -236,7 +236,7 @@ const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) co for(auto existingHero : myHeroes) { - if(ai->isHeroLocked(existingHero) + if(ai->isHeroLocked(existingHero) && ai->getHeroLockedReason(existingHero) == HeroLockedReason::DEFENCE || existingHero->getArmyStrength() >armyLimit || getHeroRole(existingHero) == HeroRole::MAIN || existingHero->movementPointsRemaining() diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp index 192628111..c626a36d2 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp @@ -360,7 +360,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) auto armyValue = (float)upgrade.upgradeValue / path.getHeroStrength(); - if((armyValue < 0.1f && upgrade.upgradeValue < 20000) || upgrade.upgradeValue < 300) // avoid small upgrades + if((armyValue < 0.25f && upgrade.upgradeValue < 40000) || upgrade.upgradeValue < 2000) // 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 8aadd804b..66b28ca8e 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -134,6 +134,9 @@ void Nullkiller::updateAiState(int pass, bool fast) activeHero = nullptr; setTargetObject(-1); + decomposer->reset(); + buildAnalyzer->update(); + if(!fast) { memory->removeInvisibleObjects(cb.get()); @@ -179,8 +182,6 @@ void Nullkiller::updateAiState(int pass, bool fast) } armyManager->update(); - buildAnalyzer->update(); - decomposer->reset(); logAi->debug("AI state updated in %ld", timeElapsed(start)); } diff --git a/AI/Nullkiller/Engine/Nullkiller.h b/AI/Nullkiller/Engine/Nullkiller.h index 36f3504fd..04578ebec 100644 --- a/AI/Nullkiller/Engine/Nullkiller.h +++ b/AI/Nullkiller/Engine/Nullkiller.h @@ -23,7 +23,7 @@ namespace NKAI { -const float MAX_GOLD_PEASURE = 0.3f; +const float MAX_GOLD_PEASURE = 0.6f; const float MIN_PRIORITY = 0.01f; const float SMALL_SCAN_MIN_PRIORITY = 0.4f; diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index ac6c9618c..bbf931355 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -420,10 +420,10 @@ float RewardEvaluator::getTotalResourceRequirementStrength(int resType) const return 0; float ratio = dailyIncome[resType] == 0 - ? (float)requiredResources[resType] / 50.0f - : (float)requiredResources[resType] / dailyIncome[resType] / 50.0f; + ? (float)requiredResources[resType] / 10.0f + : (float)requiredResources[resType] / dailyIncome[resType] / 20.0f; - return std::min(ratio, 1.0f); + return std::min(ratio, 2.0f); } uint64_t RewardEvaluator::townArmyGrowth(const CGTownInstance * town) const @@ -954,7 +954,9 @@ public: if(bi.notEnoughRes && bi.prerequisitesCount == 1) { - evaluationContext.strategicalValue /= 2; + evaluationContext.strategicalValue /= 3; + evaluationContext.movementCostByRole[evaluationContext.heroRole] += 5; + evaluationContext.turn += 5; } } }; diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index 2fbd50a72..4ff2e4ef9 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -879,8 +879,12 @@ void AINodeStorage::setHeroes(std::map heroes) for(auto & hero : heroes) { // do not allow our own heroes in garrison to act on map - if(hero.first->getOwner() == ai->playerID && hero.first->inTownGarrison && ai->isHeroLocked(hero.first)) + if(hero.first->getOwner() == ai->playerID + && hero.first->inTownGarrison + && (ai->isHeroLocked(hero.first) || ai->heroManager->heroCapReached())) + { continue; + } uint64_t mask = FirstActorMask << actors.size(); auto actor = std::make_shared(hero.first, hero.second, mask, ai); From f1a9ae99ee5920a943ba8c71db069280c18f5e84 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 30 Jul 2023 11:33:52 +0300 Subject: [PATCH 17/21] NKAI: various behavior fixes, undo max_gold_preasure --- AI/BattleAI/BattleAI.cpp | 2 +- AI/Nullkiller/AIGateway.cpp | 29 ++++------------ AI/Nullkiller/AIGateway.h | 1 - AI/Nullkiller/Analyzers/BuildAnalyzer.cpp | 2 +- AI/Nullkiller/Analyzers/ObjectClusterizer.cpp | 7 +++- AI/Nullkiller/Engine/FuzzyHelper.cpp | 10 +++--- AI/Nullkiller/Engine/Nullkiller.h | 2 +- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 34 +++++++++++++------ AI/Nullkiller/Pathfinding/AINodeStorage.h | 4 +-- config/ai/object-priorities.txt | 6 +++- 10 files changed, 49 insertions(+), 48 deletions(-) diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index d3c4ba13c..b3986de94 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -863,7 +863,7 @@ std::optional CBattleAI::considerFleeingOrSurrendering() bs.turnsSkippedByDefense = movesSkippedByDefense / bs.ourStacks.size(); - if(!bs.canFlee || !bs.canSurrender) + if(!bs.canFlee && !bs.canSurrender) { return std::nullopt; } diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 6301b9223..a8911d3b6 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -779,25 +779,21 @@ void AIGateway::makeTurn() boost::shared_lock gsLock(CGameState::mutex); setThreadName("AIGateway::makeTurn"); + cb->sendMessage("vcmieagles"); + + retrieveVisitableObjs(); + if(cb->getDate(Date::DAY_OF_WEEK) == 1) { - std::vector objs; - retrieveVisitableObjs(objs, true); - - for(const CGObjectInstance * obj : objs) + for(const CGObjectInstance * obj : nullkiller->memory->visitableObjs) { if(isWeeklyRevisitable(obj)) { - addVisitableObj(obj); nullkiller->memory->markObjectUnvisited(obj); } } } - cb->sendMessage("vcmieagles"); - - retrieveVisitableObjs(); - #if NKAI_TRACE_LEVEL == 0 try { @@ -1106,26 +1102,13 @@ void AIGateway::waitTillFree() status.waitTillFree(); } -void AIGateway::retrieveVisitableObjs(std::vector & out, bool includeOwned) const -{ - foreach_tile_pos([&](const int3 & pos) - { - for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false)) - { - if(includeOwned || obj->tempOwner != playerID) - out.push_back(obj); - } - }); -} - void AIGateway::retrieveVisitableObjs() { foreach_tile_pos([&](const int3 & pos) { for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false)) { - if(obj->tempOwner != playerID) - addVisitableObj(obj); + addVisitableObj(obj); } }); } diff --git a/AI/Nullkiller/AIGateway.h b/AI/Nullkiller/AIGateway.h index cab6ce797..3a9c23b31 100644 --- a/AI/Nullkiller/AIGateway.h +++ b/AI/Nullkiller/AIGateway.h @@ -195,7 +195,6 @@ public: void validateObject(const CGObjectInstance * obj); //checks if object is still visible and if not, removes references to it void validateObject(ObjectIdRef obj); //checks if object is still visible and if not, removes references to it - void retrieveVisitableObjs(std::vector & out, bool includeOwned = false) const; void retrieveVisitableObjs(); virtual std::vector getFlaggedObjects() const; diff --git a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp index 690437447..359ccc1ca 100644 --- a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp @@ -167,7 +167,7 @@ void BuildAnalyzer::update() else { goldPreasure = ai->getLockedResources()[EGameResID::GOLD] / 5000.0f - + (float)armyCost[EGameResID::GOLD] / (1 + ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f); + + (float)armyCost[EGameResID::GOLD] / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f); } logAi->trace("Gold preasure: %f", goldPreasure); diff --git a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp index 3aee0dcbf..5e2b41977 100644 --- a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp +++ b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp @@ -227,7 +227,12 @@ void ObjectClusterizer::clusterize() auto obj = objs[i]; if(!shouldVisitObject(obj)) - return; + { +#if NKAI_TRACE_LEVEL >= 2 + logAi->trace("Skip object %s%s.", obj->getObjectName(), obj->visitablePos().toString()); +#endif + continue; + } #if NKAI_TRACE_LEVEL >= 2 logAi->trace("Check object %s%s.", obj->getObjectName(), obj->visitablePos().toString()); diff --git a/AI/Nullkiller/Engine/FuzzyHelper.cpp b/AI/Nullkiller/Engine/FuzzyHelper.cpp index f9ac898dc..2757cb35a 100644 --- a/AI/Nullkiller/Engine/FuzzyHelper.cpp +++ b/AI/Nullkiller/Engine/FuzzyHelper.cpp @@ -150,17 +150,15 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj) case Obj::MINE: case Obj::ABANDONED_MINE: case Obj::PANDORAS_BOX: - { - const CArmedInstance * a = dynamic_cast(obj); - return a->getArmyStrength(); - } case Obj::CRYPT: //crypt case Obj::CREATURE_BANK: //crebank case Obj::DRAGON_UTOPIA: case Obj::SHIPWRECK: //shipwreck case Obj::DERELICT_SHIP: //derelict ship - // case Obj::PYRAMID: - return estimateBankDanger(dynamic_cast(obj)); + { + const CArmedInstance * a = dynamic_cast(obj); + return a->getArmyStrength(); + } case Obj::PYRAMID: { if(obj->subID == 0) diff --git a/AI/Nullkiller/Engine/Nullkiller.h b/AI/Nullkiller/Engine/Nullkiller.h index 04578ebec..36f3504fd 100644 --- a/AI/Nullkiller/Engine/Nullkiller.h +++ b/AI/Nullkiller/Engine/Nullkiller.h @@ -23,7 +23,7 @@ namespace NKAI { -const float MAX_GOLD_PEASURE = 0.6f; +const float MAX_GOLD_PEASURE = 0.3f; const float MIN_PRIORITY = 0.01f; const float SMALL_SCAN_MIN_PRIORITY = 0.4f; diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index bbf931355..5d2e1685a 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -162,11 +162,11 @@ uint64_t getCreatureBankArmyReward(const CGObjectInstance * target, const CGHero { result += (c.data.type->getAIValue() * c.data.count) * c.chance; } - else + /*else { //we will need to discard the weakest stack result += (c.data.type->getAIValue() * c.data.count - weakestStackPower) * c.chance; - } + }*/ } result /= 100; //divide by total chance @@ -277,6 +277,8 @@ uint64_t RewardEvaluator::getArmyReward( { const float enemyArmyEliminationRewardRatio = 0.5f; + auto relations = ai->cb->getPlayerRelations(target->tempOwner, ai->playerID); + if(!target) return 0; @@ -301,7 +303,7 @@ uint64_t RewardEvaluator::getArmyReward( case Obj::DRAGON_UTOPIA: return 10000; case Obj::HERO: - return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES + return relations == PlayerRelations::ENEMIES ? enemyArmyEliminationRewardRatio * dynamic_cast(target)->getArmyStrength() : 0; case Obj::PANDORAS_BOX: @@ -319,6 +321,11 @@ uint64_t RewardEvaluator::getArmyGrowth( if(!target) return 0; + auto relations = ai->cb->getPlayerRelations(target->tempOwner, hero->tempOwner); + + if(relations != PlayerRelations::ENEMIES) + return 0; + switch(target->ID) { case Obj::TOWN: @@ -542,15 +549,18 @@ float RewardEvaluator::getSkillReward(const CGObjectInstance * target, const CGH case Obj::GARDEN_OF_REVELATION: case Obj::MARLETTO_TOWER: case Obj::MERCENARY_CAMP: - case Obj::SHRINE_OF_MAGIC_GESTURE: - case Obj::SHRINE_OF_MAGIC_INCANTATION: case Obj::TREE_OF_KNOWLEDGE: return 1; case Obj::LEARNING_STONE: return 1.0f / std::sqrt(hero->level); case Obj::ARENA: - case Obj::SHRINE_OF_MAGIC_THOUGHT: return 2; + case Obj::SHRINE_OF_MAGIC_INCANTATION: + return 0.2f; + case Obj::SHRINE_OF_MAGIC_GESTURE: + return 0.3f; + case Obj::SHRINE_OF_MAGIC_THOUGHT: + return 0.5f; case Obj::LIBRARY_OF_ENLIGHTENMENT: return 8; case Obj::WITCH_HUT: @@ -597,6 +607,8 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG if(!target) return 0; + auto relations = ai->cb->getPlayerRelations(target->tempOwner, hero->tempOwner); + const int dailyIncomeMultiplier = 5; const float enemyArmyEliminationGoldRewardRatio = 0.2f; const int32_t heroEliminationBonus = GameConstants::HERO_GOLD_COST / 2; @@ -637,7 +649,7 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG //Objectively saves us 2500 to hire hero return GameConstants::HERO_GOLD_COST; case Obj::HERO: - return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES + return relations == PlayerRelations::ENEMIES ? heroEliminationBonus + enemyArmyEliminationGoldRewardRatio * getArmyCost(dynamic_cast(target)) : 0; default: @@ -788,7 +800,7 @@ public: if(heroRole == HeroRole::MAIN) evaluationContext.heroRole = heroRole; - if (target && ai->cb->getPlayerRelations(target->tempOwner, hero->tempOwner) == PlayerRelations::ENEMIES) + if (target) { evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero); evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold); @@ -1022,7 +1034,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task) + (evaluationContext.skillReward > 0 ? 1 : 0) + (evaluationContext.strategicalValue > 0 ? 1 : 0); - auto goldRewardPerTurn = evaluationContext.goldReward / std::log2f(evaluationContext.movementCost * 10); + float goldRewardPerTurn = evaluationContext.goldReward / std::log2f(2 + evaluationContext.movementCost * 10); double result = 0; @@ -1055,13 +1067,13 @@ 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: %d, 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: %d, danger: %d, role: %s, strategical value: %f, cwr: %f, fear: %f, result %f", task->toString(), evaluationContext.armyLossPersentage, (int)evaluationContext.turn, evaluationContext.movementCostByRole[HeroRole::MAIN], evaluationContext.movementCostByRole[HeroRole::SCOUT], - evaluationContext.goldReward, + goldRewardPerTurn, evaluationContext.goldCost, evaluationContext.armyReward, evaluationContext.danger, diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.h b/AI/Nullkiller/Pathfinding/AINodeStorage.h index 02364ad11..c127f294b 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.h +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.h @@ -11,7 +11,7 @@ #pragma once #define NKAI_PATHFINDER_TRACE_LEVEL 0 -#define NKAI_TRACE_LEVEL 2 +#define NKAI_TRACE_LEVEL 0 #include "../../../lib/pathfinder/CGPathNode.h" #include "../../../lib/pathfinder/INodeStorage.h" @@ -258,7 +258,7 @@ public: { double ratio = (double)danger / (armyValue * hero->getFightingStrength()); - return (uint64_t)(armyValue * ratio * ratio * ratio); + return (uint64_t)(armyValue * ratio * ratio); } STRONG_INLINE diff --git a/config/ai/object-priorities.txt b/config/ai/object-priorities.txt index d5439ee18..1989bc3c7 100644 --- a/config/ai/object-priorities.txt +++ b/config/ai/object-priorities.txt @@ -170,7 +170,6 @@ RuleBlock: basic rule: if heroRole is SCOUT and turn is NOW and mainTurnDistance is MEDIUM then Value is BAD rule: if heroRole is SCOUT and turn is NEXT and mainTurnDistance is LONG then Value is BAD rule: if heroRole is SCOUT and turn is NOW and scoutTurnDistance is LONG then Value is BAD - rule: if heroRole is SCOUT and turn is NOW and scoutTurnDistance is MEDIUM then Value is BAD with 0.3 rule: if heroRole is SCOUT and fear is HIGH then Value is BAD with 0.8 rule: if heroRole is SCOUT and fear is MEDIUM then Value is BAD with 0.5 rule: if heroRole is MAIN and fear is HIGH then Value is BAD with 0.5 @@ -252,6 +251,11 @@ RuleBlock: gold rule: if goldReward is MEDIUM and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is BITHIGH rule: if goldReward is SMALL and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is MEDIUM rule: if goldReward is SMALL and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is SMALL + rule: if goldReward is LOWEST then Value is SMALL with 0.1 + rule: if goldReward is SMALL then Value is SMALL with 0.2 + rule: if goldReward is MEDIUM then Value is SMALL with 0.5 + rule: if goldReward is BIG then Value is SMALL + rule: if goldReward is HUGE then Value is BITHIGH RuleBlock: skill reward enabled: true conjunction: AlgebraicProduct From ec0596f3ddcfb8a92bf37b6b5daf254601a84d71 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 30 Jul 2023 12:21:47 +0300 Subject: [PATCH 18/21] NKAI: fix error message can not take away last stack --- AI/Nullkiller/AIGateway.cpp | 13 +++++++++++++ AI/Nullkiller/Analyzers/HeroManager.cpp | 2 +- AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp | 5 +++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index a8911d3b6..8be0aa310 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -867,6 +867,19 @@ void AIGateway::pickBestCreatures(const CArmedInstance * destinationArmy, const auto bestArmy = nullkiller->armyManager->getBestArmy(destinationArmy, destinationArmy, source); + for(auto army : armies) + { + // move first stack at first slot if empty to avoid can not take away last creature + if(!army->hasStackAtSlot(SlotID(0)) && army->stacksCount() > 0) + { + cb->mergeOrSwapStacks( + army, + army, + SlotID(0), + army->Slots().begin()->first); + } + } + //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.validSlot(); i.advance(1)) //i-th strongest creature type will go to i-th slot { diff --git a/AI/Nullkiller/Analyzers/HeroManager.cpp b/AI/Nullkiller/Analyzers/HeroManager.cpp index ac5fff683..b896ab728 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -236,7 +236,7 @@ const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) co for(auto existingHero : myHeroes) { - if(ai->isHeroLocked(existingHero) && ai->getHeroLockedReason(existingHero) == HeroLockedReason::DEFENCE + if(ai->getHeroLockedReason(existingHero) == HeroLockedReason::DEFENCE || existingHero->getArmyStrength() >armyLimit || getHeroRole(existingHero) == HeroRole::MAIN || existingHero->movementPointsRemaining() diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp index c626a36d2..f5eb28c79 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp @@ -119,10 +119,11 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her HeroExchange heroExchange(hero, path); - float armyValue = (float)heroExchange.getReinforcementArmyStrength() / hero->getArmyStrength(); + uint64_t armyValue = heroExchange.getReinforcementArmyStrength(); + float armyRatio = (float)armyValue / hero->getArmyStrength(); // avoid transferring very small amount of army - if(armyValue < 0.1f && armyValue < 20000) + if((armyRatio < 0.1f && armyValue < 20000) || armyValue < 500) { #if NKAI_TRACE_LEVEL >= 2 logAi->trace("Army value is too small."); From ccfc6f57169387e85cd5bfd5fbe3557e2f1fce13 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 30 Jul 2023 18:02:56 +0300 Subject: [PATCH 19/21] NKAI: increase towns priority, buy heroes more often --- AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp | 6 ++++-- AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h | 4 ---- AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp | 6 +++--- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 14 ++++++++------ AI/Nullkiller/Markers/DefendTown.cpp | 4 ++-- AI/Nullkiller/Markers/DefendTown.h | 6 +++--- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp index 7bd330137..5b2638341 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp @@ -148,17 +148,19 @@ void DangerHitMapAnalyzer::calculateTileOwners() std::map heroTownMap; PathfinderSettings pathfinderSettings; - pathfinderSettings.mainTurnDistanceLimit = 3; + pathfinderSettings.mainTurnDistanceLimit = 5; auto addTownHero = [&](const CGTownInstance * town) { auto townHero = new CGHeroInstance(); CRandomGenerator rng; + auto visitablePos = town->visitablePos(); - townHero->pos = town->pos; + townHero->pos = visitablePos; townHero->setOwner(ai->playerID); // lets avoid having multiple colors townHero->initHero(rng, static_cast(0)); townHero->initObj(rng); + townHero->pos = townHero->convertFromVisitablePos(visitablePos); heroTownMap[townHero] = town; townHeroes[townHero] = HeroRole::MAIN; diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h index 4fed77412..79c56c2c4 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h @@ -55,10 +55,6 @@ struct HitMapNode } }; -struct TileOwner -{ -}; - class DangerHitMapAnalyzer { private: diff --git a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp index fdf75ba3d..581c725e6 100644 --- a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp +++ b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp @@ -70,10 +70,10 @@ Goals::TGoalVec RecruitHeroBehavior::decompose() const for(auto obj : ai->nullkiller->objectClusterizer->getNearbyObjects()) { - if((obj->ID == Obj::RESOURCE && obj->subID == GameResID(EGameResID::GOLD)) + if((obj->ID == Obj::RESOURCE) || obj->ID == Obj::TREASURE_CHEST || obj->ID == Obj::CAMPFIRE - || obj->ID == Obj::WATER_WHEEL + || isWeeklyRevisitable(obj) || obj->ID ==Obj::ARTIFACT) { auto tile = obj->visitablePos(); @@ -84,7 +84,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose() const } } - if(treasureSourcesCount < 10) + if(treasureSourcesCount < 5) continue; if(cb->getHeroesInfo().size() < cb->getTownsInfo().size() + 1 diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 5d2e1685a..6e3e5f754 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -108,7 +108,8 @@ int32_t estimateTownIncome(CCallback * cb, const CGObjectInstance * target, cons auto town = cb->getTown(target->id); auto fortLevel = town->fortLevel(); - if(town->hasCapitol()) return booster * 2000; + if(town->hasCapitol()) + return booster * 2000; // probably well developed town will have city hall if(fortLevel == CGTownInstance::CASTLE) return booster * 750; @@ -497,14 +498,15 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons } auto fortLevel = town->fortLevel(); - auto booster = isAnotherAi(town, *ai->cb) ? 0.3f : 0.7f; + auto booster = isAnotherAi(town, *ai->cb) ? 0.4f : 1.0f; - if(town->hasCapitol()) return booster; + if(town->hasCapitol()) + return booster * 1.5; if(fortLevel < CGTownInstance::CITADEL) - return booster * (town->hasFort() ? 0.6 : 0.4); + return booster * (town->hasFort() ? 1.0 : 0.8); else - return booster * (fortLevel == CGTownInstance::CASTLE ? 0.9 : 0.8); + return booster * (fortLevel == CGTownInstance::CASTLE ? 1.4 : 1.2); } case Obj::HERO: @@ -731,7 +733,7 @@ public: multiplier /= 1.0f + treat.turn / 5.0f; - if(defendTown.getTurn() > 0 && defendTown.isContrAttack()) + if(defendTown.getTurn() > 0 && defendTown.isCounterAttack()) { auto ourSpeed = defendTown.hero->movementPointsLimit(true); auto enemySpeed = treat.hero->movementPointsLimit(true); diff --git a/AI/Nullkiller/Markers/DefendTown.cpp b/AI/Nullkiller/Markers/DefendTown.cpp index 096def550..dd7d08e25 100644 --- a/AI/Nullkiller/Markers/DefendTown.cpp +++ b/AI/Nullkiller/Markers/DefendTown.cpp @@ -18,8 +18,8 @@ namespace NKAI using namespace Goals; -DefendTown::DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath, bool isContrattack) - : CGoal(Goals::DEFEND_TOWN), treat(treat), defenceArmyStrength(defencePath.getHeroStrength()), turn(defencePath.turn()), contrattack(isContrattack) +DefendTown::DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath, bool isCounterAttack) + : CGoal(Goals::DEFEND_TOWN), treat(treat), defenceArmyStrength(defencePath.getHeroStrength()), turn(defencePath.turn()), counterattack(isCounterAttack) { settown(town); sethero(defencePath.targetHero); diff --git a/AI/Nullkiller/Markers/DefendTown.h b/AI/Nullkiller/Markers/DefendTown.h index bfe2314d9..34b8b3427 100644 --- a/AI/Nullkiller/Markers/DefendTown.h +++ b/AI/Nullkiller/Markers/DefendTown.h @@ -24,10 +24,10 @@ namespace Goals uint64_t defenceArmyStrength; HitMapInfo treat; uint8_t turn; - bool contrattack; + bool counterattack; public: - DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath, bool isContrattack = false); + DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath, bool isCounterAttack = false); DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const CGHeroInstance * defender); virtual bool operator==(const DefendTown & other) const override; @@ -39,7 +39,7 @@ namespace Goals uint8_t getTurn() const { return turn; } - bool isContrAttack() { return contrattack; } + bool isCounterAttack() { return counterattack; } }; } From 4c0aae6fbdcf6a55bc54cf1e9aa1a9512e1b5217 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Mon, 31 Jul 2023 22:00:22 +0300 Subject: [PATCH 20/21] NKAI: fix crash for specific map --- AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp index 5b2638341..efc4bde8e 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp @@ -156,11 +156,10 @@ void DangerHitMapAnalyzer::calculateTileOwners() CRandomGenerator rng; auto visitablePos = town->visitablePos(); - townHero->pos = visitablePos; townHero->setOwner(ai->playerID); // lets avoid having multiple colors townHero->initHero(rng, static_cast(0)); - townHero->initObj(rng); townHero->pos = townHero->convertFromVisitablePos(visitablePos); + townHero->initObj(rng); heroTownMap[townHero] = town; townHeroes[townHero] = HeroRole::MAIN; From 3b238ff15e9d92adec6ced7cf60e9ada1f1ef651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Tue, 1 Aug 2023 09:54:35 +0200 Subject: [PATCH 21/21] Fix weekly visitable check --- AI/Nullkiller/AIUtility.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/AI/Nullkiller/AIUtility.cpp b/AI/Nullkiller/AIUtility.cpp index 296c47780..e6962673e 100644 --- a/AI/Nullkiller/AIUtility.cpp +++ b/AI/Nullkiller/AIUtility.cpp @@ -323,13 +323,9 @@ bool isWeeklyRevisitable(const CGObjectInstance * obj) if(dynamic_cast(obj)) return true; - if(dynamic_cast(obj)) //banks tend to respawn often in mods - return true; switch(obj->ID) { - case Obj::STABLES: - case Obj::MAGIC_WELL: case Obj::HILL_FORT: return true; case Obj::BORDER_GATE: