1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-06-17 00:07:41 +02:00

Fuzzy rework, added more defence and gather army routines

This commit is contained in:
Andrii Danylchenko
2023-06-04 16:02:02 +03:00
parent b1ca663eb6
commit b19ac01bf9
28 changed files with 710 additions and 195 deletions

View File

@ -29,7 +29,7 @@ namespace NKAI
{
// our to enemy strength ratio constants
const float SAFE_ATTACK_CONSTANT = 1.2f;
const float SAFE_ATTACK_CONSTANT = 1.1f;
const float RETREAT_THRESHOLD = 0.3f;
const double RETREAT_ABSOLUTE_THRESHOLD = 10000.;
@ -90,9 +90,11 @@ void AIGateway::heroMoved(const TryMoveHero & details, bool verbose)
LOG_TRACE(logAi);
NET_EVENT_HANDLER;
validateObject(details.id); //enemy hero may have left visible area
auto hero = cb->getHero(details.id);
if(!hero)
validateObject(details.id); //enemy hero may have left visible area
const int3 from = hero ? hero->convertToVisitablePos(details.start) : (details.start - int3(0,1,0));;
const int3 to = hero ? hero->convertToVisitablePos(details.end) : (details.end - int3(0,1,0));
@ -794,10 +796,7 @@ void AIGateway::makeTurn()
cb->sendMessage("vcmieagles");
if(cb->getDate(Date::DAY) == 1)
{
retrieveVisitableObjs();
}
retrieveVisitableObjs();
#if NKAI_TRACE_LEVEL == 0
try

View File

@ -238,7 +238,8 @@ std::shared_ptr<CCreatureSet> ArmyManager::getArmyAvailableToBuyAsCCreatureSet(
ui64 ArmyManager::howManyReinforcementsCanBuy(
const CCreatureSet * targetArmy,
const CGDwelling * dwelling,
const TResources & availableResources) const
const TResources & availableResources,
uint8_t turn) const
{
ui64 aivalue = 0;
auto army = getArmyAvailableToBuy(targetArmy, dwelling, availableResources);
@ -259,17 +260,29 @@ std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(const CCreatureSet * her
std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
const CCreatureSet * hero,
const CGDwelling * dwelling,
TResources availableRes) const
TResources availableRes,
uint8_t turn) const
{
std::vector<creInfo> creaturesInDwellings;
int freeHeroSlots = GameConstants::ARMY_SIZE - hero->stacksCount();
bool countGrowth = (cb->getDate(Date::DAY_OF_WEEK) + turn) > 7;
const CGTownInstance * town = dwelling->ID == CGTownInstance::TOWN
? dynamic_cast<const CGTownInstance *>(dwelling)
: nullptr;
for(int i = dwelling->creatures.size() - 1; i >= 0; i--)
{
auto ci = infoFromDC(dwelling->creatures[i]);
if(!ci.count || ci.creID == -1)
continue;
if(ci.creID == -1) continue;
if(i < GameConstants::CREATURES_PER_TOWN && countGrowth)
{
ci.count += town ? town->creatureGrowth(i) : ci.cre->getGrowth();
}
if(!ci.count) continue;
SlotID dst = hero->getSlotFor(ci.creID);
if(!hero->hasStackAtSlot(dst)) //need another new slot for this stack
@ -282,8 +295,7 @@ std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
vstd::amin(ci.count, availableRes / ci.cre->getFullRecruitCost()); //max count we can afford
if(!ci.count)
continue;
if(!ci.count) continue;
ci.level = i; //this is important for Dungeon Summoning Portal
creaturesInDwellings.push_back(ci);

View File

@ -45,20 +45,32 @@ public:
virtual ui64 howManyReinforcementsCanBuy(
const CCreatureSet * targetArmy,
const CGDwelling * dwelling,
const TResources & availableResources) const = 0;
const TResources & availableResources,
uint8_t turn = 0) const = 0;
virtual ui64 howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const = 0;
virtual ui64 howManyReinforcementsCanGet(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const = 0;
virtual ui64 howManyReinforcementsCanGet(
const IBonusBearer * armyCarrier,
const CCreatureSet * target,
const CCreatureSet * source) const = 0;
virtual std::vector<SlotInfo> getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const = 0;
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, TResources availableRes) const = 0;
virtual std::vector<creInfo> getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const = 0;
virtual std::vector<creInfo> getArmyAvailableToBuy(
const CCreatureSet * hero,
const CGDwelling * dwelling,
TResources availableRes,
uint8_t turn = 0) const = 0;
virtual uint64_t evaluateStackPower(const CCreature * creature, int count) const = 0;
virtual SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const = 0;
virtual ArmyUpgradeInfo calculateCreaturesUpgrade(
const CCreatureSet * army,
const CGObjectInstance * upgrader,
const TResources & availableResources) const = 0;
virtual std::vector<creInfo> getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const = 0;
virtual std::shared_ptr<CCreatureSet> getArmyAvailableToBuyAsCCreatureSet(const CGDwelling * dwelling, TResources availableRes) const = 0;
};
@ -74,18 +86,27 @@ private:
public:
ArmyManager(CPlayerSpecificInfoCallback * CB, const Nullkiller * ai): cb(CB), ai(ai) {}
void update() override;
ui64 howManyReinforcementsCanBuy(const CCreatureSet * target, const CGDwelling * source) const override;
ui64 howManyReinforcementsCanBuy(
const CCreatureSet * targetArmy,
const CGDwelling * dwelling,
const TResources & availableResources) const override;
const TResources & availableResources,
uint8_t turn = 0) const override;
ui64 howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const override;
ui64 howManyReinforcementsCanGet(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const override;
std::vector<SlotInfo> getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const override;
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, TResources availableRes) const override;
std::vector<creInfo> getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const override;
std::vector<creInfo> getArmyAvailableToBuy(
const CCreatureSet * hero,
const CGDwelling * dwelling,
TResources availableRes,
uint8_t turn = 0) const override;
std::shared_ptr<CCreatureSet> getArmyAvailableToBuyAsCCreatureSet(const CGDwelling * dwelling, TResources availableRes) const override;
uint64_t evaluateStackPower(const CCreature * creature, int count) const override;
SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const override;

View File

