1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-12-24 22:14:36 +02:00

Nullkiller AI: stabilization of build and prioritization fixes

This commit is contained in:
Andrii Danylchenko 2021-05-16 14:15:03 +03:00 committed by Andrii Danylchenko
parent de2361650b
commit 400967904b
24 changed files with 825 additions and 133 deletions

View File

@ -47,6 +47,12 @@ void AIhelper::setAI(VCAI * AI)
heroManager->setAI(AI);
}
void AIhelper::update()
{
armyManager->update();
heroManager->update();
}
bool AIhelper::getBuildingOptions(const CGTownInstance * t)
{
return buildingManager->getBuildingOptions(t);
@ -162,6 +168,16 @@ void AIhelper::updatePaths(std::vector<HeroPtr> heroes, bool useHeroChain)
pathfindingManager->updatePaths(heroes, useHeroChain);
}
uint64_t AIhelper::evaluateStackPower(const CCreature * creature, int count) const
{
return armyManager->evaluateStackPower(creature, count);
}
SlotInfo AIhelper::getTotalCreaturesAvailable(CreatureID creatureID) const
{
return armyManager->getTotalCreaturesAvailable(creatureID);
}
bool AIhelper::canGetArmy(const CArmedInstance * army, const CArmedInstance * source) const
{
return armyManager->canGetArmy(army, source);
@ -212,11 +228,6 @@ HeroRole AIhelper::getHeroRole(const HeroPtr & hero) const
return heroManager->getHeroRole(hero);
}
void AIhelper::updateHeroRoles()
{
heroManager->updateHeroRoles();
}
float AIhelper::evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const
{
return heroManager->evaluateSecSkill(skill, hero);

View File

@ -80,14 +80,17 @@ public:
std::vector<SlotInfo>::iterator getWeakestCreature(std::vector<SlotInfo> & army) const override;
std::vector<SlotInfo> getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const override;
std::vector<creInfo> getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const override;
uint64_t evaluateStackPower(const CCreature * creature, int count) const override;
SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const override;
const std::map<HeroPtr, HeroRole> & getHeroRoles() const override;
HeroRole getHeroRole(const HeroPtr & hero) const override;
int selectBestSkill(const HeroPtr & hero, const std::vector<SecondarySkill> & skills) const override;
void updateHeroRoles() override;
float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const override;
float evaluateHero(const CGHeroInstance * hero) const override;
void update() override;
private:
bool notifyGoalCompleted(Goals::TSubgoal goal) override;

View File

@ -14,6 +14,336 @@
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<VCAI> ai;
void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo)
{
auto townInfo = developmentInfo.town->town;
auto creatures = townInfo->creatures;
auto buildings = townInfo->getAllBuildings();
std::map<BuildingID, BuildingID> parentMap;
for(auto &pair : townInfo->buildings)
{
if(pair.second->upgrade != -1)
{
parentMap[pair.second->upgrade] = pair.first;
}
}
BuildingID prefixes[] = {BuildingID::DWELL_UP_FIRST, BuildingID::DWELL_FIRST};
for(int level = 0; level < GameConstants::CREATURES_PER_TOWN; level++)
{
logAi->trace("Checking dwelling level %d", level);
BuildingInfo nextToBuild = BuildingInfo();
for(BuildingID prefix : prefixes)
{
BuildingID building = BuildingID(prefix + level);
if(!vstd::contains(buildings, building))
continue; // no such building in town
auto info = getBuildingOrPrerequisite(developmentInfo.town, building);
if(info.exists)
{
developmentInfo.addExistingDwelling(info);
break;
}
nextToBuild = info;
}
if(nextToBuild.id != BuildingID::NONE)
{
developmentInfo.addBuildingToBuild(nextToBuild);
}
}
}
void BuildAnalyzer::updateOtherBuildings(TownDevelopmentInfo & developmentInfo)
{
logAi->trace("Checking other buildings");
std::vector<std::vector<BuildingID>> otherBuildings = {
{BuildingID::TOWN_HALL, BuildingID::CITY_HALL, BuildingID::CAPITOL}
};
if(developmentInfo.existingDwellings.size() >= 2 && cb->getDate(Date::DAY_OF_WEEK) > boost::date_time::Friday)
{
otherBuildings.push_back({BuildingID::CITADEL, BuildingID::CASTLE});
}
for(auto & buildingSet : otherBuildings)
{
for(auto & buildingID : buildingSet)
{
if(!developmentInfo.town->hasBuilt(buildingID))
{
developmentInfo.addBuildingToBuild(getBuildingOrPrerequisite(developmentInfo.town, buildingID));
break;
}
}
}
}
int32_t convertToGold(const TResources & res)
{
return res[Res::GOLD]
+ 75 * (res[Res::WOOD] + res[Res::ORE])
+ 125 * (res[Res::GEMS] + res[Res::CRYSTAL] + res[Res::MERCURY] + res[Res::SULFUR]);
}
TResources BuildAnalyzer::getResourcesRequiredNow() const
{
auto resourcesAvailable = cb->getResourceAmount();
auto result = requiredResources - resourcesAvailable;
result.positive();
return result;
}
TResources BuildAnalyzer::getTotalResourcesRequired() const
{
auto resourcesAvailable = cb->getResourceAmount();
auto result = totalDevelopmentCost - resourcesAvailable;
result.positive();
return result;
}
void BuildAnalyzer::update()
{
logAi->trace("Start analysing build");
BuildingInfo bi;
reset();
auto towns = cb->getTownsInfo();
for(const CGTownInstance* town : towns)
{
logAi->trace("Checking town %s", town->name);
auto townInfo = town->town;
developmentInfos.push_back(TownDevelopmentInfo(town));
TownDevelopmentInfo & developmentInfo = developmentInfos.back();
updateTownDwellings(developmentInfo);
updateOtherBuildings(developmentInfo);
requiredResources += developmentInfo.requiredResources;
totalDevelopmentCost += developmentInfo.townDevelopmentCost;
armyCost += developmentInfo.armyCost;
for(auto bi : developmentInfo.toBuild)
{
logAi->trace("Building preferences %s", bi.toString());
}
}
std::sort(developmentInfos.begin(), developmentInfos.end(), [](const TownDevelopmentInfo & t1, const TownDevelopmentInfo & t2) -> bool
{
auto val1 = convertToGold(t1.armyCost) - convertToGold(t1.townDevelopmentCost);
auto val2 = convertToGold(t2.armyCost) - convertToGold(t2.townDevelopmentCost);
return val1 > val2;
});
}
void BuildAnalyzer::reset()
{
requiredResources = TResources();
totalDevelopmentCost = TResources();
armyCost = TResources();
developmentInfos.clear();
}
BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
const CGTownInstance* town,
BuildingID toBuild,
bool excludeDwellingDependencies) const
{
BuildingID building = toBuild;
auto townInfo = town->town;
const CBuilding * buildPtr = townInfo->buildings.at(building);
const CCreature * creature = nullptr;
CreatureID baseCreatureID;
if(BuildingID::DWELL_FIRST <= toBuild && toBuild <= BuildingID::DWELL_UP_LAST)
{
int level = toBuild - BuildingID::DWELL_FIRST;
auto creatures = townInfo->creatures.at(level % GameConstants::CREATURES_PER_TOWN);
auto creatureID = creatures.at(level / GameConstants::CREATURES_PER_TOWN);
baseCreatureID = creatures.front();
creature = creatureID.toCreature();
}
auto info = BuildingInfo(buildPtr, creature, baseCreatureID);
logAi->trace("checking %s", buildPtr->Name());
logAi->trace("buildInfo %s", info.toString());
buildPtr = nullptr;
if(!town->hasBuilt(building))
{
auto canBuild = cb->canBuildStructure(town, building);
if(canBuild == EBuildingState::ALLOWED)
{
info.canBuild = true;
}
else if(canBuild == EBuildingState::NO_RESOURCES)
{
logAi->trace("cant build. Not enough resources. Need %s", info.buildCost.toString());
info.notEnoughRes = true;
}
else if(canBuild == EBuildingState::PREREQUIRES)
{
auto buildExpression = town->genBuildingRequirements(building, false);
auto missingBuildings = buildExpression.getFulfillmentCandidates([&](const BuildingID & id) -> bool
{
return town->hasBuilt(id);
});
auto otherDwelling = [](const BuildingID & id) -> bool
{
return BuildingID::DWELL_FIRST <= id && id <= BuildingID::DWELL_UP_LAST;
};
if(vstd::contains_if(missingBuildings, otherDwelling))
{
logAi->trace("cant build. Need other dwelling");
}
else
{
buildPtr = townInfo->buildings.at(building);
logAi->trace("cant build. Need %d", missingBuildings[0].num);
BuildingInfo prerequisite = getBuildingOrPrerequisite(town, missingBuildings[0], excludeDwellingDependencies);
prerequisite.buildCostWithPrerequisits += info.buildCost;
prerequisite.creatureCost = info.creatureCost;
prerequisite.creatureGrows = info.creatureGrows;
prerequisite.creatureLevel = info.creatureLevel;
prerequisite.creatureID = info.creatureID;
prerequisite.baseCreatureID = info.baseCreatureID;
prerequisite.prerequisitesCount++;
return prerequisite;
}
}
}
else
{
logAi->trace("exists");
info.exists = true;
}
return info;
}
TResources BuildAnalyzer::getDailyIncome() const
{
auto objects = cb->getMyObjects();
auto towns = cb->getTownsInfo();
TResources dailyIncome = TResources();
for(const CGObjectInstance* obj : objects)
{
const CGMine* mine = dynamic_cast<const CGMine*>(obj);
if(mine)
{
dailyIncome[mine->producedResource] += mine->producedQuantity;
}
}
for(const CGTownInstance* town : towns)
{
dailyIncome += town->dailyIncome();
}
return dailyIncome;
}
void TownDevelopmentInfo::addExistingDwelling(const BuildingInfo & existingDwelling)
{
existingDwellings.push_back(existingDwelling);
armyCost += existingDwelling.creatureCost * existingDwelling.creatureGrows;
}
void TownDevelopmentInfo::addBuildingToBuild(const BuildingInfo & nextToBuild)
{
townDevelopmentCost += nextToBuild.buildCostWithPrerequisits;
if(nextToBuild.canBuild)
{
toBuild.push_back(nextToBuild);
hasSomethingToBuild = true;
}
else if(nextToBuild.notEnoughRes)
{
requiredResources += nextToBuild.buildCost;
hasSomethingToBuild = true;
}
}
BuildingInfo::BuildingInfo()
{
id = BuildingID::NONE;
creatureGrows = 0;
creatureID = CreatureID::NONE;
buildCost = 0;
buildCostWithPrerequisits = 0;
prerequisitesCount = 0;
name = "";
}
BuildingInfo::BuildingInfo(const CBuilding * building, const CCreature * creature, CreatureID baseCreature)
{
id = building->bid;
buildCost = building->resources;
buildCostWithPrerequisits = building->resources;
dailyIncome = building->produce;
exists = false;;
prerequisitesCount = 1;
name = building->Name();
if(creature)
{
creatureGrows = creature->growth;
creatureID = creature->idNumber;
creatureCost = creature->cost;
creatureLevel = creature->level;
baseCreatureID = baseCreature;
}
else
{
creatureGrows = 0;
creatureID = CreatureID::NONE;
baseCreatureID = CreatureID::NONE;
creatureCost = TResources();
creatureLevel = 0;
}
}
std::string BuildingInfo::toString() const
{
return name + ", cost: " + buildCost.toString()
+ ", creature: " + std::to_string(creatureGrows) + " x " + std::to_string(creatureLevel)
+ " x " + creatureCost.toString()
+ ", daily: " + dailyIncome.toString();
}

