1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-08-13 19:54:17 +02:00

Merge pull request #2306 from vcmi/nkai-priority-rework

Nkai priority rework
This commit is contained in:
Andrii Danylchenko
2023-08-01 19:18:09 +03:00
committed by GitHub
41 changed files with 1403 additions and 475 deletions

View File

@@ -863,7 +863,7 @@ std::optional<BattleAction> CBattleAI::considerFleeingOrSurrendering()
bs.turnsSkippedByDefense = movesSkippedByDefense / bs.ourStacks.size(); bs.turnsSkippedByDefense = movesSkippedByDefense / bs.ourStacks.size();
if(!bs.canFlee || !bs.canSurrender) if(!bs.canFlee && !bs.canSurrender)
{ {
return std::nullopt; return std::nullopt;
} }

View File

@@ -29,7 +29,7 @@ namespace NKAI
{ {
// our to enemy strength ratio constants // 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 float RETREAT_THRESHOLD = 0.3f;
const double RETREAT_ABSOLUTE_THRESHOLD = 10000.; const double RETREAT_ABSOLUTE_THRESHOLD = 10000.;
@@ -90,9 +90,11 @@ void AIGateway::heroMoved(const TryMoveHero & details, bool verbose)
LOG_TRACE(logAi); LOG_TRACE(logAi);
NET_EVENT_HANDLER; NET_EVENT_HANDLER;
validateObject(details.id); //enemy hero may have left visible area
auto hero = cb->getHero(details.id); 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 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)); const int3 to = hero ? hero->convertToVisitablePos(details.end) : (details.end - int3(0,1,0));
@@ -777,28 +779,21 @@ void AIGateway::makeTurn()
boost::shared_lock<boost::shared_mutex> gsLock(CGameState::mutex); boost::shared_lock<boost::shared_mutex> gsLock(CGameState::mutex);
setThreadName("AIGateway::makeTurn"); setThreadName("AIGateway::makeTurn");
cb->sendMessage("vcmieagles");
retrieveVisitableObjs();
if(cb->getDate(Date::DAY_OF_WEEK) == 1) if(cb->getDate(Date::DAY_OF_WEEK) == 1)
{ {
std::vector<const CGObjectInstance *> objs; for(const CGObjectInstance * obj : nullkiller->memory->visitableObjs)
retrieveVisitableObjs(objs, true);
for(const CGObjectInstance * obj : objs)
{ {
if(isWeeklyRevisitable(obj)) if(isWeeklyRevisitable(obj))
{ {
addVisitableObj(obj);
nullkiller->memory->markObjectUnvisited(obj); nullkiller->memory->markObjectUnvisited(obj);
} }
} }
} }
cb->sendMessage("vcmieagles");
if(cb->getDate(Date::DAY) == 1)
{
retrieveVisitableObjs();
}
#if NKAI_TRACE_LEVEL == 0 #if NKAI_TRACE_LEVEL == 0
try try
{ {
@@ -872,6 +867,19 @@ void AIGateway::pickBestCreatures(const CArmedInstance * destinationArmy, const
auto bestArmy = nullkiller->armyManager->getBestArmy(destinationArmy, destinationArmy, source); auto bestArmy = nullkiller->armyManager->getBestArmy(destinationArmy, destinationArmy, source);
for(auto army : armies)
{
// move first stack at first slot if empty to avoid can not take away last creature
if(!army->hasStackAtSlot(SlotID(0)) && army->stacksCount() > 0)
{
cb->mergeOrSwapStacks(
army,
army,
SlotID(0),
army->Slots().begin()->first);
}
}
//foreach best type -> iterate over slots in both armies and if it's the appropriate type, send it to the slot where it belongs //foreach best type -> iterate over slots in both armies and if it's the appropriate type, send it to the slot where it belongs
for(SlotID i = SlotID(0); i.validSlot(); i.advance(1)) //i-th strongest creature type will go to i-th slot for(SlotID i = SlotID(0); i.validSlot(); i.advance(1)) //i-th strongest creature type will go to i-th slot
{ {
@@ -1059,6 +1067,11 @@ void AIGateway::recruitCreatures(const CGDwelling * d, const CArmedInstance * re
int count = d->creatures[i].first; int count = d->creatures[i].first;
CreatureID creID = d->creatures[i].second.back(); CreatureID creID = d->creatures[i].second.back();
if(!recruiter->getSlotFor(creID).validSlot())
{
continue;
}
vstd::amin(count, cb->getResourceAmount() / creID.toCreature()->getFullRecruitCost()); vstd::amin(count, cb->getResourceAmount() / creID.toCreature()->getFullRecruitCost());
if(count > 0) if(count > 0)
cb->recruitCreatures(d, recruiter, creID, count, i); cb->recruitCreatures(d, recruiter, creID, count, i);
@@ -1102,25 +1115,12 @@ void AIGateway::waitTillFree()
status.waitTillFree(); status.waitTillFree();
} }
void AIGateway::retrieveVisitableObjs(std::vector<const CGObjectInstance *> & out, bool includeOwned) const
{
foreach_tile_pos([&](const int3 & pos)
{
for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false))
{
if(includeOwned || obj->tempOwner != playerID)
out.push_back(obj);
}
});
}
void AIGateway::retrieveVisitableObjs() void AIGateway::retrieveVisitableObjs()
{ {
foreach_tile_pos([&](const int3 & pos) foreach_tile_pos([&](const int3 & pos)
{ {
for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false)) for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false))
{ {
if(obj->tempOwner != playerID)
addVisitableObj(obj); addVisitableObj(obj);
} }
}); });

View File

@@ -195,7 +195,6 @@ public:
void validateObject(const CGObjectInstance * obj); //checks if object is still visible and if not, removes references to it void validateObject(const CGObjectInstance * obj); //checks if object is still visible and if not, removes references to it
void validateObject(ObjectIdRef obj); //checks if object is still visible and if not, removes references to it void validateObject(ObjectIdRef obj); //checks if object is still visible and if not, removes references to it
void retrieveVisitableObjs(std::vector<const CGObjectInstance *> & out, bool includeOwned = false) const;
void retrieveVisitableObjs(); void retrieveVisitableObjs();
virtual std::vector<const CGObjectInstance *> getFlaggedObjects() const; virtual std::vector<const CGObjectInstance *> getFlaggedObjects() const;

View File

