1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-08-08 22:26:51 +02:00

Merge pull request #5380 from IvanSavenko/xilmi_develop

[1.6.6] AI improvements from AIL / Xilmi
This commit is contained in:
Ivan Savenko
2025-02-09 19:46:54 +02:00
committed by GitHub
13 changed files with 216 additions and 62 deletions

View File

@ -774,9 +774,9 @@ bool townHasFreeTavern(const CGTownInstance * town)
return canMoveVisitingHeroToGarrison; return canMoveVisitingHeroToGarrison;
} }
uint64_t getHeroArmyStrengthWithCommander(const CGHeroInstance * hero, const CCreatureSet * heroArmy) uint64_t getHeroArmyStrengthWithCommander(const CGHeroInstance * hero, const CCreatureSet * heroArmy, int fortLevel)
{ {
auto armyStrength = heroArmy->getArmyStrength(); auto armyStrength = heroArmy->getArmyStrength(fortLevel);
if(hero && hero->commander && hero->commander->alive) if(hero && hero->commander && hero->commander->alive)
{ {

View File

@ -217,7 +217,7 @@ int64_t getArtifactScoreForHero(const CGHeroInstance * hero, const CArtifactInst
int64_t getPotentialArtifactScore(const CArtifact * art); int64_t getPotentialArtifactScore(const CArtifact * art);
bool townHasFreeTavern(const CGTownInstance * town); bool townHasFreeTavern(const CGTownInstance * town);
uint64_t getHeroArmyStrengthWithCommander(const CGHeroInstance * hero, const CCreatureSet * heroArmy); uint64_t getHeroArmyStrengthWithCommander(const CGHeroInstance * hero, const CCreatureSet * heroArmy, int fortLevel = 0);
uint64_t timeElapsed(std::chrono::time_point<std::chrono::high_resolution_clock> start); uint64_t timeElapsed(std::chrono::time_point<std::chrono::high_resolution_clock> start);

View File

@ -291,6 +291,7 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
prerequisite.baseCreatureID = info.baseCreatureID; prerequisite.baseCreatureID = info.baseCreatureID;
prerequisite.prerequisitesCount++; prerequisite.prerequisitesCount++;
prerequisite.armyCost = info.armyCost; prerequisite.armyCost = info.armyCost;
prerequisite.armyStrength = info.armyStrength;
bool haveSameOrBetterFort = false; bool haveSameOrBetterFort = false;
if (prerequisite.id == BuildingID::FORT && highestFort >= CGTownInstance::EFortLevel::FORT) if (prerequisite.id == BuildingID::FORT && highestFort >= CGTownInstance::EFortLevel::FORT)
haveSameOrBetterFort = true; haveSameOrBetterFort = true;

View File

@ -459,6 +459,8 @@ void ObjectClusterizer::clusterizeObject(
continue; continue;
} }
float priority = 0;
if(path.nodes.size() > 1) if(path.nodes.size() > 1)
{ {
auto blocker = getBlocker(path); auto blocker = getBlocker(path);
@ -475,7 +477,10 @@ void ObjectClusterizer::clusterizeObject(
heroesProcessed.insert(path.targetHero); heroesProcessed.insert(path.targetHero);
float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), PriorityEvaluator::PriorityTier::HUNTER_GATHER); for (int prio = PriorityEvaluator::PriorityTier::BUILDINGS; prio <= PriorityEvaluator::PriorityTier::MAX_PRIORITY_TIER; ++prio)
{
priority = std::max(priority, priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), prio));
}
if(ai->settings->isUseFuzzy() && priority < MIN_PRIORITY) if(ai->settings->isUseFuzzy() && priority < MIN_PRIORITY)
continue; continue;
@ -498,7 +503,10 @@ void ObjectClusterizer::clusterizeObject(
heroesProcessed.insert(path.targetHero); heroesProcessed.insert(path.targetHero);
float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), PriorityEvaluator::PriorityTier::HUNTER_GATHER); for (int prio = PriorityEvaluator::PriorityTier::BUILDINGS; prio <= PriorityEvaluator::PriorityTier::MAX_PRIORITY_TIER; ++prio)
{
priority = std::max(priority, priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), prio));
}
if (ai->settings->isUseFuzzy() && priority < MIN_PRIORITY) if (ai->settings->isUseFuzzy() && priority < MIN_PRIORITY)
continue; continue;