@ -19,12 +19,12 @@ HitMapInfo HitMapInfo::NoTreat;
void DangerHitMapAnalyzer::updateHitMap()
{
if(upToDate)
if(hitMapUpToDate)
return;
logAi->trace("Update danger hitmap");
upToDate = true;
hitMapUpToDate = true;
auto start = std::chrono::high_resolution_clock::now();
auto cb = ai->cb.get();
@ -71,8 +71,10 @@ void DangerHitMapAnalyzer::updateHitMap()
auto turn = path.turn();
auto & node = hitMap[pos.x][pos.y][pos.z];
if(tileDanger / (turn / 3 + 1) > node.maximumDanger.danger / (node.maximumDanger.turn / 3 + 1)
|| (tileDanger == node.maximumDanger.danger && node.maximumDanger.turn > turn))
auto newMaxDanger = tileDanger / std::sqrt(turn / 3.0f + 1);
auto currentMaxDanger = node.maximumDanger.danger / std::sqrt(node.maximumDanger.turn / 3.0f + 1);
if(newMaxDanger > currentMaxDanger)
{
node.maximumDanger.danger = tileDanger;
node.maximumDanger.turn = turn;
@ -104,6 +106,94 @@ void DangerHitMapAnalyzer::updateHitMap()
logAi->trace("Danger hit map updated in %ld", timeElapsed(start));
}
void DangerHitMapAnalyzer::calculateTileOwners()
{
if(tileOwnersUpToDate) return;
tileOwnersUpToDate = true;
auto cb = ai->cb.get();
auto mapSize = ai->cb->getMapSize();
tileOwners.resize(boost::extents[mapSize.x][mapSize.y][mapSize.z]);
std::map<const CGHeroInstance *, HeroRole> townHeroes;
std::map<const CGHeroInstance *, const CGTownInstance *> heroTownMap;
PathfinderSettings pathfinderSettings;
pathfinderSettings.mainTurnDistanceLimit = 3;
auto addTownHero = [&](const CGTownInstance * town)
{
auto townHero = new CGHeroInstance();
CRandomGenerator rng;
townHero->pos = town->pos;
townHero->setOwner(ai->playerID); // lets avoid having multiple colors
townHero->initHero(rng, static_cast<HeroTypeID>(0));
townHero->initObj(rng);
heroTownMap[townHero] = town;
townHeroes[townHero] = HeroRole::MAIN;
};
for(auto obj : ai->memory->visitableObjs)
{
if(obj && obj->ID == Obj::TOWN)
{
addTownHero(dynamic_cast<const CGTownInstance *>(obj));
}
}
for(auto town : cb->getTownsInfo())
{
addTownHero(town);
}
ai->pathfinder->updatePaths(townHeroes, PathfinderSettings());
pforeachTilePos(mapSize, [&](const int3 & pos)
{
float ourDistance = std::numeric_limits<float>::max();
float enemyDistance = std::numeric_limits<float>::max();
const CGTownInstance * enemyTown = nullptr;
for(AIPath & path : ai->pathfinder->getPathInfo(pos))
{
if(!path.targetHero || path.getFirstBlockedAction())
continue;
auto town = heroTownMap[path.targetHero];
if(town->getOwner() == ai->playerID)
{
vstd::amin(ourDistance, path.movementCost());
}
else
{
if(enemyDistance > path.movementCost())
{
enemyDistance = path.movementCost();
enemyTown = town;
}
}
}
if(ourDistance == enemyDistance)
{
tileOwners[pos.x][pos.y][pos.z] = PlayerColor::NEUTRAL;
}
else if(!enemyTown || ourDistance < enemyDistance)
{
tileOwners[pos.x][pos.y][pos.z] = ai->playerID;
}
else
{
tileOwners[pos.x][pos.y][pos.z] = enemyTown->getOwner();
}
});
}
uint64_t DangerHitMapAnalyzer::enemyCanKillOurHeroesAlongThePath(const AIPath & path) const
{
int3 tile = path.targetTile();
@ -144,7 +234,7 @@ const std::set<const CGObjectInstance *> & DangerHitMapAnalyzer::getOneTurnAcces
void DangerHitMapAnalyzer::reset()
{
upToDate = false;
hitMapUpToDate = false;
}
}

View File

@ -55,19 +55,23 @@ class DangerHitMapAnalyzer
{
private:
boost::multi_array<HitMapNode, 3> hitMap;
boost::multi_array<PlayerColor, 3> tileOwners;
std::map<const CGHeroInstance *, std::set<const CGObjectInstance *>> enemyHeroAccessibleObjects;
bool upToDate;
bool hitMapUpToDate = false;
bool tileOwnersUpToDate = false;
const Nullkiller * ai;
public:
DangerHitMapAnalyzer(const Nullkiller * ai) :ai(ai) {}
void updateHitMap();
void calculateTileOwners();
uint64_t enemyCanKillOurHeroesAlongThePath(const AIPath & path) const;
const HitMapNode & getObjectTreat(const CGObjectInstance * obj) const;
const HitMapNode & getTileTreat(const int3 & tile) const;
const std::set<const CGObjectInstance *> & getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const;
void reset();
void resetTileOwners() { tileOwnersUpToDate = false; }
};
}

View File

@ -180,6 +180,15 @@ float HeroManager::evaluateHero(const CGHeroInstance * hero) const
return evaluateFightingStrength(hero);
}
bool HeroManager::heroCapReached() const
{
const bool includeGarnisoned = true;
int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned);
return heroCount >= ALLOWED_ROAMING_HEROES
|| heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP);
}
bool HeroManager::canRecruitHero(const CGTownInstance * town) const
{
if(!town)
@ -191,13 +200,7 @@ bool HeroManager::canRecruitHero(const CGTownInstance * town) const
if(cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST)
return false;
const bool includeGarnisoned = true;
int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned);
if(heroCount >= ALLOWED_ROAMING_HEROES)
return false;
if(heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP))
if(heroCapReached())
return false;
if(!cb->getAvailableHeroes(town).size())

View File

@ -31,6 +31,7 @@ public:
virtual float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const = 0;
virtual float evaluateHero(const CGHeroInstance * hero) const = 0;
virtual bool canRecruitHero(const CGTownInstance * t = nullptr) const = 0;
virtual bool heroCapReached() const = 0;
virtual const CGHeroInstance * findHeroWithGrail() const = 0;
};
@ -71,6 +72,7 @@ public:
float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const override;
float evaluateHero(const CGHeroInstance * hero) const override;
bool canRecruitHero(const CGTownInstance * t = nullptr) const override;
bool heroCapReached() const override;
const CGHeroInstance * findHeroWithGrail() const override;
private:

View File