View File

@ -12,11 +12,82 @@
#include "../VCAI.h"
#include "../../../lib/ResourceSet.h"
class BuildingInfo
{
public:
BuildingID id;
TResources buildCost;
TResources buildCostWithPrerequisits;
int creatureGrows;
uint8_t creatureLevel;
TResources creatureCost;
CreatureID creatureID;
CreatureID baseCreatureID;
TResources dailyIncome;
uint8_t prerequisitesCount;
std::string name;
bool exists = false;
bool canBuild = false;
bool notEnoughRes = false;
BuildingInfo();
BuildingInfo(const CBuilding* building, const CCreature* creature, CreatureID baseCreatureID);
std::string toString() const;
};
class TownDevelopmentInfo
{
public:
const CGTownInstance* town;
std::vector<BuildingInfo> toBuild;
std::vector<BuildingInfo> existingDwellings;
TResources townDevelopmentCost;
TResources requiredResources;
TResources armyCost;
int armyScore;
int economicsScore;
HeroRole townRole;
bool hasSomethingToBuild;
TownDevelopmentInfo(const CGTownInstance* town)
:town(town), armyScore(0), economicsScore(0), toBuild(), townDevelopmentCost(), requiredResources(), townRole(HeroRole::SCOUT), hasSomethingToBuild(false)
{
}
TownDevelopmentInfo() : TownDevelopmentInfo(nullptr) {}
void addBuildingToBuild(const BuildingInfo & building);
void addExistingDwelling(const BuildingInfo & existingDwelling);
};
class BuildAnalyzer
{
private:
TResources requiredResources;
TResources totalDevelopmentCost;
std::vector<TownDevelopmentInfo> developmentInfos;
TResources armyCost;
public:
void update();
TResources getResourcesRequiredNow() const;
TResources getTotalResourcesRequired() const;
const std::vector<TownDevelopmentInfo> & getDevelopmentInfo() const { return developmentInfos; }
TResources getDailyIncome() const;
private:
BuildingInfo getBuildingOrPrerequisite(
const CGTownInstance* town,
BuildingID toBuild,
bool excludeDwellingDependencies = true) const;
void updateTownDwellings(TownDevelopmentInfo & developmentInfo);
void updateOtherBuildings(TownDevelopmentInfo & developmentInfo);
void reset();
};