View File

@ -214,11 +214,15 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
std::vector<int> pathsToDefend; std::vector<int> pathsToDefend;
std::map<const CGHeroInstance *, std::vector<int>> defferedPaths; std::map<const CGHeroInstance *, std::vector<int>> defferedPaths;
AIPath* closestWay = nullptr;
for(int i = 0; i < paths.size(); i++) for(int i = 0; i < paths.size(); i++)
{ {
auto & path = paths[i]; auto & path = paths[i];
if (!closestWay || path.movementCost() < closestWay->movementCost())
closestWay = &path;
#if NKAI_TRACE_LEVEL >= 1 #if NKAI_TRACE_LEVEL >= 1
logAi->trace( logAi->trace(
"Hero %s can defend town with force %lld in %s turns, cost: %f, path: %s", "Hero %s can defend town with force %lld in %s turns, cost: %f, path: %s",
@ -382,7 +386,14 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
town->getObjectName()); town->getObjectName());
#endif #endif
sequence.push_back(sptr(ExecuteHeroChain(path, town))); ExecuteHeroChain heroChain = ExecuteHeroChain(path, town);
if (closestWay)
{
heroChain.closestWayRatio = closestWay->movementCost() / heroChain.getPath().movementCost();
}
sequence.push_back(sptr(heroChain));
composition.addNextSequence(sequence); composition.addNextSequence(sequence);
auto firstBlockedAction = path.getFirstBlockedAction(); auto firstBlockedAction = path.getFirstBlockedAction();

View File

@ -58,6 +58,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
ai->dangerHitMap->updateHitMap(); ai->dangerHitMap->updateHitMap();
int treasureSourcesCount = 0; int treasureSourcesCount = 0;
int bestClosestThreat = UINT8_MAX;
for(auto town : towns) for(auto town : towns)
{ {
@ -118,6 +119,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
bestScore = score; bestScore = score;
bestHeroToHire = hero; bestHeroToHire = hero;
bestTownToHireFrom = town; bestTownToHireFrom = town;
bestClosestThreat = closestThreat;
} }
} }
} }
@ -128,7 +130,7 @@ 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 || (bestHeroToHire->getArmyCost() > GameConstants::HERO_GOLD_COST / 2.0 && (bestClosestThreat < 1 || !ai->buildAnalyzer->isGoldPressureHigh()))
|| (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh() && haveCapitol) || (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh() && haveCapitol)
|| (ai->getFreeResources()[EGameResID::GOLD] > 30000 && !ai->buildAnalyzer->isGoldPressureHigh())) || (ai->getFreeResources()[EGameResID::GOLD] > 30000 && !ai->buildAnalyzer->isGoldPressureHigh()))
{ {

View File

@ -127,9 +127,9 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj)
auto fortLevel = town->fortLevel(); auto fortLevel = town->fortLevel();
if (fortLevel == CGTownInstance::EFortLevel::CASTLE) if (fortLevel == CGTownInstance::EFortLevel::CASTLE)
danger = std::max(danger * 2, danger + 10000); danger += 10000;
else if(fortLevel == CGTownInstance::EFortLevel::CITADEL) else if(fortLevel == CGTownInstance::EFortLevel::CITADEL)
danger = std::max(ui64(danger * 1.4), danger + 4000); danger += 4000;
} }
return danger; return danger;

View File

