From 2dd0d76412d0accd7de585a50c146a9b2a27527a Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sat, 16 Sep 2023 12:33:02 +0300 Subject: [PATCH 1/2] NKAI: water and air walking --- .../Analyzers/DangerHitMapAnalyzer.cpp | 8 +- AI/Nullkiller/CMakeLists.txt | 2 + AI/Nullkiller/Pathfinding/AINodeStorage.h | 2 +- .../Pathfinding/AIPathfinderConfig.cpp | 1 + .../AdventureSpellCastMovementActions.cpp | 82 ++++++++++++ .../AdventureSpellCastMovementActions.h | 58 ++++++++ .../Pathfinding/Actions/BoatActions.cpp | 4 +- .../Pathfinding/Actions/BoatActions.h | 4 +- .../Pathfinding/Actions/SpecialAction.h | 6 + .../Rules/AILayerTransitionRule.cpp | 124 +++++++++++++----- .../Pathfinding/Rules/AILayerTransitionRule.h | 9 +- lib/pathfinder/CPathfinder.cpp | 13 ++ lib/pathfinder/CPathfinder.h | 2 + lib/pathfinder/PathfinderOptions.cpp | 1 + lib/pathfinder/PathfinderOptions.h | 5 + 15 files changed, 282 insertions(+), 39 deletions(-) create mode 100644 AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.cpp create mode 100644 AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp index 3651e567f..aa2f91e59 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp @@ -72,7 +72,13 @@ void DangerHitMapAnalyzer::updateHitMap() if(ai->cb->getPlayerRelations(ai->playerID, pair.first) != PlayerRelations::ENEMIES) continue; - ai->pathfinder->updatePaths(pair.second, PathfinderSettings()); + PathfinderSettings ps; + + ps.mainTurnDistanceLimit = 10; + ps.scoutTurnDistanceLimit = 10; + ps.useHeroChain = false; + + ai->pathfinder->updatePaths(pair.second, ps); boost::this_thread::interruption_point(); diff --git a/AI/Nullkiller/CMakeLists.txt b/AI/Nullkiller/CMakeLists.txt index a6560989f..a22641684 100644 --- a/AI/Nullkiller/CMakeLists.txt +++ b/AI/Nullkiller/CMakeLists.txt @@ -9,6 +9,7 @@ set(Nullkiller_SRCS Pathfinding/Actions/BuyArmyAction.cpp Pathfinding/Actions/BoatActions.cpp Pathfinding/Actions/TownPortalAction.cpp + Pathfinding/Actions/AdventureSpellCastMovementActions.cpp Pathfinding/Rules/AILayerTransitionRule.cpp Pathfinding/Rules/AIMovementAfterDestinationRule.cpp Pathfinding/Rules/AIMovementToDestinationRule.cpp @@ -69,6 +70,7 @@ set(Nullkiller_HEADERS Pathfinding/Actions/BuyArmyAction.h Pathfinding/Actions/BoatActions.h Pathfinding/Actions/TownPortalAction.h + Pathfinding/Actions/AdventureSpellCastMovementActions.h Pathfinding/Rules/AILayerTransitionRule.h Pathfinding/Rules/AIMovementAfterDestinationRule.h Pathfinding/Rules/AIMovementToDestinationRule.h diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.h b/AI/Nullkiller/Pathfinding/AINodeStorage.h index c127f294b..71464509d 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.h +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.h @@ -45,7 +45,7 @@ struct AIPathNode : public CGPathNode { uint64_t danger; uint64_t armyLoss; - uint32_t manaCost; + int32_t manaCost; const AIPathNode * chainOther; std::shared_ptr specialAction; const ChainActor * actor; diff --git a/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp b/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp index b7314b3d1..2259ef029 100644 --- a/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp +++ b/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp @@ -44,6 +44,7 @@ namespace AIPathfinding std::shared_ptr nodeStorage) :PathfinderConfig(nodeStorage, makeRuleset(cb, ai, nodeStorage)), aiNodeStorage(nodeStorage) { + options.canUseCast = true; } AIPathfinderConfig::~AIPathfinderConfig() = default; diff --git a/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.cpp b/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.cpp new file mode 100644 index 000000000..ff4934b36 --- /dev/null +++ b/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.cpp @@ -0,0 +1,82 @@ +/* +* AdventureSpellCastMovementActions.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 "../../AIGateway.h" +#include "../../Goals/AdventureSpellCast.h" +#include "../../Goals/CaptureObject.h" +#include "../../Goals/Invalid.h" +#include "../../Goals/BuildBoat.h" +#include "../../../../lib/mapObjects/MapObjects.h" +#include "AdventureSpellCastMovementActions.h" + +namespace NKAI +{ + +namespace AIPathfinding +{ + AdventureCastAction::AdventureCastAction(SpellID spellToCast, const CGHeroInstance * hero) + :spellToCast(spellToCast), hero(hero) + { + manaCost = hero->getSpellCost(spellToCast.toSpell()); + } + + WaterWalkingAction::WaterWalkingAction(const CGHeroInstance * hero) + :AdventureCastAction(SpellID::WATER_WALK, hero) + { } + + AirWalkingAction::AirWalkingAction(const CGHeroInstance * hero) + : AdventureCastAction(SpellID::FLY, hero) + { + } + + void AdventureCastAction::applyOnDestination( + const CGHeroInstance * hero, + CDestinationNodeInfo & destination, + const PathNodeInfo & source, + AIPathNode * dstMode, + const AIPathNode * srcNode) const + { + dstMode->manaCost = srcNode->manaCost + manaCost; + dstMode->theNodeBefore = source.node; + } + + void AdventureCastAction::execute(const CGHeroInstance * hero) const + { + assert(hero == this->hero); + + Goals::AdventureSpellCast(hero, spellToCast).accept(ai); + } + + bool AdventureCastAction::canAct(const AIPathNode * source) const + { + assert(hero == this->hero); + + auto hero = source->actor->hero; + +#ifdef VCMI_TRACE_PATHFINDER + logAi->trace( + "Hero %s has %d mana and needed %d and already spent %d", + hero->name, + hero->mana, + getManaCost(hero), + source->manaCost); +#endif + + return hero->mana >= source->manaCost + manaCost; + } + + std::string AdventureCastAction::toString() const + { + return "Cast " + spellToCast.toSpell()->getNameTranslated() + " by " + hero->getNameTranslated(); + } +} + +} diff --git a/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h b/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h new file mode 100644 index 000000000..0667e400a --- /dev/null +++ b/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h @@ -0,0 +1,58 @@ +/* +* AdventureSpellCastMovementActions.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 "SpecialAction.h" +#include "../../../../lib/mapObjects/MapObjects.h" + +namespace NKAI +{ + +namespace AIPathfinding +{ + class AdventureCastAction : public SpecialAction + { + private: + SpellID spellToCast; + const CGHeroInstance * hero; + int manaCost; + + public: + AdventureCastAction(SpellID spellToCast, const CGHeroInstance * hero); + + virtual void execute(const CGHeroInstance * hero) const override; + + virtual void applyOnDestination( + const CGHeroInstance * hero, + CDestinationNodeInfo & destination, + const PathNodeInfo & source, + AIPathNode * dstMode, + const AIPathNode * srcNode) const override; + + virtual bool canAct(const AIPathNode * source) const override; + + virtual std::string toString() const override; + }; + + class WaterWalkingAction : public AdventureCastAction + { + public: + WaterWalkingAction(const CGHeroInstance * hero); + }; + + class AirWalkingAction : public AdventureCastAction + { + public: + AirWalkingAction(const CGHeroInstance * hero); + }; +} + +} diff --git a/AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp b/AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp index bbd1e5297..7a6ab3f10 100644 --- a/AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp +++ b/AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp @@ -114,7 +114,7 @@ namespace AIPathfinding source->manaCost); #endif - return hero->mana >= (si32)(source->manaCost + getManaCost(hero)); + return hero->mana >= source->manaCost + getManaCost(hero); } std::string SummonBoatAction::toString() const @@ -122,7 +122,7 @@ namespace AIPathfinding return "Summon Boat"; } - uint32_t SummonBoatAction::getManaCost(const CGHeroInstance * hero) const + int32_t SummonBoatAction::getManaCost(const CGHeroInstance * hero) const { SpellID summonBoat = SpellID::SUMMON_BOAT; diff --git a/AI/Nullkiller/Pathfinding/Actions/BoatActions.h b/AI/Nullkiller/Pathfinding/Actions/BoatActions.h index 92249eb78..5e6ca50d4 100644 --- a/AI/Nullkiller/Pathfinding/Actions/BoatActions.h +++ b/AI/Nullkiller/Pathfinding/Actions/BoatActions.h @@ -20,8 +20,6 @@ namespace AIPathfinding { class VirtualBoatAction : public SpecialAction { - public: - virtual const ChainActor * getActor(const ChainActor * sourceActor) const = 0; }; class SummonBoatAction : public VirtualBoatAction @@ -43,7 +41,7 @@ namespace AIPathfinding virtual std::string toString() const override; private: - uint32_t getManaCost(const CGHeroInstance * hero) const; + int32_t getManaCost(const CGHeroInstance * hero) const; }; class BuildBoatAction : public VirtualBoatAction diff --git a/AI/Nullkiller/Pathfinding/Actions/SpecialAction.h b/AI/Nullkiller/Pathfinding/Actions/SpecialAction.h index c14589e75..77270ce1a 100644 --- a/AI/Nullkiller/Pathfinding/Actions/SpecialAction.h +++ b/AI/Nullkiller/Pathfinding/Actions/SpecialAction.h @@ -22,6 +22,7 @@ namespace NKAI { struct AIPathNode; +class ChainActor; class SpecialAction { @@ -54,6 +55,11 @@ public: { return {}; } + + virtual const ChainActor * getActor(const ChainActor * sourceActor) const + { + return sourceActor; + } }; class CompositeAction : public SpecialAction diff --git a/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp b/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp index f555b18c3..7dbac9010 100644 --- a/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp +++ b/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp @@ -10,6 +10,8 @@ #include "StdInc.h" #include "AILayerTransitionRule.h" #include "../../Engine/Nullkiller.h" +#include "../../../../lib/pathfinder/CPathfinder.h" +#include "../../../../lib/pathfinder/TurnInfo.h" namespace NKAI { @@ -31,23 +33,79 @@ namespace AIPathfinding if(!destination.blocked) { - return; + if(source.node->layer == EPathfindingLayer::LAND + && (destination.node->layer == EPathfindingLayer::AIR || destination.node->layer == EPathfindingLayer::WATER)) + { + if(pathfinderHelper->getTurnInfo()->isLayerAvailable(destination.node->layer)) + return; + else + destination.blocked = true; + } + else + { + return; + } } if(source.node->layer == EPathfindingLayer::LAND && destination.node->layer == EPathfindingLayer::SAIL) { std::shared_ptr virtualBoat = findVirtualBoat(destination, source); - if(virtualBoat && tryEmbarkVirtualBoat(destination, source, virtualBoat)) + if(virtualBoat && tryUseSpecialAction(destination, source, virtualBoat, EPathNodeAction::EMBARK)) { #if NKAI_PATHFINDER_TRACE_LEVEL >= 1 logAi->trace("Embarking to virtual boat while moving %s -> %s!", source.coord.toString(), destination.coord.toString()); +#endif + } + } + + if(source.node->layer == EPathfindingLayer::LAND && destination.node->layer == EPathfindingLayer::WATER) + { + auto action = waterWalkingActions.find(nodeStorage->getHero(source.node)); + + if(action != waterWalkingActions.end() && tryUseSpecialAction(destination, source, action->second, EPathNodeAction::NORMAL)) + { +#if NKAI_PATHFINDER_TRACE_LEVEL >= 2 + logAi->trace("Casting water walk while moving %s -> %s!", source.coord.toString(), destination.coord.toString()); +#endif + } + } + + if(source.node->layer == EPathfindingLayer::LAND && destination.node->layer == EPathfindingLayer::AIR) + { + auto action = airWalkingActions.find(nodeStorage->getHero(source.node)); + + if(action != airWalkingActions.end() && tryUseSpecialAction(destination, source, action->second, EPathNodeAction::NORMAL)) + { +#if NKAI_PATHFINDER_TRACE_LEVEL >= 2 + logAi->trace("Casting fly while moving %s -> %s!", source.coord.toString(), destination.coord.toString()); #endif } } } void AILayerTransitionRule::setup() + { + SpellID waterWalk = SpellID::WATER_WALK; + SpellID airWalk = SpellID::FLY; + + for(const CGHeroInstance * hero : nodeStorage->getAllHeroes()) + { + if(hero->canCastThisSpell(waterWalk.toSpell())) + { + waterWalkingActions[hero] = std::make_shared(hero); + } + + if(hero->canCastThisSpell(airWalk.toSpell())) + { + airWalkingActions[hero] = std::make_shared(hero); + } + } + + collectVirtualBoats(); + } + + void AILayerTransitionRule::collectVirtualBoats() { std::vector shipyards; @@ -113,50 +171,56 @@ namespace AIPathfinding return virtualBoat; } - bool AILayerTransitionRule::tryEmbarkVirtualBoat( + bool AILayerTransitionRule::tryUseSpecialAction( CDestinationNodeInfo & destination, const PathNodeInfo & source, - std::shared_ptr virtualBoat) const + std::shared_ptr specialAction, + EPathNodeAction targetAction) const { bool result = false; - nodeStorage->updateAINode(destination.node, [&](AIPathNode * node) + if(!specialAction->canAct(nodeStorage->getAINode(source.node))) { - auto boatNodeOptional = nodeStorage->getOrCreateNode( - node->coord, - node->layer, - virtualBoat->getActor(node->actor)); + return false; + } - if(boatNodeOptional) + nodeStorage->updateAINode(destination.node, [&](AIPathNode * node) { - AIPathNode * boatNode = boatNodeOptional.value(); + auto castNodeOptional = nodeStorage->getOrCreateNode( + node->coord, + node->layer, + specialAction->getActor(node->actor)); - if(boatNode->action == EPathNodeAction::UNKNOWN) + if(castNodeOptional) { - boatNode->addSpecialAction(virtualBoat); - destination.blocked = false; - destination.action = EPathNodeAction::EMBARK; - destination.node = boatNode; - result = true; + AIPathNode * castNode = castNodeOptional.value(); + + if(castNode->action == EPathNodeAction::UNKNOWN) + { + castNode->addSpecialAction(specialAction); + destination.blocked = false; + destination.action = targetAction; + destination.node = castNode; + result = true; + } + else + { +#if NKAI_PATHFINDER_TRACE_LEVEL >= 1 + logAi->trace( + "Special transition node already allocated. Blocked moving %s -> %s", + source.coord.toString(), + destination.coord.toString()); +#endif + } } else { -#if NKAI_PATHFINDER_TRACE_LEVEL >= 1 - logAi->trace( - "Special transition node already allocated. Blocked moving %s -> %s", + logAi->debug( + "Can not allocate special transition node while moving %s -> %s", source.coord.toString(), destination.coord.toString()); -#endif } - } - else - { - logAi->debug( - "Can not allocate special transition node while moving %s -> %s", - source.coord.toString(), - destination.coord.toString()); - } - }); + }); return result; } diff --git a/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.h b/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.h index f7d5e27b8..243cb96a9 100644 --- a/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.h +++ b/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.h @@ -13,6 +13,7 @@ #include "../AINodeStorage.h" #include "../../AIGateway.h" #include "../Actions/BoatActions.h" +#include "../Actions/AdventureSpellCastMovementActions.h" #include "../../../../CCallback.h" #include "../../../../lib/mapObjects/MapObjects.h" #include "../../../../lib/pathfinder/PathfindingRules.h" @@ -29,6 +30,8 @@ namespace AIPathfinding std::map> virtualBoats; std::shared_ptr nodeStorage; std::map> summonableVirtualBoats; + std::map> waterWalkingActions; + std::map> airWalkingActions; public: AILayerTransitionRule(CPlayerSpecificInfoCallback * cb, Nullkiller * ai, std::shared_ptr nodeStorage); @@ -41,15 +44,17 @@ namespace AIPathfinding private: void setup(); + void collectVirtualBoats(); std::shared_ptr findVirtualBoat( CDestinationNodeInfo & destination, const PathNodeInfo & source) const; - bool tryEmbarkVirtualBoat( + bool tryUseSpecialAction( CDestinationNodeInfo & destination, const PathNodeInfo & source, - std::shared_ptr virtualBoat) const; + std::shared_ptr specialAction, + EPathNodeAction targetAction) const; }; } diff --git a/lib/pathfinder/CPathfinder.cpp b/lib/pathfinder/CPathfinder.cpp index 1cb3b7163..8044535fb 100644 --- a/lib/pathfinder/CPathfinder.cpp +++ b/lib/pathfinder/CPathfinder.cpp @@ -20,6 +20,7 @@ #include "../TerrainHandler.h" #include "../mapObjects/CGHeroInstance.h" #include "../mapping/CMap.h" +#include "spells/CSpellHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -472,6 +473,12 @@ CPathfinderHelper::CPathfinderHelper(CGameState * gs, const CGHeroInstance * Her turnsInfo.reserve(16); updateTurnInfo(); initializePatrol(); + + SpellID flySpell = SpellID::FLY; + canCastFly = Hero->canCastThisSpell(flySpell.toSpell()); + + SpellID waterWalk = SpellID::WATER_WALK; + canCastWaterWalk = Hero->canCastThisSpell(waterWalk.toSpell()); } CPathfinderHelper::~CPathfinderHelper() @@ -501,12 +508,18 @@ bool CPathfinderHelper::isLayerAvailable(const EPathfindingLayer & layer) const if(!options.useFlying) return false; + if(canCastFly && options.canUseCast) + return true; + break; case EPathfindingLayer::WATER: if(!options.useWaterWalking) return false; + if(canCastWaterWalk && options.canUseCast) + return true; + break; } diff --git a/lib/pathfinder/CPathfinder.h b/lib/pathfinder/CPathfinder.h index 0071f41eb..a52de7b65 100644 --- a/lib/pathfinder/CPathfinder.h +++ b/lib/pathfinder/CPathfinder.h @@ -72,6 +72,8 @@ public: const CGHeroInstance * hero; std::vector turnsInfo; const PathfinderOptions & options; + bool canCastFly; + bool canCastWaterWalk; CPathfinderHelper(CGameState * gs, const CGHeroInstance * Hero, const PathfinderOptions & Options); virtual ~CPathfinderHelper(); diff --git a/lib/pathfinder/PathfinderOptions.cpp b/lib/pathfinder/PathfinderOptions.cpp index 42af2f53b..4c83acafc 100644 --- a/lib/pathfinder/PathfinderOptions.cpp +++ b/lib/pathfinder/PathfinderOptions.cpp @@ -31,6 +31,7 @@ PathfinderOptions::PathfinderOptions() , oneTurnSpecialLayersLimit(true) , originalMovementRules(false) , turnLimit(std::numeric_limits::max()) + , canUseCast(false) { } diff --git a/lib/pathfinder/PathfinderOptions.h b/lib/pathfinder/PathfinderOptions.h index f26cf6273..96d75cb2a 100644 --- a/lib/pathfinder/PathfinderOptions.h +++ b/lib/pathfinder/PathfinderOptions.h @@ -71,6 +71,11 @@ struct DLL_LINKAGE PathfinderOptions /// Max number of turns to compute. Default = infinite uint8_t turnLimit; + /// + /// For AI. Allows water walk and fly layers if hero can cast appropriate spells + /// + bool canUseCast; + PathfinderOptions(); }; From ebe155fa95af9fb27c4ebab0e5e5ffd88d5feabb Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 24 Sep 2023 13:07:42 +0300 Subject: [PATCH 2/2] NKAI: mana recovery --- AI/Nullkiller/Analyzers/HeroManager.cpp | 35 ++++++++++ AI/Nullkiller/Analyzers/HeroManager.h | 2 + AI/Nullkiller/Analyzers/ObjectClusterizer.cpp | 22 +++--- .../Behaviors/StayAtTownBehavior.cpp | 70 +++++++++++++++++++ AI/Nullkiller/Behaviors/StayAtTownBehavior.h | 39 +++++++++++ AI/Nullkiller/CMakeLists.txt | 4 ++ AI/Nullkiller/Engine/Nullkiller.cpp | 4 +- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 26 +++++++ AI/Nullkiller/Engine/PriorityEvaluator.h | 1 + AI/Nullkiller/Goals/AbstractGoal.h | 4 +- AI/Nullkiller/Goals/StayAtTown.cpp | 52 ++++++++++++++ AI/Nullkiller/Goals/StayAtTown.h | 36 ++++++++++ AI/Nullkiller/Pathfinding/AINodeStorage.cpp | 4 +- AI/Nullkiller/Pathfinding/AINodeStorage.h | 1 + 14 files changed, 289 insertions(+), 11 deletions(-) create mode 100644 AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp create mode 100644 AI/Nullkiller/Behaviors/StayAtTownBehavior.h create mode 100644 AI/Nullkiller/Goals/StayAtTown.cpp create mode 100644 AI/Nullkiller/Goals/StayAtTown.h diff --git a/AI/Nullkiller/Analyzers/HeroManager.cpp b/AI/Nullkiller/Analyzers/HeroManager.cpp index b896ab728..4397620b6 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -190,6 +190,41 @@ bool HeroManager::heroCapReached() const || heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP); } +float HeroManager::getMagicStrength(const CGHeroInstance * hero) const +{ + auto hasFly = hero->spellbookContainsSpell(SpellID::FLY); + auto hasTownPortal = hero->spellbookContainsSpell(SpellID::TOWN_PORTAL); + auto manaLimit = hero->manaLimit(); + auto spellPower = hero->getPrimSkillLevel(PrimarySkill::SPELL_POWER); + auto hasEarth = hero->getSpellSchoolLevel(SpellID(SpellID::TOWN_PORTAL).toSpell()) > 0; + + auto score = 0.0f; + + for(auto spellId : hero->getSpellsInSpellbook()) + { + auto spell = spellId.toSpell(); + auto schoolLevel = hero->getSpellSchoolLevel(spell); + + score += (spell->getLevel() + 1) * (schoolLevel + 1) * 0.05f; + } + + vstd::amin(score, 1); + + score *= std::min(1.0f, spellPower / 10.0f); + + if(hasFly) + score += 0.3f; + + if(hasTownPortal && hasEarth) + score += 0.6f; + + vstd::amin(score, 1); + + score *= std::min(1.0f, manaLimit / 100.0f); + + return std::min(score, 1.0f); +} + bool HeroManager::canRecruitHero(const CGTownInstance * town) const { if(!town) diff --git a/AI/Nullkiller/Analyzers/HeroManager.h b/AI/Nullkiller/Analyzers/HeroManager.h index 1009fd31e..a7744ad1f 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.h +++ b/AI/Nullkiller/Analyzers/HeroManager.h @@ -34,6 +34,7 @@ public: virtual bool heroCapReached() const = 0; virtual const CGHeroInstance * findHeroWithGrail() const = 0; virtual const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const = 0; + virtual float getMagicStrength(const CGHeroInstance * hero) const = 0; }; class DLL_EXPORT ISecondarySkillRule @@ -76,6 +77,7 @@ public: bool heroCapReached() const override; const CGHeroInstance * findHeroWithGrail() const override; const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const override; + float getMagicStrength(const CGHeroInstance * hero) const override; private: float evaluateFightingStrength(const CGHeroInstance * hero) const; diff --git a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp index 5e2b41977..a228f1b4d 100644 --- a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp +++ b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp @@ -94,16 +94,22 @@ const CGObjectInstance * ObjectClusterizer::getBlocker(const AIPath & path) cons { for(auto node = path.nodes.rbegin(); node != path.nodes.rend(); node++) { - auto guardPos = ai->cb->getGuardingCreaturePosition(node->coord); - auto blockers = ai->cb->getVisitableObjs(node->coord); - - if(guardPos.valid()) - { - auto guard = ai->cb->getTopObj(ai->cb->getGuardingCreaturePosition(node->coord)); + std::vector blockers = {}; - if(guard) + if(node->layer == EPathfindingLayer::LAND || node->layer == EPathfindingLayer::SAIL) + { + auto guardPos = ai->cb->getGuardingCreaturePosition(node->coord); + + blockers = ai->cb->getVisitableObjs(node->coord); + + if(guardPos.valid()) { - blockers.insert(blockers.begin(), guard); + auto guard = ai->cb->getTopObj(ai->cb->getGuardingCreaturePosition(node->coord)); + + if(guard) + { + blockers.insert(blockers.begin(), guard); + } } } diff --git a/AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp b/AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp new file mode 100644 index 000000000..2f7c89b2f --- /dev/null +++ b/AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp @@ -0,0 +1,70 @@ +/* +* StartupBehavior.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 "StayAtTownBehavior.h" +#include "../AIGateway.h" +#include "../AIUtility.h" +#include "../Goals/StayAtTown.h" +#include "../Goals/Composition.h" +#include "../Goals/ExecuteHeroChain.h" +#include "lib/mapObjects/MapObjects.h" //for victory conditions +#include "../Engine/Nullkiller.h" + +namespace NKAI +{ + +using namespace Goals; + +std::string StayAtTownBehavior::toString() const +{ + return "StayAtTownBehavior"; +} + +Goals::TGoalVec StayAtTownBehavior::decompose() const +{ + Goals::TGoalVec tasks; + auto towns = cb->getTownsInfo(); + + if(!towns.size()) + return tasks; + + for(auto town : towns) + { + if(!town->hasBuilt(BuildingID::MAGES_GUILD_1)) + continue; + + auto paths = ai->nullkiller->pathfinder->getPathInfo(town->visitablePos()); + + for(auto & path : paths) + { + if(town->visitingHero && town->visitingHero.get() != path.targetHero) + continue; + + if(path.turn() == 0 && !path.getFirstBlockedAction() && path.exchangeCount <= 1) + { + if(path.targetHero->mana == path.targetHero->manaLimit()) + continue; + + Composition stayAtTown; + + stayAtTown.addNextSequence({ + sptr(ExecuteHeroChain(path)), + sptr(StayAtTown(town, path)) + }); + + tasks.push_back(sptr(stayAtTown)); + } + } + } + + return tasks; +} + +} diff --git a/AI/Nullkiller/Behaviors/StayAtTownBehavior.h b/AI/Nullkiller/Behaviors/StayAtTownBehavior.h new file mode 100644 index 000000000..260cf136a --- /dev/null +++ b/AI/Nullkiller/Behaviors/StayAtTownBehavior.h @@ -0,0 +1,39 @@ +/* +* StayAtTownBehavior.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 "lib/VCMI_Lib.h" +#include "../Goals/CGoal.h" +#include "../AIUtility.h" + +namespace NKAI +{ +namespace Goals +{ + class StayAtTownBehavior : public CGoal + { + public: + StayAtTownBehavior() + :CGoal(STAY_AT_TOWN_BEHAVIOR) + { + } + + virtual TGoalVec decompose() const override; + virtual std::string toString() const override; + + virtual bool operator==(const StayAtTownBehavior & other) const override + { + return true; + } + }; +} + + +} diff --git a/AI/Nullkiller/CMakeLists.txt b/AI/Nullkiller/CMakeLists.txt index a22641684..042cb5a0d 100644 --- a/AI/Nullkiller/CMakeLists.txt +++ b/AI/Nullkiller/CMakeLists.txt @@ -35,6 +35,7 @@ set(Nullkiller_SRCS Goals/ExecuteHeroChain.cpp Goals/ExchangeSwapTownHeroes.cpp Goals/CompleteQuest.cpp + Goals/StayAtTown.cpp Markers/ArmyUpgrade.cpp Markers/HeroExchange.cpp Markers/UnlockCluster.cpp @@ -53,6 +54,7 @@ set(Nullkiller_SRCS Behaviors/BuildingBehavior.cpp Behaviors/GatherArmyBehavior.cpp Behaviors/ClusterBehavior.cpp + Behaviors/StayAtTownBehavior.cpp Helpers/ArmyFormation.cpp AIGateway.cpp ) @@ -99,6 +101,7 @@ set(Nullkiller_HEADERS Goals/ExchangeSwapTownHeroes.h Goals/CompleteQuest.h Goals/Goals.h + Goals/StayAtTown.h Markers/ArmyUpgrade.h Markers/HeroExchange.h Markers/UnlockCluster.h @@ -117,6 +120,7 @@ set(Nullkiller_HEADERS Behaviors/BuildingBehavior.h Behaviors/GatherArmyBehavior.h Behaviors/ClusterBehavior.h + Behaviors/StayAtTownBehavior.h Helpers/ArmyFormation.h AIGateway.h ) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index b69073f33..3d9a78c33 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -18,6 +18,7 @@ #include "../Behaviors/BuildingBehavior.h" #include "../Behaviors/GatherArmyBehavior.h" #include "../Behaviors/ClusterBehavior.h" +#include "../Behaviors/StayAtTownBehavior.h" #include "../Goals/Invalid.h" #include "../Goals/Composition.h" @@ -262,7 +263,8 @@ void Nullkiller::makeTurn() choseBestTask(sptr(CaptureObjectsBehavior()), 1), choseBestTask(sptr(ClusterBehavior()), MAX_DEPTH), choseBestTask(sptr(DefenceBehavior()), MAX_DEPTH), - choseBestTask(sptr(GatherArmyBehavior()), MAX_DEPTH) + choseBestTask(sptr(GatherArmyBehavior()), MAX_DEPTH), + choseBestTask(sptr(StayAtTownBehavior()), MAX_DEPTH) }; if(cb->getDate(Date::DAY) == 1) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 202644db3..1888c876a 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -22,6 +22,7 @@ #include "../../../lib/filesystem/Filesystem.h" #include "../Goals/ExecuteHeroChain.h" #include "../Goals/BuildThis.h" +#include "../Goals/StayAtTown.h" #include "../Goals/ExchangeSwapTownHeroes.h" #include "../Goals/DismissHero.h" #include "../Markers/UnlockCluster.h" @@ -309,6 +310,9 @@ uint64_t RewardEvaluator::getArmyReward( : 0; case Obj::PANDORAS_BOX: return 5000; + case Obj::MAGIC_WELL: + case Obj::MAGIC_SPRING: + return getManaRecoveryArmyReward(hero); default: return 0; } @@ -450,6 +454,11 @@ uint64_t RewardEvaluator::townArmyGrowth(const CGTownInstance * town) const return result; } +uint64_t RewardEvaluator::getManaRecoveryArmyReward(const CGHeroInstance * hero) const +{ + return ai->heroManager->getMagicStrength(hero) * 10000 * (1.0f - std::sqrt(static_cast(hero->mana) / hero->manaLimit())); +} + float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) const { if(!target) @@ -693,6 +702,22 @@ public: } }; +class StayAtTownManaRecoveryEvaluator : public IEvaluationContextBuilder +{ +public: + virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override + { + if(task->goalType != Goals::STAY_AT_TOWN) + return; + + Goals::StayAtTown & stayAtTown = dynamic_cast(*task); + + evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero().get()); + evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted(); + evaluationContext.movementCost += stayAtTown.getMovementWasted(); + } +}; + void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uint8_t turn, uint64_t ourStrength) { HitMapInfo enemyDanger = evaluationContext.evaluator.getEnemyHeroDanger(tile, turn); @@ -998,6 +1023,7 @@ PriorityEvaluator::PriorityEvaluator(const Nullkiller * ai) evaluationContextBuilders.push_back(std::make_shared()); evaluationContextBuilders.push_back(std::make_shared()); evaluationContextBuilders.push_back(std::make_shared(ai)); + 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 5ab2a6082..4853e4aed 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -49,6 +49,7 @@ public: 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; + uint64_t getManaRecoveryArmyReward(const CGHeroInstance * hero) const; }; struct DLL_EXPORT EvaluationContext diff --git a/AI/Nullkiller/Goals/AbstractGoal.h b/AI/Nullkiller/Goals/AbstractGoal.h index a5b170c9f..d089083bf 100644 --- a/AI/Nullkiller/Goals/AbstractGoal.h +++ b/AI/Nullkiller/Goals/AbstractGoal.h @@ -71,7 +71,9 @@ namespace Goals ARMY_UPGRADE, DEFEND_TOWN, CAPTURE_OBJECT, - SAVE_RESOURCES + SAVE_RESOURCES, + STAY_AT_TOWN_BEHAVIOR, + STAY_AT_TOWN }; class DLL_EXPORT TSubgoal : public std::shared_ptr diff --git a/AI/Nullkiller/Goals/StayAtTown.cpp b/AI/Nullkiller/Goals/StayAtTown.cpp new file mode 100644 index 000000000..82342cb51 --- /dev/null +++ b/AI/Nullkiller/Goals/StayAtTown.cpp @@ -0,0 +1,52 @@ +/* +* ArmyUpgrade.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 "StayAtTown.h" +#include "../AIGateway.h" +#include "../Engine/Nullkiller.h" +#include "../AIUtility.h" + +namespace NKAI +{ + +using namespace Goals; + +StayAtTown::StayAtTown(const CGTownInstance * town, AIPath & path) + : ElementarGoal(Goals::STAY_AT_TOWN) +{ + sethero(path.targetHero); + settown(town); + movementWasted = static_cast(hero->movementPointsRemaining()) / hero->movementPointsLimit(!hero->boat) - path.movementCost(); + vstd::amax(movementWasted, 0); +} + +bool StayAtTown::operator==(const StayAtTown & other) const +{ + return hero == other.hero && town == other.town; +} + +std::string StayAtTown::toString() const +{ + return "Stay at town " + town->getNameTranslated() + + " hero " + hero->getNameTranslated() + + ", mana: " + std::to_string(hero->mana); +} + +void StayAtTown::accept(AIGateway * ai) +{ + if(hero->visitedTown != town) + { + logAi->error("Hero %s expected visiting town %s", hero->getNameTranslated(), town->getNameTranslated()); + } + + ai->nullkiller->lockHero(hero.get(), HeroLockedReason::DEFENCE); +} + +} diff --git a/AI/Nullkiller/Goals/StayAtTown.h b/AI/Nullkiller/Goals/StayAtTown.h new file mode 100644 index 000000000..880881386 --- /dev/null +++ b/AI/Nullkiller/Goals/StayAtTown.h @@ -0,0 +1,36 @@ +/* +* ArmyUpgrade.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 "../Goals/CGoal.h" +#include "../Pathfinding/AINodeStorage.h" +#include "../Analyzers/ArmyManager.h" +#include "../Analyzers/DangerHitMapAnalyzer.h" + +namespace NKAI +{ +namespace Goals +{ + class DLL_EXPORT StayAtTown : public ElementarGoal + { + private: + float movementWasted; + + public: + StayAtTown(const CGTownInstance * town, AIPath & path); + + virtual bool operator==(const StayAtTown & other) const override; + virtual std::string toString() const override; + void accept(AIGateway * ai) override; + float getMovementWasted() const { return movementWasted; } + }; +} + +} diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index 7707033eb..067525c3e 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -279,9 +279,10 @@ void AINodeStorage::commit( #if NKAI_PATHFINDER_TRACE_LEVEL >= 2 logAi->trace( - "Commited %s -> %s, cost: %f, turn: %s, mp: %d, hero: %s, mask: %x, army: %lld", + "Commited %s -> %s, layer: %d, cost: %f, turn: %s, mp: %d, hero: %s, mask: %x, army: %lld", source->coord.toString(), destination->coord.toString(), + destination->layer, destination->getCost(), std::to_string(destination->turns), destination->moveRemains, @@ -1343,6 +1344,7 @@ void AINodeStorage::fillChainInfo(const AIPathNode * node, AIPath & path, int pa pathNode.coord = node->coord; pathNode.parentIndex = parentIndex; pathNode.actionIsBlocked = false; + pathNode.layer = node->layer; if(pathNode.specialAction) { diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.h b/AI/Nullkiller/Pathfinding/AINodeStorage.h index 71464509d..068304955 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.h +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.h @@ -65,6 +65,7 @@ struct AIPathNodeInfo float cost; uint8_t turns; int3 coord; + EPathfindingLayer layer; uint64_t danger; const CGHeroInstance * targetHero; int parentIndex;