From 734f815e67659ab64339e67600b80e3bd03bf2bc Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 7 Jul 2024 15:12:05 +0200 Subject: [PATCH 001/186] Sorting tasks after buildPlan Tasks need to be sorted again after buildPlan as otherwise the correct order isn't guaranteed. This led to inconsistent behavior by the AI. --- AI/Nullkiller/Engine/Nullkiller.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index fa15363f6..81a144b72 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -399,6 +399,11 @@ void Nullkiller::makeTurn() auto selectedTasks = buildPlan(bestTasks); + std::sort(selectedTasks.begin(), selectedTasks.end(), [](const TTask& a, const TTask& b) + { + return a->priority > b->priority; + }); + logAi->debug("Decision madel in %ld", timeElapsed(start)); if(selectedTasks.empty()) From 54c6d99de3e2ca80208ebe2f0a9cd93ea4cdb50c Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 7 Jul 2024 15:17:38 +0200 Subject: [PATCH 002/186] Using only one bucket Nullkiller suggested that this change would help to further fix inconsistent behavior by the AI. I tested it and it did indeed fix different orders of how AI does things. "Important to make count 1 to not relay on object addresses They are source of random" - Nullkiller --- AI/Nullkiller/Pathfinding/AINodeStorage.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.h b/AI/Nullkiller/Pathfinding/AINodeStorage.h index 5c160238d..9f51cd531 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.h +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.h @@ -27,8 +27,8 @@ namespace NKAI { namespace AIPathfinding { - const int BUCKET_COUNT = 3; - const int BUCKET_SIZE = 7; + const int BUCKET_COUNT = 1; + const int BUCKET_SIZE = 32; const int NUM_CHAINS = BUCKET_COUNT * BUCKET_SIZE; const int CHAIN_MAX_DEPTH = 4; } From e0a81b3e69e081bdeaf846b3db42758777db861d Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 7 Jul 2024 22:08:19 +0200 Subject: [PATCH 003/186] Fixed AI-exploration-data being lost after loading savegame The information of whether objects like a redwood-observatory or subterranian gates have been interacted with by the AI will now be retrieved from the game-state instead of using an AI-internal memory that won't survive loading a save-game. --- .../Behaviors/ExplorationBehavior.cpp | 45 +++++++------------ 1 file changed, 15 insertions(+), 30 deletions(-) diff --git a/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp b/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp index cab2707f3..b088a86a4 100644 --- a/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp +++ b/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp @@ -35,44 +35,29 @@ Goals::TGoalVec ExplorationBehavior::decompose(const Nullkiller * ai) const for(auto obj : ai->memory->visitableObjs) { - if(!vstd::contains(ai->memory->alreadyVisited, obj)) + switch (obj->ID.num) { - switch(obj->ID.num) + case Obj::REDWOOD_OBSERVATORY: + case Obj::PILLAR_OF_FIRE: { - case Obj::REDWOOD_OBSERVATORY: - case Obj::PILLAR_OF_FIRE: - tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 200)).addNext(CaptureObject(obj)))); - break; - case Obj::MONOLITH_ONE_WAY_ENTRANCE: - case Obj::MONOLITH_TWO_WAY: - case Obj::SUBTERRANEAN_GATE: - auto tObj = dynamic_cast(obj); - if(TeleportChannel::IMPASSABLE != ai->memory->knownTeleportChannels[tObj->channel]->passability) - { - tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj)))); - } + auto rObj = dynamic_cast(obj); + if(!rObj->wasScouted(ai->playerID)) + tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 200)).addNext(CaptureObject(obj)))); break; } - } - else - { - switch(obj->ID.num) + case Obj::MONOLITH_ONE_WAY_ENTRANCE: + case Obj::MONOLITH_TWO_WAY: + case Obj::SUBTERRANEAN_GATE: { - case Obj::MONOLITH_TWO_WAY: - case Obj::SUBTERRANEAN_GATE: - auto tObj = dynamic_cast(obj); - if(TeleportChannel::IMPASSABLE == ai->memory->knownTeleportChannels[tObj->channel]->passability) - break; - for(auto exit : ai->memory->knownTeleportChannels[tObj->channel]->exits) + auto tObj = dynamic_cast(obj); + for (auto exit : cb->getTeleportChannelExits(tObj->channel)) { - if(!cb->getObj(exit)) - { - // Always attempt to visit two-way teleports if one of channel exits is not visible - tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj)))); - break; + if (exit != tObj->id) + { + if (!cb->isVisible(cb->getObjInstance(exit))) + tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj)))); } } - break; } } } From aa891cb8b12909a7cfbe56733a63921111ae81d9 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 7 Jul 2024 22:38:37 +0200 Subject: [PATCH 004/186] Armycost Added new method to retrieve the cost of an army to be used for AI-decision-making. --- lib/CCreatureSet.cpp | 14 ++++++++++++++ lib/CCreatureSet.h | 2 ++ 2 files changed, 16 insertions(+) diff --git a/lib/CCreatureSet.cpp b/lib/CCreatureSet.cpp index 2a184b8e9..84e590649 100644 --- a/lib/CCreatureSet.cpp +++ b/lib/CCreatureSet.cpp @@ -366,6 +366,14 @@ ui64 CCreatureSet::getArmyStrength() const return ret; } +ui64 CCreatureSet::getArmyCost() const +{ + ui64 ret = 0; + for (const auto& elem : stacks) + ret += elem.second->getCost(); + return ret; +} + ui64 CCreatureSet::getPower(const SlotID & slot) const { return getStack(slot).getPower(); @@ -858,6 +866,12 @@ ui64 CStackInstance::getPower() const return type->getAIValue() * count; } +ui64 CStackInstance::getCost() const +{ + assert(type); + return type->getFullRecruitCost().marketValue() * count; +} + ArtBearer::ArtBearer CStackInstance::bearerType() const { return ArtBearer::CREATURE; diff --git a/lib/CCreatureSet.h b/lib/CCreatureSet.h index df7ab8dd4..cb2f0afca 100644 --- a/lib/CCreatureSet.h +++ b/lib/CCreatureSet.h @@ -109,6 +109,7 @@ public: FactionID getFaction() const override; virtual ui64 getPower() const; + virtual ui64 getCost() const; CCreature::CreatureQuantityId getQuantityID() const; std::string getQuantityTXT(bool capitalized = true) const; virtual int getExpRank() const; @@ -274,6 +275,7 @@ public: int stacksCount() const; virtual bool needsLastStack() const; //true if last stack cannot be taken ui64 getArmyStrength() const; //sum of AI values of creatures + ui64 getArmyCost() const; //sum of cost of creatures ui64 getPower(const SlotID & slot) const; //value of specific stack std::string getRoughAmount(const SlotID & slot, int mode = 0) const; //rough size of specific stack std::string getArmyDescription() const; From 7b407b6432570c4eb1a5f246d0c1e7095ba52483 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 7 Jul 2024 22:44:52 +0200 Subject: [PATCH 005/186] AI-variant without fuzzy-logic It is now possible to switch to an AI-variant that uses hand-written heuristics for decision-making rather than the FuzzyLite-engine. This is configurable in nkai-settings.json via the new parameter "useFuzzy". --- AI/Nullkiller/Analyzers/ObjectClusterizer.cpp | 8 +- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 84 +++++++++++++------ AI/Nullkiller/Engine/Settings.cpp | 8 +- AI/Nullkiller/Engine/Settings.h | 2 + config/ai/nkai/nkai-settings.json | 2 +- 5 files changed, 76 insertions(+), 28 deletions(-) diff --git a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp index 7b9607390..5c4bb4cea 100644 --- a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp +++ b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp @@ -469,7 +469,9 @@ void ObjectClusterizer::clusterizeObject( float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj))); - if(priority < MIN_PRIORITY) + if(ai->settings->isUseFuzzy() && priority < MIN_PRIORITY) + continue; + else if (priority <= 0) continue; ClusterMap::accessor cluster; @@ -490,7 +492,9 @@ void ObjectClusterizer::clusterizeObject( float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj))); - if(priority < MIN_PRIORITY) + if (ai->settings->isUseFuzzy() && priority < MIN_PRIORITY) + continue; + else if (priority <= 0) continue; bool interestingObject = path.turn() <= 2 || priority > 0.5f; diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index f29153c20..73d77623a 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -1113,36 +1113,71 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task) double result = 0; - try + bool useFuzzy = ai->settings->isUseFuzzy(); + + if (task->hero) { - armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage); - heroRoleVariable->setValue(evaluationContext.heroRole); - mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]); - scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]); - goldRewardVariable->setValue(goldRewardPerTurn); - 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); - goldPressureVariable->setValue(ai->buildAnalyzer->getGoldPressure()); - goldCostVariable->setValue(evaluationContext.goldCost / ((float)ai->getFreeResources()[EGameResID::GOLD] + (float)ai->buildAnalyzer->getDailyIncome()[EGameResID::GOLD] + 1.0f)); - turnVariable->setValue(evaluationContext.turn); - fearVariable->setValue(evaluationContext.enemyHeroDangerRatio); - - engine->process(); - - result = value->getValue(); + if (task->hero->getOwner().getNum() > 1) + useFuzzy = true; } - catch(fl::Exception & fe) + + if (useFuzzy) { - logAi->error("evaluate VisitTile: %s", fe.getWhat()); + try + { + armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage); + heroRoleVariable->setValue(evaluationContext.heroRole); + mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]); + scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]); + goldRewardVariable->setValue(goldRewardPerTurn); + 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); + goldPressureVariable->setValue(ai->buildAnalyzer->getGoldPressure()); + goldCostVariable->setValue(evaluationContext.goldCost / ((float)ai->getFreeResources()[EGameResID::GOLD] + (float)ai->buildAnalyzer->getDailyIncome()[EGameResID::GOLD] + 1.0f)); + turnVariable->setValue(evaluationContext.turn); + fearVariable->setValue(evaluationContext.enemyHeroDangerRatio); + + engine->process(); + + result = value->getValue(); + } + catch (fl::Exception& fe) + { + logAi->error("evaluate VisitTile: %s", fe.getWhat()); + } + } + else + { + float score = evaluationContext.armyReward + evaluationContext.skillReward * 2000 + std::max((float)evaluationContext.goldReward, std::max((float)evaluationContext.armyGrowth, evaluationContext.strategicalValue * 1000)); + + if (task->hero) + { + score -= evaluationContext.armyLossPersentage * task->hero->getArmyCost(); + if (evaluationContext.enemyHeroDangerRatio > 1) + score /= evaluationContext.enemyHeroDangerRatio; + } + + if (score > 0) + { + result = score * evaluationContext.closestWayRatio / evaluationContext.movementCost; + if (task->hero) + { + if (task->hero->getArmyCost() > score + && evaluationContext.strategicalValue == 0) + result /= task->hero->getArmyCost() / score; + //logAi->trace("Score %s: %f Armyreward: %f skillReward: %f GoldReward: %f Strategical: %f Armygrowth: %f", task->toString(), score, evaluationContext.armyReward, evaluationContext.skillReward, evaluationContext.goldReward, evaluationContext.strategicalValue, evaluationContext.armyGrowth); + logAi->trace("Score %s: %f Cost: %f Dist: %f Armygrowth: %f Prio: %f", task->toString(), score, task->hero->getArmyCost(), evaluationContext.movementCost, evaluationContext.armyGrowth, result); + } + } } #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, danger: %d, role: %s, strategical value: %f, cwr: %f, fear: %f, result %f", + 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", task->toString(), evaluationContext.armyLossPersentage, (int)evaluationContext.turn, @@ -1151,6 +1186,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task) goldRewardPerTurn, evaluationContext.goldCost, evaluationContext.armyReward, + evaluationContext.skillReward, evaluationContext.danger, evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout", evaluationContext.strategicalValue, diff --git a/AI/Nullkiller/Engine/Settings.cpp b/AI/Nullkiller/Engine/Settings.cpp index db4e3f455..3631ce816 100644 --- a/AI/Nullkiller/Engine/Settings.cpp +++ b/AI/Nullkiller/Engine/Settings.cpp @@ -30,7 +30,8 @@ namespace NKAI maxpass(10), allowObjectGraph(true), useTroopsFromGarrisons(false), - openMap(true) + openMap(true), + useFuzzy(false) { JsonNode node = JsonUtils::assembleFromFiles("config/ai/nkai/nkai-settings"); @@ -69,6 +70,11 @@ namespace NKAI openMap = node.Struct()["openMap"].Bool(); } + if (!node.Struct()["useFuzzy"].isNull()) + { + useFuzzy = node.Struct()["useFuzzy"].Bool(); + } + if(!node.Struct()["useTroopsFromGarrisons"].isNull()) { useTroopsFromGarrisons = node.Struct()["useTroopsFromGarrisons"].Bool(); diff --git a/AI/Nullkiller/Engine/Settings.h b/AI/Nullkiller/Engine/Settings.h index 775f7f399..b0ec08b0d 100644 --- a/AI/Nullkiller/Engine/Settings.h +++ b/AI/Nullkiller/Engine/Settings.h @@ -29,6 +29,7 @@ namespace NKAI bool allowObjectGraph; bool useTroopsFromGarrisons; bool openMap; + bool useFuzzy; public: Settings(); @@ -41,5 +42,6 @@ namespace NKAI bool isObjectGraphAllowed() const { return allowObjectGraph; } bool isGarrisonTroopsUsageAllowed() const { return useTroopsFromGarrisons; } bool isOpenMap() const { return openMap; } + bool isUseFuzzy() const { return useFuzzy; } }; } diff --git a/config/ai/nkai/nkai-settings.json b/config/ai/nkai/nkai-settings.json index f597be497..1e757dee3 100644 --- a/config/ai/nkai/nkai-settings.json +++ b/config/ai/nkai/nkai-settings.json @@ -6,5 +6,5 @@ "maxGoldPressure" : 0.3, "useTroopsFromGarrisons" : true, "openMap": true, - "allowObjectGraph": true + "allowObjectGraph": false } \ No newline at end of file From a72d23ed8d1eebdc6a7bcf653d712a0dc8eb65b1 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 7 Jul 2024 22:51:50 +0200 Subject: [PATCH 006/186] Debug-Info Added some debug-info and non-fuzzy-specific-priority-cutoff. --- AI/Nullkiller/Engine/Nullkiller.cpp | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index fa15363f6..e85901ff0 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -349,6 +349,18 @@ void Nullkiller::makeTurn() const int MAX_DEPTH = 10; const float FAST_TASK_MINIMAL_PRIORITY = 0.7f; + float totalHeroStrength = 0; + int totalTownLevel = 0; + for (auto heroInfo : cb->getHeroesInfo()) + { + totalHeroStrength += heroInfo->getTotalStrength(); + } + for (auto townInfo : cb->getTownsInfo()) + { + totalTownLevel += townInfo->getTownLevel(); + } + logAi->info("%d Turn: %d Power: %f Townlevel: %d", cb->getPlayerID()->getNum(), cb->getDate(Date::DAY), totalHeroStrength, totalTownLevel); + resetAiState(); Goals::TGoalVec bestTasks; @@ -438,7 +450,7 @@ void Nullkiller::makeTurn() bestTask->priority); } - if(bestTask->priority < MIN_PRIORITY) + if((settings->isUseFuzzy() && bestTask->priority < MIN_PRIORITY) || (!settings->isUseFuzzy() && bestTask->priority <= 0)) { auto heroes = cb->getHeroesInfo(); auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool @@ -463,7 +475,8 @@ void Nullkiller::makeTurn() continue; } - + if (bestTask->getHero()) + logAi->info("Best task for %s should be %s with Prio: %f", bestTask->getHero()->getNameTranslated(), bestTask->toString(), bestTask->priority); if(!executeTask(bestTask)) { if(hasAnySuccess) From 94e5b5519c56a54e244b3a191a341e6fafaafa2a Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 8 Jul 2024 16:53:14 +0200 Subject: [PATCH 007/186] Fixed AI constantly visiting towns thinking they can get a huge upgrade Due to morale-considerations the AI sometimes calculated that their strongest army after doing an exchange had slightly lower total value than the army they used before. But by using unsigned "slightly lower" became near infinite. So they constantly wanted to upgrade their army because they considered it more useful than anything else. Changing the unsigned into signed fixes this. --- AI/Nullkiller/Analyzers/ArmyManager.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/Nullkiller/Analyzers/ArmyManager.h b/AI/Nullkiller/Analyzers/ArmyManager.h index 1617bd1bd..96b178c6d 100644 --- a/AI/Nullkiller/Analyzers/ArmyManager.h +++ b/AI/Nullkiller/Analyzers/ArmyManager.h @@ -32,7 +32,7 @@ struct SlotInfo struct ArmyUpgradeInfo { std::vector resultingArmy; - uint64_t upgradeValue = 0; + int64_t upgradeValue = 0; TResources upgradeCost; void addArmyToBuy(std::vector army); From 13bbb573bdfdb40cc6683e0a253984a744cfb5fe Mon Sep 17 00:00:00 2001 From: Xilmi Date: Tue, 9 Jul 2024 22:55:39 +0200 Subject: [PATCH 008/186] Spellcasting-bug-fix Fixed a bug that prevented the AI from using spells when attacking an enemy settlement that has towers. The bug was caused by noticing how greatly effective spells would be against towers but not being able to actually target them. By skipping invalid targets, this no longer is an issue. --- AI/BattleAI/BattleEvaluator.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AI/BattleAI/BattleEvaluator.cpp b/AI/BattleAI/BattleEvaluator.cpp index 7ad134b7c..98d1bf3e6 100644 --- a/AI/BattleAI/BattleEvaluator.cpp +++ b/AI/BattleAI/BattleEvaluator.cpp @@ -623,6 +623,9 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) for(const auto & unit : allUnits) { + if (!unit->isValidTarget()) + continue; + auto newHealth = unit->getAvailableHealth(); auto oldHealth = vstd::find_or(healthOfStack, unit->unitId(), 0); // old health value may not exist for newly summoned units From 073c5bee45e71cbcd037bf1b3ff659bc94a8e972 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Wed, 10 Jul 2024 02:40:42 +0200 Subject: [PATCH 009/186] Spellcasting fixes Allowed the AI to cast spells that are aimed at a location instead of a unit. For example meteor shower. Fixed an issue that caused the AI to not kill unit-stacks with spells due to only considering stacks where at least one unit survives in it's score-calculations. --- AI/BattleAI/BattleEvaluator.cpp | 72 ++++++++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 10 deletions(-) diff --git a/AI/BattleAI/BattleEvaluator.cpp b/AI/BattleAI/BattleEvaluator.cpp index 98d1bf3e6..3c4b2d52a 100644 --- a/AI/BattleAI/BattleEvaluator.cpp +++ b/AI/BattleAI/BattleEvaluator.cpp @@ -392,7 +392,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) vstd::erase_if(possibleSpells, [](const CSpell *s) { - return spellType(s) != SpellTypes::BATTLE || s->getTargetType() == spells::AimType::LOCATION; + return spellType(s) != SpellTypes::BATTLE; }); LOGFL("I know how %d of them works.", possibleSpells.size()); @@ -403,9 +403,6 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) { spells::BattleCast temp(cb->getBattle(battleID).get(), hero, spells::Mode::HERO, spell); - if(spell->getTargetType() == spells::AimType::LOCATION) - continue; - const bool FAST = true; for(auto & target : temp.findPotentialTargets(FAST)) @@ -574,7 +571,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) auto & ps = possibleCasts[i]; #if BATTLE_TRACE_LEVEL >= 1 - logAi->trace("Evaluating %s", ps.spell->getNameTranslated()); + logAi->trace("Evaluating %s to %d", ps.spell->getNameTranslated(), ps.dest.at(0).hexValue.hex ); #endif auto state = std::make_shared(env.get(), cb->getBattle(battleID)); @@ -582,7 +579,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) spells::BattleCast cast(state.get(), hero, spells::Mode::HERO, ps.spell); cast.castEval(state->getServerCallback(), ps.dest); - auto allUnits = state->battleGetUnitsIf([](const battle::Unit * u) -> bool { return true; }); + auto allUnits = state->battleGetUnitsIf([](const battle::Unit * u) -> bool { return u->isValidTarget(); }); auto needFullEval = vstd::contains_if(allUnits, [&](const battle::Unit * u) -> bool { @@ -621,11 +618,62 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) ps.value = scoreEvaluator.evaluateExchange(*cachedAttack, 0, *targets, innerCache, state); } - for(const auto & unit : allUnits) + //! Some units may be dead alltogether. So if they existed before but not now, we know they were killed by the spell + for (const auto& unit : all) { if (!unit->isValidTarget()) continue; - + bool isDead = true; + for (const auto& remainingUnit : allUnits) + { + if (remainingUnit->unitId() == unit->unitId()) + isDead = false; + } + if (isDead) + { + auto newHealth = 0; + auto oldHealth = vstd::find_or(healthOfStack, unit->unitId(), 0); + if (oldHealth != newHealth) + { + auto damage = std::abs(oldHealth - newHealth); + auto originalDefender = cb->getBattle(battleID)->battleGetUnitByID(unit->unitId()); + + auto dpsReduce = AttackPossibility::calculateDamageReduce( + nullptr, + originalDefender && originalDefender->alive() ? originalDefender : unit, + damage, + innerCache, + state); + + auto ourUnit = unit->unitSide() == side ? 1 : -1; + auto goodEffect = newHealth > oldHealth ? 1 : -1; + + if (ourUnit * goodEffect == 1) + { + if (ourUnit && goodEffect && (unit->isClone() || unit->isGhost())) + continue; + + ps.value += dpsReduce * scoreEvaluator.getPositiveEffectMultiplier(); + } + else + ps.value -= dpsReduce * scoreEvaluator.getNegativeEffectMultiplier(); + +#if BATTLE_TRACE_LEVEL >= 1 + logAi->trace( + "Spell %s to %d affects %s (%d), dps: %2f oldHealth: %d newHealth: %d", + ps.spell->getNameTranslated(), + ps.dest.at(0).hexValue.hex, + unit->creatureId().toCreature()->getNameSingularTranslated(), + unit->getCount(), + dpsReduce, + oldHealth, + newHealth); +#endif + } + } + } + for(const auto & unit : allUnits) + { auto newHealth = unit->getAvailableHealth(); auto oldHealth = vstd::find_or(healthOfStack, unit->unitId(), 0); // old health value may not exist for newly summoned units @@ -656,10 +704,14 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) #if BATTLE_TRACE_LEVEL >= 1 logAi->trace( - "Spell affects %s (%d), dps: %2f", + "Spell %s to %d affects %s (%d), dps: %2f oldHealth: %d newHealth: %d", + ps.spell->getNameTranslated(), + ps.dest.at(0).hexValue.hex, unit->creatureId().toCreature()->getNameSingularTranslated(), unit->getCount(), - dpsReduce); + dpsReduce, + oldHealth, + newHealth); #endif } } From 3fed58c47bbbf0832b0d976cf1518c7c6712f33f Mon Sep 17 00:00:00 2001 From: Xilmi Date: Fri, 12 Jul 2024 15:36:29 +0200 Subject: [PATCH 010/186] Gold-pressure --- AI/Nullkiller/Analyzers/BuildAnalyzer.cpp | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp index 2580a3b7e..6649dd00c 100644 --- a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp @@ -165,15 +165,8 @@ void BuildAnalyzer::update() updateDailyIncome(); - if(ai->cb->getDate(Date::DAY) == 1) - { - goldPressure = 1; - } - else - { - goldPressure = ai->getLockedResources()[EGameResID::GOLD] / 5000.0f + goldPressure = ai->getLockedResources()[EGameResID::GOLD] / 5000.0f + (float)armyCost[EGameResID::GOLD] / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f); - } logAi->trace("Gold pressure: %f", goldPressure); } From fdaac9d3c3681999234cd0230b8fabb139e02ed7 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Fri, 12 Jul 2024 15:39:50 +0200 Subject: [PATCH 011/186] Hero-hiring When we have no hero, we will definitely want to hire one. We will also want to hire heroes who already pay for more than themselves by coming with an army that has more value than the hero costs. --- AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp index f086b62cd..afe263076 100644 --- a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp +++ b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp @@ -45,6 +45,9 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const minScoreToHireMain = newScore; } } + // If we don't have any heros we might want to lower our expectations. + if (ourHeroes.empty()) + minScoreToHireMain = 0; for(auto town : towns) { @@ -55,8 +58,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const for(auto hero : availableHeroes) { auto score = ai->heroManager->evaluateHero(hero); - - if(score > minScoreToHireMain) + if(score > minScoreToHireMain || hero->getArmyCost() > GameConstants::HERO_GOLD_COST) { tasks.push_back(Goals::sptr(Goals::RecruitHero(town, hero).setpriority(200))); break; From 663ca23f6febd9fb39bc14b65fe24d7412152346 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Fri, 12 Jul 2024 15:41:55 +0200 Subject: [PATCH 012/186] Army-hiring Supressing hiring army on turn one seems just bad. Starting the main-hero as strong as possible seems like a good idea to me and hiring the available troops outright will help achieve that goal. However, if there's a hero for hire, who has army with him that is a better deal, we hire that one first. --- AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp | 24 ++++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp b/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp index d53adc023..e988c4519 100644 --- a/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp @@ -28,9 +28,6 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const { Goals::TGoalVec tasks; - if(ai->cb->getDate(Date::DAY) == 1) - return tasks; - auto heroes = cb->getHeroesInfo(); if(heroes.empty()) @@ -40,17 +37,28 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const for(auto town : cb->getTownsInfo()) { + //If we can recruit a hero that comes with more army than he costs, we are better off spending our gold on them + if (ai->heroManager->canRecruitHero(town)) + { + auto availableHeroes = ai->cb->getAvailableHeroes(town); + for (auto hero : availableHeroes) + { + if (hero->getArmyCost() > GameConstants::HERO_GOLD_COST) + return tasks; + } + } + + if (ai->buildAnalyzer->isGoldPressureHigh() && !town->hasBuilt(BuildingID::CITY_HALL)) + { + return tasks; + } + auto townArmyAvailableToBuy = ai->armyManager->getArmyAvailableToBuyAsCCreatureSet( town, ai->getFreeResources()); for(const CGHeroInstance * targetHero : heroes) { - if(ai->buildAnalyzer->isGoldPressureHigh() && !town->hasBuilt(BuildingID::CITY_HALL)) - { - continue; - } - if(ai->heroManager->getHeroRole(targetHero) == HeroRole::MAIN) { auto reinforcement = ai->armyManager->howManyReinforcementsCanGet( From 638c1350b8a4485974892801d9e06d39b88643a1 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Fri, 12 Jul 2024 15:43:40 +0200 Subject: [PATCH 013/186] Path-safety No longer excluding paths for exposing a hero to an enemy in the behaviors. There definitely are reasons for doing something anyways, even if threatened. The logic for that should be done in the PriorityEvaluator. --- .../Behaviors/CaptureObjectsBehavior.cpp | 8 -------- AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp | 17 ----------------- 2 files changed, 25 deletions(-) diff --git a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp index e20753317..1b5891a7c 100644 --- a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp +++ b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp @@ -68,14 +68,6 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals( logAi->trace("Path found %s", path.toString()); #endif - if(nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path)) - { -#if NKAI_TRACE_LEVEL >= 2 - logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.getHeroStrength()); -#endif - continue; - } - if(objToVisit && !force && !shouldVisit(nullkiller, path.targetHero, objToVisit)) { #if NKAI_TRACE_LEVEL >= 2 diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp index b9a675837..4b0dc34d1 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp @@ -89,14 +89,6 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con continue; } - if(path.turn() > 0 && ai->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path)) - { -#if NKAI_TRACE_LEVEL >= 2 - logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength()); -#endif - continue; - } - if(ai->arePathHeroesLocked(path)) { #if NKAI_TRACE_LEVEL >= 2 @@ -294,15 +286,6 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT auto heroRole = ai->heroManager->getHeroRole(path.targetHero); - if(heroRole == HeroRole::SCOUT - && ai->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path)) - { -#if NKAI_TRACE_LEVEL >= 2 - logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength()); -#endif - continue; - } - auto upgrade = ai->armyManager->calculateCreaturesUpgrade(path.heroArmy, upgrader, availableResources); if(!upgrader->garrisonHero From 9456ab41dacf06bc33d68df47d540995c207abf7 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Fri, 12 Jul 2024 17:39:52 +0200 Subject: [PATCH 014/186] Priority-tier Openmap is no longer tied to difficulty-level due to being configurable anyways. Tasks are now in different priority-tiers. For now there's 2 tiers. One for regular tasks and one for tasks of the new "conquest"-type. Regular tasks will only be considered when no possible conquest-type tasks were found. Recruit-hero-behavior is now evaluated before movement to make it more likely a new hero can exchange their stuff with others. --- AI/Nullkiller/Engine/Nullkiller.cpp | 21 +++++++++++---------- AI/Nullkiller/Engine/Nullkiller.h | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index f10f55927..2d3ed5243 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -62,7 +62,7 @@ bool canUseOpenMap(std::shared_ptr cb, PlayerColor playerID) return false; } - return cb->getStartInfo()->difficulty >= 3; + return true; } void Nullkiller::init(std::shared_ptr cb, AIGateway * gateway) @@ -122,6 +122,9 @@ void TaskPlan::merge(TSubgoal task) { TGoalVec blockers; + if (task->asTask()->priority <= 0) + return; + for(auto & item : tasks) { for(auto objid : item.affectedObjects) @@ -166,11 +169,11 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TGoalVec & tasks) const return taskptr(*bestTask); } -Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks) const +Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks, int priorityTier) const { TaskPlan taskPlan; - tbb::parallel_for(tbb::blocked_range(0, tasks.size()), [this, &tasks](const tbb::blocked_range & r) + tbb::parallel_for(tbb::blocked_range(0, tasks.size()), [this, &tasks, priorityTier](const tbb::blocked_range & r) { auto evaluator = this->priorityEvaluators->acquire(); @@ -179,7 +182,7 @@ Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks) const auto task = tasks[i]; if(task->asTask()->priority <= 0) - task->asTask()->priority = evaluator->evaluate(task); + task->asTask()->priority = evaluator->evaluate(task, priorityTier); } }); @@ -359,7 +362,6 @@ void Nullkiller::makeTurn() { totalTownLevel += townInfo->getTownLevel(); } - logAi->info("%d Turn: %d Power: %f Townlevel: %d", cb->getPlayerID()->getNum(), cb->getDate(Date::DAY), totalHeroStrength, totalTownLevel); resetAiState(); @@ -376,6 +378,7 @@ void Nullkiller::makeTurn() { bestTasks.clear(); + decompose(bestTasks, sptr(RecruitHeroBehavior()), 1); decompose(bestTasks, sptr(BuyArmyBehavior()), 1); decompose(bestTasks, sptr(BuildingBehavior()), 1); @@ -394,7 +397,6 @@ void Nullkiller::makeTurn() } } - decompose(bestTasks, sptr(RecruitHeroBehavior()), 1); decompose(bestTasks, sptr(CaptureObjectsBehavior()), 1); decompose(bestTasks, sptr(ClusterBehavior()), MAX_DEPTH); decompose(bestTasks, sptr(DefenceBehavior()), MAX_DEPTH); @@ -408,8 +410,9 @@ void Nullkiller::makeTurn() { decompose(bestTasks, sptr(StartupBehavior()), 1); } - - auto selectedTasks = buildPlan(bestTasks); + auto selectedTasks = buildPlan(bestTasks, 0); + if (selectedTasks.empty() && !settings->isUseFuzzy()) + selectedTasks = buildPlan(bestTasks, 1); std::sort(selectedTasks.begin(), selectedTasks.end(), [](const TTask& a, const TTask& b) { @@ -480,8 +483,6 @@ void Nullkiller::makeTurn() continue; } - if (bestTask->getHero()) - logAi->info("Best task for %s should be %s with Prio: %f", bestTask->getHero()->getNameTranslated(), bestTask->toString(), bestTask->priority); if(!executeTask(bestTask)) { if(hasAnySuccess) diff --git a/AI/Nullkiller/Engine/Nullkiller.h b/AI/Nullkiller/Engine/Nullkiller.h index af05e354b..0494464cd 100644 --- a/AI/Nullkiller/Engine/Nullkiller.h +++ b/AI/Nullkiller/Engine/Nullkiller.h @@ -126,7 +126,7 @@ private: void updateAiState(int pass, bool fast = false); void decompose(Goals::TGoalVec & result, Goals::TSubgoal behavior, int decompositionMaxDepth) const; Goals::TTask choseBestTask(Goals::TGoalVec & tasks) const; - Goals::TTaskVec buildPlan(Goals::TGoalVec & tasks) const; + Goals::TTaskVec buildPlan(Goals::TGoalVec & tasks, int priorityTier = 1) const; bool executeTask(Goals::TTask task); bool areAffectedObjectsPresent(Goals::TTask task) const; HeroRole getTaskRole(Goals::TTask task) const; From 92bed2305e0431d547abfb345606ac77e06e099b Mon Sep 17 00:00:00 2001 From: Xilmi Date: Fri, 12 Jul 2024 17:41:46 +0200 Subject: [PATCH 015/186] priority-tiers Tasks are now in different priority-tiers. For now there's 2 tiers. One for regular tasks and one for tasks of the new "conquest"-type. Regular tasks will only be considered when no possible conquest-type tasks were found. Slightly reworked scoring heuristics. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 156 ++++++++++++++------- AI/Nullkiller/Engine/PriorityEvaluator.h | 5 +- 2 files changed, 108 insertions(+), 53 deletions(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 73d77623a..59a0a5b3c 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -51,9 +51,11 @@ EvaluationContext::EvaluationContext(const Nullkiller * ai) heroRole(HeroRole::SCOUT), turn(0), strategicalValue(0), + conquestValue(0), evaluator(ai), enemyHeroDangerRatio(0), - armyGrowth(0) + armyGrowth(0), + armyInvolvement(0) { } @@ -551,6 +553,54 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons } } +float RewardEvaluator::getConquestValue(const CGObjectInstance* target) const +{ + if (!target) + return 0; + if (target->getOwner() == ai->playerID) + return 0; + switch (target->ID) + { + case Obj::TOWN: + { + if (ai->buildAnalyzer->getDevelopmentInfo().empty()) + return 10.0f; + + auto town = dynamic_cast(target); + + if (town->getOwner() == ai->playerID) + { + 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: + return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES + ? getEnemyHeroStrategicalValue(dynamic_cast(target)) + : 0; + + case Obj::KEYMASTER: + return 0.6f; + + default: + return 0; + } +} + float RewardEvaluator::evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const { auto rewardable = dynamic_cast(hut); @@ -880,7 +930,9 @@ public: 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); evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army); + evaluationContext.armyInvolvement += army->getArmyCost(); } vstd::amax(evaluationContext.armyLossPersentage, path.getTotalArmyLoss() / (double)path.getHeroStrength()); @@ -924,6 +976,7 @@ public: 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; @@ -1100,7 +1153,7 @@ EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal return context; } -float PriorityEvaluator::evaluate(Goals::TSubgoal task) +float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) { auto evaluationContext = buildEvaluationContext(task); @@ -1113,66 +1166,65 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task) double result = 0; - bool useFuzzy = ai->settings->isUseFuzzy(); - - if (task->hero) + float fuzzyResult = 0; + try { - if (task->hero->getOwner().getNum() > 1) - useFuzzy = true; + armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage); + heroRoleVariable->setValue(evaluationContext.heroRole); + mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]); + scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]); + goldRewardVariable->setValue(goldRewardPerTurn); + 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); + goldPressureVariable->setValue(ai->buildAnalyzer->getGoldPressure()); + goldCostVariable->setValue(evaluationContext.goldCost / ((float)ai->getFreeResources()[EGameResID::GOLD] + (float)ai->buildAnalyzer->getDailyIncome()[EGameResID::GOLD] + 1.0f)); + turnVariable->setValue(evaluationContext.turn); + fearVariable->setValue(evaluationContext.enemyHeroDangerRatio); + + engine->process(); + + fuzzyResult = value->getValue(); } - - if (useFuzzy) + catch (fl::Exception& fe) { - try - { - armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage); - heroRoleVariable->setValue(evaluationContext.heroRole); - mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]); - scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]); - goldRewardVariable->setValue(goldRewardPerTurn); - 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); - goldPressureVariable->setValue(ai->buildAnalyzer->getGoldPressure()); - goldCostVariable->setValue(evaluationContext.goldCost / ((float)ai->getFreeResources()[EGameResID::GOLD] + (float)ai->buildAnalyzer->getDailyIncome()[EGameResID::GOLD] + 1.0f)); - turnVariable->setValue(evaluationContext.turn); - fearVariable->setValue(evaluationContext.enemyHeroDangerRatio); - - engine->process(); - - result = value->getValue(); - } - catch (fl::Exception& fe) - { - logAi->error("evaluate VisitTile: %s", fe.getWhat()); - } + logAi->error("evaluate VisitTile: %s", fe.getWhat()); + } + if (ai->settings->isUseFuzzy()) + { + result = fuzzyResult; } else { - float score = evaluationContext.armyReward + evaluationContext.skillReward * 2000 + std::max((float)evaluationContext.goldReward, std::max((float)evaluationContext.armyGrowth, evaluationContext.strategicalValue * 1000)); - - if (task->hero) + float score = 0; + if (priorityTier == 0) { - score -= evaluationContext.armyLossPersentage * task->hero->getArmyCost(); - if (evaluationContext.enemyHeroDangerRatio > 1) - score /= evaluationContext.enemyHeroDangerRatio; + score += evaluationContext.conquestValue * 1000; + if (score == 0) + return score; + } + else + { + 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) { - result = score * evaluationContext.closestWayRatio / evaluationContext.movementCost; - if (task->hero) - { - if (task->hero->getArmyCost() > score - && evaluationContext.strategicalValue == 0) - result /= task->hero->getArmyCost() / score; - //logAi->trace("Score %s: %f Armyreward: %f skillReward: %f GoldReward: %f Strategical: %f Armygrowth: %f", task->toString(), score, evaluationContext.armyReward, evaluationContext.skillReward, evaluationContext.goldReward, evaluationContext.strategicalValue, evaluationContext.armyGrowth); - logAi->trace("Score %s: %f Cost: %f Dist: %f Armygrowth: %f Prio: %f", task->toString(), score, task->hero->getArmyCost(), evaluationContext.movementCost, evaluationContext.armyGrowth, result); - } + score *= evaluationContext.closestWayRatio; + if (evaluationContext.enemyHeroDangerRatio > 1) + score /= evaluationContext.enemyHeroDangerRatio; + if (evaluationContext.movementCost > 0) + score /= evaluationContext.movementCost; + score *= (1 - evaluationContext.armyLossPersentage); + result = score; } } diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index 02dbb2fad..3353ff0b9 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -40,6 +40,7 @@ public: float getEnemyHeroStrategicalValue(const CGHeroInstance * enemy) const; float getResourceRequirementStrength(int resType) const; float getStrategicalValue(const CGObjectInstance * target) const; + float getConquestValue(const CGObjectInstance* target) const; float getTotalResourceRequirementStrength(int resType) const; float evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const; float getSkillReward(const CGObjectInstance * target, const CGHeroInstance * hero, HeroRole role) const; @@ -64,10 +65,12 @@ struct DLL_EXPORT EvaluationContext int32_t goldCost; float skillReward; float strategicalValue; + float conquestValue; HeroRole heroRole; uint8_t turn; RewardEvaluator evaluator; float enemyHeroDangerRatio; + float armyInvolvement; EvaluationContext(const Nullkiller * ai); @@ -90,7 +93,7 @@ public: ~PriorityEvaluator(); void initVisitTile(); - float evaluate(Goals::TSubgoal task); + float evaluate(Goals::TSubgoal task, int priorityTier = 1); private: const Nullkiller * ai; From 102b537353c7558dbbbaafa70b6367e8e3ec7ce9 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Fri, 12 Jul 2024 23:33:58 +0200 Subject: [PATCH 016/186] Recruit-behavior Moved recruit-behavior back to tasks that plans get made for since otherwise it can mess up plans by blocking town-exits. --- AI/Nullkiller/Engine/Nullkiller.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 2d3ed5243..de45b02d8 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -378,7 +378,6 @@ void Nullkiller::makeTurn() { bestTasks.clear(); - decompose(bestTasks, sptr(RecruitHeroBehavior()), 1); decompose(bestTasks, sptr(BuyArmyBehavior()), 1); decompose(bestTasks, sptr(BuildingBehavior()), 1); @@ -397,6 +396,7 @@ void Nullkiller::makeTurn() } } + decompose(bestTasks, sptr(RecruitHeroBehavior()), 1); decompose(bestTasks, sptr(CaptureObjectsBehavior()), 1); decompose(bestTasks, sptr(ClusterBehavior()), MAX_DEPTH); decompose(bestTasks, sptr(DefenceBehavior()), MAX_DEPTH); From d878d0ce18792c01c91e0dd66027fc1e4004b364 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Fri, 12 Jul 2024 23:36:41 +0200 Subject: [PATCH 017/186] Avoid being killed Heroes with conquest-tasks will only endanger themselves to be killed when they can execute a conquest in the same turn. Heroes with other tasks will dismiss any tasks except of defending when they'd be within one turn of an enemy hero that could kill them. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 13 +++++++++---- AI/Nullkiller/Engine/PriorityEvaluator.h | 1 + 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 59a0a5b3c..f4ccd68a2 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -37,7 +37,7 @@ namespace NKAI #define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us const float MIN_CRITICAL_VALUE = 2.0f; -EvaluationContext::EvaluationContext(const Nullkiller * ai) +EvaluationContext::EvaluationContext(const Nullkiller* ai) : movementCost(0.0), manaCost(0), danger(0), @@ -55,7 +55,8 @@ EvaluationContext::EvaluationContext(const Nullkiller * ai) evaluator(ai), enemyHeroDangerRatio(0), armyGrowth(0), - armyInvolvement(0) + armyInvolvement(0), + isDefend(false) { } @@ -874,6 +875,8 @@ public: else evaluationContext.addNonCriticalStrategicalValue(1.7f * multiplier * strategicalValue); + evaluationContext.isDefend = true; + vstd::amax(evaluationContext.danger, defendTown.getTreat().danger); addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength()); } @@ -1204,11 +1207,13 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) if (priorityTier == 0) { score += evaluationContext.conquestValue * 1000; - if (score == 0) - return score; + 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; diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index 3353ff0b9..60295b397 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -71,6 +71,7 @@ struct DLL_EXPORT EvaluationContext RewardEvaluator evaluator; float enemyHeroDangerRatio; float armyInvolvement; + bool isDefend; EvaluationContext(const Nullkiller * ai); From 53c51b427864fd88106ef386b6a0d71e292f96f6 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 15 Jul 2024 17:26:06 +0200 Subject: [PATCH 018/186] Allow all buildings Added resource-silo and special buildings to things that AI can theoretically build. --- AI/Nullkiller/Analyzers/BuildAnalyzer.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp index 6649dd00c..641c75c2c 100644 --- a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp @@ -40,7 +40,6 @@ void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo) for(BuildingID prefix : prefixes) { BuildingID building = BuildingID(prefix + level); - if(!vstd::contains(buildings, building)) continue; // no such building in town @@ -79,6 +78,12 @@ void BuildAnalyzer::updateOtherBuildings(TownDevelopmentInfo & developmentInfo) otherBuildings.push_back({BuildingID::HORDE_2}); } + otherBuildings.push_back({ BuildingID::RESOURCE_SILO }); + otherBuildings.push_back({ BuildingID::SPECIAL_1 }); + otherBuildings.push_back({ BuildingID::SPECIAL_2 }); + otherBuildings.push_back({ BuildingID::SPECIAL_3 }); + otherBuildings.push_back({ BuildingID::SPECIAL_4 }); + for(auto & buildingSet : otherBuildings) { for(auto & buildingID : buildingSet) From 48ecbd4cbf6b58a7a75dbf269e81a7e5e4f835ee Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 15 Jul 2024 17:28:10 +0200 Subject: [PATCH 019/186] Threat in DangerHitMap Added new value threat to DangerHitMapAnalayzer. The purpose is to allow decisions to be less binary around enemy heros. --- AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp | 1 + AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h | 2 ++ 2 files changed, 3 insertions(+) diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp index 6d11d01ba..2f9662a62 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp @@ -96,6 +96,7 @@ void DangerHitMapAnalyzer::updateHitMap() newThreat.hero = path.targetHero; newThreat.turn = path.turn(); + newThreat.threat = path.getHeroStrength() * (1 - path.movementCost() / 2.0); newThreat.danger = path.getHeroStrength(); if(newThreat.value() > node.maximumDanger.value()) diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h index fc2890846..2bd39a2d8 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h @@ -22,6 +22,7 @@ struct HitMapInfo uint64_t danger; uint8_t turn; + float threat; HeroPtr hero; HitMapInfo() @@ -33,6 +34,7 @@ struct HitMapInfo { danger = 0; turn = 255; + threat = 0; hero = HeroPtr(); } From e2e3f9e638114006237c7510c9f43beaf7293a3c Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 15 Jul 2024 17:30:42 +0200 Subject: [PATCH 020/186] Take hero-stats into account for attacking hero-defended entities Instead of using just the strength of the raw army, the hero-stats are now taking into account for the generation of the danger-map. --- AI/Nullkiller/Engine/FuzzyHelper.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/AI/Nullkiller/Engine/FuzzyHelper.cpp b/AI/Nullkiller/Engine/FuzzyHelper.cpp index ed7abb7c1..10fab5cc9 100644 --- a/AI/Nullkiller/Engine/FuzzyHelper.cpp +++ b/AI/Nullkiller/Engine/FuzzyHelper.cpp @@ -71,6 +71,7 @@ ui64 FuzzyHelper::evaluateDanger(const int3 & tile, const CGHeroInstance * visit { objectDanger += evaluateDanger(hero->visitedTown.get()); } + objectDanger *= ai->heroManager->getFightingStrengthCached(hero); } if(objectDanger) From 83ffbdff2b3ec38658ab842a1dc973a3a61b7d4e Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 15 Jul 2024 17:35:54 +0200 Subject: [PATCH 021/186] Fix for double army-loss Fixed that army loss was taken into account both for the path and the target-object. In certain cases, like a hero defending a town, this could lead to armyloss being twice as high as it should be. --- AI/Nullkiller/Pathfinding/AINodeStorage.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index b113e27ae..b5cc485c3 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -1509,7 +1509,7 @@ uint8_t AIPath::turn() const uint64_t AIPath::getHeroStrength() const { - return targetHero->getFightingStrength() * getHeroArmyStrengthWithCommander(targetHero, heroArmy); + return targetHero->getHeroStrength() * getHeroArmyStrengthWithCommander(targetHero, heroArmy); } uint64_t AIPath::getTotalDanger() const @@ -1536,7 +1536,7 @@ bool AIPath::containsHero(const CGHeroInstance * hero) const uint64_t AIPath::getTotalArmyLoss() const { - return armyLoss + targetObjectArmyLoss; + return armyLoss > targetObjectArmyLoss ? armyLoss : targetObjectArmyLoss; } std::string AIPath::toString() const From f8f10adb2e4eb8d38a9a2a07a391cca033e925b4 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 15 Jul 2024 17:42:02 +0200 Subject: [PATCH 022/186] Take magic-capability into account for overall strength-estimation of hero-lead-armies The magic-strength of a hero now checks if the hero has a spellbook and at least one combat-spell. The impact of knowledge and spellpower to the hero's magic-strength is now also depending on it's current and max mana-pool-size as an empty mana-pool does not exactly contribute well to fights. Replaced every call of getFightingStrength() with getHeroStrength() which uses both the fightingStrength and the (reworked) magicStrength to guess how much stronger a hero-lead army is. --- AI/Nullkiller/AIUtility.cpp | 2 +- AI/Nullkiller/Analyzers/HeroManager.cpp | 6 +++--- AI/Nullkiller/Pathfinding/Actors.cpp | 2 +- lib/mapObjects/CGHeroInstance.cpp | 17 +++++++++++++++-- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/AI/Nullkiller/AIUtility.cpp b/AI/Nullkiller/AIUtility.cpp index e55330fbd..4de840cff 100644 --- a/AI/Nullkiller/AIUtility.cpp +++ b/AI/Nullkiller/AIUtility.cpp @@ -149,7 +149,7 @@ bool HeroPtr::operator==(const HeroPtr & rhs) const bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet * heroArmy, uint64_t dangerStrength) { - const ui64 heroStrength = h->getFightingStrength() * heroArmy->getArmyStrength(); + const ui64 heroStrength = h->getHeroStrength() * heroArmy->getArmyStrength(); if(dangerStrength) { diff --git a/AI/Nullkiller/Analyzers/HeroManager.cpp b/AI/Nullkiller/Analyzers/HeroManager.cpp index 44b66b46b..e2406a023 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -109,7 +109,7 @@ void HeroManager::update() for(auto & hero : myHeroes) { scores[hero] = evaluateFightingStrength(hero); - knownFightingStrength[hero->id] = hero->getFightingStrength(); + knownFightingStrength[hero->id] = hero->getHeroStrength(); } auto scoreSort = [&](const CGHeroInstance * h1, const CGHeroInstance * h2) -> bool @@ -205,7 +205,7 @@ float HeroManager::getFightingStrengthCached(const CGHeroInstance * hero) const auto cached = knownFightingStrength.find(hero->id); //FIXME: fallback to hero->getFightingStrength() is VERY slow on higher difficulties (no object graph? map reveal?) - return cached != knownFightingStrength.end() ? cached->second : hero->getFightingStrength(); + return cached != knownFightingStrength.end() ? cached->second : hero->getHeroStrength(); } float HeroManager::getMagicStrength(const CGHeroInstance * hero) const @@ -298,7 +298,7 @@ const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) co continue; } - if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength()) + if(!weakestHero || weakestHero->getHeroStrength() > existingHero->getHeroStrength()) { weakestHero = existingHero; } diff --git a/AI/Nullkiller/Pathfinding/Actors.cpp b/AI/Nullkiller/Pathfinding/Actors.cpp index ae6b6446e..1af649e66 100644 --- a/AI/Nullkiller/Pathfinding/Actors.cpp +++ b/AI/Nullkiller/Pathfinding/Actors.cpp @@ -46,7 +46,7 @@ ChainActor::ChainActor(const CGHeroInstance * hero, HeroRole heroRole, uint64_t initialMovement = hero->movementPointsRemaining(); initialTurn = 0; armyValue = getHeroArmyStrengthWithCommander(hero, hero); - heroFightingStrength = hero->getFightingStrength(); + heroFightingStrength = hero->getHeroStrength(); tiCache.reset(new TurnInfo(hero)); } diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index c99667f58..b4119f891 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -651,7 +651,20 @@ double CGHeroInstance::getFightingStrength() const double CGHeroInstance::getMagicStrength() const { - return sqrt((1.0 + 0.05*getPrimSkillLevel(PrimarySkill::KNOWLEDGE)) * (1.0 + 0.05*getPrimSkillLevel(PrimarySkill::SPELL_POWER))); + if (!hasSpellbook()) + return 1; + bool atLeastOneCombatSpell = false; + for (auto spell : spells) + { + if (spellbookContainsSpell(spell) && spell.toSpell()->isCombat()) + { + atLeastOneCombatSpell = true; + break; + } + } + if (!atLeastOneCombatSpell) + return 1; + return sqrt((1.0 + 0.05*getPrimSkillLevel(PrimarySkill::KNOWLEDGE) * mana / manaLimit()) * (1.0 + 0.05*getPrimSkillLevel(PrimarySkill::SPELL_POWER) * mana / manaLimit())); } double CGHeroInstance::getHeroStrength() const @@ -661,7 +674,7 @@ double CGHeroInstance::getHeroStrength() const ui64 CGHeroInstance::getTotalStrength() const { - double ret = getFightingStrength() * getArmyStrength(); + double ret = getHeroStrength() * getArmyStrength(); return static_cast(ret); } From ce4905ac4c882ec9d47285c403e9a70322e37693 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 15 Jul 2024 17:47:27 +0200 Subject: [PATCH 023/186] Ivan's fix to brown arrows after new turn A fix Ivan posted about on Discord that takes care of a newly introduced bug in development-branch that you had to reselect your hero manually after a new turn because he would otherwise think he's still on last-turn when it came to executing planned movement. --- client/adventureMap/AdventureMapInterface.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index 43f005199..2a4dbe0be 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -444,7 +444,7 @@ void AdventureMapInterface::onPlayerTurnStarted(PlayerColor playerID) LOCPLINT->localState->setSelection(LOCPLINT->localState->getWanderingHero(0)); } - centerOnObject(LOCPLINT->localState->getCurrentArmy()); + onSelectionChanged(LOCPLINT->localState->getCurrentArmy()); //show new day animation and sound on infobar, except for 1st day of the game if (LOCPLINT->cb->getDate(Date::DAY) != 1) From 95ba57dfe23daf947bc666e5557a78d40bceb5cd Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 15 Jul 2024 17:50:30 +0200 Subject: [PATCH 024/186] No more Startup-behavior Startup-behavior was messing with my intended logic. Mostly by getting excess heroes for no real purpose other than that it could. This wasted a lot of money that could be better invested on subsequent turns. I removed it and playing-strength actually went up. --- AI/Nullkiller/Engine/Nullkiller.cpp | 9 +++++---- AI/Nullkiller/Engine/Nullkiller.h | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index de45b02d8..f448f7b43 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -371,6 +371,7 @@ void Nullkiller::makeTurn() { auto start = std::chrono::high_resolution_clock::now(); updateAiState(i); + //logAi->info("Gold: %d", cb->getResourceAmount(EGameResID::GOLD)); Goals::TTask bestTask = taskptr(Goals::Invalid()); @@ -385,6 +386,7 @@ void Nullkiller::makeTurn() if(bestTask->priority >= FAST_TASK_MINIMAL_PRIORITY) { + //logAi->info("Performing task %s with prio: %d", bestTask->toString(), bestTask->priority); if(!executeTask(bestTask)) return; @@ -406,13 +408,11 @@ void Nullkiller::makeTurn() if(!isOpenMap()) decompose(bestTasks, sptr(ExplorationBehavior()), MAX_DEPTH); - if(cb->getDate(Date::DAY) == 1 || heroManager->getHeroRoles().empty()) - { - decompose(bestTasks, sptr(StartupBehavior()), 1); - } auto selectedTasks = buildPlan(bestTasks, 0); if (selectedTasks.empty() && !settings->isUseFuzzy()) selectedTasks = buildPlan(bestTasks, 1); + if (selectedTasks.empty() && !settings->isUseFuzzy()) + selectedTasks = buildPlan(bestTasks, 2); std::sort(selectedTasks.begin(), selectedTasks.end(), [](const TTask& a, const TTask& b) { @@ -483,6 +483,7 @@ void Nullkiller::makeTurn() continue; } + //logAi->info("Performing task %s with prio: %d", bestTask->toString(), bestTask->priority); if(!executeTask(bestTask)) { if(hasAnySuccess) diff --git a/AI/Nullkiller/Engine/Nullkiller.h b/AI/Nullkiller/Engine/Nullkiller.h index 0494464cd..4b25dd3ec 100644 --- a/AI/Nullkiller/Engine/Nullkiller.h +++ b/AI/Nullkiller/Engine/Nullkiller.h @@ -126,7 +126,7 @@ private: void updateAiState(int pass, bool fast = false); void decompose(Goals::TGoalVec & result, Goals::TSubgoal behavior, int decompositionMaxDepth) const; Goals::TTask choseBestTask(Goals::TGoalVec & tasks) const; - Goals::TTaskVec buildPlan(Goals::TGoalVec & tasks, int priorityTier = 1) const; + Goals::TTaskVec buildPlan(Goals::TGoalVec & tasks, int priorityTier = 3) const; bool executeTask(Goals::TTask task); bool areAffectedObjectsPresent(Goals::TTask task) const; HeroRole getTaskRole(Goals::TTask task) const; From 4a552d411ce4390fc169f622cabf210c0d9b31ec Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 15 Jul 2024 18:12:52 +0200 Subject: [PATCH 025/186] 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; From d4b4a6c4dbf71ffe1fefc49ebab515169a796b91 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 15 Jul 2024 19:57:33 +0200 Subject: [PATCH 026/186] Warnings Commented out relics from debugging that were causing warnings for unused variables. --- AI/Nullkiller/Engine/Nullkiller.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index f448f7b43..1fc02aaea 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -352,16 +352,16 @@ void Nullkiller::makeTurn() const int MAX_DEPTH = 10; const float FAST_TASK_MINIMAL_PRIORITY = 0.7f; - float totalHeroStrength = 0; - int totalTownLevel = 0; - for (auto heroInfo : cb->getHeroesInfo()) - { - totalHeroStrength += heroInfo->getTotalStrength(); - } - for (auto townInfo : cb->getTownsInfo()) - { - totalTownLevel += townInfo->getTownLevel(); - } + //float totalHeroStrength = 0; + //int totalTownLevel = 0; + //for (auto heroInfo : cb->getHeroesInfo()) + //{ + // totalHeroStrength += heroInfo->getTotalStrength(); + //} + //for (auto townInfo : cb->getTownsInfo()) + //{ + // totalTownLevel += townInfo->getTownLevel(); + //} resetAiState(); From 455ad648ae751d52d2790737f82d315c91b8d612 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 15 Jul 2024 20:09:11 +0200 Subject: [PATCH 027/186] More warning fixes heroRole is no longer needed here. --- AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp index 4b0dc34d1..4a17bb429 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp @@ -284,8 +284,6 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT continue; } - auto heroRole = ai->heroManager->getHeroRole(path.targetHero); - auto upgrade = ai->armyManager->calculateCreaturesUpgrade(path.heroArmy, upgrader, availableResources); if(!upgrader->garrisonHero From 37c0972a5093eb4c71cf668b08bef0db07cc1b1f Mon Sep 17 00:00:00 2001 From: Xilmi Date: Fri, 19 Jul 2024 15:16:05 +0200 Subject: [PATCH 028/186] New and restored functionality Added a new div-function to resource-sets. It allows to calculate the amount of times one resource set, for example income, has to be accumulated in order to reach another resource-set, for example required resource. It will return INT_MAX if it's impossible. Restored the "<" operator-function and made it actually work like it's supposed to. --- lib/ResourceSet.h | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/lib/ResourceSet.h b/lib/ResourceSet.h index 4d9a0e695..c16fdf01b 100644 --- a/lib/ResourceSet.h +++ b/lib/ResourceSet.h @@ -148,6 +148,26 @@ public: return ret; } + int div(const ResourceSet& income) { + int ret = 0; // Initialize to 0 because we want the maximum number of accumulations + + for (size_t i = 0; i < container.size(); ++i) { + if (container.at(i) > 0) { // We only care about fulfilling positive needs + if (income[i] == 0) { + // If income is 0 and we need a positive amount, it's impossible to fulfill + return INT_MAX; + } + else { + // Calculate the number of times we need to accumulate income to fulfill the need + float divisionResult = static_cast(container.at(i)) / static_cast(income[i]); + int ceiledResult = static_cast(std::ceil(divisionResult)); + ret = std::max(ret, ceiledResult); + } + } + } + return ret; + } + ResourceSet & operator=(const TResource &rhs) { for(int & i : container) @@ -171,14 +191,14 @@ public: // WARNING: comparison operators are used for "can afford" relation: a <= b means that foreach i a[i] <= b[i] // that doesn't work the other way: a > b doesn't mean that a cannot be afforded with b, it's still b can afford a -// bool operator<(const ResourceSet &rhs) -// { -// for(int i = 0; i < size(); i++) -// if(at(i) >= rhs[i]) -// return false; -// -// return true; -// } + bool operator<(const ResourceSet &rhs) + { + for(int i = 0; i < size(); i++) + if (this->container.at(i) < rhs[i]) + return true; + + return false; + } template void serialize(Handler &h) { From 9c6d8762c5aa060f84935a0a180edcfe5f6befaf Mon Sep 17 00:00:00 2001 From: Xilmi Date: Fri, 19 Jul 2024 15:18:02 +0200 Subject: [PATCH 029/186] Lowered restrictions from hero-hiring. Removed two restrictions from hero-hiring, that prevented AI from hiring heros in certain scenarios. --- AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp index afe263076..c0d3f8f31 100644 --- a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp +++ b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp @@ -83,9 +83,6 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const } } - if(treasureSourcesCount < 5 && (town->garrisonHero || town->getUpperArmy()->getArmyStrength() < 10000)) - continue; - if(ai->cb->getHeroesInfo().size() < ai->cb->getTownsInfo().size() + 1 || (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh())) { From f094bf96022fa9e45428ce7824c155070f253f8d Mon Sep 17 00:00:00 2001 From: Xilmi Date: Fri, 19 Jul 2024 15:19:27 +0200 Subject: [PATCH 030/186] Added marketplace to buildable buildings Before marketplaces could only be built as part of a requirement for other buildings but not on their own when that other building already existed like it is the case in certain campaign-missions. --- AI/Nullkiller/Analyzers/BuildAnalyzer.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp index 641c75c2c..b8d2929b9 100644 --- a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp @@ -83,6 +83,7 @@ void BuildAnalyzer::updateOtherBuildings(TownDevelopmentInfo & developmentInfo) otherBuildings.push_back({ BuildingID::SPECIAL_2 }); otherBuildings.push_back({ BuildingID::SPECIAL_3 }); otherBuildings.push_back({ BuildingID::SPECIAL_4 }); + otherBuildings.push_back({ BuildingID::MARKETPLACE }); for(auto & buildingSet : otherBuildings) { From 2d715f4d7e774fb3c2892d56ee9b6c4c9c213327 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Fri, 19 Jul 2024 15:21:56 +0200 Subject: [PATCH 031/186] Trading logic Added trading-logic to Nullkiller-AI. The AI can now identify which resources it is lacking the most and buy them to fix softlocks in their build-order. It can also sell excess resources that it doesn't have a need for. --- AI/Nullkiller/Engine/Nullkiller.cpp | 123 ++++++++++++++++++++++++---- AI/Nullkiller/Engine/Nullkiller.h | 1 + 2 files changed, 110 insertions(+), 14 deletions(-) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 1fc02aaea..046bccf59 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -352,16 +352,18 @@ void Nullkiller::makeTurn() const int MAX_DEPTH = 10; const float FAST_TASK_MINIMAL_PRIORITY = 0.7f; - //float totalHeroStrength = 0; - //int totalTownLevel = 0; - //for (auto heroInfo : cb->getHeroesInfo()) - //{ - // totalHeroStrength += heroInfo->getTotalStrength(); - //} - //for (auto townInfo : cb->getTownsInfo()) - //{ - // totalTownLevel += townInfo->getTownLevel(); - //} + float totalHeroStrength = 0; + int totalTownLevel = 0; + for (auto heroInfo : cb->getHeroesInfo()) + { + totalHeroStrength += heroInfo->getTotalStrength(); + } + for (auto townInfo : cb->getTownsInfo()) + { + totalTownLevel += townInfo->getTownLevel(); + } + logAi->info("Resources: %s Strength: %f Townlevel: %d", cb->getResourceAmount().toString(), totalHeroStrength, totalTownLevel); + resetAiState(); @@ -371,7 +373,6 @@ void Nullkiller::makeTurn() { auto start = std::chrono::high_resolution_clock::now(); updateAiState(i); - //logAi->info("Gold: %d", cb->getResourceAmount(EGameResID::GOLD)); Goals::TTask bestTask = taskptr(Goals::Invalid()); @@ -386,7 +387,7 @@ void Nullkiller::makeTurn() if(bestTask->priority >= FAST_TASK_MINIMAL_PRIORITY) { - //logAi->info("Performing task %s with prio: %d", bestTask->toString(), bestTask->priority); + logAi->info("Performing task %s with prio: %d", bestTask->toString(), bestTask->priority); if(!executeTask(bestTask)) return; @@ -483,7 +484,7 @@ void Nullkiller::makeTurn() continue; } - //logAi->info("Performing task %s with prio: %d", bestTask->toString(), bestTask->priority); + logAi->info("Performing task %s with prio: %d", bestTask->toString(), bestTask->priority); if(!executeTask(bestTask)) { if(hasAnySuccess) @@ -491,10 +492,11 @@ void Nullkiller::makeTurn() else return; } - hasAnySuccess = true; } + hasAnySuccess |= handleTrading(); + if(!hasAnySuccess) { logAi->trace("Nothing was done this turn. Ending turn."); @@ -574,4 +576,97 @@ void Nullkiller::lockResources(const TResources & res) lockedResources += res; } +bool Nullkiller::handleTrading() +{ + bool haveTraded = false; + bool shouldTryToTrade = true; + int marketId = -1; + for (auto town : cb->getTownsInfo()) + { + if (town->hasBuiltSomeTradeBuilding()) + { + marketId = town->id; + } + } + if (marketId == -1) + return false; + if (const CGObjectInstance* obj = cb->getObj(ObjectInstanceID(marketId), false)) + { + if (const auto* m = dynamic_cast(obj)) + { + while (shouldTryToTrade) + { + shouldTryToTrade = false; + buildAnalyzer->update(); + TResources required = buildAnalyzer->getTotalResourcesRequired(); + TResources income = buildAnalyzer->getDailyIncome(); + TResources available = getFreeResources(); + + int mostWanted = -1; + int mostExpendable = -1; + float minRatio = std::numeric_limits::max(); + float maxRatio = std::numeric_limits::min(); + + for (int i = 0; i < required.size(); ++i) + { + if (required[i] == 0) + continue; + float ratio = static_cast(available[i]) / required[i]; + + if (ratio < minRatio) { + minRatio = ratio; + mostWanted = i; + } + } + + for (int i = 0; i < required.size(); ++i) + { + float ratio = available[i]; + if (required[i] > 0) + ratio = static_cast(available[i]) / required[i]; + + bool okToSell = false; + + if (i == 6) + { + if (income[i] > 0) + okToSell = true; + } + else + { + if (available[i] > required[i]) + okToSell = true; + } + + if (ratio > maxRatio && okToSell) { + maxRatio = ratio; + mostExpendable = i; + } + } + + //logAi->info("mostExpendable: %d mostWanted: %d", mostExpendable, mostWanted); + + if (mostExpendable == mostWanted || mostWanted == -1 || mostExpendable == -1) + return false; + + int acquiredResources = 0; + + int toGive; + int toGet; + m->getOffer(mostExpendable, mostWanted, toGive, toGet, EMarketMode::RESOURCE_RESOURCE); + //logAi->info("Offer is: I get %d of %s for %d of %s at %s", toGet, mostWanted, toGive, mostExpendable, obj->getObjectName()); + //TODO trade only as much as needed + if (toGive && toGive <= available[mostExpendable]) //don't try to sell 0 resources + { + cb->trade(m, EMarketMode::RESOURCE_RESOURCE, GameResID(mostExpendable), GameResID(mostWanted), toGive); + logAi->info("Traded %d of %s for %d of %s at %s", toGive, mostExpendable, toGet, mostWanted, obj->getObjectName()); + haveTraded = true; + shouldTryToTrade = true; + } + } + } + } + return haveTraded; +} + } diff --git a/AI/Nullkiller/Engine/Nullkiller.h b/AI/Nullkiller/Engine/Nullkiller.h index 4b25dd3ec..5166421fb 100644 --- a/AI/Nullkiller/Engine/Nullkiller.h +++ b/AI/Nullkiller/Engine/Nullkiller.h @@ -120,6 +120,7 @@ public: ScanDepth getScanDepth() const { return scanDepth; } bool isOpenMap() const { return openMap; } bool isObjectGraphAllowed() const { return useObjectGraph; } + bool handleTrading(); private: void resetAiState(); From e374f24016608b7dfab737353d234c5d6b96e743 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Fri, 19 Jul 2024 15:25:24 +0200 Subject: [PATCH 032/186] complete Building-costs as evaluation-context Added building-cost including all resoruces as evaluation-context for more sophisticated building-selection and also as a countermeasure to softlocking a build-order by having no ways to obtain certain resources. For example, if the AI would drop below 5 wood, while having no market-place and no wood-income it will avoid building any buildings that neither allow trading nor produce wood. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 90 +++++++++++++++------- AI/Nullkiller/Engine/PriorityEvaluator.h | 3 +- AI/Nullkiller/Goals/AbstractGoal.h | 1 + 3 files changed, 65 insertions(+), 29 deletions(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 446b60967..6d8a5a122 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -58,8 +58,8 @@ EvaluationContext::EvaluationContext(const Nullkiller* ai) armyGrowth(0), armyInvolvement(0), isDefend(false), - isBuild(false), - involvesSailing(false) + involvesSailing(false), + isTradeBuilding(false) { } @@ -1182,14 +1182,13 @@ public: evaluationContext.heroRole = HeroRole::MAIN; evaluationContext.movementCostByRole[evaluationContext.heroRole] += bi.prerequisitesCount; 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; + evaluationContext.buildingCost += bi.buildCostWithPrerequisites; + if (bi.id == BuildingID::MARKETPLACE || bi.dailyIncome[EGameResID::WOOD] > 0) + evaluationContext.isTradeBuilding = true; + + logAi->trace("Building costs for %s : %s MarketValue: %d",bi.toString(), evaluationContext.buildingCost.toString(), evaluationContext.buildingCost.marketValue()); if(bi.creatureID != CreatureID::NONE) { @@ -1278,6 +1277,7 @@ EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal for(auto subgoal : parts) { context.goldCost += subgoal->goldCost; + context.buildingCost += subgoal->buildingCost; for(auto builder : evaluationContextBuilders) { @@ -1337,6 +1337,26 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) { float score = 0; float maxWillingToLose = ai->cb->getTownsInfo().empty() ? 1 : 0.25; + + logAi->trace("BEFORE: 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", + priorityTier, + task->toString(), + evaluationContext.armyLossPersentage, + (int)evaluationContext.turn, + evaluationContext.movementCostByRole[HeroRole::MAIN], + evaluationContext.movementCostByRole[HeroRole::SCOUT], + goldRewardPerTurn, + evaluationContext.goldCost, + evaluationContext.armyReward, + evaluationContext.skillReward, + evaluationContext.danger, + evaluationContext.threat, + evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout", + evaluationContext.strategicalValue, + evaluationContext.closestWayRatio, + evaluationContext.enemyHeroDangerRatio, + fuzzyResult); + switch (priorityTier) { case 0: //Take towns @@ -1362,33 +1382,27 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) return 0; if (evaluationContext.involvesSailing && evaluationContext.movementCostByRole[HeroRole::MAIN] > 0) return 0; + if (evaluationContext.buildingCost.marketValue() > 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; - } + if (score <= 0) + return 0; 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; - } + 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) + if (evaluationContext.buildingCost.marketValue() > 0) return 0; if (evaluationContext.enemyHeroDangerRatio > 0.5 && !evaluationContext.isDefend) return 0; @@ -1413,7 +1427,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) } break; } - case 3: //Pre-filter to see if anything is worth to be done at all + case 3: //For buildings and buying army { score += evaluationContext.conquestValue * 1000; score += evaluationContext.strategicalValue * 1000; @@ -1421,10 +1435,30 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05; score += evaluationContext.armyReward; score += evaluationContext.armyGrowth; - if (evaluationContext.isBuild) + if (evaluationContext.buildingCost.marketValue() > 0) { + if (!evaluationContext.isTradeBuilding && ai->getFreeResources()[EGameResID::WOOD] - evaluationContext.buildingCost[EGameResID::WOOD] < 5 && ai->buildAnalyzer->getDailyIncome()[EGameResID::WOOD] < 1) + { + logAi->trace("Should make sure to build market-place instead of %s", task->toString()); + for (auto town : cb->getTownsInfo()) + { + if (!town->hasBuiltSomeTradeBuilding()) + return 0; + } + } score += 1000; - score /= evaluationContext.goldCost; + auto resourcesAvailable = evaluationContext.evaluator.ai->getFreeResources(); + auto income = ai->buildAnalyzer->getDailyIncome(); + if (resourcesAvailable < evaluationContext.buildingCost) + { + TResources needed = evaluationContext.buildingCost - resourcesAvailable; + needed.positive(); + int turnsTo = needed.div(income); + if (turnsTo == INT_MAX) + return 0; + else + score /= turnsTo; + } } break; } diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index d4241efdb..2e15a4926 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -74,8 +74,9 @@ struct DLL_EXPORT EvaluationContext float threat; float armyInvolvement; bool isDefend; - bool isBuild; + TResources buildingCost; bool involvesSailing; + bool isTradeBuilding; EvaluationContext(const Nullkiller * ai); diff --git a/AI/Nullkiller/Goals/AbstractGoal.h b/AI/Nullkiller/Goals/AbstractGoal.h index b191f96a5..27adf052e 100644 --- a/AI/Nullkiller/Goals/AbstractGoal.h +++ b/AI/Nullkiller/Goals/AbstractGoal.h @@ -104,6 +104,7 @@ namespace Goals bool isAbstract; SETTER(bool, isAbstract) int value; SETTER(int, value) ui64 goldCost; SETTER(ui64, goldCost) + TResources buildingCost; SETTER(TResources, buildingCost) int resID; SETTER(int, resID) int objid; SETTER(int, objid) int aid; SETTER(int, aid) From eb26b16823dce085aeceb94e1a13e555e3045d78 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Fri, 19 Jul 2024 15:26:14 +0200 Subject: [PATCH 033/186] Trace level Increased AI-trace-level because I need it for debugging. --- AI/Nullkiller/Pathfinding/AINodeStorage.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.h b/AI/Nullkiller/Pathfinding/AINodeStorage.h index 12cf787d0..979f19a50 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.h +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.h @@ -12,7 +12,7 @@ #define NKAI_PATHFINDER_TRACE_LEVEL 0 constexpr int NKAI_GRAPH_TRACE_LEVEL = 0; -#define NKAI_TRACE_LEVEL 0 +#define NKAI_TRACE_LEVEL 2 #include "../../../lib/pathfinder/CGPathNode.h" #include "../../../lib/pathfinder/INodeStorage.h" From f0f40660c4f4a0aefea93c40f2a41e022cc5cd5f Mon Sep 17 00:00:00 2001 From: Xilmi Date: Fri, 19 Jul 2024 15:59:57 +0200 Subject: [PATCH 034/186] reverse getTotalArmyLoss()-change Due to a recent fix in army-loss calculations this method should now work correctly the way it was. --- AI/Nullkiller/Pathfinding/AINodeStorage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index 39bc8f9f1..f7a0e6c30 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -1557,7 +1557,7 @@ bool AIPath::containsHero(const CGHeroInstance * hero) const uint64_t AIPath::getTotalArmyLoss() const { - return armyLoss > targetObjectArmyLoss ? armyLoss : targetObjectArmyLoss; + return armyLoss + targetObjectArmyLoss; } std::string AIPath::toString() const From 809163b705550a0b69dcd180af380876cae5414a Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sat, 20 Jul 2024 16:10:29 +0200 Subject: [PATCH 035/186] Update SDLImage.cpp Fixed crash --- client/renderSDL/SDLImage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/renderSDL/SDLImage.cpp b/client/renderSDL/SDLImage.cpp index c7e47cd97..6a9a61d6f 100644 --- a/client/renderSDL/SDLImage.cpp +++ b/client/renderSDL/SDLImage.cpp @@ -196,7 +196,7 @@ Point SDLImageConst::dimensions() const std::shared_ptr SDLImageConst::createImageReference(EImageBlitMode mode) { - if (surf->format->palette) + if (surf && surf->format->palette) return std::make_shared(shared_from_this(), mode); else return std::make_shared(shared_from_this(), mode); From ed82e2bf4ae24279e1f1ac87813b8f82cd2326af Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sat, 20 Jul 2024 21:02:39 +0200 Subject: [PATCH 036/186] Warnings Removed unused variable. --- AI/Nullkiller/Engine/Nullkiller.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 046bccf59..9ef4816b4 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -649,8 +649,6 @@ bool Nullkiller::handleTrading() if (mostExpendable == mostWanted || mostWanted == -1 || mostExpendable == -1) return false; - int acquiredResources = 0; - int toGive; int toGet; m->getOffer(mostExpendable, mostWanted, toGive, toGet, EMarketMode::RESOURCE_RESOURCE); From 945de7c3698115900d9ce6fd5a8430893065ed5f Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sat, 20 Jul 2024 21:12:43 +0200 Subject: [PATCH 037/186] More warnings Removed unused code --- .../Behaviors/RecruitHeroBehavior.cpp | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp index c0d3f8f31..837e7e91d 100644 --- a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp +++ b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp @@ -55,6 +55,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const { auto availableHeroes = ai->cb->getAvailableHeroes(town); + //TODO: Prioritize non-main-heros too by cost of their units and whether their units fit to the current town for(auto hero : availableHeroes) { auto score = ai->heroManager->evaluateHero(hero); @@ -65,24 +66,6 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const } } - int treasureSourcesCount = 0; - - for(auto obj : ai->objectClusterizer->getNearbyObjects()) - { - if((obj->ID == Obj::RESOURCE) - || obj->ID == Obj::TREASURE_CHEST - || obj->ID == Obj::CAMPFIRE - || isWeeklyRevisitable(ai, obj) - || obj->ID ==Obj::ARTIFACT) - { - auto tile = obj->visitablePos(); - auto closestTown = ai->dangerHitMap->getClosestTown(tile); - - if(town == closestTown) - treasureSourcesCount++; - } - } - if(ai->cb->getHeroesInfo().size() < ai->cb->getTownsInfo().size() + 1 || (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh())) { From 34e4ab45ee28ecc86b4db584b22e36d99bee3505 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Wed, 24 Jul 2024 15:21:17 +0200 Subject: [PATCH 038/186] Fixed what merge-conflict-handling had broken. Restored exploration-without relying on memory but with included whirlpool --- .../Behaviors/ExplorationBehavior.cpp | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp b/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp index 0aa077688..a0bf96e63 100644 --- a/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp +++ b/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp @@ -33,33 +33,30 @@ Goals::TGoalVec ExplorationBehavior::decompose(const Nullkiller * ai) const { Goals::TGoalVec tasks; - for(auto obj : ai->memory->visitableObjs) + for (auto obj : ai->memory->visitableObjs) { switch (obj->ID.num) { case Obj::REDWOOD_OBSERVATORY: case Obj::PILLAR_OF_FIRE: - { - auto rObj = dynamic_cast(obj); - if(!rObj->wasScouted(ai->playerID)) - tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 200)).addNext(CaptureObject(obj)))); - break; - } + { + auto rObj = dynamic_cast(obj); + if (!rObj->wasScouted(ai->playerID)) + tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 200)).addNext(CaptureObject(obj)))); + break; + } case Obj::MONOLITH_ONE_WAY_ENTRANCE: case Obj::MONOLITH_TWO_WAY: case Obj::SUBTERRANEAN_GATE: case Obj::WHIRLPOOL: + { + auto tObj = dynamic_cast(obj); + for (auto exit : cb->getTeleportChannelExits(tObj->channel)) { - auto tObj = dynamic_cast(obj); - if(TeleportChannel::IMPASSABLE == ai->memory->knownTeleportChannels[tObj->channel]->passability) - break; - for(auto exit : ai->memory->knownTeleportChannels[tObj->channel]->exits) + if (exit != tObj->id) { - if (exit != tObj->id) - { - if (!cb->isVisible(cb->getObjInstance(exit))) - tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj)))); - } + if (!cb->isVisible(cb->getObjInstance(exit))) + tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj)))); } } } From 69b64a324163bfd6f1a5ef9e51c2b68a1df2be45 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Wed, 24 Jul 2024 15:40:20 +0200 Subject: [PATCH 039/186] Update ExplorationBehavior.cpp Added missing bracked and changed indenting to make it less confusing. --- .../Behaviors/ExplorationBehavior.cpp | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp b/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp index a0bf96e63..b9c6e568d 100644 --- a/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp +++ b/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp @@ -37,26 +37,27 @@ Goals::TGoalVec ExplorationBehavior::decompose(const Nullkiller * ai) const { switch (obj->ID.num) { - case Obj::REDWOOD_OBSERVATORY: - case Obj::PILLAR_OF_FIRE: - { - auto rObj = dynamic_cast(obj); - if (!rObj->wasScouted(ai->playerID)) - tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 200)).addNext(CaptureObject(obj)))); - break; - } - case Obj::MONOLITH_ONE_WAY_ENTRANCE: - case Obj::MONOLITH_TWO_WAY: - case Obj::SUBTERRANEAN_GATE: - case Obj::WHIRLPOOL: - { - auto tObj = dynamic_cast(obj); - for (auto exit : cb->getTeleportChannelExits(tObj->channel)) + case Obj::REDWOOD_OBSERVATORY: + case Obj::PILLAR_OF_FIRE: { - if (exit != tObj->id) + auto rObj = dynamic_cast(obj); + if (!rObj->wasScouted(ai->playerID)) + tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 200)).addNext(CaptureObject(obj)))); + break; + } + case Obj::MONOLITH_ONE_WAY_ENTRANCE: + case Obj::MONOLITH_TWO_WAY: + case Obj::SUBTERRANEAN_GATE: + case Obj::WHIRLPOOL: + { + auto tObj = dynamic_cast(obj); + for (auto exit : cb->getTeleportChannelExits(tObj->channel)) { - if (!cb->isVisible(cb->getObjInstance(exit))) - tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj)))); + if (exit != tObj->id) + { + if (!cb->isVisible(cb->getObjInstance(exit))) + tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj)))); + } } } } From aade79720f930972c1e6f47a251908d4d93be15b Mon Sep 17 00:00:00 2001 From: Xilmi Date: Wed, 24 Jul 2024 21:14:27 +0200 Subject: [PATCH 040/186] Update BuildAnalyzer.cpp Allow queuing citadels and castles on other days than satur- and sunday. --- AI/Nullkiller/Analyzers/BuildAnalyzer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp index 248e75cf2..6e7cee145 100644 --- a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp @@ -74,11 +74,11 @@ void BuildAnalyzer::updateOtherBuildings(TownDevelopmentInfo & developmentInfo) if(developmentInfo.existingDwellings.size() >= 2 && ai->cb->getDate(Date::DAY_OF_WEEK) > boost::date_time::Friday) { - otherBuildings.push_back({BuildingID::CITADEL, BuildingID::CASTLE}); otherBuildings.push_back({BuildingID::HORDE_1}); otherBuildings.push_back({BuildingID::HORDE_2}); } + otherBuildings.push_back({ BuildingID::CITADEL, BuildingID::CASTLE }); otherBuildings.push_back({ BuildingID::RESOURCE_SILO }); otherBuildings.push_back({ BuildingID::SPECIAL_1 }); otherBuildings.push_back({ BuildingID::SPECIAL_2 }); From bbb5157f74315b6eb7e63fdc5f23c8443c41faa8 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Wed, 24 Jul 2024 21:17:04 +0200 Subject: [PATCH 041/186] Update RecruitHeroBehavior.cpp Reworked recruit-behavior to be a bit more conservative and avoid recruiting-sprees. Stuff like buying several heros in a row because the next one is always slightly better than the last but using up the whole starting-bank for that. --- .../Behaviors/RecruitHeroBehavior.cpp | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp index 837e7e91d..961866dcc 100644 --- a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp +++ b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp @@ -13,6 +13,7 @@ #include "../AIUtility.h" #include "../Goals/RecruitHero.h" #include "../Goals/ExecuteHeroChain.h" +#include "../lib/CHeroHandler.h" namespace NKAI { @@ -49,28 +50,45 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const if (ourHeroes.empty()) minScoreToHireMain = 0; + const CGHeroInstance* bestHeroToHire = nullptr; + const CGTownInstance* bestTownToHireFrom = nullptr; + float bestScore = 0; + bool haveCapitol = false; + for(auto town : towns) { if(ai->heroManager->canRecruitHero(town)) { auto availableHeroes = ai->cb->getAvailableHeroes(town); - //TODO: Prioritize non-main-heros too by cost of their units and whether their units fit to the current town for(auto hero : availableHeroes) { auto score = ai->heroManager->evaluateHero(hero); - if(score > minScoreToHireMain || hero->getArmyCost() > GameConstants::HERO_GOLD_COST) + if(score > minScoreToHireMain) { - tasks.push_back(Goals::sptr(Goals::RecruitHero(town, hero).setpriority(200))); - break; + score *= score / minScoreToHireMain; + } + score *= hero->getArmyCost(); + if (hero->type->heroClass->faction == town->getFaction()) + score *= 1.5; + score *= town->getTownLevel(); + if (score > bestScore) + { + bestScore = score; + bestHeroToHire = hero; + bestTownToHireFrom = town; } } - - if(ai->cb->getHeroesInfo().size() < ai->cb->getTownsInfo().size() + 1 - || (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh())) - { - tasks.push_back(Goals::sptr(Goals::RecruitHero(town).setpriority(3))); - } + } + if (town->hasCapitol()) + haveCapitol = true; + } + if (bestHeroToHire && bestTownToHireFrom) + { + if (ai->cb->getHeroesInfo().size() < ai->cb->getTownsInfo().size() + 1 + || (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh() && haveCapitol)) + { + tasks.push_back(Goals::sptr(Goals::RecruitHero(bestTownToHireFrom, bestHeroToHire).setpriority((float)3 / ourHeroes.size()))); } } From 19b969406c2e9455c73020532dd9e63092f793c9 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Wed, 24 Jul 2024 21:22:14 +0200 Subject: [PATCH 042/186] Update PriorityEvaluator.cpp Delivering troops to mains is now considered a priority 0 task that should immediately be fulfilled. Defending nearby towns against nearby enemies is now also considered a priority 0 task. Priority 0 tasks are now exclusively scored by distance and armyloss has only a cut-off-point instead of lowering the score. Building-cost now has more impact on their score. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 30 ++++++++++++---------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 6d8a5a122..5c91eca81 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -895,6 +895,7 @@ public: uint64_t armyStrength = heroExchange.getReinforcementArmyStrength(evaluationContext.evaluator.ai); evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength()); + evaluationContext.conquestValue += 2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength(); evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero); } }; @@ -1338,7 +1339,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) float score = 0; float maxWillingToLose = ai->cb->getTownsInfo().empty() ? 1 : 0.25; - logAi->trace("BEFORE: 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", + logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, fuzzy: %f", priorityTier, task->toString(), evaluationContext.armyLossPersentage, @@ -1348,11 +1349,13 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) goldRewardPerTurn, evaluationContext.goldCost, evaluationContext.armyReward, + evaluationContext.armyGrowth, evaluationContext.skillReward, evaluationContext.danger, evaluationContext.threat, evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout", evaluationContext.strategicalValue, + evaluationContext.conquestValue, evaluationContext.closestWayRatio, evaluationContext.enemyHeroDangerRatio, fuzzyResult); @@ -1361,15 +1364,16 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) { case 0: //Take towns { - score += evaluationContext.conquestValue * 1000; + //score += evaluationContext.conquestValue * 1000; + if(evaluationContext.conquestValue > 0 || (evaluationContext.isDefend && evaluationContext.turn <= 1 && evaluationContext.threat > evaluationContext.armyInvolvement)) + score = 1000; if (score == 0 || (evaluationContext.enemyHeroDangerRatio > 0.5 && evaluationContext.turn > 0 && !ai->cb->getTownsInfo().empty())) return 0; + if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) + 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 @@ -1384,6 +1388,8 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) return 0; if (evaluationContext.buildingCost.marketValue() > 0) return 0; + if (evaluationContext.closestWayRatio < 1) + return 0; score += evaluationContext.strategicalValue * 1000; score += evaluationContext.goldReward; score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05; @@ -1404,8 +1410,6 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) { if (evaluationContext.buildingCost.marketValue() > 0) 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; @@ -1415,12 +1419,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) 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.threat > 0) + score /= evaluationContext.threat; if (evaluationContext.movementCost > 0) score /= evaluationContext.movementCost; score *= (maxWillingToLose - evaluationContext.armyLossPersentage); @@ -1449,6 +1450,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) score += 1000; auto resourcesAvailable = evaluationContext.evaluator.ai->getFreeResources(); auto income = ai->buildAnalyzer->getDailyIncome(); + score /= evaluationContext.buildingCost.marketValue(); if (resourcesAvailable < evaluationContext.buildingCost) { TResources needed = evaluationContext.buildingCost - resourcesAvailable; @@ -1467,7 +1469,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) } #if NKAI_TRACE_LEVEL >= 2 - 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", + logAi->trace("priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, fuzzy: %f, result %f", priorityTier, task->toString(), evaluationContext.armyLossPersentage, @@ -1477,11 +1479,13 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) goldRewardPerTurn, evaluationContext.goldCost, evaluationContext.armyReward, + evaluationContext.armyGrowth, evaluationContext.skillReward, evaluationContext.danger, evaluationContext.threat, evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout", evaluationContext.strategicalValue, + evaluationContext.conquestValue, evaluationContext.closestWayRatio, evaluationContext.enemyHeroDangerRatio, fuzzyResult, From e2e3cd281c5f97601eceef13b5cfbbf5bbe93bba Mon Sep 17 00:00:00 2001 From: Xilmi Date: Wed, 24 Jul 2024 21:23:20 +0200 Subject: [PATCH 043/186] Update RecruitHero.h Multiple orders for the same hero by different towns are now handled properly instead of trying to buy the same hero several times. --- AI/Nullkiller/Goals/RecruitHero.h | 1 + 1 file changed, 1 insertion(+) diff --git a/AI/Nullkiller/Goals/RecruitHero.h b/AI/Nullkiller/Goals/RecruitHero.h index 101588f19..2fb932ed5 100644 --- a/AI/Nullkiller/Goals/RecruitHero.h +++ b/AI/Nullkiller/Goals/RecruitHero.h @@ -44,6 +44,7 @@ namespace Goals } std::string toString() const override; + const CGHeroInstance* getHero() { return heroToBuy; } void accept(AIGateway * ai) override; }; } From 8152b003fe89e92b0be6f74590d2dd7984a388dc Mon Sep 17 00:00:00 2001 From: Xilmi Date: Wed, 24 Jul 2024 21:26:53 +0200 Subject: [PATCH 044/186] Update Nullkiller.cpp Fixed a bug that caused tasks that are generated with an initial priority not being executed. Fixed an error-message wrongfully claiming a hero was locked by STARTUP, when infact the hero was locked by something else (usually a hero-chain). Recruiting-heros is now handled alongside buying army and buildings. --- AI/Nullkiller/Engine/Nullkiller.cpp | 30 +++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 9ef4816b4..37a44d221 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -129,7 +129,7 @@ void TaskPlan::merge(TSubgoal task) { for(auto objid : item.affectedObjects) { - if(task == item.task || task->asTask()->isObjectAffected(objid)) + if(task == item.task || task->asTask()->isObjectAffected(objid) || task->asTask()->getHero() == item.task->asTask()->getHero()) { if(item.task->asTask()->priority >= task->asTask()->priority) return; @@ -180,8 +180,7 @@ Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks, int priorityTier) const for(size_t i = r.begin(); i != r.end(); i++) { auto task = tasks[i]; - - if(task->asTask()->priority <= 0) + if (task->asTask()->priority <= 0 || priorityTier != 3) task->asTask()->priority = evaluator->evaluate(task, priorityTier); } }); @@ -329,7 +328,7 @@ bool Nullkiller::arePathHeroesLocked(const AIPath & path) const if(lockReason != HeroLockedReason::NOT_LOCKED) { #if NKAI_TRACE_LEVEL >= 1 - logAi->trace("Hero %s is locked by STARTUP. Discarding %s", path.targetHero->getObjectName(), path.toString()); + logAi->trace("Hero %s is locked by %d. Discarding %s", path.targetHero->getObjectName(), (int)lockReason, path.toString()); #endif return true; } @@ -362,8 +361,6 @@ void Nullkiller::makeTurn() { totalTownLevel += townInfo->getTownLevel(); } - logAi->info("Resources: %s Strength: %f Townlevel: %d", cb->getResourceAmount().toString(), totalHeroStrength, totalTownLevel); - resetAiState(); @@ -371,6 +368,7 @@ void Nullkiller::makeTurn() for(int i = 1; i <= settings->getMaxPass() && cb->getPlayerStatus(playerID) == EPlayerStatus::INGAME; i++) { + logAi->info("Strength: %f Townlevel: %d Resources: %s", totalHeroStrength, totalTownLevel, cb->getResourceAmount().toString()); auto start = std::chrono::high_resolution_clock::now(); updateAiState(i); @@ -380,12 +378,13 @@ void Nullkiller::makeTurn() { bestTasks.clear(); + decompose(bestTasks, sptr(RecruitHeroBehavior()), 1); decompose(bestTasks, sptr(BuyArmyBehavior()), 1); decompose(bestTasks, sptr(BuildingBehavior()), 1); bestTask = choseBestTask(bestTasks); - if(bestTask->priority >= FAST_TASK_MINIMAL_PRIORITY) + if(bestTask->priority > 0) { logAi->info("Performing task %s with prio: %d", bestTask->toString(), bestTask->priority); if(!executeTask(bestTask)) @@ -399,7 +398,6 @@ void Nullkiller::makeTurn() } } - decompose(bestTasks, sptr(RecruitHeroBehavior()), 1); decompose(bestTasks, sptr(CaptureObjectsBehavior()), 1); decompose(bestTasks, sptr(ClusterBehavior()), MAX_DEPTH); decompose(bestTasks, sptr(DefenceBehavior()), MAX_DEPTH); @@ -409,11 +407,15 @@ void Nullkiller::makeTurn() if(!isOpenMap()) decompose(bestTasks, sptr(ExplorationBehavior()), MAX_DEPTH); - auto selectedTasks = buildPlan(bestTasks, 0); - if (selectedTasks.empty() && !settings->isUseFuzzy()) - selectedTasks = buildPlan(bestTasks, 1); - if (selectedTasks.empty() && !settings->isUseFuzzy()) - selectedTasks = buildPlan(bestTasks, 2); + TTaskVec selectedTasks; + int prioOfTask = 0; + for (int prio = 0; prio <= 2; ++prio) + { + prioOfTask = prio; + selectedTasks = buildPlan(bestTasks, prio); + if (!selectedTasks.empty() || settings->isUseFuzzy()) + break; + } std::sort(selectedTasks.begin(), selectedTasks.end(), [](const TTask& a, const TTask& b) { @@ -484,7 +486,7 @@ void Nullkiller::makeTurn() continue; } - logAi->info("Performing task %s with prio: %d", bestTask->toString(), bestTask->priority); + logAi->info("Performing prio %d task %s with prio: %d", prioOfTask, bestTask->toString(), bestTask->priority); if(!executeTask(bestTask)) { if(hasAnySuccess) From 1e2021fb6d67b9e6eff45563288321eee49bcc19 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Wed, 24 Jul 2024 21:41:49 +0200 Subject: [PATCH 045/186] Update RecruitHero.h Fix warning. --- AI/Nullkiller/Goals/RecruitHero.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/Nullkiller/Goals/RecruitHero.h b/AI/Nullkiller/Goals/RecruitHero.h index 2fb932ed5..c49644948 100644 --- a/AI/Nullkiller/Goals/RecruitHero.h +++ b/AI/Nullkiller/Goals/RecruitHero.h @@ -44,7 +44,7 @@ namespace Goals } std::string toString() const override; - const CGHeroInstance* getHero() { return heroToBuy; } + const CGHeroInstance* getHero() const override { return heroToBuy; } void accept(AIGateway * ai) override; }; } From 909c308614b77d675b398b2425c744cacc2d6587 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Wed, 24 Jul 2024 21:46:18 +0200 Subject: [PATCH 046/186] Update Nullkiller.cpp Removed unused variable. --- AI/Nullkiller/Engine/Nullkiller.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 37a44d221..3276f5dbd 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -349,7 +349,6 @@ void Nullkiller::makeTurn() boost::lock_guard sharedStorageLock(AISharedStorage::locker); const int MAX_DEPTH = 10; - const float FAST_TASK_MINIMAL_PRIORITY = 0.7f; float totalHeroStrength = 0; int totalTownLevel = 0; From 38da53135bd7d944f44224bf90bad04b9e07cf02 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 25 Jul 2024 21:47:56 +0200 Subject: [PATCH 047/186] Update DefenceBehavior.cpp A town will no longer communitcate that it doesn't need defenses, when it currently has a garrisioned hero. Because otherwise the garrisoned hero would just leave and let the town undefended. --- AI/Nullkiller/Behaviors/DefenceBehavior.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp index e23efcb1c..7487791b2 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp @@ -158,11 +158,6 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta threats.push_back(threatNode.fastestDanger); // no guarantee that fastest danger will be there - if(town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks, ai)) - { - return; - } - if(!threatNode.fastestDanger.hero) { logAi->trace("No threat found for town %s", town->getNameTranslated()); From 183ce82b99a4c7c0b5f71e22376424e7193015f7 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 25 Jul 2024 21:48:44 +0200 Subject: [PATCH 048/186] Update Nullkiller.cpp Readded prio-3 tasks for heroes when no prio 0-2 tasks were found. It's anything that doesn't lose more than the loss-threshold. --- AI/Nullkiller/Engine/Nullkiller.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 3276f5dbd..5dee24025 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -408,7 +408,7 @@ void Nullkiller::makeTurn() TTaskVec selectedTasks; int prioOfTask = 0; - for (int prio = 0; prio <= 2; ++prio) + for (int prio = 0; prio <= 3; ++prio) { prioOfTask = prio; selectedTasks = buildPlan(bestTasks, prio); From 07108ce03da683597d86bd0036d83dbf654a9b2b Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 25 Jul 2024 21:49:17 +0200 Subject: [PATCH 049/186] Update PriorityEvaluator.cpp Allow defending in priority 2. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 5c91eca81..8389282c4 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -1408,6 +1408,8 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) } case 2: //Collect guarded stuff { + if (evaluationContext.enemyHeroDangerRatio > 0.5 && !evaluationContext.isDefend) + return 0; if (evaluationContext.buildingCost.marketValue() > 0) return 0; score += evaluationContext.strategicalValue * 1000; @@ -1430,6 +1432,8 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) } case 3: //For buildings and buying army { + if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) + return 0; score += evaluationContext.conquestValue * 1000; score += evaluationContext.strategicalValue * 1000; score += evaluationContext.goldReward; From b0e4551dbfdc3ba50e8dbd943e0cc7f9a3710bc9 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Wed, 7 Aug 2024 01:35:17 +0200 Subject: [PATCH 050/186] Update BuyArmyBehavior.cpp No longer saving money for city-halls when city-halls cannot be built. --- AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp b/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp index e988c4519..610b16bcd 100644 --- a/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp @@ -48,7 +48,7 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const } } - if (ai->buildAnalyzer->isGoldPressureHigh() && !town->hasBuilt(BuildingID::CITY_HALL)) + if (ai->buildAnalyzer->isGoldPressureHigh() && !town->hasBuilt(BuildingID::CITY_HALL) && cb->canBuildStructure(town, BuildingID::CITY_HALL) != EBuildingState::FORBIDDEN) { return tasks; } From ec5da0e6b3393abd1adac8b0731be82b13bd6d7a Mon Sep 17 00:00:00 2001 From: Xilmi Date: Wed, 7 Aug 2024 01:36:27 +0200 Subject: [PATCH 051/186] Update PriorityEvaluator.cpp Further working towards heroes avoiding danger. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 8389282c4..ab9039bad 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -1466,6 +1466,11 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) score /= turnsTo; } } + else + { + if (evaluationContext.enemyHeroDangerRatio > 0.5 && !evaluationContext.isDefend) + return 0; + } break; } } From ea4e412f9acb4d910587953d1fa42d69dc51ccc8 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Wed, 7 Aug 2024 01:37:15 +0200 Subject: [PATCH 052/186] Update Nullkiller.cpp Only actual heroes and not nullptrs will exculde each other in comparison. --- AI/Nullkiller/Engine/Nullkiller.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 5dee24025..d843258a1 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -129,7 +129,7 @@ void TaskPlan::merge(TSubgoal task) { for(auto objid : item.affectedObjects) { - if(task == item.task || task->asTask()->isObjectAffected(objid) || task->asTask()->getHero() == item.task->asTask()->getHero()) + if(task == item.task || task->asTask()->isObjectAffected(objid) || (task->asTask()->getHero() != nullptr && task->asTask()->getHero() == item.task->asTask()->getHero())) { if(item.task->asTask()->priority >= task->asTask()->priority) return; From 730e574bef9fad9e4e3baba06ce42015c70c367b Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 11 Aug 2024 12:44:07 +0200 Subject: [PATCH 053/186] Fixed AI ignoring garrisioned heroes when it comes to danger-analysis. AI now considers garrisoned heros a potential threat for their heroes too. --- AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp index 17f69c2af..27bfe5dd0 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp @@ -89,6 +89,14 @@ void DangerHitMapAnalyzer::updateHitMap() heroes[hero->tempOwner][hero] = HeroRole::MAIN; } + if (obj->ID == Obj::TOWN) + { + auto town = dynamic_cast(obj); + auto hero = town->garrisonHero; + + if(hero) + heroes[hero->tempOwner][hero] = HeroRole::MAIN; + } } auto ourTowns = cb->getTownsInfo(); From 5907aae05771dbfbd2d88a90cd16df9dd7b3000d Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 11 Aug 2024 17:59:10 +0200 Subject: [PATCH 054/186] Fixed AI not taking hero-strength into account when hero is garrisoned. When evaluating their fighting-chance against towns with a garrisoned hero the AI didn't consider the attribute-boosts of the defending hero. Now it does. --- AI/Nullkiller/Engine/FuzzyHelper.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/AI/Nullkiller/Engine/FuzzyHelper.cpp b/AI/Nullkiller/Engine/FuzzyHelper.cpp index 10fab5cc9..912b29e8a 100644 --- a/AI/Nullkiller/Engine/FuzzyHelper.cpp +++ b/AI/Nullkiller/Engine/FuzzyHelper.cpp @@ -73,6 +73,14 @@ ui64 FuzzyHelper::evaluateDanger(const int3 & tile, const CGHeroInstance * visit } objectDanger *= ai->heroManager->getFightingStrengthCached(hero); } + if (objWithID(dangerousObject)) + { + auto town = dynamic_cast(dangerousObject); + auto hero = town->garrisonHero; + + if (hero) + objectDanger *= ai->heroManager->getFightingStrengthCached(hero); + } if(objectDanger) { From 3218363bc06aacd5fac07f6eb91d47a132caac92 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 11 Aug 2024 18:13:52 +0200 Subject: [PATCH 055/186] Update googletest No idea what this is. But I guess I'll commit it. --- test/googletest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/googletest b/test/googletest index b514bdc89..b796f7d44 160000 --- a/test/googletest +++ b/test/googletest @@ -1 +1 @@ -Subproject commit b514bdc898e2951020cbdca1304b75f5950d1f59 +Subproject commit b796f7d44681514f58a683a3a71ff17c94edb0c1 From 958aeb3a60495798309a17d3884ba7c50fad6729 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 11 Aug 2024 18:21:24 +0200 Subject: [PATCH 056/186] AI improvements Deliberately defending towns in danger will now only be performed when the town has at least a citatel. The AI will now count the buildings per town-type and add a score-bonus to dwellings of towns it already has a lot of buildings of. This shall help with getting a stronger army with less morale-issues. Scoring for mage-guild now increased the bigger the existing armies are. AI is less afraid of enemy-heros than previously. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 25 ++++++++++++++++------ AI/Nullkiller/Engine/PriorityEvaluator.h | 1 + 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index ab9039bad..b58d2878b 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -57,6 +57,7 @@ EvaluationContext::EvaluationContext(const Nullkiller* ai) threat(0), armyGrowth(0), armyInvolvement(0), + defenseValue(0), isDefend(false), involvesSailing(false), isTradeBuilding(false) @@ -999,6 +1000,7 @@ public: else evaluationContext.addNonCriticalStrategicalValue(1.7f * multiplier * strategicalValue); + evaluationContext.defenseValue = town->fortLevel(); evaluationContext.isDefend = true; vstd::amax(evaluationContext.danger, defendTown.getTreat().danger); @@ -1207,6 +1209,13 @@ public: evaluationContext.addNonCriticalStrategicalValue(potentialUpgradeValue / 10000.0f / (float)bi.prerequisitesCount); evaluationContext.armyReward += potentialUpgradeValue / (float)bi.prerequisitesCount; } + int sameTownBonus = 0; + for (auto town : cb->getTownsInfo()) + { + if (buildThis.town->getFaction() == town->getFaction()) + sameTownBonus+=town->getTownLevel(); + } + evaluationContext.armyReward *= sameTownBonus; } else if(bi.id == BuildingID::CITADEL || bi.id == BuildingID::CASTLE) { @@ -1216,6 +1225,10 @@ public: else if(bi.id >= BuildingID::MAGES_GUILD_1 && bi.id <= BuildingID::MAGES_GUILD_5) { evaluationContext.skillReward += 2 * (bi.id - BuildingID::MAGES_GUILD_1); + for (auto hero : cb->getHeroesInfo()) + { + evaluationContext.armyInvolvement += hero->getArmyCost(); + } } if(evaluationContext.goldReward) @@ -1365,9 +1378,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) case 0: //Take towns { //score += evaluationContext.conquestValue * 1000; - if(evaluationContext.conquestValue > 0 || (evaluationContext.isDefend && evaluationContext.turn <= 1 && evaluationContext.threat > evaluationContext.armyInvolvement)) + if(evaluationContext.conquestValue > 0 || (evaluationContext.defenseValue >= CGTownInstance::EFortLevel::CITADEL && evaluationContext.turn <= 1 && evaluationContext.threat > evaluationContext.armyInvolvement)) score = 1000; - if (score == 0 || (evaluationContext.enemyHeroDangerRatio > 0.5 && evaluationContext.turn > 0 && !ai->cb->getTownsInfo().empty())) + if (score == 0 || (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.turn > 0 && !ai->cb->getTownsInfo().empty())) return 0; if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) return 0; @@ -1378,7 +1391,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) } case 1: //Collect unguarded stuff { - if (evaluationContext.enemyHeroDangerRatio > 0.5 && !evaluationContext.isDefend) + if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend) return 0; if (evaluationContext.isDefend && evaluationContext.enemyHeroDangerRatio == 0) return 0; @@ -1408,7 +1421,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) } case 2: //Collect guarded stuff { - if (evaluationContext.enemyHeroDangerRatio > 0.5 && !evaluationContext.isDefend) + if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend) return 0; if (evaluationContext.buildingCost.marketValue() > 0) return 0; @@ -1445,7 +1458,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) if (!evaluationContext.isTradeBuilding && ai->getFreeResources()[EGameResID::WOOD] - evaluationContext.buildingCost[EGameResID::WOOD] < 5 && ai->buildAnalyzer->getDailyIncome()[EGameResID::WOOD] < 1) { logAi->trace("Should make sure to build market-place instead of %s", task->toString()); - for (auto town : cb->getTownsInfo()) + for (auto town : ai->cb->getTownsInfo()) { if (!town->hasBuiltSomeTradeBuilding()) return 0; @@ -1468,7 +1481,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) } else { - if (evaluationContext.enemyHeroDangerRatio > 0.5 && !evaluationContext.isDefend) + if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend && evaluationContext.conquestValue == 0) return 0; } break; diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index 2e15a4926..9b935f407 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -73,6 +73,7 @@ struct DLL_EXPORT EvaluationContext float enemyHeroDangerRatio; float threat; float armyInvolvement; + int defenseValue; bool isDefend; TResources buildingCost; bool involvesSailing; From ed059393ec38b486bea9262b2f394b1a20909aa9 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 11 Aug 2024 18:23:02 +0200 Subject: [PATCH 057/186] No more prio 3 tasks The AI no longer performs prio 3 tasks, when no prio 0-2 tasks were found. --- AI/Nullkiller/Engine/Nullkiller.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index d843258a1..4dbbe36c8 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -408,7 +408,7 @@ void Nullkiller::makeTurn() TTaskVec selectedTasks; int prioOfTask = 0; - for (int prio = 0; prio <= 3; ++prio) + for (int prio = 0; prio <= 2; ++prio) { prioOfTask = prio; selectedTasks = buildPlan(bestTasks, prio); @@ -658,7 +658,7 @@ bool Nullkiller::handleTrading() if (toGive && toGive <= available[mostExpendable]) //don't try to sell 0 resources { cb->trade(m, EMarketMode::RESOURCE_RESOURCE, GameResID(mostExpendable), GameResID(mostWanted), toGive); - logAi->info("Traded %d of %s for %d of %s at %s", toGive, mostExpendable, toGet, mostWanted, obj->getObjectName()); + logAi->debug("Traded %d of %s for %d of %s at %s", toGive, mostExpendable, toGet, mostWanted, obj->getObjectName()); haveTraded = true; shouldTryToTrade = true; } From fba34a743ef4e7e1e02bd76ee34574ff6f562bee Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 11 Aug 2024 18:23:52 +0200 Subject: [PATCH 058/186] Update DefenceBehavior.cpp Reverted previous change to defense-behavior. Both approaches have pros and cons and neither really works as I want. This still needs work. --- AI/Nullkiller/Behaviors/DefenceBehavior.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp index 7487791b2..a740d48fd 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp @@ -158,6 +158,10 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta threats.push_back(threatNode.fastestDanger); // no guarantee that fastest danger will be there + if (town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks, ai)) + { + return; + } if(!threatNode.fastestDanger.hero) { logAi->trace("No threat found for town %s", town->getNameTranslated()); From a7b26e237ffe23b463df2f273891a512adc883e6 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 11 Aug 2024 21:59:06 +0200 Subject: [PATCH 059/186] Fix crash Fixed a crash due to improperly trying to access "cb". --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index b58d2878b..2f57d1a02 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -1210,7 +1210,7 @@ public: evaluationContext.armyReward += potentialUpgradeValue / (float)bi.prerequisitesCount; } int sameTownBonus = 0; - for (auto town : cb->getTownsInfo()) + for (auto town : evaluationContext.evaluator.ai->cb->getTownsInfo()) { if (buildThis.town->getFaction() == town->getFaction()) sameTownBonus+=town->getTownLevel(); @@ -1225,7 +1225,7 @@ public: else if(bi.id >= BuildingID::MAGES_GUILD_1 && bi.id <= BuildingID::MAGES_GUILD_5) { evaluationContext.skillReward += 2 * (bi.id - BuildingID::MAGES_GUILD_1); - for (auto hero : cb->getHeroesInfo()) + for (auto hero : evaluationContext.evaluator.ai->cb->getHeroesInfo()) { evaluationContext.armyInvolvement += hero->getArmyCost(); } From 068e3bdc59d5f619352c85e2cf057ce49f81e2b0 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 12 Aug 2024 19:47:15 +0200 Subject: [PATCH 060/186] Fix endless loop Fixed an endless-loop caused by someone removing this ", turn++". --- AI/BattleAI/BattleExchangeVariant.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index 0200915c9..726c33304 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -731,7 +731,7 @@ std::vector BattleExchangeEvaluator::getOneTurnReachableUn { std::vector result; - for(int i = 0; i < turnOrder.size(); i++) + for(int i = 0; i < turnOrder.size(); i++, turn++) { auto & turnQueue = turnOrder[i]; HypotheticBattle turnBattle(env.get(), cb); From 6bd442e6f16dccda0969ff30e961c096ab59ab5d Mon Sep 17 00:00:00 2001 From: Xilmi Date: Tue, 13 Aug 2024 23:43:56 +0200 Subject: [PATCH 061/186] BattleAI: Fix endless loop Fixed an issue where the part of a 2-tile-unit that is outside of the map was considered a target and caused an endless-loop in the pathfinding being unable to get there. --- AI/BattleAI/AttackPossibility.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/BattleAI/AttackPossibility.cpp b/AI/BattleAI/AttackPossibility.cpp index d663a1bf4..ced430425 100644 --- a/AI/BattleAI/AttackPossibility.cpp +++ b/AI/BattleAI/AttackPossibility.cpp @@ -245,7 +245,7 @@ AttackPossibility AttackPossibility::evaluate( std::vector defenderHex; if(attackInfo.shooting) - defenderHex = defender->getHexes(); + defenderHex.push_back(defender->getPosition()); else defenderHex = CStack::meleeAttackHexes(attacker, defender, hex); From c10c04779fa74d6d85844d003705be7f45bc582f Mon Sep 17 00:00:00 2001 From: Xilmi Date: Wed, 14 Aug 2024 22:52:19 +0200 Subject: [PATCH 062/186] Adaptive Build-order When not threatened by nearby enemies the AI adds missing gold-income-buildings towards gold-pressure. This impacts the build-order in a way that they try to rush these more and get up a good economy more quickly. --- AI/Nullkiller/Analyzers/BuildAnalyzer.cpp | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp index be50aa6da..8cab129aa 100644 --- a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp @@ -149,9 +149,17 @@ void BuildAnalyzer::update() auto towns = ai->cb->getTownsInfo(); + float economyDevelopmentCost = 0; + uint8_t closestThreat = UINT8_MAX; + ai->dangerHitMap->updateHitMap(); + for(const CGTownInstance* town : towns) { - logAi->trace("Checking town %s", town->getNameTranslated()); + for (auto threat : ai->dangerHitMap->getTownThreats(town)) + { + closestThreat = std::min(closestThreat, threat.turn); + } + logAi->trace("Checking town %s closest threat: %u", town->getNameTranslated(), (unsigned int)closestThreat); developmentInfos.push_back(TownDevelopmentInfo(town)); TownDevelopmentInfo & developmentInfo = developmentInfos.back(); @@ -161,6 +169,11 @@ void BuildAnalyzer::update() requiredResources += developmentInfo.requiredResources; totalDevelopmentCost += developmentInfo.townDevelopmentCost; + for(auto building : developmentInfo.toBuild) + { + if (building.dailyIncome[EGameResID::GOLD] > 0) + economyDevelopmentCost += building.buildCostWithPrerequisites[EGameResID::GOLD]; + } armyCost += developmentInfo.armyCost; for(auto bi : developmentInfo.toBuild) @@ -169,6 +182,9 @@ void BuildAnalyzer::update() } } + if (closestThreat < 7) + economyDevelopmentCost = 0; + std::sort(developmentInfos.begin(), developmentInfos.end(), [](const TownDevelopmentInfo & t1, const TownDevelopmentInfo & t2) -> bool { auto val1 = convertToGold(t1.armyCost) - convertToGold(t1.townDevelopmentCost); @@ -180,7 +196,7 @@ void BuildAnalyzer::update() updateDailyIncome(); goldPressure = ai->getLockedResources()[EGameResID::GOLD] / 5000.0f - + (float)armyCost[EGameResID::GOLD] / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f); + + ((float)armyCost[EGameResID::GOLD] + economyDevelopmentCost) / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f); logAi->trace("Gold pressure: %f", goldPressure); } From b7e4219fdeeac3fb7a7f4abaa57cacb205df1c7d Mon Sep 17 00:00:00 2001 From: Xilmi Date: Wed, 14 Aug 2024 22:53:48 +0200 Subject: [PATCH 063/186] More purposeful defending Avoid defending towns that are out of reach for the enemy. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 24 ++++++++++++++-------- AI/Nullkiller/Engine/PriorityEvaluator.h | 2 ++ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 2f57d1a02..7e9891317 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -59,8 +59,10 @@ EvaluationContext::EvaluationContext(const Nullkiller* ai) armyInvolvement(0), defenseValue(0), isDefend(false), + threatTurns(INT_MAX), involvesSailing(false), - isTradeBuilding(false) + isTradeBuilding(false), + isChain(false) { } @@ -1002,6 +1004,7 @@ public: evaluationContext.defenseValue = town->fortLevel(); evaluationContext.isDefend = true; + evaluationContext.threatTurns = treat.turn; vstd::amax(evaluationContext.danger, defendTown.getTreat().danger); addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength()); @@ -1027,6 +1030,7 @@ public: vstd::amax(evaluationContext.danger, path.getTotalDanger()); evaluationContext.movementCost += path.movementCost(); evaluationContext.closestWayRatio = chain.closestWayRatio; + evaluationContext.isChain = true; std::map costsPerHero; @@ -1352,7 +1356,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) float score = 0; float maxWillingToLose = ai->cb->getTownsInfo().empty() ? 1 : 0.25; - logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, fuzzy: %f", + logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %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, fuzzy: %f", priorityTier, task->toString(), evaluationContext.armyLossPersentage, @@ -1365,6 +1369,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) evaluationContext.armyGrowth, evaluationContext.skillReward, evaluationContext.danger, + evaluationContext.threatTurns, evaluationContext.threat, evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout", evaluationContext.strategicalValue, @@ -1378,7 +1383,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) case 0: //Take towns { //score += evaluationContext.conquestValue * 1000; - if(evaluationContext.conquestValue > 0 || (evaluationContext.defenseValue >= CGTownInstance::EFortLevel::CITADEL && evaluationContext.turn <= 1 && evaluationContext.threat > evaluationContext.armyInvolvement)) + if(evaluationContext.conquestValue > 0 || (evaluationContext.defenseValue >= CGTownInstance::EFortLevel::CITADEL && evaluationContext.turn <= 1 && evaluationContext.threat > evaluationContext.armyInvolvement && evaluationContext.threatTurns == 0)) score = 1000; if (score == 0 || (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.turn > 0 && !ai->cb->getTownsInfo().empty())) return 0; @@ -1391,9 +1396,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) } case 1: //Collect unguarded stuff { - if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend) + if (evaluationContext.enemyHeroDangerRatio > 1) return 0; - if (evaluationContext.isDefend && evaluationContext.enemyHeroDangerRatio == 0) + if (evaluationContext.isDefend) return 0; if (evaluationContext.armyLossPersentage > 0) return 0; @@ -1425,6 +1430,8 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) return 0; if (evaluationContext.buildingCost.marketValue() > 0) return 0; + if (evaluationContext.isDefend && (evaluationContext.enemyHeroDangerRatio < 1 || evaluationContext.threatTurns > 0 || evaluationContext.turn > 0)) + return 0; score += evaluationContext.strategicalValue * 1000; score += evaluationContext.goldReward; score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05; @@ -1435,8 +1442,8 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) if (score > 0) { score *= evaluationContext.closestWayRatio; - if (evaluationContext.threat > 0) - score /= evaluationContext.threat; + if (evaluationContext.enemyHeroDangerRatio > 1) + score /= evaluationContext.enemyHeroDangerRatio; if (evaluationContext.movementCost > 0) score /= evaluationContext.movementCost; score *= (maxWillingToLose - evaluationContext.armyLossPersentage); @@ -1491,7 +1498,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) } #if NKAI_TRACE_LEVEL >= 2 - logAi->trace("priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, fuzzy: %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, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, fuzzy: %f, result %f", priorityTier, task->toString(), evaluationContext.armyLossPersentage, @@ -1504,6 +1511,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) evaluationContext.armyGrowth, evaluationContext.skillReward, evaluationContext.danger, + evaluationContext.threatTurns, evaluationContext.threat, evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout", evaluationContext.strategicalValue, diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index 9b935f407..b7c2bff9a 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -75,9 +75,11 @@ struct DLL_EXPORT EvaluationContext float armyInvolvement; int defenseValue; bool isDefend; + int threatTurns; TResources buildingCost; bool involvesSailing; bool isTradeBuilding; + bool isChain; EvaluationContext(const Nullkiller * ai); From f0ca1c6112c495dedb617f36d19634e9f2d9048c Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 15 Aug 2024 18:13:44 +0200 Subject: [PATCH 064/186] Update BuildAnalyzer.cpp Removed the restrictions of the greedy-playstyle. Only count forts as gold-producing prerequisites when no same- or higher-level fort exists somewhere else in the empire. --- AI/Nullkiller/Analyzers/BuildAnalyzer.cpp | 27 ++++++++++++++--------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp index 8cab129aa..648023bf3 100644 --- a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp @@ -150,16 +150,10 @@ void BuildAnalyzer::update() auto towns = ai->cb->getTownsInfo(); float economyDevelopmentCost = 0; - uint8_t closestThreat = UINT8_MAX; - ai->dangerHitMap->updateHitMap(); for(const CGTownInstance* town : towns) { - for (auto threat : ai->dangerHitMap->getTownThreats(town)) - { - closestThreat = std::min(closestThreat, threat.turn); - } - logAi->trace("Checking town %s closest threat: %u", town->getNameTranslated(), (unsigned int)closestThreat); + logAi->trace("Checking town %s", town->getNameTranslated()); developmentInfos.push_back(TownDevelopmentInfo(town)); TownDevelopmentInfo & developmentInfo = developmentInfos.back(); @@ -182,9 +176,6 @@ void BuildAnalyzer::update() } } - if (closestThreat < 7) - economyDevelopmentCost = 0; - std::sort(developmentInfos.begin(), developmentInfos.end(), [](const TownDevelopmentInfo & t1, const TownDevelopmentInfo & t2) -> bool { auto val1 = convertToGold(t1.armyCost) - convertToGold(t1.townDevelopmentCost); @@ -254,6 +245,12 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite( logAi->trace("checking %s", info.name); logAi->trace("buildInfo %s", info.toString()); + int highestFort = 0; + for (auto twn : ai->cb->getTownsInfo()) + { + highestFort = std::max(highestFort, (int)twn->fortLevel()); + } + if(!town->hasBuilt(building)) { auto canBuild = ai->cb->canBuildStructure(town, building); @@ -298,7 +295,15 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite( prerequisite.baseCreatureID = info.baseCreatureID; prerequisite.prerequisitesCount++; prerequisite.armyCost = info.armyCost; - prerequisite.dailyIncome = info.dailyIncome; + bool haveSameOrBetterFort = false; + if (prerequisite.id == BuildingID::FORT && highestFort >= CGTownInstance::EFortLevel::FORT) + haveSameOrBetterFort = true; + if (prerequisite.id == BuildingID::CITADEL && highestFort >= CGTownInstance::EFortLevel::CITADEL) + haveSameOrBetterFort = true; + if (prerequisite.id == BuildingID::CASTLE && highestFort >= CGTownInstance::EFortLevel::CASTLE) + haveSameOrBetterFort = true; + if(!haveSameOrBetterFort) + prerequisite.dailyIncome = info.dailyIncome; return prerequisite; } From 6193e6224f84afaeac63ed797cd8facd47cff564 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 15 Aug 2024 18:14:48 +0200 Subject: [PATCH 065/186] Update FuzzyHelper.cpp Added Multiplicative danger-modifier to citadels and castles. --- AI/Nullkiller/Engine/FuzzyHelper.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/AI/Nullkiller/Engine/FuzzyHelper.cpp b/AI/Nullkiller/Engine/FuzzyHelper.cpp index 912b29e8a..80daaca09 100644 --- a/AI/Nullkiller/Engine/FuzzyHelper.cpp +++ b/AI/Nullkiller/Engine/FuzzyHelper.cpp @@ -145,10 +145,10 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj) { auto fortLevel = town->fortLevel(); - if(fortLevel == CGTownInstance::EFortLevel::CASTLE) - danger += 10000; + if (fortLevel == CGTownInstance::EFortLevel::CASTLE) + danger = std::max(danger * 2, danger + 10000); else if(fortLevel == CGTownInstance::EFortLevel::CITADEL) - danger += 4000; + danger = std::max(ui64(danger * 1.4), danger + 4000); } return danger; From a79f76f32be284ea693d7e7fcca3ca8f13e3939a Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 15 Aug 2024 18:15:29 +0200 Subject: [PATCH 066/186] Update Nullkiller.cpp Fix issue with selling/buying the same resources back and forth. But probably leads to not using the market early on. (needs more testing) --- AI/Nullkiller/Engine/Nullkiller.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 4dbbe36c8..8b3843c90 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -601,7 +601,10 @@ bool Nullkiller::handleTrading() buildAnalyzer->update(); TResources required = buildAnalyzer->getTotalResourcesRequired(); TResources income = buildAnalyzer->getDailyIncome(); - TResources available = getFreeResources(); + TResources available = cb->getResourceAmount(); + + logAi->debug("Available %s", available.toString()); + logAi->debug("Required %s", required.toString()); int mostWanted = -1; int mostExpendable = -1; @@ -610,7 +613,7 @@ bool Nullkiller::handleTrading() for (int i = 0; i < required.size(); ++i) { - if (required[i] == 0) + if (required[i] <= 0) continue; float ratio = static_cast(available[i]) / required[i]; @@ -625,6 +628,8 @@ bool Nullkiller::handleTrading() float ratio = available[i]; if (required[i] > 0) ratio = static_cast(available[i]) / required[i]; + else + ratio = available[i]; bool okToSell = false; @@ -635,7 +640,7 @@ bool Nullkiller::handleTrading() } else { - if (available[i] > required[i]) + if (required[i] <= 0) okToSell = true; } @@ -645,7 +650,7 @@ bool Nullkiller::handleTrading() } } - //logAi->info("mostExpendable: %d mostWanted: %d", mostExpendable, mostWanted); + logAi->debug("mostExpendable: %d mostWanted: %d", mostExpendable, mostWanted); if (mostExpendable == mostWanted || mostWanted == -1 || mostExpendable == -1) return false; From 8ad6d712c0ad82b41f1f2924d5f7d143032bc26c Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 15 Aug 2024 18:16:23 +0200 Subject: [PATCH 067/186] lowered aggression Being less willing to rush across half the map to attack an enemy town only to find it too well defended when arriving there. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 10 +++++++++- AI/Nullkiller/Engine/PriorityEvaluator.h | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 7e9891317..5b7463fd2 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -62,7 +62,8 @@ EvaluationContext::EvaluationContext(const Nullkiller* ai) threatTurns(INT_MAX), involvesSailing(false), isTradeBuilding(false), - isChain(false) + isChain(false), + isEnemy(false) { } @@ -1068,6 +1069,8 @@ public: evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target); evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army); evaluationContext.armyInvolvement += army->getArmyCost(); + if (target->tempOwner != PlayerColor::NEUTRAL) + evaluationContext.isEnemy = true; } vstd::amax(evaluationContext.armyLossPersentage, path.getTotalArmyLoss() / (double)path.getHeroStrength()); @@ -1116,6 +1119,9 @@ public: evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost; evaluationContext.movementCost += objInfo.second.movementCost / boost; + if (target->tempOwner != PlayerColor::NEUTRAL) + evaluationContext.isEnemy = true; + vstd::amax(evaluationContext.turn, objInfo.second.turn / boost); boost <<= 1; @@ -1385,6 +1391,8 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) //score += evaluationContext.conquestValue * 1000; if(evaluationContext.conquestValue > 0 || (evaluationContext.defenseValue >= CGTownInstance::EFortLevel::CITADEL && evaluationContext.turn <= 1 && evaluationContext.threat > evaluationContext.armyInvolvement && evaluationContext.threatTurns == 0)) score = 1000; + if (evaluationContext.isEnemy && evaluationContext.turn > 0 && !ai->cb->getTownsInfo().empty()) + return 0; if (score == 0 || (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.turn > 0 && !ai->cb->getTownsInfo().empty())) return 0; if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index b7c2bff9a..a7d346e17 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -80,6 +80,7 @@ struct DLL_EXPORT EvaluationContext bool involvesSailing; bool isTradeBuilding; bool isChain; + bool isEnemy; EvaluationContext(const Nullkiller * ai); From 3be25d94147ad27cc79cee532e607f93d35850f7 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 18 Aug 2024 09:47:05 +0200 Subject: [PATCH 068/186] Update PriorityEvaluator.cpp Defend towns 1 turn earlier. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 5b7463fd2..b11f19334 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -1389,7 +1389,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) case 0: //Take towns { //score += evaluationContext.conquestValue * 1000; - if(evaluationContext.conquestValue > 0 || (evaluationContext.defenseValue >= CGTownInstance::EFortLevel::CITADEL && evaluationContext.turn <= 1 && evaluationContext.threat > evaluationContext.armyInvolvement && evaluationContext.threatTurns == 0)) + if(evaluationContext.conquestValue > 0 || (evaluationContext.defenseValue >= CGTownInstance::EFortLevel::CITADEL && evaluationContext.turn <= 1 && evaluationContext.threat > evaluationContext.armyInvolvement && evaluationContext.threatTurns <= 1)) score = 1000; if (evaluationContext.isEnemy && evaluationContext.turn > 0 && !ai->cb->getTownsInfo().empty()) return 0; From 284f2761088541727133721be9b21241e9cca974 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 18 Aug 2024 09:47:37 +0200 Subject: [PATCH 069/186] Update Nullkiller.cpp Don't trade away gold when the gold-pressure is high. --- AI/Nullkiller/Engine/Nullkiller.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 8b3843c90..f9954a6d2 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -635,7 +635,7 @@ bool Nullkiller::handleTrading() if (i == 6) { - if (income[i] > 0) + if (income[i] > 0 && !buildAnalyzer->isGoldPressureHigh()) okToSell = true; } else @@ -663,7 +663,7 @@ bool Nullkiller::handleTrading() if (toGive && toGive <= available[mostExpendable]) //don't try to sell 0 resources { cb->trade(m, EMarketMode::RESOURCE_RESOURCE, GameResID(mostExpendable), GameResID(mostWanted), toGive); - logAi->debug("Traded %d of %s for %d of %s at %s", toGive, mostExpendable, toGet, mostWanted, obj->getObjectName()); + logAi->info("Traded %d of %s for %d of %s at %s", toGive, mostExpendable, toGet, mostWanted, obj->getObjectName()); haveTraded = true; shouldTryToTrade = true; } From ea5ee039ca60227480add95a98c33b5d064e27a8 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 18 Aug 2024 09:48:16 +0200 Subject: [PATCH 070/186] Update BuildingBehavior.cpp Prioritize defensive buildings in threatened towns. --- AI/Nullkiller/Behaviors/BuildingBehavior.cpp | 49 ++++++++++++++------ 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/AI/Nullkiller/Behaviors/BuildingBehavior.cpp b/AI/Nullkiller/Behaviors/BuildingBehavior.cpp index 2cdc2ead3..b24ce0079 100644 --- a/AI/Nullkiller/Behaviors/BuildingBehavior.cpp +++ b/AI/Nullkiller/Behaviors/BuildingBehavior.cpp @@ -49,28 +49,47 @@ Goals::TGoalVec BuildingBehavior::decompose(const Nullkiller * ai) const auto & developmentInfos = ai->buildAnalyzer->getDevelopmentInfo(); auto isGoldPressureLow = !ai->buildAnalyzer->isGoldPressureHigh(); + ai->dangerHitMap->updateHitMap(); + for(auto & developmentInfo : developmentInfos) { - for(auto & buildingInfo : developmentInfo.toBuild) + uint8_t closestThreat = UINT8_MAX; + for (auto threat : ai->dangerHitMap->getTownThreats(developmentInfo.town)) { - if(isGoldPressureLow || buildingInfo.dailyIncome[EGameResID::GOLD] > 0) + closestThreat = std::min(closestThreat, threat.turn); + } + int fortLevel = developmentInfo.town->fortLevel(); + for (auto& buildingInfo : developmentInfo.toBuild) + { + if (closestThreat <= 1 && developmentInfo.town->fortLevel() < BuildingID::CASTLE && !buildingInfo.notEnoughRes) { - if(buildingInfo.notEnoughRes) - { - if(ai->getLockedResources().canAfford(buildingInfo.buildCost)) - continue; - - Composition composition; - - composition.addNext(BuildThis(buildingInfo, developmentInfo)); - composition.addNext(SaveResources(buildingInfo.buildCost)); - - tasks.push_back(sptr(composition)); - } - else + if (buildingInfo.id == BuildingID::FORT || buildingInfo.id == BuildingID::CITADEL || buildingInfo.id == BuildingID::CASTLE) tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo))); } } + if (tasks.empty()) + { + for (auto& buildingInfo : developmentInfo.toBuild) + { + if (isGoldPressureLow || buildingInfo.dailyIncome[EGameResID::GOLD] > 0) + { + if (buildingInfo.notEnoughRes) + { + if (ai->getLockedResources().canAfford(buildingInfo.buildCost)) + continue; + + Composition composition; + + composition.addNext(BuildThis(buildingInfo, developmentInfo)); + composition.addNext(SaveResources(buildingInfo.buildCost)); + + tasks.push_back(sptr(composition)); + } + else + tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo))); + } + } + } } return tasks; From 65b85766877e28cc67960d9689a597a130d8d04e Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 18 Aug 2024 09:49:02 +0200 Subject: [PATCH 071/186] Update BuyArmyBehavior.cpp Allow building army in threatened town even when it wants to save for a building. --- AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp b/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp index 610b16bcd..3d15ff9e8 100644 --- a/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp @@ -35,6 +35,8 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const return tasks; } + ai->dangerHitMap->updateHitMap(); + for(auto town : cb->getTownsInfo()) { //If we can recruit a hero that comes with more army than he costs, we are better off spending our gold on them @@ -48,7 +50,13 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const } } - if (ai->buildAnalyzer->isGoldPressureHigh() && !town->hasBuilt(BuildingID::CITY_HALL) && cb->canBuildStructure(town, BuildingID::CITY_HALL) != EBuildingState::FORBIDDEN) + uint8_t closestThreat = UINT8_MAX; + for (auto threat : ai->dangerHitMap->getTownThreats(town)) + { + closestThreat = std::min(closestThreat, threat.turn); + } + + if (closestThreat >=2 && ai->buildAnalyzer->isGoldPressureHigh() && !town->hasBuilt(BuildingID::CITY_HALL) && cb->canBuildStructure(town, BuildingID::CITY_HALL) != EBuildingState::FORBIDDEN) { return tasks; } From bdbb9d02fc3a597cfec39f3e77ee741727114f3c Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 18 Aug 2024 09:49:38 +0200 Subject: [PATCH 072/186] Update DefenceBehavior.cpp Fixed an issue where heroes that were leaving towns were still considered as defending the town. --- AI/Nullkiller/Behaviors/DefenceBehavior.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp index a740d48fd..c0dacd8f4 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp @@ -130,7 +130,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5))); - return true; + return false; } else if(ai->heroManager->getHeroRole(town->garrisonHero.get()) == HeroRole::MAIN) { @@ -141,7 +141,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa { tasks.push_back(Goals::sptr(Goals::DismissHero(heroToDismiss).setpriority(5))); - return true; + return false; } } } @@ -342,7 +342,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta } else if(town->visitingHero && path.targetHero != town->visitingHero && !path.containsHero(town->visitingHero)) { - if(town->garrisonHero) + if(town->garrisonHero && town->garrisonHero != path.targetHero) { if(ai->heroManager->getHeroRole(town->visitingHero.get()) == HeroRole::SCOUT && town->visitingHero->getArmyStrength() < path.heroArmy->getArmyStrength() / 20) From af7d5c7f7fda798369deb6cf973b4656f906900d Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 18 Aug 2024 09:50:32 +0200 Subject: [PATCH 073/186] Update RecruitHeroBehavior.cpp Don't hire a hero in a town where another hero is currently defending against a threat. This would mean one of them has to stay outside and be exposed. --- AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp index 961866dcc..27a784df6 100644 --- a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp +++ b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp @@ -54,9 +54,19 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const const CGTownInstance* bestTownToHireFrom = nullptr; float bestScore = 0; bool haveCapitol = false; + + ai->dangerHitMap->updateHitMap(); for(auto town : towns) { + uint8_t closestThreat = UINT8_MAX; + for (auto threat : ai->dangerHitMap->getTownThreats(town)) + { + closestThreat = std::min(closestThreat, threat.turn); + } + //Don' hire a hero in a threatened town as one would have to stay outside + if (closestThreat <= 1 && (town->visitingHero || town->garrisonHero)) + continue; if(ai->heroManager->canRecruitHero(town)) { auto availableHeroes = ai->cb->getAvailableHeroes(town); From 00e5770aa33d82a016f0fe0fd3156f8001c37c65 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 18 Aug 2024 21:22:05 +0200 Subject: [PATCH 074/186] Update PriorityEvaluator.cpp Revert change that made AI too passive. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index b08785852..0aa42144e 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -1389,8 +1389,6 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) //score += evaluationContext.conquestValue * 1000; if(evaluationContext.conquestValue > 0 || (evaluationContext.defenseValue >= CGTownInstance::EFortLevel::CITADEL && evaluationContext.turn <= 1 && evaluationContext.threat > evaluationContext.armyInvolvement && evaluationContext.threatTurns <= 1)) score = 1000; - if (evaluationContext.isEnemy && evaluationContext.turn > 0 && !ai->cb->getTownsInfo().empty()) - return 0; if (score == 0 || (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.turn > 0 && !ai->cb->getTownsInfo().empty())) return 0; if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) From e86ca49c379a9ce341f086d50d20382a5699efb3 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 19 Aug 2024 21:15:25 +0200 Subject: [PATCH 075/186] Update BuildingBehavior.cpp Fixed warning --- AI/Nullkiller/Behaviors/BuildingBehavior.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/AI/Nullkiller/Behaviors/BuildingBehavior.cpp b/AI/Nullkiller/Behaviors/BuildingBehavior.cpp index b24ce0079..551f0d73c 100644 --- a/AI/Nullkiller/Behaviors/BuildingBehavior.cpp +++ b/AI/Nullkiller/Behaviors/BuildingBehavior.cpp @@ -58,7 +58,6 @@ Goals::TGoalVec BuildingBehavior::decompose(const Nullkiller * ai) const { closestThreat = std::min(closestThreat, threat.turn); } - int fortLevel = developmentInfo.town->fortLevel(); for (auto& buildingInfo : developmentInfo.toBuild) { if (closestThreat <= 1 && developmentInfo.town->fortLevel() < BuildingID::CASTLE && !buildingInfo.notEnoughRes) From 8cf99616d01f317102234ef24cb664b41dd7b537 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 19 Aug 2024 21:21:56 +0200 Subject: [PATCH 076/186] Update BuildingBehavior.cpp Fixed a warning which, in this case, was actually also a logical error! :o --- AI/Nullkiller/Behaviors/BuildingBehavior.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/Nullkiller/Behaviors/BuildingBehavior.cpp b/AI/Nullkiller/Behaviors/BuildingBehavior.cpp index 551f0d73c..cb9af8405 100644 --- a/AI/Nullkiller/Behaviors/BuildingBehavior.cpp +++ b/AI/Nullkiller/Behaviors/BuildingBehavior.cpp @@ -60,7 +60,7 @@ Goals::TGoalVec BuildingBehavior::decompose(const Nullkiller * ai) const } for (auto& buildingInfo : developmentInfo.toBuild) { - if (closestThreat <= 1 && developmentInfo.town->fortLevel() < BuildingID::CASTLE && !buildingInfo.notEnoughRes) + if (closestThreat <= 1 && developmentInfo.town->fortLevel() < CGTownInstance::EFortLevel::CASTLE && !buildingInfo.notEnoughRes) { if (buildingInfo.id == BuildingID::FORT || buildingInfo.id == BuildingID::CITADEL || buildingInfo.id == BuildingID::CASTLE) tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo))); From 1d494f049d1633eaa99ba0c2d30f3789f3c313b4 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sat, 24 Aug 2024 14:54:00 +0200 Subject: [PATCH 077/186] Fix closest way ratio not initialized for ExecuteHeroChain Fix closest way ratio not initialized for ExecuteHeroChain --- AI/Nullkiller/Goals/ExecuteHeroChain.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp index 8fe4851b2..566427f4d 100644 --- a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp +++ b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp @@ -22,6 +22,7 @@ ExecuteHeroChain::ExecuteHeroChain(const AIPath & path, const CGObjectInstance * { hero = path.targetHero; tile = path.targetTile(); + closestWayRatio = 1; if(obj) { From b92862c04dcf5fe35da03cec1a1caff710e18c5e Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sat, 24 Aug 2024 14:55:26 +0200 Subject: [PATCH 078/186] New priorities Added more priority-tiers --- AI/Nullkiller/Engine/Nullkiller.cpp | 2 +- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 28 ++++++++++++++++++---- AI/Nullkiller/Engine/PriorityEvaluator.h | 2 +- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index f9954a6d2..460515ed0 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -408,7 +408,7 @@ void Nullkiller::makeTurn() TTaskVec selectedTasks; int prioOfTask = 0; - for (int prio = 0; prio <= 2; ++prio) + for (int prio = 1; prio <= 5; ++prio) { prioOfTask = prio; selectedTasks = buildPlan(bestTasks, prio); diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 0aa42144e..87841c13e 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -1384,10 +1384,19 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) switch (priorityTier) { - case 0: //Take towns + case 1: //Defend immediately threatened towns + { + if (evaluationContext.isDefend && evaluationContext.threatTurns == 0 && evaluationContext.turn == 0) + score = evaluationContext.armyInvolvement; + score *= evaluationContext.closestWayRatio; + if (evaluationContext.movementCost > 0) + score /= evaluationContext.movementCost; + break; + } + case 2: //Take towns { //score += evaluationContext.conquestValue * 1000; - if(evaluationContext.conquestValue > 0 || (evaluationContext.defenseValue >= CGTownInstance::EFortLevel::CITADEL && evaluationContext.turn <= 1 && evaluationContext.threat > evaluationContext.armyInvolvement && evaluationContext.threatTurns <= 1)) + if(evaluationContext.conquestValue > 0) score = 1000; if (score == 0 || (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.turn > 0 && !ai->cb->getTownsInfo().empty())) return 0; @@ -1398,7 +1407,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) score /= evaluationContext.movementCost; break; } - case 1: //Collect unguarded stuff + case 3: //Collect unguarded stuff { if (evaluationContext.enemyHeroDangerRatio > 1) return 0; @@ -1428,7 +1437,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) score /= evaluationContext.movementCost; break; } - case 2: //Collect guarded stuff + case 4: //Collect guarded stuff { if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend) return 0; @@ -1454,7 +1463,16 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) } break; } - case 3: //For buildings and buying army + case 5: //Defend whatever if nothing else is to do + { + if (evaluationContext.isDefend) + score = evaluationContext.armyInvolvement; + score *= evaluationContext.closestWayRatio; + if (evaluationContext.movementCost > 0) + score /= evaluationContext.movementCost; + break; + } + case 0: //For buildings and buying army { if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) return 0; diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index a7d346e17..fb7960c67 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -103,7 +103,7 @@ public: ~PriorityEvaluator(); void initVisitTile(); - float evaluate(Goals::TSubgoal task, int priorityTier = 3); + float evaluate(Goals::TSubgoal task, int priorityTier = 0); private: const Nullkiller * ai; From 69dc32a128eeb2d68bb474546e5975183ba4159d Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sat, 24 Aug 2024 17:15:15 +0200 Subject: [PATCH 079/186] Don't cast spells with below 0 score. The AI will no longer cast spells if the best spell's value is still below 0. --- AI/BattleAI/BattleEvaluator.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/BattleAI/BattleEvaluator.cpp b/AI/BattleAI/BattleEvaluator.cpp index 0c9941e2b..09da17878 100644 --- a/AI/BattleAI/BattleEvaluator.cpp +++ b/AI/BattleAI/BattleEvaluator.cpp @@ -790,7 +790,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) }; auto castToPerform = *vstd::maxElementByFun(possibleCasts, pscValue); - if(castToPerform.value > cachedScore) + if(castToPerform.value > cachedScore && castToPerform.value > 0) { LOGFL("Best spell is %s (value %d). Will cast.", castToPerform.spell->getNameTranslated() % castToPerform.value); BattleAction spellcast; From 7ae7c139646c65fc5b315cccc0a3574b9344d309 Mon Sep 17 00:00:00 2001 From: MichalZr6 Date: Wed, 28 Aug 2024 15:12:33 +0200 Subject: [PATCH 080/186] drop setting reachability for turrets --- AI/BattleAI/BattleExchangeVariant.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index 04ada089e..b1b7accdb 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -484,15 +484,18 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits( vstd::concatenate(allReachableUnits, turn == 0 ? reachabilityMap.at(hex) : getOneTurnReachableUnits(turn, hex)); } - for(auto hex : ap.attack.attacker->getHexes()) + if(!ap.attack.attacker->isTurret()) { - auto unitsReachingAttacker = turn == 0 ? reachabilityMap.at(hex) : getOneTurnReachableUnits(turn, hex); - for(auto unit : unitsReachingAttacker) + for(auto hex : ap.attack.attacker->getHexes()) { - if(unit->unitSide() != ap.attack.attacker->unitSide()) + auto unitsReachingAttacker = turn == 0 ? reachabilityMap.at(hex) : getOneTurnReachableUnits(turn, hex); + for(auto unit : unitsReachingAttacker) { - allReachableUnits.push_back(unit); - result.enemyUnitsReachingAttacker.insert(unit->unitId()); + if(unit->unitSide() != ap.attack.attacker->unitSide()) + { + allReachableUnits.push_back(unit); + result.enemyUnitsReachingAttacker.insert(unit->unitId()); + } } } } From ca3d81f047962dcee38813d8d8b2e3c7381e3214 Mon Sep 17 00:00:00 2001 From: MichalZr6 Date: Wed, 28 Aug 2024 15:16:33 +0200 Subject: [PATCH 081/186] fix crash on heroRoles.clear() --- AI/Nullkiller/Analyzers/HeroManager.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/AI/Nullkiller/Analyzers/HeroManager.cpp b/AI/Nullkiller/Analyzers/HeroManager.cpp index 44b66b46b..a67fd8ebc 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -126,20 +126,23 @@ void HeroManager::update() } std::sort(myHeroes.begin(), myHeroes.end(), scoreSort); - heroRoles.clear(); + + std::map newHeroRoles; for(auto hero : myHeroes) { if(hero->patrol.patrolling) { - heroRoles[hero] = HeroRole::MAIN; + newHeroRoles[hero] = HeroRole::MAIN; } else { - heroRoles[hero] = (globalMainCount--) > 0 ? HeroRole::MAIN : HeroRole::SCOUT; + newHeroRoles[hero] = (globalMainCount--) > 0 ? HeroRole::MAIN : HeroRole::SCOUT; } } + heroRoles = std::move(newHeroRoles); + for(auto hero : myHeroes) { logAi->trace("Hero %s has role %s", hero->getNameTranslated(), heroRoles[hero] == HeroRole::MAIN ? "main" : "scout"); From b8dacfc0bef79cb76682458cdff7fb26e74cf95d Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 29 Aug 2024 20:49:01 +0200 Subject: [PATCH 082/186] Update Nullkiller.cpp Fixed trade no longer working and changed log-output. --- AI/Nullkiller/Engine/Nullkiller.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 460515ed0..d10e76cdb 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -365,9 +365,9 @@ void Nullkiller::makeTurn() Goals::TGoalVec bestTasks; + logAi->info("Beginning: Strength: %f Townlevel: %d Resources: %s", totalHeroStrength, totalTownLevel, cb->getResourceAmount().toString()); for(int i = 1; i <= settings->getMaxPass() && cb->getPlayerStatus(playerID) == EPlayerStatus::INGAME; i++) { - logAi->info("Strength: %f Townlevel: %d Resources: %s", totalHeroStrength, totalTownLevel, cb->getResourceAmount().toString()); auto start = std::chrono::high_resolution_clock::now(); updateAiState(i); @@ -485,7 +485,7 @@ void Nullkiller::makeTurn() continue; } - logAi->info("Performing prio %d task %s with prio: %d", prioOfTask, bestTask->toString(), bestTask->priority); + logAi->info("Pass %d: Performing prio %d task %s with prio: %d", i, prioOfTask, bestTask->toString(), bestTask->priority); if(!executeTask(bestTask)) { if(hasAnySuccess) @@ -509,6 +509,15 @@ void Nullkiller::makeTurn() logAi->warn("Maxpass exceeded. Terminating AI turn."); } } + for (auto heroInfo : cb->getHeroesInfo()) + { + totalHeroStrength += heroInfo->getTotalStrength(); + } + for (auto townInfo : cb->getTownsInfo()) + { + totalTownLevel += townInfo->getTownLevel(); + } + logAi->info("End: Strength: %f Townlevel: %d Resources: %s", totalHeroStrength, totalTownLevel, cb->getResourceAmount().toString()); } bool Nullkiller::areAffectedObjectsPresent(Goals::TTask task) const @@ -662,7 +671,7 @@ bool Nullkiller::handleTrading() //TODO trade only as much as needed if (toGive && toGive <= available[mostExpendable]) //don't try to sell 0 resources { - cb->trade(m, EMarketMode::RESOURCE_RESOURCE, GameResID(mostExpendable), GameResID(mostWanted), toGive); + cb->trade(m->getObjInstanceID(), EMarketMode::RESOURCE_RESOURCE, GameResID(mostExpendable), GameResID(mostWanted), toGive); logAi->info("Traded %d of %s for %d of %s at %s", toGive, mostExpendable, toGet, mostWanted, obj->getObjectName()); haveTraded = true; shouldTryToTrade = true; From dcec5637cd5bfde197b2b99b0a006aeaddf60302 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 29 Aug 2024 21:01:06 +0200 Subject: [PATCH 083/186] Fix for defense-evaluation. Defense-evaluation didn't fill armyInvolvement but it was what created the score for it. So there was only score if it also included a HeroExchange. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 87841c13e..c83e2fcc9 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -1141,6 +1141,14 @@ public: Goals::ExchangeSwapTownHeroes & swapCommand = dynamic_cast(*task); const CGHeroInstance * garrisonHero = swapCommand.getGarrisonHero(); + logAi->trace("buildEvaluationContext ExchangeSwapTownHeroesContextBuilder %s affected objects: %d", swapCommand.toString(), swapCommand.getAffectedObjects().size()); + for (auto obj : swapCommand.getAffectedObjects()) + { + logAi->trace("affected object: %s", evaluationContext.evaluator.ai->cb->getObj(obj)->getObjectName()); + } + if (garrisonHero) + logAi->debug("with %s and %d", garrisonHero->getNameTranslated(), int(swapCommand.getLockingReason())); + if(garrisonHero && swapCommand.getLockingReason() == HeroLockedReason::DEFENCE) { auto defenderRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(garrisonHero); @@ -1149,6 +1157,9 @@ public: evaluationContext.movementCost += mpLeft; evaluationContext.movementCostByRole[defenderRole] += mpLeft; evaluationContext.heroRole = defenderRole; + evaluationContext.isDefend = true; + evaluationContext.armyInvolvement = garrisonHero->getArmyStrength(); + logAi->debug("evaluationContext.isDefend: %d", evaluationContext.isDefend); } } }; @@ -1360,7 +1371,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) float score = 0; float maxWillingToLose = ai->cb->getTownsInfo().empty() ? 1 : 0.25; - logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %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, fuzzy: %f", + logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %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, isDefend: %d, fuzzy: %f", priorityTier, task->toString(), evaluationContext.armyLossPersentage, @@ -1380,6 +1391,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) evaluationContext.conquestValue, evaluationContext.closestWayRatio, evaluationContext.enemyHeroDangerRatio, + evaluationContext.isDefend, fuzzyResult); switch (priorityTier) @@ -1389,8 +1401,6 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) if (evaluationContext.isDefend && evaluationContext.threatTurns == 0 && evaluationContext.turn == 0) score = evaluationContext.armyInvolvement; score *= evaluationContext.closestWayRatio; - if (evaluationContext.movementCost > 0) - score /= evaluationContext.movementCost; break; } case 2: //Take towns @@ -1468,8 +1478,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) if (evaluationContext.isDefend) score = evaluationContext.armyInvolvement; score *= evaluationContext.closestWayRatio; - if (evaluationContext.movementCost > 0) - score /= evaluationContext.movementCost; + score /= (evaluationContext.turn + 1); break; } case 0: //For buildings and buying army From 05d948b5825816d4587e0f63ab5a627209ee8dda Mon Sep 17 00:00:00 2001 From: Xilmi Date: Fri, 30 Aug 2024 16:46:36 +0200 Subject: [PATCH 084/186] Priorities Swapped priority of attacking and defending. Troop-delivery-missions will check safety of the delivering hero. --- AI/Nullkiller/Engine/Nullkiller.cpp | 4 +++- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 24 +++++++++++++--------- AI/Nullkiller/Engine/PriorityEvaluator.h | 1 + 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index d10e76cdb..d97aeca70 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -624,6 +624,8 @@ bool Nullkiller::handleTrading() { if (required[i] <= 0) continue; + if (i != 6 && income[i] > 0) + continue; float ratio = static_cast(available[i]) / required[i]; if (ratio < minRatio) { @@ -649,7 +651,7 @@ bool Nullkiller::handleTrading() } else { - if (required[i] <= 0) + if (required[i] <= 0 && income[i] > 0) okToSell = true; } diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index c83e2fcc9..f46279c52 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -63,7 +63,8 @@ EvaluationContext::EvaluationContext(const Nullkiller* ai) involvesSailing(false), isTradeBuilding(false), isChain(false), - isEnemy(false) + isEnemy(false), + isExchange(false) { } @@ -899,6 +900,7 @@ public: evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength()); evaluationContext.conquestValue += 2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength(); evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero); + evaluationContext.isExchange = true; } }; @@ -1396,19 +1398,12 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) switch (priorityTier) { - case 1: //Defend immediately threatened towns - { - if (evaluationContext.isDefend && evaluationContext.threatTurns == 0 && evaluationContext.turn == 0) - score = evaluationContext.armyInvolvement; - score *= evaluationContext.closestWayRatio; - break; - } - case 2: //Take towns + case 1: //Take towns { //score += evaluationContext.conquestValue * 1000; if(evaluationContext.conquestValue > 0) score = 1000; - if (score == 0 || (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.turn > 0 && !ai->cb->getTownsInfo().empty())) + if (score == 0 || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty())) return 0; if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) return 0; @@ -1417,6 +1412,13 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) score /= evaluationContext.movementCost; break; } + case 2: //Defend immediately threatened towns + { + if (evaluationContext.isDefend && evaluationContext.threatTurns == 0 && evaluationContext.turn == 0) + score = evaluationContext.armyInvolvement; + score *= evaluationContext.closestWayRatio; + break; + } case 3: //Collect unguarded stuff { if (evaluationContext.enemyHeroDangerRatio > 1) @@ -1475,6 +1477,8 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) } case 5: //Defend whatever if nothing else is to do { + if (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.isExchange) + return 0; if (evaluationContext.isDefend) score = evaluationContext.armyInvolvement; score *= evaluationContext.closestWayRatio; diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index fb7960c67..c6cef4960 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -81,6 +81,7 @@ struct DLL_EXPORT EvaluationContext bool isTradeBuilding; bool isChain; bool isEnemy; + bool isExchange; EvaluationContext(const Nullkiller * ai); From 56988e054a65ce5cf4dc6430a743e668bd24ad9a Mon Sep 17 00:00:00 2001 From: Xilmi Date: Fri, 30 Aug 2024 18:05:47 +0200 Subject: [PATCH 085/186] New priority 1. Take / kill what is reachable in same turn 2. Defend 3. Take / kill what is further away --- AI/Nullkiller/Engine/Nullkiller.cpp | 2 +- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 24 +++++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index d97aeca70..b6aaab7fc 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -408,7 +408,7 @@ void Nullkiller::makeTurn() TTaskVec selectedTasks; int prioOfTask = 0; - for (int prio = 1; prio <= 5; ++prio) + for (int prio = 1; prio <= 6; ++prio) { prioOfTask = prio; selectedTasks = buildPlan(bestTasks, prio); diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index f46279c52..f095e5919 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -1398,9 +1398,10 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) switch (priorityTier) { - case 1: //Take towns + case 1: //Take towns / kill heroes in immediate reach { - //score += evaluationContext.conquestValue * 1000; + if (evaluationContext.turn > 0) + return 0; if(evaluationContext.conquestValue > 0) score = 1000; if (score == 0 || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty())) @@ -1419,7 +1420,20 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) score *= evaluationContext.closestWayRatio; break; } - case 3: //Collect unguarded stuff + case 3: //Take towns / kill heroes that are further away + { + if (evaluationContext.conquestValue > 0) + score = 1000; + if (score == 0 || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty())) + return 0; + if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) + return 0; + score *= evaluationContext.closestWayRatio; + if (evaluationContext.movementCost > 0) + score /= evaluationContext.movementCost; + break; + } + case 4: //Collect unguarded stuff { if (evaluationContext.enemyHeroDangerRatio > 1) return 0; @@ -1449,7 +1463,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) score /= evaluationContext.movementCost; break; } - case 4: //Collect guarded stuff + case 5: //Collect guarded stuff { if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend) return 0; @@ -1475,7 +1489,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) } break; } - case 5: //Defend whatever if nothing else is to do + case 6: //Defend whatever if nothing else is to do { if (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.isExchange) return 0; From ac8e5b3711b2fd17968cd06868d1058c4eb4b6fd Mon Sep 17 00:00:00 2001 From: Xilmi Date: Fri, 30 Aug 2024 21:02:50 +0200 Subject: [PATCH 086/186] Update PriorityEvaluator.cpp AI should score citadels and castles higher for better developed towns so that it focuses on finishing the main-town quicker as opposed to developing several smaller towns simultaneously. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index f095e5919..d1a2dcd16 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -1205,10 +1205,10 @@ 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; - int32_t cost = bi.buildCostWithPrerequisites[EGameResID::GOLD]; + int32_t cost = bi.buildCost[EGameResID::GOLD]; evaluationContext.goldCost += cost; evaluationContext.closestWayRatio = 1; - evaluationContext.buildingCost += bi.buildCostWithPrerequisites; + evaluationContext.buildingCost += bi.buildCost; if (bi.id == BuildingID::MARKETPLACE || bi.dailyIncome[EGameResID::WOOD] > 0) evaluationContext.isTradeBuilding = true; @@ -1230,13 +1230,6 @@ public: evaluationContext.addNonCriticalStrategicalValue(potentialUpgradeValue / 10000.0f / (float)bi.prerequisitesCount); evaluationContext.armyReward += potentialUpgradeValue / (float)bi.prerequisitesCount; } - int sameTownBonus = 0; - for (auto town : evaluationContext.evaluator.ai->cb->getTownsInfo()) - { - if (buildThis.town->getFaction() == town->getFaction()) - sameTownBonus+=town->getTownLevel(); - } - evaluationContext.armyReward *= sameTownBonus; } else if(bi.id == BuildingID::CITADEL || bi.id == BuildingID::CASTLE) { @@ -1251,6 +1244,13 @@ public: evaluationContext.armyInvolvement += hero->getArmyCost(); } } + int sameTownBonus = 0; + for (auto town : evaluationContext.evaluator.ai->cb->getTownsInfo()) + { + if (buildThis.town->getFaction() == town->getFaction()) + sameTownBonus += town->getTownLevel(); + } + evaluationContext.armyReward *= sameTownBonus; if(evaluationContext.goldReward) { From 72597b549b66602c5194f00228121f4365a300d9 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sat, 31 Aug 2024 23:00:27 +0200 Subject: [PATCH 087/186] hero spread Prefer hiring heroes at towns that don't have heroes nearby. --- AI/Nullkiller/Analyzers/BuildAnalyzer.cpp | 8 ++++++++ AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp | 15 ++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp index 49ae295ca..f6e72e42b 100644 --- a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp @@ -148,6 +148,7 @@ void BuildAnalyzer::update() auto towns = ai->cb->getTownsInfo(); float economyDevelopmentCost = 0; + TResources nonGoldEconomyResources; for(const CGTownInstance* town : towns) { @@ -164,7 +165,10 @@ void BuildAnalyzer::update() for(auto building : developmentInfo.toBuild) { if (building.dailyIncome[EGameResID::GOLD] > 0) + { economyDevelopmentCost += building.buildCostWithPrerequisites[EGameResID::GOLD]; + nonGoldEconomyResources += building.buildCostWithPrerequisites; + } } armyCost += developmentInfo.armyCost; @@ -173,6 +177,10 @@ void BuildAnalyzer::update() logAi->trace("Building preferences %s", bi.toString()); } } + nonGoldEconomyResources[EGameResID::GOLD] = 0; + //If we don't have the non-gold-resources to build a structure, we also don't need to save gold for it and can consider building something else instead + if (!ai->getFreeResources().canAfford(nonGoldEconomyResources)) + economyDevelopmentCost = 0; std::sort(developmentInfos.begin(), developmentInfos.end(), [](const TownDevelopmentInfo & t1, const TownDevelopmentInfo & t2) -> bool { diff --git a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp index 27a784df6..f183a11a0 100644 --- a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp +++ b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp @@ -64,9 +64,15 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const { closestThreat = std::min(closestThreat, threat.turn); } - //Don' hire a hero in a threatened town as one would have to stay outside - if (closestThreat <= 1 && (town->visitingHero || town->garrisonHero)) + //Don't hire a hero where there already is one present + if (town->visitingHero || town->garrisonHero) continue; + float visitability = 0; + for (auto checkHero : ourHeroes) + { + if (ai->dangerHitMap->getClosestTown(checkHero.first.get()->pos) == town) + visitability++; + } if(ai->heroManager->canRecruitHero(town)) { auto availableHeroes = ai->cb->getAvailableHeroes(town); @@ -81,7 +87,10 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const score *= hero->getArmyCost(); if (hero->type->heroClass->faction == town->getFaction()) score *= 1.5; - score *= town->getTownLevel(); + if (visitability == 0) + score *= 30 * town->getTownLevel(); + else + score *= town->getTownLevel() / visitability; if (score > bestScore) { bestScore = score; From 0c488145b9bd0aa1165a68dde3b1a353b7ff58b4 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 1 Sep 2024 00:02:47 +0200 Subject: [PATCH 088/186] Update BuildAnalyzer.cpp Revert unintentionally commited changes --- AI/Nullkiller/Analyzers/BuildAnalyzer.cpp | 8 -------- 1 file changed, 8 deletions(-) diff --git a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp index f6e72e42b..49ae295ca 100644 --- a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp @@ -148,7 +148,6 @@ void BuildAnalyzer::update() auto towns = ai->cb->getTownsInfo(); float economyDevelopmentCost = 0; - TResources nonGoldEconomyResources; for(const CGTownInstance* town : towns) { @@ -165,10 +164,7 @@ void BuildAnalyzer::update() for(auto building : developmentInfo.toBuild) { if (building.dailyIncome[EGameResID::GOLD] > 0) - { economyDevelopmentCost += building.buildCostWithPrerequisites[EGameResID::GOLD]; - nonGoldEconomyResources += building.buildCostWithPrerequisites; - } } armyCost += developmentInfo.armyCost; @@ -177,10 +173,6 @@ void BuildAnalyzer::update() logAi->trace("Building preferences %s", bi.toString()); } } - nonGoldEconomyResources[EGameResID::GOLD] = 0; - //If we don't have the non-gold-resources to build a structure, we also don't need to save gold for it and can consider building something else instead - if (!ai->getFreeResources().canAfford(nonGoldEconomyResources)) - economyDevelopmentCost = 0; std::sort(developmentInfos.begin(), developmentInfos.end(), [](const TownDevelopmentInfo & t1, const TownDevelopmentInfo & t2) -> bool { From df5d1438229c192295d4c34f7373f84690ebfedd Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 1 Sep 2024 01:33:31 +0200 Subject: [PATCH 089/186] Difficulty-cheats Added the difficulty-dependent resource-cheats from the original game. --- server/processors/NewTurnProcessor.cpp | 46 ++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/server/processors/NewTurnProcessor.cpp b/server/processors/NewTurnProcessor.cpp index 4a34be20a..1078d5d33 100644 --- a/server/processors/NewTurnProcessor.cpp +++ b/server/processors/NewTurnProcessor.cpp @@ -234,6 +234,52 @@ ResourceSet NewTurnProcessor::generatePlayerIncome(PlayerColor playerID, bool ne for (auto obj : state.getOwnedObjects()) incomeHandicapped += obj->asOwnable()->dailyIncome(); + if (!state.isHuman()) + { + // Initialize bonuses for different resources + std::array weeklyBonuses = {}; + + // Calculate weekly bonuses based on difficulty + if (gameHandler->gameState()->getStartInfo()->difficulty == 0) + { + weeklyBonuses[EGameResID::GOLD] = static_cast(std::round(incomeHandicapped[EGameResID::GOLD] * (0.75 - 1) * 7)); + } + else if (gameHandler->gameState()->getStartInfo()->difficulty == 3) + { + weeklyBonuses[EGameResID::GOLD] = static_cast(std::round(incomeHandicapped[EGameResID::GOLD] * 0.25 * 7)); + weeklyBonuses[EGameResID::WOOD] = static_cast(std::round(incomeHandicapped[EGameResID::WOOD] * 0.39 * 7)); + weeklyBonuses[EGameResID::ORE] = static_cast(std::round(incomeHandicapped[EGameResID::ORE] * 0.39 * 7)); + weeklyBonuses[EGameResID::MERCURY] = static_cast(std::round(incomeHandicapped[EGameResID::MERCURY] * 0.14 * 7)); + weeklyBonuses[EGameResID::CRYSTAL] = static_cast(std::round(incomeHandicapped[EGameResID::CRYSTAL] * 0.14 * 7)); + weeklyBonuses[EGameResID::SULFUR] = static_cast(std::round(incomeHandicapped[EGameResID::SULFUR] * 0.14 * 7)); + weeklyBonuses[EGameResID::GEMS] = static_cast(std::round(incomeHandicapped[EGameResID::GEMS] * 0.14 * 7)); + } + else if (gameHandler->gameState()->getStartInfo()->difficulty == 4) + { + weeklyBonuses[EGameResID::GOLD] = static_cast(std::round(incomeHandicapped[EGameResID::GOLD] * 0.5 * 7)); + weeklyBonuses[EGameResID::WOOD] = static_cast(std::round(incomeHandicapped[EGameResID::WOOD] * 0.53 * 7)); + weeklyBonuses[EGameResID::ORE] = static_cast(std::round(incomeHandicapped[EGameResID::ORE] * 0.53 * 7)); + weeklyBonuses[EGameResID::MERCURY] = static_cast(std::round(incomeHandicapped[EGameResID::MERCURY] * 0.28 * 7)); + weeklyBonuses[EGameResID::CRYSTAL] = static_cast(std::round(incomeHandicapped[EGameResID::CRYSTAL] * 0.28 * 7)); + weeklyBonuses[EGameResID::SULFUR] = static_cast(std::round(incomeHandicapped[EGameResID::SULFUR] * 0.28 * 7)); + weeklyBonuses[EGameResID::GEMS] = static_cast(std::round(incomeHandicapped[EGameResID::GEMS] * 0.28 * 7)); + } + + // Distribute weekly bonuses over 7 days, depending on the current day of the week + for (int i = 0; i < GameResID::COUNT; ++i) + { + int dailyBonus = weeklyBonuses[i] / 7; + int remainderBonus = weeklyBonuses[i] % 7; + + // Apply the daily bonus for each day, and distribute the remainder accordingly + incomeHandicapped[static_cast(i)] += dailyBonus; + if (gameHandler->gameState()->getDate(Date::DAY_OF_WEEK) - 1 < remainderBonus) + { + incomeHandicapped[static_cast(i)] += 1; + } + } + } + return incomeHandicapped; } From be43c4d5f06733cc80d0bebb949231c2561e0b9f Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 1 Sep 2024 12:33:54 +0200 Subject: [PATCH 090/186] New hero-not acting Fixed an issue that caused newly hired heroes to do nothing on the turn they were hired under certain circumstances. --- AI/Nullkiller/Goals/RecruitHero.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/AI/Nullkiller/Goals/RecruitHero.cpp b/AI/Nullkiller/Goals/RecruitHero.cpp index c6a6c4d4e..156f41696 100644 --- a/AI/Nullkiller/Goals/RecruitHero.cpp +++ b/AI/Nullkiller/Goals/RecruitHero.cpp @@ -69,6 +69,7 @@ void RecruitHero::accept(AIGateway * ai) cb->recruitHero(t, heroToHire); ai->nullkiller->heroManager->update(); + ai->nullkiller->objectClusterizer->reset(); if(t->visitingHero) ai->moveHeroToTile(t->visitablePos(), t->visitingHero.get()); From 751f3b0e7d9cdb6d8b534127c1301f751a0b8521 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 1 Sep 2024 13:46:44 +0200 Subject: [PATCH 091/186] Update BuildingBehavior.cpp Fixed an issue that prevented generating more building-tasks when there already were tasks. --- AI/Nullkiller/Behaviors/BuildingBehavior.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/AI/Nullkiller/Behaviors/BuildingBehavior.cpp b/AI/Nullkiller/Behaviors/BuildingBehavior.cpp index cb9af8405..caed8fc1e 100644 --- a/AI/Nullkiller/Behaviors/BuildingBehavior.cpp +++ b/AI/Nullkiller/Behaviors/BuildingBehavior.cpp @@ -53,6 +53,7 @@ Goals::TGoalVec BuildingBehavior::decompose(const Nullkiller * ai) const for(auto & developmentInfo : developmentInfos) { + bool emergencyDefense = false; uint8_t closestThreat = UINT8_MAX; for (auto threat : ai->dangerHitMap->getTownThreats(developmentInfo.town)) { @@ -63,13 +64,17 @@ Goals::TGoalVec BuildingBehavior::decompose(const Nullkiller * ai) const if (closestThreat <= 1 && developmentInfo.town->fortLevel() < CGTownInstance::EFortLevel::CASTLE && !buildingInfo.notEnoughRes) { if (buildingInfo.id == BuildingID::FORT || buildingInfo.id == BuildingID::CITADEL || buildingInfo.id == BuildingID::CASTLE) + { tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo))); + emergencyDefense = true; + } } } - if (tasks.empty()) + if (!emergencyDefense) { for (auto& buildingInfo : developmentInfo.toBuild) { + logAi->trace("Looking at %s", buildingInfo.toString()); if (isGoldPressureLow || buildingInfo.dailyIncome[EGameResID::GOLD] > 0) { if (buildingInfo.notEnoughRes) @@ -81,11 +86,14 @@ Goals::TGoalVec BuildingBehavior::decompose(const Nullkiller * ai) const composition.addNext(BuildThis(buildingInfo, developmentInfo)); composition.addNext(SaveResources(buildingInfo.buildCost)); - + logAi->trace("Generate task to build: %s", buildingInfo.toString()); tasks.push_back(sptr(composition)); } else + { tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo))); + logAi->trace("Generate task to build: %s", buildingInfo.toString()); + } } } } From 1ef5e8ab1b7be79c67a585d123b6879778270fd8 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 1 Sep 2024 13:47:30 +0200 Subject: [PATCH 092/186] Update PriorityEvaluator.cpp Prevent building more buildings when we are saving for our favorite building. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index d1a2dcd16..d898ae1b8 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -1208,7 +1208,7 @@ public: int32_t cost = bi.buildCost[EGameResID::GOLD]; evaluationContext.goldCost += cost; evaluationContext.closestWayRatio = 1; - evaluationContext.buildingCost += bi.buildCost; + evaluationContext.buildingCost += bi.buildCostWithPrerequisites; if (bi.id == BuildingID::MARKETPLACE || bi.dailyIncome[EGameResID::WOOD] > 0) evaluationContext.isTradeBuilding = true; @@ -1503,6 +1503,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) { if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) return 0; + //If we already have locked resources, we don't look at other buildings + if (ai->getLockedResources().marketValue() > 0) + return 0; score += evaluationContext.conquestValue * 1000; score += evaluationContext.strategicalValue * 1000; score += evaluationContext.goldReward; From 0e91f10bbc1c74993196eb4f47c6418081534ca7 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 1 Sep 2024 17:21:53 +0200 Subject: [PATCH 093/186] Update Nullkiller.cpp ResetAIState so that units realize what they can do after unlocking a cluster. --- AI/Nullkiller/Engine/Nullkiller.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index b6aaab7fc..592cec6f4 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -369,6 +369,8 @@ void Nullkiller::makeTurn() for(int i = 1; i <= settings->getMaxPass() && cb->getPlayerStatus(playerID) == EPlayerStatus::INGAME; i++) { auto start = std::chrono::high_resolution_clock::now(); + //TODO: It's only necessary to do a resetAiState when the last action was UnlockCluster + resetAiState(); updateAiState(i); Goals::TTask bestTask = taskptr(Goals::Invalid()); From 7c6f96344a600eaeb909d4306c3b175cb570e619 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 1 Sep 2024 19:33:43 +0200 Subject: [PATCH 094/186] Update Nullkiller.cpp Removed resetAiState from loop cause it has too many side-effects. Such as the loop going through all passes. --- AI/Nullkiller/Engine/Nullkiller.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 592cec6f4..b6aaab7fc 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -369,8 +369,6 @@ void Nullkiller::makeTurn() for(int i = 1; i <= settings->getMaxPass() && cb->getPlayerStatus(playerID) == EPlayerStatus::INGAME; i++) { auto start = std::chrono::high_resolution_clock::now(); - //TODO: It's only necessary to do a resetAiState when the last action was UnlockCluster - resetAiState(); updateAiState(i); Goals::TTask bestTask = taskptr(Goals::Invalid()); From 64c3fbd51928b81bcc412ced989dc8906e389236 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 1 Sep 2024 23:58:47 +0200 Subject: [PATCH 095/186] Update ExecuteHeroChain.cpp Now resetting the ObjectClusterizer as killing something might change the situation. --- AI/Nullkiller/Goals/ExecuteHeroChain.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp index 566427f4d..1a39ff776 100644 --- a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp +++ b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp @@ -86,6 +86,7 @@ void ExecuteHeroChain::accept(AIGateway * ai) ai->nullkiller->setActive(chainPath.targetHero, tile); ai->nullkiller->setTargetObject(objid); + ai->nullkiller->objectClusterizer->reset(); auto targetObject = ai->myCb->getObj(static_cast(objid), false); From c667ca46d1a49ed7182df9782592a7cdf72a71c6 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 2 Sep 2024 00:00:36 +0200 Subject: [PATCH 096/186] Using correct priorityTier for Clusterization Clusterizer now uses PriorityTier = 5 for evaluation, which is used to generate priority for guarded objects --- AI/Nullkiller/Analyzers/ObjectClusterizer.cpp | 4 ++-- AI/Nullkiller/Engine/Nullkiller.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp index 5c4bb4cea..8d318badf 100644 --- a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp +++ b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp @@ -467,7 +467,7 @@ void ObjectClusterizer::clusterizeObject( heroesProcessed.insert(path.targetHero); - float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj))); + float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), 5); if(ai->settings->isUseFuzzy() && priority < MIN_PRIORITY) continue; @@ -490,7 +490,7 @@ void ObjectClusterizer::clusterizeObject( heroesProcessed.insert(path.targetHero); - float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj))); + float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), 5); if (ai->settings->isUseFuzzy() && priority < MIN_PRIORITY) continue; diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index b6aaab7fc..9984dbf51 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -180,7 +180,7 @@ Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks, int priorityTier) const for(size_t i = r.begin(); i != r.end(); i++) { auto task = tasks[i]; - if (task->asTask()->priority <= 0 || priorityTier != 3) + if (task->asTask()->priority <= 0 || priorityTier != 0) task->asTask()->priority = evaluator->evaluate(task, priorityTier); } }); @@ -385,7 +385,7 @@ void Nullkiller::makeTurn() if(bestTask->priority > 0) { - logAi->info("Performing task %s with prio: %d", bestTask->toString(), bestTask->priority); + logAi->info("Pass %d: Performing task %s with prio: %d", bestTask->toString(), bestTask->priority); if(!executeTask(bestTask)) return; From 09badeb5befd3254341331cc554b57bfc29066bd Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 2 Sep 2024 00:16:19 +0200 Subject: [PATCH 097/186] Enum for PriorityTiers In order to not confuse PriorityTiers, especially after adding new ones, now using an enum to identify them. --- AI/Nullkiller/Analyzers/ObjectClusterizer.cpp | 4 ++-- AI/Nullkiller/Engine/Nullkiller.cpp | 6 +++--- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 14 +++++++------- AI/Nullkiller/Engine/PriorityEvaluator.h | 13 ++++++++++++- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp index 8d318badf..7e46dc713 100644 --- a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp +++ b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp @@ -467,7 +467,7 @@ void ObjectClusterizer::clusterizeObject( heroesProcessed.insert(path.targetHero); - float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), 5); + float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), PriorityEvaluator::PriorityTier::HUNTER_GATHER); if(ai->settings->isUseFuzzy() && priority < MIN_PRIORITY) continue; @@ -490,7 +490,7 @@ void ObjectClusterizer::clusterizeObject( heroesProcessed.insert(path.targetHero); - float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), 5); + float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), PriorityEvaluator::PriorityTier::HUNTER_GATHER); if (ai->settings->isUseFuzzy() && priority < MIN_PRIORITY) continue; diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 9984dbf51..bc6dae208 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -180,7 +180,7 @@ Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks, int priorityTier) const for(size_t i = r.begin(); i != r.end(); i++) { auto task = tasks[i]; - if (task->asTask()->priority <= 0 || priorityTier != 0) + if (task->asTask()->priority <= 0 || priorityTier != PriorityEvaluator::PriorityTier::BUILDINGS) task->asTask()->priority = evaluator->evaluate(task, priorityTier); } }); @@ -385,7 +385,7 @@ void Nullkiller::makeTurn() if(bestTask->priority > 0) { - logAi->info("Pass %d: Performing task %s with prio: %d", bestTask->toString(), bestTask->priority); + logAi->info("Pass %d: Performing task %s with prio: %d", i, bestTask->toString(), bestTask->priority); if(!executeTask(bestTask)) return; @@ -408,7 +408,7 @@ void Nullkiller::makeTurn() TTaskVec selectedTasks; int prioOfTask = 0; - for (int prio = 1; prio <= 6; ++prio) + for (int prio = PriorityEvaluator::PriorityTier::INSTAKILL; prio <= PriorityEvaluator::PriorityTier::DEFEND; ++prio) { prioOfTask = prio; selectedTasks = buildPlan(bestTasks, prio); diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index d898ae1b8..51c2ac804 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -1398,7 +1398,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) switch (priorityTier) { - case 1: //Take towns / kill heroes in immediate reach + case PriorityTier::INSTAKILL: //Take towns / kill heroes in immediate reach { if (evaluationContext.turn > 0) return 0; @@ -1413,14 +1413,14 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) score /= evaluationContext.movementCost; break; } - case 2: //Defend immediately threatened towns + case PriorityTier::INSTADEFEND: //Defend immediately threatened towns { if (evaluationContext.isDefend && evaluationContext.threatTurns == 0 && evaluationContext.turn == 0) score = evaluationContext.armyInvolvement; score *= evaluationContext.closestWayRatio; break; } - case 3: //Take towns / kill heroes that are further away + case PriorityTier::KILL: //Take towns / kill heroes that are further away { if (evaluationContext.conquestValue > 0) score = 1000; @@ -1433,7 +1433,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) score /= evaluationContext.movementCost; break; } - case 4: //Collect unguarded stuff + case PriorityTier::GATHER: //Collect unguarded stuff { if (evaluationContext.enemyHeroDangerRatio > 1) return 0; @@ -1463,7 +1463,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) score /= evaluationContext.movementCost; break; } - case 5: //Collect guarded stuff + case PriorityTier::HUNTER_GATHER: //Collect guarded stuff { if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend) return 0; @@ -1489,7 +1489,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) } break; } - case 6: //Defend whatever if nothing else is to do + case PriorityTier::DEFEND: //Defend whatever if nothing else is to do { if (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.isExchange) return 0; @@ -1499,7 +1499,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) score /= (evaluationContext.turn + 1); break; } - case 0: //For buildings and buying army + case PriorityTier::BUILDINGS: //For buildings and buying army { if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) return 0; diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index c6cef4960..771a913e3 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -104,7 +104,18 @@ public: ~PriorityEvaluator(); void initVisitTile(); - float evaluate(Goals::TSubgoal task, int priorityTier = 0); + float evaluate(Goals::TSubgoal task, int priorityTier = BUILDINGS); + + enum PriorityTier : int32_t + { + BUILDINGS = 0, + INSTAKILL, + INSTADEFEND, + KILL, + GATHER, + HUNTER_GATHER, + DEFEND + }; private: const Nullkiller * ai; From 1176628a88fa5a8385b621a596c587d0912a2034 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 2 Sep 2024 01:37:21 +0200 Subject: [PATCH 098/186] Update PriorityEvaluator.cpp Workaround for weird -nan(ind) closestWayRatios. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 51c2ac804..144ab00d4 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -1547,6 +1547,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) } } 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 From 3f916ab54310bf133d5d46ceaa23bf55064c48ef Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Mon, 2 Sep 2024 14:24:22 +0300 Subject: [PATCH 099/186] BattleAI: avoid standing in moat --- AI/BattleAI/AttackPossibility.cpp | 76 +++++++++++++++++++++++++++ AI/BattleAI/AttackPossibility.h | 4 ++ AI/BattleAI/BattleEvaluator.cpp | 31 +++++++++-- AI/BattleAI/BattleExchangeVariant.cpp | 6 +++ AI/BattleAI/PotentialTargets.cpp | 1 + 5 files changed, 115 insertions(+), 3 deletions(-) diff --git a/AI/BattleAI/AttackPossibility.cpp b/AI/BattleAI/AttackPossibility.cpp index e2fb50dc7..04349003e 100644 --- a/AI/BattleAI/AttackPossibility.cpp +++ b/AI/BattleAI/AttackPossibility.cpp @@ -12,6 +12,10 @@ #include "../../lib/CStack.h" // TODO: remove // Eventually only IBattleInfoCallback and battle::Unit should be used, // CUnitState should be private and CStack should be removed completely +#include "../../lib/spells/CSpellHandler.h" +#include "../../lib/spells/ISpellMechanics.h" +#include "../../lib/spells/ObstacleCasterProxy.h" +#include "../../lib/battle/CObstacleInstance.h" uint64_t averageDmg(const DamageRange & range) { @@ -25,9 +29,55 @@ void DamageCache::cacheDamage(const battle::Unit * attacker, const battle::Unit damageCache[attacker->unitId()][defender->unitId()] = static_cast(damage) / attacker->getCount(); } +void DamageCache::buildObstacleDamageCache(std::shared_ptr hb, BattleSide side) +{ + for(const auto & obst : hb->battleGetAllObstacles(side)) + { + auto spellObstacle = dynamic_cast(obst.get()); + + if(!spellObstacle || !obst->triggersEffects()) + continue; + + auto triggerAbility = VLC->spells()->getById(obst->getTrigger()); + auto triggerIsNegative = triggerAbility->isNegative() || triggerAbility->isDamage(); + + if(!triggerIsNegative) + continue; + + const auto * hero = hb->battleGetFightingHero(spellObstacle->casterSide); + auto caster = spells::ObstacleCasterProxy(hb->getSidePlayer(spellObstacle->casterSide), hero, *spellObstacle); + + auto affectedHexes = obst->getAffectedTiles(); + auto stacks = hb->battleGetUnitsIf([](const battle::Unit * u) -> bool { return u->alive(); }); + + for(auto stack : stacks) + { + std::shared_ptr inner = std::make_shared(hb->env, hb); + auto cast = spells::BattleCast(hb.get(), &caster, spells::Mode::PASSIVE, obst->getTrigger().toSpell()); + auto updated = inner->getForUpdate(stack->unitId()); + + spells::Target target; + target.push_back(spells::Destination(updated.get())); + + cast.castEval(inner->getServerCallback(), target); + + auto damageDealt = stack->getAvailableHealth() - updated->getAvailableHealth(); + + for(auto hex : affectedHexes) + { + obstacleDamage[hex][stack->unitId()] = damageDealt; + } + } + } +} void DamageCache::buildDamageCache(std::shared_ptr hb, BattleSide side) { + if(parent == nullptr) + { + buildObstacleDamageCache(hb, side); + } + auto stacks = hb->battleGetUnitsIf([=](const battle::Unit * u) -> bool { return u->isValidTarget(); @@ -70,6 +120,23 @@ int64_t DamageCache::getDamage(const battle::Unit * attacker, const battle::Unit return damageCache[attacker->unitId()][defender->unitId()] * attacker->getCount(); } +int64_t DamageCache::getObstacleDamage(BattleHex hex, const battle::Unit * defender) +{ + if(parent) + return parent->getObstacleDamage(hex, defender); + + auto damages = obstacleDamage.find(hex); + + if(damages == obstacleDamage.end()) + return 0; + + auto damage = damages->second.find(defender->unitId()); + + return damage == damages->second.end() + ? 0 + : damage->second; +} + int64_t DamageCache::getOriginalDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr hb) { if(parent) @@ -288,6 +355,15 @@ AttackPossibility AttackPossibility::evaluate( { retaliatedUnits.push_back(attacker); } + + auto obstacleDamage = damageCache.getObstacleDamage(hex, attacker); + + if(obstacleDamage > 0) + { + ap.attackerDamageReduce += calculateDamageReduce(nullptr, attacker, obstacleDamage, damageCache, state); + + ap.attackerState->damage(obstacleDamage); + } } // ensure the defender is also affected diff --git a/AI/BattleAI/AttackPossibility.h b/AI/BattleAI/AttackPossibility.h index 990dcdb00..3ef8e1523 100644 --- a/AI/BattleAI/AttackPossibility.h +++ b/AI/BattleAI/AttackPossibility.h @@ -18,14 +18,18 @@ class DamageCache { private: std::unordered_map> damageCache; + std::map> obstacleDamage; DamageCache * parent; + void buildObstacleDamageCache(std::shared_ptr hb, BattleSide side); + public: DamageCache() : parent(nullptr) {} DamageCache(DamageCache * parent) : parent(parent) {} void cacheDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr hb); int64_t getDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr hb); + int64_t getObstacleDamage(BattleHex hex, const battle::Unit * defender); int64_t getOriginalDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr hb); void buildDamageCache(std::shared_ptr hb, BattleSide side); }; diff --git a/AI/BattleAI/BattleEvaluator.cpp b/AI/BattleAI/BattleEvaluator.cpp index 8ee042f14..555865619 100644 --- a/AI/BattleAI/BattleEvaluator.cpp +++ b/AI/BattleAI/BattleEvaluator.cpp @@ -226,11 +226,36 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack) { return BattleAction::makeDefend(stack); } - else + + auto enemyMellee = hb->getUnitsIf([this](const battle::Unit * u) -> bool + { + return u->unitSide() == BattleSide::ATTACKER && !hb->battleCanShoot(u); + }); + + bool isTargetOutsideFort = bestAttack.dest.getY() < GameConstants::BFIELD_WIDTH - 4; + bool siegeDefense = stack->unitSide() == BattleSide::DEFENDER + && !bestAttack.attack.shooting + && hb->battleGetFortifications().hasMoat + && !enemyMellee.empty() + && isTargetOutsideFort; + + if(siegeDefense) { - activeActionMade = true; - return BattleAction::makeMeleeAttack(stack, bestAttack.attack.defenderPos, bestAttack.from); + logAi->trace("Evaluating exchange at %d self-defense", stack->getPosition().hex); + + BattleAttackInfo bai(stack, stack, 0, false); + AttackPossibility apDefend(stack->getPosition(), stack->getPosition(), bai); + + float defenseValue = scoreEvaluator.evaluateExchange(apDefend, 0, *targets, damageCache, hb); + + if((defenseValue > score && score <= 0) || (defenseValue > 2 * score && score > 0)) + { + return BattleAction::makeDefend(stack); + } } + + activeActionMade = true; + return BattleAction::makeMeleeAttack(stack, bestAttack.attack.defenderPos, bestAttack.from); } } } diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index 70c881d23..4f3b1550e 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -28,6 +28,12 @@ float BattleExchangeVariant::trackAttack( std::shared_ptr hb, DamageCache & damageCache) { + if(!ap.attackerState) + { + logAi->trace("Skipping fake ap attack"); + return 0; + } + auto attacker = hb->getForUpdate(ap.attack.attacker->unitId()); float attackValue = ap.attackValue(); diff --git a/AI/BattleAI/PotentialTargets.cpp b/AI/BattleAI/PotentialTargets.cpp index a341921e6..f38415ef7 100644 --- a/AI/BattleAI/PotentialTargets.cpp +++ b/AI/BattleAI/PotentialTargets.cpp @@ -10,6 +10,7 @@ #include "StdInc.h" #include "PotentialTargets.h" #include "../../lib/CStack.h"//todo: remove +#include "../../lib/mapObjects/CGTownInstance.h" PotentialTargets::PotentialTargets( const battle::Unit * attacker, From 64fad53532d7ae3a70c8baa8b58c59495a9aba70 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Tue, 3 Sep 2024 20:51:13 +0200 Subject: [PATCH 100/186] Revert "Merge branch 'pr/4528' into develop" This reverts commit f4578c6d3ab9449a0b571857b1736515f0339670, reversing changes made to ac8e5b3711b2fd17968cd06868d1058c4eb4b6fd. --- AI/BattleAI/BattleExchangeVariant.cpp | 15 ++++++--------- AI/Nullkiller/Analyzers/HeroManager.cpp | 9 +++------ 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index 33e1a9041..a6bd2758e 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -491,18 +491,15 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits( vstd::concatenate(allReachableUnits, turn == 0 ? reachabilityMap.at(hex) : getOneTurnReachableUnits(turn, hex)); } - if(!ap.attack.attacker->isTurret()) + for(auto hex : ap.attack.attacker->getHexes()) { - for(auto hex : ap.attack.attacker->getHexes()) + auto unitsReachingAttacker = turn == 0 ? reachabilityMap.at(hex) : getOneTurnReachableUnits(turn, hex); + for(auto unit : unitsReachingAttacker) { - auto unitsReachingAttacker = turn == 0 ? reachabilityMap.at(hex) : getOneTurnReachableUnits(turn, hex); - for(auto unit : unitsReachingAttacker) + if(unit->unitSide() != ap.attack.attacker->unitSide()) { - if(unit->unitSide() != ap.attack.attacker->unitSide()) - { - allReachableUnits.push_back(unit); - result.enemyUnitsReachingAttacker.insert(unit->unitId()); - } + allReachableUnits.push_back(unit); + result.enemyUnitsReachingAttacker.insert(unit->unitId()); } } } diff --git a/AI/Nullkiller/Analyzers/HeroManager.cpp b/AI/Nullkiller/Analyzers/HeroManager.cpp index eb95e5ebc..e2406a023 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -126,23 +126,20 @@ void HeroManager::update() } std::sort(myHeroes.begin(), myHeroes.end(), scoreSort); - - std::map newHeroRoles; + heroRoles.clear(); for(auto hero : myHeroes) { if(hero->patrol.patrolling) { - newHeroRoles[hero] = HeroRole::MAIN; + heroRoles[hero] = HeroRole::MAIN; } else { - newHeroRoles[hero] = (globalMainCount--) > 0 ? HeroRole::MAIN : HeroRole::SCOUT; + heroRoles[hero] = (globalMainCount--) > 0 ? HeroRole::MAIN : HeroRole::SCOUT; } } - heroRoles = std::move(newHeroRoles); - for(auto hero : myHeroes) { logAi->trace("Hero %s has role %s", hero->getNameTranslated(), heroRoles[hero] == HeroRole::MAIN ? "main" : "scout"); From dfa992951bd9fb71591febc5e7de406db13255c9 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Tue, 3 Sep 2024 20:57:05 +0200 Subject: [PATCH 101/186] Revert "Merge branch 'fix-battle-ai' into develop" This reverts commit b489816d29f15a6d0a0abc5827887294bc08db03, reversing changes made to 5ee7061ab76bff92ab7f579c137dd19c2710908e. --- AI/BattleAI/AttackPossibility.cpp | 76 --------------------------- AI/BattleAI/AttackPossibility.h | 4 -- AI/BattleAI/BattleEvaluator.cpp | 31 ++--------- AI/BattleAI/BattleExchangeVariant.cpp | 6 --- AI/BattleAI/PotentialTargets.cpp | 1 - 5 files changed, 3 insertions(+), 115 deletions(-) diff --git a/AI/BattleAI/AttackPossibility.cpp b/AI/BattleAI/AttackPossibility.cpp index 04349003e..e2fb50dc7 100644 --- a/AI/BattleAI/AttackPossibility.cpp +++ b/AI/BattleAI/AttackPossibility.cpp @@ -12,10 +12,6 @@ #include "../../lib/CStack.h" // TODO: remove // Eventually only IBattleInfoCallback and battle::Unit should be used, // CUnitState should be private and CStack should be removed completely -#include "../../lib/spells/CSpellHandler.h" -#include "../../lib/spells/ISpellMechanics.h" -#include "../../lib/spells/ObstacleCasterProxy.h" -#include "../../lib/battle/CObstacleInstance.h" uint64_t averageDmg(const DamageRange & range) { @@ -29,55 +25,9 @@ void DamageCache::cacheDamage(const battle::Unit * attacker, const battle::Unit damageCache[attacker->unitId()][defender->unitId()] = static_cast(damage) / attacker->getCount(); } -void DamageCache::buildObstacleDamageCache(std::shared_ptr hb, BattleSide side) -{ - for(const auto & obst : hb->battleGetAllObstacles(side)) - { - auto spellObstacle = dynamic_cast(obst.get()); - - if(!spellObstacle || !obst->triggersEffects()) - continue; - - auto triggerAbility = VLC->spells()->getById(obst->getTrigger()); - auto triggerIsNegative = triggerAbility->isNegative() || triggerAbility->isDamage(); - - if(!triggerIsNegative) - continue; - - const auto * hero = hb->battleGetFightingHero(spellObstacle->casterSide); - auto caster = spells::ObstacleCasterProxy(hb->getSidePlayer(spellObstacle->casterSide), hero, *spellObstacle); - - auto affectedHexes = obst->getAffectedTiles(); - auto stacks = hb->battleGetUnitsIf([](const battle::Unit * u) -> bool { return u->alive(); }); - - for(auto stack : stacks) - { - std::shared_ptr inner = std::make_shared(hb->env, hb); - auto cast = spells::BattleCast(hb.get(), &caster, spells::Mode::PASSIVE, obst->getTrigger().toSpell()); - auto updated = inner->getForUpdate(stack->unitId()); - - spells::Target target; - target.push_back(spells::Destination(updated.get())); - - cast.castEval(inner->getServerCallback(), target); - - auto damageDealt = stack->getAvailableHealth() - updated->getAvailableHealth(); - - for(auto hex : affectedHexes) - { - obstacleDamage[hex][stack->unitId()] = damageDealt; - } - } - } -} void DamageCache::buildDamageCache(std::shared_ptr hb, BattleSide side) { - if(parent == nullptr) - { - buildObstacleDamageCache(hb, side); - } - auto stacks = hb->battleGetUnitsIf([=](const battle::Unit * u) -> bool { return u->isValidTarget(); @@ -120,23 +70,6 @@ int64_t DamageCache::getDamage(const battle::Unit * attacker, const battle::Unit return damageCache[attacker->unitId()][defender->unitId()] * attacker->getCount(); } -int64_t DamageCache::getObstacleDamage(BattleHex hex, const battle::Unit * defender) -{ - if(parent) - return parent->getObstacleDamage(hex, defender); - - auto damages = obstacleDamage.find(hex); - - if(damages == obstacleDamage.end()) - return 0; - - auto damage = damages->second.find(defender->unitId()); - - return damage == damages->second.end() - ? 0 - : damage->second; -} - int64_t DamageCache::getOriginalDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr hb) { if(parent) @@ -355,15 +288,6 @@ AttackPossibility AttackPossibility::evaluate( { retaliatedUnits.push_back(attacker); } - - auto obstacleDamage = damageCache.getObstacleDamage(hex, attacker); - - if(obstacleDamage > 0) - { - ap.attackerDamageReduce += calculateDamageReduce(nullptr, attacker, obstacleDamage, damageCache, state); - - ap.attackerState->damage(obstacleDamage); - } } // ensure the defender is also affected diff --git a/AI/BattleAI/AttackPossibility.h b/AI/BattleAI/AttackPossibility.h index 3ef8e1523..990dcdb00 100644 --- a/AI/BattleAI/AttackPossibility.h +++ b/AI/BattleAI/AttackPossibility.h @@ -18,18 +18,14 @@ class DamageCache { private: std::unordered_map> damageCache; - std::map> obstacleDamage; DamageCache * parent; - void buildObstacleDamageCache(std::shared_ptr hb, BattleSide side); - public: DamageCache() : parent(nullptr) {} DamageCache(DamageCache * parent) : parent(parent) {} void cacheDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr hb); int64_t getDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr hb); - int64_t getObstacleDamage(BattleHex hex, const battle::Unit * defender); int64_t getOriginalDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr hb); void buildDamageCache(std::shared_ptr hb, BattleSide side); }; diff --git a/AI/BattleAI/BattleEvaluator.cpp b/AI/BattleAI/BattleEvaluator.cpp index 6422b43e2..c54bc94e7 100644 --- a/AI/BattleAI/BattleEvaluator.cpp +++ b/AI/BattleAI/BattleEvaluator.cpp @@ -226,36 +226,11 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack) { return BattleAction::makeDefend(stack); } - - auto enemyMellee = hb->getUnitsIf([this](const battle::Unit * u) -> bool - { - return u->unitSide() == BattleSide::ATTACKER && !hb->battleCanShoot(u); - }); - - bool isTargetOutsideFort = bestAttack.dest.getY() < GameConstants::BFIELD_WIDTH - 4; - bool siegeDefense = stack->unitSide() == BattleSide::DEFENDER - && !bestAttack.attack.shooting - && hb->battleGetFortifications().hasMoat - && !enemyMellee.empty() - && isTargetOutsideFort; - - if(siegeDefense) + else { - logAi->trace("Evaluating exchange at %d self-defense", stack->getPosition().hex); - - BattleAttackInfo bai(stack, stack, 0, false); - AttackPossibility apDefend(stack->getPosition(), stack->getPosition(), bai); - - float defenseValue = scoreEvaluator.evaluateExchange(apDefend, 0, *targets, damageCache, hb); - - if((defenseValue > score && score <= 0) || (defenseValue > 2 * score && score > 0)) - { - return BattleAction::makeDefend(stack); - } + activeActionMade = true; + return BattleAction::makeMeleeAttack(stack, bestAttack.attack.defenderPos, bestAttack.from); } - - activeActionMade = true; - return BattleAction::makeMeleeAttack(stack, bestAttack.attack.defenderPos, bestAttack.from); } } } diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index a6bd2758e..097ffb07b 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -28,12 +28,6 @@ float BattleExchangeVariant::trackAttack( std::shared_ptr hb, DamageCache & damageCache) { - if(!ap.attackerState) - { - logAi->trace("Skipping fake ap attack"); - return 0; - } - auto attacker = hb->getForUpdate(ap.attack.attacker->unitId()); float attackValue = ap.attackValue(); diff --git a/AI/BattleAI/PotentialTargets.cpp b/AI/BattleAI/PotentialTargets.cpp index f38415ef7..a341921e6 100644 --- a/AI/BattleAI/PotentialTargets.cpp +++ b/AI/BattleAI/PotentialTargets.cpp @@ -10,7 +10,6 @@ #include "StdInc.h" #include "PotentialTargets.h" #include "../../lib/CStack.h"//todo: remove -#include "../../lib/mapObjects/CGTownInstance.h" PotentialTargets::PotentialTargets( const battle::Unit * attacker, From d0aefdfbe6e899042b1623bad2840a3e9ee845fe Mon Sep 17 00:00:00 2001 From: Xilmi Date: Tue, 3 Sep 2024 21:17:06 +0200 Subject: [PATCH 102/186] Update RecruitHero.cpp Removed a A --- AI/Nullkiller/Goals/RecruitHero.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/Nullkiller/Goals/RecruitHero.cpp b/AI/Nullkiller/Goals/RecruitHero.cpp index b78ac92bb..5ea60317f 100644 --- a/AI/Nullkiller/Goals/RecruitHero.cpp +++ b/AI/Nullkiller/Goals/RecruitHero.cpp @@ -76,7 +76,7 @@ void RecruitHero::accept(AIGateway * ai) ai->nullkiller->objectClusterizer->reset(); } - if(t->visitingHero)A + if(t->visitingHero) ai->moveHeroToTile(t->visitablePos(), t->visitingHero.get()); } From db16a9d234748bcbe9da44673c50e5437e2d0169 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Wed, 4 Sep 2024 16:41:47 +0200 Subject: [PATCH 103/186] A bit of clean-up for merge Set back trace level to 0 Removed EvaluationContexts that weren't used Encapsulated many debug-messages behinde trace-levels --- AI/Nullkiller/Engine/Nullkiller.cpp | 45 +++++++++++++--------- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 11 +----- AI/Nullkiller/Engine/PriorityEvaluator.h | 2 - AI/Nullkiller/Pathfinding/AINodeStorage.h | 2 +- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index bc6dae208..46d7825ab 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -350,6 +350,11 @@ void Nullkiller::makeTurn() const int MAX_DEPTH = 10; + resetAiState(); + + Goals::TGoalVec bestTasks; + +#if NKAI_TRACE_LEVEL >= 1 float totalHeroStrength = 0; int totalTownLevel = 0; for (auto heroInfo : cb->getHeroesInfo()) @@ -360,12 +365,8 @@ void Nullkiller::makeTurn() { totalTownLevel += townInfo->getTownLevel(); } - - resetAiState(); - - Goals::TGoalVec bestTasks; - logAi->info("Beginning: Strength: %f Townlevel: %d Resources: %s", totalHeroStrength, totalTownLevel, cb->getResourceAmount().toString()); +#endif for(int i = 1; i <= settings->getMaxPass() && cb->getPlayerStatus(playerID) == EPlayerStatus::INGAME; i++) { auto start = std::chrono::high_resolution_clock::now(); @@ -385,7 +386,9 @@ void Nullkiller::makeTurn() if(bestTask->priority > 0) { +#if NKAI_TRACE_LEVEL >= 1 logAi->info("Pass %d: Performing task %s with prio: %d", i, bestTask->toString(), bestTask->priority); +#endif if(!executeTask(bestTask)) return; @@ -485,7 +488,9 @@ void Nullkiller::makeTurn() continue; } +#if NKAI_TRACE_LEVEL >= 1 logAi->info("Pass %d: Performing prio %d task %s with prio: %d", i, prioOfTask, bestTask->toString(), bestTask->priority); +#endif if(!executeTask(bestTask)) { if(hasAnySuccess) @@ -501,6 +506,17 @@ void Nullkiller::makeTurn() if(!hasAnySuccess) { logAi->trace("Nothing was done this turn. Ending turn."); +#if NKAI_TRACE_LEVEL >= 1 + for (auto heroInfo : cb->getHeroesInfo()) + { + totalHeroStrength += heroInfo->getTotalStrength(); + } + for (auto townInfo : cb->getTownsInfo()) + { + totalTownLevel += townInfo->getTownLevel(); + } + logAi->info("End: Strength: %f Townlevel: %d Resources: %s", totalHeroStrength, totalTownLevel, cb->getResourceAmount().toString()); +#endif return; } @@ -509,15 +525,6 @@ void Nullkiller::makeTurn() logAi->warn("Maxpass exceeded. Terminating AI turn."); } } - for (auto heroInfo : cb->getHeroesInfo()) - { - totalHeroStrength += heroInfo->getTotalStrength(); - } - for (auto townInfo : cb->getTownsInfo()) - { - totalTownLevel += townInfo->getTownLevel(); - } - logAi->info("End: Strength: %f Townlevel: %d Resources: %s", totalHeroStrength, totalTownLevel, cb->getResourceAmount().toString()); } bool Nullkiller::areAffectedObjectsPresent(Goals::TTask task) const @@ -611,10 +618,10 @@ bool Nullkiller::handleTrading() TResources required = buildAnalyzer->getTotalResourcesRequired(); TResources income = buildAnalyzer->getDailyIncome(); TResources available = cb->getResourceAmount(); - +#if NKAI_TRACE_LEVEL >= 2 logAi->debug("Available %s", available.toString()); logAi->debug("Required %s", required.toString()); - +#endif int mostWanted = -1; int mostExpendable = -1; float minRatio = std::numeric_limits::max(); @@ -660,9 +667,9 @@ bool Nullkiller::handleTrading() mostExpendable = i; } } - +#if NKAI_TRACE_LEVEL >= 2 logAi->debug("mostExpendable: %d mostWanted: %d", mostExpendable, mostWanted); - +#endif if (mostExpendable == mostWanted || mostWanted == -1 || mostExpendable == -1) return false; @@ -674,7 +681,9 @@ bool Nullkiller::handleTrading() if (toGive && toGive <= available[mostExpendable]) //don't try to sell 0 resources { cb->trade(m->getObjInstanceID(), EMarketMode::RESOURCE_RESOURCE, GameResID(mostExpendable), GameResID(mostWanted), toGive); +#if NKAI_TRACE_LEVEL >= 1 logAi->info("Traded %d of %s for %d of %s at %s", toGive, mostExpendable, toGet, mostWanted, obj->getObjectName()); +#endif haveTraded = true; shouldTryToTrade = true; } diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 144ab00d4..cb282fa50 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -62,8 +62,6 @@ EvaluationContext::EvaluationContext(const Nullkiller* ai) threatTurns(INT_MAX), involvesSailing(false), isTradeBuilding(false), - isChain(false), - isEnemy(false), isExchange(false) { } @@ -1031,7 +1029,6 @@ public: vstd::amax(evaluationContext.danger, path.getTotalDanger()); evaluationContext.movementCost += path.movementCost(); evaluationContext.closestWayRatio = chain.closestWayRatio; - evaluationContext.isChain = true; std::map costsPerHero; @@ -1069,8 +1066,6 @@ public: evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target); evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army); evaluationContext.armyInvolvement += army->getArmyCost(); - if (target->tempOwner != PlayerColor::NEUTRAL) - evaluationContext.isEnemy = true; } vstd::amax(evaluationContext.armyLossPersentage, path.getTotalArmyLoss() / (double)path.getHeroStrength()); @@ -1119,9 +1114,6 @@ public: evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost; evaluationContext.movementCost += objInfo.second.movementCost / boost; - if (target->tempOwner != PlayerColor::NEUTRAL) - evaluationContext.isEnemy = true; - vstd::amax(evaluationContext.turn, objInfo.second.turn / boost); boost <<= 1; @@ -1372,7 +1364,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) { float score = 0; float maxWillingToLose = ai->cb->getTownsInfo().empty() ? 1 : 0.25; - +#if NKAI_TRACE_LEVEL >= 2 logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %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, isDefend: %d, fuzzy: %f", priorityTier, task->toString(), @@ -1395,6 +1387,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) evaluationContext.enemyHeroDangerRatio, evaluationContext.isDefend, fuzzyResult); +#endif switch (priorityTier) { diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index 771a913e3..5e8e8c365 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -79,8 +79,6 @@ struct DLL_EXPORT EvaluationContext TResources buildingCost; bool involvesSailing; bool isTradeBuilding; - bool isChain; - bool isEnemy; bool isExchange; EvaluationContext(const Nullkiller * ai); diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.h b/AI/Nullkiller/Pathfinding/AINodeStorage.h index 2f823f2de..cea1b0f88 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.h +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.h @@ -12,7 +12,7 @@ #define NKAI_PATHFINDER_TRACE_LEVEL 0 constexpr int NKAI_GRAPH_TRACE_LEVEL = 0; -#define NKAI_TRACE_LEVEL 2 +#define NKAI_TRACE_LEVEL 0 #include "../../../lib/pathfinder/CGPathNode.h" #include "../../../lib/pathfinder/INodeStorage.h" From b32c9615ede64b789234a2b992834374f140d490 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 5 Sep 2024 15:59:09 +0200 Subject: [PATCH 104/186] Update Nullkiller.cpp Removed unused variable. --- AI/Nullkiller/Engine/Nullkiller.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 46d7825ab..77cac935a 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -410,10 +410,8 @@ void Nullkiller::makeTurn() decompose(bestTasks, sptr(ExplorationBehavior()), MAX_DEPTH); TTaskVec selectedTasks; - int prioOfTask = 0; for (int prio = PriorityEvaluator::PriorityTier::INSTAKILL; prio <= PriorityEvaluator::PriorityTier::DEFEND; ++prio) { - prioOfTask = prio; selectedTasks = buildPlan(bestTasks, prio); if (!selectedTasks.empty() || settings->isUseFuzzy()) break; From 07afb2d64968b978d13872e5ecd4e14873789684 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 5 Sep 2024 16:20:15 +0200 Subject: [PATCH 105/186] Update lib/ResourceSet.h Use suggestion. Co-authored-by: Ivan Savenko --- lib/ResourceSet.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/ResourceSet.h b/lib/ResourceSet.h index c080d1dff..1f1d3467b 100644 --- a/lib/ResourceSet.h +++ b/lib/ResourceSet.h @@ -159,8 +159,7 @@ public: } else { // Calculate the number of times we need to accumulate income to fulfill the need - float divisionResult = static_cast(container.at(i)) / static_cast(income[i]); - int ceiledResult = static_cast(std::ceil(divisionResult)); + int ceiledResult = vstd::divideAndCeil(container.at(i), income[i]); ret = std::max(ret, ceiledResult); } } From 23cd54c998ad3923aada46c2fdd7d7ba817963b6 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 5 Sep 2024 16:22:25 +0200 Subject: [PATCH 106/186] Preparations for merge No longer using FuzzyEngine just to create a log-message. It's now only used when isUseFuzzy is set. Also: Removed < operator and instead use already existing "canAfford"-Method. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 58 +++++++++++----------- lib/ResourceSet.h | 11 ---- 2 files changed, 29 insertions(+), 40 deletions(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index cb282fa50..b04ae252e 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -1328,36 +1328,36 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) double result = 0; - float fuzzyResult = 0; - try - { - armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage); - heroRoleVariable->setValue(evaluationContext.heroRole); - mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]); - scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]); - goldRewardVariable->setValue(goldRewardPerTurn); - 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); - goldPressureVariable->setValue(ai->buildAnalyzer->getGoldPressure()); - goldCostVariable->setValue(evaluationContext.goldCost / ((float)ai->getFreeResources()[EGameResID::GOLD] + (float)ai->buildAnalyzer->getDailyIncome()[EGameResID::GOLD] + 1.0f)); - turnVariable->setValue(evaluationContext.turn); - fearVariable->setValue(evaluationContext.enemyHeroDangerRatio); - - engine->process(); - - fuzzyResult = value->getValue(); - } - catch (fl::Exception& fe) - { - logAi->error("evaluate VisitTile: %s", fe.getWhat()); - } if (ai->settings->isUseFuzzy()) { + float fuzzyResult = 0; + try + { + armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage); + heroRoleVariable->setValue(evaluationContext.heroRole); + mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]); + scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]); + goldRewardVariable->setValue(goldRewardPerTurn); + 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); + goldPressureVariable->setValue(ai->buildAnalyzer->getGoldPressure()); + goldCostVariable->setValue(evaluationContext.goldCost / ((float)ai->getFreeResources()[EGameResID::GOLD] + (float)ai->buildAnalyzer->getDailyIncome()[EGameResID::GOLD] + 1.0f)); + 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 @@ -1520,7 +1520,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) auto resourcesAvailable = evaluationContext.evaluator.ai->getFreeResources(); auto income = ai->buildAnalyzer->getDailyIncome(); score /= evaluationContext.buildingCost.marketValue(); - if (resourcesAvailable < evaluationContext.buildingCost) + if (resourcesAvailable.canAfford(evaluationContext.buildingCost)) { TResources needed = evaluationContext.buildingCost - resourcesAvailable; needed.positive(); diff --git a/lib/ResourceSet.h b/lib/ResourceSet.h index c080d1dff..0e133890b 100644 --- a/lib/ResourceSet.h +++ b/lib/ResourceSet.h @@ -189,17 +189,6 @@ public: return this->container == rhs.container; } -// WARNING: comparison operators are used for "can afford" relation: a <= b means that foreach i a[i] <= b[i] -// that doesn't work the other way: a > b doesn't mean that a cannot be afforded with b, it's still b can afford a - bool operator<(const ResourceSet &rhs) - { - for(int i = 0; i < size(); i++) - if (this->container.at(i) < rhs[i]) - return true; - - return false; - } - template void serialize(Handler &h) { h & container; From 79531c859626938ff7d24e3659e8c39e44763f73 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 5 Sep 2024 16:30:15 +0200 Subject: [PATCH 107/186] Update ResourceSet.h Use a more descriptive method name and add comment of what it does. --- lib/ResourceSet.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/ResourceSet.h b/lib/ResourceSet.h index b740d5e70..f9dd6abaa 100644 --- a/lib/ResourceSet.h +++ b/lib/ResourceSet.h @@ -148,7 +148,8 @@ public: return ret; } - int div(const ResourceSet& income) { + //Returns how many items of "this" we can afford with provided income + int maxPurchasableCount(const ResourceSet& income) { int ret = 0; // Initialize to 0 because we want the maximum number of accumulations for (size_t i = 0; i < container.size(); ++i) { From 044dc272c2bdc6c5a61a2c7b8133a9bf5398cb6b Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 5 Sep 2024 16:31:44 +0200 Subject: [PATCH 108/186] Update lib/CCreatureSet.h Use a better name and add a comment to explain what it does. Co-authored-by: Ivan Savenko --- lib/CCreatureSet.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/CCreatureSet.h b/lib/CCreatureSet.h index cb2f0afca..82ec1083d 100644 --- a/lib/CCreatureSet.h +++ b/lib/CCreatureSet.h @@ -109,7 +109,8 @@ public: FactionID getFaction() const override; virtual ui64 getPower() const; - virtual ui64 getCost() const; + /// Returns total market value of resources needed to recruit this unit + virtual ui64 getMarketValue() const; CCreature::CreatureQuantityId getQuantityID() const; std::string getQuantityTXT(bool capitalized = true) const; virtual int getExpRank() const; From 20cfe712c9a5cd1d162eb712e625bf8e89956343 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 5 Sep 2024 16:34:42 +0200 Subject: [PATCH 109/186] Update ResourceSet.h Rename income to availableFunds --- lib/ResourceSet.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/ResourceSet.h b/lib/ResourceSet.h index f9dd6abaa..9c33db5fc 100644 --- a/lib/ResourceSet.h +++ b/lib/ResourceSet.h @@ -148,19 +148,19 @@ public: return ret; } - //Returns how many items of "this" we can afford with provided income - int maxPurchasableCount(const ResourceSet& income) { + //Returns how many items of "this" we can afford with provided funds + int maxPurchasableCount(const ResourceSet& availableFunds) { int ret = 0; // Initialize to 0 because we want the maximum number of accumulations for (size_t i = 0; i < container.size(); ++i) { if (container.at(i) > 0) { // We only care about fulfilling positive needs - if (income[i] == 0) { + if (availableFunds[i] == 0) { // If income is 0 and we need a positive amount, it's impossible to fulfill return INT_MAX; } else { // Calculate the number of times we need to accumulate income to fulfill the need - int ceiledResult = vstd::divideAndCeil(container.at(i), income[i]); + int ceiledResult = vstd::divideAndCeil(container.at(i), availableFunds[i]); ret = std::max(ret, ceiledResult); } } From c186de2d521a038abc5c2390b9b00113b3d77457 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 5 Sep 2024 16:36:07 +0200 Subject: [PATCH 110/186] Update AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp Avoid checking float against an exact value. Co-authored-by: Ivan Savenko --- AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp index f183a11a0..00d6a4e26 100644 --- a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp +++ b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp @@ -87,7 +87,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const score *= hero->getArmyCost(); if (hero->type->heroClass->faction == town->getFaction()) score *= 1.5; - if (visitability == 0) + if (vstd::isAlmostZero(visitability)) score *= 30 * town->getTownLevel(); else score *= town->getTownLevel() / visitability; From dafc9cd8a898d7668a444906467cf2bd709da9e2 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 5 Sep 2024 16:40:06 +0200 Subject: [PATCH 111/186] Update PriorityEvaluator.cpp Replace float-comparisons with zero by vstd::isAlmostZero --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index b04ae252e..40b3f1943 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -1397,7 +1397,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) return 0; if(evaluationContext.conquestValue > 0) score = 1000; - if (score == 0 || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty())) + if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty())) return 0; if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) return 0; @@ -1417,7 +1417,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) { if (evaluationContext.conquestValue > 0) score = 1000; - if (score == 0 || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty())) + if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty())) return 0; if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) return 0; @@ -1524,7 +1524,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) { TResources needed = evaluationContext.buildingCost - resourcesAvailable; needed.positive(); - int turnsTo = needed.div(income); + int turnsTo = needed.maxPurchasableCount(income); if (turnsTo == INT_MAX) return 0; else @@ -1533,7 +1533,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) } else { - if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend && evaluationContext.conquestValue == 0) + if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend && vstd::isAlmostZero(evaluationContext.conquestValue)) return 0; } break; From b3115f65c5b1c50feaf3b675166bcbf22d58fbfb Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 5 Sep 2024 16:45:45 +0200 Subject: [PATCH 112/186] Update BuildingBehavior.cpp Use std::numeric_limits::max(); instead of UINT8_MAX; and remove some leftover-trace-messages from debugging. --- AI/Nullkiller/Behaviors/BuildingBehavior.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/AI/Nullkiller/Behaviors/BuildingBehavior.cpp b/AI/Nullkiller/Behaviors/BuildingBehavior.cpp index caed8fc1e..ab33207df 100644 --- a/AI/Nullkiller/Behaviors/BuildingBehavior.cpp +++ b/AI/Nullkiller/Behaviors/BuildingBehavior.cpp @@ -54,7 +54,7 @@ Goals::TGoalVec BuildingBehavior::decompose(const Nullkiller * ai) const for(auto & developmentInfo : developmentInfos) { bool emergencyDefense = false; - uint8_t closestThreat = UINT8_MAX; + uint8_t closestThreat = std::numeric_limits::max(); for (auto threat : ai->dangerHitMap->getTownThreats(developmentInfo.town)) { closestThreat = std::min(closestThreat, threat.turn); @@ -74,7 +74,6 @@ Goals::TGoalVec BuildingBehavior::decompose(const Nullkiller * ai) const { for (auto& buildingInfo : developmentInfo.toBuild) { - logAi->trace("Looking at %s", buildingInfo.toString()); if (isGoldPressureLow || buildingInfo.dailyIncome[EGameResID::GOLD] > 0) { if (buildingInfo.notEnoughRes) @@ -86,13 +85,11 @@ Goals::TGoalVec BuildingBehavior::decompose(const Nullkiller * ai) const composition.addNext(BuildThis(buildingInfo, developmentInfo)); composition.addNext(SaveResources(buildingInfo.buildCost)); - logAi->trace("Generate task to build: %s", buildingInfo.toString()); tasks.push_back(sptr(composition)); } else { tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo))); - logAi->trace("Generate task to build: %s", buildingInfo.toString()); } } } From d9fe8d7fa0099387382a64333dcb712077e9c1f4 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 5 Sep 2024 16:50:22 +0200 Subject: [PATCH 113/186] Update BuyArmyBehavior.cpp Removed pointless check for hero-army being more valuable than buying army directly as it was never the case anyways. --- AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp b/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp index 3d15ff9e8..99c362bac 100644 --- a/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp @@ -39,17 +39,6 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const for(auto town : cb->getTownsInfo()) { - //If we can recruit a hero that comes with more army than he costs, we are better off spending our gold on them - if (ai->heroManager->canRecruitHero(town)) - { - auto availableHeroes = ai->cb->getAvailableHeroes(town); - for (auto hero : availableHeroes) - { - if (hero->getArmyCost() > GameConstants::HERO_GOLD_COST) - return tasks; - } - } - uint8_t closestThreat = UINT8_MAX; for (auto threat : ai->dangerHitMap->getTownThreats(town)) { From 7c42e43fe516e9c32f4a898fa7da7584c347e67c Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 5 Sep 2024 17:16:06 +0200 Subject: [PATCH 114/186] Update CCreatureSet.cpp Use getMarketValue instead of getCost. --- lib/CCreatureSet.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/CCreatureSet.cpp b/lib/CCreatureSet.cpp index 45011035e..b340e15e4 100644 --- a/lib/CCreatureSet.cpp +++ b/lib/CCreatureSet.cpp @@ -370,7 +370,7 @@ ui64 CCreatureSet::getArmyCost() const { ui64 ret = 0; for (const auto& elem : stacks) - ret += elem.second->getCost(); + ret += elem.second->getMarketValue(); return ret; } @@ -866,7 +866,7 @@ ui64 CStackInstance::getPower() const return type->getAIValue() * count; } -ui64 CStackInstance::getCost() const +ui64 CStackInstance::getMarketValue() const { assert(type); return type->getFullRecruitCost().marketValue() * count; From 581a142a204f7ce27fddf5f1c510db3541796163 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 5 Sep 2024 17:38:27 +0200 Subject: [PATCH 115/186] HeroStrengthForCampaign Make sure to take our magic-specialist to the next campaign-mission even if he's totally out of mana. --- lib/campaign/CampaignState.cpp | 2 +- lib/mapObjects/CGHeroInstance.cpp | 10 ++++++++++ lib/mapObjects/CGHeroInstance.h | 4 +++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/campaign/CampaignState.cpp b/lib/campaign/CampaignState.cpp index f1fa305cb..367a00e0e 100644 --- a/lib/campaign/CampaignState.cpp +++ b/lib/campaign/CampaignState.cpp @@ -325,7 +325,7 @@ void CampaignState::setCurrentMapAsConquered(std::vector heroe { range::sort(heroes, [](const CGHeroInstance * a, const CGHeroInstance * b) { - return a->getHeroStrength() > b->getHeroStrength(); + return a->getHeroStrengthForCampaign() > b->getHeroStrengthForCampaign(); }); logGlobal->info("Scenario %d of campaign %s (%s) has been completed", currentMap->getNum(), getFilename(), getNameTranslated()); diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 2eeeafdb9..b470517bd 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -663,11 +663,21 @@ double CGHeroInstance::getMagicStrength() const return sqrt((1.0 + 0.05*getPrimSkillLevel(PrimarySkill::KNOWLEDGE) * mana / manaLimit()) * (1.0 + 0.05*getPrimSkillLevel(PrimarySkill::SPELL_POWER) * mana / manaLimit())); } +double CGHeroInstance::getMagicStrengthForCampaign() const +{ + return sqrt((1.0 + 0.05 * getPrimSkillLevel(PrimarySkill::KNOWLEDGE)) * (1.0 + 0.05 * getPrimSkillLevel(PrimarySkill::SPELL_POWER))); +} + double CGHeroInstance::getHeroStrength() const { return sqrt(pow(getFightingStrength(), 2.0) * pow(getMagicStrength(), 2.0)); } +double CGHeroInstance::getHeroStrengthForCampaign() const +{ + return sqrt(pow(getFightingStrength(), 2.0) * pow(getMagicStrengthForCampaign(), 2.0)); +} + ui64 CGHeroInstance::getTotalStrength() const { double ret = getHeroStrength() * getArmyStrength(); diff --git a/lib/mapObjects/CGHeroInstance.h b/lib/mapObjects/CGHeroInstance.h index 2ad2811a9..da4563247 100644 --- a/lib/mapObjects/CGHeroInstance.h +++ b/lib/mapObjects/CGHeroInstance.h @@ -224,8 +224,10 @@ public: int movementPointsAfterEmbark(int MPsBefore, int basicCost, bool disembark = false, const TurnInfo * ti = nullptr) const; double getFightingStrength() const; // takes attack / defense skill into account - double getMagicStrength() const; // takes knowledge / spell power skill into account + double getMagicStrength() const; // takes knowledge / spell power skill but also current mana, whether the hero owns a spell-book and whether that books contains anything into account + double getMagicStrengthForCampaign() const; // takes knowledge / spell power skill into account double getHeroStrength() const; // includes fighting and magic strength + double getHeroStrengthForCampaign() const; // includes fighting and the for-campaign-version of magic strength ui64 getTotalStrength() const; // includes fighting strength and army strength TExpType calculateXp(TExpType exp) const; //apply learning skill From 5488a0a29c2b5e1c75c7807b0c2417e77691cf2c Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 5 Sep 2024 19:35:47 +0200 Subject: [PATCH 116/186] Removed the "GATHER"-priorityTier There was no real need for it to be a separated tier from Hunter_gather. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 30 ---------------------- AI/Nullkiller/Engine/PriorityEvaluator.h | 1 - 2 files changed, 31 deletions(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 40b3f1943..0a4d53b30 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -1426,36 +1426,6 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) score /= evaluationContext.movementCost; break; } - case PriorityTier::GATHER: //Collect unguarded stuff - { - if (evaluationContext.enemyHeroDangerRatio > 1) - return 0; - if (evaluationContext.isDefend) - return 0; - if (evaluationContext.armyLossPersentage > 0) - return 0; - if (evaluationContext.involvesSailing && evaluationContext.movementCostByRole[HeroRole::MAIN] > 0) - return 0; - if (evaluationContext.buildingCost.marketValue() > 0) - return 0; - if (evaluationContext.closestWayRatio < 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; - 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 PriorityTier::HUNTER_GATHER: //Collect guarded stuff { if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index 5e8e8c365..15d06ef05 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -110,7 +110,6 @@ public: INSTAKILL, INSTADEFEND, KILL, - GATHER, HUNTER_GATHER, DEFEND }; From db2416cb6b0d257e0c1396527859f5511a8fb07d Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 5 Sep 2024 23:41:05 +0200 Subject: [PATCH 117/186] Update Nullkiller.cpp Readded prioOfTask because it's needed in trace-messages. --- AI/Nullkiller/Engine/Nullkiller.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 77cac935a..46d7825ab 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -410,8 +410,10 @@ void Nullkiller::makeTurn() decompose(bestTasks, sptr(ExplorationBehavior()), MAX_DEPTH); TTaskVec selectedTasks; + int prioOfTask = 0; for (int prio = PriorityEvaluator::PriorityTier::INSTAKILL; prio <= PriorityEvaluator::PriorityTier::DEFEND; ++prio) { + prioOfTask = prio; selectedTasks = buildPlan(bestTasks, prio); if (!selectedTasks.empty() || settings->isUseFuzzy()) break; From e43492d8b586fac729fa397970cbe5414855066b Mon Sep 17 00:00:00 2001 From: Xilmi Date: Fri, 6 Sep 2024 00:12:44 +0200 Subject: [PATCH 118/186] Update PriorityEvaluator.cpp Fixed affordabilitycheck not being negated. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 0a4d53b30..a81f300ba 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -1490,7 +1490,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) auto resourcesAvailable = evaluationContext.evaluator.ai->getFreeResources(); auto income = ai->buildAnalyzer->getDailyIncome(); score /= evaluationContext.buildingCost.marketValue(); - if (resourcesAvailable.canAfford(evaluationContext.buildingCost)) + if (!resourcesAvailable.canAfford(evaluationContext.buildingCost)) { TResources needed = evaluationContext.buildingCost - resourcesAvailable; needed.positive(); From 35d8705fea1dccb4d2f6d13555c89f1fed63b03e Mon Sep 17 00:00:00 2001 From: Xilmi Date: Fri, 6 Sep 2024 17:20:12 +0200 Subject: [PATCH 119/186] Update Nullkiller.cpp prioOfTask-variable-usage bound to trace-level as otherwise a warning will ensue. --- AI/Nullkiller/Engine/Nullkiller.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 46d7825ab..c7f408a51 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -410,10 +410,14 @@ void Nullkiller::makeTurn() decompose(bestTasks, sptr(ExplorationBehavior()), MAX_DEPTH); TTaskVec selectedTasks; +#if NKAI_TRACE_LEVEL >= 1 int prioOfTask = 0; +#endif for (int prio = PriorityEvaluator::PriorityTier::INSTAKILL; prio <= PriorityEvaluator::PriorityTier::DEFEND; ++prio) { +#if NKAI_TRACE_LEVEL >= 1 prioOfTask = prio; +#endif selectedTasks = buildPlan(bestTasks, prio); if (!selectedTasks.empty() || settings->isUseFuzzy()) break; From cf338e04ad45fc46bbc1f26b1bade81c9d83d8ba Mon Sep 17 00:00:00 2001 From: Xilmi Date: Fri, 6 Sep 2024 21:40:23 +0200 Subject: [PATCH 120/186] Update Nullkiller.cpp AI can now also buy resources that it has income for. --- AI/Nullkiller/Engine/Nullkiller.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index c7f408a51..d12b6193c 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -635,8 +635,6 @@ bool Nullkiller::handleTrading() { if (required[i] <= 0) continue; - if (i != 6 && income[i] > 0) - continue; float ratio = static_cast(available[i]) / required[i]; if (ratio < minRatio) { From 06f894140cae3e37bd48cfacd0be815d99616bab Mon Sep 17 00:00:00 2001 From: Xilmi Date: Fri, 6 Sep 2024 21:42:15 +0200 Subject: [PATCH 121/186] Update BuildAnalyzer.cpp Modified goldPressure-formula to no longer use completely arbitrary part of lockedresources/5000. Lockedresources is now just divided by a factor of the free gold like everything else. --- AI/Nullkiller/Analyzers/BuildAnalyzer.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp index 49ae295ca..ce9c00f36 100644 --- a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp @@ -184,8 +184,7 @@ void BuildAnalyzer::update() updateDailyIncome(); - goldPressure = ai->getLockedResources()[EGameResID::GOLD] / 5000.0f - + ((float)armyCost[EGameResID::GOLD] + economyDevelopmentCost) / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f); + goldPressure = (ai->getLockedResources()[EGameResID::GOLD] + (float)armyCost[EGameResID::GOLD] + economyDevelopmentCost) / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f); logAi->trace("Gold pressure: %f", goldPressure); } From 099341e143496e9247c67c270f04db509d43a0b0 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Fri, 6 Sep 2024 22:10:14 +0200 Subject: [PATCH 122/186] Update Nullkiller.cpp Fixed incorrect trace-message at end of turn. --- AI/Nullkiller/Engine/Nullkiller.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index d12b6193c..994e06415 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -511,6 +511,8 @@ void Nullkiller::makeTurn() { logAi->trace("Nothing was done this turn. Ending turn."); #if NKAI_TRACE_LEVEL >= 1 + totalHeroStrength = 0; + totalTownLevel = 0; for (auto heroInfo : cb->getHeroesInfo()) { totalHeroStrength += heroInfo->getTotalStrength(); From 0edc17b7d878d86aff7aeead6ef5bf9db519286c Mon Sep 17 00:00:00 2001 From: Xilmi Date: Fri, 6 Sep 2024 22:14:59 +0200 Subject: [PATCH 123/186] Going to town when nothing to do. The StayAtTown-behavior now always creates tasks for all heroes to go and stay at a town. It will be treated differently than going to a town for mana in the sense that it is only considered at the lowest priority-tier. So it will only happen when the AI doesn't find anything else to do. It should resolve one of the two main-reasons for losing weak heros. The hunter-gather-priority-tier now goes strictly by distance for all taks that are considered above 0 in value. --- AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp | 11 +---------- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 13 +++++++------ AI/Nullkiller/Goals/StayAtTown.cpp | 5 ----- 3 files changed, 8 insertions(+), 21 deletions(-) diff --git a/AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp b/AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp index 595830a66..1b2e0a04b 100644 --- a/AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp +++ b/AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp @@ -39,9 +39,6 @@ Goals::TGoalVec StayAtTownBehavior::decompose(const Nullkiller * ai) const for(auto town : towns) { - if(!town->hasBuilt(BuildingID::MAGES_GUILD_1)) - continue; - ai->pathfinder->calculatePathInfo(paths, town->visitablePos()); for(auto & path : paths) @@ -49,14 +46,8 @@ Goals::TGoalVec StayAtTownBehavior::decompose(const Nullkiller * ai) const if(town->visitingHero && town->visitingHero.get() != path.targetHero) continue; - if(!path.targetHero->hasSpellbook() || path.targetHero->mana >= 0.75f * path.targetHero->manaLimit()) - continue; - - if(path.turn() == 0 && !path.getFirstBlockedAction() && path.exchangeCount <= 1) + if(!path.getFirstBlockedAction() && path.exchangeCount <= 1) { - if(path.targetHero->mana == path.targetHero->manaLimit()) - continue; - Composition stayAtTown; stayAtTown.addNextSequence({ diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index a81f300ba..8b6246eb9 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -944,6 +944,8 @@ public: Goals::StayAtTown & stayAtTown = dynamic_cast(*task); evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero()); + if (evaluationContext.armyReward == 0) + evaluationContext.isDefend = true; evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted(); evaluationContext.movementCost += stayAtTown.getMovementWasted(); } @@ -1365,7 +1367,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) float score = 0; float maxWillingToLose = ai->cb->getTownsInfo().empty() ? 1 : 0.25; #if NKAI_TRACE_LEVEL >= 2 - logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %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, isDefend: %d, fuzzy: %f", + logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %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, isDefend: %d", priorityTier, task->toString(), evaluationContext.armyLossPersentage, @@ -1385,8 +1387,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) evaluationContext.conquestValue, evaluationContext.closestWayRatio, evaluationContext.enemyHeroDangerRatio, - evaluationContext.isDefend, - fuzzyResult); + evaluationContext.isDefend); #endif switch (priorityTier) @@ -1443,6 +1444,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) score -= evaluationContext.armyInvolvement * evaluationContext.armyLossPersentage; if (score > 0) { + score = 1000; score *= evaluationContext.closestWayRatio; if (evaluationContext.enemyHeroDangerRatio > 1) score /= evaluationContext.enemyHeroDangerRatio; @@ -1457,7 +1459,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) if (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.isExchange) return 0; if (evaluationContext.isDefend) - score = evaluationContext.armyInvolvement; + score = 1000; score *= evaluationContext.closestWayRatio; score /= (evaluationContext.turn + 1); break; @@ -1516,7 +1518,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) } #if NKAI_TRACE_LEVEL >= 2 - logAi->trace("priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %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, fuzzy: %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, 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(), evaluationContext.armyLossPersentage, @@ -1536,7 +1538,6 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) evaluationContext.conquestValue, evaluationContext.closestWayRatio, evaluationContext.enemyHeroDangerRatio, - fuzzyResult, result); #endif diff --git a/AI/Nullkiller/Goals/StayAtTown.cpp b/AI/Nullkiller/Goals/StayAtTown.cpp index 346b2c44d..250e16371 100644 --- a/AI/Nullkiller/Goals/StayAtTown.cpp +++ b/AI/Nullkiller/Goals/StayAtTown.cpp @@ -41,11 +41,6 @@ std::string StayAtTown::toString() const void StayAtTown::accept(AIGateway * ai) { - if(hero->visitedTown != town) - { - logAi->error("Hero %s expected visiting town %s", hero->getNameTranslated(), town->getNameTranslated()); - } - ai->nullkiller->lockHero(hero, HeroLockedReason::DEFENCE); } From 8c3f6fc1e2cd569ed043c5ee261e992bad99234a Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 8 Sep 2024 02:19:19 +0200 Subject: [PATCH 124/186] Update RecruitHeroBehavior.cpp Fixed crash caused by mistakenly assuming that "pos" is the position of a hero on the map and not its bottom-right-corner that can be outside of the map. --- AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp index 00d6a4e26..91f4f03f4 100644 --- a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp +++ b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp @@ -70,7 +70,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const float visitability = 0; for (auto checkHero : ourHeroes) { - if (ai->dangerHitMap->getClosestTown(checkHero.first.get()->pos) == town) + if (ai->dangerHitMap->getClosestTown(checkHero.first.get()->visitablePos()) == town) visitability++; } if(ai->heroManager->canRecruitHero(town)) From 5999c6d891525125e321096e712364903127a855 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 9 Sep 2024 19:54:20 +0200 Subject: [PATCH 125/186] Update BattleEvaluator.cpp Removed now unnecessary additional check for dead units. --- AI/BattleAI/BattleEvaluator.cpp | 54 --------------------------------- 1 file changed, 54 deletions(-) diff --git a/AI/BattleAI/BattleEvaluator.cpp b/AI/BattleAI/BattleEvaluator.cpp index 6422b43e2..ea5d0e7a3 100644 --- a/AI/BattleAI/BattleEvaluator.cpp +++ b/AI/BattleAI/BattleEvaluator.cpp @@ -729,60 +729,6 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) ps.value = scoreEvaluator.evaluateExchange(updatedAttack, cachedAttack.turn, *targets, innerCache, state); } - //! Some units may be dead alltogether. So if they existed before but not now, we know they were killed by the spell - for (const auto& unit : all) - { - if (!unit->isValidTarget()) - continue; - bool isDead = true; - for (const auto& remainingUnit : allUnits) - { - if (remainingUnit->unitId() == unit->unitId()) - isDead = false; - } - if (isDead) - { - auto newHealth = 0; - auto oldHealth = vstd::find_or(healthOfStack, unit->unitId(), 0); - if (oldHealth != newHealth) - { - auto damage = std::abs(oldHealth - newHealth); - auto originalDefender = cb->getBattle(battleID)->battleGetUnitByID(unit->unitId()); - - auto dpsReduce = AttackPossibility::calculateDamageReduce( - nullptr, - originalDefender && originalDefender->alive() ? originalDefender : unit, - damage, - innerCache, - state); - - auto ourUnit = unit->unitSide() == side ? 1 : -1; - auto goodEffect = newHealth > oldHealth ? 1 : -1; - - if (ourUnit * goodEffect == 1) - { - if (ourUnit && goodEffect && (unit->isClone() || unit->isGhost())) - continue; - - ps.value += dpsReduce * scoreEvaluator.getPositiveEffectMultiplier(); - } - else - ps.value -= dpsReduce * scoreEvaluator.getNegativeEffectMultiplier(); - -#if BATTLE_TRACE_LEVEL >= 1 - logAi->trace( - "Spell %s to %d affects %s (%d), dps: %2f oldHealth: %d newHealth: %d", - ps.spell->getNameTranslated(), - ps.dest.at(0).hexValue.hex, - unit->creatureId().toCreature()->getNameSingularTranslated(), - unit->getCount(), - dpsReduce, - oldHealth, - newHealth); -#endif - } - } - } for(const auto & unit : allUnits) { if(!unit->isValidTarget(true)) From e7e3f6dcbe0049a2ced37889bebf779557d17cca Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 9 Sep 2024 19:55:03 +0200 Subject: [PATCH 126/186] Update DefenceBehavior.cpp Only hire heroes for defence if the enemy is already really close. (Otherwise AI hired too many heroes from defensebehavior) --- AI/Nullkiller/Behaviors/DefenceBehavior.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp index c0dacd8f4..b5e0ab8d0 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp @@ -404,6 +404,9 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & threat, const CGTownInstance * town, const Nullkiller * ai) const { + if (threat.turn > 0) + return; + if(town->hasBuilt(BuildingID::TAVERN) && ai->cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST) { From f8e4aa1d25d92a1eebd89098cc6a919d123374f0 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 9 Sep 2024 23:20:53 +0200 Subject: [PATCH 127/186] Update Nullkiller.cpp Use Enum for Gold. --- AI/Nullkiller/Engine/Nullkiller.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 994e06415..e136699e4 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -655,7 +655,7 @@ bool Nullkiller::handleTrading() bool okToSell = false; - if (i == 6) + if (i == GameResID::GOLD) { if (income[i] > 0 && !buildAnalyzer->isGoldPressureHigh()) okToSell = true; From a329f607c93844e641fa77cc8bfe3d1d605bd03d Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 9 Sep 2024 23:23:28 +0200 Subject: [PATCH 128/186] Update Nullkiller.cpp No more map-hack on 3rd difficulty-level. Only starting from the fourth. --- AI/Nullkiller/Engine/Nullkiller.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index e136699e4..39054c75d 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -62,7 +62,7 @@ bool canUseOpenMap(std::shared_ptr cb, PlayerColor playerID) return false; } - return true; + return cb->getStartInfo()->difficulty >= 3; } void Nullkiller::init(std::shared_ptr cb, AIGateway * gateway) From faa5a02659ca8b9eed88353523283a3a4d77bc82 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 9 Sep 2024 23:25:09 +0200 Subject: [PATCH 129/186] Update RecruitHeroBehavior.cpp Fix potential division by zero. --- AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp index 91f4f03f4..0be723a2a 100644 --- a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp +++ b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp @@ -107,7 +107,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const if (ai->cb->getHeroesInfo().size() < ai->cb->getTownsInfo().size() + 1 || (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh() && haveCapitol)) { - tasks.push_back(Goals::sptr(Goals::RecruitHero(bestTownToHireFrom, bestHeroToHire).setpriority((float)3 / ourHeroes.size()))); + tasks.push_back(Goals::sptr(Goals::RecruitHero(bestTownToHireFrom, bestHeroToHire).setpriority((float)3 / (ourHeroes.size() + 1)))); } } From 37f9f939485816a81624da03a13028b65adb7306 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 9 Sep 2024 23:38:28 +0200 Subject: [PATCH 130/186] Update RecruitHeroBehavior.cpp Modified how to score what hero to hire to make it more likely to rehire fled heroes with high levels when the army-gain from the hero would be rather insignificant. --- AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp index 0be723a2a..e46483fca 100644 --- a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp +++ b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp @@ -32,9 +32,11 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const auto ourHeroes = ai->heroManager->getHeroRoles(); auto minScoreToHireMain = std::numeric_limits::max(); + int currentArmyValue = 0; for(auto hero : ourHeroes) { + currentArmyValue += hero.first->getArmyCost(); if(hero.second != HeroRole::MAIN) continue; @@ -84,7 +86,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const { score *= score / minScoreToHireMain; } - score *= hero->getArmyCost(); + score *= (hero->getArmyCost() + currentArmyValue); if (hero->type->heroClass->faction == town->getFaction()) score *= 1.5; if (vstd::isAlmostZero(visitability)) From df119370c7057f3b4e088074e76ca6a6949025be Mon Sep 17 00:00:00 2001 From: Xilmi Date: Tue, 10 Sep 2024 00:23:17 +0200 Subject: [PATCH 131/186] Exploration Slightly adjust the value of exploring within the hunter-gather-prirority. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 7 +++++-- AI/Nullkiller/Engine/PriorityEvaluator.h | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 8b6246eb9..07f26f55a 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -62,7 +62,8 @@ EvaluationContext::EvaluationContext(const Nullkiller* ai) threatTurns(INT_MAX), involvesSailing(false), isTradeBuilding(false), - isExchange(false) + isExchange(false), + isExplore(false) { } @@ -930,6 +931,7 @@ public: int tilesDiscovered = task->value; evaluationContext.addNonCriticalStrategicalValue(0.03f * tilesDiscovered); + evaluationContext.isExplore = true; } }; @@ -1444,7 +1446,8 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) score -= evaluationContext.armyInvolvement * evaluationContext.armyLossPersentage; if (score > 0) { - score = 1000; + if(!evaluationContext.isExplore) + score = 1000; score *= evaluationContext.closestWayRatio; if (evaluationContext.enemyHeroDangerRatio > 1) score /= evaluationContext.enemyHeroDangerRatio; diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index 15d06ef05..c04e2477d 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -80,6 +80,7 @@ struct DLL_EXPORT EvaluationContext bool involvesSailing; bool isTradeBuilding; bool isExchange; + bool isExplore; EvaluationContext(const Nullkiller * ai); From aefe2fda3652ff0318d3fecb4bfa641e70c128bc Mon Sep 17 00:00:00 2001 From: Xilmi Date: Tue, 10 Sep 2024 23:42:51 +0200 Subject: [PATCH 132/186] Update BuildingBehavior.cpp No longer rush a fort in a threatened town. --- AI/Nullkiller/Behaviors/BuildingBehavior.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/Nullkiller/Behaviors/BuildingBehavior.cpp b/AI/Nullkiller/Behaviors/BuildingBehavior.cpp index ab33207df..50f800913 100644 --- a/AI/Nullkiller/Behaviors/BuildingBehavior.cpp +++ b/AI/Nullkiller/Behaviors/BuildingBehavior.cpp @@ -63,7 +63,7 @@ Goals::TGoalVec BuildingBehavior::decompose(const Nullkiller * ai) const { if (closestThreat <= 1 && developmentInfo.town->fortLevel() < CGTownInstance::EFortLevel::CASTLE && !buildingInfo.notEnoughRes) { - if (buildingInfo.id == BuildingID::FORT || buildingInfo.id == BuildingID::CITADEL || buildingInfo.id == BuildingID::CASTLE) + if (buildingInfo.id == BuildingID::CITADEL || buildingInfo.id == BuildingID::CASTLE) { tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo))); emergencyDefense = true; From d4fd4ed670eb2b4dd4860c1fe87e1670113ff47f Mon Sep 17 00:00:00 2001 From: Xilmi Date: Wed, 11 Sep 2024 16:05:53 +0200 Subject: [PATCH 133/186] Update BattleEvaluator.cpp Make sure trace-message doesn't crash from accessing invalid element. --- AI/BattleAI/BattleEvaluator.cpp | 34 ++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/AI/BattleAI/BattleEvaluator.cpp b/AI/BattleAI/BattleEvaluator.cpp index ea5d0e7a3..d59324880 100644 --- a/AI/BattleAI/BattleEvaluator.cpp +++ b/AI/BattleAI/BattleEvaluator.cpp @@ -768,15 +768,31 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) ps.value -= 4 * dpsReduce * scoreEvaluator.getNegativeEffectMultiplier(); #if BATTLE_TRACE_LEVEL >= 1 - logAi->trace( - "Spell %s to %d affects %s (%d), dps: %2f oldHealth: %d newHealth: %d", - ps.spell->getNameTranslated(), - ps.dest.at(0).hexValue.hex, - unit->creatureId().toCreature()->getNameSingularTranslated(), - unit->getCount(), - dpsReduce, - oldHealth, - newHealth); + // Ensure ps.dest is not empty before accessing the first element + if (!ps.dest.empty()) + { + logAi->trace( + "Spell %s to %d affects %s (%d), dps: %2f oldHealth: %d newHealth: %d", + ps.spell->getNameTranslated(), + ps.dest.at(0).hexValue.hex, // Safe to access .at(0) now + unit->creatureId().toCreature()->getNameSingularTranslated(), + unit->getCount(), + dpsReduce, + oldHealth, + newHealth); + } + else + { + // Handle the case where ps.dest is empty + logAi->trace( + "Spell %s has no destination, affects %s (%d), dps: %2f oldHealth: %d newHealth: %d", + ps.spell->getNameTranslated(), + unit->creatureId().toCreature()->getNameSingularTranslated(), + unit->getCount(), + dpsReduce, + oldHealth, + newHealth); + } #endif } } From 5ed888b284ec9a19ae773056c5939e8ccac47a09 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 12 Sep 2024 20:07:22 +0200 Subject: [PATCH 134/186] Update BuyArmyBehavior.cpp Accomplish the same but with simpler code. --- AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp b/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp index 99c362bac..738196e2d 100644 --- a/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp @@ -39,11 +39,7 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const for(auto town : cb->getTownsInfo()) { - uint8_t closestThreat = UINT8_MAX; - for (auto threat : ai->dangerHitMap->getTownThreats(town)) - { - closestThreat = std::min(closestThreat, threat.turn); - } + uint8_t closestThreat = ai->dangerHitMap->getTileThreat(town->visitablePos()).fastestDanger.turn; if (closestThreat >=2 && ai->buildAnalyzer->isGoldPressureHigh() && !town->hasBuilt(BuildingID::CITY_HALL) && cb->canBuildStructure(town, BuildingID::CITY_HALL) != EBuildingState::FORBIDDEN) { From ab64edf7dda83f78006eccc327c93c659ac0c33f Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 12 Sep 2024 20:08:07 +0200 Subject: [PATCH 135/186] Update RecruitHero.cpp Remove code that, according to Ivan shouldn't do anything but cause errors. No noticable regressing in playing-strength was observed. --- AI/Nullkiller/Goals/RecruitHero.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/AI/Nullkiller/Goals/RecruitHero.cpp b/AI/Nullkiller/Goals/RecruitHero.cpp index 5ea60317f..810a6162c 100644 --- a/AI/Nullkiller/Goals/RecruitHero.cpp +++ b/AI/Nullkiller/Goals/RecruitHero.cpp @@ -75,9 +75,6 @@ void RecruitHero::accept(AIGateway * ai) ai->nullkiller->heroManager->update(); ai->nullkiller->objectClusterizer->reset(); } - - if(t->visitingHero) - ai->moveHeroToTile(t->visitablePos(), t->visitingHero.get()); } } From af2df5763f29ad12dc13fe57ef74b875ac01cdcd Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 12 Sep 2024 22:53:45 +0200 Subject: [PATCH 136/186] Update PriorityEvaluator.cpp Only if there is a high gold-pressure a buildings' cost will deter from its score. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 295428a06..b6ee0b112 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -1441,7 +1441,8 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) score += 1000; auto resourcesAvailable = evaluationContext.evaluator.ai->getFreeResources(); auto income = ai->buildAnalyzer->getDailyIncome(); - score /= evaluationContext.buildingCost.marketValue(); + if(ai->buildAnalyzer->isGoldPressureHigh()) + score /= evaluationContext.buildingCost.marketValue(); if (!resourcesAvailable.canAfford(evaluationContext.buildingCost)) { TResources needed = evaluationContext.buildingCost - resourcesAvailable; From ab441d8e677bb63052427b321fa21fd6437b30bd Mon Sep 17 00:00:00 2001 From: Xilmi Date: Fri, 13 Sep 2024 23:06:49 +0200 Subject: [PATCH 137/186] Update PriorityEvaluator.cpp AI now should no longer ignore spell-scrolls and artifacts of the treasure-class. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index b6ee0b112..f2882e124 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -261,6 +261,8 @@ static uint64_t evaluateArtifactArmyValue(const CArtifact * art) switch(art->aClass) { + case CArtifact::EartClass::ART_TREASURE: + //FALL_THROUGH case CArtifact::EartClass::ART_MINOR: classValue = 1000; break; @@ -299,6 +301,8 @@ uint64_t RewardEvaluator::getArmyReward( case Obj::CREATURE_GENERATOR3: case Obj::CREATURE_GENERATOR4: return getDwellingArmyValue(ai->cb.get(), target, checkGold); + case Obj::SPELL_SCROLL: + //FALL_THROUGH case Obj::ARTIFACT: return evaluateArtifactArmyValue(dynamic_cast(target)->storedArtifact->artType); case Obj::HERO: From 9b9a50c0aef5c6551d8c960e5d755ddc2449d86f Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sat, 14 Sep 2024 02:51:33 +0200 Subject: [PATCH 138/186] Update StayAtTown.cpp Showing mana-limit too for Stay At Town. --- AI/Nullkiller/Goals/StayAtTown.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AI/Nullkiller/Goals/StayAtTown.cpp b/AI/Nullkiller/Goals/StayAtTown.cpp index 250e16371..817ddbe1c 100644 --- a/AI/Nullkiller/Goals/StayAtTown.cpp +++ b/AI/Nullkiller/Goals/StayAtTown.cpp @@ -36,7 +36,8 @@ std::string StayAtTown::toString() const { return "Stay at town " + town->getNameTranslated() + " hero " + hero->getNameTranslated() - + ", mana: " + std::to_string(hero->mana); + + ", mana: " + std::to_string(hero->mana) + + " / " + std::to_string(hero->manaLimit()); } void StayAtTown::accept(AIGateway * ai) From 22222f0fba5a666c89da62a8c433f7999f49d4e7 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sat, 14 Sep 2024 02:58:23 +0200 Subject: [PATCH 139/186] Priorization-improvements Manarecoveryreward now uses float instead of unsigned int in order to avoid extremely high instead of negative scores when the hero has more mana than his mana-limit for example due to mana-vortex. Moved upgrading armies to a lower priority tier as otherwise the AI would go back to their cities all the time even though there were plenty of other things to do. Improved exploration logic by putting different kinds of exploration to different priority-tiers. Looking at the other side of a portal has high priority, visiting an observatory has medium priority and scouting by visiting nearby tiles has low priority. --- AI/Nullkiller/Engine/Nullkiller.cpp | 2 +- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 50 ++++++++++++++++++---- AI/Nullkiller/Engine/PriorityEvaluator.h | 6 ++- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 39054c75d..4a10d8b6c 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -387,7 +387,7 @@ void Nullkiller::makeTurn() if(bestTask->priority > 0) { #if NKAI_TRACE_LEVEL >= 1 - logAi->info("Pass %d: Performing task %s with prio: %d", i, bestTask->toString(), bestTask->priority); + logAi->info("Pass %d: Performing prio 0 task %s with prio: %d", i, bestTask->toString(), bestTask->priority); #endif if(!executeTask(bestTask)) return; diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index f2882e124..e6ad50e97 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -63,7 +63,8 @@ EvaluationContext::EvaluationContext(const Nullkiller* ai) involvesSailing(false), isTradeBuilding(false), isExchange(false), - isExplore(false) + isArmyUpgrade(false), + explorePriority(0) { } @@ -493,7 +494,7 @@ uint64_t RewardEvaluator::townArmyGrowth(const CGTownInstance * town) const return result; } -uint64_t RewardEvaluator::getManaRecoveryArmyReward(const CGHeroInstance * hero) const +float RewardEvaluator::getManaRecoveryArmyReward(const CGHeroInstance * hero) const { return ai->heroManager->getMagicStrength(hero) * 10000 * (1.0f - std::sqrt(static_cast(hero->mana) / hero->manaLimit())); } @@ -868,6 +869,7 @@ public: evaluationContext.armyReward += upgradeValue; evaluationContext.addNonCriticalStrategicalValue(upgradeValue / (float)armyUpgrade.hero->getArmyStrength()); + evaluationContext.isArmyUpgrade = true; } }; @@ -882,7 +884,24 @@ public: int tilesDiscovered = task->value; evaluationContext.addNonCriticalStrategicalValue(0.03f * tilesDiscovered); - evaluationContext.isExplore = true; + for (auto obj : evaluationContext.evaluator.ai->cb->getVisitableObjs(task->tile)) + { + switch (obj->ID.num) + { + case Obj::MONOLITH_ONE_WAY_ENTRANCE: + case Obj::MONOLITH_TWO_WAY: + case Obj::SUBTERRANEAN_GATE: + case Obj::WHIRLPOOL: + evaluationContext.explorePriority = 1; + break; + case Obj::REDWOOD_OBSERVATORY: + case Obj::PILLAR_OF_FIRE: + evaluationContext.explorePriority = 2; + break; + } + } + if (evaluationContext.explorePriority == 0) + evaluationContext.explorePriority = 3; } }; @@ -1320,7 +1339,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) float score = 0; float maxWillingToLose = ai->cb->getTownsInfo().empty() ? 1 : 0.25; #if NKAI_TRACE_LEVEL >= 2 - logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %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, isDefend: %d", + logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, explorePriority: %d isDefend: %d", priorityTier, task->toString(), evaluationContext.armyLossPersentage, @@ -1340,6 +1359,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) evaluationContext.conquestValue, evaluationContext.closestWayRatio, evaluationContext.enemyHeroDangerRatio, + evaluationContext.explorePriority, evaluationContext.isDefend); #endif @@ -1369,7 +1389,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) } case PriorityTier::KILL: //Take towns / kill heroes that are further away { - if (evaluationContext.conquestValue > 0) + if (evaluationContext.conquestValue > 0 || evaluationContext.explorePriority == 1) score = 1000; if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty())) return 0; @@ -1388,6 +1408,10 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) return 0; if (evaluationContext.isDefend && (evaluationContext.enemyHeroDangerRatio < 1 || evaluationContext.threatTurns > 0 || evaluationContext.turn > 0)) return 0; + if (evaluationContext.explorePriority == 3) + return 0; + if (evaluationContext.isArmyUpgrade) + return 0; score += evaluationContext.strategicalValue * 1000; score += evaluationContext.goldReward; score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05; @@ -1397,8 +1421,6 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) score -= evaluationContext.armyInvolvement * evaluationContext.armyLossPersentage; if (score > 0) { - if(!evaluationContext.isExplore) - score = 1000; score *= evaluationContext.closestWayRatio; if (evaluationContext.enemyHeroDangerRatio > 1) score /= evaluationContext.enemyHeroDangerRatio; @@ -1408,11 +1430,23 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) } break; } + case PriorityTier::LOW_PRIO_EXPLORE: + { + if (evaluationContext.enemyHeroDangerRatio > 1) + return 0; + if (evaluationContext.explorePriority != 3) + return 0; + score = 1000; + score *= evaluationContext.closestWayRatio; + if (evaluationContext.movementCost > 0) + score /= evaluationContext.movementCost; + break; + } case PriorityTier::DEFEND: //Defend whatever if nothing else is to do { if (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.isExchange) return 0; - if (evaluationContext.isDefend) + if (evaluationContext.isDefend || evaluationContext.isArmyUpgrade) score = 1000; score *= evaluationContext.closestWayRatio; score /= (evaluationContext.turn + 1); diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index c04e2477d..4978bd28f 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -49,7 +49,7 @@ public: uint64_t getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const; const HitMapInfo & getEnemyHeroDanger(const int3 & tile, uint8_t turn) const; uint64_t townArmyGrowth(const CGTownInstance * town) const; - uint64_t getManaRecoveryArmyReward(const CGHeroInstance * hero) const; + float getManaRecoveryArmyReward(const CGHeroInstance * hero) const; }; struct DLL_EXPORT EvaluationContext @@ -80,7 +80,8 @@ struct DLL_EXPORT EvaluationContext bool involvesSailing; bool isTradeBuilding; bool isExchange; - bool isExplore; + bool isArmyUpgrade; + int explorePriority; EvaluationContext(const Nullkiller * ai); @@ -112,6 +113,7 @@ public: INSTADEFEND, KILL, HUNTER_GATHER, + LOW_PRIO_EXPLORE, DEFEND }; From c88165f90050e54cee5c6540dca7a39977959c99 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sat, 14 Sep 2024 13:41:14 +0200 Subject: [PATCH 140/186] Update PriorityEvaluator.cpp Whirlpools are no longer explorePriority 1 as the AI would then try out all of it's 5 entries on both sides. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index e6ad50e97..c373dff6c 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -891,7 +891,6 @@ public: case Obj::MONOLITH_ONE_WAY_ENTRANCE: case Obj::MONOLITH_TWO_WAY: case Obj::SUBTERRANEAN_GATE: - case Obj::WHIRLPOOL: evaluationContext.explorePriority = 1; break; case Obj::REDWOOD_OBSERVATORY: From 1495ec56f693ae1b1660337c2b70bf7d79f137e4 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 15 Sep 2024 20:46:17 +0200 Subject: [PATCH 141/186] Update AINodeStorage.cpp The node of a disembark-action can no longer be part of a hero-chain since sea-to-land-trade isn't possible and landing first eats up all movement-points. --- AI/Nullkiller/Pathfinding/AINodeStorage.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index 3f67d4b15..132928a4e 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -721,6 +721,7 @@ void HeroChainCalculationTask::calculateHeroChain( if(node->action == EPathNodeAction::BATTLE || node->action == EPathNodeAction::TELEPORT_BATTLE || node->action == EPathNodeAction::TELEPORT_NORMAL + || node->action == EPathNodeAction::DISEMBARK || node->action == EPathNodeAction::TELEPORT_BLOCKING_VISIT) { continue; From 90d72a4458424b76645f19faa86628e6579ce5b4 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 22 Sep 2024 13:06:07 +0200 Subject: [PATCH 142/186] Chase & FFA-changes The AI should no longer chase enemy heroes that are not reachable in the same turn, when there's other options as this behavior was quite exploitable. The AI should now take their overall strength into account when deciding whether to attack or not. Previously it would attack as long as their assumed army-loss was at most 25%. Now that is 50% times the ratio of their power compared to the total power of everyone. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 25 +++++++++++++++++++--- AI/Nullkiller/Engine/PriorityEvaluator.h | 1 + 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index c373dff6c..ed0c82388 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -64,6 +64,7 @@ EvaluationContext::EvaluationContext(const Nullkiller* ai) isTradeBuilding(false), isExchange(false), isArmyUpgrade(false), + isHero(false), explorePriority(0) { } @@ -1037,6 +1038,8 @@ public: 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; evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army); evaluationContext.armyInvolvement += army->getArmyCost(); } @@ -1336,9 +1339,22 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) else { float score = 0; - float maxWillingToLose = ai->cb->getTownsInfo().empty() ? 1 : 0.25; + float myPower = 0; + float totalPower = 0; + for (auto heroInfo : ai->cb->getHeroesInfo(false)) + { + if (heroInfo->getOwner() == ai->cb->getPlayerID()) + myPower += heroInfo->getTotalStrength(); + totalPower += heroInfo->getTotalStrength(); + } + float powerRatio = 1; + if (totalPower > 0) + powerRatio = myPower / totalPower; + + float maxWillingToLose = ai->cb->getTownsInfo().empty() ? 1 : 0.5 * powerRatio; + #if NKAI_TRACE_LEVEL >= 2 - logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, explorePriority: %d isDefend: %d", + logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, explorePriority: %d isDefend: %d powerRatio: %d", priorityTier, task->toString(), evaluationContext.armyLossPersentage, @@ -1359,7 +1375,8 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) evaluationContext.closestWayRatio, evaluationContext.enemyHeroDangerRatio, evaluationContext.explorePriority, - evaluationContext.isDefend); + evaluationContext.isDefend, + powerRatio); #endif switch (priorityTier) @@ -1388,6 +1405,8 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) } case PriorityTier::KILL: //Take towns / kill heroes that are further away { + if (evaluationContext.turn > 0 && evaluationContext.isHero) + return 0; if (evaluationContext.conquestValue > 0 || evaluationContext.explorePriority == 1) score = 1000; if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty())) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index 4978bd28f..6e1cbe5ac 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -81,6 +81,7 @@ struct DLL_EXPORT EvaluationContext bool isTradeBuilding; bool isExchange; bool isArmyUpgrade; + bool isHero; int explorePriority; EvaluationContext(const Nullkiller * ai); From e7e4d6cc72497b2661d03f11e758d1276ab2ef36 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 23 Sep 2024 01:23:18 +0200 Subject: [PATCH 143/186] Fix invincible Hellbardiers --- config/creatures/castle.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/config/creatures/castle.json b/config/creatures/castle.json index 6f53c399f..a88d5a115 100644 --- a/config/creatures/castle.json +++ b/config/creatures/castle.json @@ -11,10 +11,6 @@ { "type" : "CHARGE_IMMUNITY" }, - "invincible" : - { - "type" : "INVINCIBLE" - } }, "graphics" : { From 20f7751e169b02d4da0f56d638a08968e51442f4 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 23 Sep 2024 01:25:58 +0200 Subject: [PATCH 144/186] Weekend-decisions When attacking non-neutral towns that are far away, the AI now considers whether their attack would arrive in the same week. If it wouldn't, it means there's a high risk that newly bought troops might flip around who is stronger. So they now refrain from sending a hero towards an enemy town that is too far away. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 9 +++++++++ AI/Nullkiller/Engine/PriorityEvaluator.h | 1 + 2 files changed, 10 insertions(+) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index ed0c82388..1d343d028 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -65,6 +65,7 @@ EvaluationContext::EvaluationContext(const Nullkiller* ai) isExchange(false), isArmyUpgrade(false), isHero(false), + isEnemy(false), explorePriority(0) { } @@ -1040,6 +1041,8 @@ public: evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target); if (target->ID == Obj::HERO) evaluationContext.isHero = true; + if (target->getOwner() != PlayerColor::NEUTRAL && ai->cb->getPlayerRelations(ai->playerID, target->getOwner()) == PlayerRelations::ENEMIES) + evaluationContext.isEnemy = true; evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army); evaluationContext.armyInvolvement += army->getArmyCost(); } @@ -1353,6 +1356,10 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) float maxWillingToLose = ai->cb->getTownsInfo().empty() ? 1 : 0.5 * powerRatio; + bool arriveNextWeek = false; + if (ai->cb->getDate(Date::DAY_OF_WEEK) + evaluationContext.turn > 7) + arriveNextWeek = true; + #if NKAI_TRACE_LEVEL >= 2 logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, explorePriority: %d isDefend: %d powerRatio: %d", priorityTier, @@ -1407,6 +1414,8 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) { if (evaluationContext.turn > 0 && evaluationContext.isHero) return 0; + if (arriveNextWeek && evaluationContext.isEnemy) + return 0; if (evaluationContext.conquestValue > 0 || evaluationContext.explorePriority == 1) score = 1000; if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty())) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index 6e1cbe5ac..d912130b7 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -82,6 +82,7 @@ struct DLL_EXPORT EvaluationContext bool isExchange; bool isArmyUpgrade; bool isHero; + bool isEnemy; int explorePriority; EvaluationContext(const Nullkiller * ai); From 71504a3140cf81c3fa636f45141522cc2d7b9bbe Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 23 Sep 2024 17:25:17 +0200 Subject: [PATCH 145/186] Hire despite hero present Only if both the garrison and the outside of a town are blocked are hires of heros being blocked. --- AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp index e46483fca..eab159597 100644 --- a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp +++ b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp @@ -67,7 +67,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const closestThreat = std::min(closestThreat, threat.turn); } //Don't hire a hero where there already is one present - if (town->visitingHero || town->garrisonHero) + if (town->visitingHero && town->garrisonHero) continue; float visitability = 0; for (auto checkHero : ourHeroes) From d87f195bc7a4157e03b558630eaab6b338c990dc Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 23 Sep 2024 18:39:18 +0200 Subject: [PATCH 146/186] Update AINodeStorage.cpp Nodes along the path are now also considered for how dangerous it is. --- AI/Nullkiller/Pathfinding/AINodeStorage.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index 360bcd5bd..247836768 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -1419,6 +1419,10 @@ void AINodeStorage::calculateChainInfo(std::vector & paths, const int3 & path.heroArmy = node.actor->creatureSet; path.armyLoss = node.armyLoss; path.targetObjectDanger = ai->dangerEvaluator->evaluateDanger(pos, path.targetHero, !node.actor->allowBattle); + for (auto pathNode : path.nodes) + { + path.targetObjectDanger = std::max(ai->dangerEvaluator->evaluateDanger(pathNode.coord, path.targetHero, !node.actor->allowBattle), path.targetObjectDanger); + } if(path.targetObjectDanger > 0) { From 433c58f8b1d4125abab5a11d0fca5498a0440e7d Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 23 Sep 2024 18:42:31 +0200 Subject: [PATCH 147/186] Workaround for previously masked issue A recent fix made it so that towns that weren't supposed to be defended are now no longer defended. This caused scouts with minimal army to also go after them in addition to the main-hero. Problem is when two heroes go for the same town it's a massive waste of movement-points. So for the time being only main-heroes will go for faraway captures. Better solution would be to memorize who was sent to attack what on the same turn and filter out tasks going for the same target. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 1d343d028..456f8af0d 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -1412,6 +1412,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) } case PriorityTier::KILL: //Take towns / kill heroes that are further away { + //TODO: This is a workaround for duplicating the same capture town-task being given to several heroes. A better solution ought to be found. + if (evaluationContext.movementCostByRole[HeroRole::MAIN] == 0) + return 0; if (evaluationContext.turn > 0 && evaluationContext.isHero) return 0; if (arriveNextWeek && evaluationContext.isEnemy) From e4ef95f8dd347eeb7f81363e300f169f04841000 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Wed, 25 Sep 2024 16:12:53 +0200 Subject: [PATCH 148/186] Revert dynamic maxWillingToLose Out maxWillingToLose back to a static 25%. Dynamic could become too suicidal or too passive. 25% is a good sweetspot. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 456f8af0d..0dca4e1ae 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -1342,26 +1342,14 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) else { float score = 0; - float myPower = 0; - float totalPower = 0; - for (auto heroInfo : ai->cb->getHeroesInfo(false)) - { - if (heroInfo->getOwner() == ai->cb->getPlayerID()) - myPower += heroInfo->getTotalStrength(); - totalPower += heroInfo->getTotalStrength(); - } - float powerRatio = 1; - if (totalPower > 0) - powerRatio = myPower / totalPower; - - float maxWillingToLose = ai->cb->getTownsInfo().empty() ? 1 : 0.5 * powerRatio; + float maxWillingToLose = ai->cb->getTownsInfo().empty() ? 1 : 0.25; bool arriveNextWeek = false; if (ai->cb->getDate(Date::DAY_OF_WEEK) + evaluationContext.turn > 7) arriveNextWeek = true; #if NKAI_TRACE_LEVEL >= 2 - logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, explorePriority: %d isDefend: %d powerRatio: %d", + logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, explorePriority: %d isDefend: %d", priorityTier, task->toString(), evaluationContext.armyLossPersentage, @@ -1382,8 +1370,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) evaluationContext.closestWayRatio, evaluationContext.enemyHeroDangerRatio, evaluationContext.explorePriority, - evaluationContext.isDefend, - powerRatio); + evaluationContext.isDefend); #endif switch (priorityTier) From 3b7834495d0f8ec87ec8638118c12d24f8276b8a Mon Sep 17 00:00:00 2001 From: Xilmi Date: Fri, 27 Sep 2024 19:38:07 +0200 Subject: [PATCH 149/186] Update PriorityEvaluator.cpp Fix an issue that caused AI to take their hero's attributes into consideration twice when calculating how much army they think they'll lose. Fixed an issue where offensive defending didn't take into consideration whether the hero would actually be strong enough to beat the enemy hero it was trying to dispatch. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 0dca4e1ae..dbf5d7d9a 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -1047,7 +1047,7 @@ public: evaluationContext.armyInvolvement += army->getArmyCost(); } - vstd::amax(evaluationContext.armyLossPersentage, path.getTotalArmyLoss() / (double)path.getHeroStrength()); + vstd::amax(evaluationContext.armyLossPersentage, (float)path.getTotalArmyLoss() / (float)army->getArmyStrength()); addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength()); vstd::amax(evaluationContext.turn, path.turn()); } @@ -1394,6 +1394,8 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) { if (evaluationContext.isDefend && evaluationContext.threatTurns == 0 && evaluationContext.turn == 0) score = evaluationContext.armyInvolvement; + if (evaluationContext.isEnemy) + score *= (maxWillingToLose - evaluationContext.armyLossPersentage); score *= evaluationContext.closestWayRatio; break; } From 0b016b9b14a7da034e6c53e30904b14ff020f22b Mon Sep 17 00:00:00 2001 From: Xilmi Date: Fri, 27 Sep 2024 23:02:40 +0200 Subject: [PATCH 150/186] Update RecruitHeroBehavior.cpp If the AI is very rich it will buy more heroes even if it doesn't have a capitol. --- AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp index eab159597..010c8df78 100644 --- a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp +++ b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp @@ -107,7 +107,8 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const if (bestHeroToHire && bestTownToHireFrom) { if (ai->cb->getHeroesInfo().size() < ai->cb->getTownsInfo().size() + 1 - || (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh() && haveCapitol)) + || (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh() && haveCapitol) + || (ai->getFreeResources()[EGameResID::GOLD] > 30000 && !ai->buildAnalyzer->isGoldPressureHigh())) { tasks.push_back(Goals::sptr(Goals::RecruitHero(bestTownToHireFrom, bestHeroToHire).setpriority((float)3 / (ourHeroes.size() + 1)))); } From ea535b211ad3c6b35484849659693ecec4e36a47 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Fri, 27 Sep 2024 23:03:09 +0200 Subject: [PATCH 151/186] Update Nullkiller.h Fixed issues caused by running buildPlan with wrong default-priority-tier. --- AI/Nullkiller/Engine/Nullkiller.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/Nullkiller/Engine/Nullkiller.h b/AI/Nullkiller/Engine/Nullkiller.h index 5166421fb..6a442dc80 100644 --- a/AI/Nullkiller/Engine/Nullkiller.h +++ b/AI/Nullkiller/Engine/Nullkiller.h @@ -127,7 +127,7 @@ private: void updateAiState(int pass, bool fast = false); void decompose(Goals::TGoalVec & result, Goals::TSubgoal behavior, int decompositionMaxDepth) const; Goals::TTask choseBestTask(Goals::TGoalVec & tasks) const; - Goals::TTaskVec buildPlan(Goals::TGoalVec & tasks, int priorityTier = 3) const; + Goals::TTaskVec buildPlan(Goals::TGoalVec & tasks, int priorityTier = PriorityEvaluator::PriorityTier::HUNTER_GATHER) const; bool executeTask(Goals::TTask task); bool areAffectedObjectsPresent(Goals::TTask task) const; HeroRole getTaskRole(Goals::TTask task) const; From 8d93c0c9c9902c36b3e692d1effaa11db2107a15 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Fri, 27 Sep 2024 23:03:35 +0200 Subject: [PATCH 152/186] Update PriorityEvaluator.cpp Removed workaround that was likely necessitated by other issues. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index dbf5d7d9a..f588406fb 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -1401,9 +1401,6 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) } case PriorityTier::KILL: //Take towns / kill heroes that are further away { - //TODO: This is a workaround for duplicating the same capture town-task being given to several heroes. A better solution ought to be found. - if (evaluationContext.movementCostByRole[HeroRole::MAIN] == 0) - return 0; if (evaluationContext.turn > 0 && evaluationContext.isHero) return 0; if (arriveNextWeek && evaluationContext.isEnemy) From 769268cfe34a50f38e789b600afb24e316583ad2 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 29 Sep 2024 01:15:09 +0200 Subject: [PATCH 153/186] Eternal Garrison Fixed an issue that caused heroes to stay garrisoned for ever when hero-cap was reached. --- AI/Nullkiller/Analyzers/HeroManager.cpp | 5 ++--- AI/Nullkiller/Analyzers/HeroManager.h | 2 +- AI/Nullkiller/Pathfinding/AINodeStorage.cpp | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/AI/Nullkiller/Analyzers/HeroManager.cpp b/AI/Nullkiller/Analyzers/HeroManager.cpp index 594b2e394..7e88bcdce 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -189,10 +189,9 @@ float HeroManager::evaluateHero(const CGHeroInstance * hero) const return evaluateFightingStrength(hero); } -bool HeroManager::heroCapReached() const +bool HeroManager::heroCapReached(bool includeGarrisoned) const { - const bool includeGarnisoned = true; - int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned); + int heroCount = cb->getHeroCount(ai->playerID, includeGarrisoned); return heroCount >= ALLOWED_ROAMING_HEROES || heroCount >= ai->settings->getMaxRoamingHeroes() diff --git a/AI/Nullkiller/Analyzers/HeroManager.h b/AI/Nullkiller/Analyzers/HeroManager.h index 675357626..9d7f6c4e0 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.h +++ b/AI/Nullkiller/Analyzers/HeroManager.h @@ -56,7 +56,7 @@ public: float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const; float evaluateHero(const CGHeroInstance * hero) const; bool canRecruitHero(const CGTownInstance * t = nullptr) const; - bool heroCapReached() const; + bool heroCapReached(bool includeGarrisoned = true) const; const CGHeroInstance * findHeroWithGrail() const; const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const; float getMagicStrength(const CGHeroInstance * hero) const; diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index 247836768..fe8912c7a 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -962,7 +962,7 @@ void AINodeStorage::setHeroes(std::map heroes) // do not allow our own heroes in garrison to act on map if(hero.first->getOwner() == ai->playerID && hero.first->inTownGarrison - && (ai->isHeroLocked(hero.first) || ai->heroManager->heroCapReached())) + && (ai->isHeroLocked(hero.first) || ai->heroManager->heroCapReached(false))) { continue; } From 4a5ecdf25e85481212e05aa06aa51436b1e6477d Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 29 Sep 2024 01:15:53 +0200 Subject: [PATCH 154/186] Update Nullkiller.h Removed pointless default-parameter. --- AI/Nullkiller/Engine/Nullkiller.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/Nullkiller/Engine/Nullkiller.h b/AI/Nullkiller/Engine/Nullkiller.h index 6a442dc80..941e71f16 100644 --- a/AI/Nullkiller/Engine/Nullkiller.h +++ b/AI/Nullkiller/Engine/Nullkiller.h @@ -127,7 +127,7 @@ private: void updateAiState(int pass, bool fast = false); void decompose(Goals::TGoalVec & result, Goals::TSubgoal behavior, int decompositionMaxDepth) const; Goals::TTask choseBestTask(Goals::TGoalVec & tasks) const; - Goals::TTaskVec buildPlan(Goals::TGoalVec & tasks, int priorityTier = PriorityEvaluator::PriorityTier::HUNTER_GATHER) const; + Goals::TTaskVec buildPlan(Goals::TGoalVec & tasks, int priorityTier) const; bool executeTask(Goals::TTask task); bool areAffectedObjectsPresent(Goals::TTask task) const; HeroRole getTaskRole(Goals::TTask task) const; From f7a961793ab9983cb0dc48f3ef1f0471fca2ebd6 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 29 Sep 2024 01:23:13 +0200 Subject: [PATCH 155/186] Update PriorityEvaluator.cpp AI is more careful when gathering stuff near enemies. The wasted movement-points are no longer considered when calculating which own city to fall back to when there's nothing better to do. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index f588406fb..3275ba9aa 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -909,18 +909,21 @@ public: class StayAtTownManaRecoveryEvaluator : public IEvaluationContextBuilder { public: - void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override + void buildEvaluationContext(EvaluationContext& evaluationContext, Goals::TSubgoal task) const override { - if(task->goalType != Goals::STAY_AT_TOWN) + if (task->goalType != Goals::STAY_AT_TOWN) return; - Goals::StayAtTown & stayAtTown = dynamic_cast(*task); + Goals::StayAtTown& stayAtTown = dynamic_cast(*task); evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero()); if (evaluationContext.armyReward == 0) evaluationContext.isDefend = true; - evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted(); - evaluationContext.movementCost += stayAtTown.getMovementWasted(); + else + { + evaluationContext.movementCost += stayAtTown.getMovementWasted(); + evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted(); + } } }; @@ -1428,6 +1431,8 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) return 0; if (evaluationContext.isArmyUpgrade) return 0; + if (evaluationContext.enemyHeroDangerRatio > 0 && arriveNextWeek) + return 0; score += evaluationContext.strategicalValue * 1000; score += evaluationContext.goldReward; score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05; @@ -1438,8 +1443,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) if (score > 0) { score *= evaluationContext.closestWayRatio; - if (evaluationContext.enemyHeroDangerRatio > 1) - score /= evaluationContext.enemyHeroDangerRatio; + score /= (1 + evaluationContext.enemyHeroDangerRatio); if (evaluationContext.movementCost > 0) score /= evaluationContext.movementCost; score *= (maxWillingToLose - evaluationContext.armyLossPersentage); From a2904584d3ef04e7a9c65deb921a0c9b478fd1c1 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 29 Sep 2024 02:01:09 +0200 Subject: [PATCH 156/186] Update Nullkiller.cpp Build and hire-tasks no longer eat into the pass-depth. --- AI/Nullkiller/Engine/Nullkiller.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 4a10d8b6c..e4db13bbc 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -374,7 +374,7 @@ void Nullkiller::makeTurn() Goals::TTask bestTask = taskptr(Goals::Invalid()); - for(;i <= settings->getMaxPass(); i++) + while(true) { bestTasks.clear(); From cfe4d7592a587d63a138a0d537ed2dc1b76c50ef Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 29 Sep 2024 03:13:21 +0200 Subject: [PATCH 157/186] Update DefenceBehavior.cpp Heroes will no longer rush to defend towns that have a standing garrison that they can't merge their armies with. --- AI/Nullkiller/Behaviors/DefenceBehavior.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp index b5e0ab8d0..bc5a11bbe 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp @@ -249,6 +249,16 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta continue; } + if (!path.targetHero->canBeMergedWith(*town)) + { +#if NKAI_TRACE_LEVEL >= 1 + logAi->trace("Can't merge armies of hero %s and town %s", + path.targetHero->getObjectName(), + town->getObjectName()); +#endif + continue; + } + if(path.targetHero == town->visitingHero.get() && path.exchangeCount == 1) { #if NKAI_TRACE_LEVEL >= 1 From d59a1fe9e9f518f9397a20709c5c68f8bf05aff0 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 30 Sep 2024 17:57:41 +0200 Subject: [PATCH 158/186] Fix case of "Got false in applying struct CastAdvSpell" Heroes now leave the garrison before trying (and failing) to cast adventure-map-spells. --- AI/Nullkiller/Goals/AdventureSpellCast.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AI/Nullkiller/Goals/AdventureSpellCast.cpp b/AI/Nullkiller/Goals/AdventureSpellCast.cpp index 1868d7c60..8e8df0241 100644 --- a/AI/Nullkiller/Goals/AdventureSpellCast.cpp +++ b/AI/Nullkiller/Goals/AdventureSpellCast.cpp @@ -53,6 +53,9 @@ void AdventureSpellCast::accept(AIGateway * ai) throw cannotFulfillGoalException("The town is already occupied by " + town->visitingHero->getNameTranslated()); } + if (hero->inTownGarrison) + ai->myCb->swapGarrisonHero(hero->visitedTown); + auto wait = cb->waitTillRealize; cb->waitTillRealize = true; From 74f3aedcc9286e0a21f5c08d17f61bfdc831ce60 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 30 Sep 2024 19:32:27 +0200 Subject: [PATCH 159/186] TP onto friend attempt Fixed an issue that caused the AI to think it can townportal onto heroes of other factions, for example their allies. --- AI/Nullkiller/Pathfinding/AINodeStorage.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index fe8912c7a..c22ef912f 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -1197,6 +1197,10 @@ void AINodeStorage::calculateTownPortal( continue; } + if (targetTown->visitingHero + && targetTown->visitingHero.get()->getFaction() != actor->hero->getFaction()) + continue; + auto nodeOptional = townPortalFinder.createTownPortalNode(targetTown); if(nodeOptional) From 73e7d3f5bb6ba09c6e3c8171f50175acacb6326c Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 30 Sep 2024 19:41:39 +0200 Subject: [PATCH 160/186] Another reason not to try to town-portal Even if the hero blocking a town is from the own faction, the town must not become a target if the city has stashed armies because in that case the hero ontop of it won't be able to go into garrison for the TP. --- AI/Nullkiller/Pathfinding/AINodeStorage.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index c22ef912f..45824c9ca 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -1198,7 +1198,8 @@ void AINodeStorage::calculateTownPortal( } if (targetTown->visitingHero - && targetTown->visitingHero.get()->getFaction() != actor->hero->getFaction()) + && (targetTown->visitingHero.get()->getFaction() != actor->hero->getFaction() + || targetTown->getUpperArmy()->stacksCount())) continue; auto nodeOptional = townPortalFinder.createTownPortalNode(targetTown); From a01e84214f124d0c766a5f9e8b623cba4251b318 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 30 Sep 2024 21:00:50 +0200 Subject: [PATCH 161/186] Fixed errors AI no longer tries to access tiles it cannot see while clusterizing objects. --- AI/Nullkiller/Analyzers/ObjectClusterizer.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp index e24f0a757..5b9e0a12b 100644 --- a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp +++ b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp @@ -97,9 +97,10 @@ std::optional ObjectClusterizer::getBlocker(const AIPa { auto guardPos = ai->cb->getGuardingCreaturePosition(node.coord); - blockers = ai->cb->getVisitableObjs(node.coord); + if (ai->cb->isVisible(node.coord)) + blockers = ai->cb->getVisitableObjs(node.coord); - if(guardPos.valid()) + if(guardPos.valid() && ai->cb->isVisible(guardPos)) { auto guard = ai->cb->getTopObj(ai->cb->getGuardingCreaturePosition(node.coord)); From 9c52e3a0b2db623b7f56a744f4de891809dfb636 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Tue, 1 Oct 2024 00:42:01 +0200 Subject: [PATCH 162/186] Fix for inconsistency in planned and performed army-merge. Removed the usage of BonusModifiers because depending on the case the function was sometimes called with and sometimes without an actual hero as first parameter. This lead to inconsistencies between planned and performed army-merge and got the AI stuck in a loop where it ordered an army-merge over and over that then would not conclude. The inclusion of bonuses of the hero for determining which army is better on them is unnecessarily convoluted and just causes issues. It took me like 4 hours to figure out why the AI didn't act. --- AI/Nullkiller/Analyzers/ArmyManager.cpp | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/AI/Nullkiller/Analyzers/ArmyManager.cpp b/AI/Nullkiller/Analyzers/ArmyManager.cpp index 5c2a7f4a8..9c70410b5 100644 --- a/AI/Nullkiller/Analyzers/ArmyManager.cpp +++ b/AI/Nullkiller/Analyzers/ArmyManager.cpp @@ -152,16 +152,6 @@ std::vector ArmyManager::getBestArmy(const IBonusBearer * armyCarrier, uint64_t armyValue = 0; TemporaryArmy newArmyInstance; - auto bonusModifiers = armyCarrier->getBonuses(Selector::type()(BonusType::MORALE)); - - for(auto bonus : *bonusModifiers) - { - // army bonuses will change and object bonuses are temporary - if(bonus->source != BonusSource::ARMY && bonus->source != BonusSource::OBJECT_INSTANCE && bonus->source != BonusSource::OBJECT_TYPE) - { - newArmyInstance.addNewBonus(std::make_shared(*bonus)); - } - } while(allowedFactions.size() < alignmentMap.size()) { From 2d783211cea896642caa65edb3db0b57f30f3299 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Tue, 1 Oct 2024 01:11:17 +0200 Subject: [PATCH 163/186] Fixed a crash Fixed a crash from trying to access nonexisting map-element. --- AI/Nullkiller/Analyzers/HeroManager.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/AI/Nullkiller/Analyzers/HeroManager.cpp b/AI/Nullkiller/Analyzers/HeroManager.cpp index 7e88bcdce..e305f761c 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -148,7 +148,10 @@ void HeroManager::update() HeroRole HeroManager::getHeroRole(const HeroPtr & hero) const { - return heroRoles.at(hero); + if (heroRoles.find(hero) != heroRoles.end()) + return heroRoles.at(hero); + else + return HeroRole::SCOUT; } const std::map & HeroManager::getHeroRoles() const From f2b8b40925b879d8d433124039c6874c5664ce1c Mon Sep 17 00:00:00 2001 From: Xilmi Date: Tue, 1 Oct 2024 17:20:54 +0200 Subject: [PATCH 164/186] Being less dismissive Reduced the amount of circumstances under which the AI dismisses heroes. Among others to prevent a loop through all passes where it repeatedly hires and dismisses heroes. --- AI/Nullkiller/Analyzers/HeroManager.cpp | 3 ++- AI/Nullkiller/Analyzers/HeroManager.h | 2 +- AI/Nullkiller/Behaviors/DefenceBehavior.cpp | 21 ++++++--------------- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/AI/Nullkiller/Analyzers/HeroManager.cpp b/AI/Nullkiller/Analyzers/HeroManager.cpp index e305f761c..e559bd264 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -284,7 +284,7 @@ const CGHeroInstance * HeroManager::findHeroWithGrail() const return nullptr; } -const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) const +const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit, const CGTownInstance* townToSpare) const { const CGHeroInstance * weakestHero = nullptr; auto myHeroes = ai->cb->getHeroesInfo(); @@ -295,6 +295,7 @@ const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) co || existingHero->getArmyStrength() >armyLimit || getHeroRole(existingHero) == HeroRole::MAIN || existingHero->movementPointsRemaining() + || (townToSpare != nullptr && existingHero->visitedTown == townToSpare) || existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1)) { continue; diff --git a/AI/Nullkiller/Analyzers/HeroManager.h b/AI/Nullkiller/Analyzers/HeroManager.h index 9d7f6c4e0..383f3c13a 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.h +++ b/AI/Nullkiller/Analyzers/HeroManager.h @@ -58,7 +58,7 @@ public: bool canRecruitHero(const CGTownInstance * t = nullptr) const; bool heroCapReached(bool includeGarrisoned = true) const; const CGHeroInstance * findHeroWithGrail() const; - const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const; + const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit, const CGTownInstance * townToSpare = nullptr) const; float getMagicStrength(const CGHeroInstance * hero) const; float getFightingStrengthCached(const CGHeroInstance * hero) const; diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp index bc5a11bbe..158fd8989 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp @@ -354,21 +354,12 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta { if(town->garrisonHero && town->garrisonHero != path.targetHero) { - if(ai->heroManager->getHeroRole(town->visitingHero.get()) == HeroRole::SCOUT - && town->visitingHero->getArmyStrength() < path.heroArmy->getArmyStrength() / 20) - { - if(path.turn() == 0) - sequence.push_back(sptr(DismissHero(town->visitingHero.get()))); - } - else - { #if NKAI_TRACE_LEVEL >= 1 - logAi->trace("Cancel moving %s to defend town %s as the town has garrison hero", - path.targetHero->getObjectName(), - town->getObjectName()); + logAi->trace("Cancel moving %s to defend town %s as the town has garrison hero", + path.targetHero->getObjectName(), + town->getObjectName()); #endif - continue; - } + continue; } else if(path.turn() == 0) { @@ -414,7 +405,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & threat, const CGTownInstance * town, const Nullkiller * ai) const { - if (threat.turn > 0) + if (threat.turn > 0 || town->garrisonHero || town->visitingHero) return; if(town->hasBuilt(BuildingID::TAVERN) @@ -463,7 +454,7 @@ void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitM } else if(ai->heroManager->heroCapReached()) { - heroToDismiss = ai->heroManager->findWeakHeroToDismiss(hero->getArmyStrength()); + heroToDismiss = ai->heroManager->findWeakHeroToDismiss(hero->getArmyStrength(), town); if(!heroToDismiss) continue; From c19d885603cafb114e92f42e61707012fb3f70d7 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Wed, 2 Oct 2024 00:10:45 +0200 Subject: [PATCH 165/186] Subterranean Gateway fix Fixed an issue that caused pathfinding for player and AI not working when a subterranean gate was too close to the edge of the map. Fixed another issue that played into pathfinding for AI not working when a subterranean gate was too close to the edge of the map. --- lib/CGameInfoCallback.cpp | 2 +- lib/mapObjects/MiscObjects.cpp | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index de5a77433..8e404e34c 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -964,7 +964,7 @@ std::vector CGameInfoCallback::getVisibleTeleportObjects(std:: vstd::erase_if(ids, [&](const ObjectInstanceID & id) -> bool { const auto * obj = getObj(id, false); - return player != PlayerColor::UNFLAGGABLE && (!obj || !isVisible(obj->pos, player)); + return player != PlayerColor::UNFLAGGABLE && (!obj || !isVisible(obj->visitablePos(), player)); }); return ids; } diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 9c01e64aa..054b4fcff 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -602,13 +602,13 @@ void CGSubterraneanGate::postInit(IGameCallback * cb) //matches subterranean gat auto * hlp = dynamic_cast(cb->gameState()->getObjInstance(obj->id)); if(hlp) - gatesSplit[hlp->pos.z].push_back(hlp); + gatesSplit[hlp->visitablePos().z].push_back(hlp); } //sort by position std::sort(gatesSplit[0].begin(), gatesSplit[0].end(), [](const CGObjectInstance * a, const CGObjectInstance * b) { - return a->pos < b->pos; + return a->visitablePos() < b->visitablePos(); }); auto assignToChannel = [&](CGSubterraneanGate * obj) @@ -631,7 +631,7 @@ void CGSubterraneanGate::postInit(IGameCallback * cb) //matches subterranean gat CGSubterraneanGate *checked = gatesSplit[1][j]; if(checked->channel != TeleportChannelID()) continue; - si32 hlp = checked->pos.dist2dSQ(objCurrent->pos); + si32 hlp = checked->visitablePos().dist2dSQ(objCurrent->visitablePos()); if(hlp < best.second) { best.first = j; From f0802c0b3c8536a722932092dbe4140a4cf926f5 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Wed, 2 Oct 2024 20:11:20 +0200 Subject: [PATCH 166/186] Move foreign hero fix Fixed an issue that caused the AI to try and move heroes of other players. --- AI/Nullkiller/Pathfinding/AINodeStorage.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index 45824c9ca..b087d0124 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -1413,7 +1413,8 @@ void AINodeStorage::calculateChainInfo(std::vector & paths, const int3 & || node.layer != layer || node.action == EPathNodeAction::UNKNOWN || !node.actor - || !node.actor->hero) + || !node.actor->hero + || node.actor->hero->getOwner() != ai->playerID) { continue; } From 0143a5275566737fd0b998e3d971a35f29241eb9 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Wed, 2 Oct 2024 20:12:47 +0200 Subject: [PATCH 167/186] Update PriorityEvaluator.cpp Scores for all sorts of visitable and pickable objects are now unified in order to prevent AI from ignoring nearby valuables. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 3275ba9aa..9e23377f3 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -1431,7 +1431,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) return 0; if (evaluationContext.isArmyUpgrade) return 0; - if (evaluationContext.enemyHeroDangerRatio > 0 && arriveNextWeek) + if ((evaluationContext.enemyHeroDangerRatio > 0 && arriveNextWeek) || evaluationContext.enemyHeroDangerRatio > 1) + return 0; + if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) return 0; score += evaluationContext.strategicalValue * 1000; score += evaluationContext.goldReward; @@ -1442,11 +1444,10 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) score -= evaluationContext.armyInvolvement * evaluationContext.armyLossPersentage; if (score > 0) { + score = 1000; score *= evaluationContext.closestWayRatio; - score /= (1 + evaluationContext.enemyHeroDangerRatio); if (evaluationContext.movementCost > 0) score /= evaluationContext.movementCost; - score *= (maxWillingToLose - evaluationContext.armyLossPersentage); } break; } From 5e6fefdc509dcbad510032f1ea41e3e95336d33f Mon Sep 17 00:00:00 2001 From: Xilmi Date: Wed, 2 Oct 2024 23:40:51 +0200 Subject: [PATCH 168/186] Reverted fix for not trying to moving foreign heroes While it fixed the bug it was supposed to fix it caused another severe bug: AI wasn't generating a thread-map anymore. Original bug needs to find another fix. --- AI/Nullkiller/Pathfinding/AINodeStorage.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index b087d0124..45824c9ca 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -1413,8 +1413,7 @@ void AINodeStorage::calculateChainInfo(std::vector & paths, const int3 & || node.layer != layer || node.action == EPathNodeAction::UNKNOWN || !node.actor - || !node.actor->hero - || node.actor->hero->getOwner() != ai->playerID) + || !node.actor->hero) { continue; } From 68e264d990391858f14a8aebddea17377709b7cb Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 3 Oct 2024 15:06:34 +0200 Subject: [PATCH 169/186] Prune paths involving enemy heroes Capture objects and gather army will now skip paths that involve foreign heroes. --- AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp | 3 +++ AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp | 3 +++ 2 files changed, 6 insertions(+) diff --git a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp index d9288af38..1e7f39569 100644 --- a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp +++ b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp @@ -79,6 +79,9 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals( auto hero = path.targetHero; auto danger = path.getTotalDanger(); + if (hero->getOwner() != nullkiller->playerID) + continue; + if(nullkiller->heroManager->getHeroRole(hero) == HeroRole::SCOUT && (path.getTotalDanger() == 0 || path.turn() > 0) && path.exchangeCount > 1) diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp index 4a17bb429..14ad346f1 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp @@ -81,6 +81,9 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength()); #endif + if (path.targetHero->getOwner() != ai->playerID) + continue; + if(path.containsHero(hero)) { #if NKAI_TRACE_LEVEL >= 2 From 6c443548439592675ca9fbbdd9d62c4f5e0d6608 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Fri, 4 Oct 2024 11:49:20 +0200 Subject: [PATCH 170/186] Update CGameHandler.cpp Improve debugability with more helpful error-texts. --- server/CGameHandler.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 383e3bbe3..23de4565e 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -786,7 +786,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, EMovementMode moveme return false; } - logGlobal->trace("Player %d (%s) wants to move hero %d from %s to %s", asker, asker.toString(), hid.getNum(), h->pos.toString(), dst.toString()); + logGlobal->trace("Player %d (%s) wants to move hero %s from %s to %s", asker, asker.toString(), h->getNameTranslated(), h->pos.toString(), dst.toString()); const int3 hmpos = h->convertToVisitablePos(dst); if (!gs->map->isInTheMap(hmpos)) @@ -869,7 +869,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, EMovementMode moveme return complainRet("Cannot disembark hero, tile is blocked!"); if(distance(h->pos, dst) >= 1.5 && movementMode == EMovementMode::STANDARD) - return complainRet("Tiles are not neighboring!"); + return complainRet("Tiles " + h->pos.toString()+ " and "+ dst.toString() +" are not neighboring!"); if(h->inTownGarrison) return complainRet("Can not move garrisoned hero!"); From da32b8b58f72175ddd821836c16447303b8a650e Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 10 Oct 2024 17:33:54 +0200 Subject: [PATCH 171/186] Restore compatibility with removal of getFaction() --- AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp | 2 +- AI/Nullkiller/Pathfinding/AINodeStorage.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp index 010c8df78..aa7d4b23f 100644 --- a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp +++ b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp @@ -87,7 +87,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const score *= score / minScoreToHireMain; } score *= (hero->getArmyCost() + currentArmyValue); - if (hero->type->heroClass->faction == town->getFaction()) + if (hero->getFactionID() == town->getFactionID()) score *= 1.5; if (vstd::isAlmostZero(visitability)) score *= 30 * town->getTownLevel(); diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index 45824c9ca..713e36131 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -1198,7 +1198,7 @@ void AINodeStorage::calculateTownPortal( } if (targetTown->visitingHero - && (targetTown->visitingHero.get()->getFaction() != actor->hero->getFaction() + && (targetTown->visitingHero.get()->getFactionID() != actor->hero->getFactionID() || targetTown->getUpperArmy()->stacksCount())) continue; From 6adaffa2c2393311969cd8860c657713b3009968 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 10 Oct 2024 18:52:25 +0200 Subject: [PATCH 172/186] Update SelectionTab.cpp Fix for save-game-list having no names. --- client/lobby/SelectionTab.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 71604a58d..4a447922d 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -989,6 +989,6 @@ void SelectionTab::ListItem::updateItem(std::shared_ptr info, bool iconLossCondition->setFrame(info->mapHeader->defeatIconIndex, 0); labelName->setMaxWidth(185); } - labelName->setText(info->name); + labelName->setText(info->getNameForList()); labelName->setColor(color); } From eb94c9f0bee4a7e165fa51539ee6800035c00455 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 10 Oct 2024 18:53:28 +0200 Subject: [PATCH 173/186] Update PriorityEvaluator.cpp AI will be more aggressive when defending their territory. And more aggressive means more willing to take losses while fighting. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 1ae243055..1e616b875 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -1345,7 +1345,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) else { float score = 0; - float maxWillingToLose = ai->cb->getTownsInfo().empty() ? 1 : 0.25; + float maxWillingToLose = ai->cb->getTownsInfo().empty() || (evaluationContext.isDefend && evaluationContext.threatTurns == 0) ? 1 : 0.25; bool arriveNextWeek = false; if (ai->cb->getDate(Date::DAY_OF_WEEK) + evaluationContext.turn > 7) From c838f5d0c24286505fdd47b4a1b31c6010dc7b73 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Fri, 11 Oct 2024 20:27:18 +0200 Subject: [PATCH 174/186] Update BattleEvaluator.cpp Restored spell-damage-calculations for units that would die from spells. --- AI/BattleAI/BattleEvaluator.cpp | 50 ++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/AI/BattleAI/BattleEvaluator.cpp b/AI/BattleAI/BattleEvaluator.cpp index 2d2e59a79..ce70e7bce 100644 --- a/AI/BattleAI/BattleEvaluator.cpp +++ b/AI/BattleAI/BattleEvaluator.cpp @@ -731,7 +731,55 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) ps.value = scoreEvaluator.evaluateExchange(updatedAttack, cachedAttack.turn, *targets, innerCache, state); } - + //! Some units may be dead alltogether. So if they existed before but not now, we know they were killed by the spell + for (const auto& unit : all) + { + if (!unit->isValidTarget()) + continue; + bool isDead = true; + for (const auto& remainingUnit : allUnits) + { + if (remainingUnit->unitId() == unit->unitId()) + isDead = false; + } + if (isDead) + { + auto newHealth = 0; + auto oldHealth = vstd::find_or(healthOfStack, unit->unitId(), 0); + if (oldHealth != newHealth) + { + auto damage = std::abs(oldHealth - newHealth); + auto originalDefender = cb->getBattle(battleID)->battleGetUnitByID(unit->unitId()); + auto dpsReduce = AttackPossibility::calculateDamageReduce( + nullptr, + originalDefender && originalDefender->alive() ? originalDefender : unit, + damage, + innerCache, + state); + auto ourUnit = unit->unitSide() == side ? 1 : -1; + auto goodEffect = newHealth > oldHealth ? 1 : -1; + if (ourUnit * goodEffect == 1) + { + if (ourUnit && goodEffect && (unit->isClone() || unit->isGhost())) + continue; + ps.value += dpsReduce * scoreEvaluator.getPositiveEffectMultiplier(); + } + else + ps.value -= dpsReduce * scoreEvaluator.getNegativeEffectMultiplier(); +#if BATTLE_TRACE_LEVEL >= 1 + logAi->trace( + "Spell %s to %d affects %s (%d), dps: %2f oldHealth: %d newHealth: %d", + ps.spell->getNameTranslated(), + ps.dest.at(0).hexValue.hex, + unit->creatureId().toCreature()->getNameSingularTranslated(), + unit->getCount(), + dpsReduce, + oldHealth, + newHealth); +#endif + } + } + } for(const auto & unit : allUnits) { if(!unit->isValidTarget(true)) From f04e110e2cef83e1c642d51fb33c37a7038010af Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 20 Oct 2024 12:59:08 +0200 Subject: [PATCH 175/186] Update RecruitHeroBehavior.cpp Removed inclusion of no longer existing and also not needed header. --- AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp index aa7d4b23f..7cfab3731 100644 --- a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp +++ b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp @@ -13,7 +13,6 @@ #include "../AIUtility.h" #include "../Goals/RecruitHero.h" #include "../Goals/ExecuteHeroChain.h" -#include "../lib/CHeroHandler.h" namespace NKAI { From 11980e0f97392860617e153e818ba1eb8a2103c9 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 20 Oct 2024 15:49:23 +0200 Subject: [PATCH 176/186] Garrisoning behavior improvement AI will now also garrison a hero as defender if the town to be defended has troops as long as the hero can merge their own troops with the town. AI will no longer just dismiss existing troops in a town if a hero trying to garrison there can merge with it. --- AI/Nullkiller/Behaviors/DefenceBehavior.cpp | 1 + AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp index 158fd8989..95f1d6980 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp @@ -270,6 +270,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta // dismiss creatures we are not able to pick to be able to hide in garrison if(town->garrisonHero || town->getUpperArmy()->stacksCount() == 0 + || path.targetHero->canBeMergedWith(*town) || (town->getUpperArmy()->getArmyStrength() < 500 && town->fortLevel() >= CGTownInstance::CITADEL)) { tasks.push_back( diff --git a/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp b/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp index e03901910..5e7f8df63 100644 --- a/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp +++ b/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp @@ -90,9 +90,12 @@ void ExchangeSwapTownHeroes::accept(AIGateway * ai) if(!town->garrisonHero) { - while(upperArmy->stacksCount() != 0) + if (!garrisonHero->canBeMergedWith(*town)) { - cb->dismissCreature(upperArmy, upperArmy->Slots().begin()->first); + while (upperArmy->stacksCount() != 0) + { + cb->dismissCreature(upperArmy, upperArmy->Slots().begin()->first); + } } } From 3a8a67407b9e348b29f221ec15cdf6f863edcc2f Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 20 Oct 2024 16:37:03 +0200 Subject: [PATCH 177/186] Update RecruitHeroBehavior.cpp AI's willingness to hire hero now depends more on the availability of treasure again. --- .../Behaviors/RecruitHeroBehavior.cpp | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp index 7cfab3731..16c19ed62 100644 --- a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp +++ b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp @@ -57,6 +57,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const bool haveCapitol = false; ai->dangerHitMap->updateHitMap(); + int treasureSourcesCount = 0; for(auto town : towns) { @@ -77,6 +78,22 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const if(ai->heroManager->canRecruitHero(town)) { auto availableHeroes = ai->cb->getAvailableHeroes(town); + + for (auto obj : ai->objectClusterizer->getNearbyObjects()) + { + if ((obj->ID == Obj::RESOURCE) + || obj->ID == Obj::TREASURE_CHEST + || obj->ID == Obj::CAMPFIRE + || isWeeklyRevisitable(ai, obj) + || obj->ID == Obj::ARTIFACT) + { + auto tile = obj->visitablePos(); + auto closestTown = ai->dangerHitMap->getClosestTown(tile); + + if (town == closestTown) + treasureSourcesCount++; + } + } for(auto hero : availableHeroes) { @@ -105,7 +122,8 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const } if (bestHeroToHire && bestTownToHireFrom) { - if (ai->cb->getHeroesInfo().size() < ai->cb->getTownsInfo().size() + 1 + if (ai->cb->getHeroesInfo().size() == 0 + || treasureSourcesCount > ai->cb->getHeroesInfo().size() * 5 || (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh() && haveCapitol) || (ai->getFreeResources()[EGameResID::GOLD] > 30000 && !ai->buildAnalyzer->isGoldPressureHigh())) { From d93c6211da20a88e9d38edd2edf963fe64480ab3 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Sun, 20 Oct 2024 23:32:39 +0200 Subject: [PATCH 178/186] Road exploration The non-cheating-AI on 100% and below is now smarter about exploration and will explore alongside roads with higher priority. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 18 +++++++++++++++++- AI/Nullkiller/Engine/PriorityEvaluator.h | 1 + 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 1e616b875..420686f46 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -15,6 +15,8 @@ #include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h" #include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h" #include "../../../lib/mapObjects/MapObjects.h" +#include "../../../lib/mapping/CMapDefines.h" +#include "../../../lib/RoadHandler.h" #include "../../../lib/CCreatureHandler.h" #include "../../../lib/VCMI_Lib.h" #include "../../../lib/StartInfo.h" @@ -901,6 +903,8 @@ public: break; } } + if(evaluationContext.evaluator.ai->cb->getTile(task->tile)->roadType && evaluationContext.evaluator.ai->cb->getTile(task->tile)->roadType->getId() != RoadId::NO_ROAD) + evaluationContext.explorePriority = 1; if (evaluationContext.explorePriority == 0) evaluationContext.explorePriority = 3; } @@ -1408,7 +1412,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) return 0; if (arriveNextWeek && evaluationContext.isEnemy) return 0; - if (evaluationContext.conquestValue > 0 || evaluationContext.explorePriority == 1) + if (evaluationContext.conquestValue > 0) score = 1000; if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty())) return 0; @@ -1419,6 +1423,18 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) score /= evaluationContext.movementCost; break; } + case PriorityTier::HIGH_PRIO_EXPLORE: + { + if (evaluationContext.enemyHeroDangerRatio > 1) + return 0; + if (evaluationContext.explorePriority != 1) + return 0; + score = 1000; + score *= evaluationContext.closestWayRatio; + if (evaluationContext.movementCost > 0) + score /= evaluationContext.movementCost; + break; + } case PriorityTier::HUNTER_GATHER: //Collect guarded stuff { if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index d912130b7..2e757c819 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -114,6 +114,7 @@ public: INSTAKILL, INSTADEFEND, KILL, + HIGH_PRIO_EXPLORE, HUNTER_GATHER, LOW_PRIO_EXPLORE, DEFEND From 76f5d925e61310629d0a56af17a8854c8cce0f58 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 21 Oct 2024 08:59:18 +0200 Subject: [PATCH 179/186] Update PriorityEvaluator.cpp The army loss will no longer affect the score for defensive decisions. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 420686f46..919138d7f 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -1401,8 +1401,8 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) { if (evaluationContext.isDefend && evaluationContext.threatTurns == 0 && evaluationContext.turn == 0) score = evaluationContext.armyInvolvement; - if (evaluationContext.isEnemy) - score *= (maxWillingToLose - evaluationContext.armyLossPersentage); + if (evaluationContext.isEnemy && maxWillingToLose - evaluationContext.armyLossPersentage < 0) + return 0; score *= evaluationContext.closestWayRatio; break; } From 60084243af0fab78509d3febe70ee93c683dab53 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 21 Oct 2024 09:21:00 +0200 Subject: [PATCH 180/186] Update PriorityEvaluator.cpp Fixed losing heroes while exploring. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 919138d7f..ce2f94570 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -1429,6 +1429,8 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) return 0; if (evaluationContext.explorePriority != 1) return 0; + if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) + return 0; score = 1000; score *= evaluationContext.closestWayRatio; if (evaluationContext.movementCost > 0) @@ -1473,6 +1475,8 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) return 0; if (evaluationContext.explorePriority != 3) return 0; + if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) + return 0; score = 1000; score *= evaluationContext.closestWayRatio; if (evaluationContext.movementCost > 0) From 2b994147935cc23dafab1c34d0306ead837e70da Mon Sep 17 00:00:00 2001 From: Xilmi Date: Mon, 21 Oct 2024 09:37:44 +0200 Subject: [PATCH 181/186] Using hero's stats instead of level to determine their role. Since there are custom maps/campaigns in which heroes have pretty high base-stats even at level 1. --- AI/Nullkiller/Analyzers/HeroManager.cpp | 2 +- lib/mapObjects/CGHeroInstance.cpp | 6 ++++++ lib/mapObjects/CGHeroInstance.h | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/AI/Nullkiller/Analyzers/HeroManager.cpp b/AI/Nullkiller/Analyzers/HeroManager.cpp index e18961663..2d5166bc3 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -95,7 +95,7 @@ float HeroManager::evaluateSpeciality(const CGHeroInstance * hero) const float HeroManager::evaluateFightingStrength(const CGHeroInstance * hero) const { - return evaluateSpeciality(hero) + wariorSkillsScores.evaluateSecSkills(hero) + hero->level * 1.5f; + return evaluateSpeciality(hero) + wariorSkillsScores.evaluateSecSkills(hero) + hero->getBasePrimarySkillValue(PrimarySkill::ATTACK) + hero->getBasePrimarySkillValue(PrimarySkill::DEFENSE) + hero->getBasePrimarySkillValue(PrimarySkill::SPELL_POWER) + hero->getBasePrimarySkillValue(PrimarySkill::KNOWLEDGE); } void HeroManager::update() diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index b14e727de..47cc2ce36 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -1898,5 +1898,11 @@ const IOwnableObject * CGHeroInstance::asOwnable() const return this; } +int CGHeroInstance::getBasePrimarySkillValue(PrimarySkill which) const +{ + std::string cachingStr = "type_PRIMARY_SKILL_base_" + std::to_string(static_cast(which)); + auto selector = Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(which)).And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL)); + return valOfBonuses(selector, cachingStr); +} VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CGHeroInstance.h b/lib/mapObjects/CGHeroInstance.h index 960bc89fa..04ca63ac8 100644 --- a/lib/mapObjects/CGHeroInstance.h +++ b/lib/mapObjects/CGHeroInstance.h @@ -229,6 +229,7 @@ public: double getHeroStrengthForCampaign() const; // includes fighting and the for-campaign-version of magic strength ui64 getTotalStrength() const; // includes fighting strength and army strength TExpType calculateXp(TExpType exp) const; //apply learning skill + int getBasePrimarySkillValue(PrimarySkill which) const; //the value of a base-skill without items or temporary bonuses CStackBasicDescriptor calculateNecromancy (const BattleResult &battleResult) const; void showNecromancyDialog(const CStackBasicDescriptor &raisedStack, vstd::RNG & rand) const; From 9d2fc1b1c999a1a734ab91fe23cc0efa89463d17 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 31 Oct 2024 00:41:51 +0100 Subject: [PATCH 182/186] Update DefenceBehavior.cpp Fixed an issue that caused the AI to try buying the same hero in two different towns. --- AI/Nullkiller/Behaviors/DefenceBehavior.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp index 95f1d6980..710492fbc 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp @@ -41,6 +41,9 @@ Goals::TGoalVec DefenceBehavior::decompose(const Nullkiller * ai) const for(auto town : ai->cb->getTownsInfo()) { evaluateDefence(tasks, town, ai); + //Let's do only one defence-task per pass since otherwise it can try to hire the same hero twice + if (!tasks.empty()) + break; } return tasks; From 5979f53a264a0fc545bf4c376ab8e9a3251217ee Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 31 Oct 2024 00:42:33 +0100 Subject: [PATCH 183/186] Upgrade priority New priority to upgrade existing armies to make it less likely to fight the AI with only part of its army. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 14 ++++++++++++++ AI/Nullkiller/Engine/PriorityEvaluator.h | 1 + 2 files changed, 15 insertions(+) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index ce2f94570..b192bfefe 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -1423,6 +1423,20 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier) score /= evaluationContext.movementCost; break; } + case PriorityTier::UPGRADE: + { + if (!evaluationContext.isArmyUpgrade) + return 0; + if (evaluationContext.enemyHeroDangerRatio > 1) + return 0; + if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) + return 0; + score = 1000; + score *= evaluationContext.closestWayRatio; + if (evaluationContext.movementCost > 0) + score /= evaluationContext.movementCost; + break; + } case PriorityTier::HIGH_PRIO_EXPLORE: { if (evaluationContext.enemyHeroDangerRatio > 1) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index 2e757c819..ee983e43b 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -114,6 +114,7 @@ public: INSTAKILL, INSTADEFEND, KILL, + UPGRADE, HIGH_PRIO_EXPLORE, HUNTER_GATHER, LOW_PRIO_EXPLORE, From 2786797a4eddff3a5245cb013d927ab33c86f4b0 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Thu, 7 Nov 2024 17:37:18 +0100 Subject: [PATCH 184/186] Fixed incompatibility with latest merge Incompatibility fix --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 2 +- lib/CCreatureSet.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index c740ecef1..a71d236db 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -903,7 +903,7 @@ public: break; } } - if(evaluationContext.evaluator.ai->cb->getTile(task->tile)->roadType && evaluationContext.evaluator.ai->cb->getTile(task->tile)->roadType->getId() != RoadId::NO_ROAD) + if(evaluationContext.evaluator.ai->cb->getTile(task->tile)->roadType != RoadId::NO_ROAD) evaluationContext.explorePriority = 1; if (evaluationContext.explorePriority == 0) evaluationContext.explorePriority = 3; diff --git a/lib/CCreatureSet.cpp b/lib/CCreatureSet.cpp index 3316e9faf..590002df9 100644 --- a/lib/CCreatureSet.cpp +++ b/lib/CCreatureSet.cpp @@ -863,8 +863,8 @@ ui64 CStackInstance::getPower() const ui64 CStackInstance::getMarketValue() const { - assert(type); - return type->getFullRecruitCost().marketValue() * count; + assert(getType()); + return getType()->getFullRecruitCost().marketValue() * count; } ArtBearer::ArtBearer CStackInstance::bearerType() const From cd65e69c911f882cb60900cab9a4acadf9490901 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Tue, 26 Nov 2024 15:15:08 +0100 Subject: [PATCH 185/186] Update GatherArmyBehavior.cpp Fixed an issue that made AI consider unpurchased troops as reinforcements for their armies which caused AI to visit cities over and over without actually doing anything there. Buying units is handled by BuyArmyBehavior. --- AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp index 14ad346f1..ceabfd0cf 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp @@ -304,14 +304,6 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT armyToGetOrBuy.upgradeValue -= path.heroArmy->getArmyStrength(); - armyToGetOrBuy.addArmyToBuy( - ai->armyManager->toSlotInfo( - ai->armyManager->getArmyAvailableToBuy( - path.heroArmy, - upgrader, - ai->getFreeResources(), - path.turn()))); - upgrade.upgradeValue += armyToGetOrBuy.upgradeValue; upgrade.upgradeCost += armyToGetOrBuy.upgradeCost; vstd::concatenate(upgrade.resultingArmy, armyToGetOrBuy.resultingArmy); @@ -323,8 +315,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT { for(auto hero : cb->getAvailableHeroes(upgrader)) { - auto scoutReinforcement = ai->armyManager->howManyReinforcementsCanBuy(hero, upgrader) - + ai->armyManager->howManyReinforcementsCanGet(hero, upgrader); + auto scoutReinforcement = ai->armyManager->howManyReinforcementsCanGet(hero, upgrader); if(scoutReinforcement >= armyToGetOrBuy.upgradeValue && ai->getFreeGold() >20000 From 84571f1ae895fc1eb29b3c8513829656fab5cbf3 Mon Sep 17 00:00:00 2001 From: Xilmi Date: Tue, 26 Nov 2024 15:39:35 +0100 Subject: [PATCH 186/186] Update PriorityEvaluator.cpp Just killing stuff even if there is no apparent reason now also is considered for the mere purpose of gaining XP. This also helps the non-cheating AI to keep attacking enemies when they can't see anything worth exploring behind them. --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index a71d236db..eb42a0c1f 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -1052,6 +1052,8 @@ public: evaluationContext.isEnemy = true; evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army); evaluationContext.armyInvolvement += army->getArmyCost(); + if(evaluationContext.danger > 0) + evaluationContext.skillReward += (float)evaluationContext.danger / (float)hero->getArmyStrength(); } vstd::amax(evaluationContext.armyLossPersentage, (float)path.getTotalArmyLoss() / (float)army->getArmyStrength());