@ -446,7 +446,7 @@ void Nullkiller::makeTurn()
#if NKAI_TRACE_LEVEL >= 1 #if NKAI_TRACE_LEVEL >= 1
int prioOfTask = 0; int prioOfTask = 0;
#endif #endif
for (int prio = PriorityEvaluator::PriorityTier::INSTAKILL; prio <= PriorityEvaluator::PriorityTier::DEFEND; ++prio) for (int prio = PriorityEvaluator::PriorityTier::INSTAKILL; prio <= PriorityEvaluator::PriorityTier::MAX_PRIORITY_TIER; ++prio)
{ {
#if NKAI_TRACE_LEVEL >= 1 #if NKAI_TRACE_LEVEL >= 1
prioOfTask = prio; prioOfTask = prio;
@ -535,7 +535,10 @@ void Nullkiller::makeTurn()
else else
return; return;
} }
hasAnySuccess = true; else
{
hasAnySuccess = true;
}
} }
hasAnySuccess |= handleTrading(); hasAnySuccess |= handleTrading();
@ -721,7 +724,7 @@ bool Nullkiller::handleTrading()
if (toGive && toGive <= available[mostExpendable]) //don't try to sell 0 resources if (toGive && toGive <= available[mostExpendable]) //don't try to sell 0 resources
{ {
cb->trade(m->getObjInstanceID(), EMarketMode::RESOURCE_RESOURCE, GameResID(mostExpendable), GameResID(mostWanted), toGive); cb->trade(m->getObjInstanceID(), EMarketMode::RESOURCE_RESOURCE, GameResID(mostExpendable), GameResID(mostWanted), toGive);
#if NKAI_TRACE_LEVEL >= 1 #if NKAI_TRACE_LEVEL >= 2
logAi->info("Traded %d of %s for %d of %s at %s", toGive, mostExpendable, toGet, mostWanted, obj->getObjectName()); logAi->info("Traded %d of %s for %d of %s at %s", toGive, mostExpendable, toGet, mostWanted, obj->getObjectName());
#endif #endif
haveTraded = true; haveTraded = true;

View File

@ -66,7 +66,8 @@ EvaluationContext::EvaluationContext(const Nullkiller* ai)
isArmyUpgrade(false), isArmyUpgrade(false),
isHero(false), isHero(false),
isEnemy(false), isEnemy(false),
explorePriority(0) explorePriority(0),
powerRatio(0)
{ {
} }
@ -609,9 +610,6 @@ float RewardEvaluator::getConquestValue(const CGObjectInstance* target) const
? getEnemyHeroStrategicalValue(dynamic_cast<const CGHeroInstance*>(target)) ? getEnemyHeroStrategicalValue(dynamic_cast<const CGHeroInstance*>(target))
: 0; : 0;
case Obj::KEYMASTER:
return 0.6f;
default: default:
return 0; return 0;
} }
@ -889,7 +887,14 @@ public:
Goals::StayAtTown& stayAtTown = dynamic_cast<Goals::StayAtTown&>(*task); Goals::StayAtTown& stayAtTown = dynamic_cast<Goals::StayAtTown&>(*task);
evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero()); if (stayAtTown.getHero() != nullptr && stayAtTown.getHero()->movementPointsRemaining() < 100)
{
return;
}
if(stayAtTown.town->mageGuildLevel() > 0)
evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero());
if (evaluationContext.armyReward == 0) if (evaluationContext.armyReward == 0)
evaluationContext.isDefend = true; evaluationContext.isDefend = true;
else else
@ -1018,6 +1023,45 @@ public:
if(heroRole == HeroRole::MAIN) if(heroRole == HeroRole::MAIN)
evaluationContext.heroRole = heroRole; evaluationContext.heroRole = heroRole;
if (hero)
{
// Assuming Slots() returns a collection of slots with slot.second->getCreatureID() and slot.second->getPower()
float heroPower = 0;
float totalPower = 0;
// Map to store the aggregated power of creatures by CreatureID
std::map<int, float> totalPowerByCreatureID;
// Calculate hero power and total power by CreatureID
for (auto slot : hero->Slots())
{
int creatureID = slot.second->getCreatureID();
float slotPower = slot.second->getPower();
// Add the power of this slot to the heroPower
heroPower += slotPower;
// Accumulate the total power for the specific CreatureID
if (totalPowerByCreatureID.find(creatureID) == totalPowerByCreatureID.end())
{
// First time encountering this CreatureID, retrieve total creatures' power
totalPowerByCreatureID[creatureID] = ai->armyManager->getTotalCreaturesAvailable(creatureID).power;
}
}
// Calculate total power based on unique CreatureIDs
for (const auto& entry : totalPowerByCreatureID)
{
totalPower += entry.second;
}
// Compute the power ratio if total power is greater than zero
if (totalPower > 0)
{
evaluationContext.powerRatio = heroPower / totalPower;
}
}
if (target) if (target)
{ {
evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero); evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero);
@ -1030,6 +1074,8 @@ public:
evaluationContext.isHero = true; evaluationContext.isHero = true;
if (target->getOwner().isValidPlayer() && ai->cb->getPlayerRelations(ai->playerID, target->getOwner()) == PlayerRelations::ENEMIES) if (target->getOwner().isValidPlayer() && ai->cb->getPlayerRelations(ai->playerID, target->getOwner()) == PlayerRelations::ENEMIES)
evaluationContext.isEnemy = true; evaluationContext.isEnemy = true;
if (target->ID == Obj::TOWN)
evaluationContext.defenseValue = dynamic_cast<const CGTownInstance*>(target)->fortLevel();
evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army); evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army);
if(evaluationContext.danger > 0) if(evaluationContext.danger > 0)
evaluationContext.skillReward += (float)evaluationContext.danger / (float)hero->getArmyStrength(); evaluationContext.skillReward += (float)evaluationContext.danger / (float)hero->getArmyStrength();
@ -1169,6 +1215,19 @@ public:
evaluationContext.goldCost += cost; evaluationContext.goldCost += cost;
evaluationContext.closestWayRatio = 1; evaluationContext.closestWayRatio = 1;
evaluationContext.buildingCost += bi.buildCostWithPrerequisites; evaluationContext.buildingCost += bi.buildCostWithPrerequisites;
bool alreadyOwn = false;
int highestMageGuildPossible = BuildingID::MAGES_GUILD_3;
for (auto town : evaluationContext.evaluator.ai->cb->getTownsInfo())
{
if (town->hasBuilt(bi.id))
alreadyOwn = true;
if (evaluationContext.evaluator.ai->cb->canBuildStructure(town, BuildingID::MAGES_GUILD_5) != EBuildingState::FORBIDDEN)
highestMageGuildPossible = BuildingID::MAGES_GUILD_5;
else if (evaluationContext.evaluator.ai->cb->canBuildStructure(town, BuildingID::MAGES_GUILD_4) != EBuildingState::FORBIDDEN)
highestMageGuildPossible = BuildingID::MAGES_GUILD_4;
}
if (bi.id == BuildingID::MARKETPLACE || bi.dailyIncome[EGameResID::WOOD] > 0) if (bi.id == BuildingID::MARKETPLACE || bi.dailyIncome[EGameResID::WOOD] > 0)
evaluationContext.isTradeBuilding = true; evaluationContext.isTradeBuilding = true;
@ -1183,14 +1242,19 @@ public:
if(bi.baseCreatureID == bi.creatureID) if(bi.baseCreatureID == bi.creatureID)
{ {
evaluationContext.addNonCriticalStrategicalValue((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 * 1.5;
} }
else else
{ {
auto potentialUpgradeValue = evaluationContext.evaluator.getUpgradeArmyReward(buildThis.town, bi); auto potentialUpgradeValue = evaluationContext.evaluator.getUpgradeArmyReward(buildThis.town, bi);
evaluationContext.addNonCriticalStrategicalValue(potentialUpgradeValue / 10000.0f / (float)bi.prerequisitesCount); evaluationContext.addNonCriticalStrategicalValue(potentialUpgradeValue / 10000.0f / (float)bi.prerequisitesCount);
evaluationContext.armyReward += potentialUpgradeValue / (float)bi.prerequisitesCount; if(bi.id.IsDwelling())
evaluationContext.armyReward += bi.armyStrength - evaluationContext.evaluator.ai->armyManager->evaluateStackPower(bi.baseCreatureID.toCreature(), bi.creatureGrows);
else //This is for prerequisite-buildings
evaluationContext.armyReward += evaluationContext.evaluator.ai->armyManager->evaluateStackPower(bi.baseCreatureID.toCreature(), bi.creatureGrows);
if(alreadyOwn)
evaluationContext.armyReward /= bi.buildCostWithPrerequisites.marketValue();
} }
} }
else if(bi.id == BuildingID::CITADEL || bi.id == BuildingID::CASTLE) else if(bi.id == BuildingID::CITADEL || bi.id == BuildingID::CASTLE)
@ -1201,9 +1265,14 @@ public:
else if(bi.id >= BuildingID::MAGES_GUILD_1 && bi.id <= BuildingID::MAGES_GUILD_5) else if(bi.id >= BuildingID::MAGES_GUILD_1 && bi.id <= BuildingID::MAGES_GUILD_5)
{ {
evaluationContext.skillReward += 2 * (bi.id - BuildingID::MAGES_GUILD_1); evaluationContext.skillReward += 2 * (bi.id - BuildingID::MAGES_GUILD_1);
for (auto hero : evaluationContext.evaluator.ai->cb->getHeroesInfo()) if (!alreadyOwn && evaluationContext.evaluator.ai->cb->canBuildStructure(buildThis.town, highestMageGuildPossible) != EBuildingState::FORBIDDEN)
{ {
evaluationContext.armyInvolvement += hero->getArmyCost(); for (auto hero : evaluationContext.evaluator.ai->cb->getHeroesInfo())
{
if(hero->getPrimSkillLevel(PrimarySkill::SPELL_POWER) + hero->getPrimSkillLevel(PrimarySkill::KNOWLEDGE) > hero->getPrimSkillLevel(PrimarySkill::ATTACK) + hero->getPrimSkillLevel(PrimarySkill::DEFENSE)
&& hero->manaLimit() > 30)
evaluationContext.armyReward += hero->getArmyCost();
}
} }
} }
int sameTownBonus = 0; int sameTownBonus = 0;
@ -1333,18 +1402,35 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
else else
{ {
float score = 0; float score = 0;
const bool amIInDanger = ai->cb->getTownsInfo().empty() || (evaluationContext.isDefend && evaluationContext.threatTurns == 0); bool currentPositionThreatened = false;
const float maxWillingToLose = amIInDanger ? 1 : ai->settings->getMaxArmyLossTarget(); if (task->hero)
{
auto currentTileThreat = ai->dangerHitMap->getTileThreat(task->hero->visitablePos());
if (currentTileThreat.fastestDanger.turn < 1 && currentTileThreat.fastestDanger.danger > task->hero->getTotalStrength())
currentPositionThreatened = true;
}
if (priorityTier == PriorityTier::FAR_HUNTER_GATHER && currentPositionThreatened == false)
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Skip FAR_HUNTER_GATHER because hero is not threatened.");
#endif
return 0;
}
const bool amIInDanger = ai->cb->getTownsInfo().empty();
const float maxWillingToLose = amIInDanger ? 1 : ai->settings->getMaxArmyLossTarget() * evaluationContext.powerRatio > 0 ? ai->settings->getMaxArmyLossTarget() * evaluationContext.powerRatio : 1.0;
float dangerThreshold = 1;
dangerThreshold *= evaluationContext.powerRatio > 0 ? evaluationContext.powerRatio : 1.0;
bool arriveNextWeek = false; bool arriveNextWeek = false;
if (ai->cb->getDate(Date::DAY_OF_WEEK) + evaluationContext.turn > 7 && priorityTier < PriorityTier::FAR_KILL) if (ai->cb->getDate(Date::DAY_OF_WEEK) + evaluationContext.turn > 7 && priorityTier < PriorityTier::FAR_KILL)
arriveNextWeek = true; arriveNextWeek = true;
#if NKAI_TRACE_LEVEL >= 2 #if NKAI_TRACE_LEVEL >= 2
logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, army-involvement: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, explorePriority: %d isDefend: %d isEnemy: %d arriveNextWeek: %d", logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, maxWillingToLose: %f, turn: %d, turns main: %f, scout: %f, army-involvement: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, dangerThreshold: %f explorePriority: %d isDefend: %d isEnemy: %d arriveNextWeek: %d powerRatio: %f",
priorityTier, priorityTier,
task->toString(), task->toString(),
evaluationContext.armyLossPersentage, evaluationContext.armyLossPersentage,
maxWillingToLose,
(int)evaluationContext.turn, (int)evaluationContext.turn,
evaluationContext.movementCostByRole[HeroRole::MAIN], evaluationContext.movementCostByRole[HeroRole::MAIN],
evaluationContext.movementCostByRole[HeroRole::SCOUT], evaluationContext.movementCostByRole[HeroRole::SCOUT],
@ -1362,23 +1448,27 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
evaluationContext.conquestValue, evaluationContext.conquestValue,
evaluationContext.closestWayRatio, evaluationContext.closestWayRatio,
evaluationContext.enemyHeroDangerRatio, evaluationContext.enemyHeroDangerRatio,
dangerThreshold,
evaluationContext.explorePriority, evaluationContext.explorePriority,
evaluationContext.isDefend, evaluationContext.isDefend,
evaluationContext.isEnemy, evaluationContext.isEnemy,
arriveNextWeek); arriveNextWeek,
evaluationContext.powerRatio);
#endif #endif
switch (priorityTier) switch (priorityTier)
{ {
case PriorityTier::INSTAKILL: //Take towns / kill heroes in immediate reach case PriorityTier::INSTAKILL: //Take towns / kill heroes in immediate reach
{ {
if (evaluationContext.turn > 0) if (evaluationContext.turn > 0 || evaluationContext.isExchange)
return 0; return 0;
if (evaluationContext.movementCost >= 1) if (evaluationContext.movementCost >= 1)
return 0; return 0;
if (evaluationContext.defenseValue < 2 && evaluationContext.enemyHeroDangerRatio > dangerThreshold)
return 0;
if(evaluationContext.conquestValue > 0) if(evaluationContext.conquestValue > 0)
score = evaluationContext.armyInvolvement; score = evaluationContext.armyInvolvement;
if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty())) if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > dangerThreshold && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty()))
return 0; return 0;
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0; return 0;
@ -1388,23 +1478,47 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
} }
case PriorityTier::INSTADEFEND: //Defend immediately threatened towns case PriorityTier::INSTADEFEND: //Defend immediately threatened towns
{ {
if (evaluationContext.isDefend && evaluationContext.threatTurns == 0 && evaluationContext.turn == 0) //No point defending if we don't have defensive-structures
score = evaluationContext.armyInvolvement; if (evaluationContext.defenseValue < 2)
if (evaluationContext.isEnemy && maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0; return 0;
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0;
if (evaluationContext.closestWayRatio < 1.0)
return 0;
if (evaluationContext.isEnemy && evaluationContext.turn > 0)
return 0;
if (evaluationContext.isDefend && evaluationContext.threatTurns <= evaluationContext.turn)
{
const float OPTIMAL_PERCENTAGE = 0.75f; // We want army to be 75% of the threat
float optimalStrength = evaluationContext.threat * OPTIMAL_PERCENTAGE;
// Calculate how far the army is from optimal strength
float deviation = std::abs(evaluationContext.armyInvolvement - optimalStrength);
// Convert deviation to a percentage of the threat to normalize it
float deviationPercentage = deviation / evaluationContext.threat;
// Calculate score: 1.0 is perfect, decreasing as deviation increases
score = 1.0f / (1.0f + deviationPercentage);
// Apply turn penalty to still prefer earlier moves when scores are close
score = score / (evaluationContext.turn + 1);
}
break; break;
} }
case PriorityTier::KILL: //Take towns / kill heroes that are further away case PriorityTier::KILL: //Take towns / kill heroes that are further away
//FALL_THROUGH //FALL_THROUGH
case PriorityTier::FAR_KILL: case PriorityTier::FAR_KILL:
{ {
if (evaluationContext.defenseValue < 2 && evaluationContext.enemyHeroDangerRatio > dangerThreshold)
return 0;
if (evaluationContext.turn > 0 && evaluationContext.isHero) if (evaluationContext.turn > 0 && evaluationContext.isHero)
return 0; return 0;
if (arriveNextWeek && evaluationContext.isEnemy) if (arriveNextWeek && evaluationContext.isEnemy)
return 0; return 0;
if (evaluationContext.conquestValue > 0) if (evaluationContext.conquestValue > 0)
score = evaluationContext.armyInvolvement; score = evaluationContext.armyInvolvement;
if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty())) if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > dangerThreshold && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty()))
return 0; return 0;
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0; return 0;
@ -1413,24 +1527,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
score /= evaluationContext.movementCost; score /= evaluationContext.movementCost;
break; break;
} }
case PriorityTier::UPGRADE:
{
if (!evaluationContext.isArmyUpgrade)
return 0;
if (evaluationContext.enemyHeroDangerRatio > 1)
return 0;
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0;
if (vstd::isAlmostZero(evaluationContext.armyLossPersentage) && evaluationContext.closestWayRatio < 1.0)
return 0;
score = 1000;
if (evaluationContext.movementCost > 0)
score /= evaluationContext.movementCost;
break;
}
case PriorityTier::HIGH_PRIO_EXPLORE: case PriorityTier::HIGH_PRIO_EXPLORE:
{ {
if (evaluationContext.enemyHeroDangerRatio > 1) if (evaluationContext.enemyHeroDangerRatio > dangerThreshold)
return 0; return 0;
if (evaluationContext.explorePriority != 1) if (evaluationContext.explorePriority != 1)
return 0; return 0;
@ -1447,17 +1546,15 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
//FALL_THROUGH //FALL_THROUGH
case PriorityTier::FAR_HUNTER_GATHER: case PriorityTier::FAR_HUNTER_GATHER:
{ {
if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend) if (evaluationContext.enemyHeroDangerRatio > dangerThreshold && !evaluationContext.isDefend && priorityTier != PriorityTier::FAR_HUNTER_GATHER)
return 0; return 0;
if (evaluationContext.buildingCost.marketValue() > 0) if (evaluationContext.buildingCost.marketValue() > 0)
return 0; return 0;
if (evaluationContext.isDefend && (evaluationContext.enemyHeroDangerRatio < 1 || evaluationContext.threatTurns > 0 || evaluationContext.turn > 0)) if (priorityTier != PriorityTier::FAR_HUNTER_GATHER && evaluationContext.isDefend && (evaluationContext.enemyHeroDangerRatio > dangerThreshold || evaluationContext.threatTurns > 0 || evaluationContext.turn > 0))
return 0; return 0;
if (evaluationContext.explorePriority == 3) if (evaluationContext.explorePriority == 3)
return 0; return 0;
if (evaluationContext.isArmyUpgrade) if (priorityTier != PriorityTier::FAR_HUNTER_GATHER && ((evaluationContext.enemyHeroDangerRatio > 0 && arriveNextWeek) || evaluationContext.enemyHeroDangerRatio > dangerThreshold))
return 0;
if ((evaluationContext.enemyHeroDangerRatio > 0 && arriveNextWeek) || evaluationContext.enemyHeroDangerRatio > 1)
return 0; return 0;
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0; return 0;
@ -1475,12 +1572,14 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
score = 1000; score = 1000;
if (evaluationContext.movementCost > 0) if (evaluationContext.movementCost > 0)
score /= evaluationContext.movementCost; score /= evaluationContext.movementCost;
if(priorityTier == PriorityTier::FAR_HUNTER_GATHER && evaluationContext.enemyHeroDangerRatio > 0)
score /= evaluationContext.enemyHeroDangerRatio;
} }
break; break;
} }
case PriorityTier::LOW_PRIO_EXPLORE: case PriorityTier::LOW_PRIO_EXPLORE:
{ {
if (evaluationContext.enemyHeroDangerRatio > 1) if (evaluationContext.enemyHeroDangerRatio > dangerThreshold)
return 0; return 0;
if (evaluationContext.explorePriority != 3) if (evaluationContext.explorePriority != 3)
return 0; return 0;
@ -1495,7 +1594,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
} }
case PriorityTier::DEFEND: //Defend whatever if nothing else is to do case PriorityTier::DEFEND: //Defend whatever if nothing else is to do
{ {
if (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.isExchange) if (evaluationContext.enemyHeroDangerRatio > dangerThreshold)
return 0; return 0;
if (evaluationContext.isDefend || evaluationContext.isArmyUpgrade) if (evaluationContext.isDefend || evaluationContext.isArmyUpgrade)
score = evaluationContext.armyInvolvement; score = evaluationContext.armyInvolvement;
@ -1536,9 +1635,15 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
TResources needed = evaluationContext.buildingCost - resourcesAvailable; TResources needed = evaluationContext.buildingCost - resourcesAvailable;
needed.positive(); needed.positive();
int turnsTo = needed.maxPurchasableCount(income); int turnsTo = needed.maxPurchasableCount(income);
bool haveEverythingButGold = true;
for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; i++)
{
if (i != GameResID::GOLD && resourcesAvailable[i] < evaluationContext.buildingCost[i])
haveEverythingButGold = false;
}
if (turnsTo == INT_MAX) if (turnsTo == INT_MAX)
return 0; return 0;
else if (!haveEverythingButGold)
score /= turnsTo; score /= turnsTo;
} }
} }

