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),
evaluator(ai),
enemyHeroDangerRatio(0),
threat(0),
armyGrowth(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 creaturesAreFree = creature->getLevel() == 1;
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())
{
value += stack.second->getCreatureID().toCreature()->getRecruitCost(EGameResID::GOLD) * stack.second->count;
value += stack.second->getCreatureID().toCreature()->getFullRecruitCost().marketValue() * stack.second->count;
}
return value;
@ -823,15 +826,8 @@ void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uin
if(enemyDanger.danger)
{
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.threat, enemyDanger.threat);
}
}
@ -907,6 +903,8 @@ public:
for(auto & node : path.nodes)
{
vstd::amax(costsPerHero[node.targetHero], node.cost);
if (node.layer == EPathfindingLayer::SAIL)
evaluationContext.involvesSailing = true;
}
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.heroRole = HeroRole::MAIN;
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.isBuild = true;
if(bi.creatureID != CreatureID::NONE)
{
@ -1204,15 +1209,61 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
else
{
float score = 0;
if (priorityTier == 0)
float maxWillingToLose = ai->cb->getTownsInfo().empty() ? 1 : 0.25;
switch (priorityTier)
{
case 0: //Take towns
{
score += evaluationContext.conquestValue * 1000;
if (score == 0 || (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.movementCost > 1))
if (score == 0 || (evaluationContext.enemyHeroDangerRatio > 0.5 && evaluationContext.turn > 0 && !ai->cb->getTownsInfo().empty()))
return 0;
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 1: //Collect unguarded stuff
{
if (evaluationContext.enemyHeroDangerRatio > 0.5 && !evaluationContext.isDefend)
return 0;
if (evaluationContext.isDefend && evaluationContext.enemyHeroDangerRatio == 0)
return 0;
if (evaluationContext.armyLossPersentage > 0)
return 0;
if (evaluationContext.involvesSailing && evaluationContext.movementCostByRole[HeroRole::MAIN] > 0)
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;
if (evaluationContext.isBuild)
{
score += 1000;
score /= evaluationContext.goldCost;
}
else
{
if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend && evaluationContext.movementCost <= 1)
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;
@ -1220,21 +1271,43 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
score += evaluationContext.armyReward;
score += evaluationContext.armyGrowth;
score -= evaluationContext.goldCost;
}
score -= evaluationContext.armyInvolvement * evaluationContext.armyLossPersentage;
if (score > 0)
{
score *= evaluationContext.closestWayRatio;
if (evaluationContext.enemyHeroDangerRatio > 1)
score /= evaluationContext.enemyHeroDangerRatio;
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 *= (1 - evaluationContext.armyLossPersentage);
result = score;
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
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(),
evaluationContext.armyLossPersentage,
(int)evaluationContext.turn,
@ -1245,10 +1318,12 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
evaluationContext.armyReward,
evaluationContext.skillReward,
evaluationContext.danger,
evaluationContext.threat,
evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout",
evaluationContext.strategicalValue,
evaluationContext.closestWayRatio,
evaluationContext.enemyHeroDangerRatio,
fuzzyResult,
result);
#endif

View File

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