1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-11-23 22:37:55 +02:00
Files
vcmi/AI/Nullkiller2/Analyzers/BuildAnalyzer.cpp
2025-09-10 01:42:56 +02:00

498 lines
15 KiB
C++

/*
* BuildAnalyzer.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 <boost/range/algorithm/sort.hpp>
#include "../Engine/Nullkiller.h"
#include "../../../lib/entities/building/CBuilding.h"
#include "../../../lib/IGameSettings.h"
#include "../AIUtility.h"
namespace NK2AI
{
TResources BuildAnalyzer::getMissingResourcesNow(const float armyGoldRatio) const
{
auto armyGold = goldOnly(armyCost);
armyGold[GameResID::GOLD] = armyGoldRatio * armyGold[GameResID::GOLD];
auto result = requiredResources + goldRemove(armyCost) + armyGold - aiNk->getFreeResources();
result.positive();
return result;
}
TResources BuildAnalyzer::getMissingResourcesInTotal(const float armyGoldRatio) const
{
auto armyGold = goldOnly(armyCost);
armyGold[GameResID::GOLD] = armyGoldRatio * armyGold[GameResID::GOLD];
auto result = totalDevelopmentCost + goldRemove(armyCost) + armyGold - aiNk->getFreeResources();
result.positive();
return result;
}
TResources BuildAnalyzer::getFreeResourcesAfterMissingTotal(const float armyGoldRatio) const
{
auto armyGold = goldOnly(armyCost);
armyGold[GameResID::GOLD] = armyGoldRatio * armyGold[GameResID::GOLD];
auto result = aiNk->getFreeResources() - totalDevelopmentCost - goldRemove(armyCost) - armyGold;
result.positive();
return result;
}
bool BuildAnalyzer::isGoldPressureOverMax() const
{
return goldPressure > aiNk->settings->getMaxGoldPressure();
}
void BuildAnalyzer::update()
{
logAi->trace("Start BuildAnalyzer::update");
reset();
const auto towns = aiNk->cc->getTownsInfo();
float economyDevelopmentCost = 0;
for(const CGTownInstance * town : towns)
{
if(town->built >= aiNk->cc->getSettings().getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP))
continue; // Not much point in trying anything - can't built in this town anymore today
#if NK2AI_TRACE_LEVEL >= 1
logAi->trace("Checking town %s", town->getNameTranslated());
#endif
developmentInfos.push_back(TownDevelopmentInfo(town));
TownDevelopmentInfo & tdi = developmentInfos.back();
updateDwellings(tdi, aiNk->armyManager, aiNk->cc);
updateOtherBuildings(tdi, aiNk->armyManager, aiNk->cc);
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 NK2AI_TRACE_LEVEL >= 1
for(const auto & biToBuild : tdi.toBuild)
logAi->trace("Building preferences %s", biToBuild.toString());
#endif
}
boost::range::sort(developmentInfos, [](const TownDevelopmentInfo & tdi1, const TownDevelopmentInfo & tdi2) -> bool
{
auto val1 = goldApproximate(tdi1.armyCost) - goldApproximate(tdi1.townDevelopmentCost);
auto val2 = goldApproximate(tdi2.armyCost) - goldApproximate(tdi2.townDevelopmentCost);
return val1 > val2;
});
dailyIncome = calculateDailyIncome(aiNk->cc->getMyObjects(), aiNk->cc->getTownsInfo());
goldPressure = calculateGoldPressure(aiNk->getLockedResources()[EGameResID::GOLD],
static_cast<float>(armyCost[EGameResID::GOLD]),
economyDevelopmentCost,
aiNk->getFreeGold(),
static_cast<float>(dailyIncome[EGameResID::GOLD]));
}
void BuildAnalyzer::reset()
{
requiredResources = TResources();
totalDevelopmentCost = TResources();
armyCost = TResources();
developmentInfos.clear();
}
bool BuildAnalyzer::isBuilt(FactionID alignment, BuildingID bid) const
{
for(const auto & tdi : developmentInfos)
{
if(tdi.town->getFactionID() == alignment && tdi.town->hasBuilt(bid))
return true;
}
return false;
}
void TownDevelopmentInfo::addBuildingBuilt(const BuildingInfo & bi)
{
armyCost += bi.armyCost;
armyStrength += bi.armyStrength;
built.push_back(bi);
}
void TownDevelopmentInfo::addBuildingToBuild(const BuildingInfo & bi)
{
townDevelopmentCost += bi.buildCostWithPrerequisites;
townDevelopmentCost += BuildAnalyzer::goldRemove(bi.armyCost);
if (bi.isBuildable)
{
toBuild.push_back(bi);
}
else if (bi.isMissingResources)
{
requiredResources += bi.buildCost;
toBuild.push_back(bi);
}
}
BuildingInfo::BuildingInfo() {}
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;
isBuilt = town->hasBuilt(id);
prerequisitesCount = 1;
name = building->getNameTranslated();
if(creature)
{
creatureGrowth = creature->getGrowth();
creatureID = creature->getId();
baseCreatureID = baseCreature;
creatureUnitCost = creature->getFullRecruitCost();
creatureLevel = creature->getLevel();
if(isBuilt)
{
creatureGrowth = town->creatureGrowth(creatureLevel - 1);
}
else
{
if(id.isDwelling())
{
creatureGrowth = creature->getGrowth();
if(town->hasBuilt(BuildingID::CASTLE))
creatureGrowth *= 2;
else if(town->hasBuilt(BuildingID::CITADEL))
creatureGrowth += creatureGrowth / 2;
}
else
{
creatureGrowth = creature->getHorde();
}
}
armyStrength = armyManager->evaluateStackPower(creature, creatureGrowth);
armyCost = creatureUnitCost * creatureGrowth;
}
}
std::string BuildingInfo::toString() const
{
return name + ", cost: " + buildCost.toString()
+ ", creature: " + std::to_string(creatureGrowth) + " x " + std::to_string(creatureLevel)
+ " x " + creatureUnitCost.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 NK2AI_TRACE_LEVEL >= 1
logAi->trace("Gold pressure: %f", pressure);
#endif
return pressure;
}
TResources BuildAnalyzer::calculateDailyIncome(const std::vector<const CGObjectInstance *> & objects, const std::vector<const CGTownInstance *> & townInfos)
{
auto result = TResources();
for(const CGObjectInstance * obj : objects)
{
if(const auto * mine = dynamic_cast<const CGMine *>(obj))
result += mine->dailyIncome();
}
for(const CGTownInstance * town : townInfos)
{
result += town->dailyIncome();
}
return result;
}
void BuildAnalyzer::updateDwellings(TownDevelopmentInfo & developmentInfo, std::unique_ptr<ArmyManager> & armyManager, std::shared_ptr<CCallback> & cc)
{
for(int level = 0; level < developmentInfo.town->getTown()->creatures.size(); level++)
{
#if NK2AI_TRACE_LEVEL >= 1
logAi->trace("Checking dwelling level %d", level);
#endif
std::vector<BuildingID> dwellingsInTown;
for(BuildingID buildID = BuildingID::getDwellingFromLevel(level, 0); buildID.hasValue(); BuildingID::advanceDwelling(buildID))
if(developmentInfo.town->getTown()->buildings.count(buildID) != 0)
dwellingsInTown.push_back(buildID);
// find best, already built dwelling
for (const auto & buildID : boost::adaptors::reverse(dwellingsInTown))
{
if (!developmentInfo.town->hasBuilt(buildID))
continue;
const auto & info = getBuildingOrPrerequisite(developmentInfo.town, buildID, armyManager, cc);
developmentInfo.addBuildingBuilt(info);
break;
}
// find all non-built dwellings that can be built and add them for consideration
for (const auto & buildID : dwellingsInTown)
{
if (developmentInfo.town->hasBuilt(buildID))
continue;
const auto & info = getBuildingOrPrerequisite(developmentInfo.town, buildID, armyManager, cc);
if (info.isBuildable || info.isMissingResources)
developmentInfo.addBuildingToBuild(info);
}
}
}
void BuildAnalyzer::updateOtherBuildings(TownDevelopmentInfo & developmentInfo,
std::unique_ptr<ArmyManager> & armyManager,
std::shared_ptr<CCallback> & cc)
{
logAi->trace("Checking other buildings");
std::vector<std::vector<BuildingID>> otherBuildings = {
{BuildingID::TOWN_HALL, BuildingID::CITY_HALL, BuildingID::CAPITOL},
{BuildingID::MAGES_GUILD_3, BuildingID::MAGES_GUILD_5}
};
if(developmentInfo.built.size() >= 2 && cc->getDate(Date::DAY_OF_WEEK) > 4)
{
otherBuildings.push_back({BuildingID::HORDE_1});
otherBuildings.push_back({BuildingID::HORDE_2});
}
otherBuildings.push_back({ BuildingID::CITADEL, BuildingID::CASTLE });
otherBuildings.push_back({ BuildingID::RESOURCE_SILO });
otherBuildings.push_back({ BuildingID::SPECIAL_1 });
otherBuildings.push_back({ BuildingID::SPECIAL_2 });
otherBuildings.push_back({ BuildingID::SPECIAL_3 });
otherBuildings.push_back({ BuildingID::SPECIAL_4 });
otherBuildings.push_back({ BuildingID::MARKETPLACE });
for(auto & buildingSet : otherBuildings)
{
for(auto & buildingID : buildingSet)
{
if(!developmentInfo.town->hasBuilt(buildingID) && developmentInfo.town->getTown()->buildings.count(buildingID))
{
developmentInfo.addBuildingToBuild(getBuildingOrPrerequisite(developmentInfo.town, buildingID, armyManager, cc));
break;
}
}
}
}
BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
const CGTownInstance * town,
BuildingID b,
std::unique_ptr<ArmyManager> & armyManager,
std::shared_ptr<CCallback> & cc,
bool excludeDwellingDependencies)
{
// TODO: Mircea: Remove redundant variable
BuildingID building = b;
const auto * townInfo = town->getTown();
const auto & buildPtr = townInfo->buildings.at(building);
const CCreature * creature = nullptr;
CreatureID baseCreatureID;
int creatureLevelIndex = -1;
int creatureUpgradeNo = 0;
if(b.isDwelling())
{
creatureLevelIndex = BuildingID::getLevelIndexFromDwelling(b);
creatureUpgradeNo = BuildingID::getUpgradeNoFromDwelling(b);
}
else if(b == BuildingID::HORDE_1 || b == BuildingID::HORDE_1_UPGR)
{
creatureLevelIndex = townInfo->hordeLvl.at(0);
}
else if(b == BuildingID::HORDE_2 || b == BuildingID::HORDE_2_UPGR)
{
creatureLevelIndex = townInfo->hordeLvl.at(1);
}
if(creatureLevelIndex >= 0)
{
auto creatures = townInfo->creatures.at(creatureLevelIndex);
auto creatureID = creatures.size() > creatureUpgradeNo
? creatures.at(creatureUpgradeNo)
: creatures.front();
baseCreatureID = creatures.front();
creature = creatureID.toCreature();
}
auto info = BuildingInfo(buildPtr.get(), creature, baseCreatureID, town, armyManager);
//logAi->trace("checking %s buildInfo %s", info.name, info.toString());
int highestFort = 0;
for (const auto * ti : cc->getTownsInfo())
{
highestFort = std::max(highestFort, static_cast<int>(ti->fortLevel()));
}
if(!town->hasBuilt(building))
{
auto canBuild = cc->canBuildStructure(town, building);
if(canBuild == EBuildingState::ALLOWED)
{
info.isBuildable = true;
}
else if(canBuild == EBuildingState::NO_RESOURCES)
{
//logAi->trace("cant build. Not enough resources. Need %s", info.buildCost.toString());
info.isMissingResources = true;
}
else if(canBuild == EBuildingState::PREREQUIRES)
{
auto buildExpression = town->genBuildingRequirements(building, false);
auto missingBuildings = buildExpression.getFulfillmentCandidates([&](const BuildingID & id) -> bool
{
return town->hasBuilt(id);
});
auto otherDwelling = [](const BuildingID & id) -> bool
{
return id.isDwelling();
};
if(vstd::contains_if(missingBuildings, otherDwelling))
{
#if NK2AI_TRACE_LEVEL >= 1
logAi->trace("Can't build %d. Needs other dwelling %d", b.getNum(), missingBuildings.front().getNum());
#endif
}
else if(missingBuildings[0] != b)
{
#if NK2AI_TRACE_LEVEL >= 1
logAi->trace("Can't build %d. Needs %d", b.getNum(), missingBuildings[0].num);
#endif
BuildingInfo prerequisite = getBuildingOrPrerequisite(town, missingBuildings[0], armyManager, cc, excludeDwellingDependencies);
prerequisite.buildCostWithPrerequisites += info.buildCost;
prerequisite.creatureUnitCost = info.creatureUnitCost;
prerequisite.creatureGrowth = info.creatureGrowth;
prerequisite.creatureLevel = info.creatureLevel;
prerequisite.creatureID = info.creatureID;
prerequisite.baseCreatureID = info.baseCreatureID;
prerequisite.prerequisitesCount++;
prerequisite.armyCost = info.armyCost;
prerequisite.armyStrength = info.armyStrength;
bool haveSameOrBetterFort = false;
if (prerequisite.id == BuildingID::FORT && highestFort >= CGTownInstance::EFortLevel::FORT)
haveSameOrBetterFort = true;
if (prerequisite.id == BuildingID::CITADEL && highestFort >= CGTownInstance::EFortLevel::CITADEL)
haveSameOrBetterFort = true;
if (prerequisite.id == BuildingID::CASTLE && highestFort >= CGTownInstance::EFortLevel::CASTLE)
haveSameOrBetterFort = true;
if(!haveSameOrBetterFort)
prerequisite.dailyIncome = info.dailyIncome;
return prerequisite;
}
else
{
#if NK2AI_TRACE_LEVEL >= 1
logAi->trace("Can't build. The building requires itself as prerequisite");
#endif
return info;
}
}
else
{
#if NK2AI_TRACE_LEVEL >= 1
logAi->trace("Can't build. Reason: %d", static_cast<int>(canBuild));
#endif
}
}
else
{
#if NK2AI_TRACE_LEVEL >= 1
logAi->trace("Dwelling %d exists", b.getNum());
#endif
info.isBuilt = true;
}
return info;
}
TResource BuildAnalyzer::goldApproximate(const TResources & res)
{
// TODO: Mircea: Would it make sense to use the marketplace rate of the player? See ResourceTrader::trade()
return goldApproximate(res[EGameResID::WOOD], EGameResID::WOOD) + goldApproximate(res[EGameResID::MERCURY], EGameResID::MERCURY) +
goldApproximate(res[EGameResID::ORE], EGameResID::ORE) + goldApproximate(res[EGameResID::SULFUR], EGameResID::SULFUR) +
goldApproximate(res[EGameResID::CRYSTAL], EGameResID::CRYSTAL) + goldApproximate(res[EGameResID::GEMS], EGameResID::GEMS) +
goldApproximate(res[EGameResID::GOLD], EGameResID::GOLD) + goldApproximate(res[EGameResID::MITHRIL], EGameResID::MITHRIL);
}
TResource BuildAnalyzer::goldApproximate(const TResource & res, EGameResID resId)
{
switch(resId)
{
case EGameResID::WOOD:
case EGameResID::ORE:
return res * 75;
case EGameResID::MERCURY:
case EGameResID::SULFUR:
case EGameResID::CRYSTAL:
case EGameResID::GEMS:
return res * 125;
case EGameResID::GOLD:
return res;
case EGameResID::MITHRIL:
return res; // TODO: Mircea: What multiplier to give for mithril?
default:
throw std::runtime_error("Unsupported resource ID" + std::to_string(resId));
}
}
TResources BuildAnalyzer::goldRemove(TResources other)
{
TResources copy;
for(int i = 0; i < GameResID::COUNT; ++i)
copy[i] = other[i];
copy[GameResID::GOLD] = 0;
return copy;
}
TResources BuildAnalyzer::goldOnly(TResources other)
{
TResources copy;
copy[GameResID::GOLD] = other[GameResID::GOLD];
return copy;
}
}