1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-12-14 10:12:59 +02:00

Decisionmaking-changes

There's 3 new evaluation-contexts that are now taken into account:
Whether an action is building, whether an action involves sailing and the newly introduced threat.

The value-evaluation of creatures now also takes special resources into account.

No longer treating other AIs differently than players when it comes to how afraid we shall be of them.

The cost of buildings for decision-making now determines missing resources. Available resources are ignored when it comes to how they impact the cost. But missing-resources will heftily impact the assumed price by calculating their market-value. This shall encourage the AI to rather build what it currently can build instead of saving up for something that it lacking the special resources for.

AI is no longer willing to sacrifice more than 25% of their army for any attack except when it has no towns left.

Revamped the priority-tiers of AI decision-making.

Higest priority is conquering enemy towns and killing enemy heroes. However, the AI will no longer try to do so when the target is more than one turn away and protected by a nearby enemy-hero that could kill the one tasked with dealing with the target. Except when they have no towns left. Then they get desperate and try everything.

As a general rule of thumb one could say the AI will prioritize conquest over collecting freebies over investing army to get something that isn't a city. It's a bit more complex than that but this is roughly what can be expected. It will also highly value their own heroes safety during all this.
This commit is contained in:
Xilmi 2024-07-15 18:12:52 +02:00
parent 95ba57dfe2
commit 4a552d411c
2 changed files with 117 additions and 39 deletions

View File