@ -56,7 +56,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
tasks.reserve(paths.size());
const AIPath * closestWay = nullptr;
std::unordered_map<HeroRole, const AIPath *> closestWaysByRole;
std::vector<ExecuteHeroChain *> waysToVisitObj;
for(auto & path : paths)
@ -128,8 +128,9 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
auto heroRole = ai->nullkiller->heroManager->getHeroRole(path.targetHero);
if(heroRole == HeroRole::SCOUT
&& (!closestWay || closestWay->movementCost() > path.movementCost()))
auto & closestWay = closestWaysByRole[heroRole];
if(!closestWay || closestWay->movementCost() > path.movementCost())
{
closestWay = &path;
}
@ -142,9 +143,12 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
}
}
if(closestWay)
for(auto way : waysToVisitObj)
{
for(auto way : waysToVisitObj)
auto heroRole = ai->nullkiller->heroManager->getHeroRole(way->getPath().targetHero);
auto closestWay = closestWaysByRole[heroRole];
if(closestWay)
{
way->closestWayRatio
= closestWay->movementCost() / way->getPath().movementCost();

View File

@ -60,27 +60,27 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
if(town->garrisonHero)
{
if(!ai->nullkiller->isHeroLocked(town->garrisonHero.get()))
if(ai->nullkiller->isHeroLocked(town->garrisonHero.get()))
{
if(!town->visitingHero && cb->getHeroCount(ai->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER)
{
logAi->trace(
"Extracting hero %s from garrison of town %s",
town->garrisonHero->getNameTranslated(),
town->getNameTranslated());
logAi->trace(
"Hero %s in garrison of town %s is suposed to defend the town",
town->garrisonHero->getNameTranslated(),
town->getNameTranslated());
tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5)));
return;
}
return;
}
logAi->trace(
"Hero %s in garrison of town %s is suposed to defend the town",
town->garrisonHero->getNameTranslated(),
town->getNameTranslated());
if(!town->visitingHero && cb->getHeroCount(ai->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER)
{
logAi->trace(
"Extracting hero %s from garrison of town %s",
town->garrisonHero->getNameTranslated(),
town->getNameTranslated());
return;
tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5)));
return;
}
}
if(!treatNode.fastestDanger.hero)
@ -113,11 +113,21 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
for(AIPath & path : paths)
{
if(town->visitingHero && path.targetHero != town->visitingHero.get())
continue;
if(town->visitingHero && path.getHeroStrength() < town->visitingHero->getHeroStrength())
continue;
if(town->visitingHero && path.targetHero == town->visitingHero.get())
{
if(path.getHeroStrength() < town->visitingHero->getHeroStrength())
continue;
}
else if(town->garrisonHero && path.targetHero == town->garrisonHero.get())
{
if(path.getHeroStrength() < town->visitingHero->getHeroStrength())
continue;
}
else
{
if(town->visitingHero)
continue;
}
if(treat.hero.validAndSet()
&& treat.turn <= 1
@ -158,53 +168,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
if(treatIsUnderControl)
continue;
if(!town->visitingHero
&& town->hasBuilt(BuildingID::TAVERN)
&& cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST)
{
auto heroesInTavern = cb->getAvailableHeroes(town);
for(auto hero : heroesInTavern)
{
if(hero->getTotalStrength() > treat.danger)
{
auto myHeroes = cb->getHeroesInfo();
if(cb->getHeroesInfo().size() < ALLOWED_ROAMING_HEROES)
{
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Hero %s can be recruited to defend %s", hero->getObjectName(), town->getObjectName());
#endif
tasks.push_back(Goals::sptr(Goals::RecruitHero(town, hero).setpriority(1)));
continue;
}
else
{
const CGHeroInstance * weakestHero = nullptr;
for(auto existingHero : myHeroes)
{
if(ai->nullkiller->isHeroLocked(existingHero)
|| existingHero->getArmyStrength() > hero->getArmyStrength()
|| ai->nullkiller->heroManager->getHeroRole(existingHero) == HeroRole::MAIN
|| existingHero->movementPointsRemaining()
|| existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1))
continue;
if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength())
{
weakestHero = existingHero;
}
if(weakestHero)
{
tasks.push_back(Goals::sptr(Goals::DismissHero(weakestHero)));
}
}
}
}
}
}
evaluateRecruitingHero(tasks, treat, town);
if(paths.empty())
{
@ -275,9 +239,11 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
tasks.push_back(
Goals::sptr(Composition()
.addNext(DefendTown(town, treat, path))
.addNext(ExchangeSwapTownHeroes(town, town->visitingHero.get()))
.addNext(ExecuteHeroChain(path, town))
.addNext(ExchangeSwapTownHeroes(town, path.targetHero, HeroLockedReason::DEFENCE))));
.addNextSequence({
sptr(ExchangeSwapTownHeroes(town, town->visitingHero.get())),
sptr(ExecuteHeroChain(path, town)),
sptr(ExchangeSwapTownHeroes(town, path.targetHero, HeroLockedReason::DEFENCE))
})));
continue;
}
@ -313,15 +279,45 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
continue;
}
}
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Move %s to defend town %s",
path.targetHero->getObjectName(),
town->getObjectName());
#endif
Composition composition;
composition.addNext(DefendTown(town, treat, path)).addNext(ExecuteHeroChain(path, town));
composition.addNext(DefendTown(town, treat, path));
TGoalVec sequence;
if(town->visitingHero && path.targetHero != town->visitingHero && !path.containsHero(town->visitingHero))
{
if(town->garrisonHero)
{
if(ai->nullkiller->heroManager->getHeroRole(town->visitingHero.get()) == HeroRole::SCOUT
&& town->visitingHero->getArmyStrength() < path.heroArmy->getArmyStrength() / 20)
{
if(path.turn() == 0)
sequence.push_back(sptr(DismissHero(town->visitingHero.get())));
}
else
{
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Cancel moving %s to defend town %s as the town has garrison hero",
path.targetHero->getObjectName(),
town->getObjectName());
#endif
continue;
}
}
else if(path.turn() == 0)
{
sequence.push_back(sptr(ExchangeSwapTownHeroes(town, town->visitingHero.get())));
}
}
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Move %s to defend town %s",
path.targetHero->getObjectName(),
town->getObjectName());
#endif
sequence.push_back(sptr(ExecuteHeroChain(path, town)));
composition.addNextSequence(sequence);
auto firstBlockedAction = path.getFirstBlockedAction();
if(firstBlockedAction)
@ -350,4 +346,87 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
logAi->debug("Found %d tasks", tasks.size());
}
void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & treat, const CGTownInstance * town) const
{
if(town->hasBuilt(BuildingID::TAVERN)
&& cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST)
{
auto heroesInTavern = cb->getAvailableHeroes(town);
for(auto hero : heroesInTavern)
{
if(hero->getTotalStrength() < treat.danger)
continue;
auto myHeroes = cb->getHeroesInfo();
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Hero %s can be recruited to defend %s", hero->getObjectName(), town->getObjectName());
#endif
bool needSwap = false;
const CGHeroInstance * heroToDismiss = nullptr;
if(town->visitingHero)
{
if(!town->garrisonHero)
needSwap = true;
else
{
if(town->visitingHero->getArmyStrength() < town->garrisonHero->getArmyStrength())
{
if(town->visitingHero->getArmyStrength() >= hero->getArmyStrength())
continue;
heroToDismiss = town->visitingHero.get();
}
else if(town->garrisonHero->getArmyStrength() >= hero->getArmyStrength())
continue;
else
{
needSwap = true;
heroToDismiss = town->garrisonHero.get();
}
}
}
else if(ai->nullkiller->heroManager->heroCapReached())
{
const CGHeroInstance * weakestHero = nullptr;
for(auto existingHero : myHeroes)
{
if(ai->nullkiller->isHeroLocked(existingHero)
|| existingHero->getArmyStrength() > hero->getArmyStrength()
|| ai->nullkiller->heroManager->getHeroRole(existingHero) == HeroRole::MAIN
|| existingHero->movementPointsRemaining()
|| existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1))
continue;
if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength())
{
weakestHero = existingHero;
}
}
if(!weakestHero)
continue;
heroToDismiss = weakestHero;
}
TGoalVec sequence;
Goals::Composition recruitHeroComposition;
if(needSwap)
sequence.push_back(sptr(ExchangeSwapTownHeroes(town, town->visitingHero.get())));
if(heroToDismiss)
sequence.push_back(sptr(DismissHero(heroToDismiss)));
sequence.push_back(sptr(Goals::RecruitHero(town, hero)));
tasks.push_back(sptr(Goals::Composition().addNext(DefendTown(town, treat, hero)).addNextSequence(sequence)));
}
}
}
}

