diff --git a/AI/VCAI/AIUtility.cpp b/AI/VCAI/AIUtility.cpp index de3a83efd..a87c59c5e 100644 --- a/AI/VCAI/AIUtility.cpp +++ b/AI/VCAI/AIUtility.cpp @@ -126,6 +126,11 @@ const CGHeroInstance * HeroPtr::operator*() const return get(); } +bool HeroPtr::operator==(const HeroPtr & rhs) const +{ + return h == rhs.get(true); +} + void foreach_tile_pos(std::function foo) { // some micro-optimizations since this function gets called a LOT @@ -475,6 +480,50 @@ void getVisibleNeighbours(const std::vector & tiles, std::vector & o } } +creInfo infoFromDC(const dwellingContent & dc) +{ + creInfo ci; + ci.count = dc.first; + ci.creID = dc.second.size() ? dc.second.back() : CreatureID(-1); //should never be accessed + if (ci.creID != -1) + { + ci.cre = VLC->creh->creatures[ci.creID]; + ci.level = ci.cre->level; //this is cretaure tier, while tryRealize expects dwelling level. Ignore. + } + else + { + ci.cre = nullptr; + ci.level = 0; + } + return ci; +} + +ui64 howManyReinforcementsCanBuy(HeroPtr h, const CGTownInstance * t) +{ + ui64 aivalue = 0; + + int freeHeroSlots = GameConstants::ARMY_SIZE - h->stacksCount(); + for (auto const dc : t->creatures) + { + creInfo ci = infoFromDC(dc); + if (ci.count && ci.creID != -1) //valid creature at this level + { + //can be merged with another stack? + SlotID dst = h->getSlotFor(ci.creID); + if (!h->hasStackAtSlot(dst)) //need another new slot for this stack + if (!freeHeroSlots) //no more place for stacks + continue; + else + freeHeroSlots--; //new slot will be occupied + + //we found matching occupied or free slot + aivalue += ci.count * ci.cre->AIValue; + } + } + + return aivalue; +} + ui64 howManyReinforcementsCanGet(HeroPtr h, const CGTownInstance * t) { ui64 ret = 0; diff --git a/AI/VCAI/AIUtility.h b/AI/VCAI/AIUtility.h index dbf45c84b..eacbafbf6 100644 --- a/AI/VCAI/AIUtility.h +++ b/AI/VCAI/AIUtility.h @@ -20,9 +20,11 @@ #include "../../lib/CPathfinder.h" class CCallback; +struct creInfo; typedef const int3 & crint3; typedef const std::string & crstring; +typedef std::pair> dwellingContent; const int GOLD_MINE_PRODUCTION = 1000, WOOD_ORE_MINE_PRODUCTION = 2, RESOURCE_MINE_PRODUCTION = 1; const int ACTUAL_RESOURCE_COUNT = 7; @@ -35,7 +37,7 @@ extern const int GOLD_RESERVE; //provisional class for AI to store a reference to an owned hero object //checks if it's valid on access, should be used in place of const CGHeroInstance* -struct HeroPtr +struct DLL_EXPORT HeroPtr { const CGHeroInstance * h; ObjectInstanceID hid; @@ -56,6 +58,7 @@ public: bool operator<(const HeroPtr & rhs) const; const CGHeroInstance * operator->() const; const CGHeroInstance * operator*() const; //not that consistent with -> but all interfaces use CGHeroInstance*, so it's convenient + bool operator==(const HeroPtr & rhs) const; const CGHeroInstance * get(bool doWeExpectNull = false) const; bool validAndSet() const; @@ -137,6 +140,15 @@ bool objWithID(const CGObjectInstance * obj) return obj->ID == id; } +struct creInfo +{ + int count; + CreatureID creID; + CCreature * cre; + int level; +}; +creInfo infoFromDC(const dwellingContent & dc); + void foreach_tile_pos(std::function foo); void foreach_tile_pos(CCallback * cbp, std::function foo); // avoid costly retrieval of thread-specific pointer void foreach_neighbour(const int3 & pos, std::function foo); @@ -160,6 +172,7 @@ bool compareMovement(HeroPtr lhs, HeroPtr rhs); bool compareHeroStrength(HeroPtr h1, HeroPtr h2); bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2); bool compareArtifacts(const CArtifactInstance * a1, const CArtifactInstance * a2); +ui64 howManyReinforcementsCanBuy(HeroPtr h, const CGTownInstance * t); ui64 howManyReinforcementsCanGet(HeroPtr h, const CGTownInstance * t); int3 whereToExplore(HeroPtr h); @@ -173,4 +186,4 @@ public: { } bool operator()(const CGObjectInstance * lhs, const CGObjectInstance * rhs); -}; +}; \ No newline at end of file diff --git a/AI/VCAI/AIhelper.cpp b/AI/VCAI/AIhelper.cpp new file mode 100644 index 000000000..fac645f2d --- /dev/null +++ b/AI/VCAI/AIhelper.cpp @@ -0,0 +1,84 @@ +/* +* AIhelper.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 +* +*/ +#include "StdInc.h" + +#include "AIhelper.h" +#include "ResourceManager.h" + +boost::thread_specific_ptr ah; + +AIhelper::AIhelper() +{ + resourceManager.reset(new ResourceManager); +} + +AIhelper::~AIhelper() +{ +} + +bool AIhelper::notifyGoalCompleted(Goals::TSubgoal goal) +{ + return resourceManager->notifyGoalCompleted(goal); +} + +void AIhelper::setCB(CPlayerSpecificInfoCallback * CB) +{ + resourceManager->setCB(CB); +} + +void AIhelper::setAI(VCAI * AI) +{ + resourceManager->setAI(AI); +} + +Goals::TSubgoal AIhelper::whatToDo(TResources & res, Goals::TSubgoal goal) +{ + return resourceManager->whatToDo(res, goal); +} + +Goals::TSubgoal AIhelper::whatToDo() const +{ + return resourceManager->whatToDo(); +} + +bool AIhelper::hasTasksLeft() const +{ + return resourceManager->hasTasksLeft(); +} + +bool AIhelper::canAfford(const TResources & cost) const +{ + return resourceManager->canAfford(cost); +} + +TResources AIhelper::reservedResources() const +{ + return resourceManager->reservedResources(); +} + +TResources AIhelper::freeResources() const +{ + return resourceManager->freeResources(); +} + +TResource AIhelper::freeGold() const +{ + return resourceManager->freeGold(); +} + +TResources AIhelper::allResources() const +{ + return resourceManager->allResources(); +} + +TResource AIhelper::allGold() const +{ + return resourceManager->allGold(); +} diff --git a/AI/VCAI/AIhelper.h b/AI/VCAI/AIhelper.h new file mode 100644 index 000000000..052c4163b --- /dev/null +++ b/AI/VCAI/AIhelper.h @@ -0,0 +1,52 @@ +/* +* AIhelper.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 + +/* + !!! Note: Include THIS file at the end of include list to avoid "undefined base class" error +*/ + +#include "ResourceManager.h" + +class ResourceManager; + +//indirection interface for various modules +class DLL_EXPORT AIhelper : public IResourceManager +{ + friend class VCAI; + friend struct SetGlobalState; //mess? + + //members are thread_specific. AIhelper is global + std::shared_ptr resourceManager; + std::shared_ptr ai; +public: + AIhelper(); + ~AIhelper(); + + //TODO: consider common interface with Resource Manager? + bool canAfford(const TResources & cost) const; + TResources reservedResources() const override; + TResources freeResources() const override; + TResource freeGold() const override; + TResources allResources() const override; + TResource allGold() const override; + + Goals::TSubgoal whatToDo(TResources &res, Goals::TSubgoal goal) override; + Goals::TSubgoal whatToDo() const override; //peek highest-priority goal + bool hasTasksLeft() const override; + +private: + bool notifyGoalCompleted(Goals::TSubgoal goal); + + void setCB(CPlayerSpecificInfoCallback * CB) override; + void setAI(VCAI * AI) override; +}; + diff --git a/AI/VCAI/Fuzzy.cpp b/AI/VCAI/Fuzzy.cpp index 87750a443..c1b4af713 100644 --- a/AI/VCAI/Fuzzy.cpp +++ b/AI/VCAI/Fuzzy.cpp @@ -322,7 +322,7 @@ float FuzzyHelper::evaluate(Goals::Explore & g) } float FuzzyHelper::evaluate(Goals::RecruitHero & g) { - return 1; //just try to recruit hero as one of options + return 1; } FuzzyHelper::EvalVisitTile::~EvalVisitTile() { @@ -518,7 +518,7 @@ float FuzzyHelper::evaluate(Goals::ClearWayTo & g) float FuzzyHelper::evaluate(Goals::BuildThis & g) { - return 1; + return g.priority; //TODO } float FuzzyHelper::evaluate(Goals::DigAtTile & g) { @@ -526,12 +526,16 @@ float FuzzyHelper::evaluate(Goals::DigAtTile & g) } float FuzzyHelper::evaluate(Goals::CollectRes & g) { - return 0; + return g.priority; //handled by ResourceManager } float FuzzyHelper::evaluate(Goals::Build & g) { return 0; } +float FuzzyHelper::evaluate(Goals::BuyArmy & g) +{ + return g.priority; +} float FuzzyHelper::evaluate(Goals::Invalid & g) { return -1e10; @@ -541,7 +545,7 @@ float FuzzyHelper::evaluate(Goals::AbstractGoal & g) logAi->warn("Cannot evaluate goal %s", g.name()); return g.priority; } -void FuzzyHelper::setPriority(Goals::TSubgoal & g) +void FuzzyHelper::setPriority(Goals::TSubgoal & g) //calls evaluate - Visitor pattern { g->setpriority(g->accept(this)); //this enforces returned value is set } diff --git a/AI/VCAI/Fuzzy.h b/AI/VCAI/Fuzzy.h index 6e4f4d27b..1879e929b 100644 --- a/AI/VCAI/Fuzzy.h +++ b/AI/VCAI/Fuzzy.h @@ -73,6 +73,7 @@ public: float evaluate(Goals::DigAtTile & g); float evaluate(Goals::CollectRes & g); float evaluate(Goals::Build & g); + float evaluate(Goals::BuyArmy & g); float evaluate(Goals::GatherArmy & g); float evaluate(Goals::ClearWayTo & g); float evaluate(Goals::Invalid & g); diff --git a/AI/VCAI/Goals.cpp b/AI/VCAI/Goals.cpp index a47406dd3..6a3b1db75 100644 --- a/AI/VCAI/Goals.cpp +++ b/AI/VCAI/Goals.cpp @@ -11,18 +11,23 @@ #include "Goals.h" #include "VCAI.h" #include "Fuzzy.h" +#include "ResourceManager.h" #include "../../lib/mapping/CMap.h" //for victory conditions #include "../../lib/CPathfinder.h" +#include "StringConstants.h" + +#include "AIhelper.h" extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; //TODO: this logic should be moved inside VCAI +extern boost::thread_specific_ptr ah; +extern FuzzyHelper * fh; using namespace Goals; TSubgoal Goals::sptr(const AbstractGoal & tmp) { - std::shared_ptr ptr; + TSubgoal ptr; ptr.reset(tmp.clone()); return ptr; } @@ -48,6 +53,9 @@ std::string Goals::AbstractGoal::name() const //TODO: virtualize case GATHER_ARMY: desc = "GATHER ARMY"; break; + case BUY_ARMY: + return "BUY ARMY"; + break; case BOOST_HERO: desc = "BOOST_HERO (unsupported)"; break; @@ -56,7 +64,7 @@ std::string Goals::AbstractGoal::name() const //TODO: virtualize case BUILD_STRUCTURE: return "BUILD STRUCTURE"; case COLLECT_RES: - desc = "COLLECT RESOURCE"; + desc = "COLLECT RESOURCE " + GameConstants::RESOURCE_NAMES[resID] + " (" + boost::lexical_cast(value) + ")"; break; case GATHER_TROOPS: desc = "GATHER TROOPS"; @@ -114,21 +122,25 @@ bool Goals::AbstractGoal::operator==(AbstractGoal & g) case INVALID: case WIN: case DO_NOT_LOSE: - case RECRUIT_HERO: //recruit any hero, as yet + case RECRUIT_HERO: //overloaded return true; break; //assigned to hero, no parameters case CONQUER: case EXPLORE: - case GATHER_ARMY: //actual value is indifferent case BOOST_HERO: return g.hero.h == hero.h; //how comes HeroPtrs are equal for different heroes? break; + case GATHER_ARMY: //actual value is indifferent + return (g.hero.h == hero.h || town == g.town); //TODO: gather army for town maybe? + break; + //assigned hero and tile case VISIT_TILE: case CLEAR_WAY_TO: + case DIG_AT_TILE: return (g.hero.h == hero.h && g.tile == tile); break; @@ -137,16 +149,20 @@ bool Goals::AbstractGoal::operator==(AbstractGoal & g) case FIND_OBJ: //TODO: use subtype? case VISIT_HERO: case GET_ART_TYPE: - case DIG_AT_TILE: return (g.hero.h == hero.h && g.objid == objid); break; + case BUILD_STRUCTURE: + return (town == g.town && bid == g.bid); //build specific structure in specific town + break; + //no check atm case COLLECT_RES: + return (resID == g.resID); //every hero may collect resources + break; case GATHER_TROOPS: case ISSUE_COMMAND: case BUILD: //TODO: should be decomposed to build specific structures - case BUILD_STRUCTURE: default: return false; } @@ -156,28 +172,56 @@ bool Goals::AbstractGoal::operator==(AbstractGoal & g) namespace Goals { -template<> -void CGoal::accept(VCAI * ai) -{ - ai->tryRealize(static_cast(*this)); -} + template<> + void CGoal::accept(VCAI * ai) + { + ai->tryRealize(static_cast(*this)); + } -template<> -void CGoal::accept(VCAI * ai) -{ - ai->tryRealize(static_cast(*this)); -} -template<> -float CGoal::accept(FuzzyHelper * f) -{ - return f->evaluate(static_cast(*this)); -} + template<> + void CGoal::accept(VCAI * ai) + { + ai->tryRealize(static_cast(*this)); + } + template<> + float CGoal::accept(FuzzyHelper * f) + { + return f->evaluate(static_cast(*this)); + } -template<> -float CGoal::accept(FuzzyHelper * f) -{ - return f->evaluate(static_cast(*this)); -} + template<> + float CGoal::accept(FuzzyHelper * f) + { + return f->evaluate(static_cast(*this)); + } + bool TSubgoal::operator==(const TSubgoal & rhs) const + { + return *get() == *rhs.get(); //comparison for Goals is overloaded, so they don't need to be identical to match + } + + bool BuyArmy::operator==(BuyArmy & g) + { + //if (hero && hero != g.hero) + // return false; + return town == g.town; + } + bool BuyArmy::fulfillsMe(TSubgoal goal) + { + //if (hero && hero != goal->hero) + // return false; + return town == goal->town && goal->value >= value; //can always buy more army + } + TSubgoal BuyArmy::whatToDoToAchieve() + { + //TODO: calculate the actual cost of units instead + TResources price; + price[Res::GOLD] = value * 0.4f; //some approximate value + return ah->whatToDo(price, iAmElementar()); //buy right now or gather resources + } + std::string BuyArmy::completeMessage() const + { + return boost::format("Bought army of value %d in town of %s") % boost::lexical_cast(value), town->name; + } } //TSubgoal AbstractGoal::whatToDoToAchieve() @@ -251,7 +295,7 @@ TSubgoal Win::whatToDoToAchieve() if(h->visitedTown && !vstd::contains(h->visitedTown->forbiddenBuildings, BuildingID::GRAIL)) { const CGTownInstance * t = h->visitedTown; - return sptr(Goals::BuildThis(BuildingID::GRAIL, t)); + return sptr(Goals::BuildThis(BuildingID::GRAIL, t).setpriority(10)); } else { @@ -371,6 +415,19 @@ TSubgoal FindObj::whatToDoToAchieve() return sptr(Goals::Explore()); } +bool Goals::FindObj::fulfillsMe(TSubgoal goal) +{ + if (goal->goalType == Goals::VISIT_TILE) //visiting tile visits object at same time + { + if (!hero || hero == goal->hero) + for (auto obj : cb->getVisitableObjs(goal->tile)) //check if any object on that tile matches criteria + if (obj->visitablePos() == goal->tile) //object could be removed + if (obj->ID == objid && obj->subID == resID) //same type and subtype + return true; + } + return false; +} + std::string GetObj::completeMessage() const { return "hero " + hero.get()->name + " captured Object ID = " + boost::lexical_cast(objid); @@ -403,13 +460,15 @@ TSubgoal GetObj::whatToDoToAchieve() bool GetObj::fulfillsMe(TSubgoal goal) { - if(goal->goalType == Goals::VISIT_TILE) + if(goal->goalType == Goals::VISIT_TILE) //visiting tile visits object at same time { - auto obj = cb->getObj(ObjectInstanceID(objid)); - if(obj && obj->visitablePos() == goal->tile) //object could be removed - return true; + if (!hero || hero == goal->hero) + { + auto obj = cb->getObj(ObjectInstanceID(objid)); + if (obj && obj->visitablePos() == goal->tile) //object could be removed + return true; + } } - return false; } @@ -441,17 +500,18 @@ TSubgoal VisitHero::whatToDoToAchieve() bool VisitHero::fulfillsMe(TSubgoal goal) { - if(goal->goalType != Goals::VISIT_TILE) + //TODO: VisitObj shoudl not be used for heroes, but... + if(goal->goalType == Goals::VISIT_TILE) { - return false; + auto obj = cb->getObj(ObjectInstanceID(objid)); + if (!obj) + { + logAi->error("Hero %s: VisitHero::fulfillsMe at %s: object %d not found", hero.name, goal->tile.toString(), objid); + return false; + } + return obj->visitablePos() == goal->tile; } - auto obj = cb->getObj(ObjectInstanceID(objid)); - if(!obj) - { - logAi->error("Hero %s: VisitHero::fulfillsMe at %s: object %d not found", hero.name, goal->tile.toString(), objid); - return false; - } - return obj->visitablePos() == goal->tile; + return false; } TSubgoal GetArtOfType::whatToDoToAchieve() @@ -474,6 +534,14 @@ TSubgoal ClearWayTo::whatToDoToAchieve() return (fh->chooseSolution(getAllPossibleSubgoals())); } +bool Goals::ClearWayTo::fulfillsMe(TSubgoal goal) +{ + if (goal->goalType == Goals::VISIT_TILE) + if (!hero || hero == goal->hero) + return tile == goal->tile; + return false; +} + TGoalVec ClearWayTo::getAllPossibleSubgoals() { TGoalVec ret; @@ -702,17 +770,21 @@ bool Explore::fulfillsMe(TSubgoal goal) return false; } - TSubgoal RecruitHero::whatToDoToAchieve() { const CGTownInstance * t = ai->findTownWithTavern(); if(!t) - return sptr(Goals::BuildThis(BuildingID::TAVERN)); + return sptr(Goals::BuildThis(BuildingID::TAVERN).setpriority(2)); - if(cb->getResourceAmount(Res::GOLD) < GameConstants::HERO_GOLD_COST) - return sptr(Goals::CollectRes(Res::GOLD, GameConstants::HERO_GOLD_COST)); + TResources res; + res[Res::GOLD] = GameConstants::HERO_GOLD_COST; + return ah->whatToDo(res, iAmElementar()); //either buy immediately, or collect res +} - return iAmElementar(); +bool Goals::RecruitHero::operator==(RecruitHero & g) +{ + //TODO: check town and hero + return true; //for now, recruiting any hero will do } std::string VisitTile::completeMessage() const @@ -798,25 +870,158 @@ TSubgoal DigAtTile::whatToDoToAchieve() TSubgoal BuildThis::whatToDoToAchieve() { - //TODO check res - //look for town - //prerequisites? - return iAmElementar(); + auto b = BuildingID(bid); + + // find town if not set + if (!town && hero) + town = hero->visitedTown; + + if (!town) + { + for (const CGTownInstance * t : cb->getTownsInfo()) + { + switch (cb->canBuildStructure(town, b)) + { + case EBuildingState::ALLOWED: + town = t; + break; //TODO: look for prerequisites? this is not our reponsibility + default: + continue; + } + } + } + if (town) //we have specific town to build this + { + auto res = town->town->buildings.at(BuildingID(bid))->resources; + return ah->whatToDo(res, iAmElementar()); //realize immediately or gather resources + } + else + throw cannotFulfillGoalException("Cannot find town to build this"); +} + +TGoalVec Goals::CollectRes::getAllPossibleSubgoals() +{ + TGoalVec ret; + + auto givesResource = [this](const CGObjectInstance * obj) -> bool + { + //TODO: move this logic to object side + //TODO: remember mithril exists + //TODO: water objects + //TODO: Creature banks + + //return false first from once-visitable, before checking if they were even visited + switch (obj->ID.num) + { + case Obj::TREASURE_CHEST: + return resID == Res::GOLD; + break; + case Obj::RESOURCE: + return obj->subID == resID; + break; + case Obj::MINE: + return (obj->subID == resID && + (cb->getPlayerRelations(obj->tempOwner, ai->playerID) == PlayerRelations::ENEMIES)); //don't capture our mines + break; + case Obj::CAMPFIRE: + return true; //contains all resources + break; + case Obj::WINDMILL: + switch (resID) + { + case Res::GOLD: + case Res::WOOD: + return false; + } + break; + case Obj::WATER_WHEEL: + if (resID != Res::GOLD) + return false; + break; + case Obj::MYSTICAL_GARDEN: + if ((resID != Res::GOLD) && (resID != Res::GEMS)) + return false; + break; + case Obj::LEAN_TO: + case Obj::WAGON: + if (resID != Res::GOLD) + return false; + break; + default: + return false; + break; + } + return !vstd::contains(ai->alreadyVisited, obj); //for weekly / once visitable + }; + + std::vector objs; + for (auto obj : ai->visitableObjs) + { + if (givesResource(obj)) + objs.push_back(obj); + } + for (auto h : cb->getHeroesInfo()) + { + auto sm = ai->getCachedSectorMap(h); + std::vector ourObjs(objs); //copy common objects + + for (auto obj : ai->reservedHeroesMap[h]) //add objects reserved by this hero + { + if (givesResource(obj)) + ourObjs.push_back(obj); + } + for (auto obj : ourObjs) + { + int3 dest = obj->visitablePos(); + auto t = sm->firstTileToGet(h, dest); //we assume that no more than one tile on the way is guarded + if (t.valid()) //we know any path at all + { + if (ai->isTileNotReserved(h, t)) //no other hero wants to conquer that tile + { + if (isSafeToVisit(h, dest)) + { + if (dest != t) //there is something blocking our way + ret.push_back(sptr(Goals::ClearWayTo(dest, h).setisAbstract(true))); + else + { + ret.push_back(sptr(Goals::VisitTile(dest).sethero(h).setisAbstract(true))); + } + } + else //we need to get army in order to pick that object + ret.push_back(sptr(Goals::GatherArmy(evaluateDanger(dest, h) * SAFE_ATTACK_CONSTANT).sethero(h).setisAbstract(true))); + } + } + } + } + return ret; } TSubgoal CollectRes::whatToDoToAchieve() +{ + auto goals = getAllPossibleSubgoals(); + auto trade = whatToDoToTrade(); + if (!trade->invalid()) + goals.push_back(trade); + + if (goals.empty()) + return sptr(Goals::Explore()); //we can always do that + else + return fh->chooseSolution(goals); //TODO: evaluate trading +} + +TSubgoal Goals::CollectRes::whatToDoToTrade() { std::vector markets; std::vector visObjs; ai->retrieveVisitableObjs(visObjs, true); - for(const CGObjectInstance * obj : visObjs) + for (const CGObjectInstance * obj : visObjs) { - if(const IMarket * m = IMarket::castFrom(obj, false)) + if (const IMarket * m = IMarket::castFrom(obj, false)) { - if(obj->ID == Obj::TOWN && obj->tempOwner == ai->playerID && m->allowsTrade(EMarketMode::RESOURCE_RESOURCE)) + if (obj->ID == Obj::TOWN && obj->tempOwner == ai->playerID && m->allowsTrade(EMarketMode::RESOURCE_RESOURCE)) markets.push_back(m); - else if(obj->ID == Obj::TRADING_POST) //TODO a moze po prostu test na pozwalanie handlu? + else if (obj->ID == Obj::TRADING_POST) markets.push_back(m); } } @@ -828,20 +1033,20 @@ TSubgoal CollectRes::whatToDoToAchieve() markets.erase(boost::remove_if(markets, [](const IMarket * market) -> bool { - if(!(market->o->ID == Obj::TOWN && market->o->tempOwner == ai->playerID)) + if (!(market->o->ID == Obj::TOWN && market->o->tempOwner == ai->playerID)) { - if(!ai->isAccessible(market->o->visitablePos())) + if (!ai->isAccessible(market->o->visitablePos())) return true; } return false; }), markets.end()); - if(!markets.size()) + if (!markets.size()) { - for(const CGTownInstance * t : cb->getTownsInfo()) + for (const CGTownInstance * t : cb->getTownsInfo()) { - if(cb->canBuildStructure(t, BuildingID::MARKETPLACE) == EBuildingState::ALLOWED) - return sptr(Goals::BuildThis(BuildingID::MARKETPLACE, t)); + if (cb->canBuildStructure(t, BuildingID::MARKETPLACE) == EBuildingState::ALLOWED) + return sptr(Goals::BuildThis(BuildingID::MARKETPLACE, t).setpriority(2)); } } else @@ -849,9 +1054,9 @@ TSubgoal CollectRes::whatToDoToAchieve() const IMarket * m = markets.back(); //attempt trade at back (best prices) int howManyCanWeBuy = 0; - for(Res::ERes i = Res::WOOD; i <= Res::GOLD; vstd::advance(i, 1)) + for (Res::ERes i = Res::WOOD; i <= Res::GOLD; vstd::advance(i, 1)) { - if(i == resID) + if (i == resID) continue; int toGive = -1, toReceive = -1; m->getOffer(i, resID, toGive, toReceive, EMarketMode::RESOURCE_RESOURCE); @@ -859,21 +1064,36 @@ TSubgoal CollectRes::whatToDoToAchieve() howManyCanWeBuy += toReceive * (cb->getResourceAmount(i) / toGive); } - if(howManyCanWeBuy + cb->getResourceAmount(static_cast(resID)) >= value) + if (howManyCanWeBuy >= value) { auto backObj = cb->getTopObj(m->o->visitablePos()); //it'll be a hero if we have one there; otherwise marketplace assert(backObj); - if(backObj->tempOwner != ai->playerID) + auto objid = m->o->id.getNum(); + if (backObj->tempOwner != ai->playerID) { - return sptr(Goals::GetObj(m->o->id.getNum())); + return sptr(Goals::GetObj(objid)); } else { - return sptr(Goals::GetObj(m->o->id.getNum()).setisElementar(true)); + if (m->o->ID == Obj::TOWN) //just trade remotely using town objid + return sptr(setobjid(objid).setisElementar(true)); + else //just go there + return sptr(Goals::GetObj(objid).setisElementar(true)); } } } - return sptr(setisElementar(true)); //all the conditions for trade are met + return sptr(Goals::Invalid()); //cannot trade + //TODO: separate goal to execute trade? + //return sptr(setisElementar(true)); //not sure why we are here +} + +bool CollectRes::fulfillsMe(TSubgoal goal) +{ + if (goal->resID == resID) + if (goal->value >= value) + return true; + + return false; } TSubgoal GatherTroops::whatToDoToAchieve() @@ -899,7 +1119,7 @@ TSubgoal GatherTroops::whatToDoToAchieve() } else { - return sptr(Goals::BuildThis(bid, t)); + return sptr(Goals::BuildThis(bid, t).setpriority(priority)); } } } @@ -915,7 +1135,7 @@ TSubgoal GatherTroops::whatToDoToAchieve() { for(auto type : creature.second) { - if(type == objid && ai->freeResources().canAfford(VLC->creh->creatures[type]->cost)) + if(type == objid && ah->freeResources().canAfford(VLC->creh->creatures[type]->cost)) dwellings.push_back(d); } } @@ -962,6 +1182,15 @@ TSubgoal GatherTroops::whatToDoToAchieve() //TODO: exchange troops between heroes } +bool Goals::GatherTroops::fulfillsMe(TSubgoal goal) +{ + if (!hero || hero == goal->hero) //we got army for desired hero or any hero + if (goal->objid == objid) //same creature type //TODO: consider upgrades? + if (goal->value >= value) //notify every time we get resources? + return true; + return false; +} + TSubgoal Conquer::whatToDoToAchieve() { return fh->chooseSolution(getAllPossibleSubgoals()); @@ -1050,6 +1279,14 @@ TSubgoal Build::whatToDoToAchieve() return iAmElementar(); } +bool Goals::Build::fulfillsMe(TSubgoal goal) +{ + if (goal->goalType == Goals::BUILD || goal->goalType == Goals::BUILD_STRUCTURE) + return (!town || town == goal->town); //building anything will do, in this town if set + else + return false; +} + TSubgoal Invalid::whatToDoToAchieve() { return iAmElementar(); @@ -1082,14 +1319,25 @@ TGoalVec GatherArmy::getAllPossibleSubgoals() auto pos = t->visitablePos(); if(ai->isAccessibleForHero(pos, hero)) { + //grab army from town if(!t->visitingHero && howManyReinforcementsCanGet(hero, t)) { if(!vstd::contains(ai->townVisitsThisWeek[hero], t)) ret.push_back(sptr(Goals::VisitTile(pos).sethero(hero))); } + //buy army in town + if (!t->visitingHero || t->visitingHero != hero.get(true)) + { + ui32 val = std::min(value, howManyReinforcementsCanBuy(hero, t)); + if (val) + ret.push_back(sptr(Goals::BuyArmy(t, val).sethero(hero))); + } + //build dwelling auto bid = ai->canBuildAnyStructure(t, std::vector(unitsSource, unitsSource + ARRAY_COUNT(unitsSource)), 8 - cb->getDate(Date::DAY_OF_WEEK)); - if(bid != BuildingID::NONE) - ret.push_back(sptr(BuildThis(bid, t))); + if (bid != BuildingID::NONE) + { + ret.push_back(sptr(BuildThis(bid, t).setpriority(priority))); + } } } @@ -1134,7 +1382,7 @@ TGoalVec GatherArmy::getAllPossibleSubgoals() for(auto & creatureID : creLevel.second) { auto creature = VLC->creh->creatures[creatureID]; - if(ai->freeResources().canAfford(creature->cost)) + if(ah->freeResources().canAfford(creature->cost)) objs.push_back(obj); } } @@ -1154,7 +1402,7 @@ TGoalVec GatherArmy::getAllPossibleSubgoals() } } - if(ai->canRecruitAnyHero() && ai->freeResources()[Res::GOLD] > GameConstants::HERO_GOLD_COST) //this is not stupid in early phase of game + if(ai->canRecruitAnyHero() && ah->freeGold() > GameConstants::HERO_GOLD_COST) //this is not stupid in early phase of game { if(auto t = ai->findTownWithTavern()) { diff --git a/AI/VCAI/Goals.h b/AI/VCAI/Goals.h index 80dc3a9da..96dfb1f5b 100644 --- a/AI/VCAI/Goals.h +++ b/AI/VCAI/Goals.h @@ -23,14 +23,21 @@ namespace Goals { class AbstractGoal; class VisitTile; -typedef std::shared_ptr TSubgoal; + +class DLL_EXPORT TSubgoal : public std::shared_ptr +{ + public: + bool operator==(const TSubgoal & rhs) const; + //TODO: serialize? +}; 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, + EXPLORE, GATHER_ARMY, + BOOST_HERO, RECRUIT_HERO, BUILD_STRUCTURE, //if hero set, then in visited town COLLECT_RES, @@ -48,7 +55,8 @@ enum EGoals VISIT_TILE, //tile, in conjunction with hero elementar; assumes tile is reachable CLEAR_WAY_TO, - DIG_AT_TILE //elementar with hero on tile + DIG_AT_TILE,//elementar with hero on tile + BUY_ARMY //at specific town }; //method chaining + clone pattern @@ -61,9 +69,9 @@ enum EGoals enum {LOW_PR = -1}; -TSubgoal sptr(const AbstractGoal & tmp); +DLL_EXPORT TSubgoal sptr(const AbstractGoal & tmp); -class AbstractGoal +class DLL_EXPORT AbstractGoal { public: bool isElementar; VSETTER(bool, isElementar) @@ -129,7 +137,7 @@ public: virtual bool operator==(AbstractGoal & g); virtual bool fulfillsMe(Goals::TSubgoal goal) //TODO: multimethod instead of type check { - return false; + return false; //use this method to check if goal is fulfilled by another (not equal) goal, operator == is handled spearately } template void serialize(Handler & h, const int version) @@ -149,7 +157,7 @@ public: } }; -template class CGoal : public AbstractGoal +template class DLL_EXPORT CGoal : public AbstractGoal { public: CGoal(EGoals goal = INVALID) : AbstractGoal(goal) @@ -186,8 +194,8 @@ public: } TSubgoal iAmElementar() { - setisElementar(true); - std::shared_ptr ptr; + setisElementar(true); //FIXME: it's not const-correct, maybe we shoudl only set returned clone? + TSubgoal ptr; ptr.reset(clone()); return ptr; } @@ -199,7 +207,7 @@ public: } }; -class Invalid : public CGoal +class DLL_EXPORT Invalid : public CGoal { public: Invalid() @@ -214,7 +222,7 @@ public: TSubgoal whatToDoToAchieve() override; }; -class Win : public CGoal +class DLL_EXPORT Win : public CGoal { public: Win() @@ -229,7 +237,7 @@ public: TSubgoal whatToDoToAchieve() override; }; -class NotLose : public CGoal +class DLL_EXPORT NotLose : public CGoal { public: NotLose() @@ -244,7 +252,7 @@ public: //TSubgoal whatToDoToAchieve() override; }; -class Conquer : public CGoal +class DLL_EXPORT Conquer : public CGoal { public: Conquer() @@ -256,7 +264,7 @@ public: TSubgoal whatToDoToAchieve() override; }; -class Build : public CGoal +class DLL_EXPORT Build : public CGoal { public: Build() @@ -269,9 +277,10 @@ public: return TGoalVec(); } TSubgoal whatToDoToAchieve() override; + bool fulfillsMe(TSubgoal goal) override; }; -class Explore : public CGoal +class DLL_EXPORT Explore : public CGoal { public: Explore() @@ -291,7 +300,7 @@ public: bool fulfillsMe(TSubgoal goal) override; }; -class GatherArmy : public CGoal +class DLL_EXPORT GatherArmy : public CGoal { public: GatherArmy() @@ -309,7 +318,28 @@ public: std::string completeMessage() const override; }; -class BoostHero : public CGoal +class DLL_EXPORT BuyArmy : public CGoal +{ +private: + BuyArmy() + : CGoal(Goals::BUY_ARMY) + {} +public: + BuyArmy(const CGTownInstance * Town, int val) + : CGoal(Goals::BUY_ARMY) + { + town = Town; //where to buy this army + value = val; //expressed in AI unit strength + priority = 2;//TODO: evaluate? + } + bool operator==(BuyArmy & g); + bool fulfillsMe(TSubgoal goal) override; + + TSubgoal whatToDoToAchieve() override; + std::string completeMessage() const override; +}; + +class DLL_EXPORT BoostHero : public CGoal { public: BoostHero() @@ -324,7 +354,7 @@ public: //TSubgoal whatToDoToAchieve() override {return sptr(Invalid());}; }; -class RecruitHero : public CGoal +class DLL_EXPORT RecruitHero : public CGoal { public: RecruitHero() @@ -337,37 +367,37 @@ public: return TGoalVec(); } TSubgoal whatToDoToAchieve() override; + bool operator==(RecruitHero & g); }; -class BuildThis : public CGoal +class DLL_EXPORT BuildThis : public CGoal { public: - BuildThis() + BuildThis() //should be private, but unit test uses it : CGoal(Goals::BUILD_STRUCTURE) - { - //FIXME: should be not allowed (private) - } + {} BuildThis(BuildingID Bid, const CGTownInstance * tid) : CGoal(Goals::BUILD_STRUCTURE) { bid = Bid; town = tid; - priority = 5; + priority = 1; } BuildThis(BuildingID Bid) : CGoal(Goals::BUILD_STRUCTURE) { bid = Bid; - priority = 5; + priority = 1; } TGoalVec getAllPossibleSubgoals() override { return TGoalVec(); } TSubgoal whatToDoToAchieve() override; + //bool fulfillsMe(TSubgoal goal) override; }; -class CollectRes : public CGoal +class DLL_EXPORT CollectRes : public CGoal { public: CollectRes() @@ -381,14 +411,13 @@ public: value = val; priority = 2; } - TGoalVec getAllPossibleSubgoals() override - { - return TGoalVec(); - }; + TGoalVec getAllPossibleSubgoals() override; TSubgoal whatToDoToAchieve() override; + TSubgoal whatToDoToTrade(); + bool fulfillsMe(TSubgoal goal) override; }; -class GatherTroops : public CGoal +class DLL_EXPORT GatherTroops : public CGoal { public: GatherTroops() @@ -408,9 +437,10 @@ public: return TGoalVec(); } TSubgoal whatToDoToAchieve() override; + bool fulfillsMe(TSubgoal goal) override; }; -class GetObj : public CGoal +class DLL_EXPORT GetObj : public CGoal { public: GetObj() {} // empty constructor not allowed @@ -434,7 +464,7 @@ public: std::string completeMessage() const override; }; -class FindObj : public CGoal +class DLL_EXPORT FindObj : public CGoal { public: FindObj() {} // empty constructor not allowed @@ -443,6 +473,7 @@ public: : CGoal(Goals::FIND_OBJ) { objid = ID; + resID = -1; //subid unspecified priority = 1; } FindObj(int ID, int subID) @@ -457,9 +488,10 @@ public: return TGoalVec(); } TSubgoal whatToDoToAchieve() override; + bool fulfillsMe(TSubgoal goal) override; }; -class VisitHero : public CGoal +class DLL_EXPORT VisitHero : public CGoal { public: VisitHero() @@ -485,7 +517,7 @@ public: std::string completeMessage() const override; }; -class GetArtOfType : public CGoal +class DLL_EXPORT GetArtOfType : public CGoal { public: GetArtOfType() @@ -505,7 +537,7 @@ public: TSubgoal whatToDoToAchieve() override; }; -class VisitTile : public CGoal +class DLL_EXPORT VisitTile : public CGoal //tile, in conjunction with hero elementar; assumes tile is reachable { public: @@ -526,7 +558,7 @@ public: std::string completeMessage() const override; }; -class ClearWayTo : public CGoal +class DLL_EXPORT ClearWayTo : public CGoal { public: ClearWayTo() @@ -552,9 +584,10 @@ public: { return g.goalType == goalType && g.tile == tile; } + bool fulfillsMe(TSubgoal goal) override; }; -class DigAtTile : public CGoal +class DLL_EXPORT DigAtTile : public CGoal //elementar with hero on tile { public: @@ -579,7 +612,7 @@ public: } }; -class CIssueCommand : public CGoal +class DLL_EXPORT CIssueCommand : public CGoal { std::function command; diff --git a/AI/VCAI/ResourceManager.cpp b/AI/VCAI/ResourceManager.cpp new file mode 100644 index 000000000..cf7469bef --- /dev/null +++ b/AI/VCAI/ResourceManager.cpp @@ -0,0 +1,312 @@ +/* +* ResourceManager.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 "ResourceManager.h" + +#include "../../CCallback.h" +#include "../../lib/mapObjects/MapObjects.h" + +#define GOLD_RESERVE (10000); //at least we'll be able to reach capitol + +ResourceObjective::ResourceObjective(TResources & Res, Goals::TSubgoal Goal) + : resources(Res), goal(Goal) +{ +} + +bool ResourceObjective::operator<(const ResourceObjective & ro) const +{ + return goal->priority < ro.goal->priority; +} + +ResourceManager::ResourceManager(CPlayerSpecificInfoCallback * CB, VCAI * AI) + : ai(AI), cb(CB) +{ +} + +void ResourceManager::setCB(CPlayerSpecificInfoCallback * CB) +{ + cb = CB; +} + +void ResourceManager::setAI(VCAI * AI) +{ + ai = AI; +} + +bool ResourceManager::canAfford(const TResources & cost) const +{ + return freeResources().canAfford(cost); +} + +TResources ResourceManager::estimateIncome() const +{ + TResources ret; + for (const CGTownInstance * t : cb->getTownsInfo()) + { + ret += t->dailyIncome(); + } + + for (const CGObjectInstance * obj : ai->getFlaggedObjects()) + { + if (obj->ID == Obj::MINE) + { + switch (obj->subID) + { + case Res::WOOD: + case Res::ORE: + ret[obj->subID] += WOOD_ORE_MINE_PRODUCTION; + break; + case Res::GOLD: + case 7: //abandoned mine -> also gold + ret[Res::GOLD] += GOLD_MINE_PRODUCTION; + break; + default: + ret[obj->subID] += RESOURCE_MINE_PRODUCTION; + break; + } + } + } + + return ret; +} + +void ResourceManager::reserveResoures(TResources & res, Goals::TSubgoal goal) +{ + if (!goal->invalid()) + tryPush(ResourceObjective(res, goal)); + else + logAi->warn("Attempt to reserve resources for Invalid goal"); +} + +Goals::TSubgoal ResourceManager::collectResourcesForOurGoal(ResourceObjective &o) const +{ + auto allResources = cb->getResourceAmount(); + auto income = estimateIncome(); + Res::ERes resourceType = Res::INVALID; + TResource amountToCollect = 0; + + typedef std::pair resPair; + std::map missingResources; + + //TODO: unit test for complex resource sets + + //sum missing resources of given type for ALL reserved objectives + for (auto it = queue.ordered_begin(); it != queue.ordered_end(); it++) + { + //choose specific resources we need for this goal (not 0) + for (auto r = Res::ResourceSet::nziterator(o.resources); r.valid(); r++) + missingResources[r->resType] += it->resources[r->resType]; //goal it costs r units of resType + } + for (auto it = Res::ResourceSet::nziterator(o.resources); it.valid(); it++) + { + missingResources[it->resType] -= allResources[it->resType]; //missing = (what we need) - (what we have) + vstd::amax(missingResources[it->resType], 0); // if we have more resources than reserved, we don't need them + } + vstd::erase_if(missingResources, [=](const resPair & p) -> bool + { + return !(p.second); //in case evaluated to 0 or less + }); + if (missingResources.empty()) //FIXME: should be unit-tested out + { + logAi->error("We don't need to collect resources %s for goal %s", o.resources.toString(), o.goal->name()); + return o.goal; + } + + float goalPriority = 10; //arbitrary, will be divided + for (const resPair & p : missingResources) + { + if (!income[p.first]) //prioritize resources with 0 income + { + resourceType = p.first; + amountToCollect = p.second; + goalPriority /= amountToCollect; //need more resources -> lower priority + break; + } + } + if (resourceType == Res::INVALID) //no needed resources has 0 income, + { + //find the one which takes longest to collect + typedef std::pair timePair; + std::map daysToEarn; + for (auto it : missingResources) + daysToEarn[it.first] = (float)missingResources[it.first] / income[it.first]; + auto incomeComparer = [&income](const timePair & lhs, const timePair & rhs) -> bool + { + //theoretically income can be negative, but that falls into this comparison + return lhs.second < rhs.second; + }; + + resourceType = boost::max_element(daysToEarn, incomeComparer)->first; + amountToCollect = missingResources[resourceType]; + goalPriority /= daysToEarn[resourceType]; //more days - lower priority + } + if (resourceType == Res::GOLD) + goalPriority *= 1000; + + return Goals::sptr(Goals::CollectRes(resourceType, amountToCollect)); +} + +Goals::TSubgoal ResourceManager::whatToDo() const //suggest any goal +{ + if (queue.size()) + { + auto o = queue.top(); + + auto allResources = cb->getResourceAmount(); //we don't consider savings, it's out top-priority goal + if (allResources.canAfford(o.resources)) + return o.goal; + else //we can't afford even top-priority goal, need to collect resources + return collectResourcesForOurGoal(o); + } + else + return Goals::sptr(Goals::Invalid()); //nothing else to do +} + +Goals::TSubgoal ResourceManager::whatToDo(TResources &res, Goals::TSubgoal goal) +{ + TResources accumulatedResources; + auto allResources = cb->getResourceAmount(); + + ResourceObjective ro(res, goal); + tryPush(ro); + //check if we can afford all the objectives with higher priority first + for (auto it = queue.ordered_begin(); it != queue.ordered_end(); it++) + { + accumulatedResources += it->resources; + if (!accumulatedResources.canBeAfforded(allResources)) //can't afford + return collectResourcesForOurGoal(ro); + else //can afford all goals up to this point + { + if (it->goal == goal) + return goal; //can afford immediately + } + } + return collectResourcesForOurGoal(ro); //fallback, ever needed? +} + +bool ResourceManager::notifyGoalCompleted(Goals::TSubgoal goal) +{ + if (goal->invalid()) + logAi->warn("Attempt to complete Invalid goal"); + + bool removedGoal = false; + while (true) + { //unfortunatelly we can't use remove_if on heap + auto it = boost::find_if(queue, [goal](const ResourceObjective & ro) -> bool + { + return ro.goal == goal || ro.goal->fulfillsMe (goal); + }); + if (it != queue.end()) //removed at least one + { + queue.erase(queue.s_handle_from_iterator(it)); + logAi->debug("Removed goal %s from ResourceManager.", it->goal->name()); + removedGoal = true; + } + else //found nothing more to remove + return removedGoal; + } + return removedGoal; +} + +bool ResourceManager::updateGoal(Goals::TSubgoal goal) +{ + //we update priority of goal if it is easier or more difficult to complete + if (goal->invalid()) + logAi->warn("Attempt to update Invalid goal"); + + auto it = boost::find_if(queue, [goal](const ResourceObjective & ro) -> bool + { + return ro.goal == goal; + }); + if (it != queue.end()) + { + it->goal->setpriority(goal->priority); + auto handle = queue.s_handle_from_iterator(it); + queue.update(handle); //restore order + return true; + } + else + return false; +} + +bool ResourceManager::tryPush(ResourceObjective & o) +{ + auto goal = o.goal; + + auto it = boost::find_if(queue, [goal](const ResourceObjective & ro) -> bool + { + return ro.goal == goal; + }); + if (it != queue.end()) + { + auto handle = queue.s_handle_from_iterator(it); + vstd::amax(goal->priority, it->goal->priority); //increase priority if case + //update resources with new value + queue.update(handle, ResourceObjective(o.resources, goal)); //restore order + return false; + } + else + { + queue.push(o); //add new objective + logAi->debug("Reserved resources (%s) for %s", o.resources.toString(), goal->name()); + return true; + } +} + +bool ResourceManager::hasTasksLeft() const +{ + return !queue.empty(); +} + +TResources ResourceManager::reservedResources() const +{ + TResources res; + for (auto it : queue) //substract the value of reserved goals + res += it.resources; + return res; +} + +TResources ResourceManager::freeResources() const +{ + TResources myRes = cb->getResourceAmount(); + auto towns = cb->getTownsInfo(); + if (towns.size()) //we don't save for Capitol if there are no towns + { + if (std::none_of(towns.begin(), towns.end(), [](const CGTownInstance * x) -> bool + { + return x->builtBuildings.find(BuildingID::CAPITOL) != x->builtBuildings.end(); + })) + { + myRes[Res::GOLD] -= GOLD_RESERVE; + //what if capitol is blocked from building in all possessed towns (set in map editor)? + } + } + myRes -= reservedResources(); //substract the value of reserved goals + + for (auto & val : myRes) + vstd::amax(val, 0); //never negative + + return myRes; +} + +TResource ResourceManager::freeGold() const +{ + return freeResources()[Res::GOLD]; +} + +TResources ResourceManager::allResources() const +{ + return cb->getResourceAmount(); +} + +TResource ResourceManager::allGold() const +{ + return cb->getResourceAmount()[Res::GOLD]; +} diff --git a/AI/VCAI/ResourceManager.h b/AI/VCAI/ResourceManager.h new file mode 100644 index 000000000..5a27ef04c --- /dev/null +++ b/AI/VCAI/ResourceManager.h @@ -0,0 +1,109 @@ +/* +* ResourceManager.h, part of VCMI engine +* +* Authors: listed in file AUTHORS in main folder +* +* License: GNU General Public License v2.0 or later +* Full text of license available in license.txt file, in main folder +* +*/ +#pragma once + +#include "AIUtility.h" +#include "Goals.h" +#include "../../lib/GameConstants.h" +#include "../../lib/VCMI_Lib.h" +#include "VCAI.h" +#include + +class AIhelper; +class IResourceManager; + +struct DLL_EXPORT ResourceObjective +{ + ResourceObjective() = default; + ResourceObjective(TResources &res, Goals::TSubgoal goal); + bool operator < (const ResourceObjective &ro) const; + + TResources resources; //how many resoures do we need + Goals::TSubgoal goal; //what for (build, gather army etc...) + + //TODO: register? + template void serializeInternal(Handler & h, const int version) + { + h & resources; + //h & goal; //FIXME: goal serialization is broken + } +}; + +class IResourceManager //: public: IAbstractManager +{ +public: + virtual ~IResourceManager() = default; + virtual void setCB(CPlayerSpecificInfoCallback * CB) = 0; + virtual void setAI(VCAI * AI) = 0; + + virtual TResources reservedResources() const = 0; + virtual TResources freeResources() const = 0; + virtual TResource freeGold() const = 0; + virtual TResources allResources() const = 0; + virtual TResource allGold() const = 0; + + virtual Goals::TSubgoal whatToDo() const = 0;//get highest-priority goal + virtual Goals::TSubgoal whatToDo(TResources &res, Goals::TSubgoal goal) = 0; + virtual bool hasTasksLeft() const = 0; +private: + virtual bool notifyGoalCompleted(Goals::TSubgoal goal) = 0; +}; + +class DLL_EXPORT ResourceManager : public IResourceManager +{ + /*Resource Manager is a smart shopping list for AI to help + Uses priority queue based on CGoal.priority */ + friend class VCAI; + friend class AIhelper; + friend struct SetGlobalState; + + CPlayerSpecificInfoCallback * cb; //this is enough, but we downcast from CCallback + VCAI * ai; + +public: + ResourceManager() = default; + ResourceManager(CPlayerSpecificInfoCallback * CB, VCAI * AI = nullptr); //for tests only + + bool canAfford(const TResources & cost) const; + TResources reservedResources() const override; + TResources freeResources() const override; + TResource freeGold() const override; + TResources allResources() const override; + TResource allGold() const override; + + Goals::TSubgoal whatToDo() const override; //peek highest-priority goal + Goals::TSubgoal whatToDo(TResources &res, Goals::TSubgoal goal); //can we afford this goal or need to CollectRes? + bool hasTasksLeft() const override; + +protected: //not-const actions only for AI + virtual void reserveResoures(TResources &res, Goals::TSubgoal goal = Goals::TSubgoal()); + virtual bool notifyGoalCompleted(Goals::TSubgoal goal); + virtual bool updateGoal(Goals::TSubgoal goal); //new goal must have same properties but different priority + virtual bool tryPush(ResourceObjective &o); + + //inner processing + virtual TResources estimateIncome() const; + virtual Goals::TSubgoal collectResourcesForOurGoal(ResourceObjective &o) const; + + void setCB(CPlayerSpecificInfoCallback * CB) override; + void setAI(VCAI * AI) override; + +private: + TResources saving; + + boost::heap::binomial_heap queue; + + //TODO: register? + template void serializeInternal(Handler & h, const int version) + { + h & saving; + h & queue; + } +}; \ No newline at end of file diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index 841875492..2b7b27c9b 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -10,6 +10,7 @@ #include "StdInc.h" #include "VCAI.h" #include "Fuzzy.h" +#include "ResourceManager.h" #include "../../lib/UnlockGuard.h" #include "../../lib/mapObjects/MapObjects.h" @@ -22,16 +23,18 @@ #include "../../lib/serializer/BinarySerializer.h" #include "../../lib/serializer/BinaryDeserializer.h" +#include "AIhelper.h" + extern FuzzyHelper * fh; class CGVisitableOPW; const double SAFE_ATTACK_CONSTANT = 1.5; -const int GOLD_RESERVE = 10000; //when buying creatures we want to keep at least this much gold (10000 so at least we'll be able to reach capitol) //one thread may be turn of AI and another will be handling a side effect for AI2 boost::thread_specific_ptr cb; boost::thread_specific_ptr ai; +extern boost::thread_specific_ptr ah; //std::map > HeroView::infosCount; @@ -45,9 +48,15 @@ struct SetGlobalState ai.reset(AI); cb.reset(AI->myCb.get()); + if (!ah.get()) + ah.reset(new AIhelper()); + ah->setAI(AI); //does this make any sense? + ah->setCB(cb.get()); } ~SetGlobalState() { + //TODO: how to handle rm? shouldn't be called after ai is destroyed, hopefully + //TODO: to ensure that, make rm unique_ptr ai.release(); cb.release(); } @@ -537,6 +546,8 @@ void VCAI::objectPropertyChanged(const SetObjectProperty * sop) void VCAI::buildChanged(const CGTownInstance * town, BuildingID buildingID, int what) { LOG_TRACE_PARAMS(logAi, "what '%i'", what); + if (town->getOwner() == playerID && what == 1) //built + completeGoal(sptr(Goals::BuildThis(buildingID, town))); NET_EVENT_HANDLER; } @@ -564,7 +575,10 @@ void VCAI::init(std::shared_ptr CB) LOG_TRACE(logAi); myCb = CB; cbc = CB; - NET_EVENT_HANDLER; + + ah.reset(new AIhelper()); + + NET_EVENT_HANDLER; //sets ah->rm->cb playerID = *myCb->getMyColor(); myCb->waitTillRealize = true; myCb->unlockGsWhenWaiting = true; @@ -772,8 +786,6 @@ void VCAI::makeTurn() It is not supposed to work this way in final version of VCAI. It consists of few actions/loops done in particular order, hard parts are explained below with focus on explaining hero management logic*/ void VCAI::makeTurnInternal() { - saving = 0; - //it looks messy here, but it's better to have armed heroes before attempting realizing goals for(const CGTownInstance * t : cb->getTownsInfo()) moveCreaturesToHero(t); @@ -814,11 +826,14 @@ void VCAI::makeTurnInternal() /*below line performs goal decomposition, result of the function is ONE goal for ONE hero to realize.*/ striveToGoal(sptr(Goals::Win())); + //TODO: add ResourceManager goals to the pool and process them all at once + if (ah->hasTasksLeft()) + striveToGoal(ah->whatToDo()); + /*Explanation of below loop: At the time of writing this - goals get decomposited either to GatherArmy or Visit Tile. Visit tile that is about visiting object gets processed at beginning of MakeTurnInternal without re-evaluation. Rest of goals that got started via striveToGoal(sptr(Goals::Win())); in previous turns and not finished get continued here. Also they are subject for re-evaluation to see if there is better goal to start (still talking only about heroes that got goals started by via striveToGoal(sptr(Goals::Win())); in previous turns.*/ - //finally, continue our abstract long-term goals int oldMovement = 0; int newMovement = 0; @@ -863,6 +878,7 @@ void VCAI::makeTurnInternal() striveToQuest(quest); } + //TODO: striveToGoal striveToGoal(sptr(Goals::Build())); //TODO: smarter building management /*Below function is also responsible for hero movement via internal wander function. By design it is separate logic for heroes that have nothing to do. @@ -914,7 +930,7 @@ void VCAI::performObjectInteraction(const CGObjectInstance * obj, HeroPtr h) if(h->visitedTown) //we are inside, not just attacking { townVisitsThisWeek[h].insert(h->visitedTown); - if(!h->hasSpellbook() && cb->getResourceAmount(Res::GOLD) >= GameConstants::SPELLBOOK_GOLD_COST + saving[Res::GOLD]) + if(!h->hasSpellbook() && ah->freeGold() >= GameConstants::SPELLBOOK_GOLD_COST) { if(h->visitedTown->hasBuilt(BuildingID::MAGES_GUILD_1)) cb->buyArtifact(h.get(), ArtifactID::SPELLBOOK); @@ -1150,6 +1166,7 @@ void VCAI::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * ot void VCAI::recruitCreatures(const CGDwelling * d, const CArmedInstance * recruiter) { + //now used only for visited dwellings / towns, not BuyArmy goal for(int i = 0; i < d->creatures.size(); i++) { if(!d->creatures[i].second.size()) @@ -1157,17 +1174,14 @@ void VCAI::recruitCreatures(const CGDwelling * d, const CArmedInstance * recruit int count = d->creatures[i].first; CreatureID creID = d->creatures[i].second.back(); -// const CCreature *c = VLC->creh->creatures[creID]; -// if(containsSavedRes(c->cost)) -// continue; - vstd::amin(count, freeResources() / VLC->creh->creatures[creID]->cost); + vstd::amin(count, ah->freeResources() / VLC->creh->creatures[creID]->cost); if(count > 0) cb->recruitCreatures(d, recruiter, creID, count, i); } } -bool VCAI::tryBuildStructure(const CGTownInstance * t, BuildingID building, unsigned int maxDays) const +bool VCAI::tryBuildThisStructure(const CGTownInstance * t, BuildingID building, unsigned int maxDays) { if(maxDays == 0) { @@ -1199,52 +1213,52 @@ bool VCAI::tryBuildStructure(const CGTownInstance * t, BuildingID building, unsi if(maxDays && toBuild.size() > maxDays) return false; - TResources currentRes = cb->getResourceAmount(); - //TODO: calculate if we have enough resources to build it in maxDays + //TODO: calculate if we have enough resources to build it in maxDays? for(const auto & buildID : toBuild) { const CBuilding * b = t->town->buildings.at(buildID); EBuildingState::EBuildingState canBuild = cb->canBuildStructure(t, buildID); - if(canBuild == EBuildingState::ALLOWED) + if (canBuild == EBuildingState::ALLOWED) { - if(!containsSavedRes(b->resources)) - { - logAi->debug("Player %d will build %s in town of %s at %s", playerID, b->Name(), t->name, t->pos.toString()); - cb->buildBuilding(t, buildID); - return true; - } - continue; + buildStructure(t, buildID); + return true; } - else if(canBuild == EBuildingState::NO_RESOURCES) - { - //We can't do anything about it - no requests from this function - continue; - } - else if(canBuild == EBuildingState::PREREQUIRES) + else if (canBuild == EBuildingState::PREREQUIRES) { // can happen when dependencies have their own missing dependencies - if(tryBuildStructure(t, buildID, maxDays - 1)) + if (tryBuildThisStructure(t, buildID, maxDays - 1)) return true; } - else if(canBuild == EBuildingState::MISSING_BASE) + else if (canBuild == EBuildingState::MISSING_BASE) { - if(tryBuildStructure(t, b->upgrade, maxDays - 1)) - return true; + if (tryBuildThisStructure(t, b->upgrade, maxDays - 1)) + return true; } + else if (canBuild == EBuildingState::NO_RESOURCES) + { + //we may need to gather resources for those + PotentialBuilding pb; + pb.bid = buildID; + pb.price = t->getBuildingCost(buildID); + potentialBuildings.push_back(pb); //these are checked again in try + return false; + } + else + return false; } return false; } -bool VCAI::tryBuildAnyStructure(const CGTownInstance * t, std::vector buildList, unsigned int maxDays) const +bool VCAI::tryBuildAnyStructure(const CGTownInstance * t, std::vector buildList, unsigned int maxDays) { for(const auto & building : buildList) { if(t->hasBuilt(building)) continue; - if(tryBuildStructure(t, building, maxDays)) - return true; + return tryBuildThisStructure(t, building, maxDays); + } return false; //Can't build anything } @@ -1261,17 +1275,24 @@ BuildingID VCAI::canBuildAnyStructure(const CGTownInstance * t, std::vector buildList, unsigned int maxDays) const +bool VCAI::tryBuildNextStructure(const CGTownInstance * t, std::vector buildList, unsigned int maxDays) { for(const auto & building : buildList) { if(t->hasBuilt(building)) continue; - return tryBuildStructure(t, building, maxDays); + return tryBuildThisStructure(t, building, maxDays); } return false; //Nothing to build } +void VCAI::buildStructure(const CGTownInstance * t, BuildingID building) +{ + auto name = t->town->buildings.at(building)->Name(); + logAi->debug("Player %d will build %s in town of %s at %s", playerID, name, t->name, t->pos.toString()); + cb->buildBuilding(t, building); //just do this; +} + //Set of buildings for different goals. Does not include any prerequisites. static const BuildingID essential[] = {BuildingID::TAVERN, BuildingID::TOWN_HALL}; static const BuildingID goldSource[] = {BuildingID::TOWN_HALL, BuildingID::CITY_HALL, BuildingID::CAPITOL}; @@ -1287,7 +1308,7 @@ static const BuildingID _spells[] = {BuildingID::MAGES_GUILD_1, BuildingID::MAGE static const BuildingID extra[] = {BuildingID::RESOURCE_SILO, BuildingID::SPECIAL_1, BuildingID::SPECIAL_2, BuildingID::SPECIAL_3, BuildingID::SPECIAL_4, BuildingID::SHIPYARD}; // all remaining buildings -void VCAI::buildStructure(const CGTownInstance * t) const +bool VCAI::tryBuildStructure(const CGTownInstance * t) { //TODO make *real* town development system //TODO: faction-specific development: use special buildings, build dwellings in better order, etc @@ -1299,41 +1320,40 @@ void VCAI::buildStructure(const CGTownInstance * t) const TResources currentIncome = t->dailyIncome(); if(tryBuildAnyStructure(t, std::vector(essential, essential + ARRAY_COUNT(essential)))) - return; + return true; //the more gold the better and less problems later if(tryBuildNextStructure(t, std::vector(goldSource, goldSource + ARRAY_COUNT(goldSource)))) - return; + return true; //workaround for mantis #2696 - build fort and citadel - building castle will be handled without bug - if(vstd::contains(t->builtBuildings, BuildingID::CITY_HALL) && cb->canBuildStructure(t, BuildingID::CAPITOL) != EBuildingState::HAVE_CAPITAL) + if(vstd::contains(t->builtBuildings, BuildingID::CITY_HALL) && + cb->canBuildStructure(t, BuildingID::CAPITOL) != EBuildingState::HAVE_CAPITAL) { if(cb->canBuildStructure(t, BuildingID::CAPITOL) != EBuildingState::FORBIDDEN) { - if(tryBuildNextStructure(t, std::vector(capitolRequirements, capitolRequirements + ARRAY_COUNT(capitolRequirements)))) - return; + if(tryBuildNextStructure(t, std::vector(capitolRequirements, + capitolRequirements + ARRAY_COUNT(capitolRequirements)))) + return true; } } - //save money for capitol or city hall if capitol unavailable, do not build other things (unless gold source buildings are disabled in map editor) - if(!vstd::contains(t->builtBuildings, BuildingID::CAPITOL) && cb->canBuildStructure(t, BuildingID::CAPITOL) != EBuildingState::HAVE_CAPITAL && cb->canBuildStructure(t, BuildingID::CAPITOL) != EBuildingState::FORBIDDEN) - return; - else if(!vstd::contains(t->builtBuildings, BuildingID::CITY_HALL) && cb->canBuildStructure(t, BuildingID::CAPITOL) == EBuildingState::HAVE_CAPITAL && cb->canBuildStructure(t, BuildingID::CITY_HALL) != EBuildingState::FORBIDDEN) - return; - else if(!vstd::contains(t->builtBuildings, BuildingID::TOWN_HALL) && cb->canBuildStructure(t, BuildingID::TOWN_HALL) != EBuildingState::FORBIDDEN) - return; + //TODO: save money for capitol or city hall if capitol unavailable + //do not build other things (unless gold source buildings are disabled in map editor) + if(cb->getDate(Date::DAY_OF_WEEK) > 6) // last 2 days of week - try to focus on growth { if(tryBuildNextStructure(t, std::vector(unitGrowth, unitGrowth + ARRAY_COUNT(unitGrowth)), 2)) - return; + return true; } // first in-game week or second half of any week: try build dwellings if(cb->getDate(Date::DAY) < 7 || cb->getDate(Date::DAY_OF_WEEK) > 3) { - if(tryBuildAnyStructure(t, std::vector(unitsSource, unitsSource + ARRAY_COUNT(unitsSource)), 8 - cb->getDate(Date::DAY_OF_WEEK))) - return; + if(tryBuildAnyStructure(t, std::vector(unitsSource, + unitsSource + ARRAY_COUNT(unitsSource)), 8 - cb->getDate(Date::DAY_OF_WEEK))) + return true; } //try to upgrade dwelling @@ -1341,16 +1361,16 @@ void VCAI::buildStructure(const CGTownInstance * t) const { if(t->hasBuilt(unitsSource[i]) && !t->hasBuilt(unitsUpgrade[i])) { - if(tryBuildStructure(t, unitsUpgrade[i])) - return; + if(tryBuildThisStructure(t, unitsUpgrade[i])) + return true; } } //remaining tasks if(tryBuildNextStructure(t, std::vector(_spells, _spells + ARRAY_COUNT(_spells)))) - return; + return true; if(tryBuildAnyStructure(t, std::vector(extra, extra + ARRAY_COUNT(extra)))) - return; + return true; //at the end, try to get and build any extra buildings with nonstandard slots (for example HotA 3rd level dwelling) std::vector extraBuildings; @@ -1360,7 +1380,9 @@ void VCAI::buildStructure(const CGTownInstance * t) const extraBuildings.push_back(buildingInfo.first); } if(tryBuildAnyStructure(t, extraBuildings)) - return; + return true; + + return false; } bool VCAI::isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, SectorMap & sm) @@ -1419,7 +1441,7 @@ bool VCAI::canRecruitAnyHero(const CGTownInstance * t) const t = findTownWithTavern(); if(!t) return false; - if(cb->getResourceAmount(Res::GOLD) < GameConstants::HERO_GOLD_COST) + if(cb->getResourceAmount(Res::GOLD) < GameConstants::HERO_GOLD_COST) //TODO: use ResourceManager return false; if(cb->getHeroesInfo().size() >= ALLOWED_ROAMING_HEROES) return false; @@ -1602,12 +1624,13 @@ void VCAI::evaluateGoal(HeroPtr h) void VCAI::completeGoal(Goals::TSubgoal goal) { logAi->trace("Completing goal: %s", goal->name()); + ah->notifyGoalCompleted(goal); if(const CGHeroInstance * h = goal->hero.get(true)) { auto it = lockedHeroes.find(h); if(it != lockedHeroes.end()) { - if(it->second == goal) + if(it->second == goal || it->second->fulfillsMe(goal)) //FIXME this is overspecified, fulfillsMe shoudl be complete { logAi->debug(goal->completeMessage()); lockedHeroes.erase(it); //goal fulfilled, free hero @@ -1618,7 +1641,7 @@ void VCAI::completeGoal(Goals::TSubgoal goal) { vstd::erase_if(lockedHeroes, [goal](std::pair p) { - if(*(p.second) == *goal || p.second->fulfillsMe(goal)) //we could have fulfilled goals of other heroes by chance + if(p.second == goal || p.second->fulfillsMe(goal)) //we could have fulfilled goals of other heroes by chance { logAi->debug(p.second->completeMessage()); return true; @@ -2090,30 +2113,19 @@ void VCAI::tryRealize(Goals::VisitHero & g) void VCAI::tryRealize(Goals::BuildThis & g) { - const CGTownInstance * t = g.town; + auto b = BuildingID(g.bid); + auto t = g.town; - if(!t && g.hero) - t = g.hero->visitedTown; - - if(!t) + if (t) { - for(const CGTownInstance * t : cb->getTownsInfo()) + if (cb->canBuildStructure(t, b) == EBuildingState::ALLOWED) { - switch(cb->canBuildStructure(t, BuildingID(g.bid))) - { - case EBuildingState::ALLOWED: - cb->buildBuilding(t, BuildingID(g.bid)); - return; - default: - break; - } + logAi->debug("Player %d will build %s in town of %s at %s", + playerID, t->town->buildings.at(b)->Name(), t->name, t->pos.toString()); + cb->buildBuilding(t, b); + throw goalFulfilledException(sptr(g)); } } - else if(cb->canBuildStructure(t, BuildingID(g.bid)) == EBuildingState::ALLOWED) - { - cb->buildBuilding(t, BuildingID(g.bid)); - return; - } throw cannotFulfillGoalException("Cannot build a given structure!"); } @@ -2132,10 +2144,10 @@ void VCAI::tryRealize(Goals::DigAtTile & g) } } -void VCAI::tryRealize(Goals::CollectRes & g) +void VCAI::tryRealize(Goals::CollectRes & g) //trade { - if(cb->getResourceAmount(static_cast(g.resID)) >= g.value) - throw cannotFulfillGoalException("Goal is already fulfilled!"); + if(ah->freeResources()[g.resID] >= g.value) //goal is already fulfilled. Why we need this chek, anyway? + throw goalFulfilledException(sptr(g)); if(const CGObjectInstance * obj = cb->getObj(ObjectInstanceID(g.objid), false)) { @@ -2143,15 +2155,16 @@ void VCAI::tryRealize(Goals::CollectRes & g) { for(Res::ERes i = Res::WOOD; i <= Res::GOLD; vstd::advance(i, 1)) { - if(i == g.resID) + if(i == g.resID) //sell any other resource continue; + //TODO: check all our reserved resources and avoid them int toGive, toGet; m->getOffer(i, g.resID, toGive, toGet, EMarketMode::RESOURCE_RESOURCE); toGive = toGive * (cb->getResourceAmount(i) / toGive); //TODO trade only as much as needed cb->trade(obj, EMarketMode::RESOURCE_RESOURCE, i, g.resID, toGive); - if(cb->getResourceAmount(static_cast(g.resID)) >= g.value) - return; + if (ah->freeResources()[g.resID] >= g.value) + throw goalFulfilledException(sptr(g)); } throw cannotFulfillGoalException("I cannot get needed resources by trade!"); @@ -2163,28 +2176,87 @@ void VCAI::tryRealize(Goals::CollectRes & g) } else { - saving[g.resID] = 1; throw cannotFulfillGoalException("No object that could be used to raise resources!"); } } void VCAI::tryRealize(Goals::Build & g) { + bool didWeBuildSomething = false; for(const CGTownInstance * t : cb->getTownsInfo()) { logAi->debug("Looking into %s", t->name); - buildStructure(t); - - if(!ai->primaryHero() || - (t->getArmyStrength() > ai->primaryHero()->getArmyStrength() * 2 && !isAccessibleForHero(t->visitablePos(), ai->primaryHero()))) + potentialBuildings.clear(); //start fresh with every town + if (tryBuildStructure(t)) + didWeBuildSomething = true; + else if (potentialBuildings.size()) { - recruitHero(t); - buildArmyIn(t); + auto pb = potentialBuildings.front(); //gather resources for any we can't afford + auto goal = ah->whatToDo(pb.price, sptr(Goals::BuildThis(pb.bid, t))); + if (goal->goalType == Goals::BUILD_STRUCTURE) + { + logAi->error("We were supposed to NOT afford any building"); + buildStructure(t, pb.bid); //do it right now + didWeBuildSomething = true; + } + else + { + //TODO: right now we do that for every town in order. Consider comparison of all potential goals. + striveToGoal(goal); //gather resources, or something else? + } } } - throw cannotFulfillGoalException("BUILD has been realized as much as possible."); + if (!didWeBuildSomething) + throw cannotFulfillGoalException("BUILD has been realized as much as possible."); //who catches it and what for? } + +void VCAI::tryRealize(Goals::BuyArmy & g) +{ + auto t = g.town; + + ui64 valueBought = 0; + //buy the stacks with largest AI value + + while (valueBought < g.value) + { + auto res = ah->allResources(); + std::vector creaturesInDwellings; + for (int i = 0; i < t->creatures.size(); i++) + { + auto ci = infoFromDC(t->creatures[i]); + ci.level = i; //this is important for Dungeon Summoning Portal + creaturesInDwellings.push_back(ci); + } + vstd::erase_if(creaturesInDwellings, [](const creInfo & ci) -> bool + { + return !ci.count || ci.creID == -1; + }); + if (creaturesInDwellings.empty()) + throw cannotFulfillGoalException("Can't buy any more creatures!"); + + creInfo ci = + *boost::max_element(creaturesInDwellings, [&res](const creInfo & lhs, const creInfo & rhs) + { + //max value of creatures we can buy with our res + int value1 = lhs.cre->AIValue * (std::min(lhs.count, res / lhs.cre->cost)), + value2 = rhs.cre->AIValue * (std::min(rhs.count, res / rhs.cre->cost)); + + return value1 < value2; + }); + + vstd::amin(ci.count, res / ci.cre->cost); //max count we can afford + if (ci.count > 0) + { + cb->recruitCreatures(t, t->getUpperArmy(), ci.creID, ci.count, ci.level); + valueBought += ci.count * ci.cre->AIValue; + } + else + throw cannotFulfillGoalException("Can't buy any more creatures!"); + } + throw goalFulfilledException(sptr(g)); //we bought as many creatures as we wanted +} + void VCAI::tryRealize(Goals::Invalid & g) { throw cannotFulfillGoalException("I don't know how to fulfill this!"); @@ -2303,7 +2375,7 @@ Goals::TSubgoal VCAI::striveToGoalInternal(Goals::TSubgoal ultimateGoal, bool on boost::this_thread::interruption_point(); goal = goal->whatToDoToAchieve(); --maxGoals; - if(*goal == *ultimateGoal) //compare objects by value + if(goal == ultimateGoal) //compare objects by value throw cannotFulfillGoalException("Goal dependency loop detected!"); } catch(goalFulfilledException & e) @@ -2319,7 +2391,6 @@ Goals::TSubgoal VCAI::striveToGoalInternal(Goals::TSubgoal ultimateGoal, bool on return sptr(Goals::Invalid()); } } - try { boost::this_thread::interruption_point(); @@ -2328,10 +2399,10 @@ Goals::TSubgoal VCAI::striveToGoalInternal(Goals::TSubgoal ultimateGoal, bool on { if(ultimateGoal->hero) // we seemingly don't know what to do with hero, free him vstd::erase_if_present(lockedHeroes, ultimateGoal->hero); - std::runtime_error e("Too many subgoals, don't know what to do"); - throw (e); + throw (std::runtime_error("Too many subgoals, don't know what to do")); + } - else //we can proceed + else //we found elementar goal and can proceed { if(goal->hero) //lock this hero to fulfill ultimate goal { @@ -2345,7 +2416,7 @@ Goals::TSubgoal VCAI::striveToGoalInternal(Goals::TSubgoal ultimateGoal, bool on logAi->debug("Choosing abstract goal %s", goal->name()); break; } - else + else //try realize { logAi->debug("Trying to realize %s (value %2.3f)", goal->name(), goal->priority); goal->accept(this); @@ -2360,7 +2431,9 @@ Goals::TSubgoal VCAI::striveToGoalInternal(Goals::TSubgoal ultimateGoal, bool on } catch(goalFulfilledException & e) { - //the goal was completed successfully + //the sub-goal was completed successfully + completeGoal(e.goal); + //local goal was also completed... TODO: or not? completeGoal(goal); //completed goal was main goal //TODO: find better condition if(ultimateGoal->fulfillsMe(goal) || maxGoals > searchDepth2) @@ -2515,11 +2588,6 @@ void VCAI::striveToQuest(const QuestInfo & q) void VCAI::performTypicalActions() { - //TODO: build army only on request - for(auto t : cb->getTownsInfo()) - { - buildArmyIn(t); - } for(auto h : getUnblockedHeroes()) { if(!h) //hero might be lost. getUnblockedHeroes() called once on start of turn @@ -2688,49 +2756,6 @@ int3 VCAI::explorationDesperate(HeroPtr h) return bestTile; } -TResources VCAI::estimateIncome() const -{ - TResources ret; - for(const CGTownInstance * t : cb->getTownsInfo()) - { - ret += t->dailyIncome(); - } - - for(const CGObjectInstance * obj : getFlaggedObjects()) - { - if(obj->ID == Obj::MINE) - { - switch(obj->subID) - { - case Res::WOOD: - case Res::ORE: - ret[obj->subID] += WOOD_ORE_MINE_PRODUCTION; - break; - case Res::GOLD: - case 7: //abandoned mine -> also gold - ret[Res::GOLD] += GOLD_MINE_PRODUCTION; - break; - default: - ret[obj->subID] += RESOURCE_MINE_PRODUCTION; - break; - } - } - } - - return ret; -} - -bool VCAI::containsSavedRes(const TResources & cost) const -{ - for(int i = 0; i < GameConstants::RESOURCE_QUANTITY; i++) - { - if(saving[i] && cost[i]) - return true; - } - - return false; -} - void VCAI::checkHeroArmy(HeroPtr h) { auto it = lockedHeroes.find(h); @@ -2755,6 +2780,7 @@ void VCAI::recruitHero(const CGTownInstance * t, bool throwing) hero = heroes[1]; } cb->recruitHero(t, hero); + throw goalFulfilledException(sptr(Goals::RecruitHero().settown(t))); } else if(throwing) { @@ -2849,20 +2875,6 @@ void VCAI::validateObject(ObjectIdRef obj) } } -TResources VCAI::freeResources() const -{ - TResources myRes = cb->getResourceAmount(); - auto iterator = cb->getTownsInfo(); - if(std::none_of(iterator.begin(), iterator.end(), [](const CGTownInstance * x) -> bool - { - return x->builtBuildings.find(BuildingID::CAPITOL) != x->builtBuildings.end(); - }) - /*|| std::all_of(iterator.begin(), iterator.end(), [](const CGTownInstance * x) -> bool { return x->forbiddenBuildings.find(BuildingID::CAPITOL) != x->forbiddenBuildings.end(); })*/ ) - myRes[Res::GOLD] -= GOLD_RESERVE; //what if capitol is blocked from building in all possessed towns (set in map editor)? What about reserve for city hall or something similar in that case? - vstd::amax(myRes[Res::GOLD], 0); - return myRes; -} - std::shared_ptr VCAI::getCachedSectorMap(HeroPtr h) { auto it = cachedSectorMaps.find(h); @@ -3271,8 +3283,7 @@ bool shouldVisit(HeroPtr h, const CGObjectInstance * obj) case Obj::SCHOOL_OF_MAGIC: case Obj::SCHOOL_OF_WAR: { - TResources myRes = ai->myCb->getResourceAmount(); - if(myRes[Res::GOLD] - GOLD_RESERVE < 1000) + if (ah->freeGold() < 1000) return false; break; } @@ -3282,8 +3293,8 @@ bool shouldVisit(HeroPtr h, const CGObjectInstance * obj) break; case Obj::TREE_OF_KNOWLEDGE: { - TResources myRes = ai->myCb->getResourceAmount(); - if(myRes[Res::GOLD] - GOLD_RESERVE < 2000 || myRes[Res::GEMS] < 10) + TResources myRes = ah->freeResources(); + if(myRes[Res::GOLD] < 2000 || myRes[Res::GEMS] < 10) return false; break; } @@ -3297,7 +3308,7 @@ bool shouldVisit(HeroPtr h, const CGObjectInstance * obj) //TODO: only on request if(ai->myCb->getHeroesInfo().size() >= VLC->modh->settings.MAX_HEROES_ON_MAP_PER_PLAYER) return false; - else if(ai->myCb->getResourceAmount()[Res::GOLD] - GOLD_RESERVE < GameConstants::HERO_GOLD_COST) + else if(ah->freeGold() < GameConstants::HERO_GOLD_COST) return false; break; } diff --git a/AI/VCAI/VCAI.h b/AI/VCAI/VCAI.h index 6884a51e9..fab6d08b3 100644 --- a/AI/VCAI/VCAI.h +++ b/AI/VCAI/VCAI.h @@ -126,21 +126,33 @@ struct SectorMap int3 findFirstVisitableTile(HeroPtr h, crint3 dst); }; -class VCAI : public CAdventureAI +class DLL_EXPORT VCAI : public CAdventureAI { public: //internal methods for town development + //TODO: refactor to separate class BuildManager - //try build an unbuilt structure in maxDays at most (0 = indefinite) - /*bool canBuildStructure(const CGTownInstance * t, BuildingID building, unsigned int maxDays=7);*/ - bool tryBuildStructure(const CGTownInstance * t, BuildingID building, unsigned int maxDays = 7) const; + //try build anything in given town, and execute resulting Goal if any + bool tryBuildStructure(const CGTownInstance * t); + bool tryBuildAnyStructure(const CGTownInstance * t, std::vector buildList, unsigned int maxDays = 7); + //try build first unbuilt structure + + bool tryBuildThisStructure(const CGTownInstance * t, BuildingID building, unsigned int maxDays = 7); //try build ANY unbuilt structure BuildingID canBuildAnyStructure(const CGTownInstance * t, std::vector buildList, unsigned int maxDays = 7) const; - bool tryBuildAnyStructure(const CGTownInstance * t, std::vector buildList, unsigned int maxDays = 7) const; - //try build first unbuilt structure - bool tryBuildNextStructure(const CGTownInstance * t, std::vector buildList, unsigned int maxDays = 7) const; + bool tryBuildNextStructure(const CGTownInstance * t, std::vector buildList, unsigned int maxDays = 7); + void buildStructure(const CGTownInstance * t, BuildingID building); //actually execute build operation + + struct PotentialBuilding + { + BuildingID bid; + TResources price; + //days to build? + }; + std::vector potentialBuildings; //what we can build in current town friend class FuzzyHelper; + friend class ResourceManager; std::map> knownTeleportChannels; std::map knownSubterraneanGates; @@ -161,8 +173,6 @@ public: std::map> cachedSectorMaps; //TODO: serialize? not necessary - TResources saving; - AIStatus status; std::string battlename; @@ -182,6 +192,7 @@ public: void tryRealize(Goals::DigAtTile & g); void tryRealize(Goals::CollectRes & g); void tryRealize(Goals::Build & g); + void tryRealize(Goals::BuyArmy & g); void tryRealize(Goals::Invalid & g); void tryRealize(Goals::AbstractGoal & g); @@ -189,71 +200,70 @@ public: int3 explorationNewPoint(HeroPtr h); int3 explorationDesperate(HeroPtr h); bool isTileNotReserved(const CGHeroInstance * h, int3 t); //the tile is not occupied by allied hero and the object is not reserved - void recruitHero(); - virtual std::string getBattleAIName() const override; + std::string getBattleAIName() const override; - virtual void init(std::shared_ptr CB) override; - virtual void yourTurn() override; + void init(std::shared_ptr CB) override; + void yourTurn() override; - virtual void heroGotLevel(const CGHeroInstance * hero, PrimarySkill::PrimarySkill pskill, std::vector & skills, QueryID queryID) override; //pskill is gained primary skill, interface has to choose one of given skills and call callback with selection id - virtual void commanderGotLevel(const CCommanderInstance * commander, std::vector skills, QueryID queryID) override; //TODO - virtual void showBlockingDialog(const std::string & text, const std::vector & components, QueryID askID, const int soundID, bool selection, bool cancel) override; //Show a dialog, player must take decision. If selection then he has to choose between one of given components, if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called with number of selected component (1 - n) or 0 for cancel (if allowed) and askID. - virtual void showGarrisonDialog(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits, QueryID queryID) override; //all stacks operations between these objects become allowed, interface has to call onEnd when done - virtual void showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override; + void heroGotLevel(const CGHeroInstance * hero, PrimarySkill::PrimarySkill pskill, std::vector & skills, QueryID queryID) override; //pskill is gained primary skill, interface has to choose one of given skills and call callback with selection id + void commanderGotLevel(const CCommanderInstance * commander, std::vector skills, QueryID queryID) override; //TODO + void showBlockingDialog(const std::string & text, const std::vector & components, QueryID askID, const int soundID, bool selection, bool cancel) override; //Show a dialog, player must take decision. If selection then he has to choose between one of given components, if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called with number of selected component (1 - n) or 0 for cancel (if allowed) and askID. + void showGarrisonDialog(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits, QueryID queryID) override; //all stacks operations between these objects become allowed, interface has to call onEnd when done + void showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override; void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) override; - virtual void saveGame(BinarySerializer & h, const int version) override; //saving - virtual void loadGame(BinaryDeserializer & h, const int version) override; //loading - virtual void finish() override; + void saveGame(BinarySerializer & h, const int version) override; //saving + void loadGame(BinaryDeserializer & h, const int version) override; //loading + void finish() override; - virtual void availableCreaturesChanged(const CGDwelling * town) override; - virtual void heroMoved(const TryMoveHero & details) override; - virtual void heroInGarrisonChange(const CGTownInstance * town) override; - virtual void centerView(int3 pos, int focusTime) override; - virtual void tileHidden(const std::unordered_set & pos) override; - virtual void artifactMoved(const ArtifactLocation & src, const ArtifactLocation & dst) override; - virtual void artifactAssembled(const ArtifactLocation & al) override; - virtual void showTavernWindow(const CGObjectInstance * townOrTavern) override; - virtual void showThievesGuildWindow(const CGObjectInstance * obj) override; - virtual void playerBlocked(int reason, bool start) override; - virtual void showPuzzleMap() override; - virtual void showShipyardDialog(const IShipyard * obj) override; - virtual void gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) override; - virtual void artifactPut(const ArtifactLocation & al) override; - virtual void artifactRemoved(const ArtifactLocation & al) override; - virtual void artifactDisassembled(const ArtifactLocation & al) override; - virtual void heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start) override; - virtual void availableArtifactsChanged(const CGBlackMarket * bm = nullptr) override; - virtual void heroVisitsTown(const CGHeroInstance * hero, const CGTownInstance * town) override; - virtual void tileRevealed(const std::unordered_set & pos) override; - virtual void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) override; - virtual void heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val) override; - virtual void showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level) override; - virtual void heroMovePointsChanged(const CGHeroInstance * hero) override; - virtual void garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) override; - virtual void newObject(const CGObjectInstance * obj) override; - virtual void showHillFortWindow(const CGObjectInstance * object, const CGHeroInstance * visitor) override; - virtual void playerBonusChanged(const Bonus & bonus, bool gain) override; - virtual void heroCreated(const CGHeroInstance *) override; - virtual void advmapSpellCast(const CGHeroInstance * caster, int spellID) override; - virtual void showInfoDialog(const std::string & text, const std::vector & components, int soundID) override; - virtual void requestRealized(PackageApplied * pa) override; - virtual void receivedResource() override; - virtual void objectRemoved(const CGObjectInstance * obj) override; - virtual void showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor) override; - virtual void heroManaPointsChanged(const CGHeroInstance * hero) override; - virtual void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) override; - virtual void battleResultsApplied() override; - virtual void objectPropertyChanged(const SetObjectProperty * sop) override; - virtual void buildChanged(const CGTownInstance * town, BuildingID buildingID, int what) override; - virtual void heroBonusChanged(const CGHeroInstance * hero, const Bonus & bonus, bool gain) override; - virtual void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor) override; + void availableCreaturesChanged(const CGDwelling * town) override; + void heroMoved(const TryMoveHero & details) override; + void heroInGarrisonChange(const CGTownInstance * town) override; + void centerView(int3 pos, int focusTime) override; + void tileHidden(const std::unordered_set & pos) override; + void artifactMoved(const ArtifactLocation & src, const ArtifactLocation & dst) override; + void artifactAssembled(const ArtifactLocation & al) override; + void showTavernWindow(const CGObjectInstance * townOrTavern) override; + void showThievesGuildWindow(const CGObjectInstance * obj) override; + void playerBlocked(int reason, bool start) override; + void showPuzzleMap() override; + void showShipyardDialog(const IShipyard * obj) override; + void gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) override; + void artifactPut(const ArtifactLocation & al) override; + void artifactRemoved(const ArtifactLocation & al) override; + void artifactDisassembled(const ArtifactLocation & al) override; + void heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start) override; + void availableArtifactsChanged(const CGBlackMarket * bm = nullptr) override; + void heroVisitsTown(const CGHeroInstance * hero, const CGTownInstance * town) override; + void tileRevealed(const std::unordered_set & pos) override; + void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) override; + void heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val) override; + void showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level) override; + void heroMovePointsChanged(const CGHeroInstance * hero) override; + void garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) override; + void newObject(const CGObjectInstance * obj) override; + void showHillFortWindow(const CGObjectInstance * object, const CGHeroInstance * visitor) override; + void playerBonusChanged(const Bonus & bonus, bool gain) override; + void heroCreated(const CGHeroInstance *) override; + void advmapSpellCast(const CGHeroInstance * caster, int spellID) override; + void showInfoDialog(const std::string & text, const std::vector & components, int soundID) override; + void requestRealized(PackageApplied * pa) override; + void receivedResource() override; + void objectRemoved(const CGObjectInstance * obj) override; + void showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor) override; + void heroManaPointsChanged(const CGHeroInstance * hero) override; + void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) override; + void battleResultsApplied() override; + void objectPropertyChanged(const SetObjectProperty * sop) override; + void buildChanged(const CGTownInstance * town, BuildingID buildingID, int what) override; + void heroBonusChanged(const CGHeroInstance * hero, const Bonus & bonus, bool gain) override; + void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor) override; void showWorldViewEx(const std::vector & objectPositions) override; - virtual void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side) override; - virtual void battleEnd(const BattleResult * br) override; - void makeTurn(); + void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side) override; + void battleEnd(const BattleResult * br) override; + void makeTurn(); void makeTurnInternal(); void performTypicalActions(); @@ -269,7 +279,6 @@ public: void recruitHero(const CGTownInstance * t, bool throwing = false); bool isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, SectorMap & sm); - void buildStructure(const CGTownInstance * t) const; //void recruitCreatures(const CGTownInstance * t); void recruitCreatures(const CGDwelling * d, const CArmedInstance * recruiter); bool canGetArmy(const CGHeroInstance * h, const CGHeroInstance * source); //can we get any better stacks from other hero? @@ -299,7 +308,7 @@ public: void validateVisitableObjs(); void retrieveVisitableObjs(std::vector & out, bool includeOwned = false) const; void retrieveVisitableObjs(); - std::vector getFlaggedObjects() const; + virtual std::vector getFlaggedObjects() const; const CGObjectInstance * lookForArt(int aid) const; bool isAccessible(const int3 & pos); @@ -317,9 +326,6 @@ public: bool canAct(HeroPtr h) const; std::vector getUnblockedHeroes() const; HeroPtr primaryHero() const; - TResources freeResources() const; //owned resources minus gold reserve - TResources estimateIncome() const; - bool containsSavedRes(const TResources & cost) const; void checkHeroArmy(HeroPtr h); void requestSent(const CPackForServer * pack, int requestID) override; @@ -406,7 +412,11 @@ public: h & visitableObjs; h & alreadyVisited; h & reservedObjs; - h & saving; + if (version < 788 && !h.saving) + { + TResources saving; + h & saving; //mind the ambiguity + } h & status; h & battlename; h & heroesUnableToExplore; diff --git a/lib/CGameInfoCallback.h b/lib/CGameInfoCallback.h index 4715a4f39..84f3542b8 100644 --- a/lib/CGameInfoCallback.h +++ b/lib/CGameInfoCallback.h @@ -50,102 +50,102 @@ protected: public: //various - int getDate(Date::EDateType mode=Date::DAY)const; //mode=0 - total days in game, mode=1 - day of week, mode=2 - current week, mode=3 - current month - const StartInfo * getStartInfo(bool beforeRandomization = false)const; - bool isAllowed(int type, int id); //type: 0 - spell; 1- artifact; 2 - secondary skill + virtual int getDate(Date::EDateType mode=Date::DAY)const; //mode=0 - total days in game, mode=1 - day of week, mode=2 - current week, mode=3 - current month + virtual const StartInfo * getStartInfo(bool beforeRandomization = false)const; + virtual bool isAllowed(int type, int id); //type: 0 - spell; 1- artifact; 2 - secondary skill //player - const PlayerState * getPlayer(PlayerColor color, bool verbose = true) const; - int getResource(PlayerColor Player, Res::ERes which) const; - bool isVisible(int3 pos) const; - PlayerRelations::PlayerRelations getPlayerRelations(PlayerColor color1, PlayerColor color2) const; - void getThievesGuildInfo(SThievesGuildInfo & thi, const CGObjectInstance * obj); //get thieves' guild info obtainable while visiting given object - EPlayerStatus::EStatus getPlayerStatus(PlayerColor player, bool verbose = true) const; //-1 if no such player - PlayerColor getCurrentPlayer() const; //player that currently makes move // TODO synchronous turns + virtual const PlayerState * getPlayer(PlayerColor color, bool verbose = true) const; + virtual int getResource(PlayerColor Player, Res::ERes which) const; + virtual bool isVisible(int3 pos) const; + virtual PlayerRelations::PlayerRelations getPlayerRelations(PlayerColor color1, PlayerColor color2) const; + virtual void getThievesGuildInfo(SThievesGuildInfo & thi, const CGObjectInstance * obj); //get thieves' guild info obtainable while visiting given object + virtual EPlayerStatus::EStatus getPlayerStatus(PlayerColor player, bool verbose = true) const; //-1 if no such player + virtual PlayerColor getCurrentPlayer() const; //player that currently makes move // TODO synchronous turns virtual PlayerColor getLocalPlayer() const; //player that is currently owning given client (if not a client, then returns current player) - const PlayerSettings * getPlayerSettings(PlayerColor color) const; + virtual const PlayerSettings * getPlayerSettings(PlayerColor color) const; //armed object - void getUpgradeInfo(const CArmedInstance *obj, SlotID stackPos, UpgradeInfo &out)const; + virtual void getUpgradeInfo(const CArmedInstance *obj, SlotID stackPos, UpgradeInfo &out)const; //hero - const CGHeroInstance* getHero(ObjectInstanceID objid) const; - const CGHeroInstance* getHeroWithSubid(int subid) const; - int getHeroCount(PlayerColor player, bool includeGarrisoned) const; - bool getHeroInfo(const CGObjectInstance * hero, InfoAboutHero & dest, const CGObjectInstance * selectedObject = nullptr) const; - int getSpellCost(const CSpell * sp, const CGHeroInstance * caster) const; //when called during battle, takes into account creatures' spell cost reduction - int64_t estimateSpellDamage(const CSpell * sp, const CGHeroInstance * hero) const; //estimates damage of given spell; returns 0 if spell causes no dmg - const CArtifactInstance * getArtInstance(ArtifactInstanceID aid) const; - const CGObjectInstance * getObjInstance(ObjectInstanceID oid) const; - const CGObjectInstance * getArmyInstance(ObjectInstanceID oid) const; + virtual const CGHeroInstance* getHero(ObjectInstanceID objid) const; + virtual const CGHeroInstance* getHeroWithSubid(int subid) const; + virtual int getHeroCount(PlayerColor player, bool includeGarrisoned) const; + virtual bool getHeroInfo(const CGObjectInstance * hero, InfoAboutHero & dest, const CGObjectInstance * selectedObject = nullptr) const; + virtual int getSpellCost(const CSpell * sp, const CGHeroInstance * caster) const; //when called during battle, takes into account creatures' spell cost reduction + virtual int64_t estimateSpellDamage(const CSpell * sp, const CGHeroInstance * hero) const; //estimates damage of given spell; returns 0 if spell causes no dmg + virtual const CArtifactInstance * getArtInstance(ArtifactInstanceID aid) const; + virtual const CGObjectInstance * getObjInstance(ObjectInstanceID oid) const; + //virtual const CGObjectInstance * getArmyInstance(ObjectInstanceID oid) const; //objects - const CGObjectInstance* getObj(ObjectInstanceID objid, bool verbose = true) const; - std::vector getBlockingObjs(int3 pos)const; - std::vector getVisitableObjs(int3 pos, bool verbose = true)const; - std::vector getFlaggableObjects(int3 pos) const; - const CGObjectInstance * getTopObj (int3 pos) const; - PlayerColor getOwner(ObjectInstanceID heroID) const; - const CGObjectInstance *getObjByQuestIdentifier(int identifier) const; //nullptr if object has been removed (eg. killed) + virtual const CGObjectInstance* getObj(ObjectInstanceID objid, bool verbose = true) const; + virtual std::vector getBlockingObjs(int3 pos)const; + virtual std::vector getVisitableObjs(int3 pos, bool verbose = true)const; + virtual std::vector getFlaggableObjects(int3 pos) const; + virtual const CGObjectInstance * getTopObj (int3 pos) const; + virtual PlayerColor getOwner(ObjectInstanceID heroID) const; + virtual const CGObjectInstance *getObjByQuestIdentifier(int identifier) const; //nullptr if object has been removed (eg. killed) //map - int3 guardingCreaturePosition (int3 pos) const; - std::vector getGuardingCreatures (int3 pos) const; - const CMapHeader * getMapHeader()const; - int3 getMapSize() const; //returns size of map - z is 1 for one - level map and 2 for two level map - const TerrainTile * getTile(int3 tile, bool verbose = true) const; - std::shared_ptr> getAllVisibleTiles() const; - bool isInTheMap(const int3 &pos) const; - void getVisibleTilesInRange(std::unordered_set &tiles, int3 pos, int radious, int3::EDistanceFormula distanceFormula = int3::DIST_2D) const; + virtual int3 guardingCreaturePosition (int3 pos) const; + virtual std::vector getGuardingCreatures (int3 pos) const; + virtual const CMapHeader * getMapHeader()const; + virtual int3 getMapSize() const; //returns size of map - z is 1 for one - level map and 2 for two level map + virtual const TerrainTile * getTile(int3 tile, bool verbose = true) const; + virtual std::shared_ptr> getAllVisibleTiles() const; + virtual bool isInTheMap(const int3 &pos) const; + virtual void getVisibleTilesInRange(std::unordered_set &tiles, int3 pos, int radious, int3::EDistanceFormula distanceFormula = int3::DIST_2D) const; //town - const CGTownInstance* getTown(ObjectInstanceID objid) const; - int howManyTowns(PlayerColor Player) const; - const CGTownInstance * getTownInfo(int val, bool mode)const; //mode = 0 -> val = player town serial; mode = 1 -> val = object id (serial) - std::vector getAvailableHeroes(const CGObjectInstance * townOrTavern) const; //heroes that can be recruited - std::string getTavernRumor(const CGObjectInstance * townOrTavern) const; - EBuildingState::EBuildingState canBuildStructure(const CGTownInstance *t, BuildingID ID);//// 0 - no more than one capitol, 1 - lack of water, 2 - forbidden, 3 - Add another level to Mage Guild, 4 - already built, 5 - cannot build, 6 - cannot afford, 7 - build, 8 - lack of requirements + virtual const CGTownInstance* getTown(ObjectInstanceID objid) const; + virtual int howManyTowns(PlayerColor Player) const; + //virtual const CGTownInstance * getTownInfo(int val, bool mode)const; //mode = 0 -> val = player town serial; mode = 1 -> val = object id (serial) + virtual std::vector getAvailableHeroes(const CGObjectInstance * townOrTavern) const; //heroes that can be recruited + virtual std::string getTavernRumor(const CGObjectInstance * townOrTavern) const; + virtual EBuildingState::EBuildingState canBuildStructure(const CGTownInstance *t, BuildingID ID);//// 0 - no more than one capitol, 1 - lack of water, 2 - forbidden, 3 - Add another level to Mage Guild, 4 - already built, 5 - cannot build, 6 - cannot afford, 7 - build, 8 - lack of requirements virtual bool getTownInfo(const CGObjectInstance * town, InfoAboutTown & dest, const CGObjectInstance * selectedObject = nullptr) const; - const CTown *getNativeTown(PlayerColor color) const; + virtual const CTown *getNativeTown(PlayerColor color) const; //from gs - const TeamState *getTeam(TeamID teamID) const; - const TeamState *getPlayerTeam(PlayerColor color) const; - EBuildingState::EBuildingState canBuildStructure(const CGTownInstance *t, BuildingID ID) const;// 0 - no more than one capitol, 1 - lack of water, 2 - forbidden, 3 - Add another level to Mage Guild, 4 - already built, 5 - cannot build, 6 - cannot afford, 7 - build, 8 - lack of requirements + virtual const TeamState *getTeam(TeamID teamID) const; + virtual const TeamState *getPlayerTeam(PlayerColor color) const; + //virtual EBuildingState::EBuildingState canBuildStructure(const CGTownInstance *t, BuildingID ID) const;// 0 - no more than one capitol, 1 - lack of water, 2 - forbidden, 3 - Add another level to Mage Guild, 4 - already built, 5 - cannot build, 6 - cannot afford, 7 - build, 8 - lack of requirements //teleport - std::vector getVisibleTeleportObjects(std::vector ids, PlayerColor player) const; - std::vector getTeleportChannelEntraces(TeleportChannelID id, PlayerColor Player = PlayerColor::UNFLAGGABLE) const; - std::vector getTeleportChannelExits(TeleportChannelID id, PlayerColor Player = PlayerColor::UNFLAGGABLE) const; - ETeleportChannelType getTeleportChannelType(TeleportChannelID id, PlayerColor player = PlayerColor::UNFLAGGABLE) const; - bool isTeleportChannelImpassable(TeleportChannelID id, PlayerColor player = PlayerColor::UNFLAGGABLE) const; - bool isTeleportChannelBidirectional(TeleportChannelID id, PlayerColor player = PlayerColor::UNFLAGGABLE) const; - bool isTeleportChannelUnidirectional(TeleportChannelID id, PlayerColor player = PlayerColor::UNFLAGGABLE) const; - bool isTeleportEntrancePassable(const CGTeleport * obj, PlayerColor player) const; + virtual std::vector getVisibleTeleportObjects(std::vector ids, PlayerColor player) const; + virtual std::vector getTeleportChannelEntraces(TeleportChannelID id, PlayerColor Player = PlayerColor::UNFLAGGABLE) const; + virtual std::vector getTeleportChannelExits(TeleportChannelID id, PlayerColor Player = PlayerColor::UNFLAGGABLE) const; + virtual ETeleportChannelType getTeleportChannelType(TeleportChannelID id, PlayerColor player = PlayerColor::UNFLAGGABLE) const; + virtual bool isTeleportChannelImpassable(TeleportChannelID id, PlayerColor player = PlayerColor::UNFLAGGABLE) const; + virtual bool isTeleportChannelBidirectional(TeleportChannelID id, PlayerColor player = PlayerColor::UNFLAGGABLE) const; + virtual bool isTeleportChannelUnidirectional(TeleportChannelID id, PlayerColor player = PlayerColor::UNFLAGGABLE) const; + virtual bool isTeleportEntrancePassable(const CGTeleport * obj, PlayerColor player) const; }; class DLL_LINKAGE CPlayerSpecificInfoCallback : public CGameInfoCallback { public: - int howManyTowns() const; - int howManyHeroes(bool includeGarrisoned = true) const; - int3 getGrailPos(double *outKnownRatio); - boost::optional getMyColor() const; + virtual int howManyTowns() const; + virtual int howManyHeroes(bool includeGarrisoned = true) const; + virtual int3 getGrailPos(double *outKnownRatio); + virtual boost::optional getMyColor() const; - std::vector getTownsInfo(bool onlyOur = true) const; //true -> only owned; false -> all visible - int getHeroSerial(const CGHeroInstance * hero, bool includeGarrisoned=true) const; - const CGTownInstance* getTownBySerial(int serialId) const; // serial id is [0, number of towns) - const CGHeroInstance* getHeroBySerial(int serialId, bool includeGarrisoned=true) const; // serial id is [0, number of heroes) - std::vector getHeroesInfo(bool onlyOur = true) const; //true -> only owned; false -> all visible - std::vector getMyDwellings() const; //returns all dwellings that belong to player - std::vector getMyObjects() const; //returns all objects flagged by belonging player - std::vector getMyQuests() const; + virtual std::vector getTownsInfo(bool onlyOur = true) const; //true -> only owned; false -> all visible + virtual int getHeroSerial(const CGHeroInstance * hero, bool includeGarrisoned=true) const; + virtual const CGTownInstance* getTownBySerial(int serialId) const; // serial id is [0, number of towns) + virtual const CGHeroInstance* getHeroBySerial(int serialId, bool includeGarrisoned=true) const; // serial id is [0, number of heroes) + virtual std::vector getHeroesInfo(bool onlyOur = true) const; //true -> only owned; false -> all visible + virtual std::vector getMyDwellings() const; //returns all dwellings that belong to player + virtual std::vector getMyObjects() const; //returns all objects flagged by belonging player + virtual std::vector getMyQuests() const; - int getResourceAmount(Res::ERes type) const; - TResources getResourceAmount() const; - const std::vector< std::vector< std::vector > > & getVisibilityMap()const; //returns visibility map - const PlayerSettings * getPlayerSettings(PlayerColor color) const; + virtual int getResourceAmount(Res::ERes type) const; + virtual TResources getResourceAmount() const; + virtual const std::vector< std::vector< std::vector > > & getVisibilityMap()const; //returns visibility map + //virtual const PlayerSettings * getPlayerSettings(PlayerColor color) const; }; class DLL_LINKAGE IGameEventRealizer diff --git a/lib/ResourceSet.cpp b/lib/ResourceSet.cpp index 06d5c62f4..0a4e5ddf9 100644 --- a/lib/ResourceSet.cpp +++ b/lib/ResourceSet.cpp @@ -28,6 +28,21 @@ Res::ResourceSet::ResourceSet(const JsonNode & node) push_back(node[name].Float()); } +Res::ResourceSet::ResourceSet(TResource wood, TResource mercury, TResource ore, TResource sulfur, TResource crystal, + TResource gems, TResource gold, TResource mithril) +{ + resize(GameConstants::RESOURCE_QUANTITY); + auto d = data(); + d[Res::WOOD] = wood; + d[Res::MERCURY] = mercury; + d[Res::ORE] = ore; + d[Res::SULFUR] = sulfur; + d[Res::CRYSTAL] = crystal; + d[Res::GEMS] = gems; + d[Res::GOLD] = gold; + d[Res::MITHRIL] = mithril; +} + void Res::ResourceSet::serializeJson(JsonSerializeFormat & handler, const std::string & fieldName) { if(!handler.saving) diff --git a/lib/ResourceSet.h b/lib/ResourceSet.h index d143e6fad..7a6e58123 100644 --- a/lib/ResourceSet.h +++ b/lib/ResourceSet.h @@ -25,7 +25,8 @@ namespace Res { WOOD = 0, MERCURY, ORE, SULFUR, CRYSTAL, GEMS, GOLD, MITHRIL, - WOOD_AND_ORE = 127 // special case for town bonus resource + WOOD_AND_ORE = 127, // special case for town bonus resource + INVALID = -1 }; //class to be representing a vector of resource @@ -35,6 +36,8 @@ namespace Res DLL_LINKAGE ResourceSet(); // read resources set from json. Format example: { "gold": 500, "wood":5 } DLL_LINKAGE ResourceSet(const JsonNode & node); + DLL_LINKAGE ResourceSet(TResource wood, TResource mercury, TResource ore, TResource sulfur, TResource crystal, + TResource gems, TResource gold, TResource mithril = 0); #define scalarOperator(OPSIGN) \ diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index c970aac73..79d1e7730 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -1262,6 +1262,18 @@ bool CGTownInstance::hasBuilt(BuildingID buildingID, int townID) const return false; } +TResources CGTownInstance::getBuildingCost(BuildingID buildingID) const +{ + if (vstd::contains(town->buildings, buildingID)) + return town->buildings.at(buildingID)->resources; + else + { + logGlobal->error("Town %s at %s has no possible building %d!", name, pos.toString(), buildingID.toEnum()); + return TResources(); + } + +} + bool CGTownInstance::hasBuilt(BuildingID buildingID) const { return vstd::contains(builtBuildings, buildingID); diff --git a/lib/mapObjects/CGTownInstance.h b/lib/mapObjects/CGTownInstance.h index 203d510aa..4b5f5801d 100644 --- a/lib/mapObjects/CGTownInstance.h +++ b/lib/mapObjects/CGTownInstance.h @@ -265,6 +265,7 @@ public: //checks if building is constructed and town has same subID bool hasBuilt(BuildingID buildingID) const; bool hasBuilt(BuildingID buildingID, int townID) const; + TResources getBuildingCost(BuildingID buildingID) const; TResources dailyIncome() const; //calculates daily income of this town int spellsAtLevel(int level, bool checkGuild) const; //levels are counted from 1 (1 - 5) bool armedGarrison() const; //true if town has creatures in garrison or garrisoned hero diff --git a/lib/rmg/CMapGenOptions.cpp b/lib/rmg/CMapGenOptions.cpp index 321ae123d..42d3b9319 100644 --- a/lib/rmg/CMapGenOptions.cpp +++ b/lib/rmg/CMapGenOptions.cpp @@ -137,7 +137,7 @@ EMonsterStrength::EMonsterStrength CMapGenOptions::getMonsterStrength() const void CMapGenOptions::setMonsterStrength(EMonsterStrength::EMonsterStrength value) { - monsterStrength = value; + monsterStrength = value; } void CMapGenOptions::resetPlayersMap() diff --git a/lib/serializer/CSerializer.h b/lib/serializer/CSerializer.h index 1e16806c6..8c1e1e5b0 100644 --- a/lib/serializer/CSerializer.h +++ b/lib/serializer/CSerializer.h @@ -12,7 +12,7 @@ #include "../ConstTransitivePtr.h" #include "../GameConstants.h" -const ui32 SERIALIZATION_VERSION = 787; +const ui32 SERIALIZATION_VERSION = 788; const ui32 MINIMAL_SERIALIZATION_VERSION = 753; const std::string SAVEGAME_MAGIC = "VCMISVG"; diff --git a/test/mock/mock_CPSICallback.cpp b/test/mock/mock_CPSICallback.cpp new file mode 100644 index 000000000..8d0c87d5b --- /dev/null +++ b/test/mock/mock_CPSICallback.cpp @@ -0,0 +1,13 @@ +/* +* {file}.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 "mock_CPSICallback.h" diff --git a/test/mock/mock_CPSICallback.h b/test/mock/mock_CPSICallback.h new file mode 100644 index 000000000..267f3bcf4 --- /dev/null +++ b/test/mock/mock_CPSICallback.h @@ -0,0 +1,28 @@ +/* +* mock_CPLayerSpecificInfoCallback.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 "gtest/gtest.h" + +#include "../lib/CGameInfoCallback.h" +#include "../lib/ResourceSet.h" + +class CCallback; + +class GameCallbackMock : public CPlayerSpecificInfoCallback +{ +public: + using CPlayerSpecificInfoCallback::CPlayerSpecificInfoCallback; + + MOCK_CONST_METHOD0(getResourceAmount, TResources()); + //std::vector getTownsInfo(bool onlyOur = true) const; + MOCK_CONST_METHOD0(getTownsInfo, std::vector ()); + MOCK_CONST_METHOD1(getTownsInfo, std::vector (bool)); +}; \ No newline at end of file diff --git a/test/vcai/ResourceManagerTest.h b/test/vcai/ResourceManagerTest.h new file mode 100644 index 000000000..4b3992cca --- /dev/null +++ b/test/vcai/ResourceManagerTest.h @@ -0,0 +1,13 @@ +/* +* ResourceManagerTest.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 + +extern boost::thread_specific_ptr cb; \ No newline at end of file diff --git a/test/vcai/ResurceManagerTest.cpp b/test/vcai/ResurceManagerTest.cpp new file mode 100644 index 000000000..45ab179a7 --- /dev/null +++ b/test/vcai/ResurceManagerTest.cpp @@ -0,0 +1,254 @@ +/* +* ResourceManagerTest.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 "gtest/gtest.h" + +#include "../AI/VCAI/VCAI.h" +#include "ResourceManagerTest.h" +#include "../AI/VCAI/Goals.h" +#include "mock_VCAI_CGoal.h" +#include "mock_VCAI.h" +#include "mock_ResourceManager.h" +#include "../mock/mock_CPSICallback.h" +#include "../lib/CGameInfoCallback.h" + +using namespace Goals; +using namespace ::testing; + +struct ResourceManagerTest : public Test//, public IResourceManager +{ + std::unique_ptr rm; + + NiceMock gcm; + NiceMock aim; + TSubgoal invalidGoal, gatherArmy, buildThis, buildAny, recruitHero; + + ResourceManagerTest() + { + rm = std::make_unique>(&gcm, &aim); + + //note: construct new goal for modfications + invalidGoal = sptr(StrictMock()); + gatherArmy = sptr(StrictMock()); + buildThis = sptr(StrictMock()); + buildAny = sptr(StrictMock()); + recruitHero = sptr(StrictMock()); + + //auto AI = CDynLibHandler::getNewAI("VCAI.dll"); + //SET_GLOBAL_STATE(AI); + + //gtest couldn't deduce default return value; + ON_CALL(gcm, getTownsInfo(_)) + .WillByDefault(Return(std::vector < const CGTownInstance *>())); + + ON_CALL(gcm, getTownsInfo()) + .WillByDefault(Return(std::vector < const CGTownInstance *>())); + + ON_CALL(aim, getFlaggedObjects()) + .WillByDefault(Return(std::vector < const CGObjectInstance *>())); + + //enable if get unexpected exceptions + //ON_CALL(gcm, getResourceAmount()) + // .WillByDefault(Return(TResources())); + } +}; + +TEST_F(ResourceManagerTest, canAffordMaths) +{ + //mocking cb calls inside canAfford() + + ON_CALL(gcm, getResourceAmount()) + .WillByDefault(Return(TResources(10, 0, 11, 0, 0, 0, 12345))); + + TResources buildingCost(10, 0, 10, 0, 0, 0, 5000); + //EXPECT_CALL(gcm, getResourceAmount()).RetiresOnSaturation(); + //EXPECT_CALL(gcm, getTownsInfo(_)).RetiresOnSaturation(); + EXPECT_NO_THROW(rm->canAfford(buildingCost)); + EXPECT_TRUE(rm->canAfford(buildingCost)); + + TResources armyCost(0, 0, 0, 0, 0, 0, 54321); + EXPECT_FALSE(rm->canAfford(armyCost)); + + rm->reserveResoures(armyCost, gatherArmy); + EXPECT_FALSE(rm->canAfford(buildingCost)) << "Reserved value should be substracted from free resources"; +} + +TEST_F(ResourceManagerTest, notifyGoalImplemented) +{ + ASSERT_FALSE(rm->hasTasksLeft()); + + EXPECT_FALSE(rm->notifyGoalCompleted(invalidGoal)); + EXPECT_FALSE(rm->hasTasksLeft()); + + TResources res(0,0,0,0,0,0,12345);; + rm->reserveResoures(res, invalidGoal); + ASSERT_FALSE(rm->hasTasksLeft()) << "Can't push Invalid goal"; + EXPECT_FALSE(rm->notifyGoalCompleted(invalidGoal)); + + EXPECT_FALSE(rm->notifyGoalCompleted(gatherArmy)) << "Queue should be empty"; + rm->reserveResoures(res, gatherArmy); + EXPECT_TRUE(rm->notifyGoalCompleted(gatherArmy)) << "Not implemented"; //TODO: try it with not a copy + EXPECT_FALSE(rm->notifyGoalCompleted(gatherArmy)); //already completed +} + +TEST_F(ResourceManagerTest, notifyFulfillsAll) +{ + TResources res; + ASSERT_TRUE(buildAny->fulfillsMe(buildThis)) << "Goal dependency implemented incorrectly"; //TODO: goal mock? + rm->reserveResoures(res, buildAny); + rm->reserveResoures(res, buildAny); + rm->reserveResoures(res, buildAny); + ASSERT_TRUE(rm->hasTasksLeft()); //regardless if duplicates are allowed or not + rm->notifyGoalCompleted(buildThis); + ASSERT_FALSE(rm->hasTasksLeft()) << "BuildThis didn't remove Build Any!"; +} + +TEST_F(ResourceManagerTest, queueOrder) +{ + ASSERT_FALSE(rm->hasTasksLeft()); + + TSubgoal buildLow = sptr(StrictMock()), + buildBit = sptr(StrictMock()), + buildMed = sptr(StrictMock()), + buildHigh = sptr(StrictMock()), + buildVeryHigh = sptr(StrictMock()), + buildExtra = sptr(StrictMock()), + buildNotSoExtra = sptr(StrictMock()); + + buildLow->setpriority(0).setbid(1); + buildLow->setpriority(1).setbid(2); + buildMed->setpriority(2).setbid(3); + buildHigh->setpriority(5).setbid(4); + buildVeryHigh->setpriority(10).setbid(5); + + TResources price(0, 0, 0, 0, 0, 0, 1000); + rm->reserveResoures(price, buildLow); + rm->reserveResoures(price, buildHigh); + rm->reserveResoures(price, buildMed); + + ON_CALL(gcm, getResourceAmount()) + .WillByDefault(Return(TResources(0,0,0,0,0,0,4000,0))); //we can afford 4 top goals + + auto goal = rm->whatToDo(); + EXPECT_EQ(goal->goalType, Goals::BUILD_STRUCTURE); + ASSERT_EQ(rm->whatToDo()->bid, 4); + rm->reserveResoures(price, buildBit); + rm->reserveResoures(price, buildVeryHigh); + goal = rm->whatToDo(); + EXPECT_EQ(goal->goalType, Goals::BUILD_STRUCTURE); + ASSERT_EQ(goal->bid, 5); + + buildExtra->setpriority(3).setbid(100); + EXPECT_EQ(rm->whatToDo(price, buildExtra)->bid, 100); + + buildNotSoExtra->setpriority(0.7f).setbid(7); + goal = rm->whatToDo(price, buildNotSoExtra); + EXPECT_NE(goal->bid, 7); + EXPECT_EQ(goal->goalType, Goals::COLLECT_RES) << "We can't afford this goal, need to collect resources"; + EXPECT_EQ(goal->resID, Res::GOLD) << "We need to collect gold"; + + goal = rm->whatToDo(); + EXPECT_NE(goal->goalType, Goals::COLLECT_RES); + EXPECT_EQ(goal->bid, 5) << "Should return highest-priority goal"; +} + +TEST_F(ResourceManagerTest, updateGoalImplemented) +{ + ASSERT_FALSE(rm->hasTasksLeft()); + + TResources res; + res[Res::GOLD] = 12345; + + buildThis->setpriority(1); + buildThis->bid = 666; + + EXPECT_FALSE(rm->updateGoal(buildThis)); //try update with no objectives -> fail + + rm->reserveResoures(res, buildThis); + ASSERT_TRUE(rm->hasTasksLeft()); + buildThis->setpriority(4.444f); + + auto buildThis2 = sptr(StrictMock()); + buildThis2->bid = 777; + buildThis2->setpriority(3.33f); + + EXPECT_FALSE(rm->updateGoal(buildThis2)) << "Shouldn't update with wrong goal"; + EXPECT_TRUE(rm->updateGoal(buildThis)) << "Not implemented"; //try update with copy of reserved goal -> true + + EXPECT_FALSE(rm->updateGoal(invalidGoal)) << "Can't update Invalid goal"; +} + +TEST_F(ResourceManagerTest, complexGoalUpdates) +{ + //TODO + ASSERT_FALSE(rm->hasTasksLeft()); +} + +TEST_F(ResourceManagerTest, tasksLeft) +{ + ASSERT_FALSE(rm->hasTasksLeft()); +} + +TEST_F(ResourceManagerTest, reservedResources) +{ + //TOOO, not worth it now +} + +TEST_F(ResourceManagerTest, freeResources) +{ + ON_CALL(gcm, getResourceAmount()) //in case callback or gs gets crazy + .WillByDefault(Return(TResources(-1, 0, -13.0f, -38763, -93764, -464, -12, -98765))); + + auto res = rm->freeResources(); + ASSERT_GE(res[Res::WOOD], 0); + ASSERT_GE(res[Res::MERCURY], 0); + ASSERT_GE(res[Res::ORE], 0); + ASSERT_GE(res[Res::SULFUR], 0); + ASSERT_GE(res[Res::CRYSTAL], 0); + ASSERT_GE(res[Res::GEMS], 0); + ASSERT_GE(res[Res::GOLD], 0); + ASSERT_GE(res[Res::MITHRIL], 0); +} + +TEST_F(ResourceManagerTest, freeResourcesWithManyGoals) +{ + ON_CALL(gcm, getResourceAmount()) + .WillByDefault(Return(TResources(20, 10, 20, 10, 10, 10, 20000, 0))); + + ASSERT_EQ(rm->freeResources(), TResources(20, 10, 20, 10, 10, 10, 20000, 0)); + + rm->reserveResoures(TResources(0, 4, 0, 0, 0, 0, 13000), gatherArmy); + ASSERT_EQ(rm->freeResources(), TResources(20, 6, 20, 10, 10, 10, 7000, 0)); + rm->reserveResoures(TResources(5, 4, 5, 4, 4, 4, 5000), buildThis); + ASSERT_EQ(rm->freeResources(), TResources(15, 2, 15, 6, 6, 6, 2000, 0)); + rm->reserveResoures(TResources(0, 0, 0, 0, 0, 0, 2500), recruitHero); + auto res = rm->freeResources(); + EXPECT_EQ(res[Res::GOLD], 0) << "We should have 0 gold left"; + + ON_CALL(gcm, getResourceAmount()) + .WillByDefault(Return(TResources(20, 10, 20, 10, 10, 10, 30000, 0))); //+10000 gold + + res = rm->freeResources(); + EXPECT_EQ(res[Res::GOLD], 9500) << "We should have extra savings now"; +} + +TEST_F(ResourceManagerTest, freeGold) +{ + ON_CALL(gcm, getResourceAmount()) + .WillByDefault(Return(TResources(0, 0, 0, 0, 0, 0, 666))); + + ASSERT_EQ(rm->freeGold(), 666); + + ON_CALL(gcm, getResourceAmount()) + .WillByDefault(Return(TResources(0, 0, 0, 0, 0, 0, -3762363))); + + ASSERT_GE(rm->freeGold(), 0) << "We should never see negative savings"; +} \ No newline at end of file diff --git a/test/vcai/mock_ResourceManager.cpp b/test/vcai/mock_ResourceManager.cpp new file mode 100644 index 000000000..a780543e5 --- /dev/null +++ b/test/vcai/mock_ResourceManager.cpp @@ -0,0 +1,18 @@ +#include "StdInc.h" + +#include "mock_ResourceManager.h" + +void ResourceManagerMock::reserveResoures(TResources &res, Goals::TSubgoal goal) +{ + ResourceManager::reserveResoures(res, goal); +} + +bool ResourceManagerMock::updateGoal(Goals::TSubgoal goal) +{ + return ResourceManager::updateGoal(goal); +} + +bool ResourceManagerMock::notifyGoalCompleted(Goals::TSubgoal goal) +{ + return ResourceManager::notifyGoalCompleted(goal); +} diff --git a/test/vcai/mock_ResourceManager.h b/test/vcai/mock_ResourceManager.h new file mode 100644 index 000000000..aa3ca6682 --- /dev/null +++ b/test/vcai/mock_ResourceManager.h @@ -0,0 +1,19 @@ +#pragma once + +#include "gtest/gtest.h" +#include "../AI/VCAI/ResourceManager.h" + +class ResourceManager; +class CPlayerSpecificInfoCallback; +class VCAI; + +class ResourceManagerMock : public ResourceManager +{ + friend class ResourceManagerTest; //everything is public +public: + using ResourceManager::ResourceManager; + //access protected members, TODO: consider other architecture? + void reserveResoures(TResources &res, Goals::TSubgoal goal = Goals::TSubgoal()) override; + bool updateGoal(Goals::TSubgoal goal) override; + bool notifyGoalCompleted(Goals::TSubgoal goal) override; +}; \ No newline at end of file diff --git a/test/vcai/mock_VCAI.cpp b/test/vcai/mock_VCAI.cpp new file mode 100644 index 000000000..3b377d73c --- /dev/null +++ b/test/vcai/mock_VCAI.cpp @@ -0,0 +1,23 @@ +/* +* ResourceManagerTest.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 "mock_VCAI.h" + +VCAIMock::VCAIMock() + : VCAI() +{ + makingTurn = nullptr; +} + +VCAIMock::~VCAIMock() +{ +} diff --git a/test/vcai/mock_VCAI.h b/test/vcai/mock_VCAI.h new file mode 100644 index 000000000..5bd57186e --- /dev/null +++ b/test/vcai/mock_VCAI.h @@ -0,0 +1,96 @@ +/* +* mock_VCAI.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 "gtest/gtest.h" + +#include "../AI/VCAI/VCAI.h" + +//dependency hell +#include "../../lib/NetPacks.h" //for Component, TryMoveHero +#include "../../lib/serializer/BinarySerializer.h" +#include "../../lib/serializer/BinaryDeserializer.h" + +class VCAIMock : public VCAI +{ +public: + VCAIMock(); + ~VCAIMock() override; + + //overloading all "override" methods from VCAI. AI should never call them, anyway + MOCK_CONST_METHOD0(getBattleAIName, std::string()); + + MOCK_METHOD1(init, void(std::shared_ptr CB)); + MOCK_METHOD0(yourTurn, void()); + + MOCK_METHOD4(heroGotLevel, void(const CGHeroInstance * hero, PrimarySkill::PrimarySkill pskill, std::vector & skills, QueryID queryID)); + MOCK_METHOD3(commanderGotLevel, void(const CCommanderInstance * commander, std::vector skills, QueryID queryID)); + MOCK_METHOD6(showBlockingDialog, void(const std::string & text, const std::vector & components, QueryID askID, const int soundID, bool selection, bool cancel)); + MOCK_METHOD4(showGarrisonDialog, void(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits, QueryID queryID)); + MOCK_METHOD4(showTeleportDialog, void(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID)); + MOCK_METHOD5(showMapObjectSelectDialog, void(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects)); + MOCK_METHOD2(saveGame, void(BinarySerializer & h, const int version)); + MOCK_METHOD2(loadGame, void(BinaryDeserializer & h, const int version)); + MOCK_METHOD0(finish, void()); + + MOCK_METHOD1(availableCreaturesChanged, void(const CGDwelling * town)); + MOCK_METHOD1(heroMoved, void(const TryMoveHero & details)); + MOCK_METHOD1(heroInGarrisonChange, void(const CGTownInstance * town)); + MOCK_METHOD2(centerView, void(int3 pos, int focusTime)); + MOCK_METHOD1(tileHidden, void(const std::unordered_set & pos)); + MOCK_METHOD2(artifactMoved, void(const ArtifactLocation & src, const ArtifactLocation & dst)); + MOCK_METHOD1(artifactAssembled, void(const ArtifactLocation & al)); + MOCK_METHOD1(showTavernWindow, void(const CGObjectInstance * townOrTavern)); + MOCK_METHOD1(showThievesGuildWindow, void(const CGObjectInstance * obj)); + MOCK_METHOD2(playerBlocked, void(int reason, bool start)); + MOCK_METHOD0(showPuzzleMap, void()); + MOCK_METHOD1(showShipyardDialog, void(const IShipyard * obj)); + MOCK_METHOD2(gameOver, void(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult)); + MOCK_METHOD1(artifactPut, void(const ArtifactLocation & al)); + MOCK_METHOD1(artifactRemoved, void(const ArtifactLocation & al)); + MOCK_METHOD1(artifactDisassembled, void(const ArtifactLocation & al)); + MOCK_METHOD3(heroVisit, void(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start)); + MOCK_METHOD1(availableArtifactsChanged, void(const CGBlackMarket * bm)); + MOCK_METHOD2(heroVisitsTown, void(const CGHeroInstance * hero, const CGTownInstance * town)); + MOCK_METHOD1(tileRevealed, void(const std::unordered_set & pos)); + MOCK_METHOD3(heroExchangeStarted, void(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query)); + MOCK_METHOD3(heroPrimarySkillChanged, void(const CGHeroInstance * hero, int which, si64 val)); + MOCK_METHOD3(showRecruitmentDialog, void(const CGDwelling * dwelling, const CArmedInstance * dst, int level)); + MOCK_METHOD1(heroMovePointsChanged, void(const CGHeroInstance * hero)); + MOCK_METHOD2(garrisonsChanged, void(ObjectInstanceID id1, ObjectInstanceID id2)); + MOCK_METHOD1(newObject, void(const CGObjectInstance * obj)); + MOCK_METHOD2(showHillFortWindow, void(const CGObjectInstance * object, const CGHeroInstance * visitor)); + MOCK_METHOD2(playerBonusChanged, void(const Bonus & bonus, bool gain)); + MOCK_METHOD1(heroCreated, void(const CGHeroInstance *)); + MOCK_METHOD2(advmapSpellCast, void(const CGHeroInstance * caster, int spellID)); + MOCK_METHOD3(showInfoDialog, void(const std::string & text, const std::vector & components, int soundID)); + MOCK_METHOD1(requestRealized, void(PackageApplied * pa)); + MOCK_METHOD0(receivedResource, void()); + MOCK_METHOD1(objectRemoved, void(const CGObjectInstance * obj)); + MOCK_METHOD2(showUniversityWindow, void(const IMarket * market, const CGHeroInstance * visitor)); + MOCK_METHOD1(heroManaPointsChanged, void(const CGHeroInstance * hero)); + MOCK_METHOD3(heroSecondarySkillChanged, void(const CGHeroInstance * hero, int which, int val)); + MOCK_METHOD0(battleResultsApplied, void()); + MOCK_METHOD1(objectPropertyChanged, void(const SetObjectProperty * sop)); + MOCK_METHOD3(buildChanged, void(const CGTownInstance * town, BuildingID buildingID, int what)); + MOCK_METHOD3(heroBonusChanged, void(const CGHeroInstance * hero, const Bonus & bonus, bool gain)); + MOCK_METHOD2(showMarketWindow, void(const IMarket * market, const CGHeroInstance * visitor)); + MOCK_METHOD1(showWorldViewEx, void(const std::vector & objectPositions)); + + MOCK_METHOD6(battleStart, void(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side)); + MOCK_METHOD1(battleEnd, void(const BattleResult * br)); + + MOCK_METHOD2(requestSent, void(const CPackForServer * pack, int requestID)); + + //interetsing stuff + + MOCK_CONST_METHOD0(getFlaggedObjects, std::vector()); + +}; diff --git a/test/vcai/mock_VCAI_CGoal.h b/test/vcai/mock_VCAI_CGoal.h new file mode 100644 index 000000000..4086fb7b9 --- /dev/null +++ b/test/vcai/mock_VCAI_CGoal.h @@ -0,0 +1,35 @@ +/* +* mock_VCAI_CGoal.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 "../AI/VCAI/Goals.h" + +namespace Goals +{ + class InvalidGoalMock : public Invalid + { + public: + //MOCK_METHOD1(accept, void(VCAI *)); + //MOCK_METHOD1(accept, float(FuzzyHelper *)); + //MOCK_METHOD1(fulfillsMe, bool(TSubgoal)); + //bool operator==(AbstractGoal & g) override + //{ + // return false; + //}; + //MOCK_METHOD0(getAllPossibleSubgoals, TGoalVec()); + //MOCK_METHOD0(whatToDoToAchieve, TSubgoal()); + + }; + + class GatherArmyGoalMock : public GatherArmy + { + }; +}