1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-11-27 22:49:25 +02:00

squashed: BuildAnalyzer wip; Nulkiller decompose; Nulkiller makeTurnHelperPriorityPass; Nulkiller minor refactoring

This commit is contained in:
Mircea TheHonestCTO
2025-08-14 04:48:04 +02:00
parent 87417296aa
commit a9ecaca81f
12 changed files with 350 additions and 346 deletions

View File

@@ -716,7 +716,7 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector<C
&& components.size() == 2 && components.size() == 2
&& components.front().type == ComponentType::RESOURCE && components.front().type == ComponentType::RESOURCE
&& (nullkiller->heroManager->getHeroRole(hero) != HeroRole::MAIN && (nullkiller->heroManager->getHeroRole(hero) != HeroRole::MAIN
|| nullkiller->buildAnalyzer->isGoldPressureHigh())) || nullkiller->buildAnalyzer->isGoldPressureOverMax()))
{ {
sel = 1; sel = 1;
} }

View File

@@ -7,6 +7,8 @@
* Full text of license available in license.txt file, in main folder * Full text of license available in license.txt file, in main folder
* *
*/ */
#include <boost/range/algorithm/sort.hpp>
#include "../StdInc.h" #include "../StdInc.h"
#include "../Engine/Nullkiller.h" #include "../Engine/Nullkiller.h"
#include "../Engine/Nullkiller.h" #include "../Engine/Nullkiller.h"
@@ -16,7 +18,235 @@
namespace NK2AI namespace NK2AI
{ {
void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo) TResources BuildAnalyzer::getResourcesRequiredNow() const
{
auto resourcesAvailable = ai->getFreeResources();
auto result = withoutGold(armyCost) + requiredResources - resourcesAvailable;
result.positive();
return result;
}
TResources BuildAnalyzer::getTotalResourcesRequired() const
{
auto resourcesAvailable = ai->getFreeResources();
auto result = totalDevelopmentCost + withoutGold(armyCost) - resourcesAvailable;
result.positive();
return result;
}
bool BuildAnalyzer::isGoldPressureOverMax() const
{
return goldPressure > ai->settings->getMaxGoldPressure();
}
void BuildAnalyzer::update()
{
logAi->trace("Start BuildAnalyzer::update");
BuildingInfo bi;
reset();
auto towns = ai->cb->getTownsInfo();
float economyDevelopmentCost = 0;
for(const CGTownInstance* town : towns)
{
if(town->built >= cb->getSettings().getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP))
continue; // Not much point in trying anything - can't built in this town anymore today
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Checking town %s", town->getNameTranslated());
#endif
developmentInfos.push_back(TownDevelopmentInfo(town));
TownDevelopmentInfo & tdi = developmentInfos.back();
updateTownDwellings(tdi, ai->armyManager, ai->cb);
updateOtherBuildings(tdi, ai->armyManager, ai->cb);
requiredResources += tdi.requiredResources;
totalDevelopmentCost += tdi.townDevelopmentCost;
for(auto building : tdi.toBuild)
{
if (building.dailyIncome[EGameResID::GOLD] > 0)
economyDevelopmentCost += building.buildCostWithPrerequisites[EGameResID::GOLD];
}
armyCost += tdi.armyCost;
#if NKAI_TRACE_LEVEL >= 1
for(const auto & bi : tdi.toBuild)
logAi->trace("Building preferences %s", bi.toString());
#endif
}
boost::range::sort(developmentInfos, [](const TownDevelopmentInfo & tdi1, const TownDevelopmentInfo & tdi2) -> bool
{
auto val1 = approximateInGold(tdi1.armyCost) - approximateInGold(tdi1.townDevelopmentCost);
auto val2 = approximateInGold(tdi2.armyCost) - approximateInGold(tdi2.townDevelopmentCost);
return val1 > val2;
});
dailyIncome = calculateDailyIncome(ai->cb->getMyObjects(), ai->cb->getTownsInfo());
goldPressure = calculateGoldPressure(ai->getLockedResources()[EGameResID::GOLD],
(float)armyCost[EGameResID::GOLD],
economyDevelopmentCost,
ai->getFreeGold(),
(float)dailyIncome[EGameResID::GOLD]);
}
void BuildAnalyzer::reset()
{
requiredResources = TResources();
totalDevelopmentCost = TResources();
armyCost = TResources();
developmentInfos.clear();
}
bool BuildAnalyzer::isBuilt(FactionID alignment, BuildingID bid) const
{
for(auto tdi : developmentInfos)
{
if(tdi.town->getFactionID() == alignment && tdi.town->hasBuilt(bid))
return true;
}
return false;
}
void TownDevelopmentInfo::addExistingDwelling(const BuildingInfo & existingDwelling)
{
existingDwellings.push_back(existingDwelling);
armyCost += existingDwelling.armyCost;
armyStrength += existingDwelling.armyStrength;
}
void TownDevelopmentInfo::addBuildingToBuild(const BuildingInfo & nextToBuild)
{
townDevelopmentCost += nextToBuild.buildCostWithPrerequisites;
townDevelopmentCost += BuildAnalyzer::withoutGold(nextToBuild.armyCost);
if(nextToBuild.canBuild)
{
hasSomethingToBuild = true;
toBuild.push_back(nextToBuild);
}
else if(nextToBuild.notEnoughRes)
{
requiredResources += nextToBuild.buildCost;
hasSomethingToBuild = true;
toBuild.push_back(nextToBuild);
}
}
BuildingInfo::BuildingInfo()
{
id = BuildingID::NONE;
creatureGrows = 0;
creatureID = CreatureID::NONE;
buildCost = 0;
buildCostWithPrerequisites = 0;
prerequisitesCount = 0;
name.clear();
armyStrength = 0;
}
BuildingInfo::BuildingInfo(
const CBuilding * building,
const CCreature * creature,
CreatureID baseCreature,
const CGTownInstance * town,
const std::unique_ptr<ArmyManager> & armyManager)
{
id = building->bid;
buildCost = building->resources;
buildCostWithPrerequisites = building->resources;
dailyIncome = building->produce;
exists = town->hasBuilt(id);
prerequisitesCount = 1;
name = building->getNameTranslated();
if(creature)
{
creatureGrows = creature->getGrowth();
creatureID = creature->getId();
creatureCost = creature->getFullRecruitCost();
creatureLevel = creature->getLevel();
baseCreatureID = baseCreature;
if(exists)
{
creatureGrows = town->creatureGrowth(creatureLevel - 1);
}
else
{
if(id.isDwelling())
{
creatureGrows = creature->getGrowth();
if(town->hasBuilt(BuildingID::CASTLE))
creatureGrows *= 2;
else if(town->hasBuilt(BuildingID::CITADEL))
creatureGrows += creatureGrows / 2;
}
else
{
creatureGrows = creature->getHorde();
}
}
armyStrength = armyManager->evaluateStackPower(creature, creatureGrows);
armyCost = creatureCost * creatureGrows;
}
else
{
creatureGrows = 0;
creatureID = CreatureID::NONE;
baseCreatureID = CreatureID::NONE;
creatureCost = TResources();
armyCost = TResources();
creatureLevel = 0;
armyStrength = 0;
}
}
std::string BuildingInfo::toString() const
{
return name + ", cost: " + buildCost.toString()
+ ", creature: " + std::to_string(creatureGrows) + " x " + std::to_string(creatureLevel)
+ " x " + creatureCost.toString()
+ ", daily: " + dailyIncome.toString();
}
float BuildAnalyzer::calculateGoldPressure(TResource lockedGold, float armyCostGold, float economyDevelopmentCost, float freeGold, float dailyIncomeGold)
{
auto pressure = (lockedGold + armyCostGold + economyDevelopmentCost) / (1 + 2 * freeGold + dailyIncomeGold * 7.0f);
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Gold pressure: %f", pressure);
#endif
return pressure;
}
TResources BuildAnalyzer::calculateDailyIncome(std::vector<const CGObjectInstance *> objects, std::vector<const CGTownInstance *> townInfos)
{
auto result = TResources();
for(const CGObjectInstance * obj : objects)
{
if(const CGMine * mine = dynamic_cast<const CGMine *>(obj))
result += mine->dailyIncome();
}
for(const CGTownInstance * town : townInfos)
{
result += town->dailyIncome();
}
return result;
}
void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo, std::unique_ptr<ArmyManager> & armyManager, std::shared_ptr<CCallback> & cb)
{ {
for(int level = 0; level < developmentInfo.town->getTown()->creatures.size(); level++) for(int level = 0; level < developmentInfo.town->getTown()->creatures.size(); level++)
{ {
@@ -35,7 +265,7 @@ void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo)
if (!developmentInfo.town->hasBuilt(buildID)) if (!developmentInfo.town->hasBuilt(buildID))
continue; continue;
const auto & info = getBuildingOrPrerequisite(developmentInfo.town, buildID); const auto & info = getBuildingOrPrerequisite(developmentInfo.town, buildID, armyManager, cb);
developmentInfo.addExistingDwelling(info); developmentInfo.addExistingDwelling(info);
break; break;
} }
@@ -46,14 +276,16 @@ void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo)
if (developmentInfo.town->hasBuilt(buildID)) if (developmentInfo.town->hasBuilt(buildID))
continue; continue;
const auto & info = getBuildingOrPrerequisite(developmentInfo.town, buildID); const auto & info = getBuildingOrPrerequisite(developmentInfo.town, buildID, armyManager, cb);
if (info.canBuild || info.notEnoughRes) if (info.canBuild || info.notEnoughRes)
developmentInfo.addBuildingToBuild(info); developmentInfo.addBuildingToBuild(info);
} }
} }
} }
void BuildAnalyzer::updateOtherBuildings(TownDevelopmentInfo & developmentInfo) void BuildAnalyzer::updateOtherBuildings(TownDevelopmentInfo & developmentInfo,
std::unique_ptr<ArmyManager> & armyManager,
std::shared_ptr<CCallback> & cb)
{ {
logAi->trace("Checking other buildings"); logAi->trace("Checking other buildings");
@@ -62,7 +294,7 @@ void BuildAnalyzer::updateOtherBuildings(TownDevelopmentInfo & developmentInfo)
{BuildingID::MAGES_GUILD_3, BuildingID::MAGES_GUILD_5} {BuildingID::MAGES_GUILD_3, BuildingID::MAGES_GUILD_5}
}; };
if(developmentInfo.existingDwellings.size() >= 2 && ai->cb->getDate(Date::DAY_OF_WEEK) > 4) if(developmentInfo.existingDwellings.size() >= 2 && cb->getDate(Date::DAY_OF_WEEK) > 4)
{ {
otherBuildings.push_back({BuildingID::HORDE_1}); otherBuildings.push_back({BuildingID::HORDE_1});
otherBuildings.push_back({BuildingID::HORDE_2}); otherBuildings.push_back({BuildingID::HORDE_2});
@@ -82,122 +314,19 @@ void BuildAnalyzer::updateOtherBuildings(TownDevelopmentInfo & developmentInfo)
{ {
if(!developmentInfo.town->hasBuilt(buildingID) && developmentInfo.town->getTown()->buildings.count(buildingID)) if(!developmentInfo.town->hasBuilt(buildingID) && developmentInfo.town->getTown()->buildings.count(buildingID))
{ {
developmentInfo.addBuildingToBuild(getBuildingOrPrerequisite(developmentInfo.town, buildingID)); developmentInfo.addBuildingToBuild(getBuildingOrPrerequisite(developmentInfo.town, buildingID, armyManager, cb));
break; break;
} }
} }
} }
} }
int32_t convertToGold(const TResources & res)
{
return res[EGameResID::GOLD]
+ 75 * (res[EGameResID::WOOD] + res[EGameResID::ORE])
+ 125 * (res[EGameResID::GEMS] + res[EGameResID::CRYSTAL] + res[EGameResID::MERCURY] + res[EGameResID::SULFUR]);
}
TResources withoutGold(TResources other)
{
other[GameResID::GOLD] = 0;
return other;
}
TResources BuildAnalyzer::getResourcesRequiredNow() const
{
auto resourcesAvailable = ai->getFreeResources();
auto result = withoutGold(armyCost) + requiredResources - resourcesAvailable;
result.positive();
return result;
}
TResources BuildAnalyzer::getTotalResourcesRequired() const
{
auto resourcesAvailable = ai->getFreeResources();
auto result = totalDevelopmentCost + withoutGold(armyCost) - resourcesAvailable;
result.positive();
return result;
}
bool BuildAnalyzer::isGoldPressureHigh() const
{
return goldPressure > ai->settings->getMaxGoldPressure();
}
void BuildAnalyzer::update()
{
logAi->trace("Start analysing build");
BuildingInfo bi;
reset();
auto towns = ai->cb->getTownsInfo();
float economyDevelopmentCost = 0;
for(const CGTownInstance* town : towns)
{
if(town->built >= cb->getSettings().getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP))
continue; // Not much point in trying anything - can't built in this town anymore today
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Checking town %s", town->getNameTranslated());
#endif
developmentInfos.push_back(TownDevelopmentInfo(town));
TownDevelopmentInfo & developmentInfo = developmentInfos.back();
updateTownDwellings(developmentInfo);
updateOtherBuildings(developmentInfo);
requiredResources += developmentInfo.requiredResources;
totalDevelopmentCost += developmentInfo.townDevelopmentCost;
for(auto building : developmentInfo.toBuild)
{
if (building.dailyIncome[EGameResID::GOLD] > 0)
economyDevelopmentCost += building.buildCostWithPrerequisites[EGameResID::GOLD];
}
armyCost += developmentInfo.armyCost;
#if NKAI_TRACE_LEVEL >= 1
for(auto bi : developmentInfo.toBuild)
logAi->trace("Building preferences %s", bi.toString());
#endif
}
std::sort(developmentInfos.begin(), developmentInfos.end(), [](const TownDevelopmentInfo & t1, const TownDevelopmentInfo & t2) -> bool
{
auto val1 = convertToGold(t1.armyCost) - convertToGold(t1.townDevelopmentCost);
auto val2 = convertToGold(t2.armyCost) - convertToGold(t2.townDevelopmentCost);
return val1 > val2;
});
updateDailyIncome();
goldPressure = (ai->getLockedResources()[EGameResID::GOLD] + (float)armyCost[EGameResID::GOLD] + economyDevelopmentCost) / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f);
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Gold pressure: %f", goldPressure);
#endif
}
void BuildAnalyzer::reset()
{
requiredResources = TResources();
totalDevelopmentCost = TResources();
armyCost = TResources();
developmentInfos.clear();
}
BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite( BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
const CGTownInstance * town, const CGTownInstance * town,
BuildingID toBuild, BuildingID toBuild,
bool excludeDwellingDependencies) const std::unique_ptr<ArmyManager> & armyManager,
std::shared_ptr<CCallback> & cb,
bool excludeDwellingDependencies)
{ {
BuildingID building = toBuild; BuildingID building = toBuild;
auto townInfo = town->getTown(); auto townInfo = town->getTown();
@@ -234,19 +363,19 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
creature = creatureID.toCreature(); creature = creatureID.toCreature();
} }
auto info = BuildingInfo(buildPtr.get(), creature, baseCreatureID, town, ai); auto info = BuildingInfo(buildPtr.get(), creature, baseCreatureID, town, armyManager);
//logAi->trace("checking %s buildInfo %s", info.name, info.toString()); //logAi->trace("checking %s buildInfo %s", info.name, info.toString());
int highestFort = 0; int highestFort = 0;
for (auto twn : ai->cb->getTownsInfo()) for (auto ti : cb->getTownsInfo())
{ {
highestFort = std::max(highestFort, (int)twn->fortLevel()); highestFort = std::max(highestFort, (int)ti->fortLevel());
} }
if(!town->hasBuilt(building)) if(!town->hasBuilt(building))
{ {
auto canBuild = ai->cb->canBuildStructure(town, building); auto canBuild = cb->canBuildStructure(town, building);
if(canBuild == EBuildingState::ALLOWED) if(canBuild == EBuildingState::ALLOWED)
{ {
@@ -281,7 +410,7 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
#if NKAI_TRACE_LEVEL >= 1 #if NKAI_TRACE_LEVEL >= 1
logAi->trace("cant build %d. Need %d", toBuild.getNum(), missingBuildings[0].num); logAi->trace("cant build %d. Need %d", toBuild.getNum(), missingBuildings[0].num);
#endif #endif
BuildingInfo prerequisite = getBuildingOrPrerequisite(town, missingBuildings[0], excludeDwellingDependencies); BuildingInfo prerequisite = getBuildingOrPrerequisite(town, missingBuildings[0], armyManager, cb, excludeDwellingDependencies);
prerequisite.buildCostWithPrerequisites += info.buildCost; prerequisite.buildCostWithPrerequisites += info.buildCost;
prerequisite.creatureCost = info.creatureCost; prerequisite.creatureCost = info.creatureCost;
@@ -330,141 +459,18 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
return info; return info;
} }
void BuildAnalyzer::updateDailyIncome() int32_t BuildAnalyzer::approximateInGold(const TResources & res)
{ {
auto objects = ai->cb->getMyObjects(); // TODO: Would it make sense to use the marketplace rate of the player?
auto towns = ai->cb->getTownsInfo(); return res[EGameResID::GOLD]
+ 75 * (res[EGameResID::WOOD] + res[EGameResID::ORE])
dailyIncome = TResources(); + 125 * (res[EGameResID::GEMS] + res[EGameResID::CRYSTAL] + res[EGameResID::MERCURY] + res[EGameResID::SULFUR]);
for(const CGObjectInstance* obj : objects)
{
const CGMine* mine = dynamic_cast<const CGMine*>(obj);
if(mine)
dailyIncome += mine->dailyIncome();
}
for(const CGTownInstance* town : towns)
{
dailyIncome += town->dailyIncome();
}
} }
bool BuildAnalyzer::hasAnyBuilding(FactionID alignment, BuildingID bid) const TResources BuildAnalyzer::withoutGold(TResources other)
{ {
for(auto tdi : developmentInfos) other[GameResID::GOLD] = 0;
{ return other;
if(tdi.town->getFactionID() == alignment && tdi.town->hasBuilt(bid))
return true;
}
return false;
}
void TownDevelopmentInfo::addExistingDwelling(const BuildingInfo & existingDwelling)
{
existingDwellings.push_back(existingDwelling);
armyCost += existingDwelling.armyCost;
armyStrength += existingDwelling.armyStrength;
}
void TownDevelopmentInfo::addBuildingToBuild(const BuildingInfo & nextToBuild)
{
townDevelopmentCost += nextToBuild.buildCostWithPrerequisites;
townDevelopmentCost += withoutGold(nextToBuild.armyCost);
if(nextToBuild.canBuild)
{
hasSomethingToBuild = true;
toBuild.push_back(nextToBuild);
}
else if(nextToBuild.notEnoughRes)
{
requiredResources += nextToBuild.buildCost;
hasSomethingToBuild = true;
toBuild.push_back(nextToBuild);
}
}
BuildingInfo::BuildingInfo()
{
id = BuildingID::NONE;
creatureGrows = 0;
creatureID = CreatureID::NONE;
buildCost = 0;
buildCostWithPrerequisites = 0;
prerequisitesCount = 0;
name.clear();
armyStrength = 0;
}
BuildingInfo::BuildingInfo(
const CBuilding * building,
const CCreature * creature,
CreatureID baseCreature,
const CGTownInstance * town,
Nullkiller * ai)
{
id = building->bid;
buildCost = building->resources;
buildCostWithPrerequisites = building->resources;
dailyIncome = building->produce;
exists = town->hasBuilt(id);
prerequisitesCount = 1;
name = building->getNameTranslated();
if(creature)
{
creatureGrows = creature->getGrowth();
creatureID = creature->getId();
creatureCost = creature->getFullRecruitCost();
creatureLevel = creature->getLevel();
baseCreatureID = baseCreature;
if(exists)
{
creatureGrows = town->creatureGrowth(creatureLevel - 1);
}
else
{
if(id.isDwelling())
{
creatureGrows = creature->getGrowth();
if(town->hasBuilt(BuildingID::CASTLE))
creatureGrows *= 2;
else if(town->hasBuilt(BuildingID::CITADEL))
creatureGrows += creatureGrows / 2;
}
else
{
creatureGrows = creature->getHorde();
}
}
armyStrength = ai->armyManager->evaluateStackPower(creature, creatureGrows);
armyCost = creatureCost * creatureGrows;
}
else
{
creatureGrows = 0;
creatureID = CreatureID::NONE;
baseCreatureID = CreatureID::NONE;
creatureCost = TResources();
armyCost = TResources();
creatureLevel = 0;
armyStrength = 0;
}
}
std::string BuildingInfo::toString() const
{
return name + ", cost: " + buildCost.toString()
+ ", creature: " + std::to_string(creatureGrows) + " x " + std::to_string(creatureLevel)
+ " x " + creatureCost.toString()
+ ", daily: " + dailyIncome.toString();
} }
} }