@ -54,9 +54,12 @@ EvaluationContext::EvaluationContext(const Nullkiller* ai)
conquestValue(0), conquestValue(0),
evaluator(ai), evaluator(ai),
enemyHeroDangerRatio(0), enemyHeroDangerRatio(0),
threat(0),
armyGrowth(0), armyGrowth(0),
armyInvolvement(0), armyInvolvement(0),
isDefend(false) isDefend(false),
isBuild(false),
involvesSailing(false)
{ {
} }
@ -246,7 +249,7 @@ int getDwellingArmyCost(const CGObjectInstance * target)
auto creature = creLevel.second.back().toCreature(); auto creature = creLevel.second.back().toCreature();
auto creaturesAreFree = creature->getLevel() == 1; auto creaturesAreFree = creature->getLevel() == 1;
if(!creaturesAreFree) if(!creaturesAreFree)
cost += creature->getRecruitCost(EGameResID::GOLD) * creLevel.first; cost += creature->getFullRecruitCost().marketValue() * creLevel.first;
} }
} }
@ -686,7 +689,7 @@ int32_t getArmyCost(const CArmedInstance * army)
for(auto stack : army->Slots()) for(auto stack : army->Slots())
{ {
value += stack.second->getCreatureID().toCreature()->getRecruitCost(EGameResID::GOLD) * stack.second->count; value += stack.second->getCreatureID().toCreature()->getFullRecruitCost().marketValue() * stack.second->count;
} }
return value; return value;
@ -823,15 +826,8 @@ void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uin
if(enemyDanger.danger) if(enemyDanger.danger)
{ {
auto dangerRatio = enemyDanger.danger / (double)ourStrength; auto dangerRatio = enemyDanger.danger / (double)ourStrength;
auto enemyHero = evaluationContext.evaluator.ai->cb->getObj(enemyDanger.hero.hid, false);
bool isAI = enemyHero && isAnotherAi(enemyHero, *evaluationContext.evaluator.ai->cb);
if(isAI)
{
dangerRatio *= 1.5; // lets make AI bit more afraid of other AI.
}
vstd::amax(evaluationContext.enemyHeroDangerRatio, dangerRatio); vstd::amax(evaluationContext.enemyHeroDangerRatio, dangerRatio);
vstd::amax(evaluationContext.threat, enemyDanger.threat);
} }
} }
@ -907,6 +903,8 @@ public:
for(auto & node : path.nodes) for(auto & node : path.nodes)
{ {
vstd::amax(costsPerHero[node.targetHero], node.cost); vstd::amax(costsPerHero[node.targetHero], node.cost);
if (node.layer == EPathfindingLayer::SAIL)
evaluationContext.involvesSailing = true;
} }
for(auto pair : costsPerHero) for(auto pair : costsPerHero)
@ -1056,8 +1054,15 @@ public:
evaluationContext.goldReward += 7 * bi.dailyIncome[EGameResID::GOLD] / 2; // 7 day income but half we already have evaluationContext.goldReward += 7 * bi.dailyIncome[EGameResID::GOLD] / 2; // 7 day income but half we already have
evaluationContext.heroRole = HeroRole::MAIN; evaluationContext.heroRole = HeroRole::MAIN;
evaluationContext.movementCostByRole[evaluationContext.heroRole] += bi.prerequisitesCount; evaluationContext.movementCostByRole[evaluationContext.heroRole] += bi.prerequisitesCount;
evaluationContext.goldCost += bi.buildCostWithPrerequisites[EGameResID::GOLD]; int32_t cost = bi.buildCostWithPrerequisites[EGameResID::GOLD];
auto resourcesAvailable = evaluationContext.evaluator.ai->getFreeResources();
TResources missing = bi.buildCostWithPrerequisites - resourcesAvailable;
missing[EGameResID::GOLD] = 0;
missing.positive();
cost += missing.marketValue();
evaluationContext.goldCost += cost;
evaluationContext.closestWayRatio = 1; evaluationContext.closestWayRatio = 1;
evaluationContext.isBuild = true;
if(bi.creatureID != CreatureID::NONE) if(bi.creatureID != CreatureID::NONE)
{ {
@ -1204,37 +1209,105 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
else else
{ {
float score = 0; float score = 0;
if (priorityTier == 0) float maxWillingToLose = ai->cb->getTownsInfo().empty() ? 1 : 0.25;
switch (priorityTier)
{ {
score += evaluationContext.conquestValue * 1000; case 0: //Take towns
if (score == 0 || (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.movementCost > 1)) {
return 0; score += evaluationContext.conquestValue * 1000;
} if (score == 0 || (evaluationContext.enemyHeroDangerRatio > 0.5 && evaluationContext.turn > 0 && !ai->cb->getTownsInfo().empty()))
else return 0;
{ score *= evaluationContext.closestWayRatio;
if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend && evaluationContext.movementCost <= 1) if (evaluationContext.threat > evaluationContext.armyInvolvement && !evaluationContext.isDefend)
return 0; score *= evaluationContext.armyInvolvement / evaluationContext.threat;
score += evaluationContext.strategicalValue * 1000; if (evaluationContext.movementCost > 0)
score += evaluationContext.goldReward; score /= evaluationContext.movementCost;
score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05; score *= (maxWillingToLose - evaluationContext.armyLossPersentage);
score += evaluationContext.armyReward; break;
score += evaluationContext.armyGrowth; }
score -= evaluationContext.goldCost; case 1: //Collect unguarded stuff
} {
if (score > 0) if (evaluationContext.enemyHeroDangerRatio > 0.5 && !evaluationContext.isDefend)
{ return 0;
score *= evaluationContext.closestWayRatio; if (evaluationContext.isDefend && evaluationContext.enemyHeroDangerRatio == 0)
if (evaluationContext.enemyHeroDangerRatio > 1) return 0;
score /= evaluationContext.enemyHeroDangerRatio; if (evaluationContext.armyLossPersentage > 0)
if (evaluationContext.movementCost > 0) return 0;
score /= evaluationContext.movementCost; if (evaluationContext.involvesSailing && evaluationContext.movementCostByRole[HeroRole::MAIN] > 0)
score *= (1 - evaluationContext.armyLossPersentage); return 0;
result = score; score += evaluationContext.strategicalValue * 1000;
score += evaluationContext.goldReward;
score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05;
score += evaluationContext.armyReward;
score += evaluationContext.armyGrowth;
if (evaluationContext.isBuild)
{
score += 1000;
score /= evaluationContext.goldCost;
}
else
{
if (score <= 0)
return 0;
else
score = 1000;
score *= evaluationContext.closestWayRatio;
if (evaluationContext.threat > evaluationContext.armyInvolvement && !evaluationContext.isDefend)
score *= evaluationContext.armyInvolvement / evaluationContext.threat;
if (evaluationContext.movementCost > 0)
score /= evaluationContext.movementCost;
}
break;
}
case 2: //Collect guarded stuff
{
if (evaluationContext.isBuild)
return 0;
if (evaluationContext.enemyHeroDangerRatio > 0.5 && !evaluationContext.isDefend)
return 0;
score += evaluationContext.strategicalValue * 1000;
score += evaluationContext.goldReward;
score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05;
score += evaluationContext.armyReward;
score += evaluationContext.armyGrowth;
score -= evaluationContext.goldCost;
score -= evaluationContext.armyInvolvement * evaluationContext.armyLossPersentage;
if (score > 0)
{
if (evaluationContext.enemyHeroDangerRatio > 1)
if (evaluationContext.threat > 0)
score = evaluationContext.armyInvolvement / evaluationContext.threat;
score *= evaluationContext.closestWayRatio;
if (evaluationContext.threat > evaluationContext.armyInvolvement && !evaluationContext.isDefend)
score *= evaluationContext.armyInvolvement / evaluationContext.threat;
if (evaluationContext.movementCost > 0)
score /= evaluationContext.movementCost;
score *= (maxWillingToLose - evaluationContext.armyLossPersentage);
}
break;
}
case 3: //Pre-filter to see if anything is worth to be done at all
{
score += evaluationContext.conquestValue * 1000;
score += evaluationContext.strategicalValue * 1000;
score += evaluationContext.goldReward;
score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05;
score += evaluationContext.armyReward;
score += evaluationContext.armyGrowth;
if (evaluationContext.isBuild)
{
score += 1000;
score /= evaluationContext.goldCost;
}
break;
}
} }
result = score;
} }
#if NKAI_TRACE_LEVEL >= 2 #if NKAI_TRACE_LEVEL >= 2
logAi->trace("Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, skill: %f danger: %d, role: %s, strategical value: %f, cwr: %f, fear: %f, result %f", logAi->trace("priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, skill: %f danger: %d, threat: %d, role: %s, strategical value: %f, cwr: %f, fear: %f, fuzzy: %f, result %f",
priorityTier,
task->toString(), task->toString(),
evaluationContext.armyLossPersentage, evaluationContext.armyLossPersentage,
(int)evaluationContext.turn, (int)evaluationContext.turn,
@ -1245,10 +1318,12 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
evaluationContext.armyReward, evaluationContext.armyReward,
evaluationContext.skillReward, evaluationContext.skillReward,
evaluationContext.danger, evaluationContext.danger,
evaluationContext.threat,
evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout", evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout",
evaluationContext.strategicalValue, evaluationContext.strategicalValue,
evaluationContext.closestWayRatio, evaluationContext.closestWayRatio,
evaluationContext.enemyHeroDangerRatio, evaluationContext.enemyHeroDangerRatio,
fuzzyResult,
result); result);
#endif #endif

View File

@ -70,8 +70,11 @@ struct DLL_EXPORT EvaluationContext
uint8_t turn; uint8_t turn;
RewardEvaluator evaluator; RewardEvaluator evaluator;
float enemyHeroDangerRatio; float enemyHeroDangerRatio;
float threat;
float armyInvolvement; float armyInvolvement;
bool isDefend; bool isDefend;
bool isBuild;
bool involvesSailing;
EvaluationContext(const Nullkiller * ai); EvaluationContext(const Nullkiller * ai);
@ -94,7 +97,7 @@ public:
~PriorityEvaluator(); ~PriorityEvaluator();
void initVisitTile(); void initVisitTile();
float evaluate(Goals::TSubgoal task, int priorityTier = 1); float evaluate(Goals::TSubgoal task, int priorityTier = 3);
private: private:
const Nullkiller * ai; const Nullkiller * ai;