View File

@ -49,6 +49,9 @@ void DangerHitMapAnalyzer::updateHitMap()
{
for(AIPath & path : ai->ah->getPathsToTile(pos))
{
if(path.getFirstBlockedAction())
continue;
auto tileDanger = path.getHeroStrength();
auto turn = path.turn();
auto & node = hitMap[pos.x][pos.y][pos.z];

View File

@ -168,3 +168,43 @@ ui64 ArmyManager::howManyReinforcementsCanGet(const CCreatureSet * target, const
return newArmy > oldArmy ? newArmy - oldArmy : 0;
}
uint64_t ArmyManager::evaluateStackPower(const CCreature * creature, int count) const
{
return creature->AIValue * count;
}
SlotInfo ArmyManager::getTotalCreaturesAvailable(CreatureID creatureID) const
{
auto creatureInfo = totalArmy.find(creatureID);
return creatureInfo == totalArmy.end() ? SlotInfo() : creatureInfo->second;
}
void ArmyManager::update()
{
logAi->trace("Start analysing army");
std::vector<const CCreatureSet *> total;
auto heroes = cb->getHeroesInfo();
auto towns = cb->getTownsInfo();
std::copy(heroes.begin(), heroes.end(), std::back_inserter(total));
std::copy(towns.begin(), towns.end(), std::back_inserter(total));
totalArmy.clear();
for(auto army : total)
{
for(auto slot : army->Slots())
{
totalArmy[slot.second->getCreatureID()].count += slot.second->count;
}
}
for(auto army : totalArmy)
{
army.second.creature = army.first.toCreature();
army.second.power = evaluateStackPower(army.second.creature, army.second.count);
}
}

View File

@ -30,6 +30,7 @@ class DLL_EXPORT IArmyManager //: public: IAbstractManager
public:
virtual void init(CPlayerSpecificInfoCallback * CB) = 0;
virtual void setAI(VCAI * AI) = 0;
virtual void update() = 0;
virtual bool canGetArmy(const CArmedInstance * target, const CArmedInstance * source) const = 0;
virtual ui64 howManyReinforcementsCanBuy(const CCreatureSet * target, const CGDwelling * source) const = 0;
virtual ui64 howManyReinforcementsCanGet(const CCreatureSet * target, const CCreatureSet * source) const = 0;
@ -37,6 +38,8 @@ public:
virtual std::vector<SlotInfo>::iterator getWeakestCreature(std::vector<SlotInfo> & army) const = 0;
virtual std::vector<SlotInfo> getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const = 0;
virtual std::vector<creInfo> getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const = 0;
virtual uint64_t evaluateStackPower(const CCreature * creature, int count) const = 0;
virtual SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const = 0;
};
class DLL_EXPORT ArmyManager : public IArmyManager
@ -44,10 +47,12 @@ class DLL_EXPORT ArmyManager : public IArmyManager
private:
CPlayerSpecificInfoCallback * cb; //this is enough, but we downcast from CCallback
VCAI * ai;
std::map<CreatureID, SlotInfo> totalArmy;
public:
void init(CPlayerSpecificInfoCallback * CB) override;
void setAI(VCAI * AI) override;
void update() override;
bool canGetArmy(const CArmedInstance * target, const CArmedInstance * source) const override;
ui64 howManyReinforcementsCanBuy(const CCreatureSet * target, const CGDwelling * source) const override;
@ -56,4 +61,6 @@ public:
std::vector<SlotInfo>::iterator getWeakestCreature(std::vector<SlotInfo> & army) const override;
std::vector<SlotInfo> getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const override;
std::vector<creInfo> getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const override;
uint64_t evaluateStackPower(const CCreature * creature, int count) const override;
SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const override;
};

View File

@ -33,6 +33,32 @@ Goals::TGoalVec BuildingBehavior::getTasks()
{
Goals::TGoalVec tasks;
TResources resourcesRequired = ai->nullkiller->buildAnalyzer->getResourcesRequiredNow();
TResources totalDevelopmentCost = ai->nullkiller->buildAnalyzer->getTotalResourcesRequired();
TResources availableResources = cb->getResourceAmount();
TResources dailyIncome = ai->nullkiller->buildAnalyzer->getDailyIncome();
logAi->trace("Resources amount: %s", availableResources.toString());
resourcesRequired -= availableResources;
resourcesRequired.positive();
logAi->trace("daily income: %s", dailyIncome.toString());
logAi->trace("resources required to develop towns now: %s, total: %s",
resourcesRequired.toString(),
totalDevelopmentCost.toString());
auto & developmentInfos = ai->nullkiller->buildAnalyzer->getDevelopmentInfo();
for(auto & developmentInfo : developmentInfos)
{
auto town = developmentInfo.town;
for(auto & buildingInfo : developmentInfo.toBuild)
{
tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo)));
}
}
return tasks;
}