View File

@@ -14,6 +14,7 @@
namespace NK2AI namespace NK2AI
{ {
class ArmyManager;
class Nullkiller; class Nullkiller;
@@ -44,7 +45,7 @@ public:
const CCreature * creature, const CCreature * creature,
CreatureID baseCreature, CreatureID baseCreature,
const CGTownInstance * town, const CGTownInstance * town,
Nullkiller * ai); const std::unique_ptr<ArmyManager> & armyManager);
std::string toString() const; std::string toString() const;
}; };
@@ -96,20 +97,23 @@ public:
const std::vector<TownDevelopmentInfo> & getDevelopmentInfo() const { return developmentInfos; } const std::vector<TownDevelopmentInfo> & getDevelopmentInfo() const { return developmentInfos; }
TResources getDailyIncome() const { return dailyIncome; } TResources getDailyIncome() const { return dailyIncome; }
float getGoldPressure() const { return goldPressure; } float getGoldPressure() const { return goldPressure; }
bool isGoldPressureHigh() const; bool isGoldPressureOverMax() const;
bool hasAnyBuilding(FactionID alignment, BuildingID bid) const; bool isBuilt(FactionID alignment, BuildingID bid) const;
private: void reset();
BuildingInfo getBuildingOrPrerequisite(
static float calculateGoldPressure(TResource lockedGold, float armyCostGold, float economyDevelopmentCost, float freeGold, float dailyIncomeGold);
static TResources calculateDailyIncome(std::vector<const CGObjectInstance *> objects, std::vector<const CGTownInstance *> townInfos);
static void updateTownDwellings(TownDevelopmentInfo& developmentInfo, std::unique_ptr<ArmyManager>& armyManager, std::shared_ptr<CCallback>& cb);
static void updateOtherBuildings(TownDevelopmentInfo& developmentInfo, std::unique_ptr<ArmyManager>& armyManager, std::shared_ptr<CCallback>& cb);
static BuildingInfo getBuildingOrPrerequisite(
const CGTownInstance* town, const CGTownInstance* town,
BuildingID toBuild, BuildingID toBuild,
bool excludeDwellingDependencies = true) const; std::unique_ptr<ArmyManager> & armyManager,
std::shared_ptr<CCallback> & cb,
bool excludeDwellingDependencies = true);
void updateTownDwellings(TownDevelopmentInfo & developmentInfo); static int32_t approximateInGold(const TResources & res);
void updateOtherBuildings(TownDevelopmentInfo & developmentInfo); static TResources withoutGold(TResources other);
void updateDailyIncome();
void reset();
}; };
} }