View File

@ -84,6 +84,7 @@ struct DLL_EXPORT EvaluationContext
bool isHero; bool isHero;
bool isEnemy; bool isEnemy;
int explorePriority; int explorePriority;
float powerRatio;
EvaluationContext(const Nullkiller * ai); EvaluationContext(const Nullkiller * ai);
@ -114,13 +115,13 @@ public:
INSTAKILL, INSTAKILL,
INSTADEFEND, INSTADEFEND,
KILL, KILL,
UPGRADE,
HIGH_PRIO_EXPLORE, HIGH_PRIO_EXPLORE,
HUNTER_GATHER, HUNTER_GATHER,
LOW_PRIO_EXPLORE, LOW_PRIO_EXPLORE,
FAR_KILL, FAR_KILL,
DEFEND,
FAR_HUNTER_GATHER, FAR_HUNTER_GATHER,
DEFEND MAX_PRIORITY_TIER = FAR_HUNTER_GATHER
}; };
private: private:

View File

@ -1447,9 +1447,20 @@ void AINodeStorage::calculateChainInfo(std::vector<AIPath> & paths, const int3 &
} }
} }
int fortLevel = 0;
auto visitableObjects = cb->getVisitableObjs(pos);
for (auto obj : visitableObjects)
{
if (objWithID<Obj::TOWN>(obj))
{
auto town = dynamic_cast<const CGTownInstance*>(obj);
fortLevel = town->fortLevel();
}
}
path.targetObjectArmyLoss = evaluateArmyLoss( path.targetObjectArmyLoss = evaluateArmyLoss(
path.targetHero, path.targetHero,
getHeroArmyStrengthWithCommander(path.targetHero, path.heroArmy), getHeroArmyStrengthWithCommander(path.targetHero, path.heroArmy, fortLevel),
path.targetObjectDanger); path.targetObjectDanger);
path.chainMask = node.actor->chainMask; path.chainMask = node.actor->chainMask;