@@ -323,13 +323,9 @@ bool isWeeklyRevisitable(const CGObjectInstance * obj)
if(dynamic_cast<const CGDwelling *>(obj)) if(dynamic_cast<const CGDwelling *>(obj))
return true; return true;
if(dynamic_cast<const CBank *>(obj)) //banks tend to respawn often in mods
return true;
switch(obj->ID) switch(obj->ID)
{ {
case Obj::STABLES:
case Obj::MAGIC_WELL:
case Obj::HILL_FORT: case Obj::HILL_FORT:
return true; return true;
case Obj::BORDER_GATE: case Obj::BORDER_GATE:

View File

@@ -13,6 +13,7 @@
#include "../Engine/Nullkiller.h" #include "../Engine/Nullkiller.h"
#include "../../../CCallback.h" #include "../../../CCallback.h"
#include "../../../lib/mapObjects/MapObjects.h" #include "../../../lib/mapObjects/MapObjects.h"
#include "../../../lib/GameConstants.h"
namespace NKAI namespace NKAI
{ {
@@ -33,6 +34,45 @@ public:
} }
}; };
void ArmyUpgradeInfo::addArmyToBuy(std::vector<SlotInfo> army)
{
for(auto slot : army)
{
resultingArmy.push_back(slot);
upgradeValue += slot.power;
upgradeCost += slot.creature->getFullRecruitCost() * slot.count;
}
}
void ArmyUpgradeInfo::addArmyToGet(std::vector<SlotInfo> army)
{
for(auto slot : army)
{
resultingArmy.push_back(slot);
upgradeValue += slot.power;
}
}
std::vector<SlotInfo> ArmyManager::toSlotInfo(std::vector<creInfo> army) const
{
std::vector<SlotInfo> result;
for(auto i : army)
{
SlotInfo slot;
slot.creature = VLC->creh->objects[i.cre->getId()];
slot.count = i.count;
slot.power = evaluateStackPower(i.cre, i.count);
result.push_back(slot);
}
return result;
}
uint64_t ArmyManager::howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const uint64_t ArmyManager::howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const
{ {
return howManyReinforcementsCanGet(hero, hero, source); return howManyReinforcementsCanGet(hero, hero, source);
@@ -136,7 +176,7 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
{ {
if(vstd::contains(allowedFactions, slot.creature->getFaction())) if(vstd::contains(allowedFactions, slot.creature->getFaction()))
{ {
auto slotID = newArmyInstance.getSlotFor(slot.creature); auto slotID = newArmyInstance.getSlotFor(slot.creature->getId());
if(slotID.validSlot()) if(slotID.validSlot())
{ {
@@ -238,7 +278,8 @@ std::shared_ptr<CCreatureSet> ArmyManager::getArmyAvailableToBuyAsCCreatureSet(
ui64 ArmyManager::howManyReinforcementsCanBuy( ui64 ArmyManager::howManyReinforcementsCanBuy(
const CCreatureSet * targetArmy, const CCreatureSet * targetArmy,
const CGDwelling * dwelling, const CGDwelling * dwelling,
const TResources & availableResources) const const TResources & availableResources,
uint8_t turn) const
{ {
ui64 aivalue = 0; ui64 aivalue = 0;
auto army = getArmyAvailableToBuy(targetArmy, dwelling, availableResources); auto army = getArmyAvailableToBuy(targetArmy, dwelling, availableResources);
@@ -259,17 +300,29 @@ std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(const CCreatureSet * her
std::vector<creInfo> ArmyManager::getArmyAvailableToBuy( std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
const CCreatureSet * hero, const CCreatureSet * hero,
const CGDwelling * dwelling, const CGDwelling * dwelling,
TResources availableRes) const TResources availableRes,
uint8_t turn) const
{ {
std::vector<creInfo> creaturesInDwellings; std::vector<creInfo> creaturesInDwellings;
int freeHeroSlots = GameConstants::ARMY_SIZE - hero->stacksCount(); int freeHeroSlots = GameConstants::ARMY_SIZE - hero->stacksCount();
bool countGrowth = (cb->getDate(Date::DAY_OF_WEEK) + turn) > 7;
const CGTownInstance * town = dwelling->ID == Obj::TOWN
? dynamic_cast<const CGTownInstance *>(dwelling)
: nullptr;
for(int i = dwelling->creatures.size() - 1; i >= 0; i--) for(int i = dwelling->creatures.size() - 1; i >= 0; i--)
{ {
auto ci = infoFromDC(dwelling->creatures[i]); auto ci = infoFromDC(dwelling->creatures[i]);
if(!ci.count || ci.creID == -1) if(ci.creID == -1) continue;
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); SlotID dst = hero->getSlotFor(ci.creID);
if(!hero->hasStackAtSlot(dst)) //need another new slot for this stack if(!hero->hasStackAtSlot(dst)) //need another new slot for this stack
@@ -282,8 +335,7 @@ std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
vstd::amin(ci.count, availableRes / ci.cre->getFullRecruitCost()); //max count we can afford vstd::amin(ci.count, availableRes / ci.cre->getFullRecruitCost()); //max count we can afford
if(!ci.count) if(!ci.count) continue;
continue;
ci.level = i; //this is important for Dungeon Summoning Portal ci.level = i; //this is important for Dungeon Summoning Portal
creaturesInDwellings.push_back(ci); creaturesInDwellings.push_back(ci);
@@ -307,7 +359,7 @@ ui64 ArmyManager::howManyReinforcementsCanGet(const IBonusBearer * armyCarrier,
return newArmy > oldArmy ? newArmy - oldArmy : 0; return newArmy > oldArmy ? newArmy - oldArmy : 0;
} }
uint64_t ArmyManager::evaluateStackPower(const CCreature * creature, int count) const uint64_t ArmyManager::evaluateStackPower(const Creature * creature, int count) const
{ {
return creature->getAIValue() * count; return creature->getAIValue() * count;
} }

View File

@@ -34,6 +34,9 @@ struct ArmyUpgradeInfo
std::vector<SlotInfo> resultingArmy; std::vector<SlotInfo> resultingArmy;
uint64_t upgradeValue = 0; uint64_t upgradeValue = 0;
TResources upgradeCost; TResources upgradeCost;
void addArmyToBuy(std::vector<SlotInfo> army);
void addArmyToGet(std::vector<SlotInfo> army);
}; };
class DLL_EXPORT IArmyManager //: public: IAbstractManager class DLL_EXPORT IArmyManager //: public: IAbstractManager
@@ -45,20 +48,33 @@ public:
virtual ui64 howManyReinforcementsCanBuy( virtual ui64 howManyReinforcementsCanBuy(
const CCreatureSet * targetArmy, const CCreatureSet * targetArmy,
const CGDwelling * dwelling, 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 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> 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>::iterator getWeakestCreature(std::vector<SlotInfo> & army) const = 0;
virtual std::vector<SlotInfo> getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) 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<SlotInfo> toSlotInfo(std::vector<creInfo> creatures) const = 0;
virtual uint64_t evaluateStackPower(const CCreature * creature, int count) 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 Creature * creature, int count) const = 0;
virtual SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const = 0; virtual SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const = 0;
virtual ArmyUpgradeInfo calculateCreaturesUpgrade( virtual ArmyUpgradeInfo calculateCreaturesUpgrade(
const CCreatureSet * army, const CCreatureSet * army,
const CGObjectInstance * upgrader, const CGObjectInstance * upgrader,
const TResources & availableResources) const = 0; 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; virtual std::shared_ptr<CCreatureSet> getArmyAvailableToBuyAsCCreatureSet(const CGDwelling * dwelling, TResources availableRes) const = 0;
}; };
@@ -74,20 +90,30 @@ private:
public: public:
ArmyManager(CPlayerSpecificInfoCallback * CB, const Nullkiller * ai): cb(CB), ai(ai) {} ArmyManager(CPlayerSpecificInfoCallback * CB, const Nullkiller * ai): cb(CB), ai(ai) {}
void update() override; void update() override;
ui64 howManyReinforcementsCanBuy(const CCreatureSet * target, const CGDwelling * source) const override; ui64 howManyReinforcementsCanBuy(const CCreatureSet * target, const CGDwelling * source) const override;
ui64 howManyReinforcementsCanBuy( ui64 howManyReinforcementsCanBuy(
const CCreatureSet * targetArmy, const CCreatureSet * targetArmy,
const CGDwelling * dwelling, 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 CGHeroInstance * hero, const CCreatureSet * source) const override;
ui64 howManyReinforcementsCanGet(const IBonusBearer * armyCarrier, const CCreatureSet * target, 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> 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>::iterator getWeakestCreature(std::vector<SlotInfo> & army) const override;
std::vector<SlotInfo> getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) 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<SlotInfo> toSlotInfo(std::vector<creInfo> creatures) const override;
std::vector<creInfo> getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) 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; std::shared_ptr<CCreatureSet> getArmyAvailableToBuyAsCCreatureSet(const CGDwelling * dwelling, TResources availableRes) const override;
uint64_t evaluateStackPower(const CCreature * creature, int count) const override; uint64_t evaluateStackPower(const Creature * creature, int count) const override;
SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const override; SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const override;
ArmyUpgradeInfo calculateCreaturesUpgrade( ArmyUpgradeInfo calculateCreaturesUpgrade(
const CCreatureSet * army, const CCreatureSet * army,

View File

@@ -68,19 +68,22 @@ void BuildAnalyzer::updateOtherBuildings(TownDevelopmentInfo & developmentInfo)
logAi->trace("Checking other buildings"); logAi->trace("Checking other buildings");
std::vector<std::vector<BuildingID>> otherBuildings = { std::vector<std::vector<BuildingID>> otherBuildings = {
{BuildingID::TOWN_HALL, BuildingID::CITY_HALL, BuildingID::CAPITOL} {BuildingID::TOWN_HALL, BuildingID::CITY_HALL, BuildingID::CAPITOL},
{BuildingID::MAGES_GUILD_3, BuildingID::MAGES_GUILD_5}
}; };
if(developmentInfo.existingDwellings.size() >= 2 && ai->cb->getDate(Date::DAY_OF_WEEK) > boost::date_time::Friday) if(developmentInfo.existingDwellings.size() >= 2 && ai->cb->getDate(Date::DAY_OF_WEEK) > boost::date_time::Friday)
{ {
otherBuildings.push_back({BuildingID::CITADEL, BuildingID::CASTLE}); otherBuildings.push_back({BuildingID::CITADEL, BuildingID::CASTLE});
otherBuildings.push_back({BuildingID::HORDE_1});
otherBuildings.push_back({BuildingID::HORDE_2});
} }
for(auto & buildingSet : otherBuildings) for(auto & buildingSet : otherBuildings)
{ {
for(auto & buildingID : buildingSet) for(auto & buildingID : buildingSet)
{ {
if(!developmentInfo.town->hasBuilt(buildingID)) if(!developmentInfo.town->hasBuilt(buildingID) && developmentInfo.town->town->buildings.count(buildingID))
{ {
developmentInfo.addBuildingToBuild(getBuildingOrPrerequisite(developmentInfo.town, buildingID)); developmentInfo.addBuildingToBuild(getBuildingOrPrerequisite(developmentInfo.town, buildingID));
@@ -163,8 +166,8 @@ void BuildAnalyzer::update()
} }
else else
{ {
goldPreasure = ai->getLockedResources()[EGameResID::GOLD] / 10000.0f goldPreasure = ai->getLockedResources()[EGameResID::GOLD] / 5000.0f
+ (float)armyCost[EGameResID::GOLD] / (1 + ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f); + (float)armyCost[EGameResID::GOLD] / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f);
} }
logAi->trace("Gold preasure: %f", goldPreasure); logAi->trace("Gold preasure: %f", goldPreasure);
@@ -190,12 +193,28 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
const CCreature * creature = nullptr; const CCreature * creature = nullptr;
CreatureID baseCreatureID; CreatureID baseCreatureID;
int creatureLevel = -1;
int creatureUpgrade = 0;
if(BuildingID::DWELL_FIRST <= toBuild && toBuild <= BuildingID::DWELL_UP_LAST) if(BuildingID::DWELL_FIRST <= toBuild && toBuild <= BuildingID::DWELL_UP_LAST)
{ {
int level = toBuild - BuildingID::DWELL_FIRST; creatureLevel = (toBuild - BuildingID::DWELL_FIRST) % GameConstants::CREATURES_PER_TOWN;
auto creatures = townInfo->creatures.at(level % GameConstants::CREATURES_PER_TOWN); creatureUpgrade = (toBuild - BuildingID::DWELL_FIRST) / GameConstants::CREATURES_PER_TOWN;
auto creatureID = creatures.size() > level / GameConstants::CREATURES_PER_TOWN }
? creatures.at(level / GameConstants::CREATURES_PER_TOWN) else if(toBuild == BuildingID::HORDE_1 || toBuild == BuildingID::HORDE_1_UPGR)
{
creatureLevel = townInfo->hordeLvl.at(0);
}
else if(toBuild == BuildingID::HORDE_2 || toBuild == BuildingID::HORDE_2_UPGR)
{
creatureLevel = townInfo->hordeLvl.at(1);
}
if(creatureLevel >= 0)
{
auto creatures = townInfo->creatures.at(creatureLevel);
auto creatureID = creatures.size() > creatureUpgrade
? creatures.at(creatureUpgrade)
: creatures.front(); : creatures.front();
baseCreatureID = creatures.front(); baseCreatureID = creatures.front();
@@ -365,6 +384,8 @@ BuildingInfo::BuildingInfo(
creatureGrows = town->creatureGrowth(creatureLevel - 1); creatureGrows = town->creatureGrowth(creatureLevel - 1);
} }
else else
{
if(BuildingID::DWELL_FIRST <= id && id <= BuildingID::DWELL_UP_LAST)
{ {
creatureGrows = creature->getGrowth(); creatureGrows = creature->getGrowth();
@@ -373,6 +394,11 @@ BuildingInfo::BuildingInfo(
else if(town->hasBuilt(BuildingID::CITADEL)) else if(town->hasBuilt(BuildingID::CITADEL))
creatureGrows += creatureGrows / 2; creatureGrows += creatureGrows / 2;
} }
else
{
creatureGrows = creature->getHorde();
}
}
armyStrength = ai->armyManager->evaluateStackPower(creature, creatureGrows); armyStrength = ai->armyManager->evaluateStackPower(creature, creatureGrows);
armyCost = creatureCost * creatureGrows; armyCost = creatureCost * creatureGrows;

View File

@@ -17,20 +17,29 @@ namespace NKAI
HitMapInfo HitMapInfo::NoTreat; HitMapInfo HitMapInfo::NoTreat;
double HitMapInfo::value() const
{
return danger / std::sqrt(turn / 3.0f + 1);
}
void DangerHitMapAnalyzer::updateHitMap() void DangerHitMapAnalyzer::updateHitMap()
{ {
if(upToDate) if(hitMapUpToDate)
return; return;
logAi->trace("Update danger hitmap"); logAi->trace("Update danger hitmap");
upToDate = true; hitMapUpToDate = true;
auto start = std::chrono::high_resolution_clock::now(); auto start = std::chrono::high_resolution_clock::now();
auto cb = ai->cb.get(); auto cb = ai->cb.get();
auto mapSize = ai->cb->getMapSize(); auto mapSize = ai->cb->getMapSize();
if(hitMap.shape()[0] != mapSize.x || hitMap.shape()[1] != mapSize.y || hitMap.shape()[2] != mapSize.z)
hitMap.resize(boost::extents[mapSize.x][mapSize.y][mapSize.z]); hitMap.resize(boost::extents[mapSize.x][mapSize.y][mapSize.z]);
enemyHeroAccessibleObjects.clear(); enemyHeroAccessibleObjects.clear();
townTreats.clear();
std::map<PlayerColor, std::map<const CGHeroInstance *, HeroRole>> heroes; std::map<PlayerColor, std::map<const CGHeroInstance *, HeroRole>> heroes;
@@ -67,27 +76,26 @@ void DangerHitMapAnalyzer::updateHitMap()
if(path.getFirstBlockedAction()) if(path.getFirstBlockedAction())
continue; continue;
auto tileDanger = path.getHeroStrength();
auto turn = path.turn();
auto & node = hitMap[pos.x][pos.y][pos.z]; auto & node = hitMap[pos.x][pos.y][pos.z];
if(tileDanger / (turn / 3 + 1) > node.maximumDanger.danger / (node.maximumDanger.turn / 3 + 1) HitMapInfo newTreat;
|| (tileDanger == node.maximumDanger.danger && node.maximumDanger.turn > turn))
newTreat.hero = path.targetHero;
newTreat.turn = path.turn();
newTreat.danger = path.getHeroStrength();
if(newTreat.value() > node.maximumDanger.value())
{ {
node.maximumDanger.danger = tileDanger; node.maximumDanger = newTreat;
node.maximumDanger.turn = turn;
node.maximumDanger.hero = path.targetHero;
} }
if(turn < node.fastestDanger.turn if(newTreat.turn < node.fastestDanger.turn
|| (turn == node.fastestDanger.turn && node.fastestDanger.danger < tileDanger)) || (newTreat.turn == node.fastestDanger.turn && node.fastestDanger.danger < newTreat.danger))
{ {
node.fastestDanger.danger = tileDanger; node.fastestDanger = newTreat;
node.fastestDanger.turn = turn;
node.fastestDanger.hero = path.targetHero;
} }
if(turn == 0) if(newTreat.turn == 0)
{ {
auto objects = cb->getVisitableObjs(pos, false); auto objects = cb->getVisitableObjs(pos, false);
@@ -95,6 +103,26 @@ void DangerHitMapAnalyzer::updateHitMap()
{ {
if(cb->getPlayerRelations(obj->tempOwner, ai->playerID) != PlayerRelations::ENEMIES) if(cb->getPlayerRelations(obj->tempOwner, ai->playerID) != PlayerRelations::ENEMIES)
enemyHeroAccessibleObjects[path.targetHero].insert(obj); enemyHeroAccessibleObjects[path.targetHero].insert(obj);
if(obj->ID == Obj::TOWN && obj->getOwner() == ai->playerID)
{
auto & treats = townTreats[obj->id];
auto treat = std::find_if(treats.begin(), treats.end(), [&](const HitMapInfo & i) -> bool
{
return i.hero.hid == path.targetHero->id;
});
if(treat == treats.end())
{
treats.emplace_back();
treat = std::prev(treats.end(), 1);
}
if(newTreat.value() > treat->value())
{
*treat = newTreat;
}
}
} }
} }
} }
@@ -104,6 +132,122 @@ void DangerHitMapAnalyzer::updateHitMap()
logAi->trace("Danger hit map updated in %ld", timeElapsed(start)); 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();
if(hitMap.shape()[0] != mapSize.x || hitMap.shape()[1] != mapSize.y || hitMap.shape()[2] != mapSize.z)
hitMap.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 = 5;
auto addTownHero = [&](const CGTownInstance * town)
{
auto townHero = new CGHeroInstance();
CRandomGenerator rng;
auto visitablePos = town->visitablePos();
townHero->setOwner(ai->playerID); // lets avoid having multiple colors
townHero->initHero(rng, static_cast<HeroTypeID>(0));
townHero->pos = townHero->convertFromVisitablePos(visitablePos);
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;
const CGTownInstance * ourTown = nullptr;
for(AIPath & path : ai->pathfinder->getPathInfo(pos))
{
if(!path.targetHero || path.getFirstBlockedAction())
continue;
auto town = heroTownMap[path.targetHero];
if(town->getOwner() == ai->playerID)
{
if(ourDistance > path.movementCost())
{
ourDistance = path.movementCost();
ourTown = town;
}
}
else
{
if(enemyDistance > path.movementCost())
{
enemyDistance = path.movementCost();
enemyTown = town;
}
}
}
if(ourDistance == enemyDistance)
{
hitMap[pos.x][pos.y][pos.z].closestTown = nullptr;
}
else if(!enemyTown || ourDistance < enemyDistance)
{
hitMap[pos.x][pos.y][pos.z].closestTown = ourTown;
}
else
{
hitMap[pos.x][pos.y][pos.z].closestTown = enemyTown;
}
});
}
const std::vector<HitMapInfo> & DangerHitMapAnalyzer::getTownTreats(const CGTownInstance * town) const
{
static const std::vector<HitMapInfo> empty = {};
auto result = townTreats.find(town->id);
return result == townTreats.end() ? empty : result->second;
}
PlayerColor DangerHitMapAnalyzer::getTileOwner(const int3 & tile) const
{
auto town = hitMap[tile.x][tile.y][tile.z].closestTown;
return town ? town->getOwner() : PlayerColor::NEUTRAL;
}
const CGTownInstance * DangerHitMapAnalyzer::getClosestTown(const int3 & tile) const
{
return hitMap[tile.x][tile.y][tile.z].closestTown;
}
uint64_t DangerHitMapAnalyzer::enemyCanKillOurHeroesAlongThePath(const AIPath & path) const uint64_t DangerHitMapAnalyzer::enemyCanKillOurHeroesAlongThePath(const AIPath & path) const
{ {
int3 tile = path.targetTile(); int3 tile = path.targetTile();
@@ -144,7 +288,7 @@ const std::set<const CGObjectInstance *> & DangerHitMapAnalyzer::getOneTurnAcces
void DangerHitMapAnalyzer::reset() void DangerHitMapAnalyzer::reset()
{ {
upToDate = false; hitMapUpToDate = false;
} }
} }

View File

@@ -35,6 +35,8 @@ struct HitMapInfo
turn = 255; turn = 255;
hero = HeroPtr(); hero = HeroPtr();
} }
double value() const;
}; };
struct HitMapNode struct HitMapNode
@@ -42,6 +44,8 @@ struct HitMapNode
HitMapInfo maximumDanger; HitMapInfo maximumDanger;
HitMapInfo fastestDanger; HitMapInfo fastestDanger;
const CGTownInstance * closestTown = nullptr;
HitMapNode() = default; HitMapNode() = default;
void reset() void reset()
@@ -56,18 +60,25 @@ class DangerHitMapAnalyzer
private: private:
boost::multi_array<HitMapNode, 3> hitMap; boost::multi_array<HitMapNode, 3> hitMap;
std::map<const CGHeroInstance *, std::set<const CGObjectInstance *>> enemyHeroAccessibleObjects; std::map<const CGHeroInstance *, std::set<const CGObjectInstance *>> enemyHeroAccessibleObjects;
bool upToDate; bool hitMapUpToDate = false;
bool tileOwnersUpToDate = false;
const Nullkiller * ai; const Nullkiller * ai;
std::map<ObjectInstanceID, std::vector<HitMapInfo>> townTreats;
public: public:
DangerHitMapAnalyzer(const Nullkiller * ai) :ai(ai) {} DangerHitMapAnalyzer(const Nullkiller * ai) :ai(ai) {}
void updateHitMap(); void updateHitMap();
void calculateTileOwners();
uint64_t enemyCanKillOurHeroesAlongThePath(const AIPath & path) const; uint64_t enemyCanKillOurHeroesAlongThePath(const AIPath & path) const;
const HitMapNode & getObjectTreat(const CGObjectInstance * obj) const; const HitMapNode & getObjectTreat(const CGObjectInstance * obj) const;
const HitMapNode & getTileTreat(const int3 & tile) const; const HitMapNode & getTileTreat(const int3 & tile) const;
const std::set<const CGObjectInstance *> & getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const; const std::set<const CGObjectInstance *> & getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const;
void reset(); void reset();
void resetTileOwners() { tileOwnersUpToDate = false; }
PlayerColor getTileOwner(const int3 & tile) const;
const CGTownInstance * getClosestTown(const int3 & tile) const;
const std::vector<HitMapInfo> & getTownTreats(const CGTownInstance * town) const;
}; };
} }

View File

@@ -125,6 +125,7 @@ void HeroManager::update()
} }
std::sort(myHeroes.begin(), myHeroes.end(), scoreSort); std::sort(myHeroes.begin(), myHeroes.end(), scoreSort);
heroRoles.clear();
for(auto hero : myHeroes) for(auto hero : myHeroes)
{ {
@@ -180,6 +181,15 @@ float HeroManager::evaluateHero(const CGHeroInstance * hero) const
return evaluateFightingStrength(hero); 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 bool HeroManager::canRecruitHero(const CGTownInstance * town) const
{ {
if(!town) if(!town)
@@ -191,13 +201,7 @@ bool HeroManager::canRecruitHero(const CGTownInstance * town) const
if(cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST) if(cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST)
return false; return false;
const bool includeGarnisoned = true; if(heroCapReached())
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))
return false; return false;
if(!cb->getAvailableHeroes(town).size()) if(!cb->getAvailableHeroes(town).size())
@@ -225,6 +229,31 @@ const CGHeroInstance * HeroManager::findHeroWithGrail() const
return nullptr; return nullptr;
} }
const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) const
{
const CGHeroInstance * weakestHero = nullptr;
auto myHeroes = ai->cb->getHeroesInfo();
for(auto existingHero : myHeroes)
{
if(ai->getHeroLockedReason(existingHero) == HeroLockedReason::DEFENCE
|| existingHero->getArmyStrength() >armyLimit
|| getHeroRole(existingHero) == HeroRole::MAIN
|| existingHero->movementPointsRemaining()
|| existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1))
{
continue;
}
if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength())
{
weakestHero = existingHero;
}
}
return weakestHero;
}
SecondarySkillScoreMap::SecondarySkillScoreMap(std::map<SecondarySkill, float> scoreMap) SecondarySkillScoreMap::SecondarySkillScoreMap(std::map<SecondarySkill, float> scoreMap)
:scoreMap(scoreMap) :scoreMap(scoreMap)
{ {

View File

@@ -31,7 +31,9 @@ public:
virtual float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const = 0; virtual float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const = 0;
virtual float evaluateHero(const CGHeroInstance * hero) const = 0; virtual float evaluateHero(const CGHeroInstance * hero) const = 0;
virtual bool canRecruitHero(const CGTownInstance * t = nullptr) const = 0; virtual bool canRecruitHero(const CGTownInstance * t = nullptr) const = 0;
virtual bool heroCapReached() const = 0;
virtual const CGHeroInstance * findHeroWithGrail() const = 0; virtual const CGHeroInstance * findHeroWithGrail() const = 0;
virtual const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const = 0;
}; };
class DLL_EXPORT ISecondarySkillRule class DLL_EXPORT ISecondarySkillRule
@@ -71,7 +73,9 @@ public:
float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const override; float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const override;
float evaluateHero(const CGHeroInstance * hero) const override; float evaluateHero(const CGHeroInstance * hero) const override;
bool canRecruitHero(const CGTownInstance * t = nullptr) const override; bool canRecruitHero(const CGTownInstance * t = nullptr) const override;
bool heroCapReached() const override;
const CGHeroInstance * findHeroWithGrail() const override; const CGHeroInstance * findHeroWithGrail() const override;
const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const override;
private: private:
float evaluateFightingStrength(const CGHeroInstance * hero) const; float evaluateFightingStrength(const CGHeroInstance * hero) const;

View File

@@ -227,7 +227,12 @@ void ObjectClusterizer::clusterize()
auto obj = objs[i]; auto obj = objs[i];
if(!shouldVisitObject(obj)) if(!shouldVisitObject(obj))
return; {
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Skip object %s%s.", obj->getObjectName(), obj->visitablePos().toString());
#endif
continue;
}
#if NKAI_TRACE_LEVEL >= 2 #if NKAI_TRACE_LEVEL >= 2
logAi->trace("Check object %s%s.", obj->getObjectName(), obj->visitablePos().toString()); logAi->trace("Check object %s%s.", obj->getObjectName(), obj->visitablePos().toString());

View File

@@ -56,7 +56,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
tasks.reserve(paths.size()); tasks.reserve(paths.size());
const AIPath * closestWay = nullptr; std::unordered_map<HeroRole, const AIPath *> closestWaysByRole;
std::vector<ExecuteHeroChain *> waysToVisitObj; std::vector<ExecuteHeroChain *> waysToVisitObj;
for(auto & path : paths) for(auto & path : paths)
@@ -128,8 +128,9 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
auto heroRole = ai->nullkiller->heroManager->getHeroRole(path.targetHero); auto heroRole = ai->nullkiller->heroManager->getHeroRole(path.targetHero);
if(heroRole == HeroRole::SCOUT auto & closestWay = closestWaysByRole[heroRole];
&& (!closestWay || closestWay->movementCost() > path.movementCost()))
if(!closestWay || closestWay->movementCost() > path.movementCost())
{ {
closestWay = &path; 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 way->closestWayRatio
= closestWay->movementCost() / way->getPath().movementCost(); = closestWay->movementCost() / way->getPath().movementCost();
@@ -209,7 +213,7 @@ Goals::TGoalVec CaptureObjectsBehavior::decompose() const
{ {
captureObjects(ai->nullkiller->objectClusterizer->getNearbyObjects()); captureObjects(ai->nullkiller->objectClusterizer->getNearbyObjects());
if(tasks.empty() || ai->nullkiller->getScanDepth() == ScanDepth::FULL) if(tasks.empty() || ai->nullkiller->getScanDepth() != ScanDepth::SMALL)
captureObjects(ai->nullkiller->objectClusterizer->getFarObjects()); captureObjects(ai->nullkiller->objectClusterizer->getFarObjects());
} }

View File

@@ -49,20 +49,81 @@ Goals::TGoalVec DefenceBehavior::decompose() const
return tasks; return tasks;
} }
void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town) const bool isTreatUnderControl(const CGTownInstance * town, const HitMapInfo & treat, const std::vector<AIPath> & paths)
{ {
logAi->trace("Evaluating defence for %s", town->getNameTranslated());
auto treatNode = ai->nullkiller->dangerHitMap->getObjectTreat(town);
auto treats = { treatNode.maximumDanger, treatNode.fastestDanger };
int dayOfWeek = cb->getDate(Date::DAY_OF_WEEK); int dayOfWeek = cb->getDate(Date::DAY_OF_WEEK);
if(town->garrisonHero) for(const AIPath & path : paths)
{ {
if(!ai->nullkiller->isHeroLocked(town->garrisonHero.get())) bool treatIsWeak = path.getHeroStrength() / (float)treat.danger > TREAT_IGNORE_RATIO;
bool needToSaveGrowth = treat.turn == 0 && dayOfWeek == 7;
if(treatIsWeak && !needToSaveGrowth)
{ {
if(!town->visitingHero && cb->getHeroCount(ai->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER) if((path.exchangeCount == 1 && path.turn() < treat.turn)
|| path.turn() < treat.turn - 1
|| (path.turn() < treat.turn && treat.turn >= 2))
{
#if NKAI_TRACE_LEVEL >= 1
logAi->trace(
"Hero %s can eliminate danger for town %s using path %s.",
path.targetHero->getObjectName(),
town->getObjectName(),
path.toString());
#endif
return true;
}
}
}
return false;
}
void handleCounterAttack(
const CGTownInstance * town,
const HitMapInfo & treat,
const HitMapInfo & maximumDanger,
Goals::TGoalVec & tasks)
{
if(treat.hero.validAndSet()
&& treat.turn <= 1
&& (treat.danger == maximumDanger.danger || treat.turn < maximumDanger.turn))
{
auto heroCapturingPaths = ai->nullkiller->pathfinder->getPathInfo(treat.hero->visitablePos());
auto goals = CaptureObjectsBehavior::getVisitGoals(heroCapturingPaths, treat.hero.get());
for(int i = 0; i < heroCapturingPaths.size(); i++)
{
AIPath & path = heroCapturingPaths[i];
TSubgoal goal = goals[i];
if(!goal || goal->invalid() || !goal->isElementar()) continue;
Composition composition;
composition.addNext(DefendTown(town, treat, path, true)).addNext(goal);
tasks.push_back(Goals::sptr(composition));
}
}
}
bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoalVec & tasks)
{
if(ai->nullkiller->isHeroLocked(town->garrisonHero.get()))
{
logAi->trace(
"Hero %s in garrison of town %s is suposed to defend the town",
town->garrisonHero->getNameTranslated(),
town->getNameTranslated());
return true;
}
if(!town->visitingHero)
{
if(cb->getHeroCount(ai->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER)
{ {
logAi->trace( logAi->trace(
"Extracting hero %s from garrison of town %s", "Extracting hero %s from garrison of town %s",
@@ -71,15 +132,36 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5))); tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5)));
return; return true;
}
else if(ai->nullkiller->heroManager->getHeroRole(town->garrisonHero.get()) == HeroRole::MAIN)
{
auto armyDismissLimit = 1000;
auto heroToDismiss = ai->nullkiller->heroManager->findWeakHeroToDismiss(armyDismissLimit);
if(heroToDismiss)
{
tasks.push_back(Goals::sptr(Goals::DismissHero(heroToDismiss).setpriority(5)));
return true;
}
} }
} }
logAi->trace( return false;
"Hero %s in garrison of town %s is suposed to defend the town", }
town->garrisonHero->getNameTranslated(),
town->getNameTranslated());
void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town) const
{
logAi->trace("Evaluating defence for %s", town->getNameTranslated());
auto treatNode = ai->nullkiller->dangerHitMap->getObjectTreat(town);
std::vector<HitMapInfo> treats = ai->nullkiller->dangerHitMap->getTownTreats(town);
treats.push_back(treatNode.fastestDanger); // no guarantee that fastest danger will be there
if(town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks))
{
return; return;
} }
@@ -109,102 +191,14 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
std::to_string(treat.turn), std::to_string(treat.turn),
treat.hero->getNameTranslated()); treat.hero->getNameTranslated());
bool treatIsUnderControl = false; handleCounterAttack(town, treat, treatNode.maximumDanger, tasks);
for(AIPath & path : paths) if(isTreatUnderControl(town, treat, paths))
{ {
if(town->visitingHero && path.targetHero != town->visitingHero.get())
continue;
if(town->visitingHero && path.getHeroStrength() < town->visitingHero->getHeroStrength())
continue;
if(treat.hero.validAndSet()
&& treat.turn <= 1
&& (treat.danger == treatNode.maximumDanger.danger || treat.turn < treatNode.maximumDanger.turn)
&& isSafeToVisit(path.targetHero, path.heroArmy, treat.danger))
{
Composition composition;
composition.addNext(DefendTown(town, treat, path)).addNext(CaptureObject(treat.hero.get()));
tasks.push_back(Goals::sptr(composition));
}
bool treatIsWeak = path.getHeroStrength() / (float)treat.danger > TREAT_IGNORE_RATIO;
bool needToSaveGrowth = treat.turn == 0 && dayOfWeek == 7;
if(treatIsWeak && !needToSaveGrowth)
{
if((path.exchangeCount == 1 && path.turn() < treat.turn)
|| path.turn() < treat.turn - 1
|| (path.turn() < treat.turn && treat.turn >= 2))
{
#if NKAI_TRACE_LEVEL >= 1
logAi->trace(
"Hero %s can eliminate danger for town %s using path %s.",
path.targetHero->getObjectName(),
town->getObjectName(),
path.toString());
#endif
treatIsUnderControl = true;
break;
}
}
}
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; continue;
} }
else
{
const CGHeroInstance * weakestHero = nullptr;
for(auto existingHero : myHeroes) evaluateRecruitingHero(tasks, treat, town);
{
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)));
}
}
}
}
}
}
if(paths.empty()) if(paths.empty())
{ {
@@ -229,6 +223,22 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
path.movementCost(), path.movementCost(),
path.toString()); path.toString());
#endif #endif
auto townDefenseStrength = town->garrisonHero
? town->garrisonHero->getTotalStrength()
: (town->visitingHero ? town->visitingHero->getTotalStrength() : town->getUpperArmy()->getArmyStrength());
if(town->visitingHero && path.targetHero == town->visitingHero.get())
{
if(path.getHeroStrength() < townDefenseStrength)
continue;
}
else if(town->garrisonHero && path.targetHero == town->garrisonHero.get())
{
if(path.getHeroStrength() < townDefenseStrength)
continue;
}
if(path.turn() <= treat.turn - 2) if(path.turn() <= treat.turn - 2)
{ {
#if NKAI_TRACE_LEVEL >= 1 #if NKAI_TRACE_LEVEL >= 1
@@ -275,9 +285,11 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
tasks.push_back( tasks.push_back(
Goals::sptr(Composition() Goals::sptr(Composition()
.addNext(DefendTown(town, treat, path)) .addNext(DefendTown(town, treat, path))
.addNext(ExchangeSwapTownHeroes(town, town->visitingHero.get())) .addNextSequence({
.addNext(ExecuteHeroChain(path, town)) sptr(ExchangeSwapTownHeroes(town, town->visitingHero.get())),
.addNext(ExchangeSwapTownHeroes(town, path.targetHero, HeroLockedReason::DEFENCE)))); sptr(ExecuteHeroChain(path, town)),
sptr(ExchangeSwapTownHeroes(town, path.targetHero, HeroLockedReason::DEFENCE))
})));
continue; continue;
} }
@@ -313,15 +325,58 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
continue; continue;
} }
} }
Composition composition;
composition.addNext(DefendTown(town, treat, path));
TGoalVec sequence;
if(town->garrisonHero && path.targetHero == town->garrisonHero.get() && path.exchangeCount == 1)
{
composition.addNext(ExchangeSwapTownHeroes(town, town->garrisonHero.get(), HeroLockedReason::DEFENCE));
tasks.push_back(Goals::sptr(composition));
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Locking hero %s in garrison of %s",
town->garrisonHero.get()->getObjectName(),
town->getObjectName());
#endif
continue;
}
else 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 #if NKAI_TRACE_LEVEL >= 1
logAi->trace("Move %s to defend town %s", logAi->trace("Move %s to defend town %s",
path.targetHero->getObjectName(), path.targetHero->getObjectName(),
town->getObjectName()); town->getObjectName());
#endif #endif
Composition composition;
composition.addNext(DefendTown(town, treat, path)).addNext(ExecuteHeroChain(path, town)); sequence.push_back(sptr(ExecuteHeroChain(path, town)));
composition.addNextSequence(sequence);
auto firstBlockedAction = path.getFirstBlockedAction(); auto firstBlockedAction = path.getFirstBlockedAction();
if(firstBlockedAction) if(firstBlockedAction)
@@ -350,4 +405,70 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
logAi->debug("Found %d tasks", tasks.size()); 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())
{
heroToDismiss = ai->nullkiller->heroManager->findWeakHeroToDismiss(hero->getArmyStrength());
if(!heroToDismiss)
continue;
}
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 namespace NKAI
{ {
struct HitMapInfo;
namespace Goals namespace Goals
{ {
class DefenceBehavior : public CGoal<DefenceBehavior> class DefenceBehavior : public CGoal<DefenceBehavior>
{ {
public: public:
@@ -35,6 +39,7 @@ namespace Goals
private: private:
void evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town) const; void evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town) const;
void evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & treat, const CGTownInstance * town) const;
}; };
} }