View File

@ -28,17 +28,6 @@ std::string CaptureObjectsBehavior::toString() const
return "Capture objects";
}
std::shared_ptr<const ISpecialAction> getFirstBlockedAction(const AIPath & path)
{
for(auto node : path.nodes)
{
if(node.specialAction && !node.specialAction->canAct(node.targetHero))
return node.specialAction;
}
return std::shared_ptr<const ISpecialAction>();
}
Goals::TGoalVec CaptureObjectsBehavior::getTasks()
{
Goals::TGoalVec tasks;
@ -76,7 +65,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getTasks()
logAi->trace("Path found %s", path.toString());
#endif
if(getFirstBlockedAction(path))
if(path.getFirstBlockedAction())
{
#ifdef VCMI_TRACE_PATHFINDER
// TODO: decomposition?
@ -88,7 +77,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getTasks()
if(ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
{
#ifdef VCMI_TRACE_PATHFINDER
logAi->trace("Ignore path. Target hero can be killed by enemy");
logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %d", path.heroArmy->getArmyStrength());
#endif
continue;
}
@ -112,7 +101,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getTasks()
hero->name,
path.getHeroStrength(),
danger,
path.armyLoss);
path.getTotalArmyLoss());
#endif
if(isSafe)

View File

@ -40,7 +40,7 @@ Goals::TGoalVec RecruitHeroBehavior::getTasks()
if(cb->getHeroesInfo().size() < cb->getTownsInfo().size() + 1
|| cb->getResourceAmount(Res::GOLD) > 10000)
{
tasks.push_back(Goals::sptr(Goals::RecruitHero().settown(town)));
tasks.push_back(Goals::sptr(Goals::RecruitHero().settown(town).setpriority(3)));
}
}
}

View File

@ -16,6 +16,7 @@
#include "../Behaviors/BuyArmyBehavior.h"
#include "../Behaviors/StartupBehavior.h"
#include "../Behaviors/DefenceBehavior.h"
#include "../Behaviors/BuildingBehavior.h"
#include "../Goals/Invalid.h"
extern boost::thread_specific_ptr<CCallback> cb;
@ -25,6 +26,7 @@ Nullkiller::Nullkiller()
{
priorityEvaluator.reset(new PriorityEvaluator());
dangerHitMap.reset(new DangerHitMapAnalyzer());
buildAnalyzer.reset(new BuildAnalyzer());
}
Goals::TSubgoal Nullkiller::choseBestTask(Goals::TGoalVec & tasks) const
@ -78,14 +80,17 @@ void Nullkiller::updateAiState()
// TODO: move to hero manager
auto activeHeroes = ai->getMyHeroes();
vstd::erase_if(activeHeroes, [this](const HeroPtr & hero) -> bool{
vstd::erase_if(activeHeroes, [this](const HeroPtr & hero) -> bool
{
auto lockedReason = getHeroLockedReason(hero.h);
return lockedReason == HeroLockedReason::DEFENCE || lockedReason == HeroLockedReason::STARTUP;
});
ai->ah->updatePaths(activeHeroes, true);
ai->ah->updateHeroRoles();
ai->ah->update();
buildAnalyzer->update();
}
bool Nullkiller::arePathHeroesLocked(const AIPath & path) const
@ -111,7 +116,8 @@ void Nullkiller::makeTurn()
choseBestTask(std::make_shared<BuyArmyBehavior>()),
choseBestTask(std::make_shared<CaptureObjectsBehavior>()),
choseBestTask(std::make_shared<RecruitHeroBehavior>()),
choseBestTask(std::make_shared<DefenceBehavior>())
choseBestTask(std::make_shared<DefenceBehavior>()),
choseBestTask(std::make_shared<BuildingBehavior>())
};
if(cb->getDate(Date::DAY) == 1)
@ -140,12 +146,12 @@ void Nullkiller::makeTurn()
{
logAi->trace(bestTask->completeMessage());
}
catch(std::exception & e)
/*catch(std::exception & e)
{
logAi->debug("Failed to realize subgoal of type %s, I will stop.", bestTask->name());
logAi->debug("The error message was: %s", e.what());
return;
}
}*/
}
}

View File

@ -35,6 +35,7 @@ private:
public:
std::unique_ptr<DangerHitMapAnalyzer> dangerHitMap;
std::unique_ptr<BuildAnalyzer> buildAnalyzer;
Nullkiller();
void makeTurn();

View File