View File

@ -15,8 +15,12 @@
namespace NKAI
{
struct HitMapInfo;
namespace Goals
{
class DefenceBehavior : public CGoal<DefenceBehavior>
{
public:
@ -35,6 +39,7 @@ namespace Goals
private:
void evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town) const;
void evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & treat, const CGTownInstance * town) const;
};
}

View File

@ -16,6 +16,7 @@
#include "../Markers/ArmyUpgrade.h"
#include "GatherArmyBehavior.h"
#include "../AIUtility.h"
#include "../Goals/ExchangeSwapTownHeroes.h"
namespace NKAI
{
@ -78,20 +79,27 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
for(const AIPath & path : paths)
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Path found %s", path.toString());
logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength());
#endif
if(path.containsHero(hero)) continue;
if(path.turn() == 0 && hero->inTownGarrison)
if(path.containsHero(hero))
{
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Skipping garnisoned hero %s, %s", hero->getObjectName(), pos.toString());
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Selfcontaining path. Ignore");
#endif
continue;
}
if(ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
bool garrisoned = false;
if(path.turn() == 0 && hero->inTownGarrison)
{
#if NKAI_TRACE_LEVEL >= 1
garrisoned = true;
#endif
}
if(path.turn() > 0 && ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength());
@ -172,7 +180,21 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
exchangePath.closestWayRatio = 1;
composition.addNext(heroExchange);
composition.addNext(exchangePath);
if(garrisoned && path.turn() == 0)
{
auto lockReason = ai->nullkiller->getHeroLockedReason(hero);
composition.addNextSequence({
sptr(ExchangeSwapTownHeroes(hero->visitedTown)),
sptr(exchangePath),
sptr(ExchangeSwapTownHeroes(hero->visitedTown, hero, lockReason))
});
}
else
{
composition.addNext(exchangePath);
}
auto blockedAction = path.getFirstBlockedAction();
@ -221,7 +243,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
for(const AIPath & path : paths)
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Path found %s", path.toString());
logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength());
#endif
if(upgrader->visitingHero && upgrader->visitingHero.get() != path.targetHero)
{
@ -267,7 +289,14 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
ai->nullkiller->armyManager->howManyReinforcementsCanGet(
path.targetHero,
path.heroArmy,
upgrader->getUpperArmy());
upgrader->getUpperArmy());
upgrade.upgradeValue +=
ai->nullkiller->armyManager->howManyReinforcementsCanBuy(
path.heroArmy,
upgrader,
ai->nullkiller->getFreeResources(),
path.turn());
}
auto armyValue = (float)upgrade.upgradeValue / path.getHeroStrength();

View File

@ -52,6 +52,7 @@ set(Nullkiller_SRCS
Behaviors/BuildingBehavior.cpp
Behaviors/GatherArmyBehavior.cpp
Behaviors/ClusterBehavior.cpp
Helpers/ArmyFormation.cpp
AIGateway.cpp
)
@ -114,6 +115,7 @@ set(Nullkiller_HEADERS
Behaviors/BuildingBehavior.h
Behaviors/GatherArmyBehavior.h
Behaviors/ClusterBehavior.h
Helpers/ArmyFormation.h
AIGateway.h
)

View File

