|
|
@@ -66,7 +66,8 @@ EvaluationContext::EvaluationContext(const Nullkiller* ai)
|
|
|
isArmyUpgrade(false),
|
|
|
isHero(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))
|
|
|
: 0;
|
|
|
|
|
|
- case Obj::KEYMASTER:
|
|
|
- return 0.6f;
|
|
|
-
|
|
|
default:
|
|
|
return 0;
|
|
|
}
|
|
|
@@ -889,7 +887,14 @@ public:
|
|
|
|
|
|
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)
|
|
|
evaluationContext.isDefend = true;
|
|
|
else
|
|
|
@@ -1018,6 +1023,45 @@ public:
|
|
|
if(heroRole == HeroRole::MAIN)
|
|
|
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)
|
|
|
{
|
|
|
evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero);
|
|
|
@@ -1030,6 +1074,8 @@ public:
|
|
|
evaluationContext.isHero = true;
|
|
|
if (target->getOwner().isValidPlayer() && ai->cb->getPlayerRelations(ai->playerID, target->getOwner()) == PlayerRelations::ENEMIES)
|
|
|
evaluationContext.isEnemy = true;
|
|
|
+ if (target->ID == Obj::TOWN)
|
|
|
+ evaluationContext.defenseValue = dynamic_cast<const CGTownInstance*>(target)->fortLevel();
|
|
|
evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army);
|
|
|
if(evaluationContext.danger > 0)
|
|
|
evaluationContext.skillReward += (float)evaluationContext.danger / (float)hero->getArmyStrength();
|
|
|
@@ -1169,6 +1215,19 @@ public:
|
|
|
evaluationContext.goldCost += cost;
|
|
|
evaluationContext.closestWayRatio = 1;
|
|
|
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)
|
|
|
evaluationContext.isTradeBuilding = true;
|
|
|
|
|
|
@@ -1183,14 +1242,19 @@ public:
|
|
|
if(bi.baseCreatureID == bi.creatureID)
|
|
|
{
|
|
|
evaluationContext.addNonCriticalStrategicalValue((0.5f + 0.1f * bi.creatureLevel) / (float)bi.prerequisitesCount);
|
|
|
- evaluationContext.armyReward += bi.armyStrength;
|
|
|
+ evaluationContext.armyReward += bi.armyStrength * 1.5;
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
auto potentialUpgradeValue = evaluationContext.evaluator.getUpgradeArmyReward(buildThis.town, bi);
|
|
|
|
|
|
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)
|
|
|
@@ -1201,9 +1265,14 @@ public:
|
|
|
else if(bi.id >= BuildingID::MAGES_GUILD_1 && bi.id <= BuildingID::MAGES_GUILD_5)
|
|
|
{
|
|
|
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;
|
|
|
@@ -1333,18 +1402,35 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
|
|
|
else
|
|
|
{
|
|
|
float score = 0;
|
|
|
- const bool amIInDanger = ai->cb->getTownsInfo().empty() || (evaluationContext.isDefend && evaluationContext.threatTurns == 0);
|
|
|
- const float maxWillingToLose = amIInDanger ? 1 : ai->settings->getMaxArmyLossTarget();
|
|
|
+ bool currentPositionThreatened = false;
|
|
|
+ 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;
|
|
|
if (ai->cb->getDate(Date::DAY_OF_WEEK) + evaluationContext.turn > 7 && priorityTier < PriorityTier::FAR_KILL)
|
|
|
arriveNextWeek = true;
|
|
|
|
|
|
#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,
|
|
|
task->toString(),
|
|
|
evaluationContext.armyLossPersentage,
|
|
|
+ maxWillingToLose,
|
|
|
(int)evaluationContext.turn,
|
|
|
evaluationContext.movementCostByRole[HeroRole::MAIN],
|
|
|
evaluationContext.movementCostByRole[HeroRole::SCOUT],
|
|
|
@@ -1362,23 +1448,27 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
|
|
|
evaluationContext.conquestValue,
|
|
|
evaluationContext.closestWayRatio,
|
|
|
evaluationContext.enemyHeroDangerRatio,
|
|
|
+ dangerThreshold,
|
|
|
evaluationContext.explorePriority,
|
|
|
evaluationContext.isDefend,
|
|
|
evaluationContext.isEnemy,
|
|
|
- arriveNextWeek);
|
|
|
+ arriveNextWeek,
|
|
|
+ evaluationContext.powerRatio);
|
|
|
#endif
|
|
|
|
|
|
switch (priorityTier)
|
|
|
{
|
|
|
case PriorityTier::INSTAKILL: //Take towns / kill heroes in immediate reach
|
|
|
{
|
|
|
- if (evaluationContext.turn > 0)
|
|
|
+ if (evaluationContext.turn > 0 || evaluationContext.isExchange)
|
|
|
return 0;
|
|
|
if (evaluationContext.movementCost >= 1)
|
|
|
return 0;
|
|
|
+ if (evaluationContext.defenseValue < 2 && evaluationContext.enemyHeroDangerRatio > dangerThreshold)
|
|
|
+ return 0;
|
|
|
if(evaluationContext.conquestValue > 0)
|
|
|
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;
|
|
|
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
|
|
|
return 0;
|
|
|
@@ -1388,23 +1478,47 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
|
|
|
}
|
|
|
case PriorityTier::INSTADEFEND: //Defend immediately threatened towns
|
|
|
{
|
|
|
- if (evaluationContext.isDefend && evaluationContext.threatTurns == 0 && evaluationContext.turn == 0)
|
|
|
- score = evaluationContext.armyInvolvement;
|
|
|
- if (evaluationContext.isEnemy && maxWillingToLose - evaluationContext.armyLossPersentage < 0)
|
|
|
+ //No point defending if we don't have defensive-structures
|
|
|
+ if (evaluationContext.defenseValue < 2)
|
|
|
+ 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;
|
|
|
}
|
|
|
case PriorityTier::KILL: //Take towns / kill heroes that are further away
|
|
|
//FALL_THROUGH
|
|
|
case PriorityTier::FAR_KILL:
|
|
|
{
|
|
|
+ if (evaluationContext.defenseValue < 2 && evaluationContext.enemyHeroDangerRatio > dangerThreshold)
|
|
|
+ return 0;
|
|
|
if (evaluationContext.turn > 0 && evaluationContext.isHero)
|
|
|
return 0;
|
|
|
if (arriveNextWeek && evaluationContext.isEnemy)
|
|
|
return 0;
|
|
|
if (evaluationContext.conquestValue > 0)
|
|
|
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;
|
|
|
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
|
|
|
return 0;
|
|
|
@@ -1413,24 +1527,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
|
|
|
score /= evaluationContext.movementCost;
|
|
|
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:
|
|
|
{
|
|
|
- if (evaluationContext.enemyHeroDangerRatio > 1)
|
|
|
+ if (evaluationContext.enemyHeroDangerRatio > dangerThreshold)
|
|
|
return 0;
|
|
|
if (evaluationContext.explorePriority != 1)
|
|
|
return 0;
|
|
|
@@ -1447,17 +1546,15 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
|
|
|
//FALL_THROUGH
|
|
|
case PriorityTier::FAR_HUNTER_GATHER:
|
|
|
{
|
|
|
- if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend)
|
|
|
+ if (evaluationContext.enemyHeroDangerRatio > dangerThreshold && !evaluationContext.isDefend && priorityTier != PriorityTier::FAR_HUNTER_GATHER)
|
|
|
return 0;
|
|
|
if (evaluationContext.buildingCost.marketValue() > 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;
|
|
|
if (evaluationContext.explorePriority == 3)
|
|
|
return 0;
|
|
|
- if (evaluationContext.isArmyUpgrade)
|
|
|
- return 0;
|
|
|
- if ((evaluationContext.enemyHeroDangerRatio > 0 && arriveNextWeek) || evaluationContext.enemyHeroDangerRatio > 1)
|
|
|
+ if (priorityTier != PriorityTier::FAR_HUNTER_GATHER && ((evaluationContext.enemyHeroDangerRatio > 0 && arriveNextWeek) || evaluationContext.enemyHeroDangerRatio > dangerThreshold))
|
|
|
return 0;
|
|
|
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
|
|
|
return 0;
|
|
|
@@ -1475,12 +1572,14 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
|
|
|
score = 1000;
|
|
|
if (evaluationContext.movementCost > 0)
|
|
|
score /= evaluationContext.movementCost;
|
|
|
+ if(priorityTier == PriorityTier::FAR_HUNTER_GATHER && evaluationContext.enemyHeroDangerRatio > 0)
|
|
|
+ score /= evaluationContext.enemyHeroDangerRatio;
|
|
|
}
|
|
|
break;
|
|
|
}
|
|
|
case PriorityTier::LOW_PRIO_EXPLORE:
|
|
|
{
|
|
|
- if (evaluationContext.enemyHeroDangerRatio > 1)
|
|
|
+ if (evaluationContext.enemyHeroDangerRatio > dangerThreshold)
|
|
|
return 0;
|
|
|
if (evaluationContext.explorePriority != 3)
|
|
|
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
|
|
|
{
|
|
|
- if (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.isExchange)
|
|
|
+ if (evaluationContext.enemyHeroDangerRatio > dangerThreshold)
|
|
|
return 0;
|
|
|
if (evaluationContext.isDefend || evaluationContext.isArmyUpgrade)
|
|
|
score = evaluationContext.armyInvolvement;
|
|
|
@@ -1536,9 +1635,15 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
|
|
|
TResources needed = evaluationContext.buildingCost - resourcesAvailable;
|
|
|
needed.positive();
|
|
|
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)
|
|
|
return 0;
|
|
|
- else
|
|
|
+ if (!haveEverythingButGold)
|
|
|
score /= turnsTo;
|
|
|
}
|
|
|
}
|