View File

@@ -12,10 +12,13 @@
#include "../Engine/Nullkiller.h" #include "../Engine/Nullkiller.h"
#include "../Goals/ExecuteHeroChain.h" #include "../Goals/ExecuteHeroChain.h"
#include "../Goals/Composition.h" #include "../Goals/Composition.h"
#include "../Goals/RecruitHero.h"
#include "../Markers/HeroExchange.h" #include "../Markers/HeroExchange.h"
#include "../Markers/ArmyUpgrade.h" #include "../Markers/ArmyUpgrade.h"
#include "GatherArmyBehavior.h" #include "GatherArmyBehavior.h"
#include "CaptureObjectsBehavior.h"
#include "../AIUtility.h" #include "../AIUtility.h"
#include "../Goals/ExchangeSwapTownHeroes.h"
namespace NKAI namespace NKAI
{ {
@@ -78,20 +81,27 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
for(const AIPath & path : paths) for(const AIPath & path : paths)
{ {
#if NKAI_TRACE_LEVEL >= 2 #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 #endif
if(path.containsHero(hero)) continue; if(path.containsHero(hero))
if(path.turn() == 0 && hero->inTownGarrison)
{ {
#if NKAI_TRACE_LEVEL >= 1 #if NKAI_TRACE_LEVEL >= 2
logAi->trace("Skipping garnisoned hero %s, %s", hero->getObjectName(), pos.toString()); logAi->trace("Selfcontaining path. Ignore");
#endif #endif
continue; 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 #if NKAI_TRACE_LEVEL >= 2
logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength()); logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength());
@@ -109,10 +119,11 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
HeroExchange heroExchange(hero, path); HeroExchange heroExchange(hero, path);
float armyValue = (float)heroExchange.getReinforcementArmyStrength() / hero->getArmyStrength(); uint64_t armyValue = heroExchange.getReinforcementArmyStrength();
float armyRatio = (float)armyValue / hero->getArmyStrength();
// avoid transferring very small amount of army // avoid transferring very small amount of army
if(armyValue < 0.1f && armyValue < 20000) if((armyRatio < 0.1f && armyValue < 20000) || armyValue < 500)
{ {
#if NKAI_TRACE_LEVEL >= 2 #if NKAI_TRACE_LEVEL >= 2
logAi->trace("Army value is too small."); logAi->trace("Army value is too small.");
@@ -172,7 +183,21 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
exchangePath.closestWayRatio = 1; exchangePath.closestWayRatio = 1;
composition.addNext(heroExchange); composition.addNext(heroExchange);
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); composition.addNext(exchangePath);
}
auto blockedAction = path.getFirstBlockedAction(); auto blockedAction = path.getFirstBlockedAction();
@@ -212,18 +237,42 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
#endif #endif
auto paths = ai->nullkiller->pathfinder->getPathInfo(pos); auto paths = ai->nullkiller->pathfinder->getPathInfo(pos);
auto goals = CaptureObjectsBehavior::getVisitGoals(paths);
std::vector<std::shared_ptr<ExecuteHeroChain>> waysToVisitObj; std::vector<std::shared_ptr<ExecuteHeroChain>> waysToVisitObj;
#if NKAI_TRACE_LEVEL >= 1 #if NKAI_TRACE_LEVEL >= 1
logAi->trace("Found %d paths", paths.size()); logAi->trace("Found %d paths", paths.size());
#endif #endif
bool hasMainAround = false;
for(const AIPath & path : paths) for(const AIPath & path : paths)
{ {
auto heroRole = ai->nullkiller->heroManager->getHeroRole(path.targetHero);
if(heroRole == HeroRole::MAIN && path.turn() < SCOUT_TURN_DISTANCE_LIMIT)
hasMainAround = true;
}
for(int i = 0; i < paths.size(); i++)
{
auto & path = paths[i];
auto visitGoal = goals[i];
#if NKAI_TRACE_LEVEL >= 2 #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 #endif
if(upgrader->visitingHero && upgrader->visitingHero.get() != path.targetHero)
if(visitGoal->invalid())
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Ignore path. Not valid way.");
#endif
continue;
}
if(upgrader->visitingHero && (upgrader->visitingHero.get() != path.targetHero || path.exchangeCount == 1))
{ {
#if NKAI_TRACE_LEVEL >= 2 #if NKAI_TRACE_LEVEL >= 2
logAi->trace("Ignore path. Town has visiting hero."); logAi->trace("Ignore path. Town has visiting hero.");
@@ -261,18 +310,58 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
auto upgrade = ai->nullkiller->armyManager->calculateCreaturesUpgrade(path.heroArmy, upgrader, availableResources); auto upgrade = ai->nullkiller->armyManager->calculateCreaturesUpgrade(path.heroArmy, upgrader, availableResources);
if(!upgrader->garrisonHero && ai->nullkiller->heroManager->getHeroRole(path.targetHero) == HeroRole::MAIN) if(!upgrader->garrisonHero
&& (
hasMainAround
|| ai->nullkiller->heroManager->getHeroRole(path.targetHero) == HeroRole::MAIN))
{ {
upgrade.upgradeValue += ArmyUpgradeInfo armyToGetOrBuy;
ai->nullkiller->armyManager->howManyReinforcementsCanGet(
armyToGetOrBuy.addArmyToGet(
ai->nullkiller->armyManager->getBestArmy(
path.targetHero, path.targetHero,
path.heroArmy, path.heroArmy,
upgrader->getUpperArmy()); upgrader->getUpperArmy()));
armyToGetOrBuy.upgradeValue -= path.heroArmy->getArmyStrength();
armyToGetOrBuy.addArmyToBuy(
ai->nullkiller->armyManager->toSlotInfo(
ai->nullkiller->armyManager->getArmyAvailableToBuy(
path.heroArmy,
upgrader,
ai->nullkiller->getFreeResources(),
path.turn())));
upgrade.upgradeValue += armyToGetOrBuy.upgradeValue;
upgrade.upgradeCost += armyToGetOrBuy.upgradeCost;
vstd::concatenate(upgrade.resultingArmy, armyToGetOrBuy.resultingArmy);
if(!upgrade.upgradeValue
&& armyToGetOrBuy.upgradeValue > 20000
&& ai->nullkiller->heroManager->canRecruitHero(town)
&& path.turn() < SCOUT_TURN_DISTANCE_LIMIT)
{
for(auto hero : cb->getAvailableHeroes(town))
{
auto scoutReinforcement = ai->nullkiller->armyManager->howManyReinforcementsCanBuy(hero, town)
+ ai->nullkiller->armyManager->howManyReinforcementsCanGet(hero, town);
if(scoutReinforcement >= armyToGetOrBuy.upgradeValue
&& ai->nullkiller->getFreeGold() >20000
&& ai->nullkiller->buildAnalyzer->getGoldPreasure() < MAX_GOLD_PEASURE)
{
Composition recruitHero;
recruitHero.addNext(ArmyUpgrade(path.targetHero, town, armyToGetOrBuy)).addNext(RecruitHero(town, hero));
}
}
}
} }
auto armyValue = (float)upgrade.upgradeValue / path.getHeroStrength(); auto armyValue = (float)upgrade.upgradeValue / path.getHeroStrength();
if((armyValue < 0.1f && armyValue < 20000) || upgrade.upgradeValue < 300) // avoid small upgrades if((armyValue < 0.25f && upgrade.upgradeValue < 40000) || upgrade.upgradeValue < 2000) // avoid small upgrades
{ {
#if NKAI_TRACE_LEVEL >= 2 #if NKAI_TRACE_LEVEL >= 2
logAi->trace("Ignore path. Army value is too small (%f)", armyValue); logAi->trace("Ignore path. Army value is too small (%f)", armyValue);
@@ -297,11 +386,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
if(isSafe) if(isSafe)
{ {
ExecuteHeroChain newWay(path, upgrader); tasks.push_back(sptr(Composition().addNext(ArmyUpgrade(path, upgrader, upgrade)).addNext(visitGoal)));
newWay.closestWayRatio = 1;
tasks.push_back(sptr(Composition().addNext(ArmyUpgrade(path, upgrader, upgrade)).addNext(newWay)));
} }
} }

View File

@@ -66,6 +66,27 @@ Goals::TGoalVec RecruitHeroBehavior::decompose() const
} }
} }
int treasureSourcesCount = 0;
for(auto obj : ai->nullkiller->objectClusterizer->getNearbyObjects())
{
if((obj->ID == Obj::RESOURCE)
|| obj->ID == Obj::TREASURE_CHEST
|| obj->ID == Obj::CAMPFIRE
|| isWeeklyRevisitable(obj)
|| obj->ID ==Obj::ARTIFACT)
{
auto tile = obj->visitablePos();
auto closestTown = ai->nullkiller->dangerHitMap->getClosestTown(tile);
if(town == closestTown)
treasureSourcesCount++;
}
}
if(treasureSourcesCount < 5)
continue;
if(cb->getHeroesInfo().size() < cb->getTownsInfo().size() + 1 if(cb->getHeroesInfo().size() < cb->getTownsInfo().size() + 1
|| (ai->nullkiller->getFreeResources()[EGameResID::GOLD] > 10000 || (ai->nullkiller->getFreeResources()[EGameResID::GOLD] > 10000
&& ai->nullkiller->buildAnalyzer->getGoldPreasure() < MAX_GOLD_PEASURE)) && ai->nullkiller->buildAnalyzer->getGoldPreasure() < MAX_GOLD_PEASURE))

View File

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

View File

@@ -150,17 +150,15 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj)
case Obj::MINE: case Obj::MINE:
case Obj::ABANDONED_MINE: case Obj::ABANDONED_MINE:
case Obj::PANDORAS_BOX: case Obj::PANDORAS_BOX:
{
const CArmedInstance * a = dynamic_cast<const CArmedInstance *>(obj);
return a->getArmyStrength();
}
case Obj::CRYPT: //crypt case Obj::CRYPT: //crypt
case Obj::CREATURE_BANK: //crebank case Obj::CREATURE_BANK: //crebank
case Obj::DRAGON_UTOPIA: case Obj::DRAGON_UTOPIA:
case Obj::SHIPWRECK: //shipwreck case Obj::SHIPWRECK: //shipwreck
case Obj::DERELICT_SHIP: //derelict ship case Obj::DERELICT_SHIP: //derelict ship
// case Obj::PYRAMID: {
return estimateBankDanger(dynamic_cast<const CBank *>(obj)); const CArmedInstance * a = dynamic_cast<const CArmedInstance *>(obj);
return a->getArmyStrength();
}
case Obj::PYRAMID: case Obj::PYRAMID:
{ {
if(obj->subID == 0) if(obj->subID == 0)

View File

@@ -61,6 +61,7 @@ void Nullkiller::init(std::shared_ptr<CCallback> cb, PlayerColor playerID)
armyManager.reset(new ArmyManager(cb.get(), this)); armyManager.reset(new ArmyManager(cb.get(), this));
heroManager.reset(new HeroManager(cb.get(), this)); heroManager.reset(new HeroManager(cb.get(), this));
decomposer.reset(new DeepDecomposer()); decomposer.reset(new DeepDecomposer());
armyFormation.reset(new ArmyFormation(cb, this));
} }
Goals::TTask Nullkiller::choseBestTask(Goals::TTaskVec & tasks) const Goals::TTask Nullkiller::choseBestTask(Goals::TTaskVec & tasks) const
@@ -117,7 +118,7 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TSubgoal behavior, int decompositi
void Nullkiller::resetAiState() void Nullkiller::resetAiState()
{ {
lockedResources = TResources(); lockedResources = TResources();
scanDepth = ScanDepth::FULL; scanDepth = ScanDepth::MAIN_FULL;
playerID = ai->playerID; playerID = ai->playerID;
lockedHeroes.clear(); lockedHeroes.clear();
dangerHitMap->reset(); dangerHitMap->reset();
@@ -133,10 +134,14 @@ void Nullkiller::updateAiState(int pass, bool fast)
activeHero = nullptr; activeHero = nullptr;
setTargetObject(-1); setTargetObject(-1);
decomposer->reset();
buildAnalyzer->update();
if(!fast) if(!fast)
{ {
memory->removeInvisibleObjects(cb.get()); memory->removeInvisibleObjects(cb.get());
dangerHitMap->calculateTileOwners();
dangerHitMap->updateHitMap(); dangerHitMap->updateHitMap();
boost::this_thread::interruption_point(); boost::this_thread::interruption_point();
@@ -156,11 +161,15 @@ void Nullkiller::updateAiState(int pass, bool fast)
PathfinderSettings cfg; PathfinderSettings cfg;
cfg.useHeroChain = useHeroChain; cfg.useHeroChain = useHeroChain;
cfg.scoutTurnDistanceLimit = SCOUT_TURN_DISTANCE_LIMIT;
if(scanDepth != ScanDepth::FULL) if(scanDepth == ScanDepth::SMALL)
{ {
cfg.mainTurnDistanceLimit = MAIN_TURN_DISTANCE_LIMIT * ((int)scanDepth + 1); cfg.mainTurnDistanceLimit = MAIN_TURN_DISTANCE_LIMIT;
}
if(scanDepth != ScanDepth::ALL_FULL)
{
cfg.scoutTurnDistanceLimit = SCOUT_TURN_DISTANCE_LIMIT;
} }
boost::this_thread::interruption_point(); boost::this_thread::interruption_point();
@@ -173,8 +182,6 @@ void Nullkiller::updateAiState(int pass, bool fast)
} }
armyManager->update(); armyManager->update();
buildAnalyzer->update();
decomposer->reset();
logAi->debug("AI state updated in %ld", timeElapsed(start)); logAi->debug("AI state updated in %ld", timeElapsed(start));
} }
@@ -222,7 +229,7 @@ void Nullkiller::makeTurn()
boost::lock_guard<boost::mutex> sharedStorageLock(AISharedStorage::locker); boost::lock_guard<boost::mutex> sharedStorageLock(AISharedStorage::locker);
const int MAX_DEPTH = 10; const int MAX_DEPTH = 10;
const float FAST_TASK_MINIMAL_PRIORITY = 0.7; const float FAST_TASK_MINIMAL_PRIORITY = 0.7f;
resetAiState(); resetAiState();
@@ -232,7 +239,7 @@ void Nullkiller::makeTurn()
Goals::TTask bestTask = taskptr(Goals::Invalid()); Goals::TTask bestTask = taskptr(Goals::Invalid());
do for(;i <= MAXPASS; i++)
{ {
Goals::TTaskVec fastTasks = { Goals::TTaskVec fastTasks = {
choseBestTask(sptr(BuyArmyBehavior()), 1), choseBestTask(sptr(BuyArmyBehavior()), 1),
@@ -246,7 +253,11 @@ void Nullkiller::makeTurn()
executeTask(bestTask); executeTask(bestTask);
updateAiState(i, true); updateAiState(i, true);
} }
} while(bestTask->priority >= FAST_TASK_MINIMAL_PRIORITY); else
{
break;
}
}
Goals::TTaskVec bestTasks = { Goals::TTaskVec bestTasks = {
bestTask, bestTask,
@@ -265,7 +276,6 @@ void Nullkiller::makeTurn()
bestTask = choseBestTask(bestTasks); bestTask = choseBestTask(bestTasks);
HeroPtr hero = bestTask->getHero(); HeroPtr hero = bestTask->getHero();
HeroRole heroRole = HeroRole::MAIN; HeroRole heroRole = HeroRole::MAIN;
if(hero.validAndSet()) if(hero.validAndSet())
@@ -274,20 +284,39 @@ void Nullkiller::makeTurn()
if(heroRole != HeroRole::MAIN || bestTask->getHeroExchangeCount() <= 1) if(heroRole != HeroRole::MAIN || bestTask->getHeroExchangeCount() <= 1)
useHeroChain = false; useHeroChain = false;
// TODO: better to check turn distance here instead of priority
if((heroRole != HeroRole::MAIN || bestTask->priority < SMALL_SCAN_MIN_PRIORITY) if((heroRole != HeroRole::MAIN || bestTask->priority < SMALL_SCAN_MIN_PRIORITY)
&& scanDepth == ScanDepth::FULL) && scanDepth == ScanDepth::MAIN_FULL)
{ {
useHeroChain = false; useHeroChain = false;
scanDepth = ScanDepth::SMALL; scanDepth = ScanDepth::SMALL;
logAi->trace( logAi->trace(
"Goal %s has too low priority %f so increasing scan depth", "Goal %s has low priority %f so decreasing scan depth to gain performance.",
bestTask->toString(), bestTask->toString(),
bestTask->priority); bestTask->priority);
} }
if(bestTask->priority < MIN_PRIORITY) if(bestTask->priority < MIN_PRIORITY)
{ {
auto heroes = cb->getHeroesInfo();
auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool
{
return h->movementPointsRemaining() > 100;
});
if(hasMp && scanDepth != ScanDepth::ALL_FULL)
{
logAi->trace(
"Goal %s has too low priority %f so increasing scan depth to full.",
bestTask->toString(),
bestTask->priority);
scanDepth = ScanDepth::ALL_FULL;
useHeroChain = false;
continue;
}
logAi->trace("Goal %s has too low priority. It is not worth doing it. Ending turn.", bestTask->toString()); logAi->trace("Goal %s has too low priority. It is not worth doing it. Ending turn.", bestTask->toString());
return; return;

View File

@@ -18,6 +18,7 @@
#include "../Analyzers/ArmyManager.h" #include "../Analyzers/ArmyManager.h"
#include "../Analyzers/HeroManager.h" #include "../Analyzers/HeroManager.h"
#include "../Analyzers/ObjectClusterizer.h" #include "../Analyzers/ObjectClusterizer.h"
#include "../Helpers/ArmyFormation.h"
namespace NKAI namespace NKAI
{ {
@@ -39,9 +40,11 @@ enum class HeroLockedReason
enum class ScanDepth enum class ScanDepth
{ {
FULL = 0, MAIN_FULL = 0,
SMALL = 1 SMALL = 1,
ALL_FULL = 2
}; };
class Nullkiller class Nullkiller
@@ -67,6 +70,7 @@ public:
std::unique_ptr<AIMemory> memory; std::unique_ptr<AIMemory> memory;
std::unique_ptr<FuzzyHelper> dangerEvaluator; std::unique_ptr<FuzzyHelper> dangerEvaluator;
std::unique_ptr<DeepDecomposer> decomposer; std::unique_ptr<DeepDecomposer> decomposer;
std::unique_ptr<ArmyFormation> armyFormation;
PlayerColor playerID; PlayerColor playerID;
std::shared_ptr<CCallback> cb; std::shared_ptr<CCallback> cb;

View File

@@ -23,6 +23,7 @@
#include "../Goals/ExecuteHeroChain.h" #include "../Goals/ExecuteHeroChain.h"
#include "../Goals/BuildThis.h" #include "../Goals/BuildThis.h"
#include "../Goals/ExchangeSwapTownHeroes.h" #include "../Goals/ExchangeSwapTownHeroes.h"
#include "../Goals/DismissHero.h"
#include "../Markers/UnlockCluster.h" #include "../Markers/UnlockCluster.h"
#include "../Markers/HeroExchange.h" #include "../Markers/HeroExchange.h"
#include "../Markers/ArmyUpgrade.h" #include "../Markers/ArmyUpgrade.h"
@@ -33,6 +34,7 @@ namespace NKAI
#define MIN_AI_STRENGHT (0.5f) //lower when combat AI gets smarter #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 #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) EvaluationContext::EvaluationContext(const Nullkiller * ai)
: movementCost(0.0), : movementCost(0.0),
@@ -49,10 +51,16 @@ EvaluationContext::EvaluationContext(const Nullkiller * ai)
turn(0), turn(0),
strategicalValue(0), strategicalValue(0),
evaluator(ai), evaluator(ai),
enemyHeroDangerRatio(0) enemyHeroDangerRatio(0),
armyGrowth(0)
{ {
} }
void EvaluationContext::addNonCriticalStrategicalValue(float value)
{
vstd::amax(strategicalValue, std::min(value, MIN_CRITICAL_VALUE));
}
PriorityEvaluator::~PriorityEvaluator() PriorityEvaluator::~PriorityEvaluator()
{ {
delete engine; delete engine;
@@ -64,6 +72,7 @@ void PriorityEvaluator::initVisitTile()
std::string str = std::string((char *)file.first.get(), file.second); std::string str = std::string((char *)file.first.get(), file.second);
engine = fl::FllImporter().fromString(str); engine = fl::FllImporter().fromString(str);
armyLossPersentageVariable = engine->getInputVariable("armyLoss"); armyLossPersentageVariable = engine->getInputVariable("armyLoss");
armyGrowthVariable = engine->getInputVariable("armyGrowth");
heroRoleVariable = engine->getInputVariable("heroRole"); heroRoleVariable = engine->getInputVariable("heroRole");
dangerVariable = engine->getInputVariable("danger"); dangerVariable = engine->getInputVariable("danger");
turnVariable = engine->getInputVariable("turn"); turnVariable = engine->getInputVariable("turn");
@@ -99,7 +108,8 @@ int32_t estimateTownIncome(CCallback * cb, const CGObjectInstance * target, cons
auto town = cb->getTown(target->id); auto town = cb->getTown(target->id);
auto fortLevel = town->fortLevel(); auto fortLevel = town->fortLevel();
if(town->hasCapitol()) return booster * 2000; if(town->hasCapitol())
return booster * 2000;
// probably well developed town will have city hall // probably well developed town will have city hall
if(fortLevel == CGTownInstance::CASTLE) return booster * 750; if(fortLevel == CGTownInstance::CASTLE) return booster * 750;
@@ -153,18 +163,18 @@ uint64_t getCreatureBankArmyReward(const CGObjectInstance * target, const CGHero
{ {
result += (c.data.type->getAIValue() * c.data.count) * c.chance; result += (c.data.type->getAIValue() * c.data.count) * c.chance;
} }
else /*else
{ {
//we will need to discard the weakest stack //we will need to discard the weakest stack
result += (c.data.type->getAIValue() * c.data.count - weakestStackPower) * c.chance; result += (c.data.type->getAIValue() * c.data.count - weakestStackPower) * c.chance;
} }*/
} }
result /= 100; //divide by total chance result /= 100; //divide by total chance
return result; return result;
} }
uint64_t getDwellingScore(CCallback * cb, const CGObjectInstance * target, bool checkGold) uint64_t getDwellingArmyValue(CCallback * cb, const CGObjectInstance * target, bool checkGold)
{ {
auto dwelling = dynamic_cast<const CGDwelling *>(target); auto dwelling = dynamic_cast<const CGDwelling *>(target);
uint64_t score = 0; uint64_t score = 0;
@@ -185,6 +195,27 @@ uint64_t getDwellingScore(CCallback * cb, const CGObjectInstance * target, bool
return score; return score;
} }
uint64_t getDwellingArmyGrowth(CCallback * cb, const CGObjectInstance * target, PlayerColor myColor)
{
auto dwelling = dynamic_cast<const CGDwelling *>(target);
uint64_t score = 0;
if(dwelling->getOwner() == myColor)
return 0;
for(auto & creLevel : dwelling->creatures)
{
if(creLevel.second.size())
{
auto creature = creLevel.second.back().toCreature();
score += creature->getAIValue() * creature->getGrowth();
}
}
return score;
}
int getDwellingArmyCost(const CGObjectInstance * target) int getDwellingArmyCost(const CGObjectInstance * target)
{ {
auto dwelling = dynamic_cast<const CGDwelling *>(target); auto dwelling = dynamic_cast<const CGDwelling *>(target);
@@ -247,23 +278,13 @@ uint64_t RewardEvaluator::getArmyReward(
{ {
const float enemyArmyEliminationRewardRatio = 0.5f; const float enemyArmyEliminationRewardRatio = 0.5f;
auto relations = ai->cb->getPlayerRelations(target->tempOwner, ai->playerID);
if(!target) if(!target)
return 0; return 0;
switch(target->ID) switch(target->ID)
{ {
case Obj::TOWN:
{
auto town = dynamic_cast<const CGTownInstance *>(target);
auto fortLevel = town->fortLevel();
auto booster = isAnotherAi(town, *ai->cb) ? 1 : 2;
if(fortLevel < CGTownInstance::CITADEL)
return town->hasFort() ? booster * 500 : 0;
else
return booster * (fortLevel == CGTownInstance::CASTLE ? 5000 : 2000);
}
case Obj::HILL_FORT: case Obj::HILL_FORT:
return ai->armyManager->calculateCreaturesUpgrade(army, target, ai->cb->getResourceAmount()).upgradeValue; return ai->armyManager->calculateCreaturesUpgrade(army, target, ai->cb->getResourceAmount()).upgradeValue;
case Obj::CREATURE_BANK: case Obj::CREATURE_BANK:
@@ -272,7 +293,7 @@ uint64_t RewardEvaluator::getArmyReward(
case Obj::CREATURE_GENERATOR2: case Obj::CREATURE_GENERATOR2:
case Obj::CREATURE_GENERATOR3: case Obj::CREATURE_GENERATOR3:
case Obj::CREATURE_GENERATOR4: case Obj::CREATURE_GENERATOR4:
return getDwellingScore(ai->cb.get(), target, checkGold); return getDwellingArmyValue(ai->cb.get(), target, checkGold);
case Obj::CRYPT: case Obj::CRYPT:
case Obj::SHIPWRECK: case Obj::SHIPWRECK:
case Obj::SHIPWRECK_SURVIVOR: case Obj::SHIPWRECK_SURVIVOR:
@@ -283,7 +304,7 @@ uint64_t RewardEvaluator::getArmyReward(
case Obj::DRAGON_UTOPIA: case Obj::DRAGON_UTOPIA:
return 10000; return 10000;
case Obj::HERO: case Obj::HERO:
return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES return relations == PlayerRelations::ENEMIES
? enemyArmyEliminationRewardRatio * dynamic_cast<const CGHeroInstance *>(target)->getArmyStrength() ? enemyArmyEliminationRewardRatio * dynamic_cast<const CGHeroInstance *>(target)->getArmyStrength()
: 0; : 0;
case Obj::PANDORAS_BOX: case Obj::PANDORAS_BOX:
@@ -293,6 +314,47 @@ uint64_t RewardEvaluator::getArmyReward(
} }
} }
uint64_t RewardEvaluator::getArmyGrowth(
const CGObjectInstance * target,
const CGHeroInstance * hero,
const CCreatureSet * army) const
{
if(!target)
return 0;
auto relations = ai->cb->getPlayerRelations(target->tempOwner, hero->tempOwner);
if(relations != PlayerRelations::ENEMIES)
return 0;
switch(target->ID)
{
case Obj::TOWN:
{
auto town = dynamic_cast<const CGTownInstance *>(target);
auto fortLevel = town->fortLevel();
auto neutral = !town->getOwner().isValidPlayer();
auto booster = isAnotherAi(town, *ai->cb) || neutral ? 1 : 2;
if(fortLevel < CGTownInstance::CITADEL)
return town->hasFort() ? booster * 500 : 0;
else
return booster * (fortLevel == CGTownInstance::CASTLE ? 5000 : 2000);
}
case Obj::CREATURE_GENERATOR1:
case Obj::CREATURE_GENERATOR2:
case Obj::CREATURE_GENERATOR3:
case Obj::CREATURE_GENERATOR4:
return getDwellingArmyGrowth(ai->cb.get(), target, hero->getOwner());
case Obj::ARTIFACT:
// it is not supported now because hero will not sit in town on 7th day but later parts of legion may be counted as army growth as well.
return 0;
default:
return 0;
}
}
int RewardEvaluator::getGoldCost(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army) const int RewardEvaluator::getGoldCost(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army) const
{ {
if(!target) if(!target)
@@ -338,7 +400,7 @@ float RewardEvaluator::getEnemyHeroStrategicalValue(const CGHeroInstance * enemy
2. The formula quickly approaches 1.0 as hero level increases, 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 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 float RewardEvaluator::getResourceRequirementStrength(int resType) const
@@ -366,10 +428,26 @@ float RewardEvaluator::getTotalResourceRequirementStrength(int resType) const
return 0; return 0;
float ratio = dailyIncome[resType] == 0 float ratio = dailyIncome[resType] == 0
? (float)requiredResources[resType] / 50.0f ? (float)requiredResources[resType] / 10.0f
: (float)requiredResources[resType] / dailyIncome[resType] / 50.0f; : (float)requiredResources[resType] / dailyIncome[resType] / 20.0f;
return std::min(ratio, 1.0f); return std::min(ratio, 2.0f);
}
uint64_t RewardEvaluator::townArmyGrowth(const CGTownInstance * town) const
{
uint64_t result = 0;
for(auto creatureInfo : town->creatures)
{
if(creatureInfo.second.empty())
continue;
auto creature = creatureInfo.second.back().toCreature();
result += creature->getAIValue() * town->getGrowthInfo(creature->getLevel() - 1).totalGrowth();
}
return result;
} }
float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) const float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) const
@@ -407,18 +485,28 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons
case Obj::TOWN: case Obj::TOWN:
{ {
if(ai->buildAnalyzer->getDevelopmentInfo().empty()) if(ai->buildAnalyzer->getDevelopmentInfo().empty())
return 1; return 10.0f;
auto town = dynamic_cast<const CGTownInstance *>(target); auto town = dynamic_cast<const CGTownInstance *>(target);
auto fortLevel = town->fortLevel();
auto booster = isAnotherAi(town, *ai->cb) ? 0.3 : 1;
if(town->hasCapitol()) return 1; if(town->getOwner() == ai->playerID)
{
auto armyIncome = townArmyGrowth(town);
auto dailyIncome = town->dailyIncome()[EGameResID::GOLD];
return std::min(1.0f, std::sqrt(armyIncome / 40000.0f)) + std::min(0.3f, dailyIncome / 10000.0f);
}
auto fortLevel = town->fortLevel();
auto booster = isAnotherAi(town, *ai->cb) ? 0.4f : 1.0f;
if(town->hasCapitol())
return booster * 1.5;
if(fortLevel < CGTownInstance::CITADEL) if(fortLevel < CGTownInstance::CITADEL)
return booster * (town->hasFort() ? 0.6 : 0.4); return booster * (town->hasFort() ? 1.0 : 0.8);
else else
return booster * (fortLevel == CGTownInstance::CASTLE ? 0.9 : 0.8); return booster * (fortLevel == CGTownInstance::CASTLE ? 1.4 : 1.2);
} }
case Obj::HERO: case Obj::HERO:
@@ -463,15 +551,18 @@ float RewardEvaluator::getSkillReward(const CGObjectInstance * target, const CGH
case Obj::GARDEN_OF_REVELATION: case Obj::GARDEN_OF_REVELATION:
case Obj::MARLETTO_TOWER: case Obj::MARLETTO_TOWER:
case Obj::MERCENARY_CAMP: case Obj::MERCENARY_CAMP:
case Obj::SHRINE_OF_MAGIC_GESTURE:
case Obj::SHRINE_OF_MAGIC_INCANTATION:
case Obj::TREE_OF_KNOWLEDGE: case Obj::TREE_OF_KNOWLEDGE:
return 1; return 1;
case Obj::LEARNING_STONE: case Obj::LEARNING_STONE:
return 1.0f / std::sqrt(hero->level); return 1.0f / std::sqrt(hero->level);
case Obj::ARENA: case Obj::ARENA:
case Obj::SHRINE_OF_MAGIC_THOUGHT:
return 2; return 2;
case Obj::SHRINE_OF_MAGIC_INCANTATION:
return 0.2f;
case Obj::SHRINE_OF_MAGIC_GESTURE:
return 0.3f;
case Obj::SHRINE_OF_MAGIC_THOUGHT:
return 0.5f;
case Obj::LIBRARY_OF_ENLIGHTENMENT: case Obj::LIBRARY_OF_ENLIGHTENMENT:
return 8; return 8;
case Obj::WITCH_HUT: case Obj::WITCH_HUT:
@@ -513,12 +604,13 @@ int32_t getArmyCost(const CArmedInstance * army)
return value; return value;
} }
/// Gets aproximated reward in gold. Daily income is multiplied by 5
int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CGHeroInstance * hero) const int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CGHeroInstance * hero) const
{ {
if(!target) if(!target)
return 0; return 0;
auto relations = ai->cb->getPlayerRelations(target->tempOwner, hero->tempOwner);
const int dailyIncomeMultiplier = 5; const int dailyIncomeMultiplier = 5;
const float enemyArmyEliminationGoldRewardRatio = 0.2f; const float enemyArmyEliminationGoldRewardRatio = 0.2f;
const int32_t heroEliminationBonus = GameConstants::HERO_GOLD_COST / 2; const int32_t heroEliminationBonus = GameConstants::HERO_GOLD_COST / 2;
@@ -559,7 +651,7 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG
//Objectively saves us 2500 to hire hero //Objectively saves us 2500 to hire hero
return GameConstants::HERO_GOLD_COST; return GameConstants::HERO_GOLD_COST;
case Obj::HERO: case Obj::HERO:
return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES return relations == PlayerRelations::ENEMIES
? heroEliminationBonus + enemyArmyEliminationGoldRewardRatio * getArmyCost(dynamic_cast<const CGHeroInstance *>(target)) ? heroEliminationBonus + enemyArmyEliminationGoldRewardRatio * getArmyCost(dynamic_cast<const CGHeroInstance *>(target))
: 0; : 0;
default: default:
@@ -579,7 +671,8 @@ public:
uint64_t armyStrength = heroExchange.getReinforcementArmyStrength(); 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());
} }
}; };
@@ -596,7 +689,7 @@ public:
uint64_t upgradeValue = armyUpgrade.getUpgradeValue(); uint64_t upgradeValue = armyUpgrade.getUpgradeValue();
evaluationContext.armyReward += upgradeValue; evaluationContext.armyReward += upgradeValue;
evaluationContext.strategicalValue += upgradeValue / (float)armyUpgrade.hero->getArmyStrength(); evaluationContext.addNonCriticalStrategicalValue(upgradeValue / (float)armyUpgrade.hero->getArmyStrength());
} }
}; };
@@ -621,23 +714,6 @@ void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uin
class DefendTownEvaluator : public IEvaluationContextBuilder class DefendTownEvaluator : public IEvaluationContextBuilder
{ {
private:
uint64_t townArmyIncome(const CGTownInstance * town) const
{
uint64_t result = 0;
for(auto creatureInfo : town->creatures)
{
if(creatureInfo.second.empty())
continue;
auto creature = creatureInfo.second.back().toCreature();
result += creature->getAIValue() * town->getGrowthInfo(creature->getLevel() - 1).totalGrowth();
}
return result;
}
public: public:
virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
{ {
@@ -648,22 +724,34 @@ public:
const CGTownInstance * town = defendTown.town; const CGTownInstance * town = defendTown.town;
auto & treat = defendTown.getTreat(); auto & treat = defendTown.getTreat();
auto armyIncome = townArmyIncome(town); auto strategicalValue = evaluationContext.evaluator.getStrategicalValue(town);
auto dailyIncome = town->dailyIncome()[EGameResID::GOLD];
auto strategicalValue = std::sqrt(armyIncome / 20000.0f) + dailyIncome / 3000.0f;
if(evaluationContext.evaluator.ai->buildAnalyzer->getDevelopmentInfo().size() == 1)
strategicalValue = 1;
float multiplier = 1; float multiplier = 1;
if(treat.turn < defendTown.getTurn()) if(treat.turn < defendTown.getTurn())
multiplier /= 1 + (defendTown.getTurn() - treat.turn); multiplier /= 1 + (defendTown.getTurn() - treat.turn);
evaluationContext.armyReward += armyIncome * multiplier; multiplier /= 1.0f + treat.turn / 5.0f;
if(defendTown.getTurn() > 0 && defendTown.isCounterAttack())
{
auto ourSpeed = defendTown.hero->movementPointsLimit(true);
auto enemySpeed = treat.hero->movementPointsLimit(true);
if(enemySpeed > ourSpeed) multiplier *= 0.7f;
}
auto dailyIncome = town->dailyIncome()[EGameResID::GOLD];
auto armyGrowth = evaluationContext.evaluator.townArmyGrowth(town);
evaluationContext.armyGrowth += armyGrowth * multiplier;
evaluationContext.goldReward += dailyIncome * 5 * multiplier; evaluationContext.goldReward += dailyIncome * 5 * multiplier;
evaluationContext.strategicalValue += strategicalValue * multiplier;
if(evaluationContext.evaluator.ai->buildAnalyzer->getDevelopmentInfo().size() == 1)
vstd::amax(evaluationContext.strategicalValue, 2.5f * multiplier * strategicalValue);
else
evaluationContext.addNonCriticalStrategicalValue(1.7f * multiplier * strategicalValue);
vstd::amax(evaluationContext.danger, defendTown.getTreat().danger); vstd::amax(evaluationContext.danger, defendTown.getTreat().danger);
addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength()); addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength());
} }
@@ -709,18 +797,22 @@ public:
auto army = path.heroArmy; auto army = path.heroArmy;
const CGObjectInstance * target = ai->cb->getObj((ObjectInstanceID)task->objid, false); const CGObjectInstance * target = ai->cb->getObj((ObjectInstanceID)task->objid, false);
auto heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroPtr);
if (target && ai->cb->getPlayerRelations(target->tempOwner, hero->tempOwner) == PlayerRelations::ENEMIES) if(heroRole == HeroRole::MAIN)
evaluationContext.heroRole = heroRole;
if (target)
{ {
evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero); evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero);
evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold); evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold);
evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, evaluationContext.heroRole); evaluationContext.armyGrowth += evaluationContext.evaluator.getArmyGrowth(target, hero, army);
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); evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army);
} }
vstd::amax(evaluationContext.armyLossPersentage, path.getTotalArmyLoss() / (double)path.getHeroStrength()); 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()); addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength());
vstd::amax(evaluationContext.turn, path.turn()); vstd::amax(evaluationContext.turn, path.turn());
} }
@@ -760,7 +852,7 @@ public:
evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero) / boost; evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero) / boost;
evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold) / boost; evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold) / boost;
evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, role) / 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.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army) / boost;
evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost; evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost;
evaluationContext.movementCost += objInfo.second.movementCost / boost; evaluationContext.movementCost += objInfo.second.movementCost / boost;
@@ -798,6 +890,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->movementPointsRemaining();
evaluationContext.movementCost += mpLeft;
evaluationContext.movementCostByRole[role] += mpLeft;
evaluationContext.goldCost += GameConstants::HERO_GOLD_COST + getArmyCost(dismissedHero);
}
};
class BuildThisEvaluationContextBuilder : public IEvaluationContextBuilder class BuildThisEvaluationContextBuilder : public IEvaluationContextBuilder
{ {
public: public:
@@ -813,39 +930,47 @@ public:
evaluationContext.heroRole = HeroRole::MAIN; evaluationContext.heroRole = HeroRole::MAIN;
evaluationContext.movementCostByRole[evaluationContext.heroRole] += bi.prerequisitesCount; evaluationContext.movementCostByRole[evaluationContext.heroRole] += bi.prerequisitesCount;
evaluationContext.goldCost += bi.buildCostWithPrerequisits[EGameResID::GOLD]; evaluationContext.goldCost += bi.buildCostWithPrerequisits[EGameResID::GOLD];
evaluationContext.closestWayRatio = 1;
if(bi.creatureID != CreatureID::NONE) if(bi.creatureID != CreatureID::NONE)
{ {
evaluationContext.strategicalValue += buildThis.townInfo.armyStrength / 50000.0; evaluationContext.addNonCriticalStrategicalValue(buildThis.townInfo.armyStrength / 50000.0);
if(bi.baseCreatureID == bi.creatureID) 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; evaluationContext.armyReward += bi.armyStrength;
} }
else else
{ {
auto potentialUpgradeValue = evaluationContext.evaluator.getUpgradeArmyReward(buildThis.town, bi); 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; evaluationContext.armyReward += potentialUpgradeValue / (float)bi.prerequisitesCount;
} }
} }
else if(bi.id == BuildingID::CITADEL || bi.id == BuildingID::CASTLE) 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; evaluationContext.armyReward += buildThis.townInfo.armyStrength / 2;
} }
else else if(bi.id >= BuildingID::MAGES_GUILD_1 && bi.id <= BuildingID::MAGES_GUILD_5)
{
evaluationContext.skillReward += 2 * (bi.id - BuildingID::MAGES_GUILD_1);
}
if(evaluationContext.goldReward)
{ {
auto goldPreasure = evaluationContext.evaluator.ai->buildAnalyzer->getGoldPreasure(); 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) if(bi.notEnoughRes && bi.prerequisitesCount == 1)
{ {
evaluationContext.strategicalValue /= 2; evaluationContext.strategicalValue /= 3;
evaluationContext.movementCostByRole[evaluationContext.heroRole] += 5;
evaluationContext.turn += 5;
} }
} }
}; };
@@ -872,6 +997,7 @@ PriorityEvaluator::PriorityEvaluator(const Nullkiller * ai)
evaluationContextBuilders.push_back(std::make_shared<ArmyUpgradeEvaluator>()); evaluationContextBuilders.push_back(std::make_shared<ArmyUpgradeEvaluator>());
evaluationContextBuilders.push_back(std::make_shared<DefendTownEvaluator>()); evaluationContextBuilders.push_back(std::make_shared<DefendTownEvaluator>());
evaluationContextBuilders.push_back(std::make_shared<ExchangeSwapTownHeroesContextBuilder>()); evaluationContextBuilders.push_back(std::make_shared<ExchangeSwapTownHeroesContextBuilder>());
evaluationContextBuilders.push_back(std::make_shared<DismissHeroContextBuilder>(ai));
} }
EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal) const EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal) const
@@ -910,6 +1036,8 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
+ (evaluationContext.skillReward > 0 ? 1 : 0) + (evaluationContext.skillReward > 0 ? 1 : 0)
+ (evaluationContext.strategicalValue > 0 ? 1 : 0); + (evaluationContext.strategicalValue > 0 ? 1 : 0);
float goldRewardPerTurn = evaluationContext.goldReward / std::log2f(2 + evaluationContext.movementCost * 10);
double result = 0; double result = 0;
try try
@@ -918,8 +1046,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
heroRoleVariable->setValue(evaluationContext.heroRole); heroRoleVariable->setValue(evaluationContext.heroRole);
mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]); mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]);
scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]); scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]);
goldRewardVariable->setValue(evaluationContext.goldReward); goldRewardVariable->setValue(goldRewardPerTurn);
armyRewardVariable->setValue(evaluationContext.armyReward); armyRewardVariable->setValue(evaluationContext.armyReward);
armyGrowthVariable->setValue(evaluationContext.armyGrowth);
skillRewardVariable->setValue(evaluationContext.skillReward); skillRewardVariable->setValue(evaluationContext.skillReward);
dangerVariable->setValue(evaluationContext.danger); dangerVariable->setValue(evaluationContext.danger);
rewardTypeVariable->setValue(rewardType); rewardTypeVariable->setValue(rewardType);
@@ -940,13 +1069,13 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
} }
#if NKAI_TRACE_LEVEL >= 2 #if NKAI_TRACE_LEVEL >= 2
logAi->trace("Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %d, cost: %d, army gain: %d, danger: %d, role: %s, strategical value: %f, cwr: %f, fear: %f, result %f", logAi->trace("Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %d, danger: %d, role: %s, strategical value: %f, cwr: %f, fear: %f, result %f",
task->toString(), task->toString(),
evaluationContext.armyLossPersentage, evaluationContext.armyLossPersentage,
(int)evaluationContext.turn, (int)evaluationContext.turn,
evaluationContext.movementCostByRole[HeroRole::MAIN], evaluationContext.movementCostByRole[HeroRole::MAIN],
evaluationContext.movementCostByRole[HeroRole::SCOUT], evaluationContext.movementCostByRole[HeroRole::SCOUT],
evaluationContext.goldReward, goldRewardPerTurn,
evaluationContext.goldCost, evaluationContext.goldCost,
evaluationContext.armyReward, evaluationContext.armyReward,
evaluationContext.danger, evaluationContext.danger,

View File

@@ -33,6 +33,7 @@ public:
RewardEvaluator(const Nullkiller * ai) : ai(ai) {} RewardEvaluator(const Nullkiller * ai) : ai(ai) {}
uint64_t getArmyReward(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army, bool checkGold) const; uint64_t getArmyReward(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army, bool checkGold) const;
uint64_t getArmyGrowth(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army) const;
int getGoldCost(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army) const; int getGoldCost(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army) const;
float getEnemyHeroStrategicalValue(const CGHeroInstance * enemy) const; float getEnemyHeroStrategicalValue(const CGHeroInstance * enemy) const;
float getResourceRequirementStrength(int resType) const; float getResourceRequirementStrength(int resType) const;
@@ -43,6 +44,7 @@ public:
int32_t getGoldReward(const CGObjectInstance * target, const CGHeroInstance * hero) const; int32_t getGoldReward(const CGObjectInstance * target, const CGHeroInstance * hero) const;
uint64_t getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const; uint64_t getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const;
const HitMapInfo & getEnemyHeroDanger(const int3 & tile, uint8_t turn) const; const HitMapInfo & getEnemyHeroDanger(const int3 & tile, uint8_t turn) const;
uint64_t townArmyGrowth(const CGTownInstance * town) const;
}; };
struct DLL_EXPORT EvaluationContext struct DLL_EXPORT EvaluationContext
@@ -54,6 +56,7 @@ struct DLL_EXPORT EvaluationContext
float closestWayRatio; float closestWayRatio;
float armyLossPersentage; float armyLossPersentage;
float armyReward; float armyReward;
uint64_t armyGrowth;
int32_t goldReward; int32_t goldReward;
int32_t goldCost; int32_t goldCost;
float skillReward; float skillReward;
@@ -64,6 +67,8 @@ struct DLL_EXPORT EvaluationContext
float enemyHeroDangerRatio; float enemyHeroDangerRatio;
EvaluationContext(const Nullkiller * ai); EvaluationContext(const Nullkiller * ai);
void addNonCriticalStrategicalValue(float value);
}; };
class IEvaluationContextBuilder class IEvaluationContextBuilder
@@ -95,6 +100,7 @@ private:
fl::InputVariable * turnVariable; fl::InputVariable * turnVariable;
fl::InputVariable * goldRewardVariable; fl::InputVariable * goldRewardVariable;
fl::InputVariable * armyRewardVariable; fl::InputVariable * armyRewardVariable;
fl::InputVariable * armyGrowthVariable;
fl::InputVariable * dangerVariable; fl::InputVariable * dangerVariable;
fl::InputVariable * skillRewardVariable; fl::InputVariable * skillRewardVariable;
fl::InputVariable * strategicalValueVariable; fl::InputVariable * strategicalValueVariable;