@ -61,6 +61,7 @@ void Nullkiller::init(std::shared_ptr<CCallback> cb, PlayerColor playerID)
armyManager.reset(new ArmyManager(cb.get(), this));
heroManager.reset(new HeroManager(cb.get(), this));
decomposer.reset(new DeepDecomposer());
armyFormation.reset(new ArmyFormation(cb, this));
}
Goals::TTask Nullkiller::choseBestTask(Goals::TTaskVec & tasks) const
@ -137,6 +138,7 @@ void Nullkiller::updateAiState(int pass, bool fast)
{
memory->removeInvisibleObjects(cb.get());
dangerHitMap->calculateTileOwners();
dangerHitMap->updateHitMap();
boost::this_thread::interruption_point();
@ -222,7 +224,7 @@ void Nullkiller::makeTurn()
boost::lock_guard<boost::mutex> sharedStorageLock(AISharedStorage::locker);
const int MAX_DEPTH = 10;
const float FAST_TASK_MINIMAL_PRIORITY = 0.7;
const float FAST_TASK_MINIMAL_PRIORITY = 0.7f;
resetAiState();

View File

@ -18,6 +18,7 @@
#include "../Analyzers/ArmyManager.h"
#include "../Analyzers/HeroManager.h"
#include "../Analyzers/ObjectClusterizer.h"
#include "../Helpers/ArmyFormation.h"
namespace NKAI
{
@ -67,6 +68,7 @@ public:
std::unique_ptr<AIMemory> memory;
std::unique_ptr<FuzzyHelper> dangerEvaluator;
std::unique_ptr<DeepDecomposer> decomposer;
std::unique_ptr<ArmyFormation> armyFormation;
PlayerColor playerID;
std::shared_ptr<CCallback> cb;

View File

@ -23,6 +23,7 @@
#include "../Goals/ExecuteHeroChain.h"
#include "../Goals/BuildThis.h"
#include "../Goals/ExchangeSwapTownHeroes.h"
#include "../Goals/DismissHero.h"
#include "../Markers/UnlockCluster.h"
#include "../Markers/HeroExchange.h"
#include "../Markers/ArmyUpgrade.h"
@ -33,6 +34,7 @@ namespace NKAI
#define MIN_AI_STRENGHT (0.5f) //lower when combat AI gets smarter
#define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us
const float MIN_CRITICAL_VALUE = 2.0f;
EvaluationContext::EvaluationContext(const Nullkiller * ai)
: movementCost(0.0),
@ -54,6 +56,11 @@ EvaluationContext::EvaluationContext(const Nullkiller * ai)
{
}
void EvaluationContext::addNonCriticalStrategicalValue(float value)
{
vstd::amax(strategicalValue, std::min(value, MIN_CRITICAL_VALUE));
}
PriorityEvaluator::~PriorityEvaluator()
{
delete engine;
@ -399,7 +406,7 @@ float RewardEvaluator::getEnemyHeroStrategicalValue(const CGHeroInstance * enemy
2. The formula quickly approaches 1.0 as hero level increases,
but higher level always means higher value and the minimal value for level 1 hero is 0.5
*/
return std::min(1.0f, objectValue * 0.9f + (1.0f - (1.0f / (1 + enemy->level))));
return std::min(1.5f, objectValue * 0.9f + (1.5f - (1.5f / (1 + enemy->level))));
}
float RewardEvaluator::getResourceRequirementStrength(int resType) const
@ -640,7 +647,8 @@ public:
uint64_t armyStrength = heroExchange.getReinforcementArmyStrength();
evaluationContext.strategicalValue += 0.5f * armyStrength / heroExchange.hero.get()->getArmyStrength();
evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero.get()->getArmyStrength());
evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero.get());
}
};
@ -657,7 +665,7 @@ public:
uint64_t upgradeValue = armyUpgrade.getUpgradeValue();
evaluationContext.armyReward += upgradeValue;
evaluationContext.strategicalValue += upgradeValue / (float)armyUpgrade.hero->getArmyStrength();
evaluationContext.addNonCriticalStrategicalValue(upgradeValue / (float)armyUpgrade.hero->getArmyStrength());
}
};
@ -712,19 +720,21 @@ public:
auto armyIncome = townArmyIncome(town);
auto dailyIncome = town->dailyIncome()[EGameResID::GOLD];
auto strategicalValue = std::sqrt(armyIncome / 20000.0f) + dailyIncome / 3000.0f;
auto strategicalValue = std::sqrt(armyIncome / 60000.0f) + dailyIncome / 10000.0f;
if(evaluationContext.evaluator.ai->buildAnalyzer->getDevelopmentInfo().size() == 1)
strategicalValue = 1;
vstd::amax(evaluationContext.strategicalValue, 10.0);
float multiplier = 1;
if(treat.turn < defendTown.getTurn())
multiplier /= 1 + (defendTown.getTurn() - treat.turn);
evaluationContext.armyReward += armyIncome * multiplier;
multiplier /= 1.0f + treat.turn / 5.0f;
evaluationContext.armyGrowth += armyIncome * multiplier;
evaluationContext.goldReward += dailyIncome * 5 * multiplier;
evaluationContext.strategicalValue += strategicalValue * multiplier;
evaluationContext.addNonCriticalStrategicalValue(strategicalValue * multiplier);
vstd::amax(evaluationContext.danger, defendTown.getTreat().danger);
addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength());
}
@ -770,19 +780,22 @@ public:
auto army = path.heroArmy;
const CGObjectInstance * target = ai->cb->getObj((ObjectInstanceID)task->objid, false);
auto heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroPtr);
if(heroRole == HeroRole::MAIN)
evaluationContext.heroRole = heroRole;
if (target && ai->cb->getPlayerRelations(target->tempOwner, hero->tempOwner) == PlayerRelations::ENEMIES)
{
evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero);
evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold);
evaluationContext.armyGrowth += evaluationContext.evaluator.getArmyGrowth(target, hero, army);
evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, evaluationContext.heroRole);
evaluationContext.strategicalValue += evaluationContext.evaluator.getStrategicalValue(target);
evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, heroRole);
evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target));
evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army);
}
vstd::amax(evaluationContext.armyLossPersentage, path.getTotalArmyLoss() / (double)path.getHeroStrength());
evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroPtr);
addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength());
vstd::amax(evaluationContext.turn, path.turn());
}
@ -822,7 +835,7 @@ public:
evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero) / boost;
evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold) / boost;
evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, role) / boost;
evaluationContext.strategicalValue += evaluationContext.evaluator.getStrategicalValue(target) / boost;
evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target) / boost);
evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army) / boost;
evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost;
evaluationContext.movementCost += objInfo.second.movementCost / boost;
@ -860,6 +873,31 @@ public:
}
};
class DismissHeroContextBuilder : public IEvaluationContextBuilder
{
private:
const Nullkiller * ai;
public:
DismissHeroContextBuilder(const Nullkiller * ai) : ai(ai) {}
virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
{
if(task->goalType != Goals::DISMISS_HERO)
return;
Goals::DismissHero & dismissCommand = dynamic_cast<Goals::DismissHero &>(*task);
const CGHeroInstance * dismissedHero = dismissCommand.getHero().get();
auto role = ai->heroManager->getHeroRole(dismissedHero);
auto mpLeft = dismissedHero->movement;
evaluationContext.movementCost += mpLeft;
evaluationContext.movementCostByRole[role] += mpLeft;
evaluationContext.goldCost += GameConstants::HERO_GOLD_COST + getArmyCost(dismissedHero);
}
};
class BuildThisEvaluationContextBuilder : public IEvaluationContextBuilder
{
public:
@ -878,31 +916,31 @@ public:
if(bi.creatureID != CreatureID::NONE)
{
evaluationContext.strategicalValue += buildThis.townInfo.armyStrength / 50000.0;
evaluationContext.addNonCriticalStrategicalValue(buildThis.townInfo.armyStrength / 50000.0);
if(bi.baseCreatureID == bi.creatureID)
{
evaluationContext.strategicalValue += (0.5f + 0.1f * bi.creatureLevel) / (float)bi.prerequisitesCount;
evaluationContext.addNonCriticalStrategicalValue((0.5f + 0.1f * bi.creatureLevel) / (float)bi.prerequisitesCount);
evaluationContext.armyReward += bi.armyStrength;
}
else
{
auto potentialUpgradeValue = evaluationContext.evaluator.getUpgradeArmyReward(buildThis.town, bi);
evaluationContext.strategicalValue += potentialUpgradeValue / 10000.0f / (float)bi.prerequisitesCount;
evaluationContext.addNonCriticalStrategicalValue(potentialUpgradeValue / 10000.0f / (float)bi.prerequisitesCount);
evaluationContext.armyReward += potentialUpgradeValue / (float)bi.prerequisitesCount;
}
}
else if(bi.id == BuildingID::CITADEL || bi.id == BuildingID::CASTLE)
{
evaluationContext.strategicalValue += buildThis.town->creatures.size() * 0.2f;
evaluationContext.addNonCriticalStrategicalValue(buildThis.town->creatures.size() * 0.2f);
evaluationContext.armyReward += buildThis.townInfo.armyStrength / 2;
}
else
{
auto goldPreasure = evaluationContext.evaluator.ai->buildAnalyzer->getGoldPreasure();
evaluationContext.strategicalValue += evaluationContext.goldReward * goldPreasure / 3500.0f / bi.prerequisitesCount;
evaluationContext.addNonCriticalStrategicalValue(evaluationContext.goldReward * goldPreasure / 3500.0f / bi.prerequisitesCount);
}
if(bi.notEnoughRes && bi.prerequisitesCount == 1)
@ -934,6 +972,7 @@ PriorityEvaluator::PriorityEvaluator(const Nullkiller * ai)
evaluationContextBuilders.push_back(std::make_shared<ArmyUpgradeEvaluator>());
evaluationContextBuilders.push_back(std::make_shared<DefendTownEvaluator>());
evaluationContextBuilders.push_back(std::make_shared<ExchangeSwapTownHeroesContextBuilder>());
evaluationContextBuilders.push_back(std::make_shared<DismissHeroContextBuilder>(ai));
}
EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal) const

