From aec04d920e727cd49dd34d66e4fb4a662a959bac Mon Sep 17 00:00:00 2001 From: DjWarmonger Date: Mon, 23 Dec 2013 20:46:01 +0000 Subject: [PATCH] Endless crusade against AI issues and loopholes! - Fixed #1126 - ClearWayTo and GatherArmy goals will also consider multiple subgoals - GatherArmy may include building dwellings in town (experimental) --- AI/VCAI/AIUtility.cpp | 4 +- AI/VCAI/Fuzzy.cpp | 13 +- AI/VCAI/Fuzzy.h | 1 + AI/VCAI/Goals.cpp | 275 ++++++++++++++++++++++-------------------- AI/VCAI/Goals.h | 4 +- AI/VCAI/VCAI.cpp | 215 +++++++++++++++++++++++---------- AI/VCAI/VCAI.h | 22 +++- 7 files changed, 326 insertions(+), 208 deletions(-) diff --git a/AI/VCAI/AIUtility.cpp b/AI/VCAI/AIUtility.cpp index 9fcb96df1..078a2dbe2 100644 --- a/AI/VCAI/AIUtility.cpp +++ b/AI/VCAI/AIUtility.cpp @@ -237,6 +237,7 @@ int3 whereToExplore(HeroPtr h) } } } + removeDuplicates (nearbyVisitableObjs); //one object may occupy multiple tiles boost::sort(nearbyVisitableObjs, isCloser); if(nearbyVisitableObjs.size()) return nearbyVisitableObjs.back()->visitablePos(); @@ -247,8 +248,7 @@ int3 whereToExplore(HeroPtr h) } catch(cannotFulfillGoalException &e) { - std::vector > tiles; //tiles[distance_to_fow] - return ai->explorationNewPoint(radius, h, tiles); + return ai->explorationNewPoint(radius, h); } } diff --git a/AI/VCAI/Fuzzy.cpp b/AI/VCAI/Fuzzy.cpp index 0880ecbdb..c93071ec1 100644 --- a/AI/VCAI/Fuzzy.cpp +++ b/AI/VCAI/Fuzzy.cpp @@ -337,6 +337,7 @@ FuzzyHelper::EvalVisitTile::~EvalVisitTile() delete heroStrength; delete tileDistance; delete missionImportance; + delete movement; } void FuzzyHelper::initVisitTile() @@ -347,9 +348,10 @@ void FuzzyHelper::initVisitTile() vt.heroStrength = new fl::InputLVar("heroStrength"); //we want to use weakest possible hero vt.tileDistance = new fl::InputLVar("tileDistance"); //we want to use hero who is near vt.missionImportance = new fl::InputLVar("lockedMissionImportance"); //we may want to preempt hero with low-priority mission + vt.movement = new fl::InputLVar("movement"); vt.value = new fl::OutputLVar("Value"); - helper += vt.strengthRatio, vt.heroStrength, vt.tileDistance, vt.missionImportance; + helper += vt.strengthRatio, vt.heroStrength, vt.tileDistance, vt.missionImportance, vt.movement; vt.strengthRatio->addTerm (new fl::ShoulderTerm("LOW", 0.9, SAFE_ATTACK_CONSTANT, true)); vt.strengthRatio->addTerm (new fl::ShoulderTerm("HIGH", SAFE_ATTACK_CONSTANT, SAFE_ATTACK_CONSTANT * 3, false)); @@ -368,6 +370,9 @@ void FuzzyHelper::initVisitTile() vt.value->addTerm (new fl::ShoulderTerm("LOW", 0, 1.1, true)); vt.value->addTerm (new fl::ShoulderTerm("HIGH", 1, 5, false)); + + vt.movement->addTerm (new fl::ShoulderTerm("LOW", 1, 200, true)); + vt.movement->addTerm (new fl::ShoulderTerm("HIGH", 1000, 2000, false)); for (auto val : helper) { @@ -387,6 +392,9 @@ void FuzzyHelper::initVisitTile() //pick nearby objects if it's easy, avoid long walks vt.rules.addRule (new fl::MamdaniRule("if tileDistance is SMALL then Value is HIGH", engine)); vt.rules.addRule (new fl::MamdaniRule("if tileDistance is LONG then Value is LOW", engine)); + //use heroes with movement points first + vt.rules.addRule (new fl::MamdaniRule("if movement is LOW then Value is somewhat LOW", engine)); + vt.rules.addRule (new fl::MamdaniRule("if movement is HIGH then Value is somewhat HIGH", engine)); engine.addRuleBlock (&vt.rules); } @@ -416,6 +424,7 @@ float FuzzyHelper::evaluate (Goals::VisitTile & g) vt.heroStrength->setInput (g.hero->getTotalStrength()); vt.tileDistance->setInput (distance); vt.missionImportance->setInput (missionImportance); + vt.movement->setInput(g.hero->movement); engine.process (VISIT_TILE); output = vt.value->output().defuzzify(); @@ -435,7 +444,7 @@ float FuzzyHelper::evaluate (Goals::VisitHero & g) } float FuzzyHelper::evaluate (Goals::BuildThis & g) { - return 0; + return 1; } float FuzzyHelper::evaluate (Goals::DigAtTile & g) { diff --git a/AI/VCAI/Fuzzy.h b/AI/VCAI/Fuzzy.h index c0567c26d..d541937a8 100644 --- a/AI/VCAI/Fuzzy.h +++ b/AI/VCAI/Fuzzy.h @@ -47,6 +47,7 @@ class FuzzyHelper fl::InputLVar * heroStrength; fl::InputLVar * tileDistance; fl::InputLVar * missionImportance; + fl::InputLVar * movement; fl::OutputLVar * value; fl::RuleBlock rules; ~EvalVisitTile(); diff --git a/AI/VCAI/Goals.cpp b/AI/VCAI/Goals.cpp index 6e358a446..971b59618 100644 --- a/AI/VCAI/Goals.cpp +++ b/AI/VCAI/Goals.cpp @@ -29,6 +29,7 @@ TSubgoal Goals::sptr(const AbstractGoal & tmp) std::string Goals::AbstractGoal::name() const //TODO: virtualize { + std::string desc; switch (goalType) { case INVALID: @@ -42,38 +43,53 @@ std::string Goals::AbstractGoal::name() const //TODO: virtualize case BUILD: return "BUILD"; case EXPLORE: - return "EXPLORE"; + desc = "EXPLORE"; + break; case GATHER_ARMY: - return "GATHER ARMY"; + desc = "GATHER ARMY"; + break; case BOOST_HERO: - return "BOOST_HERO (unsupported)"; + desc = "BOOST_HERO (unsupported)"; + break; case RECRUIT_HERO: return "RECRUIT HERO"; case BUILD_STRUCTURE: return "BUILD STRUCTURE"; case COLLECT_RES: - return "COLLECT RESOURCE"; + desc = "COLLECT RESOURCE"; + break; case GATHER_TROOPS: - return "GATHER TROOPS"; + desc = "GATHER TROOPS"; + break; case GET_OBJ: - return "GET OBJECT " + boost::lexical_cast(objid); + desc = "GET OBJ " + cb->getObjInstance(ObjectInstanceID(objid))->getHoverText(); + break; case FIND_OBJ: - return "FIND OBJECT " + boost::lexical_cast(objid); + desc = "FIND OBJ " + boost::lexical_cast(objid); + break; case VISIT_HERO: - return "VISIT HERO " + boost::lexical_cast(objid); + desc = "VISIT HERO " + cb->getObjInstance(ObjectInstanceID(objid))->getHoverText(); + break; case GET_ART_TYPE: - return "GET ARTIFACT OF TYPE " + VLC->arth->artifacts[aid]->Name(); + desc = "GET ARTIFACT OF TYPE " + VLC->arth->artifacts[aid]->Name(); + break; case ISSUE_COMMAND: return "ISSUE COMMAND (unsupported)"; case VISIT_TILE: - return "VISIT TILE " + tile(); + desc = "VISIT TILE " + tile(); + break; case CLEAR_WAY_TO: - return "CLEAR WAY TO " + tile(); + desc = "CLEAR WAY TO " + tile(); + break; case DIG_AT_TILE: - return "DIG AT TILE " + tile(); + desc = "DIG AT TILE " + tile(); + break; default: return boost::lexical_cast(goalType); } + if (hero.h) + desc += " (" + hero->name + ")"; + return desc; } //TODO: find out why the following are not generated automatically on MVS? @@ -312,52 +328,56 @@ float GetArtOfType::importanceWhenLocked() const TSubgoal ClearWayTo::whatToDoToAchieve() { - assert(tile.x >= 0); //set tile + assert(cb->isInTheMap(tile)); //set tile if(!cb->isVisible(tile)) { logAi->errorStream() << "Clear way should be used with visible tiles!"; return sptr (Goals::Explore()); } - HeroPtr h = hero ? hero : ai->primaryHero(); - if(!h) - return sptr (Goals::RecruitHero()); + return (fh->chooseSolution(getAllPossibleSubgoals())); +} - cb->setSelection(*h); - - SectorMap sm; - bool dropToFile = false; - if(dropToFile) //for debug purposes - sm.write("test.txt"); - - int3 tileToHit = sm.firstTileToGet(h, tile); - //if(isSafeToVisit(h, tileToHit)) - if(isBlockedBorderGate(tileToHit)) - { //FIXME: this way we'll not visit gate and activate quest :? - return sptr (Goals::FindObj (Obj::KEYMASTER, cb->getTile(tileToHit)->visitableObjects.back()->subID)); - } - - //FIXME: this code shouldn't be necessary - if(tileToHit == tile) +TGoalVec ClearWayTo::getAllPossibleSubgoals() +{ + TGoalVec ret; + for (auto h : cb->getHeroesInfo()) { - logAi->errorStream() << boost::format("Very strange, tile to hit is %s and tile is also %s, while hero %s is at %s\n") - % tileToHit % tile % h->name % h->visitablePos(); - throw cannotFulfillGoalException("Retrieving first tile to hit failed (probably)!"); + cb->setSelection(h); + + SectorMap sm; + + int3 tileToHit = sm.firstTileToGet(hero ? hero : h, tile); + //if our hero is trapped, make sure we request clearing the way from OUR perspective + + 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))); + } + + ////FIXME: this code shouldn't be necessary + //if(tileToHit == tile) + //{ + // logAi->errorStream() << boost::format("Very strange, tile to hit is %s and tile is also %s, while hero %s is at %s\n") + // % tileToHit % tile % h->name % h->visitablePos(); + // throw cannotFulfillGoalException("Retrieving first tile to hit failed (probably)!"); + //} + + auto topObj = backOrNull(cb->getVisitableObjs(tileToHit)); + if(topObj && topObj->ID == Obj::HERO && cb->getPlayerRelations(h->tempOwner, topObj->tempOwner) != PlayerRelations::ENEMIES) + { + logAi->errorStream() << boost::format("%s stands in the way of %s.\n") % topObj->getHoverText() % h->getHoverText(); + } + else + ret.push_back (sptr (Goals::VisitTile(tileToHit).sethero(h))); } + if (ai->canRecruitAnyHero()) + ret.push_back (sptr (Goals::RecruitHero())); - auto topObj = backOrNull(cb->getVisitableObjs(tileToHit)); - if(topObj && topObj->ID == Obj::HERO && cb->getPlayerRelations(h->tempOwner, topObj->tempOwner) != PlayerRelations::ENEMIES) - { - std::string problem = boost::str(boost::format("%s stands in the way of %s.\n") % topObj->getHoverText() % h->getHoverText()); - throw cannotFulfillGoalException(problem); - } + if (ret.empty()) + throw cannotFulfillGoalException("There is no known way to clear the way to tile " + tile()); - return sptr (Goals::VisitTile(tileToHit).sethero(h)); - //FIXME:: attempts to visit completely unreachable tile with hero results in stall - - //TODO czy istnieje lepsza droga? - - throw cannotFulfillGoalException("Cannot reach given tile!"); //how and when could this be used? + return ret; } float ClearWayTo::importanceWhenLocked() const @@ -387,12 +407,15 @@ TSubgoal Explore::whatToDoToAchieve() TGoalVec Explore::getAllPossibleSubgoals() { TGoalVec ret; - std::vector heroes; + std::vector heroes; + //std::vector heroes; if (hero) - heroes.push_back(hero); + //heroes.push_back(hero); + heroes.push_back(hero.h); else { - heroes = ai->getUnblockedHeroes(); + //heroes = ai->getUnblockedHeroes(); + heroes = cb->getHeroesInfo(); erase_if (heroes, [](const HeroPtr h) { return !h->movement; //saves time, immobile heroes are useless anyway @@ -429,9 +452,20 @@ TGoalVec Explore::getAllPossibleSubgoals() if (cb->isInTheMap(t)) //valid tile was found - could be invalid (none) ret.push_back (sptr (Goals::VisitTile(t).sethero(h))); } - if (!hero && ai->canRecruitAnyHero())//if hero is assigned to that goal, no need to buy another one yet + //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()) + { + auto h = ai->primaryHero(); //we may need to gather big army to break! + if (h.h) + { + int3 t = ai->explorationNewPoint(h->getSightRadious(), h, true); + if (cb->isInTheMap(t)) + ret.push_back (sptr(ClearWayTo(t).setisAbstract(true).sethero(ai->primaryHero()))); + } + } if (ret.empty()) throw cannotFulfillGoalException("Cannot explore - no possible ways found!"); @@ -473,7 +507,8 @@ TSubgoal VisitTile::whatToDoToAchieve() } else { - return sptr (Goals::GatherArmy(evaluateDanger(tile, *ret->hero) * SAFE_ATTACK_CONSTANT).sethero(ret->hero)); + return sptr (Goals::GatherArmy(evaluateDanger(tile, *ret->hero) * SAFE_ATTACK_CONSTANT) + .sethero(ret->hero).setisAbstract(true)); } } return ret; @@ -507,6 +542,7 @@ TGoalVec VisitTile::getAllPossibleSubgoals() } if (ret.empty()) 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; } @@ -746,68 +782,54 @@ std::string GatherArmy::completeMessage() const TSubgoal GatherArmy::whatToDoToAchieve() { //TODO: find hero if none set - assert(hero); + assert(hero.h); - cb->setSelection(*hero); - auto compareReinforcements = [this](const CGTownInstance *lhs, const CGTownInstance *rhs) -> bool + return fh->chooseSolution (getAllPossibleSubgoals()); //find dwelling. use current hero to prevent him from doing nothing. +} +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()) { - return howManyReinforcementsCanGet(hero, lhs) < howManyReinforcementsCanGet(hero, rhs); - }; - - std::vector townsReachable; - for(const CGTownInstance *t : cb->getTownsInfo()) - { - if(!t->visitingHero && howManyReinforcementsCanGet(hero,t)) + auto pos = t->visitablePos(); + if (ai->isAccessibleForHero(pos, hero)) { - if (ai->isAccessibleForHero(t->pos, hero) && !vstd::contains (ai->townVisitsThisWeek[hero], t)) - townsReachable.push_back(t); - } - } - - if(townsReachable.size()) //try towns first - { - boost::sort(townsReachable, compareReinforcements); - return sptr (Goals::VisitTile(townsReachable.back()->visitablePos()).sethero(hero)); - } - else - { - if (hero == ai->primaryHero()) //we can get army from other heroes - { - auto otherHeroes = cb->getHeroesInfo(); - auto heroDummy = hero; - erase_if(otherHeroes, [heroDummy](const CGHeroInstance * h) + if(!t->visitingHero && howManyReinforcementsCanGet(hero,t)) { - return (h == heroDummy.h || !ai->isAccessibleForHero(heroDummy->visitablePos(), h, true) || !ai->canGetArmy(heroDummy.h, h)); - }); - if (otherHeroes.size()) - { - boost::sort(otherHeroes, compareArmyStrength); //TODO: check if hero has at least one stack more powerful than ours? not likely to fail - int primaryPath, secondaryPath; - auto h = otherHeroes.back(); - cb->setSelection(hero.h); - primaryPath = cb->getPathInfo(h->visitablePos())->turns; - cb->setSelection(h); - secondaryPath = cb->getPathInfo(hero->visitablePos())->turns; - - if (primaryPath < secondaryPath) - return sptr (Goals::VisitHero(h->id.getNum()).setisAbstract(true).sethero(hero)); - //go to the other hero if we are faster - else - return sptr (Goals::VisitHero(hero->id.getNum()).setisAbstract(true).sethero(h)); - //let the other hero come to us + if (!vstd::contains (ai->townVisitsThisWeek[hero], t)) + ret.push_back (sptr (Goals::VisitTile(pos).sethero(hero))); } + 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))); } + } - std::vector objs; //here we'll gather all dwellings - ai->retreiveVisitableObjs(objs, true); - erase_if(objs, [&](const CGObjectInstance *obj) + auto otherHeroes = cb->getHeroesInfo(); + auto heroDummy = hero; + erase_if(otherHeroes, [heroDummy](const CGHeroInstance * h) + { + return (h == heroDummy.h || !ai->isAccessibleForHero(heroDummy->visitablePos(), h, true) || !ai->canGetArmy(heroDummy.h, h)); + }); + for (auto h : otherHeroes) + { + + ret.push_back (sptr (Goals::VisitHero(h->id.getNum()).setisAbstract(true).sethero(hero))); + //go to the other hero if we are faster + ret.push_back (sptr (Goals::VisitHero(hero->id.getNum()).setisAbstract(true).sethero(h))); + //let the other hero come to us + } + + std::vector objs; + for (auto obj : ai->visitableObjs) + { + if(obj->ID == Obj::CREATURE_GENERATOR1) { - if(obj->ID != Obj::CREATURE_GENERATOR1) - return true; - auto relationToOwner = cb->getPlayerRelations(obj->getOwner(), ai->playerID); - if(relationToOwner == PlayerRelations::ALLIES) - return true; //Use flagged dwellings only when there are available creatures that we can afford if(relationToOwner == PlayerRelations::SAME_PLAYER) @@ -820,42 +842,27 @@ TSubgoal GatherArmy::whatToDoToAchieve() for(auto & creatureID : creLevel.second) { auto creature = VLC->creh->creatures[creatureID]; - if(ai->freeResources().canAfford(creature->cost)) - return false; + if (ai->freeResources().canAfford(creature->cost)) + objs.push_back(obj); } } } } - - return true; - }); - if(objs.empty()) //no possible objects, we did eveyrthing already - return sptr (Goals::Explore(hero)); - //TODO: check if we can recruit any creatures there, evaluate army - else - { - boost::sort(objs, isCloser); - HeroPtr h = nullptr; - for(const CGObjectInstance *obj : objs) - { //find safe dwelling - auto pos = obj->visitablePos(); - if (shouldVisit (hero, obj)) //creatures fit in army - h = hero; - else - { - for(auto ourHero : cb->getHeroesInfo()) //make use of multiple heroes - { - if (shouldVisit(ourHero, obj)) - h = ourHero; - } - } - if (h && isSafeToVisit(h, pos) && ai->isAccessibleForHero(pos, h)) - return sptr (Goals::VisitTile(pos).sethero(h)); - } } } + for(auto h : cb->getHeroesInfo()) + { + for (auto obj : objs) + { //find safe dwelling + auto pos = obj->visitablePos(); + if (shouldVisit (h, obj) && isSafeToVisit(h, pos) && ai->isAccessibleForHero(pos, h)) + ret.push_back (sptr (Goals::VisitTile(pos).sethero(h))); + } + } + if (ret.empty()) + ret.push_back (sptr(Goals::Explore())); - return sptr (Goals::Explore(hero)); //find dwelling. use current hero to prevent him from doing nothing. + return ret; } float GatherArmy::importanceWhenLocked() const diff --git a/AI/VCAI/Goals.h b/AI/VCAI/Goals.h index c0160e580..a8a0a1025 100644 --- a/AI/VCAI/Goals.h +++ b/AI/VCAI/Goals.h @@ -240,7 +240,7 @@ private: GatherArmy() : CGoal (Goals::GATHER_ARMY){}; public: GatherArmy(int val) : CGoal (Goals::GATHER_ARMY){value = val;}; - TGoalVec getAllPossibleSubgoals() override {return TGoalVec();}; + TGoalVec getAllPossibleSubgoals() override; TSubgoal whatToDoToAchieve() override; std::string completeMessage() const override; float importanceWhenLocked() const override; @@ -356,7 +356,7 @@ class ClearWayTo : public CGoal { public: ClearWayTo(int3 Tile) : CGoal (Goals::CLEAR_WAY_TO) {tile = Tile;}; - TGoalVec getAllPossibleSubgoals() override {return TGoalVec();}; + TGoalVec getAllPossibleSubgoals() override; TSubgoal whatToDoToAchieve() override; bool operator== (ClearWayTo &g) {return g.tile == tile;} float importanceWhenLocked() const override; diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index 32f53dcdd..d835c4b98 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -406,9 +406,11 @@ void VCAI::newStackInserted(const StackLocation &location, const CStackInstance NET_EVENT_HANDLER; } -void VCAI::heroCreated(const CGHeroInstance*) +void VCAI::heroCreated(const CGHeroInstance* h) { LOG_TRACE(logAi); + if (h->visitedTown) + townVisitsThisWeek[HeroPtr(h)].push_back(h->visitedTown); NET_EVENT_HANDLER; } @@ -523,6 +525,9 @@ void VCAI::init(shared_ptr CB) fh = new FuzzyHelper(); retreiveVisitableObjs(visitableObjs); + //for (auto h : myCb->getHeroesInfo()) //make sure heroes won't try to revisit town in first move + // if (h->visitedTown) + // markObjectVisited(h->visitedTown); } void VCAI::yourTurn() @@ -651,30 +656,6 @@ void VCAI::makeTurn() } } break; - case 7: //reconsider strategy - { - if(auto h = primaryHero()) //check if our primary hero can handle danger - { - ui64 totalDanger = 0; - int dangerousObjects = 0; - std::vector objs; - retreiveVisitableObjs(objs, false); - for (auto obj : objs) - { - if (evaluateDanger(obj)) //potentilaly dnagerous - { - totalDanger += evaluateDanger(obj->visitablePos(), *h); - ++dangerousObjects; - } - } - ui64 averageDanger = totalDanger / std::max(dangerousObjects, 1); - if (dangerousObjects && averageDanger > h->getHeroStrength()) - { - setGoal (h, sptr(Goals::GatherArmy(averageDanger * SAFE_ATTACK_CONSTANT).sethero(h).setisAbstract(true))); - } - } - } - break; } if(cb->getSelectedHero()) cb->recalculatePaths(); @@ -1006,6 +987,84 @@ bool VCAI::tryBuildStructure(const CGTownInstance * t, BuildingID building, unsi return false; } +//bool VCAI::canBuildStructure(const CGTownInstance * t, BuildingID building, unsigned int maxDays=7) +//{ +// if (maxDays == 0) +// { +// logAi->warnStream() << "Request to build building " << building << " in 0 days!"; +// return false; +// } +// +// if (!vstd::contains(t->town->buildings, building)) +// return false; // no such building in town +// +// if (t->hasBuilt(building)) //Already built? Shouldn't happen in general +// return true; +// +// const CBuilding * buildPtr = t->town->buildings.at(building); +// +// auto toBuild = buildPtr->requirements.getFulfillmentCandidates([&](const BuildingID & buildID) +// { +// return t->hasBuilt(buildID); +// }); +// toBuild.push_back(building); +// +// for(BuildingID buildID : toBuild) +// { +// EBuildingState::EBuildingState canBuild = cb->canBuildStructure(t, buildID); +// if (canBuild == EBuildingState::HAVE_CAPITAL +// || canBuild == EBuildingState::FORBIDDEN +// || canBuild == EBuildingState::NO_WATER) +// return false; //we won't be able to build this +// } +// +// if (maxDays && toBuild.size() > maxDays) +// return false; +// +// TResources currentRes = cb->getResourceAmount(); +// TResources income = estimateIncome(); +// //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(!containsSavedRes(b->resources)) +// { +// logAi->debugStream() << boost::format("Player %d will build %s in town of %s at %s") % playerID % b->Name() % t->name % t->pos; +// return true; +// } +// continue; +// } +// else if(canBuild == EBuildingState::NO_RESOURCES) +// { +// TResources cost = t->town->buildings.at(buildID)->resources; +// for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; i++) +// { +// int diff = currentRes[i] - cost[i] + income[i]; +// if(diff < 0) +// saving[i] = 1; +// } +// continue; +// } +// else if (canBuild == EBuildingState::PREREQUIRES) +// { +// // can happen when dependencies have their own missing dependencies +// if (canBuildStructure(t, buildID, maxDays - 1)) +// return true; +// } +// else if (canBuild == EBuildingState::MISSING_BASE) +// { +// if (canBuildStructure(t, b->upgrade, maxDays - 1)) +// return true; +// } +// } +// return false; +//} + bool VCAI::tryBuildAnyStructure(const CGTownInstance * t, std::vector buildList, unsigned int maxDays) { for(const auto & building : buildList) @@ -1018,6 +1077,18 @@ bool VCAI::tryBuildAnyStructure(const CGTownInstance * t, std::vector buildList, unsigned int maxDays) +{ + for(const auto & building : buildList) + { + if(t->hasBuilt(building)) + continue; + if (cb->canBuildStructure(t, building)) + return building; + } + return BuildingID::NONE; //Can't build anything +} + bool VCAI::tryBuildNextStructure(const CGTownInstance * t, std::vector buildList, unsigned int maxDays) { for(const auto & building : buildList) @@ -1036,20 +1107,6 @@ void VCAI::buildStructure(const CGTownInstance * t) //TODO: build resource silo, defences when needed //Possible - allow "locking" on specific building (build prerequisites and then building itself) - //Set of buildings for different goals. Does not include any prerequisites. - const BuildingID essential[] = {BuildingID::TAVERN, BuildingID::TOWN_HALL}; - const BuildingID goldSource[] = {BuildingID::TOWN_HALL, BuildingID::CITY_HALL, BuildingID::CAPITOL}; - 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}; - const BuildingID unitsUpgrade[] = { BuildingID::DWELL_LVL_1_UP, BuildingID::DWELL_LVL_2_UP, BuildingID::DWELL_LVL_3_UP, - BuildingID::DWELL_LVL_4_UP, BuildingID::DWELL_LVL_5_UP, BuildingID::DWELL_LVL_6_UP, BuildingID::DWELL_LVL_7_UP}; - const BuildingID unitGrowth[] = { BuildingID::FORT, BuildingID::CITADEL, BuildingID::CASTLE, BuildingID::HORDE_1, - BuildingID::HORDE_1_UPGR, BuildingID::HORDE_2, BuildingID::HORDE_2_UPGR}; - const BuildingID spells[] = {BuildingID::MAGES_GUILD_1, BuildingID::MAGES_GUILD_2, BuildingID::MAGES_GUILD_3, - BuildingID::MAGES_GUILD_4, BuildingID::MAGES_GUILD_5}; - const BuildingID extra[] = {BuildingID::RESOURCE_SILO, BuildingID::SPECIAL_1, BuildingID::SPECIAL_2, BuildingID::SPECIAL_3, - BuildingID::SPECIAL_4, BuildingID::SHIPYARD}; // all remaining buildings - TResources currentRes = cb->getResourceAmount(); TResources income = estimateIncome(); @@ -1147,6 +1204,7 @@ bool VCAI::canRecruitAnyHero (const CGTownInstance * t) const void VCAI::wander(HeroPtr h) { + TimeCheck tc("looking for wander destination"); while(1) { validateVisitableObjs(); @@ -1164,6 +1222,9 @@ void VCAI::wander(HeroPtr h) if(!dests.size()) { + if (cb->getVisitableObjs(h->visitablePos()).size() > 1) + moveHeroToTile(h->visitablePos(), h); //just in case we're standing on blocked subterranean gate + auto compareReinforcements = [h](const CGTownInstance *lhs, const CGTownInstance *rhs) -> bool { return howManyReinforcementsCanGet(h, lhs) < howManyReinforcementsCanGet(h, rhs); @@ -1189,20 +1250,22 @@ void VCAI::wander(HeroPtr h) else if(townsNotReachable.size()) { boost::sort(townsNotReachable, compareReinforcements); - //TODO pick the truly best - const CGTownInstance *t = townsNotReachable.back(); - logAi->debugStream() << boost::format("%s can't reach any town, we'll try to make our way to %s at %s") % h->name % t->name % t->visitablePos(); + //TODO pick the truly best + const CGTownInstance *t = townsNotReachable.back(); + logAi->debugStream() << boost::format("%s can't reach any town, we'll try to make our way to %s at %s") % h->name % t->name % t->visitablePos(); int3 pos1 = h->pos; - striveToGoal(sptr(Goals::VisitTile(t->visitablePos()).sethero(h))); + striveToGoal(sptr(Goals::ClearWayTo(t->visitablePos()).sethero(h))); + //if out hero is stuck, we may need to request another hero to clear the way we see + if (pos1 == h->pos && h == primaryHero()) //hero can't move { if (canRecruitAnyHero(t)) recruitHero(t); } - break; + break; } else if(cb->getResourceAmount(Res::GOLD) >= HERO_GOLD_COST) - { + { std::vector towns = cb->getTownsInfo(); erase_if(towns, [](const CGTownInstance *t) -> bool { @@ -1212,13 +1275,13 @@ void VCAI::wander(HeroPtr h) return false; }); boost::sort(towns, compareArmyStrength); - if(towns.size()) - recruitHero(towns.back()); - break; - } - else - { - logAi->debugStream() << "Nowhere more to go..."; + if(towns.size()) + recruitHero(towns.back()); + break; + } + else + { + logAi->debugStream() << "Nowhere more to go..."; break; } } @@ -1233,10 +1296,6 @@ void VCAI::wander(HeroPtr h) else { logAi->debugStream() << boost::format("Hero %s apparently used all MPs (%d left)") % h->name % h->movement; - reserveObject(h, dest); //reserve that object - we predict it will be reached soon - - //removed - do not forget abstract goal so easily - //setGoal(h, CGoal(VISIT_TILE).sethero(h).settile(dest->visitablePos())); } break; } @@ -1520,9 +1579,15 @@ bool VCAI::moveHeroToTile(int3 dst, HeroPtr h) //BNLOG("Hero %s moved from %s to %s at %s", h->name % startHpos % visitedObject->hoverName % h->visitablePos()); //throw goalFulfilledException (CGoal(GET_OBJ).setobjid(visitedObject->id)); } - if(h) //we could have lost hero after last move { + if (!ret) //reserve object we are heading towards + { + auto obj = frontOrNull(cb->getVisitableObjs(dst)); + if (obj) + reserveObject(h, obj); + } + cb->recalculatePaths(); if (startHpos == h->visitablePos() && !ret) //we didn't move and didn't reach the target { @@ -2023,9 +2088,11 @@ int3 VCAI::explorationBestNeighbour(int3 hpos, int radius, HeroPtr h) throw cannotFulfillGoalException("No neighbour will bring new discoveries!"); } -int3 VCAI::explorationNewPoint(int radius, HeroPtr h, std::vector > &tiles) +int3 VCAI::explorationNewPoint(int radius, HeroPtr h, bool breakUnsafe) { logAi->debugStream() << "Looking for an another place for exploration..."; + + std::vector > tiles; //tiles[distance_to_fow] tiles.resize(radius); foreach_tile_pos([&](const int3 &pos) @@ -2034,6 +2101,9 @@ int3 VCAI::explorationNewPoint(int radius, HeroPtr h, std::vectorgetTile(tile)->blocked) //does it shorten the time? continue; - if(cb->getPathInfo(tile)->reachable() && howManyTilesWillBeDiscovered(tile, radius) && - isSafeToVisit(h, tile) && !isBlockedBorderGate(tile)) + int ourValue = howManyTilesWillBeDiscovered(tile, radius); + if (ourValue > bestValue) //avoid costly checks of tiles that don't reveal much { - return tile; //return first tile that will discover anything + if(cb->getPathInfo(tile)->reachable() && (isSafeToVisit(h, tile) || breakUnsafe) && !isBlockedBorderGate(tile)) + { + bestTile = tile; //return first tile that will discover anything + bestValue = ourValue; + } } } } - return int3 (-1,-1,-1); - //throw cannotFulfillGoalException("No accessible tile will bring discoveries!"); + return bestTile; } TResources VCAI::estimateIncome() const @@ -2125,8 +2198,17 @@ void VCAI::recruitHero(const CGTownInstance * t, bool throwing) { logAi->debugStream() << boost::format("Trying to recruit a hero in %s at %s") % t->name % t->visitablePos(); - if(auto availableHero = frontOrNull(cb->getAvailableHeroes(t))) - cb->recruitHero(t, availableHero); + auto heroes = cb->getAvailableHeroes(t); + if(heroes.size()) + { + auto hero = heroes[0]; + if (heroes.size() >= 2) //makes sense to recruit two heroes with starting amries in first week + { + if (heroes[1]->getTotalStrength() > hero->getTotalStrength()) + hero = heroes[1]; + } + cb->recruitHero(t, hero); + } else if(throwing) throw cannotFulfillGoalException("No available heroes in tavern in " + t->nodeName()); } @@ -2496,6 +2578,9 @@ bool shouldVisit(HeroPtr h, const CGObjectInstance * obj) { switch (obj->ID) { + case Obj::TOWN: + return obj->tempOwner != h->tempOwner; //do not visit our towns at random + break; case Obj::BORDER_GATE: { for (auto q : ai->myCb->getMyQuests()) diff --git a/AI/VCAI/VCAI.h b/AI/VCAI/VCAI.h index f190079eb..d4410e4cd 100644 --- a/AI/VCAI/VCAI.h +++ b/AI/VCAI/VCAI.h @@ -8,6 +8,7 @@ #include "../../lib/CThreadHelper.h" +#include "../../lib/GameConstants.h" #include "../../lib/VCMI_Lib.h" #include "../../lib/CBuildingHandler.h" #include "../../lib/CCreatureHandler.h" @@ -109,19 +110,34 @@ struct SectorMap int3 firstTileToGet(HeroPtr h, crint3 dst); //if h wants to reach tile dst, which tile he should visit to clear the way? }; +//Set of buildings for different goals. Does not include any prerequisites. +const BuildingID essential[] = {BuildingID::TAVERN, BuildingID::TOWN_HALL}; +const BuildingID goldSource[] = {BuildingID::TOWN_HALL, BuildingID::CITY_HALL, BuildingID::CAPITOL}; +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}; +const BuildingID unitsUpgrade[] = { BuildingID::DWELL_LVL_1_UP, BuildingID::DWELL_LVL_2_UP, BuildingID::DWELL_LVL_3_UP, + BuildingID::DWELL_LVL_4_UP, BuildingID::DWELL_LVL_5_UP, BuildingID::DWELL_LVL_6_UP, BuildingID::DWELL_LVL_7_UP}; +const BuildingID unitGrowth[] = { BuildingID::FORT, BuildingID::CITADEL, BuildingID::CASTLE, BuildingID::HORDE_1, + BuildingID::HORDE_1_UPGR, BuildingID::HORDE_2, BuildingID::HORDE_2_UPGR}; +const BuildingID spells[] = {BuildingID::MAGES_GUILD_1, BuildingID::MAGES_GUILD_2, BuildingID::MAGES_GUILD_3, + BuildingID::MAGES_GUILD_4, BuildingID::MAGES_GUILD_5}; +const BuildingID extra[] = {BuildingID::RESOURCE_SILO, BuildingID::SPECIAL_1, BuildingID::SPECIAL_2, BuildingID::SPECIAL_3, + BuildingID::SPECIAL_4, BuildingID::SHIPYARD}; // all remaining buildings class VCAI : public CAdventureAI { +public: //internal methods for town development //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); //try build ANY unbuilt structure + BuildingID canBuildAnyStructure(const CGTownInstance * t, std::vector buildList, unsigned int maxDays=7); bool tryBuildAnyStructure(const CGTownInstance * t, std::vector buildList, unsigned int maxDays=7); //try build first unbuilt structure bool tryBuildNextStructure(const CGTownInstance * t, std::vector buildList, unsigned int maxDays=7); -public: friend class FuzzyHelper; std::map knownSubterraneanGates; @@ -160,7 +176,7 @@ public: void tryRealize(Goals::AbstractGoal & g); int3 explorationBestNeighbour(int3 hpos, int radius, HeroPtr h); - int3 explorationNewPoint(int radius, HeroPtr h, std::vector > &tiles); + int3 explorationNewPoint(int radius, HeroPtr h, bool breakUnsafe = false); void recruitHero(); virtual std::string getBattleAIName() const override; @@ -259,7 +275,7 @@ public: void addVisitableObj(const CGObjectInstance *obj); void markObjectVisited (const CGObjectInstance *obj); - void reserveObject (HeroPtr h, const CGObjectInstance *obj); + void reserveObject (HeroPtr h, const CGObjectInstance *obj); //TODO: reserve all objects that heroes attempt to visit //void removeVisitableObj(const CGObjectInstance *obj); void validateObject(const CGObjectInstance *obj); //checks if object is still visible and if not, removes references to it void validateObject(ObjectIdRef obj); //checks if object is still visible and if not, removes references to it