View File

@@ -47,7 +47,7 @@ Goals::TGoalVec BuildingBehavior::decompose(const Nullkiller * ai) const
totalDevelopmentCost.toString()); totalDevelopmentCost.toString());
auto & developmentInfos = ai->buildAnalyzer->getDevelopmentInfo(); auto & developmentInfos = ai->buildAnalyzer->getDevelopmentInfo();
auto isGoldPressureLow = !ai->buildAnalyzer->isGoldPressureHigh(); auto isGoldPressureLow = !ai->buildAnalyzer->isGoldPressureOverMax();
ai->dangerHitMap->updateHitMap(); ai->dangerHitMap->updateHitMap();

View File

@@ -41,7 +41,7 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const
{ {
uint8_t closestThreat = ai->dangerHitMap->getTileThreat(town->visitablePos()).fastestDanger.turn; uint8_t closestThreat = ai->dangerHitMap->getTileThreat(town->visitablePos()).fastestDanger.turn;
if (closestThreat >=2 && ai->buildAnalyzer->isGoldPressureHigh() && !town->hasBuilt(BuildingID::CITY_HALL) && cb->canBuildStructure(town, BuildingID::CITY_HALL) != EBuildingState::FORBIDDEN) if (closestThreat >=2 && ai->buildAnalyzer->isGoldPressureOverMax() && !town->hasBuilt(BuildingID::CITY_HALL) && cb->canBuildStructure(town, BuildingID::CITY_HALL) != EBuildingState::FORBIDDEN)
{ {
return tasks; return tasks;
} }

View File

@@ -320,7 +320,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
if(scoutReinforcement >= armyToGetOrBuy.upgradeValue if(scoutReinforcement >= armyToGetOrBuy.upgradeValue
&& ai->getFreeGold() >20000 && ai->getFreeGold() >20000
&& !ai->buildAnalyzer->isGoldPressureHigh()) && !ai->buildAnalyzer->isGoldPressureOverMax())
{ {
Composition recruitHero; Composition recruitHero;

View File

@@ -130,9 +130,9 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
{ {
if (ai->cb->getHeroesInfo().size() == 0 if (ai->cb->getHeroesInfo().size() == 0
|| treasureSourcesCount > ai->cb->getHeroesInfo().size() * 5 || treasureSourcesCount > ai->cb->getHeroesInfo().size() * 5
|| (bestHeroToHire->getArmyCost() > GameConstants::HERO_GOLD_COST / 2.0 && (bestClosestThreat < 1 || !ai->buildAnalyzer->isGoldPressureHigh())) || (bestHeroToHire->getArmyCost() > GameConstants::HERO_GOLD_COST / 2.0 && (bestClosestThreat < 1 || !ai->buildAnalyzer->isGoldPressureOverMax()))
|| (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh() && haveCapitol) || (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureOverMax() && haveCapitol)
|| (ai->getFreeResources()[EGameResID::GOLD] > 30000 && !ai->buildAnalyzer->isGoldPressureHigh())) || (ai->getFreeResources()[EGameResID::GOLD] > 30000 && !ai->buildAnalyzer->isGoldPressureOverMax()))
{ {
tasks.push_back(Goals::sptr(Goals::RecruitHero(bestTownToHireFrom, bestHeroToHire).setpriority((float)3 / (ourHeroes.size() + 1)))); tasks.push_back(Goals::sptr(Goals::RecruitHero(bestTownToHireFrom, bestHeroToHire).setpriority((float)3 / (ourHeroes.size() + 1))));
} }

View File

@@ -46,7 +46,7 @@ void DeepDecomposer::decompose(TGoalVec & results, TSubgoal behavior, int depthL
goals[0] = {behavior}; goals[0] = {behavior};
while(goals[0].size()) while(!goals[0].empty())
{ {
bool fromCache; bool fromCache;
TSubgoal current = goals[depth].back(); TSubgoal current = goals[depth].back();
@@ -61,7 +61,7 @@ void DeepDecomposer::decompose(TGoalVec & results, TSubgoal behavior, int depthL
goals[depth + 1].clear(); goals[depth + 1].clear();
} }
for(TSubgoal subgoal : subgoals) for(const TSubgoal & subgoal : subgoals)
{ {
if(subgoal->invalid()) if(subgoal->invalid())
continue; continue;
@@ -104,7 +104,7 @@ void DeepDecomposer::decompose(TGoalVec & results, TSubgoal behavior, int depthL
} }
} }
if(depth < depthLimit - 1 && goals[depth + 1].size()) if(depth < depthLimit - 1 && !goals[depth + 1].empty())
{ {
depth++; depth++;
} }

View File

@@ -9,6 +9,8 @@
*/ */
#include "StdInc.h" #include "StdInc.h"
#include "Nullkiller.h" #include "Nullkiller.h"
#include <boost/range/algorithm/sort.hpp>
#include "../AIGateway.h" #include "../AIGateway.h"
#include "../Behaviors/CaptureObjectsBehavior.h" #include "../Behaviors/CaptureObjectsBehavior.h"
#include "../Behaviors/RecruitHeroBehavior.h" #include "../Behaviors/RecruitHeroBehavior.h"
@@ -61,12 +63,7 @@ bool canUseOpenMap(std::shared_ptr<CCallback> cb, PlayerColor playerID)
return cb->getPlayerState(teamMateID)->isHuman(); return cb->getPlayerState(teamMateID)->isHuman();
}); });
if(hasHumanInTeam) return !hasHumanInTeam;
{
return false;
}
return true;
} }
void Nullkiller::init(std::shared_ptr<CCallback> cb, AIGateway * gateway) void Nullkiller::init(std::shared_ptr<CCallback> cb, AIGateway * gateway)
@@ -117,7 +114,7 @@ void Nullkiller::init(std::shared_ptr<CCallback> cb, AIGateway * gateway)
armyFormation.reset(new ArmyFormation(cb, this)); armyFormation.reset(new ArmyFormation(cb, this));
} }
TaskPlanItem::TaskPlanItem(TSubgoal task) TaskPlanItem::TaskPlanItem(const TSubgoal & task)
:task(task), affectedObjects(task->asTask()->getAffectedObjects()) :task(task), affectedObjects(task->asTask()->getAffectedObjects())
{ {
} }
@@ -136,7 +133,7 @@ Goals::TTaskVec TaskPlan::getTasks() const
return result; return result;
} }
void TaskPlan::merge(TSubgoal task) void TaskPlan::merge(const TSubgoal & task)
{ {
TGoalVec blockers; TGoalVec blockers;
@@ -158,9 +155,9 @@ void TaskPlan::merge(TSubgoal task)
} }
} }
vstd::erase_if(tasks, [&](const TaskPlanItem & task) vstd::erase_if(tasks, [&](const TaskPlanItem & task2)
{ {
return vstd::contains(blockers, task.task); return vstd::contains(blockers, task2.task);
}); });
tasks.emplace_back(task); tasks.emplace_back(task);
@@ -173,13 +170,13 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TGoalVec & tasks) const
return taskptr(Invalid()); return taskptr(Invalid());
} }
for(TSubgoal & task : tasks) for(const TSubgoal & task : tasks)
{ {
if(task->asTask()->priority <= 0) if(task->asTask()->priority <= 0)
task->asTask()->priority = priorityEvaluator->evaluate(task); task->asTask()->priority = priorityEvaluator->evaluate(task);
} }
auto bestTask = *vstd::maxElementByFun(tasks, [](Goals::TSubgoal task) -> float auto bestTask = *vstd::maxElementByFun(tasks, [](const Goals::TSubgoal& task) -> float
{ {
return task->asTask()->priority; return task->asTask()->priority;
}); });
@@ -197,18 +194,18 @@ Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks, int priorityTier) const
for(size_t i = r.begin(); i != r.end(); i++) for(size_t i = r.begin(); i != r.end(); i++)
{ {
auto task = tasks[i]; const auto & task = tasks[i];
if (task->asTask()->priority <= 0 || priorityTier != PriorityEvaluator::PriorityTier::BUILDINGS) if (task->asTask()->priority <= 0 || priorityTier != PriorityEvaluator::PriorityTier::BUILDINGS)
task->asTask()->priority = evaluator->evaluate(task, priorityTier); task->asTask()->priority = evaluator->evaluate(task, priorityTier);
} }
}); });
std::sort(tasks.begin(), tasks.end(), [](TSubgoal g1, TSubgoal g2) -> bool boost::range::sort(tasks, [](const TSubgoal& g1, const TSubgoal& g2) -> bool
{ {
return g2->asTask()->priority < g1->asTask()->priority; return g2->asTask()->priority < g1->asTask()->priority;
}); });
for(TSubgoal & task : tasks) for(const TSubgoal & task : tasks)
{ {
taskPlan.merge(task); taskPlan.merge(task);
} }
@@ -216,22 +213,15 @@ Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks, int priorityTier) const
return taskPlan.getTasks(); return taskPlan.getTasks();
} }
void Nullkiller::decompose(Goals::TGoalVec & results, Goals::TSubgoal behavior, int decompositionMaxDepth) const void Nullkiller::decompose(Goals::TGoalVec & results, const Goals::TSubgoal& behavior, int decompositionMaxDepth) const
{ {
makingTurnInterrupption.interruptionPoint(); makingTurnInterrupption.interruptionPoint();
logAi->debug("Decomposing behavior %s", behavior->toString());
logAi->debug("Checking behavior %s", behavior->toString());
auto start = std::chrono::high_resolution_clock::now(); auto start = std::chrono::high_resolution_clock::now();
decomposer->decompose(results, behavior, decompositionMaxDepth); decomposer->decompose(results, behavior, decompositionMaxDepth);
makingTurnInterrupption.interruptionPoint(); makingTurnInterrupption.interruptionPoint();
logAi->debug("Decomposing behavior %s done in %ld", behavior->toString(), timeElapsed(start));
logAi->debug(
"Behavior %s. Time taken %ld",
behavior->toString(),
timeElapsed(start));
} }
void Nullkiller::resetState() void Nullkiller::resetState()
@@ -266,6 +256,7 @@ void Nullkiller::updateState(bool partialUpdate)
activeHero = nullptr; activeHero = nullptr;
setTargetObject(-1); setTargetObject(-1);
decomposer->reset(); decomposer->reset();
buildAnalyzer->update(); buildAnalyzer->update();
if (!pathfinderInvalidated) if (!pathfinderInvalidated)
@@ -380,35 +371,10 @@ void Nullkiller::makeTurn()
for(int i = 1; i <= settings->getMaxPass() && cb->getPlayerStatus(playerID) == EPlayerStatus::INGAME; i++) for(int i = 1; i <= settings->getMaxPass() && cb->getPlayerStatus(playerID) == EPlayerStatus::INGAME; i++)
{ {
updateState(); updateState();
Goals::TTask bestTask = taskptr(Goals::Invalid());
for(int j = 1; j <= settings->getMaxPriorityPass() && cb->getPlayerStatus(playerID) == EPlayerStatus::INGAME; j++) if (!makeTurnHelperPriorityPass(tasks, i)) return;
{
tasks.clear();
decompose(tasks, sptr(RecruitHeroBehavior()), 1);
decompose(tasks, sptr(BuyArmyBehavior()), 1);
decompose(tasks, sptr(BuildingBehavior()), 1);
bestTask = choseBestTask(tasks);
if(bestTask->priority > 0)
{
#if NKAI_TRACE_LEVEL >= 1
logAi->info("Pass %d: Performing prio 0 task %s with prio: %d", i, bestTask->toString(), bestTask->priority);
#endif
if(!executeTask(bestTask))
return;
updateState(bestTask->getHero() == nullptr);
}
else
{
tasks.clear();
break;
}
}
tasks.clear();
decompose(tasks, sptr(CaptureObjectsBehavior()), 1); decompose(tasks, sptr(CaptureObjectsBehavior()), 1);
decompose(tasks, sptr(ClusterBehavior()), MAX_DEPTH); decompose(tasks, sptr(ClusterBehavior()), MAX_DEPTH);
decompose(tasks, sptr(DefenceBehavior()), MAX_DEPTH); decompose(tasks, sptr(DefenceBehavior()), MAX_DEPTH);
@@ -419,17 +385,10 @@ void Nullkiller::makeTurn()
decompose(tasks, sptr(ExplorationBehavior()), MAX_DEPTH); decompose(tasks, sptr(ExplorationBehavior()), MAX_DEPTH);
TTaskVec selectedTasks; TTaskVec selectedTasks;
#if NKAI_TRACE_LEVEL >= 1
int prioOfTask = 0; int prioOfTask = 0;
#endif
for (int prio = PriorityEvaluator::PriorityTier::INSTAKILL; prio <= PriorityEvaluator::PriorityTier::MAX_PRIORITY_TIER; ++prio) for (int prio = PriorityEvaluator::PriorityTier::INSTAKILL; prio <= PriorityEvaluator::PriorityTier::MAX_PRIORITY_TIER; ++prio)
{ {
#if NKAI_TRACE_LEVEL >= 1
prioOfTask = prio; prioOfTask = prio;
#endif
selectedTasks = buildPlan(tasks, prio); selectedTasks = buildPlan(tasks, prio);
if (!selectedTasks.empty() || settings->isUseFuzzy()) if (!selectedTasks.empty() || settings->isUseFuzzy())
break; break;
@@ -446,7 +405,6 @@ void Nullkiller::makeTurn()
} }
bool hasAnySuccess = false; bool hasAnySuccess = false;
for(const auto& selectedTask : selectedTasks) for(const auto& selectedTask : selectedTasks)
{ {
if(cb->getPlayerStatus(playerID) != EPlayerStatus::INGAME) if(cb->getPlayerStatus(playerID) != EPlayerStatus::INGAME)
@@ -480,7 +438,7 @@ void Nullkiller::makeTurn()
if((settings->isUseFuzzy() && selectedTask->priority < MIN_PRIORITY) || (!settings->isUseFuzzy() && selectedTask->priority <= 0)) if((settings->isUseFuzzy() && selectedTask->priority < MIN_PRIORITY) || (!settings->isUseFuzzy() && selectedTask->priority <= 0))
{ {
auto heroes = cb->getHeroesInfo(); auto heroes = cb->getHeroesInfo();
auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool const auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool
{ {
return h->movementPointsRemaining() > 100; return h->movementPointsRemaining() > 100;
}); });
@@ -499,7 +457,6 @@ void Nullkiller::makeTurn()
} }
logAi->trace("Goal %s has too low priority. It is not worth doing it.", taskDescription); logAi->trace("Goal %s has too low priority. It is not worth doing it.", taskDescription);
continue; continue;
} }
@@ -511,13 +468,9 @@ void Nullkiller::makeTurn()
{ {
if(hasAnySuccess) if(hasAnySuccess)
break; break;
else return;
return;
}
else
{
hasAnySuccess = true;
} }
hasAnySuccess = true;
} }
hasAnySuccess |= handleTrading(); hasAnySuccess |= handleTrading();
@@ -533,11 +486,48 @@ void Nullkiller::makeTurn()
if(i == settings->getMaxPass()) if(i == settings->getMaxPass())
{ {
logAi->warn("Maxpass exceeded. Terminating AI turn."); logAi->warn("MaxPass reached. Terminating AI turn.");
} }
} }
} }
bool Nullkiller::makeTurnHelperPriorityPass(Goals::TGoalVec & tempResults, int passIndex)
{
Goals::TTask bestPrioPassTask = taskptr(Goals::Invalid());
for(int i = 1; i <= settings->getMaxPriorityPass() && cb->getPlayerStatus(playerID) == EPlayerStatus::INGAME; i++)
{
tempResults.clear();
decompose(tempResults, sptr(RecruitHeroBehavior()), 1);
decompose(tempResults, sptr(BuyArmyBehavior()), 1);
decompose(tempResults, sptr(BuildingBehavior()), 1);
bestPrioPassTask = choseBestTask(tempResults);
if(bestPrioPassTask->priority > 0)
{
#if NKAI_TRACE_LEVEL >= 1
logAi->info("Pass %d: Performing priorityPass %d task %s with prio: %d", passIndex, i, bestPrioPassTask->toString(), bestPrioPassTask->priority);
#endif
if(!executeTask(bestPrioPassTask))
return false;
// TODO: Inspect why it's ok to do a partial update if condition is true
updateState(bestPrioPassTask->getHero() == nullptr);
}
else
{
break;
}
if(i == settings->getMaxPriorityPass())
{
logAi->warn("MaxPriorityPass reached. Terminating priorityPass loop.");
}
}
return true;
}
bool Nullkiller::areAffectedObjectsPresent(Goals::TTask task) const bool Nullkiller::areAffectedObjectsPresent(Goals::TTask task) const
{ {
auto affectedObjs = task->getAffectedObjects(); auto affectedObjs = task->getAffectedObjects();
@@ -661,7 +651,7 @@ bool Nullkiller::handleTrading()
if (i == GameResID::GOLD) if (i == GameResID::GOLD)
{ {
if (income[i] > 0 && !buildAnalyzer->isGoldPressureHigh()) if (income[i] > 0 && !buildAnalyzer->isGoldPressureOverMax())
okToSell = true; okToSell = true;
} }
else else

View File

@@ -60,7 +60,7 @@ struct TaskPlanItem
std::vector<ObjectInstanceID> affectedObjects; std::vector<ObjectInstanceID> affectedObjects;
Goals::TSubgoal task; Goals::TSubgoal task;
TaskPlanItem(Goals::TSubgoal goal); TaskPlanItem(const Goals::TSubgoal& goal);
}; };
class TaskPlan class TaskPlan
@@ -70,7 +70,7 @@ private:
public: public:
Goals::TTaskVec getTasks() const; Goals::TTaskVec getTasks() const;
void merge(Goals::TSubgoal task); void merge(const Goals::TSubgoal& task);
}; };
class Nullkiller class Nullkiller
@@ -115,6 +115,7 @@ public:
~Nullkiller(); ~Nullkiller();
void init(std::shared_ptr<CCallback> cb, AIGateway * gateway); void init(std::shared_ptr<CCallback> cb, AIGateway * gateway);
void makeTurn(); void makeTurn();
bool makeTurnHelperPriorityPass(Goals::TGoalVec& tempResults, int passIndex);
bool isActive(const CGHeroInstance * hero) const { return activeHero == hero; } bool isActive(const CGHeroInstance * hero) const { return activeHero == hero; }
bool isHeroLocked(const CGHeroInstance * hero) const; bool isHeroLocked(const CGHeroInstance * hero) const;
HeroPtr getActiveHero() { return activeHero; } HeroPtr getActiveHero() { return activeHero; }
@@ -142,7 +143,7 @@ public:
private: private:
void resetState(); void resetState();
void updateState(bool partialUpdate = false); void updateState(bool partialUpdate = false);
void decompose(Goals::TGoalVec & results, Goals::TSubgoal behavior, int decompositionMaxDepth) const; void decompose(Goals::TGoalVec & results, const Goals::TSubgoal& behavior, int decompositionMaxDepth) const;
Goals::TTask choseBestTask(Goals::TGoalVec & tasks) const; Goals::TTask choseBestTask(Goals::TGoalVec & tasks) const;
Goals::TTaskVec buildPlan(Goals::TGoalVec & tasks, int priorityTier) const; Goals::TTaskVec buildPlan(Goals::TGoalVec & tasks, int priorityTier) const;
bool executeTask(Goals::TTask task); bool executeTask(Goals::TTask task);

