diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index f63e8f6e8..5485d5b88 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -359,6 +359,11 @@ void AIGateway::objectRemoved(const CGObjectInstance * obj) { lostHero(cb->getHero(obj->id)); //we can promote, since objectRemoved is called just before actual deletion } + + if(obj->ID == Obj::HERO && cb->getPlayerRelations(obj->tempOwner, playerID) == PlayerRelations::ENEMIES) + { + nullkiller->dangerHitMap->reset(); + } } void AIGateway::showHillFortWindow(const CGObjectInstance * object, const CGHeroInstance * visitor) @@ -580,27 +585,38 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector always answer yes, we are a brave AI :) - auto answer = 1; + bool answer = true; auto objects = cb->getVisitableObjs(target); if(hero.validAndSet() && target.valid() && objects.size()) { - auto objType = objects.front()->ID; + auto topObj = objects.front()->id == hero->id ? objects.back() : objects.front(); + auto objType = topObj->ID; // top object should be our hero + auto goalObjectID = nullkiller->getTargetObject(); + auto ratio = (float)nullkiller->dangerEvaluator->evaluateDanger(target, hero.get()) / (float)hero->getTotalStrength(); - if(objType == Obj::ARTIFACT || objType == Obj::RESOURCE) + answer = topObj->id == goalObjectID; // no if we do not aim to visit this object + logAi->trace("Query hook: %s(%s) by %s danger ratio %f", target.toString(), topObj->getObjectName(), hero.name, ratio); + + if(cb->getObj(goalObjectID, false)) + { + logAi->trace("AI expected %s", cb->getObj(goalObjectID, false)->getObjectName()); + } + + if(objType == Obj::BORDERGUARD || objType == Obj::QUEST_GUARD) + { + answer = true; + } + else if(objType == Obj::ARTIFACT || objType == Obj::RESOURCE) { - auto ratio = (float)nullkiller->dangerEvaluator->evaluateDanger(target, hero.get()) / (float)hero->getTotalStrength(); bool dangerUnknown = ratio == 0; bool dangerTooHigh = ratio > (1 / SAFE_ATTACK_CONSTANT); - logAi->trace("Guarded object query hook: %s by %s danger ratio %f", target.toString(), hero.name, ratio); - - if(text.find("guarded") != std::string::npos && (dangerUnknown || dangerTooHigh)) - answer = 0; // no + answer = !dangerUnknown && !dangerTooHigh; } } - answerQuery(askID, answer); + answerQuery(askID, answer ? 1 : 0); }); return; @@ -1332,7 +1348,10 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h) if(auto visitedObject = vstd::frontOrNull(cb->getVisitableObjs(h->visitablePos()))) //we stand on something interesting { if(visitedObject != *h) + { performObjectInteraction(visitedObject, h); + ret = true; + } } } if(h) //we could have lost hero after last move diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp index c3cdcbb71..ce065b4ca 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp @@ -14,6 +14,8 @@ namespace NKAI { +HitMapInfo HitMapInfo::NoTreat; + void DangerHitMapAnalyzer::updateHitMap() { if(upToDate) @@ -47,6 +49,9 @@ void DangerHitMapAnalyzer::updateHitMap() for(auto pair : heroes) { + if(!pair.first.isValidPlayer()) + continue; + if(ai->cb->getPlayerRelations(ai->playerID, pair.first) != PlayerRelations::ENEMIES) continue; diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h index b74b52a4e..9696d4213 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h @@ -16,10 +16,17 @@ namespace NKAI struct HitMapInfo { + static HitMapInfo NoTreat; + uint64_t danger; uint8_t turn; HeroPtr hero; + HitMapInfo() + { + reset(); + } + void reset() { danger = 0; @@ -33,6 +40,8 @@ struct HitMapNode HitMapInfo maximumDanger; HitMapInfo fastestDanger; + HitMapNode() = default; + void reset() { maximumDanger.reset(); diff --git a/AI/Nullkiller/Analyzers/HeroManager.cpp b/AI/Nullkiller/Analyzers/HeroManager.cpp index 92467c2cb..edea22385 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -92,37 +92,6 @@ float HeroManager::evaluateFightingStrength(const CGHeroInstance * hero) const return evaluateSpeciality(hero) + wariorSkillsScores.evaluateSecSkills(hero) + hero->level * 1.5f; } -std::vector> clusterizeHeroes(CCallback * cb, std::vector heroes) -{ - std::vector> clusters; - - for(auto hero : heroes) - { - auto paths = cb->getPathsInfo(hero); - std::vector newCluster = {hero}; - - for(auto cluster = clusters.begin(); cluster != clusters.end();) - { - auto hero = std::find_if(cluster->begin(), cluster->end(), [&](const CGHeroInstance * h) -> bool - { - return paths->getNode(h->visitablePos())->turns < SCOUT_TURN_DISTANCE_LIMIT; - }); - - if(hero != cluster->end()) - { - vstd::concatenate(newCluster, *cluster); - clusters.erase(cluster); - } - else - cluster++; - } - - clusters.push_back(newCluster); - } - - return clusters; -} - void HeroManager::update() { logAi->trace("Start analysing our heroes"); @@ -140,7 +109,7 @@ void HeroManager::update() return scores.at(h1) > scores.at(h2); }; - int globalMainCount = std::min(((int)myHeroes.size() + 2) / 3, cb->getMapSize().x / 100 + 1); + int globalMainCount = std::min(((int)myHeroes.size() + 2) / 3, cb->getMapSize().x / 50 + 1); std::sort(myHeroes.begin(), myHeroes.end(), scoreSort); @@ -149,27 +118,6 @@ void HeroManager::update() heroRoles[hero] = (globalMainCount--) > 0 ? HeroRole::MAIN : HeroRole::SCOUT; } - for(auto cluster : clusterizeHeroes(cb, myHeroes)) - { - std::sort(cluster.begin(), cluster.end(), scoreSort); - - auto localMainCountMax = (cluster.size() + 2) / 3; - - for(auto hero : cluster) - { - if(heroRoles[hero] != HeroRole::MAIN) - { - heroRoles[hero] = HeroRole::MAIN; - break; - } - - localMainCountMax--; - - if(localMainCountMax == 0) - break; - } - } - for(auto hero : myHeroes) { logAi->trace("Hero %s has role %s", hero->getNameTranslated(), heroRoles[hero] == HeroRole::MAIN ? "main" : "scout"); diff --git a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp index 8c0829a36..88db0921a 100644 --- a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp +++ b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp @@ -271,7 +271,7 @@ void ObjectClusterizer::clusterize() if(!shouldVisit(ai, path.targetHero, obj)) { #if NKAI_TRACE_LEVEL >= 2 - logAi->trace("Hero %s does not need to visit %s", path.targetHero->name, obj->getObjectName()); + logAi->trace("Hero %s does not need to visit %s", path.targetHero->getObjectName(), obj->getObjectName()); #endif continue; } @@ -285,7 +285,7 @@ void ObjectClusterizer::clusterize() if(vstd::contains(heroesProcessed, path.targetHero)) { #if NKAI_TRACE_LEVEL >= 2 - logAi->trace("Hero %s is already processed.", path.targetHero->name); + logAi->trace("Hero %s is already processed.", path.targetHero->getObjectName()); #endif continue; } diff --git a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp index d6b7eee41..8717900b6 100644 --- a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp +++ b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp @@ -70,7 +70,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector if(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()); + logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.getHeroStrength()); #endif continue; } @@ -81,7 +81,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector auto hero = path.targetHero; auto danger = path.getTotalDanger(); - if(ai->nullkiller->heroManager->getHeroRole(hero) == HeroRole::SCOUT && danger == 0 && path.exchangeCount > 1) + if(ai->nullkiller->heroManager->getHeroRole(hero) == HeroRole::SCOUT && path.exchangeCount > 1) continue; auto firstBlockedAction = path.getFirstBlockedAction(); @@ -113,7 +113,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector "It is %s to visit %s by %s with army %lld, danger %lld and army loss %lld", isSafe ? "safe" : "not safe", objToVisit ? objToVisit->getObjectName() : path.targetTile().toString(), - hero->name, + hero->getObjectName(), path.getHeroStrength(), danger, path.getTotalArmyLoss()); @@ -126,8 +126,13 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector sharedPtr.reset(newWay); - if(!closestWay || closestWay->movementCost() > path.movementCost()) + auto heroRole = ai->nullkiller->heroManager->getHeroRole(path.targetHero); + + if(heroRole == HeroRole::SCOUT + && (!closestWay || closestWay->movementCost() > path.movementCost())) + { closestWay = &path; + } if(!ai->nullkiller->arePathHeroesLocked(path)) { @@ -137,11 +142,13 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector } } - assert(closestWay || waysToVisitObj.empty()); - for(auto way : waysToVisitObj) + if(closestWay) { - way->closestWayRatio - = closestWay->movementCost() / way->getPath().movementCost(); + for(auto way : waysToVisitObj) + { + way->closestWayRatio + = closestWay->movementCost() / way->getPath().movementCost(); + } } return tasks; diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp index df13141b3..8964e173e 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp @@ -18,6 +18,7 @@ #include "../Goals/RecruitHero.h" #include "../Goals/DismissHero.h" #include "../Goals/Composition.h" +#include "../Goals/CaptureObject.h" #include "../Markers/DefendTown.h" #include "../Goals/ExchangeSwapTownHeroes.h" #include "lib/mapping/CMap.h" //for victory conditions @@ -29,6 +30,8 @@ namespace NKAI extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; +const double TREAT_IGNORE_RATIO = 0.5; + using namespace Goals; std::string DefenceBehavior::toString() const @@ -53,7 +56,7 @@ 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.fastestDanger, treatNode.maximumDanger }; + auto treats = { treatNode.maximumDanger, treatNode.fastestDanger }; if(!treatNode.fastestDanger.hero) { @@ -68,12 +71,17 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta { if(!ai->nullkiller->isHeroLocked(town->garrisonHero.get())) { - if(!town->visitingHero && cb->getHeroesInfo().size() < GameConstants::MAX_HEROES_PER_PLAYER) + if(!town->visitingHero && cb->getHeroCount(ai->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER) { - tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5))); - } + 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; + } } logAi->trace( @@ -113,22 +121,37 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta if(town->visitingHero && path.getHeroStrength() < town->visitingHero->getHeroStrength()) continue; - if(path.getHeroStrength() > treat.danger) + if(treat.hero.validAndSet() + && treat.turn <= 1 + && (treat.danger == treatNode.maximumDanger.danger || treat.turn < treatNode.maximumDanger.turn) + && isSafeToVisit(path.targetHero, path.heroArmy, treat.danger)) { - if((path.turn() <= treat.turn && dayOfWeek + treat.turn < 6 && isSafeToVisit(path.targetHero, path.heroArmy, treat.danger)) - || (path.exchangeCount == 1 && path.turn() < treat.turn) + Composition composition; + + composition.addNext(DefendTown(town, treat, path)).addNext(CaptureObject(treat.hero.get())); + + tasks.push_back(Goals::sptr(composition)); + } + + bool treatIsWeak = path.getHeroStrength() / 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->name, - town->name, + path.targetHero->getObjectName(), + town->getObjectName(), path.toString()); #endif treatIsUnderControl = true; + break; } } @@ -152,7 +175,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta if(cb->getHeroesInfo().size() < ALLOWED_ROAMING_HEROES) { #if NKAI_TRACE_LEVEL >= 1 - logAi->trace("Hero %s can be recruited to defend %s", hero->name, town->name); + 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; @@ -202,7 +225,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta #if NKAI_TRACE_LEVEL >= 1 logAi->trace( "Hero %s can defend town with force %lld in %s turns, cost: %f, path: %s", - path.targetHero->name, + path.targetHero->getObjectName(), path.getHeroStrength(), std::to_string(path.turn()), path.movementCost(), @@ -212,8 +235,8 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta { #if NKAI_TRACE_LEVEL >= 1 logAi->trace("Defer defence of %s by %s because he has enough time to reach the town next trun", - town->name, - path.targetHero->name); + town->getObjectName(), + path.targetHero->getObjectName()); #endif defferedPaths[path.targetHero].push_back(i); @@ -225,8 +248,8 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta { #if NKAI_TRACE_LEVEL >= 1 logAi->trace("Put %s to garrison of town %s", - path.targetHero->name, - town->name); + path.targetHero->getObjectName(), + town->getObjectName()); #endif // dismiss creatures we are not able to pick to be able to hide in garrison @@ -249,8 +272,8 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta { #if NKAI_TRACE_LEVEL >= 1 logAi->trace("Can not move %s to defend town %s. Path is locked.", - path.targetHero->name, - town->name); + path.targetHero->getObjectName(), + town->getObjectName()); #endif continue; @@ -277,8 +300,8 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta #if NKAI_TRACE_LEVEL >= 1 logAi->trace("Move %s to defend town %s", - path.targetHero->name, - town->name); + path.targetHero->getObjectName(), + town->getObjectName()); #endif Composition composition; diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp index 9673cfd82..2ed9e21f1 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp @@ -45,8 +45,7 @@ Goals::TGoalVec GatherArmyBehavior::decompose() const for(const CGHeroInstance * hero : heroes) { - if(ai->nullkiller->heroManager->getHeroRole(hero) == HeroRole::MAIN - && hero->getArmyStrength() >= 300) + if(ai->nullkiller->heroManager->getHeroRole(hero) == HeroRole::MAIN) { vstd::concatenate(tasks, deliverArmyToHero(hero)); } @@ -70,13 +69,6 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her #if NKAI_TRACE_LEVEL >= 1 logAi->trace("Checking ways to gaher army for hero %s, %s", hero->getObjectName(), pos.toString()); #endif - if(ai->nullkiller->isHeroLocked(hero)) - { -#if NKAI_TRACE_LEVEL >= 1 - logAi->trace("Skipping locked hero %s, %s", hero->getObjectName(), pos.toString()); -#endif - return tasks; - } auto paths = ai->nullkiller->pathfinder->getPathInfo(pos); @@ -92,6 +84,14 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her if(path.containsHero(hero)) continue; + if(path.turn() == 0 && hero->inTownGarrison) + { +#if NKAI_TRACE_LEVEL >= 1 + logAi->trace("Skipping garnisoned hero %s, %s", hero->getObjectName(), pos.toString()); +#endif + continue; + } + if(ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path)) { #if NKAI_TRACE_LEVEL >= 2 @@ -124,22 +124,40 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her // avoid trying to move bigger army to the weaker one. if(armyValue > 1) { + bool hasOtherMainInPath = false; + + for(auto node : path.nodes) + { + if(!node.targetHero) continue; + + auto heroRole = ai->nullkiller->heroManager->getHeroRole(node.targetHero); + + if(heroRole == HeroRole::MAIN) + { + hasOtherMainInPath = true; + + break; + } + } + + if(hasOtherMainInPath) + { #if NKAI_TRACE_LEVEL >= 2 - logAi->trace("Army value is too large."); + logAi->trace("Army value is too large."); #endif - continue; + continue; + } } auto danger = path.getTotalDanger(); - auto isSafe = isSafeToVisit(hero, path.heroArmy, danger); #if NKAI_TRACE_LEVEL >= 2 logAi->trace( "It is %s to visit %s by %s with army %lld, danger %lld and army loss %lld", isSafe ? "safe" : "not safe", - hero->name, - path.targetHero->name, + hero->getObjectName(), + path.targetHero->getObjectName(), path.getHeroStrength(), danger, path.getTotalArmyLoss()); @@ -194,7 +212,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) #if NKAI_TRACE_LEVEL >= 2 logAi->trace("Path found %s", path.toString()); #endif - if(upgrader->visitingHero != path.targetHero) + if(upgrader->visitingHero && upgrader->visitingHero.get() != path.targetHero) { #if NKAI_TRACE_LEVEL >= 2 logAi->trace("Ignore path. Town has visiting hero."); @@ -219,7 +237,10 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) continue; } - if(ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path)) + auto heroRole = ai->nullkiller->heroManager->getHeroRole(path.targetHero); + + if(heroRole == HeroRole::SCOUT + && ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path)) { #if NKAI_TRACE_LEVEL >= 2 logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength()); @@ -228,10 +249,22 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) } auto upgrade = ai->nullkiller->armyManager->calculateCreaturesUpgrade(path.heroArmy, upgrader, availableResources); + + if(ai->nullkiller->heroManager->getHeroRole(path.targetHero) == HeroRole::MAIN) + { + upgrade.upgradeValue += + ai->nullkiller->armyManager->howManyReinforcementsCanGet(path.targetHero, path.heroArmy, upgrader); + } + auto armyValue = (float)upgrade.upgradeValue / path.getHeroStrength(); - if(armyValue < 0.1f || upgrade.upgradeValue < 300) // avoid small upgrades + if(armyValue < 0.25f || upgrade.upgradeValue < 300) // avoid small upgrades + { +#if NKAI_TRACE_LEVEL >= 2 + logAi->trace("Ignore path. Army value is too small (%f)", armyValue); +#endif continue; + } auto danger = path.getTotalDanger(); @@ -242,7 +275,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) "It is %s to visit %s by %s with army %lld, danger %lld and army loss %lld", isSafe ? "safe" : "not safe", upgrader->getObjectName(), - path.targetHero->name, + path.targetHero->getObjectName(), path.getHeroStrength(), danger, path.getTotalArmyLoss()); diff --git a/AI/Nullkiller/Engine/FuzzyHelper.cpp b/AI/Nullkiller/Engine/FuzzyHelper.cpp index 5e6297e27..68fdcd5db 100644 --- a/AI/Nullkiller/Engine/FuzzyHelper.cpp +++ b/AI/Nullkiller/Engine/FuzzyHelper.cpp @@ -115,8 +115,20 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj) { case Obj::TOWN: { - const CGTownInstance * cre = dynamic_cast(obj); - return cre->getUpperArmy()->getArmyStrength(); + const CGTownInstance * town = dynamic_cast(obj); + auto danger = town->getUpperArmy()->getArmyStrength(); + + if(danger || town->visitingHero) + { + auto fortLevel = town->fortLevel(); + + if(fortLevel == CGTownInstance::EFortLevel::CASTLE) + danger += 10000; + else if(fortLevel == CGTownInstance::EFortLevel::CITADEL) + danger += 4000; + } + + return danger; } case Obj::ARTIFACT: case Obj::RESOURCE: diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 10ebf43fd..7d2059cc7 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -131,6 +131,7 @@ void Nullkiller::updateAiState(int pass, bool fast) auto start = std::chrono::high_resolution_clock::now(); activeHero = nullptr; + setTargetObject(-1); if(!fast) { @@ -188,7 +189,7 @@ bool Nullkiller::arePathHeroesLocked(const AIPath & path) const if(getHeroLockedReason(path.targetHero) == HeroLockedReason::STARTUP) { #if NKAI_TRACE_LEVEL >= 1 - logAi->trace("Hero %s is locked by STARTUP. Discarding %s", path.targetHero->name, path.toString()); + logAi->trace("Hero %s is locked by STARTUP. Discarding %s", path.targetHero->getObjectName(), path.toString()); #endif return true; } @@ -200,7 +201,7 @@ bool Nullkiller::arePathHeroesLocked(const AIPath & path) const if(lockReason != HeroLockedReason::NOT_LOCKED) { #if NKAI_TRACE_LEVEL >= 1 - logAi->trace("Hero %s is locked by STARTUP. Discarding %s", path.targetHero->name, path.toString()); + logAi->trace("Hero %s is locked by STARTUP. Discarding %s", path.targetHero->getObjectName(), path.toString()); #endif return true; } @@ -221,6 +222,7 @@ void Nullkiller::makeTurn() boost::lock_guard sharedStorageLock(AISharedStorage::locker); const int MAX_DEPTH = 10; + const float FAST_TASK_MINIMAL_PRIORITY = 0.7; resetAiState(); @@ -240,12 +242,12 @@ void Nullkiller::makeTurn() bestTask = choseBestTask(fastTasks); - if(bestTask->priority >= 1) + if(bestTask->priority >= FAST_TASK_MINIMAL_PRIORITY) { executeTask(bestTask); updateAiState(i, true); } - } while(bestTask->priority >= 1); + } while(bestTask->priority >= FAST_TASK_MINIMAL_PRIORITY); Goals::TTaskVec bestTasks = { bestTask, @@ -272,21 +274,16 @@ void Nullkiller::makeTurn() if(heroRole != HeroRole::MAIN || bestTask->getHeroExchangeCount() <= 1) useHeroChain = false; - if(bestTask->priority < NEXT_SCAN_MIN_PRIORITY - && scanDepth != ScanDepth::FULL) + if((heroRole != HeroRole::MAIN || bestTask->priority < SMALL_SCAN_MIN_PRIORITY) + && scanDepth == ScanDepth::FULL) { - if(heroRole == HeroRole::MAIN || bestTask->priority < MIN_PRIORITY) - { - useHeroChain = false; + useHeroChain = false; + scanDepth = ScanDepth::SMALL; - logAi->trace( - "Goal %s has too low priority %f so increasing scan depth", - bestTask->toString(), - bestTask->priority); - scanDepth = (ScanDepth)((int)scanDepth + 1); - - continue; - } + logAi->trace( + "Goal %s has too low priority %f so increasing scan depth", + bestTask->toString(), + bestTask->priority); } if(bestTask->priority < MIN_PRIORITY) @@ -317,10 +314,12 @@ void Nullkiller::executeTask(Goals::TTask task) } catch(cannotFulfillGoalException & e) { - logAi->debug("Failed to realize subgoal of type %s, I will stop.", taskDescr); - logAi->debug("The error message was: %s", e.what()); + logAi->error("Failed to realize subgoal of type %s, I will stop.", taskDescr); + logAi->error("The error message was: %s", e.what()); - throw; +#if NKAI_TRACE_LEVEL == 0 + throw; // will be recatched and AI turn ended +#endif } } diff --git a/AI/Nullkiller/Engine/Nullkiller.h b/AI/Nullkiller/Engine/Nullkiller.h index e462040ff..fb3bb5cc1 100644 --- a/AI/Nullkiller/Engine/Nullkiller.h +++ b/AI/Nullkiller/Engine/Nullkiller.h @@ -24,7 +24,7 @@ namespace NKAI const float MAX_GOLD_PEASURE = 0.3f; const float MIN_PRIORITY = 0.01f; -const float NEXT_SCAN_MIN_PRIORITY = 0.4f; +const float SMALL_SCAN_MIN_PRIORITY = 0.4f; enum class HeroLockedReason { @@ -39,11 +39,9 @@ enum class HeroLockedReason enum class ScanDepth { - SMALL = 0, + FULL = 0, - MEDIUM = 1, - - FULL = 2 + SMALL = 1 }; class Nullkiller @@ -51,6 +49,7 @@ class Nullkiller private: const CGHeroInstance * activeHero; int3 targetTile; + ObjectInstanceID targetObject; std::map lockedHeroes; ScanDepth scanDepth; TResources lockedResources; @@ -79,6 +78,8 @@ public: HeroPtr getActiveHero() { return activeHero; } HeroLockedReason getHeroLockedReason(const CGHeroInstance * hero) const; int3 getTargetTile() const { return targetTile; } + ObjectInstanceID getTargetObject() const { return targetObject; } + void setTargetObject(int objid) { targetObject = ObjectInstanceID(objid); } void setActive(const CGHeroInstance * hero, int3 tile) { activeHero = hero; targetTile = tile; } void lockHero(const CGHeroInstance * hero, HeroLockedReason lockReason) { lockedHeroes[hero] = lockReason; } void unlockHero(const CGHeroInstance * hero) { lockedHeroes.erase(hero); } diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 18c976e7a..27674f2f4 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -17,10 +17,12 @@ #include "../../../lib/CPathfinder.h" #include "../../../lib/CGameStateFwd.h" #include "../../../lib/VCMI_Lib.h" +#include "../../../lib/StartInfo.h" #include "../../../CCallback.h" #include "../../../lib/filesystem/Filesystem.h" #include "../Goals/ExecuteHeroChain.h" #include "../Goals/BuildThis.h" +#include "../Goals/ExchangeSwapTownHeroes.h" #include "../Markers/UnlockCluster.h" #include "../Markers/HeroExchange.h" #include "../Markers/ArmyUpgrade.h" @@ -79,6 +81,12 @@ void PriorityEvaluator::initVisitTile() value = engine->getOutputVariable("Value"); } +bool isAnotherAi(const CGObjectInstance * obj, const CPlayerSpecificInfoCallback & cb) +{ + return obj->getOwner().isValidPlayer() + && cb.getStartInfo()->getIthPlayersSettings(obj->getOwner()).isControlledByAI(); +} + int32_t estimateTownIncome(CCallback * cb, const CGObjectInstance * target, const CGHeroInstance * hero) { auto relations = cb->getPlayerRelations(hero->tempOwner, target->tempOwner); @@ -86,11 +94,17 @@ int32_t estimateTownIncome(CCallback * cb, const CGObjectInstance * target, cons if(relations != PlayerRelations::ENEMIES) return 0; // if we already own it, no additional reward will be received by just visiting it - auto town = cb->getTown(target->id); - auto isNeutral = target->tempOwner == PlayerColor::NEUTRAL; - auto isProbablyDeveloped = !isNeutral && town->hasFort(); + auto booster = isAnotherAi(target, *cb) ? 1 : 2; - return isProbablyDeveloped ? 1500 : 500; + auto town = cb->getTown(target->id); + auto fortLevel = town->fortLevel(); + + if(town->hasCapitol()) return booster * 2000; + + // probably well developed town will have city hall + if(fortLevel == CGTownInstance::CASTLE) return booster * 750; + + return booster * (town->hasFort() && town->tempOwner != PlayerColor::NEUTRAL ? booster * 500 : 250); } TResources getCreatureBankResources(const CGObjectInstance * target, const CGHeroInstance * hero) @@ -238,7 +252,17 @@ uint64_t RewardEvaluator::getArmyReward( switch(target->ID) { case Obj::TOWN: - return target->tempOwner == PlayerColor::NEUTRAL ? 1000 : 10000; + { + 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: @@ -374,12 +398,21 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons } case Obj::TOWN: + { if(ai->buildAnalyzer->getDevelopmentInfo().empty()) return 1; - return dynamic_cast(target)->hasFort() - ? (target->tempOwner == PlayerColor::NEUTRAL ? 0.8f : 1.0f) - : 0.7f; + auto town = dynamic_cast(target); + auto fortLevel = town->fortLevel(); + auto booster = isAnotherAi(town, *ai->cb) ? 0.3 : 1; + + if(town->hasCapitol()) return 1; + + if(fortLevel < CGTownInstance::CITADEL) + return booster * (town->hasFort() ? 0.6 : 0.4); + else + return booster * (fortLevel == CGTownInstance::CASTLE ? 0.9 : 0.8); + } case Obj::HERO: return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES @@ -448,22 +481,17 @@ float RewardEvaluator::getSkillReward(const CGObjectInstance * target, const CGH } } -uint64_t RewardEvaluator::getEnemyHeroDanger(const int3 & tile, uint8_t turn) const +const HitMapInfo & RewardEvaluator::getEnemyHeroDanger(const int3 & tile, uint8_t turn) const { auto & treatNode = ai->dangerHitMap->getTileTreat(tile); if(treatNode.maximumDanger.danger == 0) - return 0; + return HitMapInfo::NoTreat; if(treatNode.maximumDanger.turn <= turn) - return treatNode.maximumDanger.danger; + return treatNode.maximumDanger; - return treatNode.fastestDanger.turn <= turn ? treatNode.fastestDanger.danger : 0; -} - -uint64_t RewardEvaluator::getEnemyHeroDanger(const AIPath & path) const -{ - return getEnemyHeroDanger(path.targetTile(), path.turn()); + return treatNode.fastestDanger.turn <= turn ? treatNode.fastestDanger : HitMapInfo::NoTreat; } int32_t getArmyCost(const CArmedInstance * army) @@ -561,9 +589,29 @@ public: uint64_t upgradeValue = armyUpgrade.getUpgradeValue(); evaluationContext.armyReward += upgradeValue; + evaluationContext.strategicalValue += upgradeValue / armyUpgrade.hero->getTotalStrength(); } }; +void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uint8_t turn, uint64_t ourStrength) +{ + HitMapInfo enemyDanger = evaluationContext.evaluator.getEnemyHeroDanger(tile, turn); + + if(enemyDanger.danger) + { + auto dangerRatio = enemyDanger.danger / (double)ourStrength; + auto enemyHero = evaluationContext.evaluator.ai->cb->getObj(enemyDanger.hero.hid, false); + bool isAI = enemyHero && isAnotherAi(enemyHero, *evaluationContext.evaluator.ai->cb); + + if(isAI) + { + dangerRatio *= 1.5; // lets make AI bit more afraid of other AI. + } + + vstd::amax(evaluationContext.enemyHeroDangerRatio, dangerRatio); + } +} + class DefendTownEvaluator : public IEvaluationContextBuilder { private: @@ -596,7 +644,7 @@ public: auto armyIncome = townArmyIncome(town); auto dailyIncome = town->dailyIncome()[Res::GOLD]; - auto strategicalValue = std::sqrt(armyIncome / 20000.0f) + dailyIncome / 10000.0f; + auto strategicalValue = std::sqrt(armyIncome / 20000.0f) + dailyIncome / 3000.0f; float multiplier = 1; @@ -607,9 +655,7 @@ public: evaluationContext.goldReward += dailyIncome * 5 * multiplier; evaluationContext.strategicalValue += strategicalValue * multiplier; vstd::amax(evaluationContext.danger, defendTown.getTreat().danger); - - auto enemyDanger = evaluationContext.evaluator.getEnemyHeroDanger(town->visitablePos(), defendTown.getTurn()); - vstd::amax(evaluationContext.enemyHeroDangerRatio, enemyDanger / (double)defendTown.getDefenceStrength()); + addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength()); } }; @@ -665,7 +711,7 @@ public: vstd::amax(evaluationContext.armyLossPersentage, path.getTotalArmyLoss() / (double)path.getHeroStrength()); evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroPtr); - vstd::amax(evaluationContext.enemyHeroDangerRatio, evaluationContext.evaluator.getEnemyHeroDanger(path) / (double)path.getHeroStrength()); + addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength()); vstd::amax(evaluationContext.turn, path.turn()); } }; @@ -719,6 +765,27 @@ public: } }; +class ExchangeSwapTownHeroesContextBuilder : public IEvaluationContextBuilder +{ +public: + virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override + { + if(task->goalType != Goals::EXCHANGE_SWAP_TOWN_HEROES) + return; + + Goals::ExchangeSwapTownHeroes & swapCommand = dynamic_cast(*task); + const CGHeroInstance * garrisonHero = swapCommand.getGarrisonHero(); + + if(garrisonHero && swapCommand.getLockingReason() == HeroLockedReason::DEFENCE) + { + auto defenderRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(garrisonHero); + + evaluationContext.movementCost += garrisonHero->movement; + evaluationContext.movementCostByRole[defenderRole] += garrisonHero->movement; + } + } +}; + class BuildThisEvaluationContextBuilder : public IEvaluationContextBuilder { public: @@ -783,6 +850,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()); } EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal) const diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index e026f2259..840169970 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -23,6 +23,7 @@ namespace NKAI class BuildingInfo; class Nullkiller; +struct HitMapInfo; class RewardEvaluator { @@ -41,8 +42,7 @@ public: float getSkillReward(const CGObjectInstance * target, const CGHeroInstance * hero, HeroRole role) const; int32_t getGoldReward(const CGObjectInstance * target, const CGHeroInstance * hero) const; uint64_t getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const; - uint64_t getEnemyHeroDanger(const AIPath & path) const; - uint64_t getEnemyHeroDanger(const int3 & tile, uint8_t turn) const; + const HitMapInfo & getEnemyHeroDanger(const int3 & tile, uint8_t turn) const; }; struct DLL_EXPORT EvaluationContext diff --git a/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp b/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp index cdb789cc3..d18975550 100644 --- a/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp +++ b/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp @@ -83,7 +83,10 @@ void ExchangeSwapTownHeroes::accept(AIGateway * ai) cb->swapGarrisonHero(town); - ai->nullkiller->lockHero(garrisonHero, lockingReason); + if(lockingReason != HeroLockedReason::NOT_LOCKED) + { + ai->nullkiller->lockHero(garrisonHero, lockingReason); + } if(town->visitingHero && town->visitingHero != garrisonHero) { diff --git a/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.h b/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.h index aad61d77e..ca25f280b 100644 --- a/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.h +++ b/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.h @@ -32,6 +32,9 @@ namespace Goals void accept(AIGateway * ai) override; std::string toString() const override; virtual bool operator==(const ExchangeSwapTownHeroes & other) const override; + + const CGHeroInstance * getGarrisonHero() const { return garrisonHero; } + HeroLockedReason getLockingReason() const { return lockingReason; } }; } diff --git a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp index 67f8a984f..4268a5296 100644 --- a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp +++ b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp @@ -52,6 +52,7 @@ void ExecuteHeroChain::accept(AIGateway * ai) logAi->debug("Executing hero chain towards %s. Path %s", targetName, chainPath.toString()); ai->nullkiller->setActive(chainPath.targetHero, tile); + ai->nullkiller->setTargetObject(objid); std::set blockedIndexes;