View File

@ -66,6 +66,8 @@ struct DLL_EXPORT EvaluationContext
float enemyHeroDangerRatio;
EvaluationContext(const Nullkiller * ai);
void addNonCriticalStrategicalValue(float value);
};
class IEvaluationContextBuilder

View File

@ -71,7 +71,7 @@ void BuyArmy::accept(AIGateway * ai)
throw cannotFulfillGoalException("No creatures to buy.");
}
if(town->visitingHero)
if(town->visitingHero && !town->garrisonHero)
{
ai->moveHeroToTile(town->visitablePos(), town->visitingHero.get());
}

View File

@ -31,9 +31,17 @@ std::string Composition::toString() const
{
std::string result = "Composition";
for(auto goal : subtasks)
for(auto step : subtasks)
{
result += " " + goal->toString();
result += "[";
for(auto goal : step)
{
if(goal->isElementar())
result += goal->toString() + " => ";
else
result += goal->toString() + ", ";
}
result += "] ";
}
return result;
@ -41,17 +49,34 @@ std::string Composition::toString() const
void Composition::accept(AIGateway * ai)
{
taskptr(*subtasks.back())->accept(ai);
for(auto task : subtasks.back())
{
if(task->isElementar())
{
taskptr(*task)->accept(ai);
}
else
{
break;
}
}
}
TGoalVec Composition::decompose() const
{
return subtasks;
TGoalVec result;
for(const TGoalVec & step : subtasks)
vstd::concatenate(result, step);
return result;
}
Composition & Composition::addNext(const AbstractGoal & goal)
Composition & Composition::addNextSequence(const TGoalVec & taskSequence)
{
return addNext(sptr(goal));
subtasks.push_back(taskSequence);
return *this;
}
Composition & Composition::addNext(TSubgoal goal)
@ -64,20 +89,35 @@ Composition & Composition::addNext(TSubgoal goal)
}
else
{
subtasks.push_back(goal);
subtasks.push_back({goal});
}
return *this;
}
Composition & Composition::addNext(const AbstractGoal & goal)
{
return addNext(sptr(goal));
}
bool Composition::isElementar() const
{
return subtasks.back()->isElementar();
return subtasks.back().front()->isElementar();
}
int Composition::getHeroExchangeCount() const
{
return isElementar() ? taskptr(*subtasks.back())->getHeroExchangeCount() : 0;
auto result = 0;
for(auto task : subtasks.back())
{
if(task->isElementar())
{
result += taskptr(*task)->getHeroExchangeCount();
}
}
return result;
}
}

View File

