From 4a552d411ce4390fc169f622cabf210c0d9b31ec Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 15 Jul 2024 18:12:52 +0200 Subject: [PATCH] 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. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 151 +++++++++++++++------ AI/Nullkiller/Engine/PriorityEvaluator.h | 5 +- 2 files changed, 117 insertions(+), 39 deletions(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index f4ccd68a2..a726be3d4 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -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,37 +1209,105 @@ 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) { - score += evaluationContext.conquestValue * 1000; - if (score == 0 || (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.movementCost > 1)) - return 0; - } - else - { - if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend && evaluationContext.movementCost <= 1) - 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; - } - if (score > 0) - { - score *= evaluationContext.closestWayRatio; - if (evaluationContext.enemyHeroDangerRatio > 1) - score /= evaluationContext.enemyHeroDangerRatio; - if (evaluationContext.movementCost > 0) - score /= evaluationContext.movementCost; - score *= (1 - evaluationContext.armyLossPersentage); - result = score; + case 0: //Take towns + { + score += evaluationContext.conquestValue * 1000; + 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 (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 - 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 diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index 60295b397..5701e2381 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -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;