@ -35,11 +35,6 @@ class CGTownInstance;
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<VCAI> ai;
PriorityEvaluator::PriorityEvaluator()
{
initVisitTile();
}
PriorityEvaluator::~PriorityEvaluator()
{
delete engine;
@ -112,7 +107,8 @@ uint64_t getDwellingScore(const CGObjectInstance * target, bool checkGold)
if(creLevel.first && creLevel.second.size())
{
auto creature = creLevel.second.back().toCreature();
if(checkGold && !cb->getResourceAmount().canAfford(creature->cost * creLevel.first))
auto creaturesAreFree = creature->level == 1;
if(!creaturesAreFree && checkGold && !cb->getResourceAmount().canAfford(creature->cost * creLevel.first))
continue;
score += creature->AIValue * creLevel.first;
@ -179,7 +175,7 @@ uint64_t getArmyReward(const CGObjectInstance * target, const CGHeroInstance * h
case Obj::SHIPWRECK:
case Obj::SHIPWRECK_SURVIVOR:
case Obj::WARRIORS_TOMB:
return 1500;
return 1000;
case Obj::ARTIFACT:
return evaluateArtifactArmyValue(dynamic_cast<const CGArtifact *>(target)->storedArtifact);
case Obj::DRAGON_UTOPIA:
@ -208,6 +204,34 @@ float getEnemyHeroStrategicalValue(const CGHeroInstance * enemy)
return objectValue / 2.0f + enemy->level / 15.0f;
}
float getResourceRequirementStrength(int resType)
{
TResources requiredResources = ai->nullkiller->buildAnalyzer->getResourcesRequiredNow();
TResources dailyIncome = ai->nullkiller->buildAnalyzer->getDailyIncome();
if(requiredResources[resType] == 0)
return 0;
if(dailyIncome[resType] == 0)
return 1;
return (float)requiredResources[resType] / dailyIncome[resType] / 3;
}
float getTotalResourceRequirementStrength(int resType)
{
TResources requiredResources = ai->nullkiller->buildAnalyzer->getTotalResourcesRequired();
TResources dailyIncome = ai->nullkiller->buildAnalyzer->getDailyIncome();
if(requiredResources[resType] == 0)
return 0;
if(dailyIncome[resType] == 0)
return requiredResources[resType] / 30;
return (float)requiredResources[resType] / dailyIncome[resType] / 30;
}
float getStrategicalValue(const CGObjectInstance * target)
{
if(!target)
@ -215,6 +239,12 @@ float getStrategicalValue(const CGObjectInstance * target)
switch(target->ID)
{
case Obj::MINE:
return target->subID == Res::GOLD ? 0.8f : 0.05f + 0.3f * getTotalResourceRequirementStrength(target->subID) + 0.5f * getResourceRequirementStrength(target->subID);
case Obj::RESOURCE:
return target->subID == Res::GOLD ? 0 : 0.5f * getResourceRequirementStrength(target->subID);
case Obj::TOWN:
return target->tempOwner == PlayerColor::NEUTRAL ? 0.5 : 1;
@ -261,7 +291,10 @@ float getSkillReward(const CGObjectInstance * target, const CGHeroInstance * her
case Obj::MERCENARY_CAMP:
case Obj::SHRINE_OF_MAGIC_GESTURE:
case Obj::SHRINE_OF_MAGIC_INCANTATION:
case Obj::TREE_OF_KNOWLEDGE:
return 1;
case Obj::LEARNING_STONE:
return 1.0f / std::sqrtf(hero->level);
case Obj::ARENA:
case Obj::SHRINE_OF_MAGIC_THOUGHT:
return 2;
@ -339,6 +372,86 @@ int32_t getGoldReward(const CGObjectInstance * target, const CGHeroInstance * he
}
}
class ExecuteHeroChainEvaluationContextBuilder : public IEvaluationContextBuilder
{
public:
virtual Goals::EvaluationContext buildEvaluationContext(Goals::TSubgoal task) const override
{
auto evaluationContext = task->evaluationContext;
int objId = task->objid;
if(task->parent)
objId = task->parent->objid;
auto heroPtr = task->hero;
const CGObjectInstance * target = cb->getObj((ObjectInstanceID)objId, false);
auto day = cb->getDate(Date::DAY);
auto hero = heroPtr.get();
bool checkGold = evaluationContext.danger == 0;
evaluationContext.armyLossPersentage = task->evaluationContext.armyLoss / (double)task->evaluationContext.heroStrength;
evaluationContext.heroRole = ai->ah->getHeroRole(heroPtr);
evaluationContext.goldReward = getGoldReward(target, hero);
evaluationContext.armyReward = getArmyReward(target, hero, checkGold);
evaluationContext.skillReward = getSkillReward(target, hero, evaluationContext.heroRole);
evaluationContext.strategicalValue = getStrategicalValue(target);
return evaluationContext;
}
};
class BuildThisEvaluationContextBuilder : public IEvaluationContextBuilder
{
public:
virtual Goals::EvaluationContext buildEvaluationContext(Goals::TSubgoal task) const override
{
Goals::EvaluationContext evaluationContext;
Goals::BuildThis & buildThis = dynamic_cast<Goals::BuildThis &>(*task);
auto & bi = buildThis.buildingInfo;
evaluationContext.goldReward = bi.dailyIncome[Res::GOLD] / 2;
evaluationContext.heroRole = HeroRole::MAIN;
evaluationContext.movementCostByRole[evaluationContext.heroRole] = bi.prerequisitesCount;
evaluationContext.armyReward = 0;
evaluationContext.strategicalValue = buildThis.townInfo.armyScore / 50000.0;
if(bi.creatureID != CreatureID::NONE)
{
evaluationContext.strategicalValue += 0.5f + 0.1f * bi.creatureLevel;
if(bi.baseCreatureID == bi.creatureID)
{
evaluationContext.armyReward = ai->ah->evaluateStackPower(bi.creatureID.toCreature(), bi.creatureGrows);
}
auto creaturesToUpgrade = ai->ah->getTotalCreaturesAvailable(bi.baseCreatureID);
auto upgradedPower = ai->ah->evaluateStackPower(bi.creatureID.toCreature(), creaturesToUpgrade.count);
evaluationContext.armyReward = upgradedPower - creaturesToUpgrade.power;
}
return evaluationContext;
}
};
PriorityEvaluator::PriorityEvaluator()
{
initVisitTile();
evaluationContextBuilders[Goals::EXECUTE_HERO_CHAIN] = std::make_shared<ExecuteHeroChainEvaluationContextBuilder>();
evaluationContextBuilders[Goals::BUILD_STRUCTURE] = std::make_shared<BuildThisEvaluationContextBuilder>();
}
Goals::EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal) const
{
auto builder = evaluationContextBuilders.find(goal->goalType);
if(builder == evaluationContextBuilders.end())
return goal->evaluationContext;
return builder->second->buildEvaluationContext(goal);
}
/// distance
/// nearest hero?
/// gold income
@ -351,45 +464,28 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
if(task->priority > 0)
return task->priority;
auto heroPtr = task->hero;
auto evaluationContext = buildEvaluationContext(task);
if(!heroPtr.validAndSet())
return 2;
int objId = task->objid;
if(task->parent)
objId = task->parent->objid;
const CGObjectInstance * target = cb->getObj((ObjectInstanceID)objId, false);
int rewardType = (evaluationContext.goldReward > 0 ? 1 : 0)
+ (evaluationContext.armyReward > 0 ? 1 : 0)
+ (evaluationContext.skillReward > 0 ? 1 : 0)
+ (evaluationContext.strategicalValue > 0 ? 1 : 0);
auto day = cb->getDate(Date::DAY);
auto hero = heroPtr.get();
auto armyTotal = task->evaluationContext.heroStrength;
double armyLossPersentage = task->evaluationContext.armyLoss / (double)armyTotal;
uint64_t danger = task->evaluationContext.danger;
HeroRole heroRole = ai->ah->getHeroRole(heroPtr);
int32_t goldReward = getGoldReward(target, hero);
bool checkGold = danger == 0;
uint64_t armyReward = getArmyReward(target, hero, checkGold);
float skillReward = getSkillReward(target, hero, heroRole);
float strategicalValue = getStrategicalValue(target);
double result = 0;
int rewardType = (goldReward > 0 ? 1 : 0) + (armyReward > 0 ? 1 : 0) + (skillReward > 0 ? 1 : 0) + (strategicalValue > 0 ? 1 : 0);
try
{
armyLossPersentageVariable->setValue(armyLossPersentage);
heroRoleVariable->setValue(heroRole);
mainTurnDistanceVariable->setValue(task->evaluationContext.movementCostByRole[HeroRole::MAIN]);
scoutTurnDistanceVariable->setValue(task->evaluationContext.movementCostByRole[HeroRole::SCOUT]);
goldRewardVariable->setValue(goldReward);
armyRewardVariable->setValue(armyReward);
skillRewardVariable->setValue(skillReward);
dangerVariable->setValue(danger);
armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage);
heroRoleVariable->setValue(evaluationContext.heroRole);
mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]);
scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]);
goldRewardVariable->setValue(evaluationContext.goldReward);
armyRewardVariable->setValue(evaluationContext.armyReward);
skillRewardVariable->setValue(evaluationContext.skillReward);
dangerVariable->setValue(evaluationContext.danger);
rewardTypeVariable->setValue(rewardType);
closestHeroRatioVariable->setValue(task->evaluationContext.closestWayRatio);
strategicalValueVariable->setValue(strategicalValue);
closestHeroRatioVariable->setValue(evaluationContext.closestWayRatio);
strategicalValueVariable->setValue(evaluationContext.strategicalValue);
engine->process();
//engine.process(VISIT_TILE); //TODO: Process only Visit_Tile
@ -402,16 +498,16 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
assert(result >= 0);
#ifdef VCMI_TRACE_PATHFINDER
logAi->trace("Evaluated %s, hero %s, loss: %f, turns: %f, gold: %d, army gain: %d, danger: %d, role: %s, strategical value: %f, result %f",
logAi->trace("Evaluated %s, loss: %f, turns main: %f, scout: %f, gold: %d, army gain: %d, danger: %d, role: %s, strategical value: %f, result %f",
task->name(),
hero->name,
armyLossPersentage,
task->evaluationContext.movementCost,
goldReward,
armyReward,
danger,
heroRole ? "scout" : "main",
strategicalValue,
evaluationContext.armyLossPersentage,
evaluationContext.movementCostByRole[HeroRole::MAIN],
evaluationContext.movementCostByRole[HeroRole::SCOUT],
evaluationContext.goldReward,
evaluationContext.armyReward,
evaluationContext.danger,
evaluationContext.heroRole ? "scout" : "main",
evaluationContext.strategicalValue,
result);
#endif

View File

@ -10,12 +10,12 @@
#pragma once
#include "fl/Headers.h"
#include "../Goals/Goals.h"
#include "../FuzzyEngines.h"
class VCAI;
class CArmedInstance;
class CBank;
struct SectorMap;
class IEvaluationContextBuilder
{
public:
virtual Goals::EvaluationContext buildEvaluationContext(Goals::TSubgoal goal) const = 0;
};
class PriorityEvaluator
{
@ -40,4 +40,7 @@ private:
fl::InputVariable * rewardTypeVariable;
fl::InputVariable * closestHeroRatioVariable;
fl::OutputVariable * value;
std::map<Goals::EGoals, std::shared_ptr<IEvaluationContextBuilder>> evaluationContextBuilders;
Goals::EvaluationContext buildEvaluationContext(Goals::TSubgoal goal) const;
};

View File

@ -96,12 +96,17 @@ namespace Goals
{
float movementCost;
std::map<HeroRole, float> movementCostByRole;
float scoutMovementCost;
int manaCost;
uint64_t danger;
float closestWayRatio;
uint64_t armyLoss;
uint64_t heroStrength;
float armyLossPersentage;
float armyReward;
int32_t goldReward;
float skillReward;
float strategicalValue;
HeroRole heroRole;
EvaluationContext()
: movementCost(0.0),
@ -110,7 +115,12 @@ namespace Goals
closestWayRatio(1),
armyLoss(0),
heroStrength(0),
movementCostByRole()
movementCostByRole(),
skillReward(0),
goldReward(0),
armyReward(0),
armyLossPersentage(0),
heroRole(HeroRole::SCOUT)
{
}
};

View File

@ -31,6 +31,11 @@ bool BuildThis::operator==(const BuildThis & other) const
return town == other.town && bid == other.bid;
}
std::string BuildThis::name() const
{
return "Build " + buildingInfo.name + "(" + std::to_string(bid) + ") in " + town->name;
}
TSubgoal BuildThis::whatToDoToAchieve()
{
auto b = BuildingID(bid);

View File

@ -10,6 +10,7 @@
#pragma once
#include "CGoal.h"
#include "../Analyzers/BuildAnalyzer.h"
struct HeroPtr;
class VCAI;
@ -20,10 +21,19 @@ namespace Goals
class DLL_EXPORT BuildThis : public CGoal<BuildThis>
{
public:
BuildingInfo buildingInfo;
TownDevelopmentInfo townInfo;
BuildThis() //should be private, but unit test uses it
: CGoal(Goals::BUILD_STRUCTURE)
{
}
BuildThis(const BuildingInfo & buildingInfo, const TownDevelopmentInfo & townInfo) //should be private, but unit test uses it
: CGoal(Goals::BUILD_STRUCTURE), buildingInfo(buildingInfo), townInfo(townInfo)
{
bid = buildingInfo.id;
town = townInfo.town;
}
BuildThis(BuildingID Bid, const CGTownInstance * tid)
: CGoal(Goals::BUILD_STRUCTURE)
{
@ -44,5 +54,6 @@ namespace Goals
TSubgoal whatToDoToAchieve() override;
//bool fulfillsMe(TSubgoal goal) override;
virtual bool operator==(const BuildThis & other) const override;
virtual std::string name() const override;
};
}

View File

@ -175,7 +175,7 @@ void ExecuteHeroChain::accept(VCAI * ai)
std::string ExecuteHeroChain::name() const
{
return "ExecuteHeroChain " + targetName;
return "ExecuteHeroChain " + targetName + " by " + chainPath.targetHero->name;
}
std::string ExecuteHeroChain::completeMessage() const

View File

@ -101,8 +101,10 @@ float HeroManager::evaluateFightingStrength(const CGHeroInstance * hero) const
return evaluateSpeciality(hero) + wariorSkillsScores.evaluateSecSkills(hero) + hero->level * 1.5f;
}
void HeroManager::updateHeroRoles()
void HeroManager::update()
{
logAi->trace("Start analysing our heroes");
std::map<HeroPtr, float> scores;
auto myHeroes = ai->getMyHeroes();

View File

@ -26,7 +26,7 @@ public:
virtual const std::map<HeroPtr, HeroRole> & getHeroRoles() const = 0;
virtual int selectBestSkill(const HeroPtr & hero, const std::vector<SecondarySkill> & skills) const = 0;
virtual HeroRole getHeroRole(const HeroPtr & hero) const = 0;
virtual void updateHeroRoles() = 0;
virtual void update() = 0;
virtual float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const = 0;
virtual float evaluateHero(const CGHeroInstance * hero) const = 0;
};
@ -64,7 +64,7 @@ public:
const std::map<HeroPtr, HeroRole> & getHeroRoles() const override;
HeroRole getHeroRole(const HeroPtr & hero) const override;
int selectBestSkill(const HeroPtr & hero, const std::vector<SecondarySkill> & skills) const override;
void updateHeroRoles() override;
void update() override;
float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const override;
float evaluateHero(const CGHeroInstance * hero) const override;