View File

@@ -1267,7 +1267,7 @@ public:
uint64_t RewardEvaluator::getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const uint64_t RewardEvaluator::getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const
{ {
if(ai->buildAnalyzer->hasAnyBuilding(town->getFactionID(), bi.id)) if(ai->buildAnalyzer->isBuilt(town->getFactionID(), bi.id))
return 0; return 0;
auto creaturesToUpgrade = ai->armyManager->getTotalCreaturesAvailable(bi.baseCreatureID); auto creaturesToUpgrade = ai->armyManager->getTotalCreaturesAvailable(bi.baseCreatureID);
@@ -1616,7 +1616,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
score += 1000; score += 1000;
auto resourcesAvailable = evaluationContext.evaluator.ai->getFreeResources(); auto resourcesAvailable = evaluationContext.evaluator.ai->getFreeResources();
auto income = ai->buildAnalyzer->getDailyIncome(); auto income = ai->buildAnalyzer->getDailyIncome();
if(ai->buildAnalyzer->isGoldPressureHigh()) if(ai->buildAnalyzer->isGoldPressureOverMax())
score /= evaluationContext.buildingCost.marketValue(); score /= evaluationContext.buildingCost.marketValue();
if (!resourcesAvailable.canAfford(evaluationContext.buildingCost)) if (!resourcesAvailable.canAfford(evaluationContext.buildingCost))
{ {

View File

@@ -22,12 +22,15 @@ using namespace Goals;
BuildThis::BuildThis(BuildingID Bid, const CGTownInstance * tid) BuildThis::BuildThis(BuildingID Bid, const CGTownInstance * tid)
: ElementarGoal(Goals::BUILD_STRUCTURE) : ElementarGoal(Goals::BUILD_STRUCTURE)
{ {
// FIXME: Remove this constructor (the parent constructor BuildThis::BuildThis)
// Seems like StartupBehavior is instantiating via this BuildThis constructor
// Or needs to be unit tested to ensure there's no problem with the limited constructor params
buildingInfo = BuildingInfo( buildingInfo = BuildingInfo(
tid->getTown()->buildings.at(Bid).get(), tid->getTown()->buildings.at(Bid).get(),
nullptr, nullptr,
CreatureID::NONE, CreatureID::NONE,
tid, tid,
nullptr); std::unique_ptr<ArmyManager>(nullptr));
bid = Bid.getNum(); bid = Bid.getNum();
town = tid; town = tid;