From 400967904b7c87c059fef9dea82eaab55567e8c3 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 16 May 2021 14:15:03 +0300 Subject: [PATCH] Nullkiller AI: stabilization of build and prioritization fixes --- AI/Nullkiller/AIhelper.cpp | 21 +- AI/Nullkiller/AIhelper.h | 5 +- AI/Nullkiller/Analyzers/BuildAnalyzer.cpp | 330 ++++++++++++++++++ AI/Nullkiller/Analyzers/BuildAnalyzer.h | 71 ++++ .../Analyzers/DangerHitMapAnalyzer.cpp | 3 + AI/Nullkiller/ArmyManager.cpp | 40 +++ AI/Nullkiller/ArmyManager.h | 7 + AI/Nullkiller/Behaviors/BuildingBehavior.cpp | 26 ++ .../Behaviors/CaptureObjectsBehavior.cpp | 17 +- .../Behaviors/RecruitHeroBehavior.cpp | 2 +- AI/Nullkiller/Engine/Nullkiller.cpp | 16 +- AI/Nullkiller/Engine/Nullkiller.h | 1 + AI/Nullkiller/Engine/PriorityEvaluator.cpp | 194 +++++++--- AI/Nullkiller/Engine/PriorityEvaluator.h | 13 +- AI/Nullkiller/Goals/AbstractGoal.h | 14 +- AI/Nullkiller/Goals/BuildThis.cpp | 5 + AI/Nullkiller/Goals/BuildThis.h | 11 + AI/Nullkiller/Goals/ExecuteHeroChain.cpp | 2 +- AI/Nullkiller/HeroManager.cpp | 4 +- AI/Nullkiller/HeroManager.h | 4 +- AI/Nullkiller/Pathfinding/AINodeStorage.cpp | 111 ++++-- AI/Nullkiller/Pathfinding/AINodeStorage.h | 18 +- AI/Nullkiller/Pathfinding/AIPathfinder.cpp | 24 +- AI/Nullkiller/VCAI.cpp | 19 +- 24 files changed, 825 insertions(+), 133 deletions(-) diff --git a/AI/Nullkiller/AIhelper.cpp b/AI/Nullkiller/AIhelper.cpp index 697fe653c..7d03a84e6 100644 --- a/AI/Nullkiller/AIhelper.cpp +++ b/AI/Nullkiller/AIhelper.cpp @@ -47,6 +47,12 @@ void AIhelper::setAI(VCAI * AI) heroManager->setAI(AI); } +void AIhelper::update() +{ + armyManager->update(); + heroManager->update(); +} + bool AIhelper::getBuildingOptions(const CGTownInstance * t) { return buildingManager->getBuildingOptions(t); @@ -162,6 +168,16 @@ void AIhelper::updatePaths(std::vector heroes, bool useHeroChain) pathfindingManager->updatePaths(heroes, useHeroChain); } +uint64_t AIhelper::evaluateStackPower(const CCreature * creature, int count) const +{ + return armyManager->evaluateStackPower(creature, count); +} + +SlotInfo AIhelper::getTotalCreaturesAvailable(CreatureID creatureID) const +{ + return armyManager->getTotalCreaturesAvailable(creatureID); +} + bool AIhelper::canGetArmy(const CArmedInstance * army, const CArmedInstance * source) const { return armyManager->canGetArmy(army, source); @@ -212,11 +228,6 @@ HeroRole AIhelper::getHeroRole(const HeroPtr & hero) const return heroManager->getHeroRole(hero); } -void AIhelper::updateHeroRoles() -{ - heroManager->updateHeroRoles(); -} - float AIhelper::evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const { return heroManager->evaluateSecSkill(skill, hero); diff --git a/AI/Nullkiller/AIhelper.h b/AI/Nullkiller/AIhelper.h index 7f8f51253..921a3b367 100644 --- a/AI/Nullkiller/AIhelper.h +++ b/AI/Nullkiller/AIhelper.h @@ -80,14 +80,17 @@ public: 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) const override; + uint64_t evaluateStackPower(const CCreature * creature, int count) const override; + SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const override; const std::map & getHeroRoles() const override; HeroRole getHeroRole(const HeroPtr & hero) const override; int selectBestSkill(const HeroPtr & hero, const std::vector & skills) const override; - void updateHeroRoles() override; float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const override; float evaluateHero(const CGHeroInstance * hero) const override; + void update() override; + private: bool notifyGoalCompleted(Goals::TSubgoal goal) override; diff --git a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp index 7edb7b374..adc0c8372 100644 --- a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp @@ -14,6 +14,336 @@ extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; +void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo) +{ + auto townInfo = developmentInfo.town->town; + auto creatures = townInfo->creatures; + auto buildings = townInfo->getAllBuildings(); + + std::map parentMap; + + for(auto &pair : townInfo->buildings) + { + if(pair.second->upgrade != -1) + { + parentMap[pair.second->upgrade] = pair.first; + } + } + + BuildingID prefixes[] = {BuildingID::DWELL_UP_FIRST, BuildingID::DWELL_FIRST}; + + for(int level = 0; level < GameConstants::CREATURES_PER_TOWN; level++) + { + logAi->trace("Checking dwelling level %d", level); + BuildingInfo nextToBuild = BuildingInfo(); + + for(BuildingID prefix : prefixes) + { + BuildingID building = BuildingID(prefix + level); + + if(!vstd::contains(buildings, building)) + continue; // no such building in town + + auto info = getBuildingOrPrerequisite(developmentInfo.town, building); + + if(info.exists) + { + developmentInfo.addExistingDwelling(info); + + break; + } + + nextToBuild = info; + } + + if(nextToBuild.id != BuildingID::NONE) + { + developmentInfo.addBuildingToBuild(nextToBuild); + } + } +} + +void BuildAnalyzer::updateOtherBuildings(TownDevelopmentInfo & developmentInfo) +{ + logAi->trace("Checking other buildings"); + + std::vector> otherBuildings = { + {BuildingID::TOWN_HALL, BuildingID::CITY_HALL, BuildingID::CAPITOL} + }; + + if(developmentInfo.existingDwellings.size() >= 2 && cb->getDate(Date::DAY_OF_WEEK) > boost::date_time::Friday) + { + otherBuildings.push_back({BuildingID::CITADEL, BuildingID::CASTLE}); + } + + for(auto & buildingSet : otherBuildings) + { + for(auto & buildingID : buildingSet) + { + if(!developmentInfo.town->hasBuilt(buildingID)) + { + developmentInfo.addBuildingToBuild(getBuildingOrPrerequisite(developmentInfo.town, buildingID)); + + break; + } + } + } +} + +int32_t convertToGold(const TResources & res) +{ + return res[Res::GOLD] + + 75 * (res[Res::WOOD] + res[Res::ORE]) + + 125 * (res[Res::GEMS] + res[Res::CRYSTAL] + res[Res::MERCURY] + res[Res::SULFUR]); +} + +TResources BuildAnalyzer::getResourcesRequiredNow() const +{ + auto resourcesAvailable = cb->getResourceAmount(); + auto result = requiredResources - resourcesAvailable; + + result.positive(); + + return result; +} + +TResources BuildAnalyzer::getTotalResourcesRequired() const +{ + auto resourcesAvailable = cb->getResourceAmount(); + auto result = totalDevelopmentCost - resourcesAvailable; + + result.positive(); + + return result; +} + +void BuildAnalyzer::update() +{ + logAi->trace("Start analysing build"); + + BuildingInfo bi; + + reset(); + + auto towns = cb->getTownsInfo(); + + for(const CGTownInstance* town : towns) + { + logAi->trace("Checking town %s", town->name); + + auto townInfo = town->town; + + developmentInfos.push_back(TownDevelopmentInfo(town)); + TownDevelopmentInfo & developmentInfo = developmentInfos.back(); + + updateTownDwellings(developmentInfo); + updateOtherBuildings(developmentInfo); + + requiredResources += developmentInfo.requiredResources; + totalDevelopmentCost += developmentInfo.townDevelopmentCost; + armyCost += developmentInfo.armyCost; + + for(auto bi : developmentInfo.toBuild) + { + logAi->trace("Building preferences %s", bi.toString()); + } + } + + std::sort(developmentInfos.begin(), developmentInfos.end(), [](const TownDevelopmentInfo & t1, const TownDevelopmentInfo & t2) -> bool + { + auto val1 = convertToGold(t1.armyCost) - convertToGold(t1.townDevelopmentCost); + auto val2 = convertToGold(t2.armyCost) - convertToGold(t2.townDevelopmentCost); + + return val1 > val2; + }); +} + void BuildAnalyzer::reset() { + requiredResources = TResources(); + totalDevelopmentCost = TResources(); + armyCost = TResources(); + developmentInfos.clear(); } + +BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite( + const CGTownInstance* town, + BuildingID toBuild, + bool excludeDwellingDependencies) const +{ + BuildingID building = toBuild; + auto townInfo = town->town; + + const CBuilding * buildPtr = townInfo->buildings.at(building); + const CCreature * creature = nullptr; + CreatureID baseCreatureID; + + 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.at(level / GameConstants::CREATURES_PER_TOWN); + + baseCreatureID = creatures.front(); + creature = creatureID.toCreature(); + } + + auto info = BuildingInfo(buildPtr, creature, baseCreatureID); + + logAi->trace("checking %s", buildPtr->Name()); + logAi->trace("buildInfo %s", info.toString()); + + buildPtr = nullptr; + + if(!town->hasBuilt(building)) + { + auto canBuild = cb->canBuildStructure(town, building); + + if(canBuild == EBuildingState::ALLOWED) + { + info.canBuild = true; + } + else if(canBuild == EBuildingState::NO_RESOURCES) + { + logAi->trace("cant build. Not enough resources. Need %s", info.buildCost.toString()); + info.notEnoughRes = true; + } + else if(canBuild == EBuildingState::PREREQUIRES) + { + auto buildExpression = town->genBuildingRequirements(building, false); + auto missingBuildings = buildExpression.getFulfillmentCandidates([&](const BuildingID & id) -> bool + { + return town->hasBuilt(id); + }); + + auto otherDwelling = [](const BuildingID & id) -> bool + { + return BuildingID::DWELL_FIRST <= id && id <= BuildingID::DWELL_UP_LAST; + }; + + if(vstd::contains_if(missingBuildings, otherDwelling)) + { + logAi->trace("cant build. Need other dwelling"); + } + else + { + buildPtr = townInfo->buildings.at(building); + + logAi->trace("cant build. Need %d", missingBuildings[0].num); + + BuildingInfo prerequisite = getBuildingOrPrerequisite(town, missingBuildings[0], excludeDwellingDependencies); + + prerequisite.buildCostWithPrerequisits += info.buildCost; + prerequisite.creatureCost = info.creatureCost; + prerequisite.creatureGrows = info.creatureGrows; + prerequisite.creatureLevel = info.creatureLevel; + prerequisite.creatureID = info.creatureID; + prerequisite.baseCreatureID = info.baseCreatureID; + prerequisite.prerequisitesCount++; + + return prerequisite; + } + } + } + else + { + logAi->trace("exists"); + info.exists = true; + } + + return info; +} + +TResources BuildAnalyzer::getDailyIncome() const +{ + auto objects = cb->getMyObjects(); + auto towns = cb->getTownsInfo(); + TResources dailyIncome = TResources(); + + for(const CGObjectInstance* obj : objects) + { + const CGMine* mine = dynamic_cast(obj); + + if(mine) + { + dailyIncome[mine->producedResource] += mine->producedQuantity; + } + } + + for(const CGTownInstance* town : towns) + { + dailyIncome += town->dailyIncome(); + } + + return dailyIncome; +} + +void TownDevelopmentInfo::addExistingDwelling(const BuildingInfo & existingDwelling) +{ + existingDwellings.push_back(existingDwelling); + + armyCost += existingDwelling.creatureCost * existingDwelling.creatureGrows; +} + +void TownDevelopmentInfo::addBuildingToBuild(const BuildingInfo & nextToBuild) +{ + townDevelopmentCost += nextToBuild.buildCostWithPrerequisits; + + if(nextToBuild.canBuild) + { + toBuild.push_back(nextToBuild); + hasSomethingToBuild = true; + } + else if(nextToBuild.notEnoughRes) + { + requiredResources += nextToBuild.buildCost; + hasSomethingToBuild = true; + } +} + +BuildingInfo::BuildingInfo() +{ + id = BuildingID::NONE; + creatureGrows = 0; + creatureID = CreatureID::NONE; + buildCost = 0; + buildCostWithPrerequisits = 0; + prerequisitesCount = 0; + name = ""; +} + +BuildingInfo::BuildingInfo(const CBuilding * building, const CCreature * creature, CreatureID baseCreature) +{ + id = building->bid; + buildCost = building->resources; + buildCostWithPrerequisits = building->resources; + dailyIncome = building->produce; + exists = false;; + prerequisitesCount = 1; + name = building->Name(); + + if(creature) + { + creatureGrows = creature->growth; + creatureID = creature->idNumber; + creatureCost = creature->cost; + creatureLevel = creature->level; + baseCreatureID = baseCreature; + } + else + { + creatureGrows = 0; + creatureID = CreatureID::NONE; + baseCreatureID = CreatureID::NONE; + creatureCost = TResources(); + creatureLevel = 0; + } +} + +std::string BuildingInfo::toString() const +{ + return name + ", cost: " + buildCost.toString() + + ", creature: " + std::to_string(creatureGrows) + " x " + std::to_string(creatureLevel) + + " x " + creatureCost.toString() + + ", daily: " + dailyIncome.toString(); +} \ No newline at end of file diff --git a/AI/Nullkiller/Analyzers/BuildAnalyzer.h b/AI/Nullkiller/Analyzers/BuildAnalyzer.h index 748d53fda..d98f81ff8 100644 --- a/AI/Nullkiller/Analyzers/BuildAnalyzer.h +++ b/AI/Nullkiller/Analyzers/BuildAnalyzer.h @@ -12,11 +12,82 @@ #include "../VCAI.h" #include "../../../lib/ResourceSet.h" + +class BuildingInfo +{ +public: + BuildingID id; + TResources buildCost; + TResources buildCostWithPrerequisits; + int creatureGrows; + uint8_t creatureLevel; + TResources creatureCost; + CreatureID creatureID; + CreatureID baseCreatureID; + TResources dailyIncome; + uint8_t prerequisitesCount; + std::string name; + bool exists = false; + bool canBuild = false; + bool notEnoughRes = false; + + BuildingInfo(); + + BuildingInfo(const CBuilding* building, const CCreature* creature, CreatureID baseCreatureID); + + std::string toString() const; +}; + +class TownDevelopmentInfo +{ +public: + const CGTownInstance* town; + std::vector toBuild; + std::vector existingDwellings; + TResources townDevelopmentCost; + TResources requiredResources; + TResources armyCost; + int armyScore; + int economicsScore; + HeroRole townRole; + bool hasSomethingToBuild; + + TownDevelopmentInfo(const CGTownInstance* town) + :town(town), armyScore(0), economicsScore(0), toBuild(), townDevelopmentCost(), requiredResources(), townRole(HeroRole::SCOUT), hasSomethingToBuild(false) + { + } + + TownDevelopmentInfo() : TownDevelopmentInfo(nullptr) {} + + void addBuildingToBuild(const BuildingInfo & building); + void addExistingDwelling(const BuildingInfo & existingDwelling); +}; + class BuildAnalyzer { private: TResources requiredResources; + TResources totalDevelopmentCost; + std::vector developmentInfos; + TResources armyCost; public: + void update(); + + TResources getResourcesRequiredNow() const; + TResources getTotalResourcesRequired() const; + const std::vector & getDevelopmentInfo() const { return developmentInfos; } + + TResources getDailyIncome() const; + +private: + BuildingInfo getBuildingOrPrerequisite( + const CGTownInstance* town, + BuildingID toBuild, + bool excludeDwellingDependencies = true) const; + + + void updateTownDwellings(TownDevelopmentInfo & developmentInfo); + void updateOtherBuildings(TownDevelopmentInfo & developmentInfo); void reset(); }; diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp index 6259adf1c..6c6318b4a 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp @@ -49,6 +49,9 @@ void DangerHitMapAnalyzer::updateHitMap() { for(AIPath & path : ai->ah->getPathsToTile(pos)) { + if(path.getFirstBlockedAction()) + continue; + auto tileDanger = path.getHeroStrength(); auto turn = path.turn(); auto & node = hitMap[pos.x][pos.y][pos.z]; diff --git a/AI/Nullkiller/ArmyManager.cpp b/AI/Nullkiller/ArmyManager.cpp index 276f3482b..a320b9fc6 100644 --- a/AI/Nullkiller/ArmyManager.cpp +++ b/AI/Nullkiller/ArmyManager.cpp @@ -168,3 +168,43 @@ ui64 ArmyManager::howManyReinforcementsCanGet(const CCreatureSet * target, const return newArmy > oldArmy ? newArmy - oldArmy : 0; } + +uint64_t ArmyManager::evaluateStackPower(const CCreature * creature, int count) const +{ + return creature->AIValue * count; +} + +SlotInfo ArmyManager::getTotalCreaturesAvailable(CreatureID creatureID) const +{ + auto creatureInfo = totalArmy.find(creatureID); + + return creatureInfo == totalArmy.end() ? SlotInfo() : creatureInfo->second; +} + +void ArmyManager::update() +{ + logAi->trace("Start analysing army"); + + std::vector total; + auto heroes = cb->getHeroesInfo(); + auto towns = cb->getTownsInfo(); + + std::copy(heroes.begin(), heroes.end(), std::back_inserter(total)); + std::copy(towns.begin(), towns.end(), std::back_inserter(total)); + + totalArmy.clear(); + + for(auto army : total) + { + for(auto slot : army->Slots()) + { + totalArmy[slot.second->getCreatureID()].count += slot.second->count; + } + } + + for(auto army : totalArmy) + { + army.second.creature = army.first.toCreature(); + army.second.power = evaluateStackPower(army.second.creature, army.second.count); + } +} \ No newline at end of file diff --git a/AI/Nullkiller/ArmyManager.h b/AI/Nullkiller/ArmyManager.h index 5124bb1a2..642f800a6 100644 --- a/AI/Nullkiller/ArmyManager.h +++ b/AI/Nullkiller/ArmyManager.h @@ -30,6 +30,7 @@ class DLL_EXPORT IArmyManager //: public: IAbstractManager public: virtual void init(CPlayerSpecificInfoCallback * CB) = 0; virtual void setAI(VCAI * AI) = 0; + virtual void update() = 0; virtual bool canGetArmy(const CArmedInstance * target, const CArmedInstance * source) const = 0; virtual ui64 howManyReinforcementsCanBuy(const CCreatureSet * target, const CGDwelling * source) const = 0; virtual ui64 howManyReinforcementsCanGet(const CCreatureSet * target, const CCreatureSet * source) const = 0; @@ -37,6 +38,8 @@ public: 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) const = 0; + virtual uint64_t evaluateStackPower(const CCreature * creature, int count) const = 0; + virtual SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const = 0; }; class DLL_EXPORT ArmyManager : public IArmyManager @@ -44,10 +47,12 @@ class DLL_EXPORT ArmyManager : public IArmyManager private: CPlayerSpecificInfoCallback * cb; //this is enough, but we downcast from CCallback VCAI * ai; + std::map totalArmy; public: void init(CPlayerSpecificInfoCallback * CB) override; void setAI(VCAI * AI) override; + void update() override; bool canGetArmy(const CArmedInstance * target, const CArmedInstance * source) const override; ui64 howManyReinforcementsCanBuy(const CCreatureSet * target, const CGDwelling * source) const override; @@ -56,4 +61,6 @@ public: 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) const override; + uint64_t evaluateStackPower(const CCreature * creature, int count) const override; + SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const override; }; diff --git a/AI/Nullkiller/Behaviors/BuildingBehavior.cpp b/AI/Nullkiller/Behaviors/BuildingBehavior.cpp index 5978fe63c..996b6861f 100644 --- a/AI/Nullkiller/Behaviors/BuildingBehavior.cpp +++ b/AI/Nullkiller/Behaviors/BuildingBehavior.cpp @@ -33,6 +33,32 @@ Goals::TGoalVec BuildingBehavior::getTasks() { Goals::TGoalVec tasks; + TResources resourcesRequired = ai->nullkiller->buildAnalyzer->getResourcesRequiredNow(); + TResources totalDevelopmentCost = ai->nullkiller->buildAnalyzer->getTotalResourcesRequired(); + TResources availableResources = cb->getResourceAmount(); + TResources dailyIncome = ai->nullkiller->buildAnalyzer->getDailyIncome(); + + logAi->trace("Resources amount: %s", availableResources.toString()); + + resourcesRequired -= availableResources; + resourcesRequired.positive(); + + logAi->trace("daily income: %s", dailyIncome.toString()); + logAi->trace("resources required to develop towns now: %s, total: %s", + resourcesRequired.toString(), + totalDevelopmentCost.toString()); + + auto & developmentInfos = ai->nullkiller->buildAnalyzer->getDevelopmentInfo(); + + for(auto & developmentInfo : developmentInfos) + { + auto town = developmentInfo.town; + + for(auto & buildingInfo : developmentInfo.toBuild) + { + tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo))); + } + } return tasks; } diff --git a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp index 9d5b01586..5c913fefe 100644 --- a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp +++ b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp @@ -28,17 +28,6 @@ std::string CaptureObjectsBehavior::toString() const return "Capture objects"; } -std::shared_ptr getFirstBlockedAction(const AIPath & path) -{ - for(auto node : path.nodes) - { - if(node.specialAction && !node.specialAction->canAct(node.targetHero)) - return node.specialAction; - } - - return std::shared_ptr(); -} - Goals::TGoalVec CaptureObjectsBehavior::getTasks() { Goals::TGoalVec tasks; @@ -76,7 +65,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getTasks() logAi->trace("Path found %s", path.toString()); #endif - if(getFirstBlockedAction(path)) + if(path.getFirstBlockedAction()) { #ifdef VCMI_TRACE_PATHFINDER // TODO: decomposition? @@ -88,7 +77,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getTasks() if(ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path)) { #ifdef VCMI_TRACE_PATHFINDER - logAi->trace("Ignore path. Target hero can be killed by enemy"); + logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %d", path.heroArmy->getArmyStrength()); #endif continue; } @@ -112,7 +101,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getTasks() hero->name, path.getHeroStrength(), danger, - path.armyLoss); + path.getTotalArmyLoss()); #endif if(isSafe) diff --git a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp index f83d6caea..86dc973b4 100644 --- a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp +++ b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp @@ -40,7 +40,7 @@ Goals::TGoalVec RecruitHeroBehavior::getTasks() if(cb->getHeroesInfo().size() < cb->getTownsInfo().size() + 1 || cb->getResourceAmount(Res::GOLD) > 10000) { - tasks.push_back(Goals::sptr(Goals::RecruitHero().settown(town))); + tasks.push_back(Goals::sptr(Goals::RecruitHero().settown(town).setpriority(3))); } } } diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 528ce9ecd..1e1514285 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -16,6 +16,7 @@ #include "../Behaviors/BuyArmyBehavior.h" #include "../Behaviors/StartupBehavior.h" #include "../Behaviors/DefenceBehavior.h" +#include "../Behaviors/BuildingBehavior.h" #include "../Goals/Invalid.h" extern boost::thread_specific_ptr cb; @@ -25,6 +26,7 @@ Nullkiller::Nullkiller() { priorityEvaluator.reset(new PriorityEvaluator()); dangerHitMap.reset(new DangerHitMapAnalyzer()); + buildAnalyzer.reset(new BuildAnalyzer()); } Goals::TSubgoal Nullkiller::choseBestTask(Goals::TGoalVec & tasks) const @@ -78,14 +80,17 @@ void Nullkiller::updateAiState() // TODO: move to hero manager auto activeHeroes = ai->getMyHeroes(); - vstd::erase_if(activeHeroes, [this](const HeroPtr & hero) -> bool{ + vstd::erase_if(activeHeroes, [this](const HeroPtr & hero) -> bool + { auto lockedReason = getHeroLockedReason(hero.h); return lockedReason == HeroLockedReason::DEFENCE || lockedReason == HeroLockedReason::STARTUP; }); ai->ah->updatePaths(activeHeroes, true); - ai->ah->updateHeroRoles(); + ai->ah->update(); + + buildAnalyzer->update(); } bool Nullkiller::arePathHeroesLocked(const AIPath & path) const @@ -111,7 +116,8 @@ void Nullkiller::makeTurn() choseBestTask(std::make_shared()), choseBestTask(std::make_shared()), choseBestTask(std::make_shared()), - choseBestTask(std::make_shared()) + choseBestTask(std::make_shared()), + choseBestTask(std::make_shared()) }; if(cb->getDate(Date::DAY) == 1) @@ -140,12 +146,12 @@ void Nullkiller::makeTurn() { logAi->trace(bestTask->completeMessage()); } - catch(std::exception & e) + /*catch(std::exception & e) { logAi->debug("Failed to realize subgoal of type %s, I will stop.", bestTask->name()); logAi->debug("The error message was: %s", e.what()); return; - } + }*/ } } diff --git a/AI/Nullkiller/Engine/Nullkiller.h b/AI/Nullkiller/Engine/Nullkiller.h index 6872dc083..89328bb9a 100644 --- a/AI/Nullkiller/Engine/Nullkiller.h +++ b/AI/Nullkiller/Engine/Nullkiller.h @@ -35,6 +35,7 @@ private: public: std::unique_ptr dangerHitMap; + std::unique_ptr buildAnalyzer; Nullkiller(); void makeTurn(); diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index c4d5afed8..7deea5658 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -35,11 +35,6 @@ class CGTownInstance; extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; -PriorityEvaluator::PriorityEvaluator() -{ - initVisitTile(); -} - PriorityEvaluator::~PriorityEvaluator() { delete engine; @@ -112,7 +107,8 @@ uint64_t getDwellingScore(const CGObjectInstance * target, bool checkGold) if(creLevel.first && creLevel.second.size()) { auto creature = creLevel.second.back().toCreature(); - if(checkGold && !cb->getResourceAmount().canAfford(creature->cost * creLevel.first)) + auto creaturesAreFree = creature->level == 1; + if(!creaturesAreFree && checkGold && !cb->getResourceAmount().canAfford(creature->cost * creLevel.first)) continue; score += creature->AIValue * creLevel.first; @@ -179,7 +175,7 @@ uint64_t getArmyReward(const CGObjectInstance * target, const CGHeroInstance * h case Obj::SHIPWRECK: case Obj::SHIPWRECK_SURVIVOR: case Obj::WARRIORS_TOMB: - return 1500; + return 1000; case Obj::ARTIFACT: return evaluateArtifactArmyValue(dynamic_cast(target)->storedArtifact); case Obj::DRAGON_UTOPIA: @@ -208,6 +204,34 @@ float getEnemyHeroStrategicalValue(const CGHeroInstance * enemy) return objectValue / 2.0f + enemy->level / 15.0f; } +float getResourceRequirementStrength(int resType) +{ + TResources requiredResources = ai->nullkiller->buildAnalyzer->getResourcesRequiredNow(); + TResources dailyIncome = ai->nullkiller->buildAnalyzer->getDailyIncome(); + + if(requiredResources[resType] == 0) + return 0; + + if(dailyIncome[resType] == 0) + return 1; + + return (float)requiredResources[resType] / dailyIncome[resType] / 3; +} + +float getTotalResourceRequirementStrength(int resType) +{ + TResources requiredResources = ai->nullkiller->buildAnalyzer->getTotalResourcesRequired(); + TResources dailyIncome = ai->nullkiller->buildAnalyzer->getDailyIncome(); + + if(requiredResources[resType] == 0) + return 0; + + if(dailyIncome[resType] == 0) + return requiredResources[resType] / 30; + + return (float)requiredResources[resType] / dailyIncome[resType] / 30; +} + float getStrategicalValue(const CGObjectInstance * target) { if(!target) @@ -215,6 +239,12 @@ float getStrategicalValue(const CGObjectInstance * target) switch(target->ID) { + case Obj::MINE: + return target->subID == Res::GOLD ? 0.8f : 0.05f + 0.3f * getTotalResourceRequirementStrength(target->subID) + 0.5f * getResourceRequirementStrength(target->subID); + + case Obj::RESOURCE: + return target->subID == Res::GOLD ? 0 : 0.5f * getResourceRequirementStrength(target->subID); + case Obj::TOWN: return target->tempOwner == PlayerColor::NEUTRAL ? 0.5 : 1; @@ -261,7 +291,10 @@ float getSkillReward(const CGObjectInstance * target, const CGHeroInstance * her 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::sqrtf(hero->level); case Obj::ARENA: case Obj::SHRINE_OF_MAGIC_THOUGHT: return 2; @@ -339,6 +372,86 @@ int32_t getGoldReward(const CGObjectInstance * target, const CGHeroInstance * he } } +class ExecuteHeroChainEvaluationContextBuilder : public IEvaluationContextBuilder +{ +public: + virtual Goals::EvaluationContext buildEvaluationContext(Goals::TSubgoal task) const override + { + auto evaluationContext = task->evaluationContext; + + int objId = task->objid; + + if(task->parent) + objId = task->parent->objid; + + auto heroPtr = task->hero; + const CGObjectInstance * target = cb->getObj((ObjectInstanceID)objId, false); + auto day = cb->getDate(Date::DAY); + auto hero = heroPtr.get(); + bool checkGold = evaluationContext.danger == 0; + + evaluationContext.armyLossPersentage = task->evaluationContext.armyLoss / (double)task->evaluationContext.heroStrength; + evaluationContext.heroRole = ai->ah->getHeroRole(heroPtr); + evaluationContext.goldReward = getGoldReward(target, hero); + evaluationContext.armyReward = getArmyReward(target, hero, checkGold); + evaluationContext.skillReward = getSkillReward(target, hero, evaluationContext.heroRole); + evaluationContext.strategicalValue = getStrategicalValue(target); + + return evaluationContext; + } +}; + +class BuildThisEvaluationContextBuilder : public IEvaluationContextBuilder +{ +public: + virtual Goals::EvaluationContext buildEvaluationContext(Goals::TSubgoal task) const override + { + Goals::EvaluationContext evaluationContext; + Goals::BuildThis & buildThis = dynamic_cast(*task); + auto & bi = buildThis.buildingInfo; + + evaluationContext.goldReward = bi.dailyIncome[Res::GOLD] / 2; + evaluationContext.heroRole = HeroRole::MAIN; + evaluationContext.movementCostByRole[evaluationContext.heroRole] = bi.prerequisitesCount; + evaluationContext.armyReward = 0; + evaluationContext.strategicalValue = buildThis.townInfo.armyScore / 50000.0; + + if(bi.creatureID != CreatureID::NONE) + { + evaluationContext.strategicalValue += 0.5f + 0.1f * bi.creatureLevel; + + if(bi.baseCreatureID == bi.creatureID) + { + evaluationContext.armyReward = ai->ah->evaluateStackPower(bi.creatureID.toCreature(), bi.creatureGrows); + } + + auto creaturesToUpgrade = ai->ah->getTotalCreaturesAvailable(bi.baseCreatureID); + auto upgradedPower = ai->ah->evaluateStackPower(bi.creatureID.toCreature(), creaturesToUpgrade.count); + + evaluationContext.armyReward = upgradedPower - creaturesToUpgrade.power; + } + + return evaluationContext; + } +}; + +PriorityEvaluator::PriorityEvaluator() +{ + initVisitTile(); + evaluationContextBuilders[Goals::EXECUTE_HERO_CHAIN] = std::make_shared(); + evaluationContextBuilders[Goals::BUILD_STRUCTURE] = std::make_shared(); +} + +Goals::EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal) const +{ + auto builder = evaluationContextBuilders.find(goal->goalType); + + if(builder == evaluationContextBuilders.end()) + return goal->evaluationContext; + + return builder->second->buildEvaluationContext(goal); +} + /// distance /// nearest hero? /// gold income @@ -351,45 +464,28 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task) if(task->priority > 0) return task->priority; - auto heroPtr = task->hero; + auto evaluationContext = buildEvaluationContext(task); - if(!heroPtr.validAndSet()) - return 2; - - int objId = task->objid; - - if(task->parent) - objId = task->parent->objid; - - const CGObjectInstance * target = cb->getObj((ObjectInstanceID)objId, false); + int rewardType = (evaluationContext.goldReward > 0 ? 1 : 0) + + (evaluationContext.armyReward > 0 ? 1 : 0) + + (evaluationContext.skillReward > 0 ? 1 : 0) + + (evaluationContext.strategicalValue > 0 ? 1 : 0); - auto day = cb->getDate(Date::DAY); - auto hero = heroPtr.get(); - auto armyTotal = task->evaluationContext.heroStrength; - double armyLossPersentage = task->evaluationContext.armyLoss / (double)armyTotal; - uint64_t danger = task->evaluationContext.danger; - HeroRole heroRole = ai->ah->getHeroRole(heroPtr); - int32_t goldReward = getGoldReward(target, hero); - bool checkGold = danger == 0; - uint64_t armyReward = getArmyReward(target, hero, checkGold); - float skillReward = getSkillReward(target, hero, heroRole); - float strategicalValue = getStrategicalValue(target); double result = 0; - int rewardType = (goldReward > 0 ? 1 : 0) + (armyReward > 0 ? 1 : 0) + (skillReward > 0 ? 1 : 0) + (strategicalValue > 0 ? 1 : 0); - + try { - armyLossPersentageVariable->setValue(armyLossPersentage); - heroRoleVariable->setValue(heroRole); - mainTurnDistanceVariable->setValue(task->evaluationContext.movementCostByRole[HeroRole::MAIN]); - scoutTurnDistanceVariable->setValue(task->evaluationContext.movementCostByRole[HeroRole::SCOUT]); - goldRewardVariable->setValue(goldReward); - armyRewardVariable->setValue(armyReward); - skillRewardVariable->setValue(skillReward); - dangerVariable->setValue(danger); + armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage); + heroRoleVariable->setValue(evaluationContext.heroRole); + mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]); + scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]); + goldRewardVariable->setValue(evaluationContext.goldReward); + armyRewardVariable->setValue(evaluationContext.armyReward); + skillRewardVariable->setValue(evaluationContext.skillReward); + dangerVariable->setValue(evaluationContext.danger); rewardTypeVariable->setValue(rewardType); - closestHeroRatioVariable->setValue(task->evaluationContext.closestWayRatio); - strategicalValueVariable->setValue(strategicalValue); + closestHeroRatioVariable->setValue(evaluationContext.closestWayRatio); + strategicalValueVariable->setValue(evaluationContext.strategicalValue); engine->process(); //engine.process(VISIT_TILE); //TODO: Process only Visit_Tile @@ -402,16 +498,16 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task) assert(result >= 0); #ifdef VCMI_TRACE_PATHFINDER - logAi->trace("Evaluated %s, hero %s, loss: %f, turns: %f, gold: %d, army gain: %d, danger: %d, role: %s, strategical value: %f, result %f", + logAi->trace("Evaluated %s, loss: %f, turns main: %f, scout: %f, gold: %d, army gain: %d, danger: %d, role: %s, strategical value: %f, result %f", task->name(), - hero->name, - armyLossPersentage, - task->evaluationContext.movementCost, - goldReward, - armyReward, - danger, - heroRole ? "scout" : "main", - strategicalValue, + evaluationContext.armyLossPersentage, + evaluationContext.movementCostByRole[HeroRole::MAIN], + evaluationContext.movementCostByRole[HeroRole::SCOUT], + evaluationContext.goldReward, + evaluationContext.armyReward, + evaluationContext.danger, + evaluationContext.heroRole ? "scout" : "main", + evaluationContext.strategicalValue, result); #endif diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index 2e5c2f73c..f2056fe41 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -10,12 +10,12 @@ #pragma once #include "fl/Headers.h" #include "../Goals/Goals.h" -#include "../FuzzyEngines.h" -class VCAI; -class CArmedInstance; -class CBank; -struct SectorMap; +class IEvaluationContextBuilder +{ +public: + virtual Goals::EvaluationContext buildEvaluationContext(Goals::TSubgoal goal) const = 0; +}; class PriorityEvaluator { @@ -40,4 +40,7 @@ private: fl::InputVariable * rewardTypeVariable; fl::InputVariable * closestHeroRatioVariable; fl::OutputVariable * value; + std::map> evaluationContextBuilders; + + Goals::EvaluationContext buildEvaluationContext(Goals::TSubgoal goal) const; }; diff --git a/AI/Nullkiller/Goals/AbstractGoal.h b/AI/Nullkiller/Goals/AbstractGoal.h index 387db0422..ad04088bc 100644 --- a/AI/Nullkiller/Goals/AbstractGoal.h +++ b/AI/Nullkiller/Goals/AbstractGoal.h @@ -96,12 +96,17 @@ namespace Goals { float movementCost; std::map movementCostByRole; - float scoutMovementCost; int manaCost; uint64_t danger; float closestWayRatio; uint64_t armyLoss; uint64_t heroStrength; + float armyLossPersentage; + float armyReward; + int32_t goldReward; + float skillReward; + float strategicalValue; + HeroRole heroRole; EvaluationContext() : movementCost(0.0), @@ -110,7 +115,12 @@ namespace Goals closestWayRatio(1), armyLoss(0), heroStrength(0), - movementCostByRole() + movementCostByRole(), + skillReward(0), + goldReward(0), + armyReward(0), + armyLossPersentage(0), + heroRole(HeroRole::SCOUT) { } }; diff --git a/AI/Nullkiller/Goals/BuildThis.cpp b/AI/Nullkiller/Goals/BuildThis.cpp index 68145538f..fe10f560e 100644 --- a/AI/Nullkiller/Goals/BuildThis.cpp +++ b/AI/Nullkiller/Goals/BuildThis.cpp @@ -31,6 +31,11 @@ bool BuildThis::operator==(const BuildThis & other) const return town == other.town && bid == other.bid; } +std::string BuildThis::name() const +{ + return "Build " + buildingInfo.name + "(" + std::to_string(bid) + ") in " + town->name; +} + TSubgoal BuildThis::whatToDoToAchieve() { auto b = BuildingID(bid); diff --git a/AI/Nullkiller/Goals/BuildThis.h b/AI/Nullkiller/Goals/BuildThis.h index 9cc86abb8..bbd25de1e 100644 --- a/AI/Nullkiller/Goals/BuildThis.h +++ b/AI/Nullkiller/Goals/BuildThis.h @@ -10,6 +10,7 @@ #pragma once #include "CGoal.h" +#include "../Analyzers/BuildAnalyzer.h" struct HeroPtr; class VCAI; @@ -20,10 +21,19 @@ namespace Goals class DLL_EXPORT BuildThis : public CGoal { public: + BuildingInfo buildingInfo; + TownDevelopmentInfo townInfo; + BuildThis() //should be private, but unit test uses it : CGoal(Goals::BUILD_STRUCTURE) { } + BuildThis(const BuildingInfo & buildingInfo, const TownDevelopmentInfo & townInfo) //should be private, but unit test uses it + : CGoal(Goals::BUILD_STRUCTURE), buildingInfo(buildingInfo), townInfo(townInfo) + { + bid = buildingInfo.id; + town = townInfo.town; + } BuildThis(BuildingID Bid, const CGTownInstance * tid) : CGoal(Goals::BUILD_STRUCTURE) { @@ -44,5 +54,6 @@ namespace Goals TSubgoal whatToDoToAchieve() override; //bool fulfillsMe(TSubgoal goal) override; virtual bool operator==(const BuildThis & other) const override; + virtual std::string name() const override; }; } diff --git a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp index c56532c4d..023028667 100644 --- a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp +++ b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp @@ -175,7 +175,7 @@ void ExecuteHeroChain::accept(VCAI * ai) std::string ExecuteHeroChain::name() const { - return "ExecuteHeroChain " + targetName; + return "ExecuteHeroChain " + targetName + " by " + chainPath.targetHero->name; } std::string ExecuteHeroChain::completeMessage() const diff --git a/AI/Nullkiller/HeroManager.cpp b/AI/Nullkiller/HeroManager.cpp index c0f99b1b6..de70b6fbd 100644 --- a/AI/Nullkiller/HeroManager.cpp +++ b/AI/Nullkiller/HeroManager.cpp @@ -101,8 +101,10 @@ float HeroManager::evaluateFightingStrength(const CGHeroInstance * hero) const return evaluateSpeciality(hero) + wariorSkillsScores.evaluateSecSkills(hero) + hero->level * 1.5f; } -void HeroManager::updateHeroRoles() +void HeroManager::update() { + logAi->trace("Start analysing our heroes"); + std::map scores; auto myHeroes = ai->getMyHeroes(); diff --git a/AI/Nullkiller/HeroManager.h b/AI/Nullkiller/HeroManager.h index f4a03482f..5b3bbdece 100644 --- a/AI/Nullkiller/HeroManager.h +++ b/AI/Nullkiller/HeroManager.h @@ -26,7 +26,7 @@ public: virtual const std::map & getHeroRoles() const = 0; virtual int selectBestSkill(const HeroPtr & hero, const std::vector & skills) const = 0; virtual HeroRole getHeroRole(const HeroPtr & hero) const = 0; - virtual void updateHeroRoles() = 0; + virtual void update() = 0; virtual float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const = 0; virtual float evaluateHero(const CGHeroInstance * hero) const = 0; }; @@ -64,7 +64,7 @@ public: const std::map & getHeroRoles() const override; HeroRole getHeroRole(const HeroPtr & hero) const override; int selectBestSkill(const HeroPtr & hero, const std::vector & skills) const override; - void updateHeroRoles() override; + void update() override; float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const override; float evaluateHero(const CGHeroInstance * hero) const override; diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index f43cc67ef..41186f77f 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -76,7 +76,7 @@ void AINodeStorage::initialize(const PathfinderOptions & options, const CGameSta void AINodeStorage::clear() { actors.clear(); - heroChainPass = false; + heroChainPass = EHeroChainPass::INITIAL; heroChainTurn = 0; heroChainMaxTurns = 1; } @@ -262,9 +262,37 @@ bool AINodeStorage::increaseHeroChainTurnLimit() return true; } +EPathfindingLayer phisycalLayers[2] = {EPathfindingLayer::LAND, EPathfindingLayer::SAIL}; + +bool AINodeStorage::calculateHeroChainFinal() +{ + heroChainPass = EHeroChainPass::FINAL; + heroChain.resize(0); + + for(auto layer : phisycalLayers) + { + foreach_tile_pos([&](const int3 & pos) + { + auto chains = nodes[pos.x][pos.y][pos.z][layer]; + + for(AIPathNode & node : chains) + { + if(node.turns > heroChainTurn + && node.action != CGPathNode::ENodeAction::UNKNOWN + && node.actor->actorExchangeCount > 1) + { + heroChain.push_back(&node); + } + } + }); + } + + return heroChain.size(); +} + bool AINodeStorage::calculateHeroChain() { - heroChainPass = true; + heroChainPass = EHeroChainPass::CHAIN; heroChain.resize(0); std::vector existingChains; @@ -273,37 +301,40 @@ bool AINodeStorage::calculateHeroChain() existingChains.reserve(NUM_CHAINS); newChains.reserve(NUM_CHAINS); - foreach_tile_pos([&](const int3 & pos) { - auto layer = EPathfindingLayer::LAND; - auto chains = nodes[pos.x][pos.y][pos.z][layer]; - - existingChains.resize(0); - newChains.resize(0); - - for(AIPathNode & node : chains) + for(auto layer : phisycalLayers) + { + foreach_tile_pos([&](const int3 & pos) { - if(node.turns <= heroChainTurn && node.action != CGPathNode::ENodeAction::UNKNOWN) - existingChains.push_back(&node); - } + auto chains = nodes[pos.x][pos.y][pos.z][layer]; - for(AIPathNode * node : existingChains) - { - if(node->actor->isMovable) + existingChains.resize(0); + newChains.resize(0); + + for(AIPathNode & node : chains) { - calculateHeroChain(node, existingChains, newChains); + if(node.turns <= heroChainTurn && node.action != CGPathNode::ENodeAction::UNKNOWN) + existingChains.push_back(&node); } - } - cleanupInefectiveChains(newChains); - addHeroChain(newChains); - }); + for(AIPathNode * node : existingChains) + { + if(node->actor->isMovable) + { + calculateHeroChain(node, existingChains, newChains); + } + } + + cleanupInefectiveChains(newChains); + addHeroChain(newChains); + }); + } return heroChain.size(); } void AINodeStorage::cleanupInefectiveChains(std::vector & result) const { - vstd::erase_if(result, [&](ExchangeCandidate & chainInfo) -> bool + vstd::erase_if(result, [&](const ExchangeCandidate & chainInfo) -> bool { auto pos = chainInfo.coord; auto chains = nodes[pos.x][pos.y][pos.z][EPathfindingLayer::LAND]; @@ -320,12 +351,27 @@ void AINodeStorage::calculateHeroChain( { for(AIPathNode * node : variants) { - if(node == srcNode - || !node->actor - || node->turns > heroChainTurn + if(node == srcNode || !node->actor) + continue; + + if(node->turns > heroChainTurn || (node->action == CGPathNode::ENodeAction::UNKNOWN && node->actor->hero) || (node->actor->chainMask & srcNode->actor->chainMask) != 0) { +#if AI_TRACE_LEVEL >= 2 + logAi->trace( + "Skip exchange %s[%x] -> %s[%x] at %s because of %s", + node->actor->toString(), + node->actor->chainMask, + srcNode->actor->toString(), + srcNode->actor->chainMask, + srcNode->coord.toString(), + (node->turns > heroChainTurn + ? "turn limit" + : (node->action == CGPathNode::ENodeAction::UNKNOWN && node->actor->hero) + ? "action unknown" + : "chain mask")); +#endif continue; } @@ -793,7 +839,7 @@ bool AINodeStorage::hasBetterChain( } } - if((candidateActor->chainMask & node.actor->chainMask) == 0) + if(candidateActor->chainMask != node.actor->chainMask) continue; auto nodeActor = node.actor; @@ -931,6 +977,17 @@ AIPath::AIPath() { } +std::shared_ptr AIPath::getFirstBlockedAction() const +{ + for(auto node : nodes) + { + if(node.specialAction && !node.specialAction->canAct(node.targetHero)) + return node.specialAction; + } + + return std::shared_ptr(); +} + int3 AIPath::firstTileToGet() const { if(nodes.size()) @@ -1005,7 +1062,7 @@ uint64_t AIPath::getTotalArmyLoss() const return armyLoss + targetObjectArmyLoss; } -std::string AIPath::toString() +std::string AIPath::toString() const { std::stringstream str; diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.h b/AI/Nullkiller/Pathfinding/AINodeStorage.h index e46a2886d..169c3f5b5 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.h +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.h @@ -79,7 +79,9 @@ struct AIPath uint64_t getHeroStrength() const; - std::string toString(); + std::string toString() const; + + std::shared_ptr AIPath::getFirstBlockedAction() const; }; struct ExchangeCandidate : public AIPathNode @@ -88,6 +90,15 @@ struct ExchangeCandidate : public AIPathNode AIPathNode * otherParent; }; +enum EHeroChainPass +{ + INITIAL, // single heroes unlimited distance + + CHAIN, // chains with limited distance + + FINAL // same as SINGLE but for heroes from CHAIN pass +}; + class AINodeStorage : public INodeStorage { private: @@ -100,7 +111,7 @@ private: std::unique_ptr dangerEvaluator; std::vector> actors; std::vector heroChain; - bool heroChainPass; // true if we need to calculate hero chain + EHeroChainPass heroChainPass; // true if we need to calculate hero chain int heroChainTurn; int heroChainMaxTurns; PlayerColor playerID; @@ -146,7 +157,7 @@ public: bool isMovementIneficient(const PathNodeInfo & source, CDestinationNodeInfo & destination) const { // further chain distribution is calculated as the last stage - if(heroChainPass && destination.node->turns > heroChainTurn) + if(heroChainPass == EHeroChainPass::CHAIN && destination.node->turns > heroChainTurn) return true; return hasBetterChain(source, destination); @@ -169,6 +180,7 @@ public: const std::set getAllHeroes() const; void clear(); bool calculateHeroChain(); + bool calculateHeroChainFinal(); uint64_t evaluateDanger(const int3 & tile, const CGHeroInstance * hero) const { diff --git a/AI/Nullkiller/Pathfinding/AIPathfinder.cpp b/AI/Nullkiller/Pathfinding/AIPathfinder.cpp index 25dbda5a7..2c0de5ef6 100644 --- a/AI/Nullkiller/Pathfinding/AIPathfinder.cpp +++ b/AI/Nullkiller/Pathfinding/AIPathfinder.cpp @@ -66,21 +66,21 @@ void AIPathfinder::updatePaths(std::vector heroes, bool useHeroChain) do { - logAi->trace("Recalculate paths pass %d", pass++); - cb->calculatePaths(config); - - if(useHeroChain) + do { - logAi->trace("Recalculate chain pass %d", pass); + logAi->trace("Recalculate paths pass %d", pass++); + cb->calculatePaths(config); + } while(useHeroChain && storage->calculateHeroChain()); - continueCalculation = storage->calculateHeroChain(); + if(!useHeroChain) + break; - if(!continueCalculation) - { - logAi->trace("Increase chain turn limit"); - - continueCalculation = storage->increaseHeroChainTurnLimit() && storage->calculateHeroChain(); - } + if(storage->calculateHeroChainFinal()) + { + logAi->trace("Recalculate paths pass final"); + cb->calculatePaths(config); } + + continueCalculation = storage->increaseHeroChainTurnLimit() && storage->calculateHeroChain(); } while(continueCalculation); } diff --git a/AI/Nullkiller/VCAI.cpp b/AI/Nullkiller/VCAI.cpp index 503744188..df92038e6 100644 --- a/AI/Nullkiller/VCAI.cpp +++ b/AI/Nullkiller/VCAI.cpp @@ -853,10 +853,10 @@ void VCAI::makeTurn() logAi->debug("Making turn thread has been interrupted. We'll end without calling endTurn."); return; } - catch (std::exception & e) + /*catch (std::exception & e) { logAi->debug("Making turn thread has caught an exception: %s", e.what()); - } + }*/ endTurn(); } @@ -1079,7 +1079,12 @@ void VCAI::performObjectInteraction(const CGObjectInstance * obj, HeroPtr h) moveCreaturesToHero(h->visitedTown); townVisitsThisWeek[h].insert(h->visitedTown); - ah->updateHeroRoles(); + + if(!ai->nullkiller) + { + ah->update(); + } + if(ah->getHeroRole(h) == HeroRole::MAIN && !h->hasSpellbook() && ah->freeGold() >= GameConstants::SPELLBOOK_GOLD_COST) { if(h->visitedTown->hasBuilt(BuildingID::MAGES_GUILD_1)) @@ -1994,9 +1999,11 @@ bool VCAI::moveHeroToTile(int3 dst, HeroPtr h) doChannelProbing(); } - if(path.nodes[0].action == CGPathNode::BLOCKING_VISIT) + if(path.nodes[0].action == CGPathNode::BLOCKING_VISIT || path.nodes[0].action == CGPathNode::BATTLE) { - ret = h && i == 0; // when we take resource we do not reach its position. We even might not move + // when we take resource we do not reach its position. We even might not move + // also guarded town is not get visited automatically after capturing + ret = h && i == 0; } } if(h) @@ -2485,6 +2492,8 @@ void VCAI::recruitHero(const CGTownInstance * t, bool throwing) } cb->recruitHero(t, hero); + ai->ah->update(); + if(t->visitingHero) moveHeroToTile(t->visitablePos(), t->visitingHero.get());