@ -18,7 +18,7 @@ namespace Goals
class DLL_EXPORT Composition : public ElementarGoal<Composition>
{
private:
TGoalVec subtasks;
std::vector<TGoalVec> subtasks; // things we want to do now
public:
Composition()
@ -26,16 +26,12 @@ namespace Goals
{
}
Composition(TGoalVec subtasks)
: ElementarGoal(Goals::COMPOSITION), subtasks(subtasks)
{
}
virtual bool operator==(const Composition & other) const override;
virtual std::string toString() const override;
void accept(AIGateway * ai) override;
Composition & addNext(const AbstractGoal & goal);
Composition & addNext(TSubgoal goal);
Composition & addNextSequence(const TGoalVec & taskSequence);
virtual TGoalVec decompose() const override;
virtual bool isElementar() const override;
virtual int getHeroExchangeCount() const override;

View File

@ -52,6 +52,20 @@ void ExecuteHeroChain::accept(AIGateway * ai)
ai->nullkiller->setActive(chainPath.targetHero, tile);
ai->nullkiller->setTargetObject(objid);
auto targetObject = ai->myCb->getObj(static_cast<ObjectInstanceID>(objid), false);
if(chainPath.turn() == 0 && targetObject && targetObject->ID == Obj::TOWN)
{
auto relations = ai->myCb->getPlayerRelations(ai->playerID, targetObject->getOwner());
if(relations == PlayerRelations::ENEMIES)
{
ai->nullkiller->armyFormation->rearrangeArmyForSiege(
dynamic_cast<const CGTownInstance *>(targetObject),
chainPath.targetHero);
}
}
std::set<int> blockedIndexes;
for(int i = chainPath.nodes.size() - 1; i >= 0; i--)

View File

@ -24,7 +24,10 @@ using namespace Goals;
std::string RecruitHero::toString() const
{
return "Recruit hero at " + town->getNameTranslated();
if(heroToBuy)
return "Recruit " + heroToBuy->getNameTranslated() + " at " + town->getNameTranslated();
else
return "Recruit hero at " + town->getNameTranslated();
}
void RecruitHero::accept(AIGateway * ai)
@ -45,20 +48,20 @@ void RecruitHero::accept(AIGateway * ai)
throw cannotFulfillGoalException("No available heroes in tavern in " + t->nodeName());
}
auto heroToHire = heroes[0];
auto heroToHire = heroToBuy;
for(auto hero : heroes)
if(!heroToHire)
{
if(objid == hero->id.getNum())
for(auto hero : heroes)
{
heroToHire = hero;
break;
if(!heroToHire || hero->getTotalStrength() > heroToHire->getTotalStrength())
heroToHire = hero;
}
if(hero->getTotalStrength() > heroToHire->getTotalStrength())
heroToHire = hero;
}
if(!heroToHire)
throw cannotFulfillGoalException("No hero to hire!");
if(t->visitingHero)
{
cb->swapGarrisonHero(t);

View File

@ -22,18 +22,20 @@ namespace Goals
{
class DLL_EXPORT RecruitHero : public ElementarGoal<RecruitHero>
{
private:
const CGHeroInstance * heroToBuy;
public:
RecruitHero(const CGTownInstance * townWithTavern, const CGHeroInstance * heroToBuy)
: RecruitHero(townWithTavern)
: ElementarGoal(Goals::RECRUIT_HERO), heroToBuy(heroToBuy)
{
objid = heroToBuy->id.getNum();
town = townWithTavern;
priority = 1;
}
RecruitHero(const CGTownInstance * townWithTavern)
: ElementarGoal(Goals::RECRUIT_HERO)
: RecruitHero(townWithTavern, nullptr)
{
priority = 1;
town = townWithTavern;
}
virtual bool operator==(const RecruitHero & other) const override

View File

@ -0,0 +1,68 @@
/*
* ArmyFormation.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 "ArmyFormation.h"
#include "../../../lib/mapObjects/CGTownInstance.h"
namespace NKAI
{
void ArmyFormation::rearrangeArmyForSiege(const CGTownInstance * town, const CGHeroInstance * attacker)
{
auto freeSlots = attacker->getFreeSlotsQueue();
while(!freeSlots.empty())
{
auto weakestCreature = vstd::minElementByFun(attacker->Slots(), [](const std::pair<SlotID, CStackInstance *> & slot) -> int
{
return slot.second->getCount() == 1
? std::numeric_limits<int>::max()
: slot.second->getCreatureID().toCreature()->getAIValue();
});
if(weakestCreature == attacker->Slots().end() || weakestCreature->second->getCount() == 1)
{
break;
}
cb->splitStack(attacker, attacker, weakestCreature->first, freeSlots.front(), 1);
freeSlots.pop();
}
if(town->fortLevel() > CGTownInstance::FORT)
{
std::vector<CStackInstance *> stacks;
for(auto slot : attacker->Slots())
stacks.push_back(slot.second);
boost::sort(
stacks,
[](CStackInstance * slot1, CStackInstance * slot2) -> bool
{
auto cre1 = slot1->getCreatureID().toCreature();
auto cre2 = slot2->getCreatureID().toCreature();
auto flying = cre1->hasBonusOfType(BonusType::FLYING) - cre2->hasBonusOfType(BonusType::FLYING);
if(flying != 0) return flying < 0;
else return cre1->getAIValue() < cre2->getAIValue();
});
for(int i = 0; i < stacks.size(); i++)
{
auto pos = vstd::findKey(attacker->Slots(), stacks[i]);
if(pos.getNum() != i)
cb->swapCreatures(attacker, attacker, static_cast<SlotID>(i), pos);
}
}
}
}

View File

@ -0,0 +1,39 @@
/*
* ArmyFormation.h, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#pragma once
#include "../AIUtility.h"
#include "../../../lib/GameConstants.h"
#include "../../../lib/VCMI_Lib.h"
#include "../../../lib/CTownHandler.h"
#include "../../../lib/CBuildingHandler.h"
namespace NKAI
{
struct HeroPtr;
class AIGateway;
class FuzzyHelper;
class Nullkiller;
class DLL_EXPORT ArmyFormation
{
private:
std::shared_ptr<CCallback> cb; //this is enough, but we downcast from CCallback
const Nullkiller * ai;
public:
ArmyFormation(std::shared_ptr<CCallback> CB, const Nullkiller * ai): cb(CB), ai(ai) {}
void rearrangeArmyForSiege(const CGTownInstance * town, const CGHeroInstance * attacker);
};
}

View File

@ -29,7 +29,7 @@ bool HeroExchange::operator==(const HeroExchange & other) const
std::string HeroExchange::toString() const
{
return "Hero exchange " + exchangePath.toString();
return "Hero exchange for " +hero.get()->getObjectName() + " by " + exchangePath.toString();
}
uint64_t HeroExchange::getReinforcementArmyStrength() const

View File

@ -879,7 +879,7 @@ void AINodeStorage::setHeroes(std::map<const CGHeroInstance *, HeroRole> heroes)
for(auto & hero : heroes)
{
// do not allow our own heroes in garrison to act on map
if(hero.first->getOwner() == ai->playerID && hero.first->inTownGarrison)
if(hero.first->getOwner() == ai->playerID && hero.first->inTownGarrison && ai->isHeroLocked(hero.first))
continue;
uint64_t mask = FirstActorMask << actors.size();

View File

@ -11,7 +11,7 @@
#pragma once
#define NKAI_PATHFINDER_TRACE_LEVEL 0
#define NKAI_TRACE_LEVEL 0
#define NKAI_TRACE_LEVEL 2
#include "../../../lib/pathfinder/CGPathNode.h"
#include "../../../lib/pathfinder/INodeStorage.h"
@ -24,8 +24,8 @@
namespace NKAI
{
const int SCOUT_TURN_DISTANCE_LIMIT = 3;
const int MAIN_TURN_DISTANCE_LIMIT = 5;
const int SCOUT_TURN_DISTANCE_LIMIT = 5;
const int MAIN_TURN_DISTANCE_LIMIT = 10;
namespace AIPathfinding
{

View File

@ -6,9 +6,9 @@ InputVariable: mainTurnDistance
range: 0.000 10.000
lock-range: true
term: LOWEST Ramp 0.250 0.000
term: LOW Discrete 0.000 1.000 0.500 0.800 1.000 0.000
term: MEDIUM Discrete 0.000 0.000 0.500 0.200 1.000 1.000 3.000 0.000
term: LONG Discrete 1.000 0.000 1.500 0.200 3.000 0.800 10.000 1.000
term: LOW Discrete 0.000 1.000 0.500 0.800 0.800 0.300 2.000 0.000
term: MEDIUM Discrete 0.000 0.000 0.500 0.200 0.800 0.700 2.000 1.000 6.000 0.000
term: LONG Discrete 2.000 0.000 6.000 1.000 10.000 0.800
InputVariable: scoutTurnDistance
description: distance to tile in turns
enabled: true
@ -43,6 +43,7 @@ InputVariable: armyLoss
term: LOW Ramp 0.200 0.000
term: MEDIUM Triangle 0.000 0.200 0.500
term: HIGH Ramp 0.200 0.500
term: ALL Ramp 0.700 1.000
InputVariable: heroRole
enabled: true
range: -0.100 1.100
@ -82,13 +83,14 @@ InputVariable: closestHeroRatio
InputVariable: strategicalValue
description: Some abstract long term benefit non gold or army or skill
enabled: true
range: 0.000 1.000
range: 0.000 3.000
lock-range: false
term: NONE Ramp 0.200 0.000
term: LOWEST Triangle 0.000 0.010 0.250
term: LOW Triangle 0.000 0.250 0.700
term: MEDIUM Triangle 0.250 0.700 1.000
term: HIGH Ramp 0.700 1.000
term: LOW Triangle 0.000 0.250 1.000
term: MEDIUM Triangle 0.250 1.000 2.000
term: HIGH Triangle 1.000 2.000 3.000
term: CRITICAL Ramp 2.000 3.000
InputVariable: goldPreasure
description: Ratio between weekly army cost and gold income
enabled: true
@ -132,21 +134,22 @@ InputVariable: armyGrowth
term: HUGE Ramp 8000.000 20000.000
OutputVariable: Value
enabled: true
range: -1.500 2.000
range: -1.500 2.500
lock-range: false
aggregation: AlgebraicSum
defuzzifier: Centroid 100
default: 0.500
lock-previous: false
term: WORST Binary -1.000 -inf 0.500
term: WORST Binary -1.000 -inf 0.700
term: BAD Rectangle -1.000 -0.700 0.500
term: BASE Rectangle -0.200 0.200 0.400
term: LOW Rectangle 1.110 1.190 0.320
term: HIGHEST Discrete 0.300 0.000 0.300 1.000 0.600 1.000 0.600 0.000 1.700 0.000 1.700 1.000 2.000 1.000 2.000 0.000 0.500
term: HIGH Discrete 0.600 0.000 0.600 1.000 0.850 1.000 0.850 0.000 1.450 0.000 1.450 1.000 1.700 1.000 1.700 0.000 0.400
term: BITHIGH Discrete 0.850 0.000 0.850 1.000 1.000 1.000 1.000 0.000 1.300 0.000 1.300 1.000 1.450 1.000 1.450 0.000 0.350
term: MEDIUM Discrete 1.000 0.000 1.000 1.000 1.100 1.000 1.100 0.000 1.200 0.000 1.200 1.000 1.300 1.000 1.300 0.000 0.330
RuleBlock: gold reward
term: BASE Rectangle -0.200 0.200 0.350
term: MEDIUM Rectangle 0.910 1.090 0.500
term: SMALL Rectangle 0.960 1.040 0.600
term: BITHIGH Rectangle 0.850 1.150 0.400
term: HIGH Rectangle 0.750 1.250 0.400
term: HIGHEST Rectangle 0.500 1.500 0.350
term: CRITICAL Ramp 0.500 2.000 0.500
RuleBlock: basic
enabled: true
conjunction: AlgebraicProduct
disjunction: AlgebraicSum
@ -154,6 +157,61 @@ RuleBlock: gold reward
activation: General
rule: if heroRole is MAIN then Value is BASE
rule: if heroRole is SCOUT then Value is BASE
rule: if heroRole is MAIN and armyGrowth is HUGE then Value is HIGH
rule: if heroRole is MAIN and armyGrowth is BIG then Value is BITHIGH
rule: if heroRole is MAIN and strategicalValue is HIGH then Value is HIGHEST
rule: if heroRole is MAIN and armyGrowth is HUGE and fear is not HIGH then Value is HIGH
rule: if heroRole is MAIN and armyGrowth is BIG and mainTurnDistance is LOW then Value is HIGH
rule: if heroRole is MAIN and armyGrowth is BIG and mainTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH
rule: if heroRole is MAIN and armyGrowth is BIG and mainTurnDistance is LONG and fear is not HIGH then Value is MEDIUM
rule: if armyLoss is ALL then Value is WORST
rule: if turn is not NOW then Value is BAD with 0.1
rule: if closestHeroRatio is LOWEST and heroRole is SCOUT then Value is WORST
rule: if closestHeroRatio is LOW and heroRole is SCOUT then Value is BAD
rule: if closestHeroRatio is LOWEST and heroRole is MAIN then Value is BAD
RuleBlock: strategicalValue
enabled: true
conjunction: AlgebraicProduct
disjunction: NormalizedSum
implication: AlgebraicProduct
activation: General
rule: if heroRole is MAIN and strategicalValue is HIGH and turn is NOW then Value is HIGHEST
rule: if heroRole is MAIN and strategicalValue is HIGH and turn is not NOW and mainTurnDistance is LOW and fear is not HIGH then Value is HIGHEST
rule: if heroRole is MAIN and strategicalValue is HIGH and mainTurnDistance is MEDIUM and fear is not HIGH then Value is HIGHEST with 0.5
rule: if heroRole is MAIN and strategicalValue is HIGH and mainTurnDistance is LONG and fear is not HIGH then Value is HIGH
rule: if heroRole is MAIN and strategicalValue is MEDIUM and mainTurnDistance is LOW then Value is HIGH
rule: if heroRole is MAIN and strategicalValue is MEDIUM and mainTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH
rule: if heroRole is MAIN and strategicalValue is MEDIUM and mainTurnDistance is LONG and fear is not HIGH then Value is MEDIUM
rule: if heroRole is MAIN and strategicalValue is LOW and mainTurnDistance is LOW and fear is not HIGH then Value is MEDIUM
rule: if heroRole is MAIN and strategicalValue is LOW and mainTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL
rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is not NONE and turn is NOW then Value is HIGH
rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is not NONE and turn is not NOW and scoutTurnDistance is LOW and fear is not HIGH then Value is HIGH
rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is not NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is HIGH with 0.5
rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is not NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is BITHIGH
rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is not NONE and scoutTurnDistance is LOW then Value is BITHIGH
rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is not NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is MEDIUM
rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is not NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is SMALL
rule: if heroRole is SCOUT and strategicalValue is LOW and danger is not NONE and scoutTurnDistance is LOW and fear is not HIGH then Value is SMALL
rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is NONE and turn is NOW then Value is HIGHEST
rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is NONE and turn is not NOW and scoutTurnDistance is LOW and fear is not HIGH then Value is HIGHEST
rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is HIGHEST with 0.5
rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is HIGH
rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is NONE and scoutTurnDistance is LOW then Value is HIGH
rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH
rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is MEDIUM
rule: if heroRole is SCOUT and strategicalValue is LOW and danger is NONE and scoutTurnDistance is LOW and fear is not HIGH then Value is MEDIUM
rule: if heroRole is SCOUT and strategicalValue is LOW and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL
rule: if armyLoss is HIGH and strategicalValue is LOW then Value is BAD
rule: if armyLoss is HIGH and strategicalValue is MEDIUM then Value is BAD with 0.7
rule: if strategicalValue is CRITICAL then Value is CRITICAL
RuleBlock: armyReward
enabled: true
conjunction: AlgebraicProduct
disjunction: AlgebraicSum
implication: AlgebraicProduct
activation: General
rule: if heroRole is MAIN and armyReward is HIGH and mainTurnDistance is LOW and fear is not HIGH then Value is HIGHEST
rule: if heroRole is MAIN and armyReward is HIGH and mainTurnDistance is MEDIUM and fear is not HIGH then Value is HIGH
rule: if heroRole is MAIN and armyReward is HIGH and mainTurnDistance is LONG and fear is not HIGH then Value is BITHIGH
rule: if heroRole is MAIN and armyReward is MEDIUM and mainTurnDistance is LOW then Value is HIGH
rule: if heroRole is MAIN and armyReward is MEDIUM and mainTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH
rule: if heroRole is MAIN and armyReward is MEDIUM and mainTurnDistance is LONG and fear is not HIGH then Value is MEDIUM
rule: if heroRole is MAIN and armyReward is LOW and mainTurnDistance is LOW and fear is not HIGH then Value is MEDIUM
rule: if heroRole is MAIN and armyReward is LOW and mainTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL