2025-08-13 14:41:15 +02:00
/*
* PriorityEvaluator . 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 <limits>
2025-08-13 17:05:30 +02:00
# include "Nullkiller.h"
2025-08-13 14:41:15 +02:00
# include "../../../lib/entities/artifact/CArtifact.h"
# include "../../../lib/mapObjectConstructors/AObjectTypeHandler.h"
# include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h"
# include "../../../lib/mapObjects/CGResource.h"
# include "../../../lib/mapping/TerrainTile.h"
# include "../../../lib/RoadHandler.h"
# include "../../../lib/CCreatureHandler.h"
# include "../../../lib/GameLibrary.h"
# include "../../../lib/StartInfo.h"
# include "../../../lib/GameSettings.h"
# include "../../../lib/filesystem/Filesystem.h"
# include "../Goals/ExecuteHeroChain.h"
# include "../Goals/BuildThis.h"
# include "../Goals/StayAtTown.h"
# include "../Goals/ExchangeSwapTownHeroes.h"
# include "../Goals/DismissHero.h"
# include "../Markers/UnlockCluster.h"
# include "../Markers/HeroExchange.h"
# include "../Markers/ArmyUpgrade.h"
# include "../Markers/DefendTown.h"
2025-08-13 17:16:27 +02:00
namespace NK2AI
2025-08-13 14:41:15 +02:00
{
2025-08-15 02:13:45 +02:00
constexpr float MAX_CRITICAL_VALUE = 2.0f ;
2025-08-13 14:41:15 +02:00
2025-08-13 17:05:30 +02:00
EvaluationContext : : EvaluationContext ( const Nullkiller * ai )
2025-08-13 14:41:15 +02:00
: movementCost ( 0.0 ) ,
manaCost ( 0 ) ,
danger ( 0 ) ,
closestWayRatio ( 1 ) ,
movementCostByRole ( ) ,
skillReward ( 0 ) ,
goldReward ( 0 ) ,
goldCost ( 0 ) ,
armyReward ( 0 ) ,
2025-08-15 02:13:45 +02:00
armyLossRatio ( 0 ) ,
2025-08-13 14:41:15 +02:00
heroRole ( HeroRole : : SCOUT ) ,
turn ( 0 ) ,
strategicalValue ( 0 ) ,
conquestValue ( 0 ) ,
evaluator ( ai ) ,
enemyHeroDangerRatio ( 0 ) ,
threat ( 0 ) ,
armyGrowth ( 0 ) ,
armyInvolvement ( 0 ) ,
defenseValue ( 0 ) ,
isDefend ( false ) ,
threatTurns ( INT_MAX ) ,
involvesSailing ( false ) ,
isTradeBuilding ( false ) ,
isExchange ( false ) ,
isArmyUpgrade ( false ) ,
isHero ( false ) ,
isEnemy ( false ) ,
explorePriority ( 0 ) ,
powerRatio ( 0 )
{
}
void EvaluationContext : : addNonCriticalStrategicalValue ( float value )
{
2025-08-15 02:13:45 +02:00
vstd : : amax ( strategicalValue , std : : min ( value , MAX_CRITICAL_VALUE ) ) ;
2025-08-13 14:41:15 +02:00
}
PriorityEvaluator : : ~ PriorityEvaluator ( )
{
delete engine ;
}
void PriorityEvaluator : : initVisitTile ( )
{
2025-08-13 18:47:59 +02:00
auto file = CResourceHandler : : get ( ) - > load ( ResourcePath ( " config/ai/nk2ai/object-priorities.txt " ) ) - > readAll ( ) ;
2025-08-13 14:41:15 +02:00
std : : string str = std : : string ( ( char * ) file . first . get ( ) , file . second ) ;
engine = fl : : FllImporter ( ) . fromString ( str ) ;
2025-08-15 02:13:45 +02:00
armyLossRatioVariable = engine - > getInputVariable ( " armyLoss " ) ;
2025-08-13 14:41:15 +02:00
armyGrowthVariable = engine - > getInputVariable ( " armyGrowth " ) ;
heroRoleVariable = engine - > getInputVariable ( " heroRole " ) ;
dangerVariable = engine - > getInputVariable ( " danger " ) ;
turnVariable = engine - > getInputVariable ( " turn " ) ;
mainTurnDistanceVariable = engine - > getInputVariable ( " mainTurnDistance " ) ;
scoutTurnDistanceVariable = engine - > getInputVariable ( " scoutTurnDistance " ) ;
goldRewardVsMovementVariable = engine - > getInputVariable ( " goldReward " ) ;
armyRewardVariable = engine - > getInputVariable ( " armyReward " ) ;
skillRewardVariable = engine - > getInputVariable ( " skillReward " ) ;
rewardTypeVariable = engine - > getInputVariable ( " rewardType " ) ;
closestHeroRatioVariable = engine - > getInputVariable ( " closestHeroRatio " ) ;
strategicalValueVariable = engine - > getInputVariable ( " strategicalValue " ) ;
goldPressureVariable = engine - > getInputVariable ( " goldPressure " ) ;
goldCostVariable = engine - > getInputVariable ( " goldCost " ) ;
fearVariable = engine - > getInputVariable ( " fear " ) ;
value = engine - > getOutputVariable ( " Value " ) ;
}
bool isAnotherAi ( const CGObjectInstance * obj , const CPlayerSpecificInfoCallback & cb )
{
return obj - > getOwner ( ) . isValidPlayer ( )
& & cb . getStartInfo ( ) - > getIthPlayersSettings ( obj - > getOwner ( ) ) . isControlledByAI ( ) ;
}
int32_t estimateTownIncome ( CCallback * cb , const CGObjectInstance * target , const CGHeroInstance * hero )
{
auto relations = cb - > getPlayerRelations ( hero - > tempOwner , target - > tempOwner ) ;
if ( relations ! = PlayerRelations : : ENEMIES )
return 0 ; // if we already own it, no additional reward will be received by just visiting it
auto booster = isAnotherAi ( target , * cb ) ? 1 : 2 ;
auto town = cb - > getTown ( target - > id ) ;
auto fortLevel = town - > fortLevel ( ) ;
if ( town - > hasCapitol ( ) )
return booster * 2000 ;
// probably well developed town will have city hall
if ( fortLevel = = CGTownInstance : : CASTLE ) return booster * 750 ;
return booster * ( town - > hasFort ( ) & & town - > tempOwner ! = PlayerColor : : NEUTRAL ? booster * 500 : 250 ) ;
}
int32_t getResourcesGoldReward ( const TResources & res )
{
int32_t result = 0 ;
for ( auto r : GameResID : : ALL_RESOURCES ( ) )
{
if ( res [ r ] > 0 )
result + = r = = EGameResID : : GOLD ? res [ r ] : res [ r ] * 100 ;
}
return result ;
}
uint64_t getDwellingArmyValue ( CCallback * cb , const CGObjectInstance * target , bool checkGold )
{
auto dwelling = dynamic_cast < const CGDwelling * > ( target ) ;
uint64_t score = 0 ;
for ( auto & creLevel : dwelling - > creatures )
{
if ( creLevel . first & & creLevel . second . size ( ) )
{
auto creature = creLevel . second . back ( ) . toCreature ( ) ;
auto creaturesAreFree = creature - > getLevel ( ) = = 1 ;
if ( ! creaturesAreFree & & checkGold & & ! cb - > getResourceAmount ( ) . canAfford ( creature - > getFullRecruitCost ( ) * creLevel . first ) )
continue ;
score + = creature - > getAIValue ( ) * creLevel . first ;
}
}
return score ;
}
uint64_t getDwellingArmyGrowth ( CCallback * cb , const CGObjectInstance * target , PlayerColor myColor )
{
auto dwelling = dynamic_cast < const CGDwelling * > ( target ) ;
uint64_t score = 0 ;
if ( dwelling - > getOwner ( ) = = myColor )
return 0 ;
for ( auto & creLevel : dwelling - > creatures )
{
if ( creLevel . second . size ( ) )
{
auto creature = creLevel . second . back ( ) . toCreature ( ) ;
score + = creature - > getAIValue ( ) * creature - > getGrowth ( ) ;
}
}
return score ;
}
int getDwellingArmyCost ( const CGObjectInstance * target )
{
auto dwelling = dynamic_cast < const CGDwelling * > ( target ) ;
int cost = 0 ;
for ( auto & creLevel : dwelling - > creatures )
{
if ( creLevel . first & & creLevel . second . size ( ) )
{
auto creature = creLevel . second . back ( ) . toCreature ( ) ;
auto creaturesAreFree = creature - > getLevel ( ) = = 1 ;
if ( ! creaturesAreFree )
cost + = creature - > getFullRecruitCost ( ) . marketValue ( ) * creLevel . first ;
}
}
return cost ;
}
static uint64_t evaluateSpellScrollArmyValue ( const SpellID & )
{
return 1500 ;
}
static uint64_t evaluateArtifactArmyValue ( const CArtifact * art )
{
if ( art - > getId ( ) = = ArtifactID : : SPELL_SCROLL )
return 1500 ;
return getPotentialArtifactScore ( art ) ;
}
uint64_t RewardEvaluator : : getArmyReward (
const CGObjectInstance * target ,
const CGHeroInstance * hero ,
const CCreatureSet * army ,
bool checkGold ) const
{
const float enemyArmyEliminationRewardRatio = 0.5f ;
2025-08-15 18:30:28 +02:00
auto relations = aiNk - > cbc - > getPlayerRelations ( target - > tempOwner , aiNk - > playerID ) ;
2025-08-13 14:41:15 +02:00
if ( ! target )
return 0 ;
switch ( target - > ID )
{
case Obj : : HILL_FORT :
2025-08-15 18:30:28 +02:00
return aiNk - > armyManager - > calculateCreaturesUpgrade ( army , target , aiNk - > cbc - > getResourceAmount ( ) ) . upgradeValue ;
2025-08-13 14:41:15 +02:00
case Obj : : CREATURE_GENERATOR1 :
case Obj : : CREATURE_GENERATOR2 :
case Obj : : CREATURE_GENERATOR3 :
case Obj : : CREATURE_GENERATOR4 :
2025-08-15 18:30:28 +02:00
return getDwellingArmyValue ( aiNk - > cbc . get ( ) , target , checkGold ) ;
2025-08-13 14:41:15 +02:00
case Obj : : SPELL_SCROLL :
return evaluateSpellScrollArmyValue ( dynamic_cast < const CGArtifact * > ( target ) - > getArtifactInstance ( ) - > getScrollSpellID ( ) ) ;
case Obj : : ARTIFACT :
return evaluateArtifactArmyValue ( dynamic_cast < const CGArtifact * > ( target ) - > getArtifactInstance ( ) - > getType ( ) ) ;
case Obj : : HERO :
return relations = = PlayerRelations : : ENEMIES
? enemyArmyEliminationRewardRatio * dynamic_cast < const CGHeroInstance * > ( target ) - > getArmyStrength ( )
: 0 ;
case Obj : : PANDORAS_BOX :
return 5000 ;
case Obj : : MAGIC_WELL :
case Obj : : MAGIC_SPRING :
return getManaRecoveryArmyReward ( hero ) ;
default :
break ;
}
auto rewardable = dynamic_cast < const Rewardable : : Interface * > ( target ) ;
if ( rewardable )
{
auto totalValue = 0 ;
2025-08-15 18:30:28 +02:00
2025-08-13 14:41:15 +02:00
for ( int index : rewardable - > getAvailableRewards ( hero , Rewardable : : EEventType : : EVENT_FIRST_VISIT ) )
{
auto & info = rewardable - > configuration . info [ index ] ;
auto rewardValue = 0 ;
for ( auto artID : info . reward . grantedArtifacts )
rewardValue + = evaluateArtifactArmyValue ( artID . toArtifact ( ) ) ;
for ( auto scroll : info . reward . grantedScrolls )
rewardValue + = evaluateSpellScrollArmyValue ( scroll ) ;
for ( const auto & stackInfo : info . reward . creatures )
rewardValue + = stackInfo . getType ( ) - > getAIValue ( ) * stackInfo . getCount ( ) ;
totalValue + = rewardValue > 0 ? rewardValue / ( info . reward . grantedArtifacts . size ( ) + info . reward . creatures . size ( ) ) : 0 ;
}
return totalValue ;
}
return 0 ;
}
uint64_t RewardEvaluator : : getArmyGrowth (
const CGObjectInstance * target ,
const CGHeroInstance * hero ,
const CCreatureSet * army ) const
{
if ( ! target )
return 0 ;
2025-08-15 18:30:28 +02:00
auto relations = aiNk - > cbc - > getPlayerRelations ( target - > tempOwner , hero - > tempOwner ) ;
2025-08-13 14:41:15 +02:00
if ( relations ! = PlayerRelations : : ENEMIES )
return 0 ;
switch ( target - > ID )
{
case Obj : : TOWN :
{
auto town = dynamic_cast < const CGTownInstance * > ( target ) ;
auto fortLevel = town - > fortLevel ( ) ;
auto neutral = ! town - > getOwner ( ) . isValidPlayer ( ) ;
2025-08-15 18:30:28 +02:00
auto booster = isAnotherAi ( town , * aiNk - > cbc ) | | neutral ? 1 : 2 ;
2025-08-13 14:41:15 +02:00
if ( fortLevel < CGTownInstance : : CITADEL )
return town - > hasFort ( ) ? booster * 500 : 0 ;
else
return booster * ( fortLevel = = CGTownInstance : : CASTLE ? 5000 : 2000 ) ;
}
case Obj : : CREATURE_GENERATOR1 :
case Obj : : CREATURE_GENERATOR2 :
case Obj : : CREATURE_GENERATOR3 :
case Obj : : CREATURE_GENERATOR4 :
2025-08-15 18:30:28 +02:00
return getDwellingArmyGrowth ( aiNk - > cbc . get ( ) , target , hero - > getOwner ( ) ) ;
2025-08-13 14:41:15 +02:00
case Obj : : ARTIFACT :
// it is not supported now because hero will not sit in town on 7th day but later parts of legion may be counted as army growth as well.
return 0 ;
default :
return 0 ;
}
}
int RewardEvaluator : : getGoldCost ( const CGObjectInstance * target , const CGHeroInstance * hero , const CCreatureSet * army ) const
{
if ( ! target )
return 0 ;
2025-08-15 18:30:28 +02:00
2025-08-13 14:41:15 +02:00
if ( auto * m = dynamic_cast < const IMarket * > ( target ) )
{
if ( m - > allowsTrade ( EMarketMode : : RESOURCE_SKILL ) )
return 2000 ;
}
switch ( target - > ID )
{
case Obj : : HILL_FORT :
2025-08-15 18:30:28 +02:00
return aiNk - > armyManager - > calculateCreaturesUpgrade ( army , target , aiNk - > cbc - > getResourceAmount ( ) ) . upgradeCost [ EGameResID : : GOLD ] ;
2025-08-13 14:41:15 +02:00
case Obj : : SCHOOL_OF_MAGIC :
case Obj : : SCHOOL_OF_WAR :
return 1000 ;
case Obj : : CREATURE_GENERATOR1 :
case Obj : : CREATURE_GENERATOR2 :
case Obj : : CREATURE_GENERATOR3 :
case Obj : : CREATURE_GENERATOR4 :
return getDwellingArmyCost ( target ) ;
default :
return 0 ;
}
}
float RewardEvaluator : : getEnemyHeroStrategicalValue ( const CGHeroInstance * enemy ) const
{
2025-08-15 18:30:28 +02:00
auto objectsUnderThreat = aiNk - > dangerHitMap - > getOneTurnAccessibleObjects ( enemy ) ;
2025-08-13 14:41:15 +02:00
float objectValue = 0 ;
2025-08-14 03:47:32 +02:00
for ( auto obj : objectsUnderThreat )
2025-08-13 14:41:15 +02:00
{
vstd : : amax ( objectValue , getStrategicalValue ( obj ) ) ;
}
/*
1. If an enemy hero can attack nearby object , it ' s not useful to capture the object on our own .
Killing the hero is almost as important ( 0.9 ) as capturing the object itself .
2. The formula quickly approaches 1.0 as hero level increases ,
but higher level always means higher value and the minimal value for level 1 hero is 0.5
*/
return std : : min ( 1.5f , objectValue * 0.9f + ( 1.5f - ( 1.5f / ( 1 + enemy - > level ) ) ) ) ;
}
/**
* getNowResourceRequirementStrength
* @ param resType
* @ return between 0 - 1.0f
*/
float RewardEvaluator : : getResourceRequirementStrength ( GameResID resType ) const
{
2025-08-15 18:30:28 +02:00
TResources requiredResources = aiNk - > buildAnalyzer - > getResourcesRequiredNow ( ) ;
TResources dailyIncome = aiNk - > buildAnalyzer - > getDailyIncome ( ) ;
2025-08-13 14:41:15 +02:00
if ( requiredResources [ resType ] = = 0 )
return 0 ;
if ( dailyIncome [ resType ] = = 0 )
return 1.0f ;
return 0.95f ;
}
/**
*
* @ param resType
* @ return between 0 - 1.0f
*/
float RewardEvaluator : : getTotalResourceRequirementStrength ( GameResID resType ) const
{
2025-08-15 18:30:28 +02:00
TResources requiredResources = aiNk - > buildAnalyzer - > getTotalResourcesRequired ( ) ;
TResources dailyIncome = aiNk - > buildAnalyzer - > getDailyIncome ( ) ;
2025-08-13 14:41:15 +02:00
if ( requiredResources [ resType ] = = 0 )
return 0 ;
if ( dailyIncome [ resType ] = = 0 )
return 1.0f ;
return 0.95f ;
}
uint64_t RewardEvaluator : : townArmyGrowth ( const CGTownInstance * town ) const
{
uint64_t result = 0 ;
for ( auto creatureInfo : town - > creatures )
{
if ( creatureInfo . second . empty ( ) )
continue ;
auto creature = creatureInfo . second . back ( ) . toCreature ( ) ;
result + = creature - > getAIValue ( ) * town - > getGrowthInfo ( creature - > getLevel ( ) - 1 ) . totalGrowth ( ) ;
}
return result ;
}
float RewardEvaluator : : getManaRecoveryArmyReward ( const CGHeroInstance * hero ) const
{
2025-08-15 18:30:28 +02:00
return aiNk - > heroManager - > getMagicStrength ( hero ) * 10000 * ( 1.0f - std : : sqrt ( static_cast < float > ( hero - > mana ) / hero - > manaLimit ( ) ) ) ;
2025-08-13 14:41:15 +02:00
}
/**
* getCombinedResourceRequirementStrength
* @ param res
* @ return between 0 - 1.0f
*/
float RewardEvaluator : : getResourceRequirementStrength ( const TResources & res ) const
{
float sum = 0.0f ;
for ( TResources : : nziterator it ( res ) ; it . valid ( ) ; it + + )
{
auto calculation = 0.6f * getResourceRequirementStrength ( it - > resType )
+ 0.4f * getTotalResourceRequirementStrength ( it - > resType ) ;
// Even not required resources should be valuable because they shouldn't be left for the enemies to collect
sum + = std : : min ( 0.5f , calculation ) ;
}
return sum ;
}
float RewardEvaluator : : getStrategicalValue ( const CGObjectInstance * target , const CGHeroInstance * hero ) const
{
if ( ! target )
return 0 ;
switch ( target - > ID )
{
case Obj : : MINE :
{
auto mine = dynamic_cast < const CGMine * > ( target ) ;
TResources res ;
res [ mine - > producedResource ] = mine - > producedQuantity ;
// Mines should have higher priority than resources
return 1.0f + getResourceRequirementStrength ( res ) ;
}
case Obj : : RESOURCE :
{
auto resource = dynamic_cast < const CGResource * > ( target ) ;
TResources res ;
res [ resource - > resourceID ( ) ] = resource - > getAmount ( ) ;
return getResourceRequirementStrength ( res ) ;
}
case Obj : : TOWN :
{
2025-08-15 18:30:28 +02:00
if ( aiNk - > buildAnalyzer - > getDevelopmentInfo ( ) . empty ( ) )
2025-08-13 14:41:15 +02:00
return 10.0f ;
auto town = dynamic_cast < const CGTownInstance * > ( target ) ;
2025-08-15 18:30:28 +02:00
if ( town - > getOwner ( ) = = aiNk - > playerID )
2025-08-13 14:41:15 +02:00
{
auto armyIncome = townArmyGrowth ( town ) ;
auto dailyIncome = town - > dailyIncome ( ) [ EGameResID : : GOLD ] ;
return std : : min ( 1.0f , std : : sqrt ( armyIncome / 40000.0f ) ) + std : : min ( 0.3f , dailyIncome / 10000.0f ) ;
}
auto fortLevel = town - > fortLevel ( ) ;
2025-08-15 18:30:28 +02:00
auto booster = isAnotherAi ( town , * aiNk - > cbc ) ? 0.4f : 1.0f ;
2025-08-13 14:41:15 +02:00
if ( town - > hasCapitol ( ) )
return booster * 1.5 ;
if ( fortLevel < CGTownInstance : : CITADEL )
return booster * ( town - > hasFort ( ) ? 1.0 : 0.8 ) ;
else
return booster * ( fortLevel = = CGTownInstance : : CASTLE ? 1.4 : 1.2 ) ;
}
case Obj : : HERO :
2025-08-15 18:30:28 +02:00
return aiNk - > cbc - > getPlayerRelations ( target - > tempOwner , aiNk - > playerID ) = = PlayerRelations : : ENEMIES
2025-08-13 14:41:15 +02:00
? getEnemyHeroStrategicalValue ( dynamic_cast < const CGHeroInstance * > ( target ) )
: 0 ;
case Obj : : KEYMASTER :
return 0.6f ;
default :
break ;
}
auto rewardable = dynamic_cast < const Rewardable : : Interface * > ( target ) ;
if ( rewardable & & hero )
{
auto resourceReward = 0.0f ;
for ( int index : rewardable - > getAvailableRewards ( hero , Rewardable : : EEventType : : EVENT_FIRST_VISIT ) )
{
resourceReward + = getResourceRequirementStrength ( rewardable - > configuration . info [ index ] . reward . resources ) ;
}
return resourceReward ;
}
return 0 ;
}
float RewardEvaluator : : getConquestValue ( const CGObjectInstance * target ) const
{
if ( ! target )
return 0 ;
2025-08-15 18:30:28 +02:00
if ( target - > getOwner ( ) = = aiNk - > playerID )
2025-08-13 14:41:15 +02:00
return 0 ;
switch ( target - > ID )
{
case Obj : : TOWN :
{
2025-08-15 18:30:28 +02:00
if ( aiNk - > buildAnalyzer - > getDevelopmentInfo ( ) . empty ( ) )
2025-08-13 14:41:15 +02:00
return 10.0f ;
auto town = dynamic_cast < const CGTownInstance * > ( target ) ;
2025-08-15 18:30:28 +02:00
if ( town - > getOwner ( ) = = aiNk - > playerID )
2025-08-13 14:41:15 +02:00
{
auto armyIncome = townArmyGrowth ( town ) ;
auto dailyIncome = town - > dailyIncome ( ) [ EGameResID : : GOLD ] ;
return std : : min ( 1.0f , std : : sqrt ( armyIncome / 40000.0f ) ) + std : : min ( 0.3f , dailyIncome / 10000.0f ) ;
}
auto fortLevel = town - > fortLevel ( ) ;
auto booster = 1.0f ;
if ( town - > hasCapitol ( ) )
return booster * 1.5 ;
if ( fortLevel < CGTownInstance : : CITADEL )
return booster * ( town - > hasFort ( ) ? 1.0 : 0.8 ) ;
else
return booster * ( fortLevel = = CGTownInstance : : CASTLE ? 1.4 : 1.2 ) ;
}
case Obj : : HERO :
2025-08-15 18:30:28 +02:00
return aiNk - > cbc - > getPlayerRelations ( target - > tempOwner , aiNk - > playerID ) = = PlayerRelations : : ENEMIES
2025-08-13 14:41:15 +02:00
? getEnemyHeroStrategicalValue ( dynamic_cast < const CGHeroInstance * > ( target ) )
: 0 ;
default :
return 0 ;
}
}
float RewardEvaluator : : evaluateWitchHutSkillScore ( const CGObjectInstance * hut , const CGHeroInstance * hero , HeroRole role ) const
{
auto rewardable = dynamic_cast < const CRewardableObject * > ( hut ) ;
assert ( rewardable ) ;
auto skill = SecondarySkill ( * rewardable - > configuration . getVariable ( " secondarySkill " , " gainedSkill " ) ) ;
if ( ! hut - > wasVisited ( hero - > tempOwner ) )
return role = = HeroRole : : SCOUT ? 2 : 0 ;
if ( hero - > getSecSkillLevel ( skill ) ! = MasteryLevel : : NONE
2025-08-16 00:22:18 +02:00
| | static_cast < int > ( hero - > secSkills . size ( ) ) > = cbcTl - > getSettings ( ) . getInteger ( EGameSettings : : HEROES_SKILL_PER_HERO ) )
2025-08-13 14:41:15 +02:00
return 0 ;
2025-08-15 18:30:28 +02:00
auto score = aiNk - > heroManager - > evaluateSecSkill ( skill , hero ) ;
2025-08-13 14:41:15 +02:00
return score > = 2 ? ( role = = HeroRole : : MAIN ? 10 : 4 ) : score ;
}
float RewardEvaluator : : getSkillReward ( const CGObjectInstance * target , const CGHeroInstance * hero , HeroRole role ) const
{
const float enemyHeroEliminationSkillRewardRatio = 0.5f ;
if ( ! target )
return 0 ;
switch ( target - > ID )
{
case Obj : : STAR_AXIS :
case Obj : : SCHOLAR :
case Obj : : SCHOOL_OF_MAGIC :
case Obj : : SCHOOL_OF_WAR :
case Obj : : GARDEN_OF_REVELATION :
case Obj : : MARLETTO_TOWER :
case Obj : : MERCENARY_CAMP :
case Obj : : TREE_OF_KNOWLEDGE :
return 1 ;
case Obj : : LEARNING_STONE :
return 1.0f / std : : sqrt ( hero - > level ) ;
case Obj : : ARENA :
return 2 ;
case Obj : : SHRINE_OF_MAGIC_INCANTATION :
return 0.25f ;
case Obj : : SHRINE_OF_MAGIC_GESTURE :
return 1.0f ;
case Obj : : SHRINE_OF_MAGIC_THOUGHT :
return 2.0f ;
case Obj : : LIBRARY_OF_ENLIGHTENMENT :
return 8 ;
case Obj : : WITCH_HUT :
return evaluateWitchHutSkillScore ( target , hero , role ) ;
case Obj : : PANDORAS_BOX :
//Can contains experience, spells, or skills (only on custom maps)
return 2.5f ;
case Obj : : HERO :
2025-08-15 18:30:28 +02:00
return aiNk - > cbc - > getPlayerRelations ( target - > tempOwner , aiNk - > playerID ) = = PlayerRelations : : ENEMIES
2025-08-13 14:41:15 +02:00
? enemyHeroEliminationSkillRewardRatio * dynamic_cast < const CGHeroInstance * > ( target ) - > level
: 0 ;
default :
break ;
}
auto rewardable = dynamic_cast < const Rewardable : : Interface * > ( target ) ;
if ( rewardable )
{
auto totalValue = 0.0f ;
for ( int index : rewardable - > getAvailableRewards ( hero , Rewardable : : EEventType : : EVENT_FIRST_VISIT ) )
{
auto & info = rewardable - > configuration . info [ index ] ;
auto rewardValue = 0.0f ;
if ( ! info . reward . spells . empty ( ) )
{
for ( auto spellID : info . reward . spells )
{
const spells : : Spell * spell = LIBRARY - > spells ( ) - > getById ( spellID ) ;
2025-08-15 18:30:28 +02:00
2025-08-13 14:41:15 +02:00
if ( hero - > canLearnSpell ( spell ) & & ! hero - > spellbookContainsSpell ( spellID ) )
{
rewardValue + = std : : sqrt ( spell - > getLevel ( ) ) / 4.0f ;
}
}
totalValue + = rewardValue / info . reward . spells . size ( ) ;
}
if ( ! info . reward . primary . empty ( ) )
{
for ( auto value : info . reward . primary )
{
totalValue + = value ;
}
}
}
return totalValue ;
}
return 0 ;
}
const HitMapInfo & RewardEvaluator : : getEnemyHeroDanger ( const int3 & tile , uint8_t turn ) const
{
2025-08-15 18:30:28 +02:00
auto & threatNode = aiNk - > dangerHitMap - > getTileThreat ( tile ) ;
2025-08-13 14:41:15 +02:00
2025-08-14 03:47:32 +02:00
if ( threatNode . maximumDanger . danger = = 0 )
2025-08-13 14:41:15 +02:00
return HitMapInfo : : NoThreat ;
2025-08-14 03:47:32 +02:00
if ( threatNode . maximumDanger . turn < = turn )
return threatNode . maximumDanger ;
2025-08-13 14:41:15 +02:00
2025-08-14 03:47:32 +02:00
return threatNode . fastestDanger . turn < = turn ? threatNode . fastestDanger : HitMapInfo : : NoThreat ;
2025-08-13 14:41:15 +02:00
}
int32_t getArmyCost ( const CArmedInstance * army )
{
int32_t value = 0 ;
for ( const auto & stack : army - > Slots ( ) )
{
value + = stack . second - > getCreatureID ( ) . toCreature ( ) - > getFullRecruitCost ( ) . marketValue ( ) * stack . second - > getCount ( ) ;
}
return value ;
}
int32_t RewardEvaluator : : getGoldReward ( const CGObjectInstance * target , const CGHeroInstance * hero ) const
{
if ( ! target )
return 0 ;
2025-08-15 18:30:28 +02:00
auto relations = aiNk - > cbc - > getPlayerRelations ( target - > tempOwner , hero - > tempOwner ) ;
2025-08-13 14:41:15 +02:00
const int dailyIncomeMultiplier = 5 ;
const float enemyArmyEliminationGoldRewardRatio = 0.2f ;
const int32_t heroEliminationBonus = GameConstants : : HERO_GOLD_COST / 2 ;
switch ( target - > ID )
{
case Obj : : RESOURCE :
{
auto * res = dynamic_cast < const CGResource * > ( target ) ;
return res & & res - > resourceID ( ) = = GameResID : : GOLD ? 600 : 100 ;
}
case Obj : : TREASURE_CHEST :
return 1500 ;
case Obj : : WATER_WHEEL :
return 1000 ;
case Obj : : TOWN :
2025-08-15 18:30:28 +02:00
return dailyIncomeMultiplier * estimateTownIncome ( aiNk - > cbc . get ( ) , target , hero ) ;
2025-08-13 14:41:15 +02:00
case Obj : : MINE :
case Obj : : ABANDONED_MINE :
{
auto * mine = dynamic_cast < const CGMine * > ( target ) ;
return dailyIncomeMultiplier * ( mine - > producedResource = = GameResID : : GOLD ? 1000 : 75 ) ;
}
case Obj : : PANDORAS_BOX :
return 2500 ;
case Obj : : PRISON :
//Objectively saves us 2500 to hire hero
return GameConstants : : HERO_GOLD_COST ;
case Obj : : HERO :
return relations = = PlayerRelations : : ENEMIES
? heroEliminationBonus + enemyArmyEliminationGoldRewardRatio * getArmyCost ( dynamic_cast < const CGHeroInstance * > ( target ) )
: 0 ;
default :
break ;
}
auto rewardable = dynamic_cast < const Rewardable : : Interface * > ( target ) ;
if ( rewardable )
{
auto goldReward = 0 ;
for ( int index : rewardable - > getAvailableRewards ( hero , Rewardable : : EEventType : : EVENT_FIRST_VISIT ) )
{
auto & info = rewardable - > configuration . info [ index ] ;
goldReward + = getResourcesGoldReward ( info . reward . resources ) ;
}
return goldReward ;
}
return 0 ;
}
class HeroExchangeEvaluator : public IEvaluationContextBuilder
{
public :
void buildEvaluationContext ( EvaluationContext & evaluationContext , Goals : : TSubgoal task ) const override
{
if ( task - > goalType ! = Goals : : HERO_EXCHANGE )
return ;
Goals : : HeroExchange & heroExchange = dynamic_cast < Goals : : HeroExchange & > ( * task ) ;
2025-08-15 18:30:28 +02:00
uint64_t armyStrength = heroExchange . getReinforcementArmyStrength ( evaluationContext . evaluator . aiNk ) ;
2025-08-13 14:41:15 +02:00
evaluationContext . addNonCriticalStrategicalValue ( 2.0f * armyStrength / ( float ) heroExchange . hero - > getArmyStrength ( ) ) ;
evaluationContext . conquestValue + = 2.0f * armyStrength / ( float ) heroExchange . hero - > getArmyStrength ( ) ;
2025-08-15 18:30:28 +02:00
evaluationContext . heroRole = evaluationContext . evaluator . aiNk - > heroManager - > getHeroRole ( heroExchange . hero ) ;
2025-08-13 14:41:15 +02:00
evaluationContext . isExchange = true ;
}
} ;
class ArmyUpgradeEvaluator : public IEvaluationContextBuilder
{
public :
void buildEvaluationContext ( EvaluationContext & evaluationContext , Goals : : TSubgoal task ) const override
{
if ( task - > goalType ! = Goals : : ARMY_UPGRADE )
return ;
Goals : : ArmyUpgrade & armyUpgrade = dynamic_cast < Goals : : ArmyUpgrade & > ( * task ) ;
uint64_t upgradeValue = armyUpgrade . getUpgradeValue ( ) ;
evaluationContext . armyReward + = upgradeValue ;
evaluationContext . addNonCriticalStrategicalValue ( upgradeValue / ( float ) armyUpgrade . hero - > getArmyStrength ( ) ) ;
evaluationContext . isArmyUpgrade = true ;
}
} ;
class ExplorePointEvaluator : public IEvaluationContextBuilder
{
public :
void buildEvaluationContext ( EvaluationContext & evaluationContext , Goals : : TSubgoal task ) const override
{
if ( task - > goalType ! = Goals : : EXPLORATION_POINT )
return ;
int tilesDiscovered = task - > value ;
evaluationContext . addNonCriticalStrategicalValue ( 0.03f * tilesDiscovered ) ;
2025-08-15 18:30:28 +02:00
for ( auto obj : evaluationContext . evaluator . aiNk - > cbc - > getVisitableObjs ( task - > tile ) )
2025-08-13 14:41:15 +02:00
{
switch ( obj - > ID . num )
{
case Obj : : MONOLITH_ONE_WAY_ENTRANCE :
case Obj : : MONOLITH_TWO_WAY :
case Obj : : SUBTERRANEAN_GATE :
evaluationContext . explorePriority = 1 ;
break ;
case Obj : : REDWOOD_OBSERVATORY :
case Obj : : PILLAR_OF_FIRE :
evaluationContext . explorePriority = 2 ;
break ;
}
}
2025-08-15 18:30:28 +02:00
if ( evaluationContext . evaluator . aiNk - > cbc - > getTile ( task - > tile ) - > roadType ! = RoadId : : NO_ROAD )
2025-08-13 14:41:15 +02:00
evaluationContext . explorePriority = 1 ;
if ( evaluationContext . explorePriority = = 0 )
evaluationContext . explorePriority = 3 ;
}
} ;
class StayAtTownManaRecoveryEvaluator : public IEvaluationContextBuilder
{
public :
void buildEvaluationContext ( EvaluationContext & evaluationContext , Goals : : TSubgoal task ) const override
{
if ( task - > goalType ! = Goals : : STAY_AT_TOWN )
return ;
Goals : : StayAtTown & stayAtTown = dynamic_cast < Goals : : StayAtTown & > ( * task ) ;
if ( stayAtTown . getHero ( ) ! = nullptr & & stayAtTown . getHero ( ) - > movementPointsRemaining ( ) < 100 )
{
return ;
}
if ( stayAtTown . town - > mageGuildLevel ( ) > 0 )
evaluationContext . armyReward + = evaluationContext . evaluator . getManaRecoveryArmyReward ( stayAtTown . getHero ( ) ) ;
if ( vstd : : isAlmostZero ( evaluationContext . armyReward ) )
evaluationContext . isDefend = true ;
else
{
evaluationContext . movementCost + = stayAtTown . getMovementWasted ( ) ;
evaluationContext . movementCostByRole [ evaluationContext . heroRole ] + = stayAtTown . getMovementWasted ( ) ;
}
}
} ;
void addTileDanger ( EvaluationContext & evaluationContext , const int3 & tile , uint8_t turn , uint64_t ourStrength )
{
HitMapInfo enemyDanger = evaluationContext . evaluator . getEnemyHeroDanger ( tile , turn ) ;
if ( enemyDanger . danger )
{
auto dangerRatio = enemyDanger . danger / ( double ) ourStrength ;
vstd : : amax ( evaluationContext . enemyHeroDangerRatio , dangerRatio ) ;
vstd : : amax ( evaluationContext . threat , enemyDanger . threat ) ;
}
}
class DefendTownEvaluator : public IEvaluationContextBuilder
{
public :
void buildEvaluationContext ( EvaluationContext & evaluationContext , Goals : : TSubgoal task ) const override
{
if ( task - > goalType ! = Goals : : DEFEND_TOWN )
return ;
Goals : : DefendTown & defendTown = dynamic_cast < Goals : : DefendTown & > ( * task ) ;
const CGTownInstance * town = defendTown . town ;
2025-08-14 03:47:32 +02:00
auto & threat = defendTown . getThreat ( ) ;
2025-08-13 14:41:15 +02:00
auto strategicalValue = evaluationContext . evaluator . getStrategicalValue ( town ) ;
float multiplier = 1 ;
2025-08-14 03:47:32 +02:00
if ( threat . turn < defendTown . getTurn ( ) )
multiplier / = 1 + ( defendTown . getTurn ( ) - threat . turn ) ;
2025-08-13 14:41:15 +02:00
2025-08-14 03:47:32 +02:00
multiplier / = 1.0f + threat . turn / 5.0f ;
2025-08-13 14:41:15 +02:00
if ( defendTown . getTurn ( ) > 0 & & defendTown . isCounterAttack ( ) )
{
auto ourSpeed = defendTown . hero - > movementPointsLimit ( true ) ;
2025-08-15 18:30:28 +02:00
auto enemySpeed = threat . hero . get ( evaluationContext . evaluator . aiNk - > cbc . get ( ) ) - > movementPointsLimit ( true ) ;
2025-08-13 14:41:15 +02:00
if ( enemySpeed > ourSpeed ) multiplier * = 0.7f ;
}
auto dailyIncome = town - > dailyIncome ( ) [ EGameResID : : GOLD ] ;
auto armyGrowth = evaluationContext . evaluator . townArmyGrowth ( town ) ;
evaluationContext . armyGrowth + = armyGrowth * multiplier ;
evaluationContext . goldReward + = dailyIncome * 5 * multiplier ;
2025-08-15 18:30:28 +02:00
if ( evaluationContext . evaluator . aiNk - > buildAnalyzer - > getDevelopmentInfo ( ) . size ( ) = = 1 )
2025-08-13 14:41:15 +02:00
vstd : : amax ( evaluationContext . strategicalValue , 2.5f * multiplier * strategicalValue ) ;
else
evaluationContext . addNonCriticalStrategicalValue ( 1.7f * multiplier * strategicalValue ) ;
evaluationContext . defenseValue = town - > fortLevel ( ) ;
evaluationContext . isDefend = true ;
2025-08-14 03:47:32 +02:00
evaluationContext . threatTurns = threat . turn ;
2025-08-13 14:41:15 +02:00
2025-08-14 03:47:32 +02:00
vstd : : amax ( evaluationContext . danger , defendTown . getThreat ( ) . danger ) ;
2025-08-13 14:41:15 +02:00
addTileDanger ( evaluationContext , town - > visitablePos ( ) , defendTown . getTurn ( ) , defendTown . getDefenceStrength ( ) ) ;
}
} ;
class ExecuteHeroChainEvaluationContextBuilder : public IEvaluationContextBuilder
{
private :
2025-08-15 18:30:28 +02:00
const Nullkiller * aiNk ;
2025-08-13 14:41:15 +02:00
public :
2025-08-15 18:30:28 +02:00
ExecuteHeroChainEvaluationContextBuilder ( const Nullkiller * ai ) : aiNk ( ai ) { }
2025-08-13 14:41:15 +02:00
void buildEvaluationContext ( EvaluationContext & evaluationContext , Goals : : TSubgoal task ) const override
{
if ( task - > goalType ! = Goals : : EXECUTE_HERO_CHAIN )
return ;
Goals : : ExecuteHeroChain & chain = dynamic_cast < Goals : : ExecuteHeroChain & > ( * task ) ;
const AIPath & path = chain . getPath ( ) ;
if ( vstd : : isAlmostZero ( path . movementCost ( ) ) )
return ;
vstd : : amax ( evaluationContext . danger , path . getTotalDanger ( ) ) ;
evaluationContext . movementCost + = path . movementCost ( ) ;
evaluationContext . closestWayRatio = chain . closestWayRatio ;
std : : map < const CGHeroInstance * , float > costsPerHero ;
for ( auto & node : path . nodes )
{
vstd : : amax ( costsPerHero [ node . targetHero ] , node . cost ) ;
if ( node . layer = = EPathfindingLayer : : SAIL )
evaluationContext . involvesSailing = true ;
}
float highestCostForSingleHero = 0 ;
for ( auto pair : costsPerHero )
{
2025-08-15 18:30:28 +02:00
auto role = evaluationContext . evaluator . aiNk - > heroManager - > getHeroRole ( pair . first ) ;
2025-08-13 14:41:15 +02:00
evaluationContext . movementCostByRole [ role ] + = pair . second ;
if ( pair . second > highestCostForSingleHero )
highestCostForSingleHero = pair . second ;
}
if ( highestCostForSingleHero > 1 & & costsPerHero . size ( ) > 1 )
{
//Chains that involve more than 1 hero doing something for more than a turn are too expensive in my book. They often involved heroes doing nothing just standing there waiting to fulfill their part of the chain.
return ;
}
evaluationContext . movementCost * = costsPerHero . size ( ) ; //further deincentivise chaining as it often involves bringing back the army afterwards
auto hero = task - > hero ;
bool checkGold = evaluationContext . danger = = 0 ;
auto army = path . heroArmy ;
2025-08-15 18:30:28 +02:00
const CGObjectInstance * target = aiNk - > cbc - > getObj ( ( ObjectInstanceID ) task - > objid , false ) ;
auto heroRole = evaluationContext . evaluator . aiNk - > heroManager - > getHeroRole ( hero ) ;
2025-08-13 14:41:15 +02:00
if ( heroRole = = HeroRole : : MAIN )
evaluationContext . heroRole = heroRole ;
// 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 < CreatureID , float > totalPowerByCreatureID ;
// Calculate hero power and total power by CreatureID
for ( const auto & slot : hero - > Slots ( ) )
{
CreatureID 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
2025-08-15 18:30:28 +02:00
totalPowerByCreatureID [ creatureID ] = aiNk - > armyManager - > getTotalCreaturesAvailable ( creatureID ) . power ;
2025-08-13 14:41:15 +02:00
}
}
// 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 ) ;
evaluationContext . armyReward + = evaluationContext . evaluator . getArmyReward ( target , hero , army , checkGold ) ;
evaluationContext . armyGrowth + = evaluationContext . evaluator . getArmyGrowth ( target , hero , army ) ;
evaluationContext . skillReward + = evaluationContext . evaluator . getSkillReward ( target , hero , heroRole ) ;
evaluationContext . addNonCriticalStrategicalValue ( evaluationContext . evaluator . getStrategicalValue ( target ) ) ;
evaluationContext . conquestValue + = evaluationContext . evaluator . getConquestValue ( target ) ;
if ( target - > ID = = Obj : : HERO )
evaluationContext . isHero = true ;
2025-08-15 18:30:28 +02:00
if ( target - > getOwner ( ) . isValidPlayer ( ) & & aiNk - > cbc - > getPlayerRelations ( aiNk - > playerID , target - > getOwner ( ) ) = = PlayerRelations : : ENEMIES )
2025-08-13 14:41:15 +02:00
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 ( ) ;
}
evaluationContext . armyInvolvement + = army - > getArmyCost ( ) ;
2025-08-15 02:13:45 +02:00
vstd : : amax ( evaluationContext . armyLossRatio , ( float ) path . getTotalArmyLoss ( ) / ( float ) army - > getArmyStrength ( ) ) ;
2025-08-13 14:41:15 +02:00
addTileDanger ( evaluationContext , path . targetTile ( ) , path . turn ( ) , path . getHeroStrength ( ) ) ;
vstd : : amax ( evaluationContext . turn , path . turn ( ) ) ;
}
} ;
class ClusterEvaluationContextBuilder : public IEvaluationContextBuilder
{
public :
2025-08-13 17:05:30 +02:00
ClusterEvaluationContextBuilder ( const Nullkiller * ai ) { }
2025-08-13 14:41:15 +02:00
void buildEvaluationContext ( EvaluationContext & evaluationContext , Goals : : TSubgoal task ) const override
{
if ( task - > goalType ! = Goals : : UNLOCK_CLUSTER )
return ;
Goals : : UnlockCluster & clusterGoal = dynamic_cast < Goals : : UnlockCluster & > ( * task ) ;
std : : shared_ptr < ObjectCluster > cluster = clusterGoal . getCluster ( ) ;
auto hero = clusterGoal . hero ;
2025-08-15 18:30:28 +02:00
auto role = evaluationContext . evaluator . aiNk - > heroManager - > getHeroRole ( hero ) ;
2025-08-13 14:41:15 +02:00
std : : vector < std : : pair < ObjectInstanceID , ClusterObjectInfo > > objects ( cluster - > objects . begin ( ) , cluster - > objects . end ( ) ) ;
std : : sort ( objects . begin ( ) , objects . end ( ) , [ ] ( std : : pair < ObjectInstanceID , ClusterObjectInfo > o1 , std : : pair < ObjectInstanceID , ClusterObjectInfo > o2 ) - > bool
{
return o1 . second . priority > o2 . second . priority ;
} ) ;
int boost = 1 ;
for ( auto & objInfo : objects )
{
2025-08-15 18:30:28 +02:00
auto target = evaluationContext . evaluator . aiNk - > cbc - > getObj ( objInfo . first ) ;
2025-08-13 14:41:15 +02:00
bool checkGold = objInfo . second . danger = = 0 ;
auto army = hero ;
evaluationContext . goldReward + = evaluationContext . evaluator . getGoldReward ( target , hero ) / boost ;
evaluationContext . armyReward + = evaluationContext . evaluator . getArmyReward ( target , hero , army , checkGold ) / boost ;
evaluationContext . skillReward + = evaluationContext . evaluator . getSkillReward ( target , hero , role ) / boost ;
evaluationContext . addNonCriticalStrategicalValue ( evaluationContext . evaluator . getStrategicalValue ( target ) / boost ) ;
evaluationContext . conquestValue + = evaluationContext . evaluator . getConquestValue ( target ) ;
evaluationContext . goldCost + = evaluationContext . evaluator . getGoldCost ( target , hero , army ) / boost ;
evaluationContext . movementCostByRole [ role ] + = objInfo . second . movementCost / boost ;
evaluationContext . movementCost + = objInfo . second . movementCost / boost ;
vstd : : amax ( evaluationContext . turn , objInfo . second . turn / boost ) ;
boost < < = 1 ;
if ( boost > 8 )
break ;
}
}
} ;
class ExchangeSwapTownHeroesContextBuilder : public IEvaluationContextBuilder
{
public :
void buildEvaluationContext ( EvaluationContext & evaluationContext , Goals : : TSubgoal task ) const override
{
if ( task - > goalType ! = Goals : : EXCHANGE_SWAP_TOWN_HEROES )
return ;
Goals : : ExchangeSwapTownHeroes & swapCommand = dynamic_cast < Goals : : ExchangeSwapTownHeroes & > ( * task ) ;
const CGHeroInstance * garrisonHero = swapCommand . getGarrisonHero ( ) ;
logAi - > trace ( " buildEvaluationContext ExchangeSwapTownHeroesContextBuilder %s affected objects: %d " , swapCommand . toString ( ) , swapCommand . getAffectedObjects ( ) . size ( ) ) ;
for ( auto obj : swapCommand . getAffectedObjects ( ) )
{
2025-08-15 18:30:28 +02:00
logAi - > trace ( " affected object: %s " , evaluationContext . evaluator . aiNk - > cbc - > getObj ( obj ) - > getObjectName ( ) ) ;
2025-08-13 14:41:15 +02:00
}
if ( garrisonHero )
logAi - > debug ( " with %s and %d " , garrisonHero - > getNameTranslated ( ) , int ( swapCommand . getLockingReason ( ) ) ) ;
if ( garrisonHero & & swapCommand . getLockingReason ( ) = = HeroLockedReason : : DEFENCE )
{
2025-08-15 18:30:28 +02:00
auto defenderRole = evaluationContext . evaluator . aiNk - > heroManager - > getHeroRole ( garrisonHero ) ;
2025-08-13 14:41:15 +02:00
auto mpLeft = garrisonHero - > movementPointsRemaining ( ) / ( float ) garrisonHero - > movementPointsLimit ( true ) ;
evaluationContext . movementCost + = mpLeft ;
evaluationContext . movementCostByRole [ defenderRole ] + = mpLeft ;
evaluationContext . heroRole = defenderRole ;
evaluationContext . isDefend = true ;
evaluationContext . armyInvolvement = garrisonHero - > getArmyStrength ( ) ;
logAi - > debug ( " evaluationContext.isDefend: %d " , evaluationContext . isDefend ) ;
}
}
} ;
class DismissHeroContextBuilder : public IEvaluationContextBuilder
{
private :
2025-08-15 18:30:28 +02:00
const Nullkiller * aiNk ;
2025-08-13 14:41:15 +02:00
public :
2025-08-15 18:30:28 +02:00
DismissHeroContextBuilder ( const Nullkiller * ai ) : aiNk ( ai ) { }
2025-08-13 14:41:15 +02:00
void buildEvaluationContext ( EvaluationContext & evaluationContext , Goals : : TSubgoal task ) const override
{
if ( task - > goalType ! = Goals : : DISMISS_HERO )
return ;
Goals : : DismissHero & dismissCommand = dynamic_cast < Goals : : DismissHero & > ( * task ) ;
const CGHeroInstance * dismissedHero = dismissCommand . getHero ( ) ;
2025-08-15 18:30:28 +02:00
auto role = aiNk - > heroManager - > getHeroRole ( dismissedHero ) ;
2025-08-13 14:41:15 +02:00
auto mpLeft = dismissedHero - > movementPointsRemaining ( ) ;
2025-08-15 18:30:28 +02:00
2025-08-13 14:41:15 +02:00
evaluationContext . movementCost + = mpLeft ;
evaluationContext . movementCostByRole [ role ] + = mpLeft ;
evaluationContext . goldCost + = GameConstants : : HERO_GOLD_COST + getArmyCost ( dismissedHero ) ;
}
} ;
class BuildThisEvaluationContextBuilder : public IEvaluationContextBuilder
{
public :
void buildEvaluationContext ( EvaluationContext & evaluationContext , Goals : : TSubgoal task ) const override
{
if ( task - > goalType ! = Goals : : BUILD_STRUCTURE )
return ;
Goals : : BuildThis & buildThis = dynamic_cast < Goals : : BuildThis & > ( * task ) ;
auto & bi = buildThis . buildingInfo ;
2025-08-15 18:30:28 +02:00
2025-08-13 14:41:15 +02:00
evaluationContext . goldReward + = 7 * bi . dailyIncome . marketValue ( ) / 2 ; // 7 day income but half we already have
evaluationContext . heroRole = HeroRole : : MAIN ;
evaluationContext . movementCostByRole [ evaluationContext . heroRole ] + = bi . prerequisitesCount ;
int32_t cost = bi . buildCost [ EGameResID : : GOLD ] ;
evaluationContext . goldCost + = cost ;
evaluationContext . closestWayRatio = 1 ;
evaluationContext . buildingCost + = bi . buildCostWithPrerequisites ;
bool alreadyOwn = false ;
int highestMageGuildPossible = BuildingID : : MAGES_GUILD_3 ;
2025-08-15 18:30:28 +02:00
for ( auto town : evaluationContext . evaluator . aiNk - > cbc - > getTownsInfo ( ) )
2025-08-13 14:41:15 +02:00
{
if ( town - > hasBuilt ( bi . id ) )
alreadyOwn = true ;
2025-08-15 18:30:28 +02:00
if ( evaluationContext . evaluator . aiNk - > cbc - > canBuildStructure ( town , BuildingID : : MAGES_GUILD_5 ) ! = EBuildingState : : FORBIDDEN )
2025-08-13 14:41:15 +02:00
highestMageGuildPossible = BuildingID : : MAGES_GUILD_5 ;
2025-08-15 18:30:28 +02:00
else if ( evaluationContext . evaluator . aiNk - > cbc - > canBuildStructure ( town , BuildingID : : MAGES_GUILD_4 ) ! = EBuildingState : : FORBIDDEN )
2025-08-13 14:41:15 +02:00
highestMageGuildPossible = BuildingID : : MAGES_GUILD_4 ;
}
if ( bi . id = = BuildingID : : MARKETPLACE | | bi . dailyIncome [ EGameResID : : WOOD ] > 0 )
evaluationContext . isTradeBuilding = true ;
# if NKAI_TRACE_LEVEL >= 1
logAi - > trace ( " Building costs for %s : %s MarketValue: %d " , bi . toString ( ) , evaluationContext . buildingCost . toString ( ) , evaluationContext . buildingCost . marketValue ( ) ) ;
# endif
if ( bi . creatureID ! = CreatureID : : NONE )
{
evaluationContext . addNonCriticalStrategicalValue ( buildThis . townInfo . armyStrength / 50000.0 ) ;
if ( bi . baseCreatureID = = bi . creatureID )
{
evaluationContext . addNonCriticalStrategicalValue ( ( 0.5f + 0.1f * bi . creatureLevel ) / ( float ) bi . prerequisitesCount ) ;
evaluationContext . armyReward + = bi . armyStrength * 1.5 ;
}
else
{
auto potentialUpgradeValue = evaluationContext . evaluator . getUpgradeArmyReward ( buildThis . town , bi ) ;
2025-08-15 18:30:28 +02:00
2025-08-13 14:41:15 +02:00
evaluationContext . addNonCriticalStrategicalValue ( potentialUpgradeValue / 10000.0f / ( float ) bi . prerequisitesCount ) ;
if ( bi . id . isDwelling ( ) )
2025-08-15 18:30:28 +02:00
evaluationContext . armyReward + = bi . armyStrength - evaluationContext . evaluator . aiNk - > armyManager - > evaluateStackPower ( bi . baseCreatureID . toCreature ( ) , bi . creatureGrows ) ;
2025-08-13 14:41:15 +02:00
else //This is for prerequisite-buildings
2025-08-15 18:30:28 +02:00
evaluationContext . armyReward + = evaluationContext . evaluator . aiNk - > armyManager - > evaluateStackPower ( bi . baseCreatureID . toCreature ( ) , bi . creatureGrows ) ;
2025-08-13 14:41:15 +02:00
if ( alreadyOwn )
evaluationContext . armyReward / = bi . buildCostWithPrerequisites . marketValue ( ) ;
}
}
else if ( bi . id = = BuildingID : : CITADEL | | bi . id = = BuildingID : : CASTLE )
{
evaluationContext . addNonCriticalStrategicalValue ( buildThis . town - > creatures . size ( ) * 0.2f ) ;
evaluationContext . armyReward + = buildThis . townInfo . armyStrength / 2 ;
}
else if ( bi . id > = BuildingID : : MAGES_GUILD_1 & & bi . id < = BuildingID : : MAGES_GUILD_5 )
{
evaluationContext . skillReward + = 2 * bi . id . getMagesGuildLevel ( ) ;
2025-08-15 18:30:28 +02:00
if ( ! alreadyOwn & & evaluationContext . evaluator . aiNk - > cbc - > canBuildStructure ( buildThis . town , highestMageGuildPossible ) ! = EBuildingState : : FORBIDDEN )
2025-08-13 14:41:15 +02:00
{
2025-08-15 18:30:28 +02:00
for ( auto hero : evaluationContext . evaluator . aiNk - > cbc - > getHeroesInfo ( ) )
2025-08-13 14:41:15 +02:00
{
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 ;
2025-08-15 18:30:28 +02:00
for ( auto town : evaluationContext . evaluator . aiNk - > cbc - > getTownsInfo ( ) )
2025-08-13 14:41:15 +02:00
{
if ( buildThis . town - > getFaction ( ) = = town - > getFaction ( ) )
sameTownBonus + = town - > getTownLevel ( ) ;
}
evaluationContext . armyReward * = sameTownBonus ;
2025-08-15 18:30:28 +02:00
2025-08-13 14:41:15 +02:00
if ( evaluationContext . goldReward )
{
2025-08-15 18:30:28 +02:00
auto goldPressure = evaluationContext . evaluator . aiNk - > buildAnalyzer - > getGoldPressure ( ) ;
2025-08-13 14:41:15 +02:00
evaluationContext . addNonCriticalStrategicalValue ( evaluationContext . goldReward * goldPressure / 3500.0f / bi . prerequisitesCount ) ;
}
2025-08-16 19:49:48 +02:00
if ( bi . isMissingResources & & bi . prerequisitesCount = = 1 )
2025-08-13 14:41:15 +02:00
{
evaluationContext . strategicalValue / = 3 ;
evaluationContext . movementCostByRole [ evaluationContext . heroRole ] + = 5 ;
evaluationContext . turn + = 5 ;
}
}
} ;
uint64_t RewardEvaluator : : getUpgradeArmyReward ( const CGTownInstance * town , const BuildingInfo & bi ) const
{
2025-08-15 18:30:28 +02:00
if ( aiNk - > buildAnalyzer - > isBuilt ( town - > getFactionID ( ) , bi . id ) )
2025-08-13 14:41:15 +02:00
return 0 ;
2025-08-15 18:30:28 +02:00
auto creaturesToUpgrade = aiNk - > armyManager - > getTotalCreaturesAvailable ( bi . baseCreatureID ) ;
auto upgradedPower = aiNk - > armyManager - > evaluateStackPower ( bi . creatureID . toCreature ( ) , creaturesToUpgrade . count ) ;
2025-08-13 14:41:15 +02:00
return upgradedPower - creaturesToUpgrade . power ;
}
2025-08-13 17:05:30 +02:00
PriorityEvaluator : : PriorityEvaluator ( const Nullkiller * ai )
2025-08-15 18:30:28 +02:00
: aiNk ( ai )
2025-08-13 14:41:15 +02:00
{
initVisitTile ( ) ;
evaluationContextBuilders . push_back ( std : : make_shared < ExecuteHeroChainEvaluationContextBuilder > ( ai ) ) ;
evaluationContextBuilders . push_back ( std : : make_shared < BuildThisEvaluationContextBuilder > ( ) ) ;
evaluationContextBuilders . push_back ( std : : make_shared < ClusterEvaluationContextBuilder > ( ai ) ) ;
evaluationContextBuilders . push_back ( std : : make_shared < HeroExchangeEvaluator > ( ) ) ;
evaluationContextBuilders . push_back ( std : : make_shared < ArmyUpgradeEvaluator > ( ) ) ;
evaluationContextBuilders . push_back ( std : : make_shared < DefendTownEvaluator > ( ) ) ;
evaluationContextBuilders . push_back ( std : : make_shared < ExchangeSwapTownHeroesContextBuilder > ( ) ) ;
evaluationContextBuilders . push_back ( std : : make_shared < DismissHeroContextBuilder > ( ai ) ) ;
evaluationContextBuilders . push_back ( std : : make_shared < StayAtTownManaRecoveryEvaluator > ( ) ) ;
evaluationContextBuilders . push_back ( std : : make_shared < ExplorePointEvaluator > ( ) ) ;
}
EvaluationContext PriorityEvaluator : : buildEvaluationContext ( Goals : : TSubgoal goal ) const
{
Goals : : TGoalVec parts ;
2025-08-15 18:30:28 +02:00
EvaluationContext context ( aiNk ) ;
2025-08-13 14:41:15 +02:00
if ( goal - > goalType = = Goals : : COMPOSITION )
{
2025-08-15 18:30:28 +02:00
parts = goal - > decompose ( aiNk ) ;
2025-08-13 14:41:15 +02:00
}
else
{
parts . push_back ( goal ) ;
}
for ( auto subgoal : parts )
{
context . goldCost + = subgoal - > goldCost ;
context . buildingCost + = subgoal - > buildingCost ;
for ( auto builder : evaluationContextBuilders )
{
builder - > buildEvaluationContext ( context , subgoal ) ;
}
}
return context ;
}
float PriorityEvaluator : : evaluate ( Goals : : TSubgoal task , int priorityTier )
{
auto evaluationContext = buildEvaluationContext ( task ) ;
2025-08-15 18:30:28 +02:00
int rewardType = ( evaluationContext . goldReward > 0 ? 1 : 0 )
2025-08-13 14:41:15 +02:00
+ ( evaluationContext . armyReward > 0 ? 1 : 0 )
+ ( evaluationContext . skillReward > 0 ? 1 : 0 )
+ ( evaluationContext . strategicalValue > 0 ? 1 : 0 ) ;
float goldRewardVsMovement = evaluationContext . goldReward / std : : log2f ( 2 + evaluationContext . movementCost * 10 ) ;
2025-08-15 18:30:28 +02:00
2025-08-13 14:41:15 +02:00
double result = 0 ;
2025-08-15 18:30:28 +02:00
if ( aiNk - > settings - > isUseFuzzy ( ) )
2025-08-13 14:41:15 +02:00
{
float fuzzyResult = 0 ;
try
{
2025-08-15 02:13:45 +02:00
armyLossRatioVariable - > setValue ( evaluationContext . armyLossRatio ) ;
2025-08-13 14:41:15 +02:00
heroRoleVariable - > setValue ( evaluationContext . heroRole ) ;
mainTurnDistanceVariable - > setValue ( evaluationContext . movementCostByRole [ HeroRole : : MAIN ] ) ;
scoutTurnDistanceVariable - > setValue ( evaluationContext . movementCostByRole [ HeroRole : : SCOUT ] ) ;
goldRewardVsMovementVariable - > setValue ( goldRewardVsMovement ) ;
armyRewardVariable - > setValue ( evaluationContext . armyReward ) ;
armyGrowthVariable - > setValue ( evaluationContext . armyGrowth ) ;
skillRewardVariable - > setValue ( evaluationContext . skillReward ) ;
dangerVariable - > setValue ( evaluationContext . danger ) ;
rewardTypeVariable - > setValue ( rewardType ) ;
closestHeroRatioVariable - > setValue ( evaluationContext . closestWayRatio ) ;
strategicalValueVariable - > setValue ( evaluationContext . strategicalValue ) ;
2025-08-15 18:30:28 +02:00
goldPressureVariable - > setValue ( aiNk - > buildAnalyzer - > getGoldPressure ( ) ) ;
goldCostVariable - > setValue ( evaluationContext . goldCost / ( ( float ) aiNk - > getFreeResources ( ) [ EGameResID : : GOLD ] + ( float ) aiNk - > buildAnalyzer - > getDailyIncome ( ) [ EGameResID : : GOLD ] + 1.0f ) ) ;
2025-08-13 14:41:15 +02:00
turnVariable - > setValue ( evaluationContext . turn ) ;
fearVariable - > setValue ( evaluationContext . enemyHeroDangerRatio ) ;
engine - > process ( ) ;
fuzzyResult = value - > getValue ( ) ;
}
catch ( fl : : Exception & fe )
{
logAi - > error ( " evaluate VisitTile: %s " , fe . getWhat ( ) ) ;
}
result = fuzzyResult ;
}
else
{
float score = 0 ;
bool currentPositionThreatened = false ;
if ( task - > hero )
{
2025-08-15 18:30:28 +02:00
auto currentTileThreat = aiNk - > dangerHitMap - > getTileThreat ( task - > hero - > visitablePos ( ) ) ;
2025-08-13 14:41:15 +02:00
if ( currentTileThreat . fastestDanger . turn < 1 & & currentTileThreat . fastestDanger . danger > task - > hero - > getTotalStrength ( ) )
currentPositionThreatened = true ;
}
if ( priorityTier = = PriorityTier : : FAR_HUNTER_GATHER & & currentPositionThreatened = = true )
{
# if NKAI_TRACE_LEVEL >= 2
logAi - > trace ( " Skip FAR_HUNTER_GATHER because hero would be threatened. " ) ;
# endif
return 0 ;
}
2025-08-15 18:30:28 +02:00
const bool amIInDanger = aiNk - > cbc - > getTownsInfo ( ) . empty ( ) ;
2025-08-13 14:41:15 +02:00
// Shouldn't it default to 0 instead of 1.0 in the end?
2025-08-15 18:30:28 +02:00
const float maxWillingToLose = amIInDanger ? 1 : aiNk - > settings - > getMaxArmyLossTarget ( ) * evaluationContext . powerRatio > 0 ? aiNk - > settings - > getMaxArmyLossTarget ( ) * evaluationContext . powerRatio : 1.0 ;
2025-08-13 14:41:15 +02:00
float dangerThreshold = 1 ;
dangerThreshold * = evaluationContext . powerRatio > 0 ? evaluationContext . powerRatio : 1.0 ;
bool arriveNextWeek = false ;
2025-08-15 18:30:28 +02:00
if ( aiNk - > cbc - > getDate ( Date : : DAY_OF_WEEK ) + evaluationContext . turn > 7 & & priorityTier < PriorityTier : : FAR_KILL )
2025-08-13 14:41:15 +02:00
arriveNextWeek = true ;
# if NKAI_TRACE_LEVEL >= 2
logAi - > trace ( " BEFORE: priorityTier %d, Evaluated %s, loss: %f, maxWillingToLose: %f, turn: %d, turns main: %f, scout: %f, armyInvolvement: %f, goldRewardVsMovement: %f, cost: %d, armyReward: %f, armyGrowth: %f skillReward: %f danger: %d, threatTurns: %d, threat: %d, heroRole: %s, strategicalValue: %f, conquestValue: %f closestWayRatio: %f, enemyHeroDangerRatio: %f, dangerThreshold: %f explorePriority: %d isDefend: %d isEnemy: %d arriveNextWeek: %d powerRatio: %f " ,
priorityTier ,
task - > toString ( ) ,
2025-08-15 02:13:45 +02:00
evaluationContext . armyLossRatio ,
2025-08-13 14:41:15 +02:00
maxWillingToLose ,
( int ) evaluationContext . turn ,
evaluationContext . movementCostByRole [ HeroRole : : MAIN ] ,
evaluationContext . movementCostByRole [ HeroRole : : SCOUT ] ,
evaluationContext . armyInvolvement ,
goldRewardVsMovement ,
evaluationContext . goldCost ,
evaluationContext . armyReward ,
evaluationContext . armyGrowth ,
evaluationContext . skillReward ,
evaluationContext . danger ,
evaluationContext . threatTurns ,
evaluationContext . threat ,
evaluationContext . heroRole = = HeroRole : : MAIN ? " main " : " scout " ,
evaluationContext . strategicalValue ,
evaluationContext . conquestValue ,
evaluationContext . closestWayRatio ,
evaluationContext . enemyHeroDangerRatio ,
dangerThreshold ,
evaluationContext . explorePriority ,
evaluationContext . isDefend ,
evaluationContext . isEnemy ,
arriveNextWeek ,
evaluationContext . powerRatio ) ;
# endif
switch ( priorityTier )
{
case PriorityTier : : INSTAKILL : //Take towns / kill heroes in immediate reach
{
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 ;
2025-08-15 18:30:28 +02:00
if ( vstd : : isAlmostZero ( score ) | | ( evaluationContext . enemyHeroDangerRatio > dangerThreshold & & ( evaluationContext . turn > 0 | | evaluationContext . isExchange ) & & ! aiNk - > cbc - > getTownsInfo ( ) . empty ( ) ) )
2025-08-13 14:41:15 +02:00
return 0 ;
2025-08-15 02:13:45 +02:00
if ( maxWillingToLose - evaluationContext . armyLossRatio < 0 )
2025-08-13 14:41:15 +02:00
return 0 ;
if ( evaluationContext . movementCost > 0 )
score / = evaluationContext . movementCost ;
break ;
}
case PriorityTier : : INSTADEFEND : //Defend immediately threatened towns
{
//No point defending if we don't have defensive-structures
if ( evaluationContext . defenseValue < 2 )
return 0 ;
2025-08-15 02:13:45 +02:00
if ( maxWillingToLose - evaluationContext . armyLossRatio < 0 )
2025-08-13 14:41:15 +02:00
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 ;
2025-08-15 18:30:28 +02:00
if ( vstd : : isAlmostZero ( score ) | | ( evaluationContext . enemyHeroDangerRatio > dangerThreshold & & ( evaluationContext . turn > 0 | | evaluationContext . isExchange ) & & ! aiNk - > cbc - > getTownsInfo ( ) . empty ( ) ) )
2025-08-13 14:41:15 +02:00
return 0 ;
2025-08-15 02:13:45 +02:00
if ( maxWillingToLose - evaluationContext . armyLossRatio < 0 )
2025-08-13 14:41:15 +02:00
return 0 ;
score * = evaluationContext . closestWayRatio ;
if ( evaluationContext . movementCost > 0 )
score / = evaluationContext . movementCost ;
break ;
}
case PriorityTier : : HIGH_PRIO_EXPLORE :
{
if ( evaluationContext . enemyHeroDangerRatio > dangerThreshold )
return 0 ;
if ( evaluationContext . explorePriority ! = 1 )
return 0 ;
2025-08-15 02:13:45 +02:00
if ( maxWillingToLose - evaluationContext . armyLossRatio < 0 )
2025-08-13 14:41:15 +02:00
return 0 ;
2025-08-15 02:13:45 +02:00
if ( vstd : : isAlmostZero ( evaluationContext . armyLossRatio ) & & evaluationContext . closestWayRatio < 1.0 )
2025-08-13 14:41:15 +02:00
return 0 ;
score = 1000 ;
if ( evaluationContext . movementCost > 0 )
score / = evaluationContext . movementCost ;
break ;
}
case PriorityTier : : HUNTER_GATHER : //Collect guarded stuff
//FALL_THROUGH
case PriorityTier : : FAR_HUNTER_GATHER :
// FIXME: Should not go to something that gives army if no slots available in the hero, but probably not in the evaluator, but in the finder
// task.get()->hero->getSlotFor(creature, 7) == false (not sure I get to know which creature is there in Orc Tower building)
// /// so I can't know for sure if it fits my stacks or not, but at least we can avoid going there with all 7 stacks occupied by other units
// task.get()->hero->getFreeSlots(7) == 7
// getDuplicatingSlots(task.get()->hero) == false
{
if ( evaluationContext . enemyHeroDangerRatio > dangerThreshold & & ! evaluationContext . isDefend & & priorityTier ! = PriorityTier : : FAR_HUNTER_GATHER )
return 0 ;
if ( evaluationContext . buildingCost . marketValue ( ) > 0 )
return 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 ( priorityTier ! = PriorityTier : : FAR_HUNTER_GATHER & & ( ( evaluationContext . enemyHeroDangerRatio > 0 & & arriveNextWeek ) | | evaluationContext . enemyHeroDangerRatio > dangerThreshold ) )
return 0 ;
2025-08-15 02:13:45 +02:00
if ( maxWillingToLose - evaluationContext . armyLossRatio < 0 )
2025-08-13 14:41:15 +02:00
return 0 ;
2025-08-15 02:13:45 +02:00
if ( vstd : : isAlmostZero ( evaluationContext . armyLossRatio ) & & evaluationContext . closestWayRatio < 1.0 )
2025-08-13 14:41:15 +02:00
return 0 ;
score + = evaluationContext . strategicalValue * 1000 ;
score + = evaluationContext . goldReward ;
2025-08-15 02:13:45 +02:00
score + = evaluationContext . skillReward * evaluationContext . armyInvolvement * ( 1 - evaluationContext . armyLossRatio ) * 0.05 ;
2025-08-13 14:41:15 +02:00
score + = evaluationContext . armyReward ;
score + = evaluationContext . armyGrowth ;
// score -= evaluationContext.goldCost; // don't include School of Magic cost or others because those locations are benefficial
2025-08-15 02:13:45 +02:00
score - = evaluationContext . armyInvolvement * evaluationContext . armyLossRatio * 0.1 ;
2025-08-13 14:41:15 +02:00
logAi - > trace ( " case PriorityTier::FAR_HUNTER_GATHER score %f, strategicalValue %f, goldReward %f, skillRewardMultiplied %f, armyReward %f, armyGrowth %f, goldCost -%f, armyInvolvementMultiplied -%f, "
" armyLossPersentage %f, movementCost %f, enemyHeroDangerRatio %f " ,
2025-08-15 02:13:45 +02:00
score , evaluationContext . strategicalValue , evaluationContext . goldReward , evaluationContext . skillReward * evaluationContext . armyInvolvement * ( 1 - evaluationContext . armyLossRatio ) * 0.05 ,
evaluationContext . armyReward , evaluationContext . armyGrowth , evaluationContext . goldCost , evaluationContext . armyInvolvement * evaluationContext . armyLossRatio ,
evaluationContext . armyLossRatio , evaluationContext . movementCost , evaluationContext . enemyHeroDangerRatio ) ;
2025-08-13 14:41:15 +02:00
if ( score > 0 )
{
score = 1000 ;
if ( evaluationContext . movementCost > 0 )
{
logAi - > trace ( " case PriorityTier::FAR_HUNTER_GATHER if 8 " ) ;
score - = evaluationContext . movementCost / 20 * score ; // we expect movement won't be over 20 turns
}
if ( evaluationContext . enemyHeroDangerRatio > 0 ) // This doesn't make sense at it always seems to be 0
{
logAi - > trace ( " case PriorityTier::FAR_HUNTER_GATHER if 9 " ) ;
score * = 1 - evaluationContext . enemyHeroDangerRatio ;
}
2025-08-15 02:13:45 +02:00
if ( evaluationContext . armyLossRatio > 0 )
2025-08-13 14:41:15 +02:00
{
logAi - > trace ( " case PriorityTier::FAR_HUNTER_GATHER if 10 " ) ;
2025-08-15 02:13:45 +02:00
score * = 1 - evaluationContext . armyLossRatio ;
2025-08-13 14:41:15 +02:00
}
}
break ;
}
case PriorityTier : : LOW_PRIO_EXPLORE :
{
if ( evaluationContext . enemyHeroDangerRatio > dangerThreshold )
return 0 ;
if ( evaluationContext . explorePriority ! = 3 )
return 0 ;
2025-08-15 02:13:45 +02:00
if ( maxWillingToLose - evaluationContext . armyLossRatio < 0 )
2025-08-13 14:41:15 +02:00
return 0 ;
if ( evaluationContext . closestWayRatio < 1.0 )
return 0 ;
score = 1000 ;
if ( evaluationContext . movementCost > 0 )
score / = evaluationContext . movementCost ;
break ;
}
case PriorityTier : : DEFEND : //Defend whatever if nothing else is to do
{
if ( evaluationContext . enemyHeroDangerRatio > dangerThreshold )
return 0 ;
if ( evaluationContext . isDefend | | evaluationContext . isArmyUpgrade )
score = evaluationContext . armyInvolvement ;
score / = ( evaluationContext . turn + 1 ) ;
break ;
}
case PriorityTier : : BUILDINGS : //For buildings and buying army
{
2025-08-15 02:13:45 +02:00
if ( maxWillingToLose - evaluationContext . armyLossRatio < 0 )
2025-08-13 14:41:15 +02:00
return 0 ;
//If we already have locked resources, we don't look at other buildings
2025-08-15 18:30:28 +02:00
if ( aiNk - > getLockedResources ( ) . marketValue ( ) > 0 )
2025-08-13 14:41:15 +02:00
return 0 ;
score + = evaluationContext . conquestValue * 1000 ;
score + = evaluationContext . strategicalValue * 1000 ;
score + = evaluationContext . goldReward ;
2025-08-15 02:13:45 +02:00
score + = evaluationContext . skillReward * evaluationContext . armyInvolvement * ( 1 - evaluationContext . armyLossRatio ) * 0.05 ;
2025-08-13 14:41:15 +02:00
score + = evaluationContext . armyReward ;
score + = evaluationContext . armyGrowth ;
if ( evaluationContext . buildingCost . marketValue ( ) > 0 )
{
2025-08-15 18:30:28 +02:00
if ( ! evaluationContext . isTradeBuilding & & aiNk - > getFreeResources ( ) [ EGameResID : : WOOD ] - evaluationContext . buildingCost [ EGameResID : : WOOD ] < 5 & & aiNk - > buildAnalyzer - > getDailyIncome ( ) [ EGameResID : : WOOD ] < 1 )
2025-08-13 14:41:15 +02:00
{
logAi - > trace ( " Should make sure to build market-place instead of %s " , task - > toString ( ) ) ;
2025-08-15 18:30:28 +02:00
for ( auto town : aiNk - > cbc - > getTownsInfo ( ) )
2025-08-13 14:41:15 +02:00
{
if ( ! town - > hasBuiltSomeTradeBuilding ( ) )
return 0 ;
}
}
score + = 1000 ;
2025-08-15 18:30:28 +02:00
auto resourcesAvailable = evaluationContext . evaluator . aiNk - > getFreeResources ( ) ;
auto income = aiNk - > buildAnalyzer - > getDailyIncome ( ) ;
if ( aiNk - > buildAnalyzer - > isGoldPressureOverMax ( ) )
2025-08-13 14:41:15 +02:00
score / = evaluationContext . buildingCost . marketValue ( ) ;
if ( ! resourcesAvailable . canAfford ( evaluationContext . buildingCost ) )
{
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 ;
if ( ! haveEverythingButGold )
score / = turnsTo ;
}
}
else
{
if ( evaluationContext . enemyHeroDangerRatio > 1 & & ! evaluationContext . isDefend & & vstd : : isAlmostZero ( evaluationContext . conquestValue ) )
return 0 ;
}
break ;
}
}
result = score ;
//TODO: Figure out the root cause for why evaluationContext.closestWayRatio has become -nan(ind).
if ( std : : isnan ( result ) )
return 0 ;
}
# if NKAI_TRACE_LEVEL >= 2
logAi - > trace ( " 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, result %f " ,
priorityTier ,
task - > toString ( ) ,
2025-08-15 02:13:45 +02:00
evaluationContext . armyLossRatio ,
2025-08-13 14:41:15 +02:00
( int ) evaluationContext . turn ,
evaluationContext . movementCostByRole [ HeroRole : : MAIN ] ,
evaluationContext . movementCostByRole [ HeroRole : : SCOUT ] ,
evaluationContext . armyInvolvement ,
goldRewardVsMovement ,
evaluationContext . goldCost ,
evaluationContext . armyReward ,
evaluationContext . armyGrowth ,
evaluationContext . skillReward ,
evaluationContext . danger ,
evaluationContext . threatTurns ,
evaluationContext . threat ,
evaluationContext . heroRole = = HeroRole : : MAIN ? " main " : " scout " ,
evaluationContext . strategicalValue ,
evaluationContext . conquestValue ,
evaluationContext . closestWayRatio ,
evaluationContext . enemyHeroDangerRatio ,
result ) ;
# endif
return result ;
}
}