/* * Goals.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "Goals.h" #include "VCAI.h" #include "FuzzyHelper.h" #include "ResourceManager.h" #include "BuildingManager.h" #include "../../lib/mapping/CMap.h" //for victory conditions #include "../../lib/CPathfinder.h" #include "../../lib/StringConstants.h" #include "AIhelper.h" extern boost::thread_specific_ptr cb; extern boost::thread_specific_ptr ai; extern boost::thread_specific_ptr ah; extern FuzzyHelper * fh; using namespace Goals; TSubgoal Goals::sptr(const AbstractGoal & tmp) { TSubgoal ptr; ptr.reset(tmp.clone()); return ptr; } std::string Goals::AbstractGoal::name() const //TODO: virtualize { std::string desc; switch(goalType) { case INVALID: return "INVALID"; case WIN: return "WIN"; case DO_NOT_LOSE: return "DO NOT LOOSE"; case CONQUER: return "CONQUER"; case BUILD: return "BUILD"; case EXPLORE: desc = "EXPLORE"; break; case GATHER_ARMY: desc = "GATHER ARMY"; break; case BUY_ARMY: return "BUY ARMY"; break; case BOOST_HERO: desc = "BOOST_HERO (unsupported)"; break; case RECRUIT_HERO: return "RECRUIT HERO"; case BUILD_STRUCTURE: return "BUILD STRUCTURE"; case COLLECT_RES: desc = "COLLECT RESOURCE " + GameConstants::RESOURCE_NAMES[resID] + " (" + boost::lexical_cast(value) + ")"; break; case TRADE: { auto obj = cb->getObjInstance(ObjectInstanceID(objid)); if (obj) desc = (boost::format("TRADE %d of %s at %s") % value % GameConstants::RESOURCE_NAMES[resID] % obj->getObjectName()).str(); } break; case GATHER_TROOPS: desc = "GATHER TROOPS"; break; case VISIT_OBJ: { auto obj = cb->getObjInstance(ObjectInstanceID(objid)); if(obj) desc = "GET OBJ " + obj->getObjectName(); } break; case FIND_OBJ: desc = "FIND OBJ " + boost::lexical_cast(objid); break; case VISIT_HERO: { auto obj = cb->getObjInstance(ObjectInstanceID(objid)); if(obj) desc = "VISIT HERO " + obj->getObjectName(); } break; case GET_ART_TYPE: desc = "GET ARTIFACT OF TYPE " + VLC->arth->artifacts[aid]->Name(); break; case ISSUE_COMMAND: return "ISSUE COMMAND (unsupported)"; case VISIT_TILE: desc = "VISIT TILE " + tile.toString(); break; case CLEAR_WAY_TO: desc = "CLEAR WAY TO " + tile.toString(); break; case DIG_AT_TILE: desc = "DIG AT TILE " + tile.toString(); break; default: return boost::lexical_cast(goalType); } if(hero.get(true)) //FIXME: used to crash when we lost hero and failed goal desc += " (" + hero->name + ")"; return desc; } bool Goals::AbstractGoal::operator==(AbstractGoal & g) { /*this operator checks if goals are EQUIVALENT, ie. if they represent same objective it does not not check isAbstract or isElementar, as this is up to VCAI decomposition logic */ if(g.goalType != goalType) return false; switch(goalType) { //no parameters case INVALID: case WIN: case DO_NOT_LOSE: case RECRUIT_HERO: //overloaded return true; break; //assigned to hero, no parameters case CONQUER: case EXPLORE: 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; //assigned hero and object case VISIT_OBJ: case FIND_OBJ: //TODO: use subtype? case VISIT_HERO: case GET_ART_TYPE: 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: case TRADE: //TODO return (resID == g.resID); //every hero may collect resources break; case BUILD: //abstract build is indentical, TODO: consider building anything in town return true; break; case GATHER_TROOPS: case ISSUE_COMMAND: default: return false; } } bool Goals::AbstractGoal::operator<(AbstractGoal & g) //for std::unique { //TODO: make sure it gets goals consistent with == operator if (goalType < g.goalType) return true; if (goalType > g.goalType) return false; if (hero < g.hero) return true; if (hero > g.hero) return false; if (tile < g.tile) return true; if (g.tile < tile) return false; if (objid < g.objid) return true; if (objid > g.objid) return false; if (town < g.town) return true; if (town > g.town) return false; if (value < g.value) return true; if (value > g.value) return false; if (priority < g.priority) return true; if (priority > g.priority) return false; if (resID < g.resID) return true; if (resID > g.resID) return false; if (bid < g.bid) return true; if (bid > g.bid) return false; if (aid < g.aid) return true; if (aid > g.aid) return false; return false; } //TODO: find out why the following are not generated automatically on MVS? 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<> 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 TSubgoal::operator<(const TSubgoal & rhs) const { return get() < rhs.get(); //compae by value } bool BuyArmy::operator==(AbstractGoal & g) { if (g.goalType != goalType) return false; //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") % value, town->name; } } TSubgoal Trade::whatToDoToAchieve() { return iAmElementar(); } bool Trade::operator==(AbstractGoal & g) { if (g.goalType != goalType) return false; if (g.resID == resID) if (g.value == value) //TODO: not sure if that logic is consitent return true; return false; } //TSubgoal AbstractGoal::whatToDoToAchieve() //{ // logAi->debug("Decomposing goal of type %s",name()); // return sptr (Goals::Explore()); //} TSubgoal Win::whatToDoToAchieve() { auto toBool = [=](const EventCondition &) { // TODO: proper implementation // Right now even already fulfilled goals will be included into generated list // Proper check should test if event condition is already fulfilled // Easiest way to do this is to call CGameState::checkForVictory but this function should not be // used on client side or in AI code return false; }; std::vector goals; for(const TriggeredEvent & event : cb->getMapHeader()->triggeredEvents) { //TODO: try to eliminate human player(s) using loss conditions that have isHuman element if(event.effect.type == EventEffect::VICTORY) { boost::range::copy(event.trigger.getFulfillmentCandidates(toBool), std::back_inserter(goals)); } } //TODO: instead of returning first encountered goal AI should generate list of possible subgoals for(const EventCondition & goal : goals) { switch(goal.condition) { case EventCondition::HAVE_ARTIFACT: return sptr(Goals::GetArtOfType(goal.objectType)); case EventCondition::DESTROY: { if(goal.object) { auto obj = cb->getObj(goal.object->id); if(obj) if(obj->getOwner() == ai->playerID) //we can't capture our own object return sptr(Goals::Conquer()); return sptr(Goals::VisitObj(goal.object->id.getNum())); } else { // TODO: destroy all objects of type goal.objectType // This situation represents "kill all creatures" condition from H3 break; } } case EventCondition::HAVE_BUILDING: { // TODO build other buildings apart from Grail // goal.objectType = buidingID to build // goal.object = optional, town in which building should be built // Represents "Improve town" condition from H3 (but unlike H3 it consists from 2 separate conditions) if(goal.objectType == BuildingID::GRAIL) { if(auto h = ai->getHeroWithGrail()) { //hero is in a town that can host Grail if(h->visitedTown && !vstd::contains(h->visitedTown->forbiddenBuildings, BuildingID::GRAIL)) { const CGTownInstance * t = h->visitedTown; return sptr(Goals::BuildThis(BuildingID::GRAIL, t).setpriority(10)); } else { auto towns = cb->getTownsInfo(); towns.erase(boost::remove_if(towns, [](const CGTownInstance * t) -> bool { return vstd::contains(t->forbiddenBuildings, BuildingID::GRAIL); }), towns.end()); boost::sort(towns, CDistanceSorter(h.get())); if(towns.size()) { return sptr(Goals::VisitTile(towns.front()->visitablePos()).sethero(h)); } } } double ratio = 0; // maybe make this check a bit more complex? For example: // 0.75 -> dig randomly within 3 tiles radius // 0.85 -> radius now 2 tiles // 0.95 -> 1 tile radius, position is fully known // AFAIK H3 AI does something like this int3 grailPos = cb->getGrailPos(&ratio); if(ratio > 0.99) { return sptr(Goals::DigAtTile(grailPos)); } //TODO: use FIND_OBJ else if(const CGObjectInstance * obj = ai->getUnvisitedObj(objWithID)) //there are unvisited Obelisks return sptr(Goals::VisitObj(obj->id.getNum())); else return sptr(Goals::Explore()); } break; } case EventCondition::CONTROL: { if(goal.object) { return sptr(Goals::VisitObj(goal.object->id.getNum())); } else { //TODO: control all objects of type "goal.objectType" // Represents H3 condition "Flag all mines" break; } } case EventCondition::HAVE_RESOURCES: //TODO mines? piles? marketplace? //save? return sptr(Goals::CollectRes(static_cast(goal.objectType), goal.value)); case EventCondition::HAVE_CREATURES: return sptr(Goals::GatherTroops(goal.objectType, goal.value)); case EventCondition::TRANSPORT: { //TODO. merge with bring Grail to town? So AI will first dig grail, then transport it using this goal and builds it // Represents "transport artifact" condition: // goal.objectType = type of artifact // goal.object = destination-town where artifact should be transported break; } case EventCondition::STANDARD_WIN: return sptr(Goals::Conquer()); // Conditions that likely don't need any implementation case EventCondition::DAYS_PASSED: break; // goal.value = number of days for condition to trigger case EventCondition::DAYS_WITHOUT_TOWN: break; // goal.value = number of days to trigger this case EventCondition::IS_HUMAN: break; // Should be only used in calculation of candidates (see toBool lambda) case EventCondition::CONST_VALUE: break; case EventCondition::HAVE_0: case EventCondition::HAVE_BUILDING_0: case EventCondition::DESTROY_0: //TODO: support new condition format return sptr(Goals::Conquer()); default: assert(0); } } return sptr(Goals::Invalid()); } TSubgoal FindObj::whatToDoToAchieve() { const CGObjectInstance * o = nullptr; if(resID > -1) //specified { for(const CGObjectInstance * obj : ai->visitableObjs) { if(obj->ID == objid && obj->subID == resID) { o = obj; break; //TODO: consider multiple objects and choose best } } } else { for(const CGObjectInstance * obj : ai->visitableObjs) { if(obj->ID == objid) { o = obj; break; //TODO: consider multiple objects and choose best } } } if(o && ai->isAccessible(o->pos)) //we don't use isAccessibleForHero as we don't know which hero it is return sptr(Goals::VisitObj(o->id.getNum())); else 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 VisitObj::completeMessage() const { return "hero " + hero.get()->name + " captured Object ID = " + boost::lexical_cast(objid); } TGoalVec VisitObj::getAllPossibleSubgoals() { TGoalVec goalList; const CGObjectInstance * obj = cb->getObjInstance(ObjectInstanceID(objid)); if(!obj) { throw cannotFulfillGoalException("Object is missing - goal is invalid now!"); } int3 pos = obj->visitablePos(); if(hero) { if(ai->isAccessibleForHero(pos, hero)) { if(isSafeToVisit(hero, pos)) goalList.push_back(sptr(Goals::VisitObj(obj->id.getNum()).sethero(hero))); else goalList.push_back(sptr(Goals::GatherArmy(evaluateDanger(pos, hero.h) * SAFE_ATTACK_CONSTANT).sethero(hero).setisAbstract(true))); return goalList; } } else { for(auto potentialVisitor : cb->getHeroesInfo()) { if(ai->isAccessibleForHero(pos, potentialVisitor)) { if(isSafeToVisit(potentialVisitor, pos)) goalList.push_back(sptr(Goals::VisitObj(obj->id.getNum()).sethero(potentialVisitor))); else goalList.push_back(sptr(Goals::GatherArmy(evaluateDanger(pos, potentialVisitor) * SAFE_ATTACK_CONSTANT).sethero(potentialVisitor).setisAbstract(true))); } } if(!goalList.empty()) { return goalList; } } goalList.push_back(sptr(Goals::ClearWayTo(pos))); return goalList; } TSubgoal VisitObj::whatToDoToAchieve() { auto bestGoal = fh->chooseSolution(getAllPossibleSubgoals()); if(bestGoal->goalType == Goals::VISIT_OBJ && bestGoal->hero) bestGoal->setisElementar(true); return bestGoal; } Goals::VisitObj::VisitObj(int Objid) : CGoal(Goals::VISIT_OBJ) { objid = Objid; tile = ai->myCb->getObjInstance(ObjectInstanceID(objid))->visitablePos(); priority = 3; } bool Goals::VisitObj::operator==(AbstractGoal & g) { if (g.goalType != goalType) return false; return g.objid == objid; } bool VisitObj::fulfillsMe(TSubgoal goal) { if(goal->goalType == Goals::VISIT_TILE) { if (!hero || hero == goal->hero) { auto obj = cb->getObjInstance(ObjectInstanceID(objid)); if (obj && obj->visitablePos() == goal->tile) //object could be removed return true; } } return false; } std::string VisitHero::completeMessage() const { return "hero " + hero.get()->name + " visited hero " + boost::lexical_cast(objid); } TSubgoal VisitHero::whatToDoToAchieve() { const CGObjectInstance * obj = cb->getObj(ObjectInstanceID(objid)); if(!obj) return sptr(Goals::Explore()); int3 pos = obj->visitablePos(); if(hero && ai->isAccessibleForHero(pos, hero, true) && isSafeToVisit(hero, pos)) //enemy heroes can get reinforcements { if(hero->pos == pos) logAi->error("Hero %s tries to visit himself.", hero.name); else { //can't use VISIT_TILE here as tile appears blocked by target hero //FIXME: elementar goal should not be abstract return sptr(Goals::VisitHero(objid).sethero(hero).settile(pos).setisElementar(true)); } } return sptr(Goals::Invalid()); } bool Goals::VisitHero::operator==(AbstractGoal & g) { if (g.goalType != goalType) return false; return g.hero == hero && g.objid == objid; } bool VisitHero::fulfillsMe(TSubgoal goal) { //TODO: VisitObj shoudl not be used for heroes, but... if(goal->goalType == Goals::VISIT_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() { TSubgoal alternativeWay = CGoal::lookForArtSmart(aid); //TODO: use if(alternativeWay->invalid()) return sptr(Goals::FindObj(Obj::ARTIFACT, aid)); return sptr(Goals::Invalid()); } TSubgoal ClearWayTo::whatToDoToAchieve() { assert(cb->isInTheMap(tile)); //set tile if(!cb->isVisible(tile)) { logAi->error("Clear way should be used with visible tiles!"); return sptr(Goals::Explore()); } return (fh->chooseSolution(getAllPossibleSubgoals())); } bool Goals::ClearWayTo::operator==(AbstractGoal & g) { if (g.goalType != goalType) return false; return g.goalType == goalType && g.tile == tile; } 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; std::vector heroes; if(hero) heroes.push_back(hero.h); else heroes = cb->getHeroesInfo(); for(auto h : heroes) { //TODO: handle clearing way to allied heroes that are blocked //if ((hero && hero->visitablePos() == tile && hero == *h) || //we can't free the way ourselves // h->visitablePos() == tile) //we are already on that tile! what does it mean? // continue; //if our hero is trapped, make sure we request clearing the way from OUR perspective auto sm = ai->getCachedSectorMap(h); int3 tileToHit = sm->firstTileToGet(h, tile); if(!tileToHit.valid()) continue; if(isBlockedBorderGate(tileToHit)) { //FIXME: this way we'll not visit gate and activate quest :? ret.push_back(sptr(Goals::FindObj(Obj::KEYMASTER, cb->getTile(tileToHit)->visitableObjects.back()->subID))); return ret; //only option } auto topObj = cb->getTopObj(tileToHit); if(topObj) { if(vstd::contains(ai->reservedObjs, topObj) && !vstd::contains(ai->reservedHeroesMap[h], topObj)) { throw goalFulfilledException(sptr(Goals::ClearWayTo(tile, h))); continue; //do not capure object reserved by other hero } if(topObj->ID == Obj::HERO && cb->getPlayerRelations(h->tempOwner, topObj->tempOwner) != PlayerRelations::ENEMIES) { if(topObj != hero.get(true)) //the hero we want to free logAi->error("%s stands in the way of %s", topObj->getObjectName(), h->getObjectName()); } if(topObj->ID == Obj::QUEST_GUARD || topObj->ID == Obj::BORDERGUARD) { if(shouldVisit(h, topObj)) { //do NOT use VISIT_TILE, as tile with quets guard can't be visited ret.push_back(sptr(Goals::VisitObj(topObj->id.getNum()).sethero(h))); //TODO: Recheck this code - object visit became elementar goal continue; //do not try to visit tile or gather army } else { //TODO: we should be able to return apriopriate quest here //ret.push_back(ai->questToGoal()); //however, visiting obj for firts time will give us quest logAi->debug("Quest guard blocks the way to %s", tile.toString()); continue; //do not access quets guard if we can't complete the quest } return ret; //try complete quest as the only option } } if(isSafeToVisit(h, tileToHit)) //this makes sense only if tile is guarded, but there is no quest object { ret.push_back(sptr(Goals::VisitTile(tileToHit).sethero(h))); } else { ret.push_back(sptr(Goals::GatherArmy(evaluateDanger(tileToHit, h) * SAFE_ATTACK_CONSTANT). sethero(h).setisAbstract(true))); } } if(ai->canRecruitAnyHero()) ret.push_back(sptr(Goals::RecruitHero())); if(ret.empty()) { logAi->warn("There is no known way to clear the way to tile %s", tile.toString()); throw goalFulfilledException(sptr(Goals::ClearWayTo(tile))); //make sure asigned hero gets unlocked } return ret; } std::string Explore::completeMessage() const { return "Hero " + hero.get()->name + " completed exploration"; } TSubgoal Explore::whatToDoToAchieve() { auto ret = fh->chooseSolution(getAllPossibleSubgoals()); if(hero) //use best step for this hero { return ret; } else { if(ret->hero.get(true)) return sptr(sethero(ret->hero.h).setisAbstract(true)); //choose this hero and then continue with him else return ret; //other solutions, like buying hero from tavern } } TGoalVec Explore::getAllPossibleSubgoals() { TGoalVec ret; std::vector heroes; if(hero) { heroes.push_back(hero.h); } else { //heroes = ai->getUnblockedHeroes(); heroes = cb->getHeroesInfo(); vstd::erase_if(heroes, [](const HeroPtr h) { if(ai->getGoal(h)->goalType == Goals::EXPLORE) //do not reassign hero who is already explorer return true; if(!ai->isAbleToExplore(h)) return true; return !h->movement; //saves time, immobile heroes are useless anyway }); } //try to use buildings that uncover map std::vector objs; for(auto obj : ai->visitableObjs) { if(!vstd::contains(ai->alreadyVisited, obj)) { switch(obj->ID.num) { case Obj::REDWOOD_OBSERVATORY: case Obj::PILLAR_OF_FIRE: case Obj::CARTOGRAPHER: objs.push_back(obj); break; case Obj::MONOLITH_ONE_WAY_ENTRANCE: case Obj::MONOLITH_TWO_WAY: case Obj::SUBTERRANEAN_GATE: auto tObj = dynamic_cast(obj); assert(ai->knownTeleportChannels.find(tObj->channel) != ai->knownTeleportChannels.end()); if(TeleportChannel::IMPASSABLE != ai->knownTeleportChannels[tObj->channel]->passability) objs.push_back(obj); break; } } else { switch(obj->ID.num) { case Obj::MONOLITH_TWO_WAY: case Obj::SUBTERRANEAN_GATE: auto tObj = dynamic_cast(obj); if(TeleportChannel::IMPASSABLE == ai->knownTeleportChannels[tObj->channel]->passability) break; for(auto exit : ai->knownTeleportChannels[tObj->channel]->exits) { if(!cb->getObj(exit)) { // Always attempt to visit two-way teleports if one of channel exits is not visible objs.push_back(obj); break; } } break; } } } auto primaryHero = ai->primaryHero().h; for(auto h : heroes) { auto sm = ai->getCachedSectorMap(h); for(auto obj : objs) //double loop, performance risk? { auto t = sm->firstTileToGet(h, obj->visitablePos()); //we assume that no more than one tile on the way is guarded if(ai->isTileNotReserved(h, t)) ret.push_back(sptr(Goals::ClearWayTo(obj->visitablePos(), h).setisAbstract(true))); } int3 t = whereToExplore(h); if(t.valid()) { ret.push_back(sptr(Goals::VisitTile(t).sethero(h))); } else { //FIXME: possible issues when gathering army to break if(hero.h == h || //exporation is assigned to this hero (!hero && h == primaryHero)) //not assigned to specific hero, let our main hero do the job { t = ai->explorationDesperate(h); //check this only ONCE, high cost if (t.valid()) //don't waste time if we are completely blocked { ret.push_back(sptr(Goals::ClearWayTo(t, h).setisAbstract(true))); continue; } } ai->markHeroUnableToExplore(h); //there is no freely accessible tile, do not poll this hero anymore } } //we either don't have hero yet or none of heroes can explore if((!hero || ret.empty()) && ai->canRecruitAnyHero()) ret.push_back(sptr(Goals::RecruitHero())); if(ret.empty()) { throw goalFulfilledException(sptr(Goals::Explore().sethero(hero))); } //throw cannotFulfillGoalException("Cannot explore - no possible ways found!"); return ret; } bool Explore::fulfillsMe(TSubgoal goal) { if(goal->goalType == Goals::EXPLORE) { if(goal->hero) return hero == goal->hero; else return true; //cancel ALL exploration } return false; } TSubgoal RecruitHero::whatToDoToAchieve() { const CGTownInstance * t = ai->findTownWithTavern(); if(!t) return sptr(Goals::BuildThis(BuildingID::TAVERN).setpriority(2)); TResources res; res[Res::GOLD] = GameConstants::HERO_GOLD_COST; return ah->whatToDo(res, iAmElementar()); //either buy immediately, or collect res } bool Goals::RecruitHero::operator==(AbstractGoal & g) { if (g.goalType != goalType) return false; //TODO: check town and hero return true; //for now, recruiting any hero will do } std::string VisitTile::completeMessage() const { return "Hero " + hero.get()->name + " visited tile " + tile.toString(); } TSubgoal VisitTile::whatToDoToAchieve() { auto ret = fh->chooseSolution(getAllPossibleSubgoals()); if(ret->hero) { if(isSafeToVisit(ret->hero, tile) && ai->isAccessibleForHero(tile, ret->hero)) { ret->setisElementar(true); return ret; } else { return sptr(Goals::GatherArmy(evaluateDanger(tile, *ret->hero) * SAFE_ATTACK_CONSTANT) .sethero(ret->hero).setisAbstract(true)); } } return ret; } bool Goals::VisitTile::operator==(AbstractGoal & g) { if (g.goalType != goalType) return false; return g.goalType == goalType && g.tile == tile; } TGoalVec VisitTile::getAllPossibleSubgoals() { assert(cb->isInTheMap(tile)); TGoalVec ret; if(!cb->isVisible(tile)) ret.push_back(sptr(Goals::Explore())); //what sense does it make? else { std::vector heroes; if(hero) heroes.push_back(hero.h); //use assigned hero if any else heroes = cb->getHeroesInfo(); //use most convenient hero for(auto h : heroes) { if(ai->isAccessibleForHero(tile, h)) ret.push_back(sptr(Goals::VisitTile(tile).sethero(h))); } if(ai->canRecruitAnyHero()) ret.push_back(sptr(Goals::RecruitHero())); } if(ret.empty()) { auto obj = vstd::frontOrNull(cb->getVisitableObjs(tile)); if(obj && obj->ID == Obj::HERO && obj->tempOwner == ai->playerID) //our own hero stands on that tile { if(hero.get(true) && hero->id == obj->id) //if it's assigned hero, visit tile. If it's different hero, we can't visit tile now ret.push_back(sptr(Goals::VisitTile(tile).sethero(dynamic_cast(obj)).setisElementar(true))); else throw cannotFulfillGoalException("Tile is already occupied by another hero "); //FIXME: we should give up this tile earlier } else ret.push_back(sptr(Goals::ClearWayTo(tile))); } //important - at least one sub-goal must handle case which is impossible to fulfill (unreachable tile) return ret; } TSubgoal DigAtTile::whatToDoToAchieve() { const CGObjectInstance * firstObj = vstd::frontOrNull(cb->getVisitableObjs(tile)); if(firstObj && firstObj->ID == Obj::HERO && firstObj->tempOwner == ai->playerID) //we have hero at dest { const CGHeroInstance * h = dynamic_cast(firstObj); sethero(h).setisElementar(true); return sptr(*this); } return sptr(Goals::VisitTile(tile)); } bool Goals::DigAtTile::operator==(AbstractGoal & g) { if (g.goalType != goalType) return false; return g.goalType == goalType && g.tile == tile; } TSubgoal BuildThis::whatToDoToAchieve() { 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 { if (cb->canBuildStructure(town, b) != EBuildingState::ALLOWED) //FIXME: decompose further? kind of mess if we're here throw cannotFulfillGoalException("Not possible to build"); else { 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) { if (ai->isAccessibleForHero(obj->visitablePos(), h)) { //further decomposition and evaluation will be handled by VisitObj ret.push_back(sptr(Goals::VisitObj(obj->id.getNum()).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) { if (const IMarket * m = IMarket::castFrom(obj, false)) { 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) markets.push_back(m); } } boost::sort(markets, [](const IMarket * m1, const IMarket * m2) -> bool { return m1->getMarketEfficiency() < m2->getMarketEfficiency(); }); markets.erase(boost::remove_if(markets, [](const IMarket * market) -> bool { if (!(market->o->ID == Obj::TOWN && market->o->tempOwner == ai->playerID)) { if (!ai->isAccessible(market->o->visitablePos())) return true; } return false; }), markets.end()); if (!markets.size()) { for (const CGTownInstance * t : cb->getTownsInfo()) { if (cb->canBuildStructure(t, BuildingID::MARKETPLACE) == EBuildingState::ALLOWED) return sptr(Goals::BuildThis(BuildingID::MARKETPLACE, t).setpriority(2)); } } else { 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)) { if (i == resID) continue; int toGive = -1, toReceive = -1; m->getOffer(i, resID, toGive, toReceive, EMarketMode::RESOURCE_RESOURCE); assert(toGive > 0 && toReceive > 0); howManyCanWeBuy += toReceive * (ah->freeResources()[i] / toGive); } if (howManyCanWeBuy >= value) { auto backObj = cb->getTopObj(m->o->visitablePos()); //it'll be a hero if we have one there; otherwise marketplace assert(backObj); auto objid = m->o->id.getNum(); if (backObj->tempOwner != ai->playerID) //top object not owned { return sptr(Goals::VisitObj(objid)); //just go there } else //either it's our town, or we have hero there { Goals::Trade trade(resID, value, objid); return sptr(trade.setisElementar(true)); //we can do this immediately - highest priority } } } return sptr(Goals::Invalid()); //cannot trade } bool CollectRes::fulfillsMe(TSubgoal goal) { if (goal->resID == resID) if (goal->value >= value) return true; return false; } bool Goals::CollectRes::operator==(AbstractGoal & g) { if (g.goalType != goalType) return false; if (g.resID == resID) if (g.value == value) //TODO: not sure if that logic is consitent return true; return false; } TSubgoal GatherTroops::whatToDoToAchieve() { std::vector dwellings; for(const CGTownInstance * t : cb->getTownsInfo()) { auto creature = VLC->creh->creatures[objid]; if(t->subID == creature->faction) //TODO: how to force AI to build unupgraded creatures? :O { auto creatures = vstd::tryAt(t->town->creatures, creature->level - 1); if(!creatures) continue; int upgradeNumber = vstd::find_pos(*creatures, creature->idNumber); if(upgradeNumber < 0) continue; BuildingID bid(BuildingID::DWELL_FIRST + creature->level - 1 + upgradeNumber * GameConstants::CREATURES_PER_TOWN); if(t->hasBuilt(bid)) //this assumes only creatures with dwellings are assigned to faction { dwellings.push_back(t); } else { return sptr(Goals::BuildThis(bid, t).setpriority(priority)); } } } for(auto obj : ai->visitableObjs) { if(obj->ID != Obj::CREATURE_GENERATOR1) //TODO: what with other creature generators? continue; auto d = dynamic_cast(obj); for(auto creature : d->creatures) { if(creature.first) //there are more than 0 creatures avaliabe { for(auto type : creature.second) { if(type == objid && ah->freeResources().canAfford(VLC->creh->creatures[type]->cost)) dwellings.push_back(d); } } } } if(dwellings.size()) { typedef std::map TDwellMap; // sorted helper auto comparator = [](const TDwellMap::value_type & a, const TDwellMap::value_type & b) -> bool { const CGPathNode * ln = ai->myCb->getPathsInfo(a.first)->getPathInfo(a.second->visitablePos()); const CGPathNode * rn = ai->myCb->getPathsInfo(b.first)->getPathInfo(b.second->visitablePos()); if(ln->turns != rn->turns) return ln->turns < rn->turns; return (ln->moveRemains > rn->moveRemains); }; // for all owned heroes generate map nearest dwelling> TDwellMap nearestDwellings; for(const CGHeroInstance * hero : cb->getHeroesInfo(true)) { nearestDwellings[hero] = *boost::range::min_element(dwellings, CDistanceSorter(hero)); } if(nearestDwellings.size()) { // find hero who is nearest to a dwelling const CGDwelling * nearest = boost::range::min_element(nearestDwellings, comparator)->second; if(!nearest) throw cannotFulfillGoalException("Cannot find nearest dwelling!"); return sptr(Goals::VisitObj(nearest->id.getNum())); } else return sptr(Goals::Explore()); } else { return sptr(Goals::Explore()); } //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()); } TGoalVec Conquer::getAllPossibleSubgoals() { TGoalVec ret; auto conquerable = [](const CGObjectInstance * obj) -> bool { if(cb->getPlayerRelations(ai->playerID, obj->tempOwner) == PlayerRelations::ENEMIES) { switch(obj->ID.num) { case Obj::TOWN: case Obj::HERO: case Obj::CREATURE_GENERATOR1: case Obj::MINE: //TODO: check ai->knownSubterraneanGates return true; } } return false; }; std::vector objs; for(auto obj : ai->visitableObjs) { if(conquerable(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(conquerable(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 { if(obj->ID.num == Obj::HERO) //enemy hero may move to other position { ret.push_back(sptr(Goals::VisitHero(obj->id.getNum()).sethero(h).setisAbstract(true))); } else //just get that object { ret.push_back(sptr(Goals::VisitObj(obj->id.getNum()).sethero(h).setisAbstract(true))); } } } else //we need to get army in order to conquer that place ret.push_back(sptr(Goals::GatherArmy(evaluateDanger(dest, h) * SAFE_ATTACK_CONSTANT).sethero(h).setisAbstract(true))); } } } } if(!objs.empty() && ai->canRecruitAnyHero()) //probably no point to recruit hero if we see no objects to capture ret.push_back(sptr(Goals::RecruitHero())); if(ret.empty()) ret.push_back(sptr(Goals::Explore())); //we need to find an enemy return ret; } TGoalVec Goals::Build::getAllPossibleSubgoals() { TGoalVec ret; for (const CGTownInstance * t : cb->getTownsInfo()) { //start fresh with every town ah->getBuildingOptions(t); auto ib = ah->immediateBuilding(); if (ib.is_initialized()) { ret.push_back(sptr(Goals::BuildThis(ib.get().bid, t).setpriority(2))); //prioritize buildings we can build quick } else //try build later { auto eb = ah->expensiveBuilding(); if (eb.is_initialized()) { auto pb = eb.get(); //gather resources for any we can't afford auto goal = ah->whatToDo(pb.price, sptr(Goals::BuildThis(pb.bid, t).setpriority(0.5))); ret.push_back(goal); } } } if (ret.empty()) throw cannotFulfillGoalException("BUILD has been realized as much as possible."); //who catches it and what for? else return ret; } TSubgoal Build::whatToDoToAchieve() { return fh->chooseSolution(getAllPossibleSubgoals()); } 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(); } std::string GatherArmy::completeMessage() const { return "Hero " + hero.get()->name + " gathered army of value " + boost::lexical_cast(value); } TSubgoal GatherArmy::whatToDoToAchieve() { //TODO: find hero if none set assert(hero.h); return fh->chooseSolution(getAllPossibleSubgoals()); //find dwelling. use current hero to prevent him from doing nothing. } static const BuildingID unitsSource[] = { BuildingID::DWELL_LVL_1, BuildingID::DWELL_LVL_2, BuildingID::DWELL_LVL_3, BuildingID::DWELL_LVL_4, BuildingID::DWELL_LVL_5, BuildingID::DWELL_LVL_6, BuildingID::DWELL_LVL_7}; TGoalVec GatherArmy::getAllPossibleSubgoals() { //get all possible towns, heroes and dwellings we may use TGoalVec ret; //TODO: include evaluation of monsters gather in calculation for(auto t : cb->getTownsInfo()) { 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) { auto goal = sptr(Goals::BuyArmy(t, val).sethero(hero)); if (!ah->containsObjective(goal)) //avoid loops caused by reserving same objective twice ret.push_back(goal); } } //build dwelling auto bid = ah->canBuildAnyStructure(t, std::vector(unitsSource, unitsSource + ARRAY_COUNT(unitsSource)), 8 - cb->getDate(Date::DAY_OF_WEEK)); if (bid.is_initialized()) { auto goal = sptr(BuildThis(bid.get(), t).setpriority(priority)); if (!ah->containsObjective(goal)) //avoid loops caused by reserving same objective twice ret.push_back(goal); } } } auto otherHeroes = cb->getHeroesInfo(); auto heroDummy = hero; vstd::erase_if(otherHeroes, [heroDummy](const CGHeroInstance * h) { if(h == heroDummy.h) return true; else if(!ai->isAccessibleForHero(heroDummy->visitablePos(), h, true)) return true; else if(!ai->canGetArmy(heroDummy.h, h)) //TODO: return actual aiValue return true; else if(ai->getGoal(h)->goalType == Goals::GATHER_ARMY) return true; else return false; }); for(auto h : otherHeroes) { // Go to the other hero if we are faster if (!vstd::contains(ai->visitedHeroes[hero], h)) //visit only once each turn //FIXME: this is only bug workaround ret.push_back(sptr(Goals::VisitHero(h->id.getNum()).setisAbstract(true).sethero(hero))); // Let the other hero come to us if (!vstd::contains(ai->visitedHeroes[h], hero)) ret.push_back(sptr(Goals::VisitHero(hero->id.getNum()).setisAbstract(true).sethero(h))); } std::vector objs; for(auto obj : ai->visitableObjs) { if(obj->ID == Obj::CREATURE_GENERATOR1) { auto relationToOwner = cb->getPlayerRelations(obj->getOwner(), ai->playerID); //Use flagged dwellings only when there are available creatures that we can afford if(relationToOwner == PlayerRelations::SAME_PLAYER) { auto dwelling = dynamic_cast(obj); for(auto & creLevel : dwelling->creatures) { if(creLevel.first) { for(auto & creatureID : creLevel.second) { auto creature = VLC->creh->creatures[creatureID]; if(ah->freeResources().canAfford(creature->cost)) objs.push_back(obj); //TODO: reserve resources? } } } } } } for(auto h : cb->getHeroesInfo()) { auto sm = ai->getCachedSectorMap(h); for(auto obj : objs) { //find safe dwelling auto pos = obj->visitablePos(); if(ai->isGoodForVisit(obj, h, *sm)) ret.push_back(sptr(Goals::VisitTile(pos).sethero(h))); } } if(ai->canRecruitAnyHero() && ah->freeGold() > GameConstants::HERO_GOLD_COST) //this is not stupid in early phase of game { if(auto t = ai->findTownWithTavern()) { for(auto h : cb->getAvailableHeroes(t)) //we assume that all towns have same set of heroes { if(h && h->getTotalStrength() > 500) //do not buy heroes with single creatures for GatherArmy { ret.push_back(sptr(Goals::RecruitHero())); break; } } } } if(ret.empty()) { if(hero == ai->primaryHero()) ret.push_back(sptr(Goals::Explore())); else throw cannotFulfillGoalException("No ways to gather army"); } return ret; } //TSubgoal AbstractGoal::whatToDoToAchieve() //{ // logAi->debug("Decomposing goal of type %s",name()); // return sptr (Goals::Explore()); //} TSubgoal AbstractGoal::goVisitOrLookFor(const CGObjectInstance * obj) { if(obj) return sptr(Goals::VisitObj(obj->id.getNum())); else return sptr(Goals::Explore()); } TSubgoal AbstractGoal::lookForArtSmart(int aid) { return sptr(Goals::Invalid()); } bool AbstractGoal::invalid() const { return goalType == INVALID; } void AbstractGoal::accept(VCAI * ai) { ai->tryRealize(*this); } template void CGoal::accept(VCAI * ai) { ai->tryRealize(static_cast(*this)); //casting enforces template instantiation } float AbstractGoal::accept(FuzzyHelper * f) { return f->evaluate(*this); } template float CGoal::accept(FuzzyHelper * f) { return f->evaluate(static_cast(*this)); //casting enforces template instantiation }