From 5d022ba77c6ed609fcbfb3c331125220bed3bfcf Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 9 Dec 2018 15:20:46 +0200 Subject: [PATCH] AI: CompleteQuest goal and summon boat spell support --- AI/VCAI/CMakeLists.txt | 4 + AI/VCAI/FuzzyHelper.cpp | 13 + AI/VCAI/FuzzyHelper.h | 1 + AI/VCAI/Goals/AbstractGoal.cpp | 4 - AI/VCAI/Goals/AbstractGoal.h | 58 ++--- AI/VCAI/Goals/AdventureSpellCast.cpp | 63 +++++ AI/VCAI/Goals/AdventureSpellCast.h | 39 +++ AI/VCAI/Goals/CGoal.h | 8 +- AI/VCAI/Goals/CompleteQuest.cpp | 276 +++++++++++++++++++++ AI/VCAI/Goals/CompleteQuest.h | 46 ++++ AI/VCAI/Goals/Conquer.cpp | 2 + AI/VCAI/Goals/GatherTroops.cpp | 2 + AI/VCAI/Goals/Goals.h | 4 +- AI/VCAI/Pathfinding/AINodeStorage.cpp | 10 +- AI/VCAI/Pathfinding/AINodeStorage.h | 12 + AI/VCAI/Pathfinding/AIPathfinder.cpp | 2 +- AI/VCAI/Pathfinding/AIPathfinderConfig.cpp | 178 ++++++++++--- AI/VCAI/Pathfinding/PathfindingManager.cpp | 20 +- AI/VCAI/VCAI.cpp | 168 +------------ AI/VCAI/VCAI.h | 1 - 20 files changed, 668 insertions(+), 243 deletions(-) create mode 100644 AI/VCAI/Goals/AdventureSpellCast.cpp create mode 100644 AI/VCAI/Goals/AdventureSpellCast.h create mode 100644 AI/VCAI/Goals/CompleteQuest.cpp create mode 100644 AI/VCAI/Goals/CompleteQuest.h diff --git a/AI/VCAI/CMakeLists.txt b/AI/VCAI/CMakeLists.txt index 58ab90cb0..d9400968b 100644 --- a/AI/VCAI/CMakeLists.txt +++ b/AI/VCAI/CMakeLists.txt @@ -29,6 +29,7 @@ set(VCAI_SRCS Goals/GatherArmy.cpp Goals/GatherTroops.cpp Goals/BuyArmy.cpp + Goals/AdventureSpellCast.cpp Goals/Win.cpp Goals/VisitTile.cpp Goals/VisitObj.cpp @@ -41,6 +42,7 @@ set(VCAI_SRCS Goals/DigAtTile.cpp Goals/GetArtOfType.cpp Goals/FindObj.cpp + Goals/CompleteQuest.cpp main.cpp VCAI.cpp ) @@ -71,6 +73,7 @@ set(VCAI_HEADERS Goals/GatherArmy.h Goals/GatherTroops.h Goals/BuyArmy.h + Goals/AdventureSpellCast.h Goals/Win.h Goals/VisitTile.h Goals/VisitObj.h @@ -83,6 +86,7 @@ set(VCAI_HEADERS Goals/DigAtTile.h Goals/GetArtOfType.h Goals/FindObj.h + Goals/CompleteQuest.h Goals/Goals.h VCAI.h ) diff --git a/AI/VCAI/FuzzyHelper.cpp b/AI/VCAI/FuzzyHelper.cpp index 82a0ae1fd..ef86eb1de 100644 --- a/AI/VCAI/FuzzyHelper.cpp +++ b/AI/VCAI/FuzzyHelper.cpp @@ -93,6 +93,19 @@ float FuzzyHelper::evaluate(Goals::BuildBoat & g) return g.parent->accept(this) - buildBoatPenalty; } +float FuzzyHelper::evaluate(Goals::CompleteQuest & g) +{ + // TODO: How to evaluate quest complexity? + const float questPenalty = 0.2; + + if(!g.parent) + { + return 0; + } + + return g.parent->accept(this) * questPenalty; +} + float FuzzyHelper::evaluate(Goals::VisitObj & g) { return visitObjEngine.evaluate(g); diff --git a/AI/VCAI/FuzzyHelper.h b/AI/VCAI/FuzzyHelper.h index 0989403aa..c82a91923 100644 --- a/AI/VCAI/FuzzyHelper.h +++ b/AI/VCAI/FuzzyHelper.h @@ -32,6 +32,7 @@ public: float evaluate(Goals::BuildBoat & g); float evaluate(Goals::GatherArmy & g); float evaluate(Goals::ClearWayTo & g); + float evaluate(Goals::CompleteQuest & g); float evaluate(Goals::Invalid & g); float evaluate(Goals::AbstractGoal & g); void setPriority(Goals::TSubgoal & g); diff --git a/AI/VCAI/Goals/AbstractGoal.cpp b/AI/VCAI/Goals/AbstractGoal.cpp index 2cecbc226..a2bae5f3b 100644 --- a/AI/VCAI/Goals/AbstractGoal.cpp +++ b/AI/VCAI/Goals/AbstractGoal.cpp @@ -40,8 +40,6 @@ std::string AbstractGoal::name() const //TODO: virtualize return "INVALID"; case WIN: return "WIN"; - case DO_NOT_LOSE: - return "DO NOT LOOSE"; case CONQUER: return "CONQUER"; case BUILD: @@ -95,8 +93,6 @@ std::string AbstractGoal::name() const //TODO: virtualize case GET_ART_TYPE: desc = "GET ARTIFACT OF TYPE " + VLC->arth->artifacts[aid]->Name(); break; - case ISSUE_COMMAND: - return "ISSUE COMMAND (unsupported)"; case VISIT_TILE: desc = "VISIT TILE " + tile.toString(); break; diff --git a/AI/VCAI/Goals/AbstractGoal.h b/AI/VCAI/Goals/AbstractGoal.h index d5ddf6a03..4151ef7c3 100644 --- a/AI/VCAI/Goals/AbstractGoal.h +++ b/AI/VCAI/Goals/AbstractGoal.h @@ -37,6 +37,35 @@ namespace Goals class ClearWayTo; class Invalid; class Trade; + class CompleteQuest; + class AdventureSpellCast; + + enum EGoals + { + INVALID = -1, + WIN, CONQUER, BUILD, //build needs to get a real reasoning + EXPLORE, GATHER_ARMY, + BOOST_HERO, + RECRUIT_HERO, + BUILD_STRUCTURE, //if hero set, then in visited town + COLLECT_RES, + GATHER_TROOPS, // val of creatures with objid + + VISIT_OBJ, //visit or defeat or collect the object + FIND_OBJ, //find and visit any obj with objid + resid //TODO: consider universal subid for various types (aid, bid) + VISIT_HERO, //heroes can move around - set goal abstract and track hero every turn + + GET_ART_TYPE, + + VISIT_TILE, //tile, in conjunction with hero elementar; assumes tile is reachable + CLEAR_WAY_TO, + DIG_AT_TILE,//elementar with hero on tile + BUY_ARMY, //at specific town + TRADE, //val resID at object objid + BUILD_BOAT, + COMPLETE_QUEST, + ADVENTURE_SPELL_CAST + }; class DLL_EXPORT TSubgoal : public std::shared_ptr { @@ -48,35 +77,6 @@ namespace Goals typedef std::vector TGoalVec; - enum EGoals - { - INVALID = -1, - WIN, DO_NOT_LOSE, CONQUER, BUILD, //build needs to get a real reasoning - EXPLORE, GATHER_ARMY, - BOOST_HERO, - RECRUIT_HERO, - BUILD_STRUCTURE, //if hero set, then in visited town - COLLECT_RES, - GATHER_TROOPS, // val of creatures with objid - - OBJECT_GOALS_BEGIN, - VISIT_OBJ, //visit or defeat or collect the object - FIND_OBJ, //find and visit any obj with objid + resid //TODO: consider universal subid for various types (aid, bid) - VISIT_HERO, //heroes can move around - set goal abstract and track hero every turn - - GET_ART_TYPE, - - //BUILD_STRUCTURE, - ISSUE_COMMAND, - - VISIT_TILE, //tile, in conjunction with hero elementar; assumes tile is reachable - CLEAR_WAY_TO, - DIG_AT_TILE,//elementar with hero on tile - BUY_ARMY, //at specific town - TRADE, //val resID at object objid - BUILD_BOAT - }; - //method chaining + clone pattern #define VSETTER(type, field) virtual AbstractGoal & set ## field(const type &rhs) {field = rhs; return *this;}; #define OSETTER(type, field) CGoal & set ## field(const type &rhs) override { field = rhs; return *this; }; diff --git a/AI/VCAI/Goals/AdventureSpellCast.cpp b/AI/VCAI/Goals/AdventureSpellCast.cpp new file mode 100644 index 000000000..cdac3873d --- /dev/null +++ b/AI/VCAI/Goals/AdventureSpellCast.cpp @@ -0,0 +1,63 @@ +/* +* AdventureSpellCast.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 "AdventureSpellCast.h" +#include "../VCAI.h" +#include "../FuzzyHelper.h" +#include "../AIhelper.h" +#include "../../lib/mapping/CMap.h" //for victory conditions +#include "../../lib/CPathfinder.h" + +extern boost::thread_specific_ptr cb; +extern boost::thread_specific_ptr ai; +extern FuzzyHelper * fh; + +using namespace Goals; + +bool AdventureSpellCast::operator==(const AdventureSpellCast & other) const +{ + return hero.h == other.hero.h; +} + +TSubgoal AdventureSpellCast::whatToDoToAchieve() +{ + if(!hero.validAndSet()) + throw cannotFulfillGoalException("Invalid hero!"); + + auto spell = spellID.toSpell(); + + logAi->trace("Decomposing adventure spell cast of %s for hero %s", spell->name, hero->name); + + if(!spell->isAdventureSpell()) + throw cannotFulfillGoalException(spell->name + " is not an adventure spell."); + + if(!vstd::contains(hero->spells, spellID)) + throw cannotFulfillGoalException("Hero has no " + spell->name); + + if(hero->mana < hero->getSpellCost(spellID.toSpell())) + throw cannotFulfillGoalException("Hero has not enough mana to cast " + spell->name); + + return iAmElementar(); +} + +void AdventureSpellCast::accept(VCAI * ai) +{ + cb->castSpell(hero.h, spellID, tile); +} + +std::string AdventureSpellCast::name() const +{ + return "AdventureSpellCast " + spellID.toSpell()->name; +} + +std::string AdventureSpellCast::completeMessage() const +{ + return "Spell casted successfully " + spellID.toSpell()->name; +} diff --git a/AI/VCAI/Goals/AdventureSpellCast.h b/AI/VCAI/Goals/AdventureSpellCast.h new file mode 100644 index 000000000..0d283cebd --- /dev/null +++ b/AI/VCAI/Goals/AdventureSpellCast.h @@ -0,0 +1,39 @@ +/* +* AdventureSpellCast.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 "CGoal.h" + +namespace Goals +{ + class DLL_EXPORT AdventureSpellCast : public CGoal + { + private: + SpellID spellID; + + public: + AdventureSpellCast(HeroPtr hero, SpellID spellID) + : CGoal(Goals::ADVENTURE_SPELL_CAST), spellID(spellID) + { + sethero(hero); + } + + TGoalVec getAllPossibleSubgoals() override + { + return TGoalVec(); + } + + TSubgoal whatToDoToAchieve() override; + void accept(VCAI * ai) override; + std::string name() const override; + std::string completeMessage() const override; + virtual bool operator==(const AdventureSpellCast & other) const override; + }; +} diff --git a/AI/VCAI/Goals/CGoal.h b/AI/VCAI/Goals/CGoal.h index bf6761be2..6a8876cbf 100644 --- a/AI/VCAI/Goals/CGoal.h +++ b/AI/VCAI/Goals/CGoal.h @@ -60,11 +60,13 @@ namespace Goals { return new T(static_cast(*this)); //casting enforces template instantiation } - TSubgoal iAmElementar() + TSubgoal iAmElementar() const { - setisElementar(true); //FIXME: it's not const-correct, maybe we shoudl only set returned clone? TSubgoal ptr; + ptr.reset(clone()); + ptr->setisElementar(true); + return ptr; } template void serialize(Handler & h, const int version) @@ -79,7 +81,7 @@ namespace Goals if(goalType != g.goalType) return false; - return (*this) == (dynamic_cast(g)); + return (*this) == (static_cast(g)); } virtual bool operator==(const T & other) const = 0; diff --git a/AI/VCAI/Goals/CompleteQuest.cpp b/AI/VCAI/Goals/CompleteQuest.cpp new file mode 100644 index 000000000..3ae742a26 --- /dev/null +++ b/AI/VCAI/Goals/CompleteQuest.cpp @@ -0,0 +1,276 @@ +/* +* CompleteQuest.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 "Goals.h" +#include "../VCAI.h" +#include "../FuzzyHelper.h" +#include "../AIhelper.h" +#include "../../lib/mapping/CMap.h" //for victory conditions +#include "../../lib/CPathfinder.h" + +extern boost::thread_specific_ptr cb; +extern boost::thread_specific_ptr ai; +extern FuzzyHelper * fh; + +using namespace Goals; + +bool CompleteQuest::operator==(const CompleteQuest & other) const +{ + return q.quest->qid == other.q.quest->qid; +} + +TGoalVec CompleteQuest::getAllPossibleSubgoals() +{ + TGoalVec solutions; + + if(q.quest->missionType && q.quest->progress != CQuest::COMPLETE) + { + logAi->debug("Trying to realize quest: %s", questToString()); + + switch(q.quest->missionType) + { + case CQuest::MISSION_ART: + return missionArt(); + + case CQuest::MISSION_HERO: + return missionHero(); + + case CQuest::MISSION_ARMY: + return missionArmy(); + + case CQuest::MISSION_RESOURCES: + return missionResources(); + + case CQuest::MISSION_KILL_HERO: + case CQuest::MISSION_KILL_CREATURE: + return missionDestroyObj(); + + case CQuest::MISSION_PRIMARY_STAT: + return missionIncreasePrimaryStat(); + + case CQuest::MISSION_LEVEL: + return missionLevel(); + + case CQuest::MISSION_PLAYER: + if(ai->playerID.getNum() != q.quest->m13489val) + logAi->debug("Can't be player of color %d", q.quest->m13489val); + + break; + + case CQuest::MISSION_KEYMASTER: + return missionKeymaster(); + + } //end of switch + } + + return TGoalVec(); +} + +TSubgoal CompleteQuest::whatToDoToAchieve() +{ + if(q.quest->missionType == CQuest::MISSION_NONE) + { + throw cannotFulfillGoalException("Can not complete inactive quest"); + } + + TGoalVec solutions = getAllPossibleSubgoals(); + + if(solutions.empty()) + throw cannotFulfillGoalException("Can not complete quest " + questToString()); + + TSubgoal result = fh->chooseSolution(solutions); + + logAi->trace( + "Returning %s, tile: %s, objid: %d, hero: %s", + result->name(), + result->tile.toString(), + result->objid, + result->hero.validAndSet() ? result->hero->name : "not specified"); + + return result; +} + +std::string CompleteQuest::name() const +{ + return "CompleteQuest"; +} + +std::string CompleteQuest::completeMessage() const +{ + return "Completed quest " + questToString(); +} + +std::string CompleteQuest::questToString() const +{ + if(q.quest->missionType == CQuest::MISSION_NONE) + return "inactive quest"; + + MetaString ms; + q.quest->getRolloverText(ms, false); + + return ms.toString(); +} + +TGoalVec CompleteQuest::tryCompleteQuest() const +{ + TGoalVec solutions; + + auto heroes = cb->getHeroesInfo(); //TODO: choose best / free hero from among many possibilities? + + for(auto hero : heroes) + { + if(q.quest->checkQuest(hero)) + { + vstd::concatenate(solutions, ai->ah->howToVisitObj(hero, ObjectIdRef(q.obj->id))); + } + } + + return solutions; +} + +TGoalVec CompleteQuest::missionArt() const +{ + TGoalVec solutions = tryCompleteQuest(); + + if(!solutions.empty()) + return solutions; + + for(auto art : q.quest->m5arts) + { + solutions.push_back(sptr(GetArtOfType(art))); //TODO: transport? + } + + return solutions; +} + +TGoalVec CompleteQuest::missionHero() const +{ + TGoalVec solutions = tryCompleteQuest(); + + if(solutions.empty()) + { + //rule of a thumb - quest heroes usually are locked in prisons + solutions.push_back(sptr(FindObj(Obj::PRISON))); + } + + return solutions; +} + +TGoalVec CompleteQuest::missionArmy() const +{ + TGoalVec solutions = tryCompleteQuest(); + + if(!solutions.empty()) + return solutions; + + for(auto creature : q.quest->m6creatures) + { + solutions.push_back(sptr(GatherTroops(creature.type->idNumber, creature.count))); + } + + return solutions; +} + +TGoalVec CompleteQuest::missionIncreasePrimaryStat() const +{ + TGoalVec solutions = tryCompleteQuest(); + + if(solutions.empty()) + { + for(int i = 0; i < q.quest->m2stats.size(); ++i) + { + // TODO: library, school and other boost objects + logAi->debug("Don't know how to increase primary stat %d", i); + } + } + + return solutions; +} + +TGoalVec CompleteQuest::missionLevel() const +{ + TGoalVec solutions = tryCompleteQuest(); + + if(solutions.empty()) + { + logAi->debug("Don't know how to reach hero level %d", q.quest->m13489val); + } + + return solutions; +} + +TGoalVec CompleteQuest::missionKeymaster() const +{ + TGoalVec solutions = tryCompleteQuest(); + + if(solutions.empty()) + { + solutions.push_back(sptr(Goals::FindObj(Obj::KEYMASTER, q.obj->subID))); + } + + return solutions; +} + +TGoalVec CompleteQuest::missionResources() const +{ + TGoalVec solutions; + + auto heroes = cb->getHeroesInfo(); //TODO: choose best / free hero from among many possibilities? + + if(heroes.size()) + { + if(q.quest->checkQuest(heroes.front())) //it doesn't matter which hero it is + { + return ai->ah->howToVisitObj(q.obj); + } + else + { + for(int i = 0; i < q.quest->m7resources.size(); ++i) + { + if(q.quest->m7resources[i]) + solutions.push_back(sptr(CollectRes(i, q.quest->m7resources[i]))); + } + } + } + else + { + solutions.push_back(sptr(Goals::RecruitHero())); //FIXME: checkQuest requires any hero belonging to player :( + } + + return solutions; +} + +TGoalVec CompleteQuest::missionDestroyObj() const +{ + TGoalVec solutions; + + auto obj = cb->getObjByQuestIdentifier(q.quest->m13489val); + + if(!obj) + return ai->ah->howToVisitObj(q.obj); + + if(obj->ID == Obj::HERO) + { + auto relations = cb->getPlayerRelations(ai->playerID, obj->tempOwner); + + if(relations == PlayerRelations::SAME_PLAYER) + { + auto heroToProtect = cb->getHero(obj->id); + + solutions.push_back(sptr(GatherArmy().sethero(heroToProtect))); + } + else if(relations == PlayerRelations::ENEMIES) + { + solutions = ai->ah->howToVisitObj(obj); + } + } + + return solutions; +} \ No newline at end of file diff --git a/AI/VCAI/Goals/CompleteQuest.h b/AI/VCAI/Goals/CompleteQuest.h new file mode 100644 index 000000000..4a4c66009 --- /dev/null +++ b/AI/VCAI/Goals/CompleteQuest.h @@ -0,0 +1,46 @@ +/* +* CompleteQuest.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 "CGoal.h" +#include "../../../lib/VCMI_Lib.h" + +namespace Goals +{ + class DLL_EXPORT CompleteQuest : public CGoal + { + private: + const QuestInfo q; + + public: + CompleteQuest(const QuestInfo quest) + : CGoal(Goals::COMPLETE_QUEST), q(quest) + { + } + + TGoalVec getAllPossibleSubgoals() override; + TSubgoal whatToDoToAchieve() override; + std::string name() const override; + std::string completeMessage() const override; + virtual bool operator==(const CompleteQuest & other) const override; + + private: + TGoalVec tryCompleteQuest() const; + TGoalVec missionArt() const; + TGoalVec missionHero() const; + TGoalVec missionArmy() const; + TGoalVec missionResources() const; + TGoalVec missionDestroyObj() const; + TGoalVec missionIncreasePrimaryStat() const; + TGoalVec missionLevel() const; + TGoalVec missionKeymaster() const; + std::string questToString() const; + }; +} diff --git a/AI/VCAI/Goals/Conquer.cpp b/AI/VCAI/Goals/Conquer.cpp index 33f46d3f4..8ac127dc1 100644 --- a/AI/VCAI/Goals/Conquer.cpp +++ b/AI/VCAI/Goals/Conquer.cpp @@ -33,6 +33,8 @@ bool Conquer::operator==(const Conquer & other) const TSubgoal Conquer::whatToDoToAchieve() { + logAi->trace("Entering goal CONQUER"); + return fh->chooseSolution(getAllPossibleSubgoals()); } diff --git a/AI/VCAI/Goals/GatherTroops.cpp b/AI/VCAI/Goals/GatherTroops.cpp index 580a7af86..59242e5b7 100644 --- a/AI/VCAI/Goals/GatherTroops.cpp +++ b/AI/VCAI/Goals/GatherTroops.cpp @@ -48,6 +48,8 @@ int GatherTroops::getCreaturesCount(const CArmedInstance * army) TSubgoal GatherTroops::whatToDoToAchieve() { + logAi->trace("Entering GatherTroops::whatToDoToAchieve"); + auto heroes = cb->getHeroesInfo(true); for(auto hero : heroes) diff --git a/AI/VCAI/Goals/Goals.h b/AI/VCAI/Goals/Goals.h index 7e1f32909..885677ca3 100644 --- a/AI/VCAI/Goals/Goals.h +++ b/AI/VCAI/Goals/Goals.h @@ -29,4 +29,6 @@ #include "GetArtOfType.h" #include "ClearWayTo.h" #include "DigAtTile.h" -#include "FindObj.h" \ No newline at end of file +#include "FindObj.h" +#include "CompleteQuest.h" +#include "AdventureSpellCast.h" \ No newline at end of file diff --git a/AI/VCAI/Pathfinding/AINodeStorage.cpp b/AI/VCAI/Pathfinding/AINodeStorage.cpp index 9fb3a0369..6192a721b 100644 --- a/AI/VCAI/Pathfinding/AINodeStorage.cpp +++ b/AI/VCAI/Pathfinding/AINodeStorage.cpp @@ -82,6 +82,7 @@ void AINodeStorage::resetTile(const int3 & coord, EPathfindingLayer layer, CGPat heroNode.chainMask = 0; heroNode.danger = 0; + heroNode.manaCost = 0; heroNode.specialAction.reset(); heroNode.update(coord, layer, accessibility); } @@ -97,6 +98,12 @@ void AINodeStorage::commit(CDestinationNodeInfo & destination, const PathNodeInf dstNode->danger = srcNode->danger; dstNode->action = destination.action; dstNode->theNodeBefore = srcNode->theNodeBefore; + dstNode->manaCost = srcNode->manaCost; + + if(dstNode->specialAction) + { + dstNode->specialAction->applyOnDestination(getHero(), destination, source, dstNode, srcNode); + } }); } @@ -186,6 +193,7 @@ std::vector AINodeStorage::getChainInfo(int3 pos, bool isOnLand) const { std::vector paths; auto chains = nodes[pos.x][pos.y][pos.z][isOnLand ? EPathfindingLayer::LAND : EPathfindingLayer::SAIL]; + auto initialPos = hero->visitablePos(); for(const AIPathNode & node : chains) { @@ -197,7 +205,7 @@ std::vector AINodeStorage::getChainInfo(int3 pos, bool isOnLand) const AIPath path; const AIPathNode * current = &node; - while(current != nullptr) + while(current != nullptr && current->coord != initialPos) { AIPathNodeInfo pathNode; diff --git a/AI/VCAI/Pathfinding/AINodeStorage.h b/AI/VCAI/Pathfinding/AINodeStorage.h index 2d72cdb19..f797c04c5 100644 --- a/AI/VCAI/Pathfinding/AINodeStorage.h +++ b/AI/VCAI/Pathfinding/AINodeStorage.h @@ -15,16 +15,28 @@ #include "../AIUtility.h" #include "../Goals/AbstractGoal.h" +class AIPathNode; + class ISpecialAction { public: virtual Goals::TSubgoal whatToDo(HeroPtr hero) const = 0; + + virtual void applyOnDestination( + HeroPtr hero, + CDestinationNodeInfo & destination, + const PathNodeInfo & source, + AIPathNode * dstMode, + const AIPathNode * srcNode) const + { + } }; struct AIPathNode : public CGPathNode { uint32_t chainMask; uint64_t danger; + uint32_t manaCost; std::shared_ptr specialAction; }; diff --git a/AI/VCAI/Pathfinding/AIPathfinder.cpp b/AI/VCAI/Pathfinding/AIPathfinder.cpp index 506a689b7..1e3e37eb2 100644 --- a/AI/VCAI/Pathfinding/AIPathfinder.cpp +++ b/AI/VCAI/Pathfinding/AIPathfinder.cpp @@ -48,10 +48,10 @@ std::vector AIPathfinder::getPathInfo(HeroPtr hero, int3 tile) } storageMap[hero] = nodeStorage; + nodeStorage->setHero(hero.get()); auto config = std::make_shared(cb, ai, nodeStorage); - nodeStorage->setHero(hero.get()); cb->calculatePaths(config, hero.get()); } else diff --git a/AI/VCAI/Pathfinding/AIPathfinderConfig.cpp b/AI/VCAI/Pathfinding/AIPathfinderConfig.cpp index 67a7a4e19..99628b033 100644 --- a/AI/VCAI/Pathfinding/AIPathfinderConfig.cpp +++ b/AI/VCAI/Pathfinding/AIPathfinderConfig.cpp @@ -16,14 +16,31 @@ namespace AIPathfinding { - class BuildBoatAction : public ISpecialAction + class VirtualBoatAction : public ISpecialAction + { + private: + uint64_t specialChain; + + public: + VirtualBoatAction(uint64_t specialChain) + :specialChain(specialChain) + { + } + + uint64_t getSpecialChain() const + { + return specialChain; + } + }; + + class BuildBoatAction : public VirtualBoatAction { private: const IShipyard * shipyard; public: BuildBoatAction(const IShipyard * shipyard) - :shipyard(shipyard) + :VirtualBoatAction(AINodeStorage::RESOURCE_CHAIN), shipyard(shipyard) { } @@ -33,6 +50,51 @@ namespace AIPathfinding } }; + class SummonBoatAction : public VirtualBoatAction + { + public: + SummonBoatAction() + :VirtualBoatAction(AINodeStorage::CAST_CHAIN) + { + } + + virtual Goals::TSubgoal whatToDo(HeroPtr hero) const override + { + return sptr(Goals::AdventureSpellCast(hero, SpellID::SUMMON_BOAT)); + } + + virtual void applyOnDestination( + HeroPtr hero, + CDestinationNodeInfo & destination, + const PathNodeInfo & source, + AIPathNode * dstMode, + const AIPathNode * srcNode) const override + { + dstMode->manaCost = srcNode->manaCost + getManaCost(hero); + dstMode->theNodeBefore = source.node; + } + + bool isAffordableBy(HeroPtr hero, const AIPathNode * source) const + { + logAi->trace( + "Hero %s has %d mana and needed %d and already spent %d", + hero->name, + hero->mana, + getManaCost(hero), + source->manaCost); + + return hero->mana >= source->manaCost + getManaCost(hero); + } + + private: + uint32_t getManaCost(HeroPtr hero) const + { + SpellID summonBoat = SpellID::SUMMON_BOAT; + + return hero->getSpellCost(summonBoat.toSpell()); + } + }; + class BattleAction : public ISpecialAction { private: @@ -58,6 +120,7 @@ namespace AIPathfinding VCAI * ai; std::map> virtualBoats; std::shared_ptr nodeStorage; + std::shared_ptr summonableVirtualBoat; public: AILayerTransitionRule(CPlayerSpecificInfoCallback * cb, VCAI * ai, std::shared_ptr nodeStorage) @@ -79,37 +142,14 @@ namespace AIPathfinding return; } - if(source.node->layer == EPathfindingLayer::LAND && destination.node->layer == EPathfindingLayer::SAIL - && vstd::contains(virtualBoats, destination.coord)) + if(source.node->layer == EPathfindingLayer::LAND && destination.node->layer == EPathfindingLayer::SAIL) { - logAi->trace("Bypassing virtual boat at %s!", destination.coord.toString()); + std::shared_ptr virtualBoat = findVirtualBoat(destination, source); - nodeStorage->updateAINode(destination.node, [&](AIPathNode * node) + if(virtualBoat && tryEmbarkVirtualBoat(destination, source, virtualBoat)) { - std::shared_ptr virtualBoat = virtualBoats.at(destination.coord); - - auto boatNodeOptional = nodeStorage->getOrCreateNode( - node->coord, - node->layer, - node->chainMask | AINodeStorage::RESOURCE_CHAIN); - - if(boatNodeOptional) - { - AIPathNode * boatNode = boatNodeOptional.get(); - - boatNode->specialAction = virtualBoat; - destination.blocked = false; - destination.action = CGPathNode::ENodeAction::EMBARK; - destination.node = boatNode; - } - else - { - logAi->trace( - "Can not allocate boat node while moving %s -> %s", - source.coord.toString(), - destination.coord.toString()); - } - }); + logAi->trace("Embarking to virtual boat while moving %s -> %s!", source.coord.toString(), destination.coord.toString()); + } } } @@ -142,6 +182,84 @@ namespace AIPathfinding logAi->debug("Virtual boat added at %s", boatLocation.toString()); } } + + auto hero = nodeStorage->getHero(); + + if(vstd::contains(hero->spells, SpellID::SUMMON_BOAT)) + { + auto summonBoatSpell = SpellID(SpellID::SUMMON_BOAT).toSpell(); + + if(hero->getSpellSchoolLevel(summonBoatSpell) == SecSkillLevel::EXPERT) + { + summonableVirtualBoat.reset(new SummonBoatAction()); + } + } + } + + std::shared_ptr findVirtualBoat( + CDestinationNodeInfo &destination, + const PathNodeInfo &source) const + { + std::shared_ptr virtualBoat; + + if(vstd::contains(virtualBoats, destination.coord)) + { + virtualBoat = virtualBoats.at(destination.coord); + } + else if( + summonableVirtualBoat + && summonableVirtualBoat->isAffordableBy(nodeStorage->getHero(), nodeStorage->getAINode(source.node))) + { + virtualBoat = summonableVirtualBoat; + } + + return virtualBoat; + } + + bool tryEmbarkVirtualBoat( + CDestinationNodeInfo &destination, + const PathNodeInfo &source, + std::shared_ptr virtualBoat) const + { + bool result = false; + + nodeStorage->updateAINode(destination.node, [&](AIPathNode * node) + { + auto boatNodeOptional = nodeStorage->getOrCreateNode( + node->coord, + node->layer, + node->chainMask | virtualBoat->getSpecialChain()); + + if(boatNodeOptional) + { + AIPathNode * boatNode = boatNodeOptional.get(); + + if(boatNode->action == CGPathNode::NOT_SET) + { + boatNode->specialAction = virtualBoat; + destination.blocked = false; + destination.action = CGPathNode::ENodeAction::EMBARK; + destination.node = boatNode; + result = true; + } + else + { + logAi->trace( + "Special transition node already allocated. Blocked moving %s -> %s", + source.coord.toString(), + destination.coord.toString()); + } + } + else + { + logAi->trace( + "Can not allocate special transition node while moving %s -> %s", + source.coord.toString(), + destination.coord.toString()); + } + }); + + return result; } }; diff --git a/AI/VCAI/Pathfinding/PathfindingManager.cpp b/AI/VCAI/Pathfinding/PathfindingManager.cpp index 552fc2439..7fa158090 100644 --- a/AI/VCAI/Pathfinding/PathfindingManager.cpp +++ b/AI/VCAI/Pathfinding/PathfindingManager.cpp @@ -11,7 +11,7 @@ #include "PathfindingManager.h" #include "AIPathfinder.h" #include "AIPathfinderConfig.h" -#include "Goals/Goals.h" +#include "../Goals/Goals.h" #include "../../../lib/CGameInfoCallback.h" #include "../../../lib/mapping/CMap.h" @@ -130,8 +130,6 @@ Goals::TGoalVec PathfindingManager::findPath( if(isSafeToVisit(hero, danger)) { - logAi->trace("It's safe for %s to visit tile %s with danger %s", hero->name, dest.toString(), std::to_string(danger)); - Goals::TSubgoal solution; if(path.specialAction) @@ -153,6 +151,8 @@ Goals::TGoalVec PathfindingManager::findPath( solution->evaluationContext.movementCost += path.movementCost(); + logAi->trace("It's safe for %s to visit tile %s with danger %s, goal %s", hero->name, dest.toString(), std::to_string(danger), solution->name()); + result.push_back(solution); continue; @@ -212,11 +212,15 @@ Goals::TSubgoal PathfindingManager::clearWayTo(HeroPtr hero, int3 firstTileToGet return sptr(Goals::VisitObj(topObj->id.getNum()).sethero(hero)); } - //TODO: we should be able to return apriopriate quest here - //ret.push_back(ai->questToGoal()); - //however, visiting obj for firts time will give us quest - //do not access quets guard if we can't complete the quest - logAi->trace("Can not visit this quest guard! Not ready!"); + auto questObj = dynamic_cast(topObj); + + if(questObj) + { + auto questInfo = QuestInfo(questObj->quest, topObj, topObj->visitablePos()); + + return sptr(Goals::CompleteQuest(questInfo)); + } + return sptr(Goals::Invalid()); } } diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index 10a679d86..ff7bcb15d 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -873,7 +873,7 @@ void VCAI::mainLoop() basicGoals.push_back(ah->whatToDo()); for (auto quest : myCb->getMyQuests()) { - basicGoals.push_back(questToGoal(quest)); + basicGoals.push_back(sptr(Goals::CompleteQuest(quest))); } basicGoals.push_back(sptr(Goals::Build())); @@ -1013,11 +1013,13 @@ void VCAI::mainLoop() //remove goals we couldn't decompose for (auto goal : goalsToRemove) vstd::erase_if_present(basicGoals, goal); + //add abstract goals boost::sort(goalsToAdd, [](const Goals::TSubgoal & lhs, const Goals::TSubgoal & rhs) -> bool { return lhs->priority > rhs->priority; //highest priority at the beginning }); + //max number of goals = 10 int i = 0; while (basicGoals.size() < 10 && goalsToAdd.size() > i) @@ -2434,170 +2436,6 @@ Goals::TSubgoal VCAI::decomposeGoal(Goals::TSubgoal ultimateGoal) return abstractGoal; } -Goals::TSubgoal VCAI::questToGoal(const QuestInfo & q) -{ - Goals::TSubgoal result = sptr(Goals::Invalid()); - - if (q.quest->missionType && q.quest->progress != CQuest::COMPLETE) - { - MetaString ms; - q.quest->getRolloverText(ms, false); - logAi->debug("Trying to realize quest: %s", ms.toString()); - auto heroes = cb->getHeroesInfo(); //TODO: choose best / free hero from among many possibilities? - - switch (q.quest->missionType) - { - case CQuest::MISSION_ART: - { - for (auto hero : heroes) - { - if (q.quest->checkQuest(hero)) - { - return sptr(Goals::VisitObj(q.obj->id.getNum()).sethero(hero)); - } - } - for (auto art : q.quest->m5arts) - { - return sptr(Goals::GetArtOfType(art)); //TODO: transport? - } - break; - } - case CQuest::MISSION_HERO: - { - //striveToGoal (CGoal(RECRUIT_HERO)); - for (auto hero : heroes) - { - if (q.quest->checkQuest(hero)) - { - return sptr(Goals::VisitObj(q.obj->id.getNum()).sethero(hero)); - } - } - return sptr(Goals::FindObj(Obj::PRISON)); //rule of a thumb - quest heroes usually are locked in prisons - //BNLOG ("Don't know how to recruit hero with id %d\n", q.quest->m13489val); - } - case CQuest::MISSION_ARMY: - { - for (auto hero : heroes) - { - if (q.quest->checkQuest(hero)) //very bad info - stacks can be split between multiple heroes :( - { - result = sptr(Goals::VisitObj(q.obj->id.getNum()).sethero(hero)); - break; - } - } - - if(result->invalid()) - { - for(auto creature : q.quest->m6creatures) - { - result = sptr(Goals::GatherTroops(creature.type->idNumber, creature.count)); - break; - } - } - //TODO: exchange armies... oh my - //BNLOG ("Don't know how to recruit %d of %s\n", (int)(creature.count) % creature.type->namePl); - break; - } - case CQuest::MISSION_RESOURCES: - { - if (heroes.size()) - { - if (q.quest->checkQuest(heroes.front())) //it doesn't matter which hero it is - { - return sptr(Goals::VisitObj(q.obj->id.getNum())); - } - else - { - for (int i = 0; i < q.quest->m7resources.size(); ++i) - { - if (q.quest->m7resources[i]) - return sptr(Goals::CollectRes(i, q.quest->m7resources[i])); - } - } - } - else - return sptr(Goals::RecruitHero()); //FIXME: checkQuest requires any hero belonging to player :( - break; - } - case CQuest::MISSION_KILL_HERO: - case CQuest::MISSION_KILL_CREATURE: - { - auto obj = cb->getObjByQuestIdentifier(q.quest->m13489val); - - if(!obj) - return sptr(Goals::VisitObj(q.obj->id.getNum())); //visit seer hut - - if(obj->ID == Obj::HERO) - { - auto relations = myCb->getPlayerRelations(playerID, obj->tempOwner); - - if(relations == PlayerRelations::SAME_PLAYER) - { - auto heroToProtect = cb->getHero(obj->id); - - return sptr(Goals::GatherArmy().sethero(heroToProtect)); - } - else if(relations == PlayerRelations::ALLIES) - { - break; - } - } - - return sptr(Goals::VisitObj(obj->id.getNum())); - } - case CQuest::MISSION_PRIMARY_STAT: - { - auto heroes = cb->getHeroesInfo(); - for (auto hero : heroes) - { - if (q.quest->checkQuest(hero)) - { - return sptr(Goals::VisitObj(q.obj->id.getNum()).sethero(hero)); - } - } - for (int i = 0; i < q.quest->m2stats.size(); ++i) - { - logAi->debug("Don't know how to increase primary stat %d", i); - } - break; - } - case CQuest::MISSION_LEVEL: - { - auto heroes = cb->getHeroesInfo(); - for (auto hero : heroes) - { - if (q.quest->checkQuest(hero)) - { - return sptr(Goals::VisitObj(q.obj->id.getNum()).sethero(hero)); //TODO: causes infinite loop :/ - } - } - logAi->debug("Don't know how to reach hero level %d", q.quest->m13489val); - break; - } - case CQuest::MISSION_PLAYER: - { - if (playerID.getNum() != q.quest->m13489val) - logAi->debug("Can't be player of color %d", q.quest->m13489val); - break; - } - case CQuest::MISSION_KEYMASTER: - { - return sptr(Goals::FindObj(Obj::KEYMASTER, q.obj->subID)); - break; - } - } //end of switch - } - - logAi->trace( - "Returning %s, tile: %s, objid: %d, hero: %s", - result->name(), - result->tile.toString(), - result->objid, - result->hero.validAndSet() ? result->hero->name : "not specified"); - - return result; -} - void VCAI::performTypicalActions() { for(auto h : getUnblockedHeroes()) diff --git a/AI/VCAI/VCAI.h b/AI/VCAI/VCAI.h index 2e7369fdf..efccea628 100644 --- a/AI/VCAI/VCAI.h +++ b/AI/VCAI/VCAI.h @@ -210,7 +210,6 @@ public: void setGoal(HeroPtr h, Goals::TSubgoal goal); void evaluateGoal(HeroPtr h); //evaluates goal assigned to hero, if any void completeGoal(Goals::TSubgoal goal); //safely removes goal from reserved hero - Goals::TSubgoal questToGoal(const QuestInfo & q); void recruitHero(const CGTownInstance * t, bool throwing = false); bool isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, boost::optional movementCostLimit = boost::none);