View File

@ -353,11 +353,23 @@ bool CCreatureSet::needsLastStack() const
return false; return false;
} }
ui64 CCreatureSet::getArmyStrength() const ui64 CCreatureSet::getArmyStrength(int fortLevel) const
{ {
ui64 ret = 0; ui64 ret = 0;
for(const auto & elem : stacks) for (const auto& elem : stacks)
ret += elem.second->getPower(); {
ui64 powerToAdd = elem.second->getPower();
if (fortLevel > 0)
{
if (!elem.second->hasBonusOfType(BonusType::FLYING))
{
powerToAdd /= fortLevel;
if (!elem.second->hasBonusOfType(BonusType::SHOOTER))
powerToAdd /= fortLevel;
}
}
ret += powerToAdd;
}
return ret; return ret;
} }

View File

@ -283,7 +283,7 @@ public:
bool slotEmpty(const SlotID & slot) const; bool slotEmpty(const SlotID & slot) const;
int stacksCount() const; int stacksCount() const;
virtual bool needsLastStack() const; //true if last stack cannot be taken virtual bool needsLastStack() const; //true if last stack cannot be taken
ui64 getArmyStrength() const; //sum of AI values of creatures ui64 getArmyStrength(int fortLevel = 0) const; //sum of AI values of creatures
ui64 getArmyCost() const; //sum of cost of creatures ui64 getArmyCost() const; //sum of cost of creatures
ui64 getPower(const SlotID & slot) const; //value of specific stack ui64 getPower(const SlotID & slot) const; //value of specific stack
std::string getRoughAmount(const SlotID & slot, int mode = 0) const; //rough size of specific stack std::string getRoughAmount(const SlotID & slot, int mode = 0) const; //rough size of specific stack