View File

@ -76,7 +76,7 @@ void AINodeStorage::initialize(const PathfinderOptions & options, const CGameSta
void AINodeStorage::clear()
{
actors.clear();
heroChainPass = false;
heroChainPass = EHeroChainPass::INITIAL;
heroChainTurn = 0;
heroChainMaxTurns = 1;
}
@ -262,9 +262,37 @@ bool AINodeStorage::increaseHeroChainTurnLimit()
return true;
}
EPathfindingLayer phisycalLayers[2] = {EPathfindingLayer::LAND, EPathfindingLayer::SAIL};
bool AINodeStorage::calculateHeroChainFinal()
{
heroChainPass = EHeroChainPass::FINAL;
heroChain.resize(0);
for(auto layer : phisycalLayers)
{
foreach_tile_pos([&](const int3 & pos)
{
auto chains = nodes[pos.x][pos.y][pos.z][layer];
for(AIPathNode & node : chains)
{
if(node.turns > heroChainTurn
&& node.action != CGPathNode::ENodeAction::UNKNOWN
&& node.actor->actorExchangeCount > 1)
{
heroChain.push_back(&node);
}
}
});
}
return heroChain.size();
}
bool AINodeStorage::calculateHeroChain()
{
heroChainPass = true;
heroChainPass = EHeroChainPass::CHAIN;
heroChain.resize(0);
std::vector<AIPathNode *> existingChains;
@ -273,37 +301,40 @@ bool AINodeStorage::calculateHeroChain()
existingChains.reserve(NUM_CHAINS);
newChains.reserve(NUM_CHAINS);
foreach_tile_pos([&](const int3 & pos) {
auto layer = EPathfindingLayer::LAND;
auto chains = nodes[pos.x][pos.y][pos.z][layer];
existingChains.resize(0);
newChains.resize(0);
for(AIPathNode & node : chains)
for(auto layer : phisycalLayers)
{
foreach_tile_pos([&](const int3 & pos)
{
if(node.turns <= heroChainTurn && node.action != CGPathNode::ENodeAction::UNKNOWN)
existingChains.push_back(&node);
}
auto chains = nodes[pos.x][pos.y][pos.z][layer];
for(AIPathNode * node : existingChains)
{
if(node->actor->isMovable)
existingChains.resize(0);
newChains.resize(0);
for(AIPathNode & node : chains)
{
calculateHeroChain(node, existingChains, newChains);
if(node.turns <= heroChainTurn && node.action != CGPathNode::ENodeAction::UNKNOWN)
existingChains.push_back(&node);
}
}
cleanupInefectiveChains(newChains);
addHeroChain(newChains);
});
for(AIPathNode * node : existingChains)
{
if(node->actor->isMovable)
{
calculateHeroChain(node, existingChains, newChains);
}
}
cleanupInefectiveChains(newChains);
addHeroChain(newChains);
});
}
return heroChain.size();
}
void AINodeStorage::cleanupInefectiveChains(std::vector<ExchangeCandidate> & result) const
{
vstd::erase_if(result, [&](ExchangeCandidate & chainInfo) -> bool
vstd::erase_if(result, [&](const ExchangeCandidate & chainInfo) -> bool
{
auto pos = chainInfo.coord;
auto chains = nodes[pos.x][pos.y][pos.z][EPathfindingLayer::LAND];
@ -320,12 +351,27 @@ void AINodeStorage::calculateHeroChain(
{
for(AIPathNode * node : variants)
{
if(node == srcNode
|| !node->actor
|| node->turns > heroChainTurn
if(node == srcNode || !node->actor)
continue;
if(node->turns > heroChainTurn
|| (node->action == CGPathNode::ENodeAction::UNKNOWN && node->actor->hero)
|| (node->actor->chainMask & srcNode->actor->chainMask) != 0)
{
#if AI_TRACE_LEVEL >= 2
logAi->trace(
"Skip exchange %s[%x] -> %s[%x] at %s because of %s",
node->actor->toString(),
node->actor->chainMask,
srcNode->actor->toString(),
srcNode->actor->chainMask,
srcNode->coord.toString(),
(node->turns > heroChainTurn
? "turn limit"
: (node->action == CGPathNode::ENodeAction::UNKNOWN && node->actor->hero)
? "action unknown"
: "chain mask"));
#endif
continue;
}
@ -793,7 +839,7 @@ bool AINodeStorage::hasBetterChain(
}
}
if((candidateActor->chainMask & node.actor->chainMask) == 0)
if(candidateActor->chainMask != node.actor->chainMask)
continue;
auto nodeActor = node.actor;
@ -931,6 +977,17 @@ AIPath::AIPath()
{
}
std::shared_ptr<const ISpecialAction> AIPath::getFirstBlockedAction() const
{
for(auto node : nodes)
{
if(node.specialAction && !node.specialAction->canAct(node.targetHero))
return node.specialAction;
}
return std::shared_ptr<const ISpecialAction>();
}
int3 AIPath::firstTileToGet() const
{
if(nodes.size())
@ -1005,7 +1062,7 @@ uint64_t AIPath::getTotalArmyLoss() const
return armyLoss + targetObjectArmyLoss;
}
std::string AIPath::toString()
std::string AIPath::toString() const
{
std::stringstream str;

View File

@ -79,7 +79,9 @@ struct AIPath
uint64_t getHeroStrength() const;
std::string toString();
std::string toString() const;
std::shared_ptr<const ISpecialAction> AIPath::getFirstBlockedAction() const;
};
struct ExchangeCandidate : public AIPathNode
@ -88,6 +90,15 @@ struct ExchangeCandidate : public AIPathNode
AIPathNode * otherParent;
};
enum EHeroChainPass
{
INITIAL, // single heroes unlimited distance
CHAIN, // chains with limited distance
FINAL // same as SINGLE but for heroes from CHAIN pass
};
class AINodeStorage : public INodeStorage
{
private:
@ -100,7 +111,7 @@ private:
std::unique_ptr<FuzzyHelper> dangerEvaluator;
std::vector<std::shared_ptr<ChainActor>> actors;
std::vector<CGPathNode *> heroChain;
bool heroChainPass; // true if we need to calculate hero chain
EHeroChainPass heroChainPass; // true if we need to calculate hero chain
int heroChainTurn;
int heroChainMaxTurns;
PlayerColor playerID;
@ -146,7 +157,7 @@ public:
bool isMovementIneficient(const PathNodeInfo & source, CDestinationNodeInfo & destination) const
{
// further chain distribution is calculated as the last stage
if(heroChainPass && destination.node->turns > heroChainTurn)
if(heroChainPass == EHeroChainPass::CHAIN && destination.node->turns > heroChainTurn)
return true;
return hasBetterChain(source, destination);
@ -169,6 +180,7 @@ public:
const std::set<const CGHeroInstance *> getAllHeroes() const;
void clear();
bool calculateHeroChain();
bool calculateHeroChainFinal();
uint64_t evaluateDanger(const int3 & tile, const CGHeroInstance * hero) const
{

View File

@ -66,21 +66,21 @@ void AIPathfinder::updatePaths(std::vector<HeroPtr> heroes, bool useHeroChain)
do
{
logAi->trace("Recalculate paths pass %d", pass++);
cb->calculatePaths(config);
if(useHeroChain)
do
{
logAi->trace("Recalculate chain pass %d", pass);
logAi->trace("Recalculate paths pass %d", pass++);
cb->calculatePaths(config);
} while(useHeroChain && storage->calculateHeroChain());
continueCalculation = storage->calculateHeroChain();
if(!useHeroChain)
break;
if(!continueCalculation)
{
logAi->trace("Increase chain turn limit");
continueCalculation = storage->increaseHeroChainTurnLimit() && storage->calculateHeroChain();
}
if(storage->calculateHeroChainFinal())
{
logAi->trace("Recalculate paths pass final");
cb->calculatePaths(config);
}
continueCalculation = storage->increaseHeroChainTurnLimit() && storage->calculateHeroChain();
} while(continueCalculation);
}

View File

@ -853,10 +853,10 @@ void VCAI::makeTurn()
logAi->debug("Making turn thread has been interrupted. We'll end without calling endTurn.");
return;
}
catch (std::exception & e)
/*catch (std::exception & e)
{
logAi->debug("Making turn thread has caught an exception: %s", e.what());
}
}*/
endTurn();
}
@ -1079,7 +1079,12 @@ void VCAI::performObjectInteraction(const CGObjectInstance * obj, HeroPtr h)
moveCreaturesToHero(h->visitedTown);
townVisitsThisWeek[h].insert(h->visitedTown);
ah->updateHeroRoles();
if(!ai->nullkiller)
{
ah->update();
}
if(ah->getHeroRole(h) == HeroRole::MAIN && !h->hasSpellbook() && ah->freeGold() >= GameConstants::SPELLBOOK_GOLD_COST)
{
if(h->visitedTown->hasBuilt(BuildingID::MAGES_GUILD_1))
@ -1994,9 +1999,11 @@ bool VCAI::moveHeroToTile(int3 dst, HeroPtr h)
doChannelProbing();
}
if(path.nodes[0].action == CGPathNode::BLOCKING_VISIT)
if(path.nodes[0].action == CGPathNode::BLOCKING_VISIT || path.nodes[0].action == CGPathNode::BATTLE)
{
ret = h && i == 0; // when we take resource we do not reach its position. We even might not move
// when we take resource we do not reach its position. We even might not move
// also guarded town is not get visited automatically after capturing
ret = h && i == 0;
}
}
if(h)
@ -2485,6 +2492,8 @@ void VCAI::recruitHero(const CGTownInstance * t, bool throwing)
}
cb->recruitHero(t, hero);
ai->ah->update();
if(t->visitingHero)
moveHeroToTile(t->visitablePos(), t->visitingHero.get());