From 35429eab52ce095ad9ae7238fe54e9cd84ca4564 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 25 Feb 2024 12:39:19 +0200 Subject: [PATCH] NKAI: moddable configuration --- AI/Nullkiller/AIGateway.cpp | 4 +- AI/Nullkiller/AIUtility.cpp | 2 +- AI/Nullkiller/Analyzers/BuildAnalyzer.cpp | 5 ++ AI/Nullkiller/Analyzers/BuildAnalyzer.h | 1 + AI/Nullkiller/Analyzers/HeroManager.cpp | 1 + AI/Nullkiller/Behaviors/BuildingBehavior.cpp | 4 +- AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp | 3 +- .../Behaviors/GatherArmyBehavior.cpp | 6 +- .../Behaviors/RecruitHeroBehavior.cpp | 3 +- AI/Nullkiller/CMakeLists.txt | 2 + AI/Nullkiller/Engine/Nullkiller.cpp | 22 +++--- AI/Nullkiller/Engine/Nullkiller.h | 3 +- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 2 +- AI/Nullkiller/Engine/Settings.cpp | 78 +++++++++++++++++++ AI/Nullkiller/Engine/Settings.h | 42 ++++++++++ AI/Nullkiller/Pathfinding/AINodeStorage.h | 3 - config/ai/nkai/nkai-settings.json | 7 ++ config/ai/{ => nkai}/object-priorities.txt | 0 18 files changed, 158 insertions(+), 30 deletions(-) create mode 100644 AI/Nullkiller/Engine/Settings.cpp create mode 100644 AI/Nullkiller/Engine/Settings.h create mode 100644 config/ai/nkai/nkai-settings.json rename config/ai/{ => nkai}/object-priorities.txt (100%) diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 0015ed995..9ad7c8421 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -677,9 +677,9 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vectorheroManager->getHeroRole(hero) != HeroRole::MAIN - || nullkiller->buildAnalyzer->getGoldPreasure() > MAX_GOLD_PEASURE)) + || nullkiller->buildAnalyzer->isGoldPreasureHigh())) { - sel = 1; // for now lets pick gold from a chest. + sel = 1; } } diff --git a/AI/Nullkiller/AIUtility.cpp b/AI/Nullkiller/AIUtility.cpp index ace41d24c..1fa07a404 100644 --- a/AI/Nullkiller/AIUtility.cpp +++ b/AI/Nullkiller/AIUtility.cpp @@ -437,7 +437,7 @@ bool shouldVisit(const Nullkiller * ai, const CGHeroInstance * h, const CGObject case Obj::MAGIC_WELL: return h->mana < h->manaLimit(); case Obj::PRISON: - return ai->cb->getHeroesInfo().size() < VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP); + return !ai->heroManager->heroCapReached(); case Obj::TAVERN: case Obj::EYE_OF_MAGI: case Obj::BOAT: diff --git a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp index 8a6edc8c7..74e5a3d70 100644 --- a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp @@ -120,6 +120,11 @@ TResources BuildAnalyzer::getTotalResourcesRequired() const return result; } +bool BuildAnalyzer::isGoldPreasureHigh() const +{ + return goldPreasure > ai->settings->getMaxGoldPreasure(); +} + void BuildAnalyzer::update() { logAi->trace("Start analysing build"); diff --git a/AI/Nullkiller/Analyzers/BuildAnalyzer.h b/AI/Nullkiller/Analyzers/BuildAnalyzer.h index 43049b295..754225070 100644 --- a/AI/Nullkiller/Analyzers/BuildAnalyzer.h +++ b/AI/Nullkiller/Analyzers/BuildAnalyzer.h @@ -96,6 +96,7 @@ public: const std::vector & getDevelopmentInfo() const { return developmentInfos; } TResources getDailyIncome() const { return dailyIncome; } float getGoldPreasure() const { return goldPreasure; } + bool isGoldPreasureHigh() const; bool hasAnyBuilding(int32_t alignment, BuildingID bid) const; private: diff --git a/AI/Nullkiller/Analyzers/HeroManager.cpp b/AI/Nullkiller/Analyzers/HeroManager.cpp index 42a3ae29f..fdb0f72ae 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -187,6 +187,7 @@ bool HeroManager::heroCapReached() const int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned); return heroCount >= ALLOWED_ROAMING_HEROES + || heroCount >= ai->settings->getMaxRoamingHeroes() || heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP) || heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP); } diff --git a/AI/Nullkiller/Behaviors/BuildingBehavior.cpp b/AI/Nullkiller/Behaviors/BuildingBehavior.cpp index d21b92965..8cf713954 100644 --- a/AI/Nullkiller/Behaviors/BuildingBehavior.cpp +++ b/AI/Nullkiller/Behaviors/BuildingBehavior.cpp @@ -47,13 +47,13 @@ Goals::TGoalVec BuildingBehavior::decompose() const totalDevelopmentCost.toString()); auto & developmentInfos = ai->nullkiller->buildAnalyzer->getDevelopmentInfo(); - auto goldPreasure = ai->nullkiller->buildAnalyzer->getGoldPreasure(); + auto isGoldPreasureLow = !ai->nullkiller->buildAnalyzer->isGoldPreasureHigh(); for(auto & developmentInfo : developmentInfos) { for(auto & buildingInfo : developmentInfo.toBuild) { - if(goldPreasure < MAX_GOLD_PEASURE || buildingInfo.dailyIncome[EGameResID::GOLD] > 0) + if(isGoldPreasureLow || buildingInfo.dailyIncome[EGameResID::GOLD] > 0) { if(buildingInfo.notEnoughRes) { diff --git a/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp b/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp index b5260ac3a..3e8251dfd 100644 --- a/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp @@ -46,8 +46,7 @@ Goals::TGoalVec BuyArmyBehavior::decompose() const for(const CGHeroInstance * targetHero : heroes) { - if(ai->nullkiller->buildAnalyzer->getGoldPreasure() > MAX_GOLD_PEASURE - && !town->hasBuilt(BuildingID::CITY_HALL)) + if(ai->nullkiller->buildAnalyzer->isGoldPreasureHigh() && !town->hasBuilt(BuildingID::CITY_HALL)) { continue; } diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp index 935e782f5..de1fab7f4 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp @@ -246,7 +246,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) { auto heroRole = ai->nullkiller->heroManager->getHeroRole(path.targetHero); - if(heroRole == HeroRole::MAIN && path.turn() < SCOUT_TURN_DISTANCE_LIMIT) + if(heroRole == HeroRole::MAIN && path.turn() < ai->nullkiller->settings->getScoutHeroTurnDistanceLimit()) hasMainAround = true; } @@ -335,7 +335,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) if(!upgrade.upgradeValue && armyToGetOrBuy.upgradeValue > 20000 && ai->nullkiller->heroManager->canRecruitHero(town) - && path.turn() < SCOUT_TURN_DISTANCE_LIMIT) + && path.turn() < ai->nullkiller->settings->getScoutHeroTurnDistanceLimit()) { for(auto hero : cb->getAvailableHeroes(town)) { @@ -344,7 +344,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) if(scoutReinforcement >= armyToGetOrBuy.upgradeValue && ai->nullkiller->getFreeGold() >20000 - && ai->nullkiller->buildAnalyzer->getGoldPreasure() < MAX_GOLD_PEASURE) + && !ai->nullkiller->buildAnalyzer->isGoldPreasureHigh()) { Composition recruitHero; diff --git a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp index 885cc7af2..91c384ff1 100644 --- a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp +++ b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp @@ -85,8 +85,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose() const continue; if(cb->getHeroesInfo().size() < cb->getTownsInfo().size() + 1 - || (ai->nullkiller->getFreeResources()[EGameResID::GOLD] > 10000 - && ai->nullkiller->buildAnalyzer->getGoldPreasure() < MAX_GOLD_PEASURE)) + || (ai->nullkiller->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->nullkiller->buildAnalyzer->isGoldPreasureHigh())) { tasks.push_back(Goals::sptr(Goals::RecruitHero(town).setpriority(3))); } diff --git a/AI/Nullkiller/CMakeLists.txt b/AI/Nullkiller/CMakeLists.txt index 8ea5f777a..c84fa0fd6 100644 --- a/AI/Nullkiller/CMakeLists.txt +++ b/AI/Nullkiller/CMakeLists.txt @@ -17,6 +17,7 @@ set(Nullkiller_SRCS AIUtility.cpp Analyzers/ArmyManager.cpp Analyzers/HeroManager.cpp + Engine/Settings.cpp Engine/FuzzyEngines.cpp Engine/FuzzyHelper.cpp Engine/AIMemory.cpp @@ -80,6 +81,7 @@ set(Nullkiller_HEADERS AIUtility.h Analyzers/ArmyManager.h Analyzers/HeroManager.h + Engine/Settings.h Engine/FuzzyEngines.h Engine/FuzzyHelper.h Engine/AIMemory.h diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index de5222570..f9620e011 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -27,15 +27,11 @@ namespace NKAI using namespace Goals; -#if NKAI_TRACE_LEVEL >= 1 -#define MAXPASS 1000000 -#else -#define MAXPASS 30 -#endif - Nullkiller::Nullkiller() + :activeHero(nullptr), scanDepth(ScanDepth::MAIN_FULL), useHeroChain(true) { - memory.reset(new AIMemory()); + memory = std::make_unique(); + settings = std::make_unique(); } void Nullkiller::init(std::shared_ptr cb, PlayerColor playerID) @@ -166,12 +162,12 @@ void Nullkiller::updateAiState(int pass, bool fast) if(scanDepth == ScanDepth::SMALL) { - cfg.mainTurnDistanceLimit = MAIN_TURN_DISTANCE_LIMIT; + cfg.mainTurnDistanceLimit = ai->nullkiller->settings->getMainHeroTurnDistanceLimit(); } if(scanDepth != ScanDepth::ALL_FULL) { - cfg.scoutTurnDistanceLimit = SCOUT_TURN_DISTANCE_LIMIT; + cfg.scoutTurnDistanceLimit = ai->nullkiller->settings->getScoutHeroTurnDistanceLimit(); } boost::this_thread::interruption_point(); @@ -235,13 +231,13 @@ void Nullkiller::makeTurn() resetAiState(); - for(int i = 1; i <= MAXPASS; i++) + for(int i = 1; i <= settings->getMaxPass(); i++) { updateAiState(i); Goals::TTask bestTask = taskptr(Goals::Invalid()); - for(;i <= MAXPASS; i++) + for(;i <= settings->getMaxPass(); i++) { Goals::TTaskVec fastTasks = { choseBestTask(sptr(BuyArmyBehavior()), 1), @@ -328,9 +324,9 @@ void Nullkiller::makeTurn() executeTask(bestTask); - if(i == MAXPASS) + if(i == settings->getMaxPass()) { - logAi->error("Goal %s exceeded maxpass. Terminating AI turn.", taskDescription); + logAi->warn("Goal %s exceeded maxpass. Terminating AI turn.", taskDescription); } } } diff --git a/AI/Nullkiller/Engine/Nullkiller.h b/AI/Nullkiller/Engine/Nullkiller.h index b75465c73..5f1ccebb8 100644 --- a/AI/Nullkiller/Engine/Nullkiller.h +++ b/AI/Nullkiller/Engine/Nullkiller.h @@ -11,6 +11,7 @@ #include "PriorityEvaluator.h" #include "FuzzyHelper.h" +#include "Settings.h" #include "AIMemory.h" #include "DeepDecomposer.h" #include "../Analyzers/DangerHitMapAnalyzer.h" @@ -23,7 +24,6 @@ namespace NKAI { -const float MAX_GOLD_PEASURE = 0.3f; const float MIN_PRIORITY = 0.01f; const float SMALL_SCAN_MIN_PRIORITY = 0.4f; @@ -71,6 +71,7 @@ public: std::unique_ptr dangerEvaluator; std::unique_ptr decomposer; std::unique_ptr armyFormation; + std::unique_ptr settings; PlayerColor playerID; std::shared_ptr cb; std::mutex aiStateMutex; diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 3540162c3..f9db14725 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -69,7 +69,7 @@ PriorityEvaluator::~PriorityEvaluator() void PriorityEvaluator::initVisitTile() { - auto file = CResourceHandler::get()->load(ResourcePath("config/ai/object-priorities.txt"))->readAll(); + auto file = CResourceHandler::get()->load(ResourcePath("config/ai/nkai/object-priorities.txt"))->readAll(); std::string str = std::string((char *)file.first.get(), file.second); engine = fl::FllImporter().fromString(str); armyLossPersentageVariable = engine->getInputVariable("armyLoss"); diff --git a/AI/Nullkiller/Engine/Settings.cpp b/AI/Nullkiller/Engine/Settings.cpp new file mode 100644 index 000000000..d004db24c --- /dev/null +++ b/AI/Nullkiller/Engine/Settings.cpp @@ -0,0 +1,78 @@ +/* +* Settings.cpp, part of VCMI engine +* +* Authors: listed in file AUTHORS in main folder +* +* License: GNU General Public License v2.0 or later +* Full text of license available in license.txt file, in main folder +* +*/ +#include "StdInc.h" +#include + +#include "Settings.h" +#include "../../../lib/mapObjectConstructors/AObjectTypeHandler.h" +#include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h" +#include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h" +#include "../../../lib/mapObjects/MapObjects.h" +#include "../../../lib/modding/CModHandler.h" +#include "../../../lib/VCMI_Lib.h" +#include "../../../lib/filesystem/Filesystem.h" +#include "../../../lib/json/JsonNode.h" + +namespace NKAI +{ + Settings::Settings() + : maxRoamingHeroes(8), + mainHeroTurnDistanceLimit(10), + scoutHeroTurnDistanceLimit(5), + maxGoldPreasure(0.3f), + maxpass(30) + { + ResourcePath resource("config/ai/nkai/nkai-settings", EResType::JSON); + + loadFromMod("core", resource); + + for(const auto & modName : VLC->modh->getActiveMods()) + { + if(CResourceHandler::get(modName)->existsResource(resource)) + loadFromMod(modName, resource); + } + } + + void Settings::loadFromMod(const std::string & modName, const ResourcePath & resource) + { + if(!CResourceHandler::get(modName)->existsResource(resource)) + { + logGlobal->error("Failed to load font %s from mod %s", resource.getName(), modName); + return; + } + + JsonNode node(JsonPath::fromResource(resource), modName); + + if(node.Struct()["maxRoamingHeroes"].isNumber()) + { + maxRoamingHeroes = node.Struct()["maxRoamingHeroes"].Integer(); + } + + if(node.Struct()["mainHeroTurnDistanceLimit"].isNumber()) + { + mainHeroTurnDistanceLimit = node.Struct()["mainHeroTurnDistanceLimit"].Integer(); + } + + if(node.Struct()["scoutHeroTurnDistanceLimit"].isNumber()) + { + scoutHeroTurnDistanceLimit = node.Struct()["scoutHeroTurnDistanceLimit"].Integer(); + } + + if(node.Struct()["maxpass"].isNumber()) + { + maxpass = node.Struct()["maxpass"].Integer(); + } + + if(node.Struct()["maxGoldPreasure"].isNumber()) + { + maxGoldPreasure = node.Struct()["maxGoldPreasure"].Float(); + } + } +} \ No newline at end of file diff --git a/AI/Nullkiller/Engine/Settings.h b/AI/Nullkiller/Engine/Settings.h new file mode 100644 index 000000000..1be6aac19 --- /dev/null +++ b/AI/Nullkiller/Engine/Settings.h @@ -0,0 +1,42 @@ +/* +* Settings.h, part of VCMI engine +* +* Authors: listed in file AUTHORS in main folder +* +* License: GNU General Public License v2.0 or later +* Full text of license available in license.txt file, in main folder +* +*/ +#pragma once + +VCMI_LIB_NAMESPACE_BEGIN + +class JsonNode; +class ResourcePath; + +VCMI_LIB_NAMESPACE_END + +namespace NKAI +{ + class Settings + { + private: + int maxRoamingHeroes; + int mainHeroTurnDistanceLimit; + int scoutHeroTurnDistanceLimit; + int maxpass; + float maxGoldPreasure; + + public: + Settings(); + + int getMaxPass() const { return maxpass; } + float getMaxGoldPreasure() const { return maxGoldPreasure; } + int getMaxRoamingHeroes() const { return maxRoamingHeroes; } + int getMainHeroTurnDistanceLimit() const { return mainHeroTurnDistanceLimit; } + int getScoutHeroTurnDistanceLimit() const { return scoutHeroTurnDistanceLimit; } + + private: + void loadFromMod(const std::string & modName, const ResourcePath & resource); + }; +} \ No newline at end of file diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.h b/AI/Nullkiller/Pathfinding/AINodeStorage.h index c986f080f..08117a5ac 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.h +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.h @@ -24,9 +24,6 @@ namespace NKAI { - const int SCOUT_TURN_DISTANCE_LIMIT = 5; - const int MAIN_TURN_DISTANCE_LIMIT = 10; - namespace AIPathfinding { #ifdef ENVIRONMENT64 diff --git a/config/ai/nkai/nkai-settings.json b/config/ai/nkai/nkai-settings.json new file mode 100644 index 000000000..a77362420 --- /dev/null +++ b/config/ai/nkai/nkai-settings.json @@ -0,0 +1,7 @@ +{ + "maxRoamingHeroes" : 8, + "maxpass" : 30, + "mainHeroTurnDistanceLimit" : 10, + "scoutHeroTurnDistanceLimit" : 5, + "maxGoldPreasure" : 0.3 +} \ No newline at end of file diff --git a/config/ai/object-priorities.txt b/config/ai/nkai/object-priorities.txt similarity index 100% rename from config/ai/object-priorities.txt rename to config/ai/nkai/object-priorities.txt