View File

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

View File

@@ -31,9 +31,17 @@ std::string Composition::toString() const
{ {
std::string result = "Composition"; 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; return result;
@@ -41,17 +49,34 @@ std::string Composition::toString() const
void Composition::accept(AIGateway * ai) 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 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) Composition & Composition::addNext(TSubgoal goal)
@@ -64,20 +89,35 @@ Composition & Composition::addNext(TSubgoal goal)
} }
else else
{ {
subtasks.push_back(goal); subtasks.push_back({goal});
} }
return *this; return *this;
} }
Composition & Composition::addNext(const AbstractGoal & goal)
{
return addNext(sptr(goal));
}
bool Composition::isElementar() const bool Composition::isElementar() const
{ {
return subtasks.back()->isElementar(); return subtasks.back().front()->isElementar();
} }
int Composition::getHeroExchangeCount() const 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> class DLL_EXPORT Composition : public ElementarGoal<Composition>
{ {
private: private:
TGoalVec subtasks; std::vector<TGoalVec> subtasks; // things we want to do now
public: public:
Composition() Composition()
@@ -26,16 +26,12 @@ namespace Goals
{ {
} }
Composition(TGoalVec subtasks)
: ElementarGoal(Goals::COMPOSITION), subtasks(subtasks)
{
}
virtual bool operator==(const Composition & other) const override; virtual bool operator==(const Composition & other) const override;
virtual std::string toString() const override; virtual std::string toString() const override;
void accept(AIGateway * ai) override; void accept(AIGateway * ai) override;
Composition & addNext(const AbstractGoal & goal); Composition & addNext(const AbstractGoal & goal);
Composition & addNext(TSubgoal goal); Composition & addNext(TSubgoal goal);
Composition & addNextSequence(const TGoalVec & taskSequence);
virtual TGoalVec decompose() const override; virtual TGoalVec decompose() const override;
virtual bool isElementar() const override; virtual bool isElementar() const override;
virtual int getHeroExchangeCount() 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->setActive(chainPath.targetHero, tile);
ai->nullkiller->setTargetObject(objid); 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; std::set<int> blockedIndexes;
for(int i = chainPath.nodes.size() - 1; i >= 0; i--) for(int i = chainPath.nodes.size() - 1; i >= 0; i--)

View File

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

View File

@@ -22,18 +22,20 @@ namespace Goals
{ {
class DLL_EXPORT RecruitHero : public ElementarGoal<RecruitHero> class DLL_EXPORT RecruitHero : public ElementarGoal<RecruitHero>
{ {
private:
const CGHeroInstance * heroToBuy;
public: public:
RecruitHero(const CGTownInstance * townWithTavern, const CGHeroInstance * heroToBuy) 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) RecruitHero(const CGTownInstance * townWithTavern)
: ElementarGoal(Goals::RECRUIT_HERO) : RecruitHero(townWithTavern, nullptr)
{ {
priority = 1;
town = townWithTavern;
} }
virtual bool operator==(const RecruitHero & other) const override 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,38 @@
/*
* 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
public:
ArmyFormation(std::shared_ptr<CCallback> CB, const Nullkiller * ai): cb(CB) {}
void rearrangeArmyForSiege(const CGTownInstance * town, const CGHeroInstance * attacker);
};
}

View File

@@ -28,6 +28,13 @@ ArmyUpgrade::ArmyUpgrade(const AIPath & upgradePath, const CGObjectInstance * up
sethero(upgradePath.targetHero); sethero(upgradePath.targetHero);
} }
ArmyUpgrade::ArmyUpgrade(const CGHeroInstance * targetMain, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade)
: CGoal(Goals::ARMY_UPGRADE), upgrader(upgrader), upgradeValue(upgrade.upgradeValue),
initialValue(targetMain->getArmyStrength()), goldCost(upgrade.upgradeCost[EGameResID::GOLD])
{
sethero(targetMain);
}
bool ArmyUpgrade::operator==(const ArmyUpgrade & other) const bool ArmyUpgrade::operator==(const ArmyUpgrade & other) const
{ {
return false; return false;

View File

@@ -27,6 +27,7 @@ namespace Goals
public: public:
ArmyUpgrade(const AIPath & upgradePath, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade); ArmyUpgrade(const AIPath & upgradePath, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade);
ArmyUpgrade(const CGHeroInstance * targetMain, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade);
virtual bool operator==(const ArmyUpgrade & other) const override; virtual bool operator==(const ArmyUpgrade & other) const override;
virtual std::string toString() const override; virtual std::string toString() const override;

View File

@@ -18,8 +18,8 @@ namespace NKAI
using namespace Goals; using namespace Goals;
DefendTown::DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath) DefendTown::DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath, bool isCounterAttack)
: CGoal(Goals::DEFEND_TOWN), treat(treat), defenceArmyStrength(defencePath.getHeroStrength()), turn(defencePath.turn()) : CGoal(Goals::DEFEND_TOWN), treat(treat), defenceArmyStrength(defencePath.getHeroStrength()), turn(defencePath.turn()), counterattack(isCounterAttack)
{ {
settown(town); settown(town);
sethero(defencePath.targetHero); sethero(defencePath.targetHero);

View File

@@ -24,9 +24,10 @@ namespace Goals
uint64_t defenceArmyStrength; uint64_t defenceArmyStrength;
HitMapInfo treat; HitMapInfo treat;
uint8_t turn; uint8_t turn;
bool counterattack;
public: public:
DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath); DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath, bool isCounterAttack = false);
DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const CGHeroInstance * defender); DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const CGHeroInstance * defender);
virtual bool operator==(const DefendTown & other) const override; virtual bool operator==(const DefendTown & other) const override;
@@ -37,6 +38,8 @@ namespace Goals
uint64_t getDefenceStrength() const { return defenceArmyStrength; } uint64_t getDefenceStrength() const { return defenceArmyStrength; }
uint8_t getTurn() const { return turn; } uint8_t getTurn() const { return turn; }
bool isCounterAttack() { return counterattack; }
}; };
} }

View File

@@ -29,7 +29,7 @@ bool HeroExchange::operator==(const HeroExchange & other) const
std::string HeroExchange::toString() 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 uint64_t HeroExchange::getReinforcementArmyStrength() const

View File

@@ -879,8 +879,12 @@ void AINodeStorage::setHeroes(std::map<const CGHeroInstance *, HeroRole> heroes)
for(auto & hero : heroes) for(auto & hero : heroes)
{ {
// do not allow our own heroes in garrison to act on map // 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) || ai->heroManager->heroCapReached()))
{
continue; continue;
}
uint64_t mask = FirstActorMask << actors.size(); uint64_t mask = FirstActorMask << actors.size();
auto actor = std::make_shared<HeroActor>(hero.first, hero.second, mask, ai); auto actor = std::make_shared<HeroActor>(hero.first, hero.second, mask, ai);

View File

@@ -24,8 +24,8 @@
namespace NKAI namespace NKAI
{ {
const int SCOUT_TURN_DISTANCE_LIMIT = 3; const int SCOUT_TURN_DISTANCE_LIMIT = 5;
const int MAIN_TURN_DISTANCE_LIMIT = 5; const int MAIN_TURN_DISTANCE_LIMIT = 10;
namespace AIPathfinding namespace AIPathfinding
{ {
@@ -258,7 +258,7 @@ public:
{ {
double ratio = (double)danger / (armyValue * hero->getFightingStrength()); double ratio = (double)danger / (armyValue * hero->getFightingStrength());
return (uint64_t)(armyValue * ratio * ratio * ratio); return (uint64_t)(armyValue * ratio * ratio);
} }
STRONG_INLINE STRONG_INLINE

View File

@@ -61,6 +61,11 @@ void AIPathfinder::updatePaths(std::map<const CGHeroInstance *, HeroRole> heroes
storage->setScoutTurnDistanceLimit(pathfinderSettings.scoutTurnDistanceLimit); storage->setScoutTurnDistanceLimit(pathfinderSettings.scoutTurnDistanceLimit);
storage->setMainTurnDistanceLimit(pathfinderSettings.mainTurnDistanceLimit); storage->setMainTurnDistanceLimit(pathfinderSettings.mainTurnDistanceLimit);
logAi->trace(
"Scout turn distance: %s, main %s",
std::to_string(pathfinderSettings.scoutTurnDistanceLimit),
std::to_string(pathfinderSettings.mainTurnDistanceLimit));
if(pathfinderSettings.useHeroChain) if(pathfinderSettings.useHeroChain)
{ {
storage->setTownsAndDwellings(cb->getTownsInfo(), ai->memory->visitableObjs); storage->setTownsAndDwellings(cb->getTownsInfo(), ai->memory->visitableObjs);

View File

@@ -134,6 +134,7 @@ void ChainActor::setBaseActor(HeroActor * base)
armyCost = base->armyCost; armyCost = base->armyCost;
actorAction = base->actorAction; actorAction = base->actorAction;
tiCache = base->tiCache; tiCache = base->tiCache;
actorExchangeCount = base->actorExchangeCount;
} }
void HeroActor::setupSpecialActors() void HeroActor::setupSpecialActors()

View File

@@ -5,10 +5,10 @@ InputVariable: mainTurnDistance
enabled: true enabled: true
range: 0.000 10.000 range: 0.000 10.000
lock-range: true lock-range: true
term: LOWEST Ramp 0.250 0.000 term: LOWEST Ramp 0.400 0.000
term: LOW Discrete 0.000 1.000 0.500 0.800 1.000 0.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 1.000 1.000 3.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 1.000 0.000 1.500 0.200 3.000 0.800 10.000 1.000 term: LONG Discrete 2.000 0.000 6.000 1.000 10.000 0.800
InputVariable: scoutTurnDistance InputVariable: scoutTurnDistance
description: distance to tile in turns description: distance to tile in turns
enabled: true enabled: true
@@ -23,11 +23,11 @@ InputVariable: goldReward
enabled: true enabled: true
range: 0.000 5000.000 range: 0.000 5000.000
lock-range: true lock-range: true
term: LOW Triangle 10.000 500.000 2000.000 term: LOWEST Triangle 0.000 100.000 200.000
term: MEDIUM Triangle 500.000 2000.000 5000.000 term: SMALL Triangle 100.000 200.000 400.000
term: HIGH Ramp 2000.000 5000.000 term: MEDIUM Triangle 200.000 400.000 1000.000
term: NONE Ramp 100.000 0.000 term: BIG Triangle 400.000 1000.000 5000.000
term: LOWEST Triangle 0.000 100.000 500.000 term: HUGE Ramp 1000.000 5000.000
InputVariable: armyReward InputVariable: armyReward
enabled: true enabled: true
range: 0.000 10000.000 range: 0.000 10000.000
@@ -43,6 +43,7 @@ InputVariable: armyLoss
term: LOW Ramp 0.200 0.000 term: LOW Ramp 0.200 0.000
term: MEDIUM Triangle 0.000 0.200 0.500 term: MEDIUM Triangle 0.000 0.200 0.500
term: HIGH Ramp 0.200 0.500 term: HIGH Ramp 0.200 0.500
term: ALL Ramp 0.700 1.000
InputVariable: heroRole InputVariable: heroRole
enabled: true enabled: true
range: -0.100 1.100 range: -0.100 1.100
@@ -82,20 +83,21 @@ InputVariable: closestHeroRatio
InputVariable: strategicalValue InputVariable: strategicalValue
description: Some abstract long term benefit non gold or army or skill description: Some abstract long term benefit non gold or army or skill
enabled: true enabled: true
range: 0.000 1.000 range: 0.000 3.000
lock-range: false lock-range: false
term: NONE Ramp 0.200 0.000 term: NONE Ramp 0.200 0.000
term: LOWEST Triangle 0.000 0.010 0.250 term: LOWEST Triangle 0.000 0.010 0.250
term: LOW Triangle 0.000 0.250 0.700 term: LOW Triangle 0.000 0.250 1.000
term: MEDIUM Triangle 0.250 0.700 1.000 term: MEDIUM Triangle 0.250 1.000 2.000
term: HIGH Ramp 0.700 1.000 term: HIGH Triangle 1.000 2.000 3.000
term: CRITICAL Ramp 2.000 3.000
InputVariable: goldPreasure InputVariable: goldPreasure
description: Ratio between weekly army cost and gold income description: Ratio between weekly army cost and gold income
enabled: true enabled: true
range: 0.000 1.000 range: 0.000 1.000
lock-range: false lock-range: false
term: LOW Ramp 0.300 0.000 term: LOW Ramp 0.300 0.000
term: HIGH Discrete 0.100 0.000 0.250 0.100 0.300 0.200 0.400 0.700 1.000 1.000 term: HIGH Discrete 0.100 0.000 0.250 0.200 0.300 0.300 0.400 0.700 1.000 1.000
InputVariable: goldCost InputVariable: goldCost
description: Action cost in gold description: Action cost in gold
enabled: true enabled: true
@@ -121,106 +123,154 @@ InputVariable: fear
term: LOW Triangle 0.000 0.500 1.000 term: LOW Triangle 0.000 0.500 1.000
term: MEDIUM Triangle 0.500 1.000 1.500 term: MEDIUM Triangle 0.500 1.000 1.500
term: HIGH Ramp 1.000 1.800 term: HIGH Ramp 1.000 1.800
InputVariable: armyGrowth
enabled: true
range: 0.000 20000.000
lock-range: false
term: NONE Ramp 100.000 0.000
term: SMALL Triangle 0.000 1000.000 3000.000
term: MEDIUM Triangle 1000.000 3000.000 8000.000
term: BIG Triangle 3000.000 8000.000 20000.000
term: HUGE Ramp 8000.000 20000.000
OutputVariable: Value OutputVariable: Value
enabled: true enabled: true
range: -0.500 1.500 range: -1.500 2.500
lock-range: false lock-range: false
aggregation: AlgebraicSum aggregation: AlgebraicSum
defuzzifier: Centroid 100 defuzzifier: Centroid 100
default: 0.500 default: 0.500
lock-previous: false lock-previous: false
term: LOWEST Discrete -0.500 0.000 -0.500 1.000 -0.200 1.000 -0.200 0.000 0.200 0.000 0.200 1.000 0.500 1.000 0.500 0.000 0.500 term: WORST Binary -1.000 -inf 0.700
term: BITLOW Rectangle -0.010 0.010 0.500 term: BAD Rectangle -1.000 -0.700 0.500
term: LOW Discrete -0.150 0.000 -0.150 1.000 -0.050 1.000 -0.050 0.000 0.050 0.000 0.050 1.000 0.150 1.000 0.150 0.000 0.500 term: BASE Rectangle -0.200 0.200 0.350
term: MEDIUM Triangle 0.450 0.500 0.550 0.050 term: MEDIUM Rectangle 0.910 1.090 0.500
term: HIGH Discrete 0.850 0.000 0.850 1.000 0.950 1.000 0.950 0.000 1.050 0.000 1.050 1.000 1.150 1.000 1.150 0.000 0.500 term: SMALL Rectangle 0.960 1.040 0.600
term: HIGHEST Discrete 0.500 0.000 0.500 1.000 0.800 1.000 0.800 0.000 1.200 0.000 1.200 1.000 1.500 1.000 1.500 0.000 0.500 term: BITHIGH Rectangle 0.850 1.150 0.400
term: BITHIGH Rectangle 0.990 1.010 0.500 term: HIGH Rectangle 0.750 1.250 0.400
RuleBlock: gold reward term: HIGHEST Rectangle 0.500 1.500 0.350
term: CRITICAL Ramp 0.500 2.000 0.500
RuleBlock: basic
enabled: true enabled: true
conjunction: AlgebraicProduct conjunction: AlgebraicProduct
disjunction: AlgebraicSum disjunction: AlgebraicSum
implication: AlgebraicProduct implication: AlgebraicProduct
activation: General activation: General
rule: if turn is NOW and mainTurnDistance is very LONG and heroRole is SCOUT then Value is LOW with 0.5 rule: if heroRole is MAIN then Value is BASE
rule: if turn is NOW and mainTurnDistance is LONG and heroRole is SCOUT then Value is LOW with 0.3 rule: if heroRole is SCOUT then Value is BASE
rule: if turn is NOW and scoutTurnDistance is LONG and heroRole is SCOUT then Value is LOW with 0.3 rule: if heroRole is MAIN and armyGrowth is HUGE and fear is not HIGH then Value is HIGH
rule: if turn is NOW and mainTurnDistance is LONG and heroRole is MAIN then Value is LOW with 0.3 rule: if heroRole is MAIN and armyGrowth is BIG and mainTurnDistance is LOW then Value is HIGH
rule: if turn is NEXT and mainTurnDistance is very LONG and heroRole is SCOUT then Value is LOW with 0.8 rule: if heroRole is MAIN and armyGrowth is BIG and mainTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH
rule: if turn is NEXT and scoutTurnDistance is LONG and heroRole is SCOUT then Value is BITLOW rule: if heroRole is MAIN and armyGrowth is BIG and mainTurnDistance is LONG and fear is not HIGH then Value is MEDIUM
rule: if turn is NEXT and mainTurnDistance is LONG and heroRole is MAIN then Value is LOW with 0.3 rule: if armyLoss is ALL then Value is WORST
rule: if turn is NEXT and mainTurnDistance is LONG and heroRole is SCOUT then Value is BITLOW with 0.3 rule: if turn is not NOW then Value is BAD with 0.1
rule: if turn is FUTURE and scoutTurnDistance is very LONG and heroRole is SCOUT then Value is LOWEST with 0.3 rule: if closestHeroRatio is LOWEST and heroRole is SCOUT then Value is WORST
rule: if turn is FUTURE and mainTurnDistance is very LONG and heroRole is SCOUT then Value is LOWEST with 0.5 rule: if closestHeroRatio is LOW and heroRole is SCOUT then Value is BAD
rule: if turn is FUTURE and mainTurnDistance is very LONG and heroRole is MAIN and strategicalValue is NONE then Value is LOWEST with 0.5 rule: if closestHeroRatio is LOWEST and heroRole is MAIN then Value is BAD
rule: if turn is FUTURE and mainTurnDistance is very LONG and heroRole is MAIN and strategicalValue is LOW then Value is LOWEST with 0.3 rule: if heroRole is SCOUT and turn is NOW and mainTurnDistance is LONG then Value is WORST
rule: if turn is FUTURE and mainTurnDistance is very LONG and heroRole is MAIN and strategicalValue is MEDIUM then Value is LOW with 0.5 rule: if heroRole is SCOUT and turn is NOW and mainTurnDistance is MEDIUM then Value is BAD
rule: if turn is FUTURE and mainTurnDistance is very LONG and heroRole is MAIN and strategicalValue is HIGH then Value is BITLOW rule: if heroRole is SCOUT and turn is NEXT and mainTurnDistance is LONG then Value is BAD
rule: if turn is FUTURE and scoutTurnDistance is LONG and heroRole is SCOUT then Value is LOW rule: if heroRole is SCOUT and turn is NOW and scoutTurnDistance is LONG then Value is BAD
rule: if turn is FUTURE and mainTurnDistance is LONG and heroRole is MAIN then Value is LOW rule: if heroRole is SCOUT and fear is HIGH then Value is BAD with 0.8
rule: if turn is FUTURE and mainTurnDistance is LONG and heroRole is SCOUT then Value is LOW rule: if heroRole is SCOUT and fear is MEDIUM then Value is BAD with 0.5
rule: if scoutTurnDistance is MEDIUM and heroRole is SCOUT then Value is BITLOW rule: if heroRole is MAIN and fear is HIGH then Value is BAD with 0.5
rule: if mainTurnDistance is MEDIUM then Value is BITLOW rule: if heroRole is MAIN and fear is MEDIUM then Value is BAD with 0.2
rule: if scoutTurnDistance is LOW and heroRole is SCOUT then Value is MEDIUM RuleBlock: strategicalValue
rule: if mainTurnDistance is LOW then Value is MEDIUM enabled: true
rule: if goldReward is HIGH and goldPreasure is HIGH and heroRole is SCOUT and danger is not NONE and armyLoss is LOW then Value is BITHIGH conjunction: AlgebraicProduct
rule: if goldReward is HIGH and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is HIGH with 0.7 disjunction: NormalizedSum
rule: if goldReward is HIGH and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW and fear is not HIGH then Value is HIGHEST implication: AlgebraicProduct
rule: if goldReward is HIGH and goldPreasure is HIGH and heroRole is MAIN and danger is NONE then Value is BITHIGH activation: General
rule: if goldReward is MEDIUM and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is HIGH rule: if heroRole is MAIN and strategicalValue is HIGH and turn is NOW then Value is HIGHEST
rule: if goldReward is MEDIUM and goldPreasure is HIGH and armyLoss is LOW and heroRole is SCOUT and danger is not NONE then Value is MEDIUM 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 goldReward is MEDIUM and heroRole is MAIN and danger is NONE and rewardType is SINGLE then Value is BITLOW 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 goldReward is MEDIUM and goldPreasure is HIGH and armyLoss is LOW and heroRole is MAIN and danger is not NONE then Value is BITHIGH rule: if heroRole is MAIN and strategicalValue is HIGH and mainTurnDistance is LONG and fear is not HIGH then Value is HIGH
rule: if goldReward is LOW and goldPreasure is HIGH and heroRole is SCOUT and armyLoss is LOW then Value is BITHIGH rule: if heroRole is MAIN and strategicalValue is MEDIUM and mainTurnDistance is LOW then Value is HIGH
rule: if goldReward is LOW and heroRole is MAIN and danger is not NONE and rewardType is SINGLE and armyLoss is LOW then Value is BITLOW rule: if heroRole is MAIN and strategicalValue is MEDIUM and mainTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH
rule: if goldReward is LOW and heroRole is MAIN and danger is NONE and rewardType is SINGLE then Value is LOW rule: if heroRole is MAIN and strategicalValue is MEDIUM and mainTurnDistance is LONG and fear is not HIGH then Value is MEDIUM
rule: if goldReward is LOWEST and heroRole is MAIN and danger is NONE and rewardType is SINGLE then Value is LOWEST rule: if heroRole is MAIN and strategicalValue is LOW and mainTurnDistance is LOW and fear is not HIGH then Value is MEDIUM
rule: if armyReward is HIGH and heroRole is SCOUT and danger is not NONE and armyLoss is LOW then Value is HIGH with 0.5 rule: if heroRole is MAIN and strategicalValue is LOW and mainTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL
rule: if armyReward is HIGH and heroRole is SCOUT and danger is NONE then Value is HIGHEST rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is not NONE and turn is NOW then Value is HIGH
rule: if armyReward is HIGH and heroRole is MAIN and rewardType is MIXED and armyLoss is LOW and fear is not HIGH then Value is HIGHEST 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 armyReward is HIGH and heroRole is MAIN and rewardType is SINGLE and mainTurnDistance is LOWEST then Value is HIGHEST 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 armyReward is HIGH and heroRole is MAIN and rewardType is SINGLE and danger is NONE 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 LONG and fear is not HIGH then Value is BITHIGH
rule: if armyReward is HIGH and heroRole is MAIN and rewardType is SINGLE and danger is not NONE and armyLoss is LOW and fear is not HIGH then Value is HIGHEST rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is not NONE and scoutTurnDistance is LOW then Value is BITHIGH
rule: if armyReward is MEDIUM and heroRole is MAIN and danger is not NONE and armyLoss is LOW and fear is not HIGH then Value is HIGHEST with 0.5 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 armyReward is MEDIUM and heroRole is MAIN and danger is NONE then Value is BITHIGH 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 armyReward is MEDIUM and heroRole is MAIN and danger is NONE and mainTurnDistance is LOWEST then Value is HIGH with 0.2 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 armyReward is MEDIUM and heroRole is SCOUT and danger is NONE then Value is HIGHEST with 0.5 rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is NONE and turn is NOW then Value is HIGHEST
rule: if armyReward is LOW and heroRole is SCOUT and danger is NONE then Value is HIGH 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 armyReward is LOW and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is HIGH 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 armyReward is LOW and heroRole is MAIN and danger is NONE then Value is BITLOW 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 armyReward is LOW and heroRole is MAIN and danger is NONE and mainTurnDistance is LOWEST 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 skillReward is LOW and heroRole is MAIN and armyLoss is LOW then Value is BITHIGH 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 skillReward is MEDIUM and heroRole is MAIN and armyLoss is LOW 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 skillReward is MEDIUM and heroRole is MAIN and rewardType is MIXED and armyLoss is LOW and fear is not HIGH then Value is HIGH with 0.5 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 skillReward is HIGH and heroRole is MAIN and armyLoss is LOW and fear is not HIGH then Value is HIGH 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 skillReward is MEDIUM and heroRole is SCOUT then Value is LOWEST rule: if armyLoss is HIGH and strategicalValue is LOW then Value is BAD
rule: if skillReward is HIGH and heroRole is SCOUT then Value is LOWEST rule: if armyLoss is HIGH and strategicalValue is MEDIUM then Value is BAD with 0.7
rule: if strategicalValue is LOW and heroRole is MAIN and armyLoss is LOW then Value is BITHIGH rule: if strategicalValue is CRITICAL and heroRole is MAIN then Value is CRITICAL
rule: if strategicalValue is LOWEST and heroRole is MAIN and armyLoss is LOW then Value is LOW rule: if strategicalValue is CRITICAL and heroRole is SCOUT then Value is CRITICAL with 0.7
rule: if strategicalValue is LOW and heroRole is SCOUT and armyLoss is LOW and fear is not HIGH then Value is HIGH with 0.5 RuleBlock: armyReward
rule: if strategicalValue is MEDIUM and heroRole is SCOUT and danger is NONE and fear is not HIGH then Value is HIGH enabled: true
rule: if strategicalValue is HIGH and heroRole is SCOUT and danger is NONE and fear is not HIGH then Value is HIGHEST with 0.5 conjunction: AlgebraicProduct
rule: if strategicalValue is HIGH and heroRole is MAIN and armyLoss is LOW and fear is not HIGH then Value is HIGHEST disjunction: AlgebraicSum
rule: if strategicalValue is HIGH and heroRole is MAIN and armyLoss is MEDIUM and fear is not HIGH then Value is HIGH implication: AlgebraicProduct
rule: if strategicalValue is MEDIUM and heroRole is MAIN and armyLoss is LOW and fear is not HIGH then Value is HIGH activation: General
rule: if rewardType is NONE then Value is LOWEST rule: if heroRole is MAIN and armyReward is HIGH and mainTurnDistance is LOW and fear is not HIGH then Value is HIGHEST
rule: if armyLoss is HIGH and strategicalValue is not HIGH and heroRole is MAIN then Value is LOWEST rule: if heroRole is MAIN and armyReward is HIGH and mainTurnDistance is MEDIUM and fear is not HIGH then Value is HIGH
rule: if armyLoss is HIGH and strategicalValue is HIGH and heroRole is MAIN then Value is LOW rule: if heroRole is MAIN and armyReward is HIGH and mainTurnDistance is LONG and fear is not HIGH then Value is BITHIGH
rule: if armyLoss is HIGH and heroRole is SCOUT then Value is LOWEST rule: if heroRole is MAIN and armyReward is MEDIUM and mainTurnDistance is LOW then Value is HIGH
rule: if heroRole is SCOUT and closestHeroRatio is LOW then Value is LOW 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 SCOUT and closestHeroRatio is LOWEST then Value is LOWEST 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 danger is NONE and skillReward is NONE and rewardType is SINGLE and closestHeroRatio is LOW then Value is LOW 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 danger is NONE and skillReward is NONE and rewardType is SINGLE and closestHeroRatio is LOWEST then Value is LOWEST rule: if heroRole is MAIN and armyReward is LOW and mainTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL
rule: if heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is BITHIGH with 0.2 rule: if heroRole is SCOUT and armyReward is HIGH and danger is NONE and scoutTurnDistance is LOW and fear is not HIGH then Value is HIGH
rule: if heroRole is SCOUT then Value is BITLOW rule: if heroRole is SCOUT and armyReward is HIGH and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is HIGH with 0.7
rule: if goldCost is not NONE and goldReward is NONE and goldPreasure is HIGH then Value is LOWEST rule: if heroRole is SCOUT and armyReward is HIGH and danger is NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is BITHIGH
rule: if turn is NOW then Value is LOW with 0.3 rule: if heroRole is SCOUT and armyReward is MEDIUM and danger is NONE and scoutTurnDistance is LOW then Value is HIGH with 0.7
rule: if turn is not NOW then Value is LOW with 0.4 rule: if heroRole is SCOUT and armyReward is MEDIUM and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH
rule: if goldPreasure is HIGH and goldReward is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW and fear is not HIGH then Value is HIGHEST rule: if heroRole is SCOUT and armyReward is MEDIUM and danger is NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is MEDIUM
rule: if goldPreasure is HIGH and goldReward is MEDIUM and heroRole is MAIN and danger is not NONE and armyLoss is LOW and fear is not HIGH then Value is HIGH rule: if heroRole is SCOUT and armyReward is LOW and danger is NONE and scoutTurnDistance is LOW and fear is not HIGH then Value is MEDIUM
rule: if goldPreasure is HIGH and goldReward is HIGH and heroRole is SCOUT and danger is NONE and armyLoss is LOW and fear is not HIGH then Value is HIGHEST rule: if heroRole is SCOUT and armyReward is LOW and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL
rule: if goldPreasure is HIGH and goldReward is MEDIUM and heroRole is SCOUT and danger is NONE and armyLoss is LOW and fear is not HIGH then Value is HIGH RuleBlock: gold
rule: if goldPreasure is HIGH and goldReward is LOW and heroRole is SCOUT and armyLoss is LOW then Value is BITHIGH enabled: true
rule: if goldPreasure is HIGH and goldReward is LOW and heroRole is SCOUT and scoutTurnDistance is LOW and armyLoss is LOW then Value is HIGH with 0.5 conjunction: AlgebraicProduct
rule: if fear is MEDIUM then Value is LOW disjunction: AlgebraicSum
rule: if fear is HIGH then Value is LOWEST implication: AlgebraicProduct
activation: General
rule: if goldReward is HUGE and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is HIGHEST
rule: if goldReward is HUGE and goldPreasure is HIGH and heroRole is SCOUT and danger is not NONE and armyLoss is LOW then Value is BITHIGH
rule: if goldReward is HUGE and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is HIGHEST
rule: if goldReward is HUGE and goldPreasure is HIGH and heroRole is MAIN and danger is NONE then Value is HIGH
rule: if goldReward is BIG and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is HIGH
rule: if goldReward is BIG and goldPreasure is HIGH and heroRole is SCOUT and danger is not NONE then Value is MEDIUM
rule: if goldReward is BIG and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is HIGH
rule: if goldReward is BIG and goldPreasure is HIGH and heroRole is MAIN and danger is NONE then Value is MEDIUM
rule: if goldReward is MEDIUM and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is BITHIGH
rule: if goldReward is MEDIUM and goldPreasure is HIGH and heroRole is SCOUT and danger is not NONE then Value is SMALL
rule: if goldReward is MEDIUM and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is BITHIGH
rule: if goldReward is SMALL and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is MEDIUM
rule: if goldReward is SMALL and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is SMALL
rule: if goldReward is LOWEST then Value is SMALL with 0.1
rule: if goldReward is SMALL then Value is SMALL with 0.2
rule: if goldReward is MEDIUM then Value is SMALL with 0.5
rule: if goldReward is BIG then Value is SMALL
rule: if goldReward is HUGE then Value is BITHIGH
RuleBlock: skill reward
enabled: true
conjunction: AlgebraicProduct
disjunction: AlgebraicSum
implication: AlgebraicProduct
activation: General
rule: if heroRole is MAIN and skillReward is LOW and mainTurnDistance is LOWEST and fear is not HIGH then Value is HIGH
rule: if heroRole is MAIN and skillReward is MEDIUM and mainTurnDistance is LOWEST and fear is not HIGH then Value is HIGHEST
rule: if heroRole is MAIN and skillReward is HIGH and mainTurnDistance is LOWEST and fear is not HIGH then Value is HIGHEST
rule: if heroRole is MAIN and skillReward is LOW and mainTurnDistance is LOW and fear is not HIGH then Value is BITHIGH
rule: if heroRole is MAIN and skillReward is MEDIUM and mainTurnDistance is LOW and fear is not HIGH then Value is HIGH
rule: if heroRole is MAIN and skillReward is HIGH and mainTurnDistance is LOW and fear is not HIGH then Value is HIGHEST
rule: if heroRole is MAIN and skillReward is LOW and mainTurnDistance is MEDIUM and fear is not HIGH then Value is MEDIUM
rule: if heroRole is MAIN and skillReward is MEDIUM and mainTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH
rule: if heroRole is MAIN and skillReward is HIGH and mainTurnDistance is MEDIUM and fear is not HIGH then Value is HIGH
rule: if heroRole is MAIN and skillReward is LOW and mainTurnDistance is LONG and fear is not HIGH then Value is SMALL
rule: if heroRole is MAIN and skillReward is MEDIUM and mainTurnDistance is LONG and fear is not HIGH then Value is MEDIUM
rule: if heroRole is MAIN and skillReward is HIGH and mainTurnDistance is LONG and fear is not HIGH then Value is BITHIGH